Обновлены модели и компоненты для управления поставками и расходниками. Добавлены новые поля в модели SupplyOrder и соответствующие резолверы для поддержки логистики. Реализованы компоненты уведомлений для отображения статуса логистических заявок и поставок. Оптимизирован интерфейс для улучшения пользовательского опыта, добавлены логи для диагностики запросов. Обновлены GraphQL схемы и мутации для поддержки новых функциональных возможностей.

This commit is contained in:
Veronika Smirnova
2025-08-03 17:04:29 +03:00
parent a33adda9d7
commit 8407ca397c
34 changed files with 5382 additions and 1795 deletions

View File

@ -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">
Дата поставки: