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

@ -14,7 +14,7 @@ import {
GET_MY_SUPPLIES,
GET_WAREHOUSE_PRODUCTS,
} from "@/graphql/queries";
import { UPDATE_SUPPLY_ORDER_STATUS } from "@/graphql/mutations";
import { FULFILLMENT_RECEIVE_ORDER } from "@/graphql/mutations";
import { useAuth } from "@/hooks/useAuth";
import { toast } from "sonner";
import {
@ -96,25 +96,38 @@ const formatDate = (dateString: string) => {
const getStatusBadge = (status: string) => {
const statusConfig = {
PENDING: {
label: "Ожидает",
label: "Ожидает одобрения поставщика",
color: "bg-yellow-500/20 text-yellow-300 border-yellow-500/30",
},
CONFIRMED: {
label: "Подтверждён",
SUPPLIER_APPROVED: {
label: "Ожидает подтверждения логистики",
color: "bg-blue-500/20 text-blue-300 border-blue-500/30",
},
IN_TRANSIT: {
LOGISTICS_CONFIRMED: {
label: "Ожидает отправки поставщиком",
color: "bg-cyan-500/20 text-cyan-300 border-cyan-500/30",
},
SHIPPED: {
label: "В пути",
color: "bg-orange-500/20 text-orange-300 border-orange-500/30",
},
DELIVERED: {
label: "Доставлен",
label: "Доставлено",
color: "bg-green-500/20 text-green-300 border-green-500/30",
},
CANCELLED: {
label: "Отменён",
label: "Отменено",
color: "bg-red-500/20 text-red-300 border-red-500/30",
},
// Устаревшие статусы для обратной совместимости
CONFIRMED: {
label: "Подтверждён (устаревший)",
color: "bg-blue-500/20 text-blue-300 border-blue-500/30",
},
IN_TRANSIT: {
label: "В пути (устаревший)",
color: "bg-orange-500/20 text-orange-300 border-orange-500/30",
},
};
const config =
@ -128,16 +141,24 @@ export function FulfillmentDetailedSuppliesTab() {
const { user } = useAuth();
const [expandedOrders, setExpandedOrders] = useState<Set<string>>(new Set());
// Мутация для обновления статуса заказа
const [updateSupplyOrderStatus] = useMutation(UPDATE_SUPPLY_ORDER_STATUS, {
// Убираем устаревшую мутацию updateSupplyOrderStatus
const [fulfillmentReceiveOrder] = useMutation(FULFILLMENT_RECEIVE_ORDER, {
refetchQueries: [
{ query: GET_SUPPLY_ORDERS }, // Обновляем заказы поставок
{ query: GET_MY_SUPPLIES }, // Обновляем склад фулфилмента (расходники фулфилмента)
{ query: GET_WAREHOUSE_PRODUCTS }, // Обновляем товары склада
{ query: GET_SUPPLY_ORDERS },
{ query: GET_MY_SUPPLIES },
{ query: GET_WAREHOUSE_PRODUCTS },
],
onCompleted: (data) => {
if (data.fulfillmentReceiveOrder.success) {
toast.success(data.fulfillmentReceiveOrder.message);
} else {
toast.error(data.fulfillmentReceiveOrder.message);
}
},
onError: (error) => {
console.error("Error updating supply order status:", error);
toast.error("Ошибка при обновлении статуса заказа");
console.error("Error receiving supply order:", error);
toast.error("Ошибка при приеме заказа поставки");
},
});
@ -177,30 +198,25 @@ export function FulfillmentDetailedSuppliesTab() {
setExpandedOrders(newExpanded);
};
// Функция для обновления статуса заказа
const handleStatusUpdate = async (orderId: string, newStatus: string) => {
// Убираем устаревшую функцию handleStatusUpdate
// Проверяем, можно ли принять заказ (для фулфилмента)
const canReceiveOrder = (status: string) => {
return status === "SHIPPED";
};
// Функция для приема заказа фулфилментом
const handleReceiveOrder = async (orderId: string) => {
try {
await updateSupplyOrderStatus({
variables: {
id: orderId,
status: newStatus,
},
await fulfillmentReceiveOrder({
variables: { id: orderId },
});
toast.success("Статус заказа обновлен");
} catch (error) {
console.error("Error updating status:", error);
console.error("Error receiving order:", error);
}
};
// Проверяем, можно ли отметить как доставленный
const canMarkAsDelivered = (status: string) => {
return status === "IN_TRANSIT";
};
// Проверяем, можно ли отметить как в пути
const canMarkAsInTransit = (status: string) => {
return status === "CONFIRMED";
};
// Убираем устаревшие функции проверки статусов
if (loading) {
return (
@ -406,34 +422,24 @@ export function FulfillmentDetailedSuppliesTab() {
<div className="flex items-center gap-2">
{getStatusBadge(order.status)}
{/* Кнопка "В пути" для подтвержденных заказов */}
{canMarkAsInTransit(order.status) && (
<Button
size="sm"
onClick={(e) => {
e.stopPropagation();
handleStatusUpdate(order.id, "IN_TRANSIT");
}}
className="bg-yellow-500/20 hover:bg-yellow-500/30 text-yellow-300 border border-yellow-500/30 text-xs px-3 py-1 h-7"
>
<Truck className="h-3 w-3 mr-1" />В пути
</Button>
)}
{/* Убираем устаревшую кнопку "В пути" */}
{/* Кнопка "Получено" для заказов в пути */}
{canMarkAsDelivered(order.status) && (
{/* Кнопка "Принять" для заказов в статусе SHIPPED */}
{canReceiveOrder(order.status) && (
<Button
size="sm"
onClick={(e) => {
e.stopPropagation();
handleStatusUpdate(order.id, "DELIVERED");
handleReceiveOrder(order.id);
}}
className="bg-green-500/20 hover:bg-green-500/30 text-green-300 border border-green-500/30 text-xs px-3 py-1 h-7"
>
<CheckCircle className="h-3 w-3 mr-1" />
Получено
Принять
</Button>
)}
{/* Убираем устаревшую кнопку "Получено" */}
</div>
</td>
</tr>