This commit is contained in:
Bivekich
2025-07-30 18:32:52 +03:00
parent 38dcfcef2b
commit 593ae16e1e
10 changed files with 192 additions and 48 deletions

View File

@ -106,6 +106,7 @@ model Organization {
supplyOrders SupplyOrder[] supplyOrders SupplyOrder[]
partnerSupplyOrders SupplyOrder[] @relation("SupplyOrderPartner") partnerSupplyOrders SupplyOrder[] @relation("SupplyOrderPartner")
fulfillmentSupplyOrders SupplyOrder[] @relation("SupplyOrderFulfillmentCenter") fulfillmentSupplyOrders SupplyOrder[] @relation("SupplyOrderFulfillmentCenter")
logisticsSupplyOrders SupplyOrder[] @relation("SupplyOrderLogistics")
wildberriesSupplies WildberriesSupply[] wildberriesSupplies WildberriesSupply[]
supplySuppliers SupplySupplier[] @relation("SupplySuppliers") supplySuppliers SupplySupplier[] @relation("SupplySuppliers")
externalAds ExternalAd[] @relation("ExternalAds") externalAds ExternalAd[] @relation("ExternalAds")
@ -472,6 +473,7 @@ model SupplyOrder {
totalAmount Decimal @db.Decimal(12, 2) totalAmount Decimal @db.Decimal(12, 2)
totalItems Int totalItems Int
fulfillmentCenterId String? fulfillmentCenterId String?
logisticsPartnerId String?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
organizationId String organizationId String
@ -479,6 +481,7 @@ model SupplyOrder {
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
partner Organization @relation("SupplyOrderPartner", fields: [partnerId], references: [id]) partner Organization @relation("SupplyOrderPartner", fields: [partnerId], references: [id])
fulfillmentCenter Organization? @relation("SupplyOrderFulfillmentCenter", fields: [fulfillmentCenterId], references: [id]) fulfillmentCenter Organization? @relation("SupplyOrderFulfillmentCenter", fields: [fulfillmentCenterId], references: [id])
logisticsPartner Organization? @relation("SupplyOrderLogistics", fields: [logisticsPartnerId], references: [id])
@@map("supply_orders") @@map("supply_orders")
} }

View File

@ -83,6 +83,8 @@ export function CreateFulfillmentConsumablesSupplyPage() {
const { user } = useAuth(); const { user } = useAuth();
const [selectedSupplier, setSelectedSupplier] = const [selectedSupplier, setSelectedSupplier] =
useState<FulfillmentConsumableSupplier | null>(null); useState<FulfillmentConsumableSupplier | null>(null);
const [selectedLogistics, setSelectedLogistics] =
useState<FulfillmentConsumableSupplier | null>(null);
const [selectedConsumables, setSelectedConsumables] = useState< const [selectedConsumables, setSelectedConsumables] = useState<
SelectedFulfillmentConsumable[] SelectedFulfillmentConsumable[]
>([]); >([]);
@ -113,6 +115,11 @@ export function CreateFulfillmentConsumablesSupplyPage() {
counterpartiesData?.myCounterparties || [] counterpartiesData?.myCounterparties || []
).filter((org: FulfillmentConsumableSupplier) => org.type === "WHOLESALE"); ).filter((org: FulfillmentConsumableSupplier) => org.type === "WHOLESALE");
// Фильтруем только логистические компании
const logisticsPartners = (
counterpartiesData?.myCounterparties || []
).filter((org: FulfillmentConsumableSupplier) => org.type === "LOGIST");
// Фильтруем поставщиков по поисковому запросу // Фильтруем поставщиков по поисковому запросу
const filteredSuppliers = consumableSuppliers.filter( const filteredSuppliers = consumableSuppliers.filter(
(supplier: FulfillmentConsumableSupplier) => (supplier: FulfillmentConsumableSupplier) =>
@ -258,6 +265,7 @@ export function CreateFulfillmentConsumablesSupplyPage() {
deliveryDate: deliveryDate, deliveryDate: deliveryDate,
// Для фулфилмента указываем себя как получателя (поставка на свой склад) // Для фулфилмента указываем себя как получателя (поставка на свой склад)
fulfillmentCenterId: user?.organization?.id, fulfillmentCenterId: user?.organization?.id,
logisticsPartnerId: selectedLogistics?.id,
items: selectedConsumables.map((consumable) => ({ items: selectedConsumables.map((consumable) => ({
productId: consumable.id, productId: consumable.id,
quantity: consumable.selectedQuantity, quantity: consumable.selectedQuantity,
@ -784,6 +792,42 @@ export function CreateFulfillmentConsumablesSupplyPage() {
required required
/> />
</div> </div>
{/* Выбор логистики */}
<div className="mb-3">
<label className="text-white/60 text-xs mb-1 block">
Логистика (опционально):
</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 rounded-md px-3 py-2 text-white text-sm focus:outline-none focus:ring-1 focus:ring-purple-500 focus:border-transparent 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 || partner.inn}
</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="flex items-center justify-between mb-3"> <div className="flex items-center justify-between mb-3">
<span className="text-white font-semibold text-sm"> <span className="text-white font-semibold text-sm">
Итого: Итого:

View File

@ -16,8 +16,8 @@ import {
import { DeliveryDetailsProps } from "./types"; import { DeliveryDetailsProps } from "./types";
const DELIVERY_STATUS_CONFIG = { const DELIVERY_STATUS_CONFIG = {
delivered: { "in-stock": {
label: "Доставлено", label: "На складе",
color: "bg-green-500/20 text-green-300", color: "bg-green-500/20 text-green-300",
icon: CheckCircle, icon: CheckCircle,
}, },
@ -26,6 +26,22 @@ const DELIVERY_STATUS_CONFIG = {
color: "bg-blue-500/20 text-blue-300", color: "bg-blue-500/20 text-blue-300",
icon: Truck, icon: Truck,
}, },
confirmed: {
label: "Подтверждено",
color: "bg-cyan-500/20 text-cyan-300",
icon: CheckCircle,
},
planned: {
label: "Запланировано",
color: "bg-yellow-500/20 text-yellow-300",
icon: Clock,
},
// Обратная совместимость
delivered: {
label: "Доставлено",
color: "bg-green-500/20 text-green-300",
icon: CheckCircle,
},
pending: { pending: {
label: "Ожидание", label: "Ожидание",
color: "bg-yellow-500/20 text-yellow-300", color: "bg-yellow-500/20 text-yellow-300",

View File

@ -32,6 +32,27 @@ import {
// Статусы расходников с цветами // Статусы расходников с цветами
const STATUS_CONFIG = { const STATUS_CONFIG = {
"in-stock": {
label: "Доступен",
color: "bg-green-500/20 text-green-300",
icon: CheckCircle,
},
"in-transit": {
label: "В пути",
color: "bg-blue-500/20 text-blue-300",
icon: Clock,
},
confirmed: {
label: "Подтверждено",
color: "bg-cyan-500/20 text-cyan-300",
icon: CheckCircle,
},
planned: {
label: "Запланировано",
color: "bg-yellow-500/20 text-yellow-300",
icon: Clock,
},
// Обратная совместимость и специальные статусы
available: { available: {
label: "Доступен", label: "Доступен",
color: "bg-green-500/20 text-green-300", color: "bg-green-500/20 text-green-300",
@ -47,11 +68,6 @@ const STATUS_CONFIG = {
color: "bg-red-500/20 text-red-300", color: "bg-red-500/20 text-red-300",
icon: AlertTriangle, icon: AlertTriangle,
}, },
"in-transit": {
label: "В пути",
color: "bg-blue-500/20 text-blue-300",
icon: Clock,
},
reserved: { reserved: {
label: "Зарезервирован", label: "Зарезервирован",
color: "bg-purple-500/20 text-purple-300", color: "bg-purple-500/20 text-purple-300",

View File

@ -477,6 +477,7 @@ export function FulfillmentWarehouseDashboard() {
supplyOrders, supplyOrders,
allProducts, allProducts,
mySupplies, mySupplies,
myFulfillmentSupplies,
suppliesReceivedToday, suppliesReceivedToday,
suppliesUsedToday, suppliesUsedToday,
productsReceivedToday, productsReceivedToday,

View File

@ -15,7 +15,6 @@ export function SuppliesGrid({
return ( return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
{supplies.map((supply) => { {supplies.map((supply) => {
const statusConfig = getStatusConfig(supply.status);
const isExpanded = expandedSupplies.has(supply.id); const isExpanded = expandedSupplies.has(supply.id);
const deliveries = getSupplyDeliveries(supply); const deliveries = getSupplyDeliveries(supply);
@ -26,7 +25,6 @@ export function SuppliesGrid({
supply={supply} supply={supply}
isExpanded={isExpanded} isExpanded={isExpanded}
onToggleExpansion={onToggleExpansion} onToggleExpansion={onToggleExpansion}
statusConfig={statusConfig}
getSupplyDeliveries={getSupplyDeliveries} getSupplyDeliveries={getSupplyDeliveries}
/> />

View File

@ -18,7 +18,6 @@ export function SupplyCard({
supply, supply,
isExpanded, isExpanded,
onToggleExpansion, onToggleExpansion,
statusConfig,
getSupplyDeliveries, getSupplyDeliveries,
}: SupplyCardProps) { }: SupplyCardProps) {
const formatCurrency = (amount: number) => { const formatCurrency = (amount: number) => {
@ -33,7 +32,6 @@ export function SupplyCard({
return new Intl.NumberFormat("ru-RU").format(num); return new Intl.NumberFormat("ru-RU").format(num);
}; };
const StatusIcon = statusConfig.icon;
const isLowStock = const isLowStock =
supply.currentStock <= supply.minStock && supply.currentStock > 0; supply.currentStock <= supply.minStock && supply.currentStock > 0;
const stockPercentage = const stockPercentage =
@ -58,12 +56,6 @@ export function SupplyCard({
{supply.description} {supply.description}
</p> </p>
</div> </div>
<div className="flex items-center space-x-2 ml-2">
<Badge className={`${statusConfig.color} text-xs`}>
<StatusIcon className="h-3 w-3 mr-1" />
{statusConfig.label}
</Badge>
</div>
</div> </div>
{/* Основная информация */} {/* Основная информация */}

View File

@ -54,7 +54,6 @@ export interface SupplyCardProps {
supply: Supply; supply: Supply;
isExpanded: boolean; isExpanded: boolean;
onToggleExpansion: (id: string) => void; onToggleExpansion: (id: string) => void;
statusConfig: StatusConfig;
getSupplyDeliveries: (supply: Supply) => Supply[]; getSupplyDeliveries: (supply: Supply) => Supply[];
} }

View File

@ -728,33 +728,57 @@ export const resolvers = {
}, },
}); });
// Получаем расходники селлеров из таблицы supply // Получаем ВСЕ расходники из таблицы supply для фулфилмента
// Это расходники, созданные при доставке заказов от селлеров const allSupplies = await prisma.supply.findMany({
const existingSupplies = await prisma.supply.findMany({
where: { organizationId: currentUser.organization.id }, where: { organizationId: currentUser.organization.id },
include: { organization: true }, include: { organization: true },
orderBy: { createdAt: "desc" }, orderBy: { createdAt: "desc" },
}); });
// Получаем все заказы фулфилмента для себя (чтобы исключить их расходники)
const fulfillmentOwnOrders = await prisma.supplyOrder.findMany({
where: {
organizationId: currentUser.organization.id, // Созданы фулфилментом
fulfillmentCenterId: currentUser.organization.id, // Для себя
status: "DELIVERED",
},
include: {
items: {
include: {
product: true,
},
},
},
});
// Создаем набор названий товаров из заказов фулфилмента для себя
const fulfillmentProductNames = new Set(
fulfillmentOwnOrders.flatMap((order) =>
order.items.map((item) => item.product.name)
)
);
// Фильтруем расходники: исключаем те, что созданы заказами фулфилмента для себя
const sellerSupplies = allSupplies.filter((supply) => {
// Если расходник соответствует товару из заказа фулфилмента для себя,
// то это расходник фулфилмента, а не селлера
return !fulfillmentProductNames.has(supply.name);
});
// Логирование для отладки // Логирование для отладки
console.log("🔥🔥🔥 SELLER SUPPLIES RESOLVER CALLED 🔥🔥🔥"); console.log("🔥🔥🔥 SELLER SUPPLIES RESOLVER CALLED 🔥🔥🔥");
console.log("📊 Расходники селлеров:", { console.log("📊 Расходники селлеров:", {
organizationId: currentUser.organization.id, organizationId: currentUser.organization.id,
organizationType: currentUser.organization.type, organizationType: currentUser.organization.type,
existingSuppliesCount: existingSupplies.length, allSuppliesCount: allSupplies.length,
fulfillmentOwnOrdersCount: fulfillmentOwnOrders.length,
fulfillmentProductNames: Array.from(fulfillmentProductNames),
filteredSellerSuppliesCount: sellerSupplies.length,
sellerOrdersCount: sellerSupplyOrders.length, sellerOrdersCount: sellerSupplyOrders.length,
sellerOrders: sellerSupplyOrders.map((o) => ({
id: o.id,
sellerName: o.organization.name,
supplierName: o.partner.name,
status: o.status,
itemsCount: o.items.length,
})),
}); });
// Возвращаем только расходники селлеров из таблицы supply // Возвращаем только расходники селлеров (исключая расходники фулфилмента)
// TODO: В будущем можно добавить фильтрацию по источнику заказа return sellerSupplies;
return existingSupplies;
}, },
// Расходники фулфилмента (материалы для работы фулфилмента) // Расходники фулфилмента (материалы для работы фулфилмента)
@ -818,12 +842,14 @@ export const resolvers = {
category: item.product.category?.name || "Расходники фулфилмента", category: item.product.category?.name || "Расходники фулфилмента",
status: status:
order.status === "PENDING" order.status === "PENDING"
? "in-transit" ? "planned"
: order.status === "CONFIRMED" : order.status === "CONFIRMED"
? "in-transit" ? "confirmed"
: order.status === "IN_TRANSIT" : order.status === "IN_TRANSIT"
? "in-transit" ? "in-transit"
: "available", : order.status === "DELIVERED"
? "in-stock"
: "planned",
date: order.createdAt, date: order.createdAt,
supplier: order.partner.name || order.partner.fullName || "Не указан", supplier: order.partner.name || order.partner.fullName || "Не указан",
minStock: Math.round(item.quantity * 0.1), minStock: Math.round(item.quantity * 0.1),
@ -3751,6 +3777,7 @@ export const resolvers = {
totalItems: totalItems, totalItems: totalItems,
organizationId: currentUser.organization.id, organizationId: currentUser.organization.id,
fulfillmentCenterId: fulfillmentCenterId, fulfillmentCenterId: fulfillmentCenterId,
logisticsPartnerId: args.input.logisticsPartnerId,
status: initialStatus, status: initialStatus,
items: { items: {
create: orderItems, create: orderItems,
@ -3772,6 +3799,11 @@ export const resolvers = {
users: true, users: true,
}, },
}, },
logisticsPartner: {
include: {
users: true,
},
},
items: { items: {
include: { include: {
product: { product: {
@ -3803,7 +3835,7 @@ export const resolvers = {
quantity: item.quantity, quantity: item.quantity,
unit: "шт", unit: "шт",
category: productWithCategory?.category?.name || "Расходники", category: productWithCategory?.category?.name || "Расходники",
status: "in-transit", // Статус "в пути" так как заказ только создан status: "planned", // Статус "запланировано" (ожидает одобрения поставщиком)
date: new Date(args.input.deliveryDate), date: new Date(args.input.deliveryDate),
supplier: partner.name || partner.fullName || "Не указан", supplier: partner.name || partner.fullName || "Не указан",
minStock: Math.round(item.quantity * 0.1), // 10% от заказанного как минимальный остаток minStock: Math.round(item.quantity * 0.1), // 10% от заказанного как минимальный остаток
@ -5288,10 +5320,53 @@ export const resolvers = {
}, },
}); });
// Если статус изменился на DELIVERED, обновляем склад фулфилмента // Обновляем статусы расходников в зависимости от статуса заказа
if (args.status === "DELIVERED" && existingOrder.fulfillmentCenterId) { const targetOrganizationId = existingOrder.fulfillmentCenterId || existingOrder.organizationId;
console.log("🚚 Обновляем склад фулфилмента:", {
if (args.status === "CONFIRMED") {
// При подтверждении поставщиком - переводим расходники в статус "confirmed"
await prisma.supply.updateMany({
where: {
organizationId: targetOrganizationId,
status: "planned",
// Находим расходники по названиям товаров из заказа
name: {
in: existingOrder.items.map(item => item.product.name)
}
},
data: {
status: "confirmed"
}
});
console.log("✅ Статусы расходников обновлены на 'confirmed'");
}
if (args.status === "IN_TRANSIT") {
// При отгрузке - переводим расходники в статус "in-transit"
await prisma.supply.updateMany({
where: {
organizationId: targetOrganizationId,
status: "confirmed",
name: {
in: existingOrder.items.map(item => item.product.name)
}
},
data: {
status: "in-transit"
}
});
console.log("✅ Статусы расходников обновлены на 'in-transit'");
}
// Если статус изменился на DELIVERED, обновляем склад
if (args.status === "DELIVERED") {
console.log("🚚 Обновляем склад организации:", {
targetOrganizationId,
fulfillmentCenterId: existingOrder.fulfillmentCenterId, fulfillmentCenterId: existingOrder.fulfillmentCenterId,
organizationId: existingOrder.organizationId,
itemsCount: existingOrder.items.length, itemsCount: existingOrder.items.length,
items: existingOrder.items.map((item) => ({ items: existingOrder.items.map((item) => ({
productName: item.product.name, productName: item.product.name,
@ -5299,19 +5374,19 @@ export const resolvers = {
})), })),
}); });
// Обновляем расходники фулфилмента // Обновляем расходники
for (const item of existingOrder.items) { for (const item of existingOrder.items) {
console.log("📦 Обрабатываем товар:", { console.log("📦 Обрабатываем товар:", {
productName: item.product.name, productName: item.product.name,
quantity: item.quantity, quantity: item.quantity,
fulfillmentCenterId: existingOrder.fulfillmentCenterId, targetOrganizationId,
}); });
// Ищем существующий расходник // Ищем существующий расходник в правильной организации
const existingSupply = await prisma.supply.findFirst({ const existingSupply = await prisma.supply.findFirst({
where: { where: {
name: item.product.name, name: item.product.name,
organizationId: existingOrder.fulfillmentCenterId, organizationId: targetOrganizationId,
}, },
}); });
@ -5329,14 +5404,14 @@ export const resolvers = {
where: { id: existingSupply.id }, where: { id: existingSupply.id },
data: { data: {
currentStock: existingSupply.currentStock + item.quantity, currentStock: existingSupply.currentStock + item.quantity,
status: "available", // Меняем статус на "доступен" status: "in-stock", // Меняем статус на "на складе"
}, },
}); });
} else { } else {
console.log(" Создаем новый расходник:", { console.log(" Создаем новый расходник:", {
name: item.product.name, name: item.product.name,
quantity: item.quantity, quantity: item.quantity,
organizationId: existingOrder.fulfillmentCenterId, organizationId: targetOrganizationId,
}); });
// Создаем новый расходник // Создаем новый расходник
@ -5350,7 +5425,7 @@ export const resolvers = {
quantity: item.quantity, quantity: item.quantity,
unit: "шт", unit: "шт",
category: item.product.category?.name || "Расходники", category: item.product.category?.name || "Расходники",
status: "available", status: "in-stock",
date: new Date(), date: new Date(),
supplier: supplier:
existingOrder.partner.name || existingOrder.partner.name ||
@ -5358,7 +5433,7 @@ export const resolvers = {
"Не указан", "Не указан",
minStock: Math.round(item.quantity * 0.1), minStock: Math.round(item.quantity * 0.1),
currentStock: item.quantity, currentStock: item.quantity,
organizationId: existingOrder.fulfillmentCenterId, organizationId: targetOrganizationId,
}, },
}); });
@ -5370,7 +5445,7 @@ export const resolvers = {
} }
} }
console.log("🎉 Склад фулфилмента успешно обновлен!"); console.log("🎉 Склад организации успешно обновлен!");
} }
return { return {