feat: Implement comprehensive three-party supply order workflow system

- Added logistics partner selection as mandatory requirement for fulfillment supply orders
- Implemented complete status workflow: PENDING → SUPPLIER_APPROVED → LOGISTICS_CONFIRMED → SHIPPED → DELIVERED
- Created dedicated interfaces for all three parties:
  * Fulfillment: Create orders with mandatory logistics selection and receive shipments
  * Suppliers: View, approve/reject orders, and ship approved orders via /supplies tab
  * Logistics: Confirm/reject transport requests via new /logistics-orders dashboard
- Updated Prisma schema with logisticsPartnerId (non-nullable) and new SupplyOrderStatus enum
- Added comprehensive GraphQL mutations for each party's workflow actions
- Fixed GraphQL resolver to include logistics partners in supplyOrders query
- Enhanced UI components with proper status badges and action buttons
- Added backward compatibility for legacy status handling
- Updated sidebar navigation routing for LOGIST organization type

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Bivekich
2025-07-31 12:19:19 +03:00
parent 4147d85b36
commit 772e135ad1
13 changed files with 2589 additions and 74 deletions

View File

@ -75,7 +75,21 @@ export function FulfillmentSuppliesTab({
</TabsContent>
<TabsContent value="supplies" className="mt-0 flex-1 overflow-hidden">
{isWholesale ? <RealSupplyOrdersTab /> : <SellerSupplyOrdersTab />}
{/* ВРЕМЕННО: Заменяем старый компонент на информационное сообщение */}
{isWholesale ? (
<div className="flex items-center justify-center h-64">
<div className="text-center">
<h3 className="text-lg font-semibold text-white mb-2">
Используйте новый интерфейс
</h3>
<p className="text-white/60">
Переходите в раздел "Входящие поставки" "Расходники фулфилмента"
</p>
</div>
</div>
) : (
<SellerSupplyOrdersTab />
)}
</TabsContent>
</Tabs>
</div>

View File

@ -9,7 +9,7 @@ import { Input } from "@/components/ui/input";
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
import { toast } from "sonner";
import { GET_SUPPLY_ORDERS } from "@/graphql/queries";
import { UPDATE_SUPPLY_ORDER_STATUS } from "@/graphql/mutations";
import { UPDATE_SUPPLY_ORDER_STATUS, SUPPLIER_APPROVE_ORDER, SUPPLIER_REJECT_ORDER, SUPPLIER_SHIP_ORDER } from "@/graphql/mutations";
import { useAuth } from "@/hooks/useAuth";
import {
ChevronRight,
@ -245,6 +245,53 @@ export function RealSupplyOrdersTab() {
}
);
// Мутации для поставщика
const [supplierApproveOrder] = useMutation(SUPPLIER_APPROVE_ORDER, {
refetchQueries: [{ query: GET_SUPPLY_ORDERS }],
awaitRefetchQueries: true,
onCompleted: (data) => {
if (data.supplierApproveOrder.success) {
toast.success(data.supplierApproveOrder.message);
} else {
toast.error(data.supplierApproveOrder.message);
}
},
onError: (error) => {
console.error("Error approving order:", error);
toast.error("Ошибка при одобрении заказа");
},
});
const [supplierRejectOrder] = useMutation(SUPPLIER_REJECT_ORDER, {
refetchQueries: [{ query: GET_SUPPLY_ORDERS }],
onCompleted: (data) => {
if (data.supplierRejectOrder.success) {
toast.success(data.supplierRejectOrder.message);
} else {
toast.error(data.supplierRejectOrder.message);
}
},
onError: (error) => {
console.error("Error rejecting order:", error);
toast.error("Ошибка при отклонении заказа");
},
});
const [supplierShipOrder] = useMutation(SUPPLIER_SHIP_ORDER, {
refetchQueries: [{ query: GET_SUPPLY_ORDERS }],
onCompleted: (data) => {
if (data.supplierShipOrder.success) {
toast.success(data.supplierShipOrder.message);
} else {
toast.error(data.supplierShipOrder.message);
}
},
onError: (error) => {
console.error("Error shipping order:", error);
toast.error("Ошибка при отправке заказа");
},
});
// Получаем ID текущей организации (поставщика)
const currentOrganizationId = user?.organization?.id;
@ -829,7 +876,66 @@ export function RealSupplyOrdersTab() {
<div className="px-3 py-2.5">
<div className="flex items-center space-x-1">
{order.status === "PENDING" && (
{/* Кнопки для поставщика */}
{console.log(`DEBUG: Заказ ${order.id.slice(-8)} - статус: ${order.status}, partnerId: ${order.partner?.id}, currentOrganizationId: ${currentOrganizationId}, показать кнопки: ${order.status === "PENDING" && order.partner?.id === currentOrganizationId}`)}
{order.status === "PENDING" && order.partner?.id === currentOrganizationId && (
<div className="flex items-center space-x-2">
<Button
size="sm"
onClick={(e) => {
e.stopPropagation();
supplierApproveOrder({ variables: { id: order.id } });
}}
disabled={updating}
className="bg-green-500/20 hover:bg-green-500/30 text-green-300 border border-green-500/30 text-xs px-2 py-1 h-6"
>
<CheckCircle className="h-3 w-3 mr-1" />
Одобрить
</Button>
<Button
size="sm"
onClick={(e) => {
e.stopPropagation();
supplierRejectOrder({ variables: { id: order.id } });
}}
disabled={updating}
className="bg-red-500/20 hover:bg-red-500/30 text-red-300 border border-red-500/30 text-xs px-2 py-1 h-6"
>
<XCircle className="h-3 w-3 mr-1" />
Отклонить
</Button>
</div>
)}
{order.status === "SUPPLIER_APPROVED" && (
<div className="text-blue-300 text-xs">
Ожидает подтверждения логистики
</div>
)}
{order.status === "LOGISTICS_CONFIRMED" && order.partner?.id === currentOrganizationId && (
<Button
size="sm"
onClick={(e) => {
e.stopPropagation();
supplierShipOrder({ variables: { id: order.id } });
}}
disabled={updating}
className="bg-orange-500/20 hover:bg-orange-500/30 text-orange-300 border border-orange-500/30 text-xs px-2 py-1 h-6"
>
<Truck className="h-3 w-3 mr-1" />
Отправить
</Button>
)}
{order.status === "SHIPPED" && (
<div className="text-orange-300 text-xs">
В пути - ожидает получения
</div>
)}
{order.status === "DELIVERED" && (
<div className="text-green-300 text-xs">
Получено
</div>
)}
{false && ( // Временно отключаем старую кнопку
<>
<Button
size="sm"