Обновлены модели и компоненты для управления поставками и расходниками. Добавлены новые поля в модели SupplyOrder и соответствующие резолверы для поддержки логистики. Реализованы компоненты уведомлений для отображения статуса логистических заявок и поставок. Оптимизирован интерфейс для улучшения пользовательского опыта, добавлены логи для диагностики запросов. Обновлены GraphQL схемы и мутации для поддержки новых функциональных возможностей.
This commit is contained in:
@ -5,6 +5,7 @@ import { useRouter } from "next/navigation";
|
||||
import { useQuery, useMutation } from "@apollo/client";
|
||||
import { Sidebar } from "@/components/dashboard/sidebar";
|
||||
import { useSidebar } from "@/hooks/useSidebar";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
@ -77,6 +78,7 @@ interface SelectedConsumable {
|
||||
|
||||
export function CreateConsumablesSupplyPage() {
|
||||
const router = useRouter();
|
||||
const { user } = useAuth();
|
||||
const { getSidebarMargin } = useSidebar();
|
||||
const [selectedSupplier, setSelectedSupplier] =
|
||||
useState<ConsumableSupplier | null>(null);
|
||||
@ -88,6 +90,8 @@ export function CreateConsumablesSupplyPage() {
|
||||
const [deliveryDate, setDeliveryDate] = useState("");
|
||||
const [selectedFulfillmentCenter, setSelectedFulfillmentCenter] =
|
||||
useState<ConsumableSupplier | null>(null);
|
||||
const [selectedLogistics, setSelectedLogistics] =
|
||||
useState<ConsumableSupplier | null>(null);
|
||||
const [isCreatingSupply, setIsCreatingSupply] = useState(false);
|
||||
|
||||
// Загружаем контрагентов-поставщиков расходников
|
||||
@ -117,6 +121,11 @@ export function CreateConsumablesSupplyPage() {
|
||||
counterpartiesData?.myCounterparties || []
|
||||
).filter((org: ConsumableSupplier) => org.type === "FULFILLMENT");
|
||||
|
||||
// Фильтруем логистические компании
|
||||
const logisticsPartners = (counterpartiesData?.myCounterparties || []).filter(
|
||||
(org: ConsumableSupplier) => org.type === "LOGIST"
|
||||
);
|
||||
|
||||
// Фильтруем поставщиков по поисковому запросу
|
||||
const filteredSuppliers = consumableSuppliers.filter(
|
||||
(supplier: ConsumableSupplier) =>
|
||||
@ -218,19 +227,82 @@ export function CreateConsumablesSupplyPage() {
|
||||
selectedConsumables.length === 0 ||
|
||||
!deliveryDate
|
||||
) {
|
||||
toast.error("Заполните все обязательные поля");
|
||||
toast.error(
|
||||
"Заполните все обязательные поля: поставщик, расходники и дата доставки"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Для селлеров требуется выбор фулфилмент-центра
|
||||
// TODO: Добавить проверку типа текущей организации
|
||||
if (!selectedFulfillmentCenter) {
|
||||
toast.error("Выберите фулфилмент-центр для доставки");
|
||||
return;
|
||||
}
|
||||
|
||||
// Логистика опциональна - может выбрать селлер или оставить фулфилменту
|
||||
if (selectedLogistics && !selectedLogistics.id) {
|
||||
toast.error("Некорректно выбрана логистическая компания");
|
||||
return;
|
||||
}
|
||||
|
||||
// Дополнительные проверки
|
||||
if (!selectedFulfillmentCenter.id) {
|
||||
toast.error("ID фулфилмент-центра не найден");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!selectedSupplier.id) {
|
||||
toast.error("ID поставщика не найден");
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedConsumables.length === 0) {
|
||||
toast.error("Не выбраны расходники");
|
||||
return;
|
||||
}
|
||||
|
||||
// Проверяем дату
|
||||
const deliveryDateObj = new Date(deliveryDate);
|
||||
if (isNaN(deliveryDateObj.getTime())) {
|
||||
toast.error("Некорректная дата поставки");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsCreatingSupply(true);
|
||||
|
||||
// 🔍 ОТЛАДКА: проверяем текущего пользователя
|
||||
console.log("👤 Текущий пользователь:", {
|
||||
userId: user?.id,
|
||||
phone: user?.phone,
|
||||
organizationId: user?.organization?.id,
|
||||
organizationType: user?.organization?.type,
|
||||
organizationName:
|
||||
user?.organization?.name || user?.organization?.fullName,
|
||||
});
|
||||
|
||||
console.log("🚀 Создаем поставку с данными:", {
|
||||
partnerId: selectedSupplier.id,
|
||||
deliveryDate: deliveryDate,
|
||||
fulfillmentCenterId: selectedFulfillmentCenter.id,
|
||||
logisticsPartnerId: selectedLogistics?.id,
|
||||
hasLogistics: !!selectedLogistics?.id,
|
||||
consumableType: "SELLER_CONSUMABLES",
|
||||
itemsCount: selectedConsumables.length,
|
||||
mutationInput: {
|
||||
partnerId: selectedSupplier.id,
|
||||
deliveryDate: deliveryDate,
|
||||
fulfillmentCenterId: selectedFulfillmentCenter.id,
|
||||
...(selectedLogistics?.id
|
||||
? { logisticsPartnerId: selectedLogistics.id }
|
||||
: {}),
|
||||
consumableType: "SELLER_CONSUMABLES",
|
||||
items: selectedConsumables.map((consumable) => ({
|
||||
productId: consumable.id,
|
||||
quantity: consumable.selectedQuantity,
|
||||
})),
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await createSupplyOrder({
|
||||
variables: {
|
||||
@ -238,6 +310,12 @@ export function CreateConsumablesSupplyPage() {
|
||||
partnerId: selectedSupplier.id,
|
||||
deliveryDate: deliveryDate,
|
||||
fulfillmentCenterId: selectedFulfillmentCenter.id,
|
||||
// 🔄 ЛОГИСТИКА ОПЦИОНАЛЬНА: селлер может выбрать или оставить фулфилменту
|
||||
...(selectedLogistics?.id
|
||||
? { logisticsPartnerId: selectedLogistics.id }
|
||||
: {}),
|
||||
// 🏷️ КЛАССИФИКАЦИЯ согласно правилам (раздел 2.2)
|
||||
consumableType: "SELLER_CONSUMABLES", // Расходники селлеров
|
||||
items: selectedConsumables.map((consumable) => ({
|
||||
productId: consumable.id,
|
||||
quantity: consumable.selectedQuantity,
|
||||
@ -270,7 +348,21 @@ export function CreateConsumablesSupplyPage() {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error creating consumables supply:", error);
|
||||
toast.error("Ошибка при создании поставки расходников");
|
||||
|
||||
// Детальная диагностика ошибки
|
||||
if (error instanceof Error) {
|
||||
console.error("Error details:", {
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
name: error.name,
|
||||
});
|
||||
|
||||
// Показываем конкретную ошибку пользователю
|
||||
toast.error(`Ошибка: ${error.message}`);
|
||||
} else {
|
||||
console.error("Unknown error:", error);
|
||||
toast.error("Ошибка при создании поставки расходников");
|
||||
}
|
||||
} finally {
|
||||
setIsCreatingSupply(false);
|
||||
}
|
||||
@ -764,7 +856,7 @@ export function CreateConsumablesSupplyPage() {
|
||||
);
|
||||
setSelectedFulfillmentCenter(center || null);
|
||||
}}
|
||||
className="w-full bg-white/10 border border-white/20 text-white h-8 text-sm rounded px-2 focus:ring-2 focus:ring-purple-400/50 focus:border-purple-400/50"
|
||||
className="w-full bg-white/10 border border-white/20 text-white h-8 text-sm rounded px-2 pr-8 focus:ring-2 focus:ring-purple-400/50 focus:border-purple-400/50 appearance-none"
|
||||
required
|
||||
>
|
||||
<option value="" className="bg-gray-800 text-white">
|
||||
@ -782,8 +874,73 @@ export function CreateConsumablesSupplyPage() {
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<div className="absolute inset-y-0 right-0 flex items-center px-2 pointer-events-none">
|
||||
<svg
|
||||
className="w-4 h-4 text-white/60"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M19 9l-7 7-7-7"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* БЛОК ВЫБОРА ЛОГИСТИЧЕСКОЙ КОМПАНИИ */}
|
||||
<div className="mb-3">
|
||||
<label className="text-white/60 text-xs mb-1 block">
|
||||
Логистическая компания:
|
||||
<span className="text-white/40 ml-1">(опционально)</span>
|
||||
</label>
|
||||
<div className="relative">
|
||||
<select
|
||||
value={selectedLogistics?.id || ""}
|
||||
onChange={(e) => {
|
||||
const logisticsId = e.target.value;
|
||||
const logistics = logisticsPartners.find(
|
||||
(p) => p.id === logisticsId
|
||||
);
|
||||
setSelectedLogistics(logistics || null);
|
||||
}}
|
||||
className="w-full bg-white/10 border border-white/20 text-white h-8 text-sm rounded px-2 pr-8 focus:ring-2 focus:ring-purple-400/50 focus:border-purple-400/50 appearance-none"
|
||||
>
|
||||
<option value="" className="bg-gray-800 text-white">
|
||||
Выберите логистику или оставьте фулфилменту
|
||||
</option>
|
||||
{logisticsPartners.map((partner) => (
|
||||
<option
|
||||
key={partner.id}
|
||||
value={partner.id}
|
||||
className="bg-gray-800 text-white"
|
||||
>
|
||||
{partner.name || partner.fullName || "Логистика"}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<div className="absolute inset-y-0 right-0 flex items-center px-2 pointer-events-none">
|
||||
<svg
|
||||
className="w-4 h-4 text-white/60"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M19 9l-7 7-7-7"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-3">
|
||||
<label className="text-white/60 text-xs mb-1 block">
|
||||
Дата поставки:
|
||||
|
@ -42,7 +42,15 @@ interface SupplyOrderItem {
|
||||
interface SupplyOrder {
|
||||
id: string;
|
||||
deliveryDate: string;
|
||||
status: "PENDING" | "CONFIRMED" | "IN_TRANSIT" | "DELIVERED" | "CANCELLED";
|
||||
status:
|
||||
| "PENDING"
|
||||
| "SUPPLIER_APPROVED"
|
||||
| "CONFIRMED"
|
||||
| "LOGISTICS_CONFIRMED"
|
||||
| "SHIPPED"
|
||||
| "IN_TRANSIT"
|
||||
| "DELIVERED"
|
||||
| "CANCELLED";
|
||||
totalAmount: number;
|
||||
totalItems: number;
|
||||
createdAt: string;
|
||||
@ -102,10 +110,22 @@ export function SellerSupplyOrdersTab() {
|
||||
label: "Ожидает одобрения",
|
||||
color: "bg-blue-500/20 text-blue-300 border-blue-500/30",
|
||||
},
|
||||
SUPPLIER_APPROVED: {
|
||||
label: "Одобрена поставщиком",
|
||||
color: "bg-cyan-500/20 text-cyan-300 border-cyan-500/30",
|
||||
},
|
||||
CONFIRMED: {
|
||||
label: "Одобрена",
|
||||
label: "Подтверждена",
|
||||
color: "bg-green-500/20 text-green-300 border-green-500/30",
|
||||
},
|
||||
LOGISTICS_CONFIRMED: {
|
||||
label: "Готова к отправке",
|
||||
color: "bg-teal-500/20 text-teal-300 border-teal-500/30",
|
||||
},
|
||||
SHIPPED: {
|
||||
label: "Отправлена",
|
||||
color: "bg-orange-500/20 text-orange-300 border-orange-500/30",
|
||||
},
|
||||
IN_TRANSIT: {
|
||||
label: "В пути",
|
||||
color: "bg-yellow-500/20 text-yellow-300 border-yellow-500/30",
|
||||
@ -119,7 +139,16 @@ export function SellerSupplyOrdersTab() {
|
||||
color: "bg-red-500/20 text-red-300 border-red-500/30",
|
||||
},
|
||||
};
|
||||
const { label, color } = statusMap[status];
|
||||
const config = statusMap[status as keyof typeof statusMap];
|
||||
if (!config) {
|
||||
// Fallback для неизвестных статусов
|
||||
return (
|
||||
<Badge className="bg-gray-500/20 text-gray-300 border-gray-500/30 border text-xs">
|
||||
{status}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
const { label, color } = config;
|
||||
return <Badge className={`${color} border text-xs`}>{label}</Badge>;
|
||||
};
|
||||
|
||||
|
@ -42,7 +42,27 @@ export function SuppliesDashboard() {
|
||||
});
|
||||
|
||||
const pendingCount = pendingData?.pendingSuppliesCount;
|
||||
const hasPendingItems = pendingCount && pendingCount.total > 0;
|
||||
// ✅ ПРАВИЛЬНО: Настраиваем уведомления по типам организаций
|
||||
const hasPendingItems = (() => {
|
||||
if (!pendingCount) return false;
|
||||
|
||||
switch (user?.organization?.type) {
|
||||
case "SELLER":
|
||||
// Селлеры не получают уведомления о поставках - только отслеживают статус
|
||||
return false;
|
||||
case "WHOLESALE":
|
||||
// Поставщики видят только входящие заказы, не заявки на партнерство
|
||||
return pendingCount.incomingSupplierOrders > 0;
|
||||
case "FULFILLMENT":
|
||||
// Фулфилмент видит только поставки к обработке, не заявки на партнерство
|
||||
return pendingCount.supplyOrders > 0;
|
||||
case "LOGIST":
|
||||
// Логистика видит только логистические заявки, не заявки на партнерство
|
||||
return pendingCount.logisticsOrders > 0;
|
||||
default:
|
||||
return pendingCount.total > 0;
|
||||
}
|
||||
})();
|
||||
|
||||
// Автоматически открываем нужную вкладку при загрузке
|
||||
useEffect(() => {
|
||||
@ -69,32 +89,33 @@ export function SuppliesDashboard() {
|
||||
<Alert className="mb-4 bg-blue-500/20 border-blue-400/30 text-blue-300 animate-pulse">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
У вас есть {pendingCount.total} элемент
|
||||
{pendingCount.total > 1
|
||||
? pendingCount.total < 5
|
||||
? "а"
|
||||
: "ов"
|
||||
: ""}
|
||||
, требующ{pendingCount.total > 1 ? "их" : "ий"} одобрения:
|
||||
{pendingCount.supplyOrders > 0 &&
|
||||
` ${pendingCount.supplyOrders} заказ${
|
||||
pendingCount.supplyOrders > 1
|
||||
? pendingCount.supplyOrders < 5
|
||||
? "а"
|
||||
: "ов"
|
||||
: ""
|
||||
} поставок`}
|
||||
{pendingCount.incomingRequests > 0 &&
|
||||
pendingCount.supplyOrders > 0 &&
|
||||
", "}
|
||||
{pendingCount.incomingRequests > 0 &&
|
||||
` ${pendingCount.incomingRequests} заявк${
|
||||
pendingCount.incomingRequests > 1
|
||||
? pendingCount.incomingRequests < 5
|
||||
? "и"
|
||||
: ""
|
||||
: "а"
|
||||
} на партнерство`}
|
||||
{(() => {
|
||||
switch (user?.organization?.type) {
|
||||
case "WHOLESALE":
|
||||
const orders = pendingCount.incomingSupplierOrders || 0;
|
||||
return `У вас ${orders} входящ${
|
||||
orders > 1 ? (orders < 5 ? "их" : "их") : "ий"
|
||||
} заказ${
|
||||
orders > 1 ? (orders < 5 ? "а" : "ов") : ""
|
||||
} от клиентов, ожидающ${
|
||||
orders > 1 ? "их" : "ий"
|
||||
} подтверждения`;
|
||||
case "FULFILLMENT":
|
||||
const supplies = pendingCount.supplyOrders || 0;
|
||||
return `У вас ${supplies} поставк${
|
||||
supplies > 1 ? (supplies < 5 ? "и" : "ов") : "а"
|
||||
} к обработке`;
|
||||
case "LOGIST":
|
||||
const logistics = pendingCount.logisticsOrders || 0;
|
||||
return `У вас ${logistics} логистическ${
|
||||
logistics > 1 ? (logistics < 5 ? "их" : "их") : "ая"
|
||||
} заявк${
|
||||
logistics > 1 ? (logistics < 5 ? "и" : "и") : "а"
|
||||
} к подтверждению`;
|
||||
default:
|
||||
return `У вас есть элементы, требующие внимания`;
|
||||
}
|
||||
})()}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
Reference in New Issue
Block a user