Оптимизирована производительность React компонентов с помощью мемоизации
КРИТИЧНЫЕ КОМПОНЕНТЫ ОПТИМИЗИРОВАНЫ: • AdminDashboard (346 kB) - добавлены React.memo, useCallback, useMemo • SellerStatisticsDashboard (329 kB) - мемоизация кэша и callback функций • CreateSupplyPage (276 kB) - оптимизированы вычисления и обработчики • EmployeesDashboard (268 kB) - мемоизация списков и функций • SalesTab + AdvertisingTab - React.memo обертка ТЕХНИЧЕСКИЕ УЛУЧШЕНИЯ: ✅ React.memo() для предотвращения лишних рендеров ✅ useMemo() для тяжелых вычислений ✅ useCallback() для стабильных ссылок на функции ✅ Мемоизация фильтрации и сортировки списков ✅ Оптимизация пропсов в компонентах-контейнерах РЕЗУЛЬТАТЫ: • Все компоненты успешно компилируются • Линтер проходит без критических ошибок • Сохранена вся функциональность • Улучшена производительность рендеринга • Снижена нагрузка на React дерево 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@ -1,25 +1,6 @@
|
||||
"use client";
|
||||
'use client'
|
||||
|
||||
import { useState } from "react";
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import {
|
||||
SUPPLIER_APPROVE_ORDER,
|
||||
SUPPLIER_REJECT_ORDER,
|
||||
SUPPLIER_SHIP_ORDER,
|
||||
} from "@/graphql/mutations";
|
||||
import { GET_SUPPLY_ORDERS } from "@/graphql/queries";
|
||||
import { toast } from "sonner";
|
||||
import { useMutation } from '@apollo/client'
|
||||
import {
|
||||
Calendar,
|
||||
Package,
|
||||
@ -37,140 +18,142 @@ import {
|
||||
ChevronUp,
|
||||
MessageCircle,
|
||||
Loader2,
|
||||
} from "lucide-react";
|
||||
} from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card } from '@/components/ui/card'
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { SUPPLIER_APPROVE_ORDER, SUPPLIER_REJECT_ORDER, SUPPLIER_SHIP_ORDER } from '@/graphql/mutations'
|
||||
import { GET_SUPPLY_ORDERS } from '@/graphql/queries'
|
||||
|
||||
interface SupplierOrderCardProps {
|
||||
order: {
|
||||
id: string;
|
||||
organizationId: string;
|
||||
partnerId: string;
|
||||
deliveryDate: string;
|
||||
id: string
|
||||
organizationId: string
|
||||
partnerId: string
|
||||
deliveryDate: string
|
||||
status:
|
||||
| "PENDING"
|
||||
| "SUPPLIER_APPROVED"
|
||||
| "CONFIRMED"
|
||||
| "LOGISTICS_CONFIRMED"
|
||||
| "SHIPPED"
|
||||
| "IN_TRANSIT"
|
||||
| "DELIVERED"
|
||||
| "CANCELLED";
|
||||
totalAmount: number;
|
||||
totalItems: number;
|
||||
createdAt: string;
|
||||
| 'PENDING'
|
||||
| 'SUPPLIER_APPROVED'
|
||||
| 'CONFIRMED'
|
||||
| 'LOGISTICS_CONFIRMED'
|
||||
| 'SHIPPED'
|
||||
| 'IN_TRANSIT'
|
||||
| 'DELIVERED'
|
||||
| 'CANCELLED'
|
||||
totalAmount: number
|
||||
totalItems: number
|
||||
createdAt: string
|
||||
organization: {
|
||||
id: string;
|
||||
name?: string;
|
||||
fullName?: string;
|
||||
type: string;
|
||||
inn?: string;
|
||||
};
|
||||
id: string
|
||||
name?: string
|
||||
fullName?: string
|
||||
type: string
|
||||
inn?: string
|
||||
}
|
||||
fulfillmentCenter?: {
|
||||
id: string;
|
||||
name?: string;
|
||||
fullName?: string;
|
||||
type: string;
|
||||
};
|
||||
id: string
|
||||
name?: string
|
||||
fullName?: string
|
||||
type: string
|
||||
}
|
||||
logisticsPartner?: {
|
||||
id: string;
|
||||
name?: string;
|
||||
fullName?: string;
|
||||
type: string;
|
||||
};
|
||||
id: string
|
||||
name?: string
|
||||
fullName?: string
|
||||
type: string
|
||||
}
|
||||
items: Array<{
|
||||
id: string;
|
||||
quantity: number;
|
||||
price: number;
|
||||
totalPrice: number;
|
||||
id: string
|
||||
quantity: number
|
||||
price: number
|
||||
totalPrice: number
|
||||
product: {
|
||||
id: string;
|
||||
name: string;
|
||||
article: string;
|
||||
description?: string;
|
||||
id: string
|
||||
name: string
|
||||
article: string
|
||||
description?: string
|
||||
category?: {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
};
|
||||
}>;
|
||||
};
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
}
|
||||
}>
|
||||
}
|
||||
}
|
||||
|
||||
export function SupplierOrderCard({ order }: SupplierOrderCardProps) {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const [showRejectModal, setShowRejectModal] = useState(false);
|
||||
const [rejectReason, setRejectReason] = useState("");
|
||||
const [isExpanded, setIsExpanded] = useState(false)
|
||||
const [showRejectModal, setShowRejectModal] = useState(false)
|
||||
const [rejectReason, setRejectReason] = useState('')
|
||||
|
||||
// Мутации для действий поставщика
|
||||
const [supplierApproveOrder, { loading: approving }] = useMutation(
|
||||
SUPPLIER_APPROVE_ORDER,
|
||||
{
|
||||
refetchQueries: [{ query: GET_SUPPLY_ORDERS }],
|
||||
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 [supplierApproveOrder, { loading: approving }] = useMutation(SUPPLIER_APPROVE_ORDER, {
|
||||
refetchQueries: [{ query: GET_SUPPLY_ORDERS }],
|
||||
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, { loading: rejecting }] = 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);
|
||||
}
|
||||
setShowRejectModal(false);
|
||||
setRejectReason("");
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error("Error rejecting order:", error);
|
||||
toast.error("Ошибка при отклонении заказа");
|
||||
},
|
||||
}
|
||||
);
|
||||
const [supplierRejectOrder, { loading: rejecting }] = 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)
|
||||
}
|
||||
setShowRejectModal(false)
|
||||
setRejectReason('')
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('Error rejecting order:', error)
|
||||
toast.error('Ошибка при отклонении заказа')
|
||||
},
|
||||
})
|
||||
|
||||
const [supplierShipOrder, { loading: shipping }] = 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("Ошибка при отправке заказа");
|
||||
},
|
||||
}
|
||||
);
|
||||
const [supplierShipOrder, { loading: shipping }] = 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('Ошибка при отправке заказа')
|
||||
},
|
||||
})
|
||||
|
||||
const handleApproveOrder = async () => {
|
||||
try {
|
||||
await supplierApproveOrder({
|
||||
variables: { id: order.id },
|
||||
});
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Error in handleApproveOrder:", error);
|
||||
console.error('Error in handleApproveOrder:', error)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const handleRejectOrder = async () => {
|
||||
if (!rejectReason.trim()) {
|
||||
toast.error("Укажите причину отклонения заявки");
|
||||
return;
|
||||
toast.error('Укажите причину отклонения заявки')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
@ -179,86 +162,62 @@ export function SupplierOrderCard({ order }: SupplierOrderCardProps) {
|
||||
id: order.id,
|
||||
reason: rejectReason,
|
||||
},
|
||||
});
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Error in handleRejectOrder:", error);
|
||||
console.error('Error in handleRejectOrder:', error)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const handleShipOrder = async () => {
|
||||
try {
|
||||
await supplierShipOrder({
|
||||
variables: { id: order.id },
|
||||
});
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Error in handleShipOrder:", error);
|
||||
console.error('Error in handleShipOrder:', error)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const getStatusBadge = (status: string) => {
|
||||
switch (status) {
|
||||
case "PENDING":
|
||||
return (
|
||||
<Badge className="bg-yellow-500/20 text-yellow-300 border-yellow-400/30">
|
||||
🟡 ОЖИДАЕТ
|
||||
</Badge>
|
||||
);
|
||||
case "SUPPLIER_APPROVED":
|
||||
return (
|
||||
<Badge className="bg-green-500/20 text-green-300 border-green-400/30">
|
||||
🟢 ОДОБРЕНО
|
||||
</Badge>
|
||||
);
|
||||
case "CONFIRMED":
|
||||
case "LOGISTICS_CONFIRMED":
|
||||
return (
|
||||
<Badge className="bg-blue-500/20 text-blue-300 border-blue-400/30">
|
||||
🔵 В РАБОТЕ
|
||||
</Badge>
|
||||
);
|
||||
case "SHIPPED":
|
||||
case "IN_TRANSIT":
|
||||
return (
|
||||
<Badge className="bg-orange-500/20 text-orange-300 border-orange-400/30">
|
||||
🟠 В ПУТИ
|
||||
</Badge>
|
||||
);
|
||||
case "DELIVERED":
|
||||
return (
|
||||
<Badge className="bg-emerald-500/20 text-emerald-300 border-emerald-400/30">
|
||||
✅ ДОСТАВЛЕНО
|
||||
</Badge>
|
||||
);
|
||||
case 'PENDING':
|
||||
return <Badge className="bg-yellow-500/20 text-yellow-300 border-yellow-400/30">🟡 ОЖИДАЕТ</Badge>
|
||||
case 'SUPPLIER_APPROVED':
|
||||
return <Badge className="bg-green-500/20 text-green-300 border-green-400/30">🟢 ОДОБРЕНО</Badge>
|
||||
case 'CONFIRMED':
|
||||
case 'LOGISTICS_CONFIRMED':
|
||||
return <Badge className="bg-blue-500/20 text-blue-300 border-blue-400/30">🔵 В РАБОТЕ</Badge>
|
||||
case 'SHIPPED':
|
||||
case 'IN_TRANSIT':
|
||||
return <Badge className="bg-orange-500/20 text-orange-300 border-orange-400/30">🟠 В ПУТИ</Badge>
|
||||
case 'DELIVERED':
|
||||
return <Badge className="bg-emerald-500/20 text-emerald-300 border-emerald-400/30">✅ ДОСТАВЛЕНО</Badge>
|
||||
default:
|
||||
return (
|
||||
<Badge className="bg-white/20 text-white/70 border-white/30">
|
||||
{status}
|
||||
</Badge>
|
||||
);
|
||||
return <Badge className="bg-white/20 text-white/70 border-white/30">{status}</Badge>
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
return new Date(dateString).toLocaleDateString("ru-RU", {
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
year: "numeric",
|
||||
});
|
||||
};
|
||||
return new Date(dateString).toLocaleDateString('ru-RU', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
})
|
||||
}
|
||||
|
||||
const getInitials = (name: string) => {
|
||||
return name
|
||||
.split(" ")
|
||||
.split(' ')
|
||||
.map((word) => word[0])
|
||||
.join("")
|
||||
.join('')
|
||||
.toUpperCase()
|
||||
.slice(0, 2);
|
||||
};
|
||||
.slice(0, 2)
|
||||
}
|
||||
|
||||
const calculateVolume = () => {
|
||||
// Примерный расчет объема - можно улучшить на основе реальных данных о товарах
|
||||
return (order.totalItems * 0.02).toFixed(1); // 0.02 м³ на единицу товара
|
||||
};
|
||||
return (order.totalItems * 0.02).toFixed(1) // 0.02 м³ на единицу товара
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -270,15 +229,11 @@ export function SupplierOrderCard({ order }: SupplierOrderCardProps) {
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Hash className="h-4 w-4 text-white/60" />
|
||||
<span className="text-white font-semibold">
|
||||
СФ-{order.id.slice(-8)}
|
||||
</span>
|
||||
<span className="text-white font-semibold">СФ-{order.id.slice(-8)}</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Calendar className="h-4 w-4 text-blue-400" />
|
||||
<span className="text-white/70 text-sm">
|
||||
{formatDate(order.createdAt)}
|
||||
</span>
|
||||
<span className="text-white/70 text-sm">{formatDate(order.createdAt)}</span>
|
||||
</div>
|
||||
{getStatusBadge(order.status)}
|
||||
</div>
|
||||
@ -295,22 +250,14 @@ export function SupplierOrderCard({ order }: SupplierOrderCardProps) {
|
||||
<div className="flex items-center space-x-3">
|
||||
<Avatar className="w-8 h-8">
|
||||
<AvatarFallback className="bg-blue-500 text-white text-sm">
|
||||
{getInitials(
|
||||
order.organization.name ||
|
||||
order.organization.fullName ||
|
||||
"ОРГ"
|
||||
)}
|
||||
{getInitials(order.organization.name || order.organization.fullName || 'ОРГ')}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div>
|
||||
<p className="text-white font-medium text-sm">
|
||||
{order.organization.name || order.organization.fullName}
|
||||
</p>
|
||||
{order.organization.inn && (
|
||||
<p className="text-white/60 text-xs">
|
||||
ИНН: {order.organization.inn}
|
||||
</p>
|
||||
)}
|
||||
{order.organization.inn && <p className="text-white/60 text-xs">ИНН: {order.organization.inn}</p>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -323,8 +270,7 @@ export function SupplierOrderCard({ order }: SupplierOrderCardProps) {
|
||||
<span className="text-white/60 text-sm">Фулфилмент:</span>
|
||||
</div>
|
||||
<p className="text-white font-medium text-sm">
|
||||
{order.fulfillmentCenter.name ||
|
||||
order.fulfillmentCenter.fullName}
|
||||
{order.fulfillmentCenter.name || order.fulfillmentCenter.fullName}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
@ -337,8 +283,7 @@ export function SupplierOrderCard({ order }: SupplierOrderCardProps) {
|
||||
<span className="text-white/60 text-sm">Логистика:</span>
|
||||
</div>
|
||||
<p className="text-white font-medium text-sm">
|
||||
{order.logisticsPartner.name ||
|
||||
order.logisticsPartner.fullName}
|
||||
{order.logisticsPartner.name || order.logisticsPartner.fullName}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
@ -351,28 +296,17 @@ export function SupplierOrderCard({ order }: SupplierOrderCardProps) {
|
||||
<Package className="h-4 w-4 text-green-400" />
|
||||
<span className="text-white text-sm">
|
||||
{order.items.length} вид
|
||||
{order.items.length === 1
|
||||
? ""
|
||||
: order.items.length < 5
|
||||
? "а"
|
||||
: "ов"}{" "}
|
||||
товаров
|
||||
{order.items.length === 1 ? '' : order.items.length < 5 ? 'а' : 'ов'} товаров
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-white text-sm">
|
||||
{order.totalItems} единиц
|
||||
</span>
|
||||
<span className="text-white text-sm">{order.totalItems} единиц</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-white text-sm">
|
||||
📏 {calculateVolume()} м³
|
||||
</span>
|
||||
<span className="text-white text-sm">📏 {calculateVolume()} м³</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-white font-semibold">
|
||||
💰 {order.totalAmount.toLocaleString()}₽
|
||||
</span>
|
||||
<span className="text-white font-semibold">💰 {order.totalAmount.toLocaleString()}₽</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -385,15 +319,11 @@ export function SupplierOrderCard({ order }: SupplierOrderCardProps) {
|
||||
className="text-white/70 hover:text-white"
|
||||
>
|
||||
Подробности
|
||||
{isExpanded ? (
|
||||
<ChevronUp className="h-4 w-4 ml-1" />
|
||||
) : (
|
||||
<ChevronDown className="h-4 w-4 ml-1" />
|
||||
)}
|
||||
{isExpanded ? <ChevronUp className="h-4 w-4 ml-1" /> : <ChevronDown className="h-4 w-4 ml-1" />}
|
||||
</Button>
|
||||
|
||||
{/* Действия для PENDING */}
|
||||
{order.status === "PENDING" && (
|
||||
{order.status === 'PENDING' && (
|
||||
<>
|
||||
<Button
|
||||
size="sm"
|
||||
@ -421,28 +351,20 @@ export function SupplierOrderCard({ order }: SupplierOrderCardProps) {
|
||||
)}
|
||||
|
||||
{/* Действие для LOGISTICS_CONFIRMED */}
|
||||
{order.status === "LOGISTICS_CONFIRMED" && (
|
||||
{order.status === 'LOGISTICS_CONFIRMED' && (
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={handleShipOrder}
|
||||
disabled={shipping}
|
||||
className="glass-button bg-orange-500/20 hover:bg-orange-500/30 text-orange-300 border border-orange-500/30"
|
||||
>
|
||||
{shipping ? (
|
||||
<Loader2 className="h-3 w-3 mr-1 animate-spin" />
|
||||
) : (
|
||||
<Truck className="h-3 w-3 mr-1" />
|
||||
)}
|
||||
{shipping ? <Loader2 className="h-3 w-3 mr-1 animate-spin" /> : <Truck className="h-3 w-3 mr-1" />}
|
||||
Отгрузить
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Кнопка связаться всегда доступна */}
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="glass-secondary text-blue-300 hover:text-blue-200"
|
||||
>
|
||||
<Button size="sm" variant="ghost" className="glass-secondary text-blue-300 hover:text-blue-200">
|
||||
<MessageCircle className="h-3 w-3 mr-1" />
|
||||
Связаться
|
||||
</Button>
|
||||
@ -460,9 +382,7 @@ export function SupplierOrderCard({ order }: SupplierOrderCardProps) {
|
||||
<div className="flex items-center space-x-2">
|
||||
<Clock className="h-4 w-4 text-white/60" />
|
||||
<span className="text-white/60 text-sm">Срок:</span>
|
||||
<span className="text-white text-sm">
|
||||
{formatDate(order.deliveryDate)}
|
||||
</span>
|
||||
<span className="text-white text-sm">{formatDate(order.deliveryDate)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -471,21 +391,14 @@ export function SupplierOrderCard({ order }: SupplierOrderCardProps) {
|
||||
{/* Расширенная детализация */}
|
||||
{isExpanded && (
|
||||
<div className="border-t border-white/10 p-4">
|
||||
<h4 className="text-white font-semibold mb-3">
|
||||
📋 ДЕТАЛИ ЗАЯВКИ #{order.id.slice(-8)}
|
||||
</h4>
|
||||
<h4 className="text-white font-semibold mb-3">📋 ДЕТАЛИ ЗАЯВКИ #{order.id.slice(-8)}</h4>
|
||||
|
||||
{/* Товары в заявке */}
|
||||
<div className="mb-4">
|
||||
<h5 className="text-white/80 font-medium mb-2">
|
||||
📦 ТОВАРЫ В ЗАЯВКЕ:
|
||||
</h5>
|
||||
<h5 className="text-white/80 font-medium mb-2">📦 ТОВАРЫ В ЗАЯВКЕ:</h5>
|
||||
<div className="space-y-2">
|
||||
{order.items.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="flex items-center justify-between p-2 bg-white/5 rounded"
|
||||
>
|
||||
<div key={item.id} className="flex items-center justify-between p-2 bg-white/5 rounded">
|
||||
<div className="flex-1">
|
||||
<span className="text-white text-sm">
|
||||
{item.product.name} • {item.quantity} шт • {item.price}
|
||||
@ -493,8 +406,7 @@ export function SupplierOrderCard({ order }: SupplierOrderCardProps) {
|
||||
</span>
|
||||
<div className="text-white/60 text-xs">
|
||||
Артикул: {item.product.article}
|
||||
{item.product.category &&
|
||||
` • ${item.product.category.name}`}
|
||||
{item.product.category && ` • ${item.product.category.name}`}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -509,23 +421,15 @@ export function SupplierOrderCard({ order }: SupplierOrderCardProps) {
|
||||
|
||||
{/* Логистическая информация */}
|
||||
<div className="mb-4">
|
||||
<h5 className="text-white/80 font-medium mb-2">
|
||||
📍 ЛОГИСТИЧЕСКАЯ ИНФОРМАЦИЯ:
|
||||
</h5>
|
||||
<h5 className="text-white/80 font-medium mb-2">📍 ЛОГИСТИЧЕСКАЯ ИНФОРМАЦИЯ:</h5>
|
||||
<div className="space-y-1 text-sm">
|
||||
<div className="text-white/70">
|
||||
• Объем груза: {calculateVolume()} м³
|
||||
</div>
|
||||
<div className="text-white/70">• Объем груза: {calculateVolume()} м³</div>
|
||||
<div className="text-white/70">
|
||||
• Предварительная стоимость доставки: ~
|
||||
{Math.round(
|
||||
parseFloat(calculateVolume()) * 3500
|
||||
).toLocaleString()}
|
||||
₽
|
||||
{Math.round(parseFloat(calculateVolume()) * 3500).toLocaleString()}₽
|
||||
</div>
|
||||
<div className="text-white/70">
|
||||
• Маршрут: Склад поставщика →{" "}
|
||||
{order.fulfillmentCenter?.name || "Фулфилмент-центр"}
|
||||
• Маршрут: Склад поставщика → {order.fulfillmentCenter?.name || 'Фулфилмент-центр'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -535,16 +439,12 @@ export function SupplierOrderCard({ order }: SupplierOrderCardProps) {
|
||||
<h5 className="text-white/80 font-medium mb-2">📞 КОНТАКТЫ:</h5>
|
||||
<div className="space-y-1 text-sm">
|
||||
<div className="text-white/70">
|
||||
• Заказчик:{" "}
|
||||
{order.organization.name || order.organization.fullName}
|
||||
{order.organization.inn &&
|
||||
` (ИНН: ${order.organization.inn})`}
|
||||
• Заказчик: {order.organization.name || order.organization.fullName}
|
||||
{order.organization.inn && ` (ИНН: ${order.organization.inn})`}
|
||||
</div>
|
||||
{order.fulfillmentCenter && (
|
||||
<div className="text-white/70">
|
||||
• Фулфилмент:{" "}
|
||||
{order.fulfillmentCenter.name ||
|
||||
order.fulfillmentCenter.fullName}
|
||||
• Фулфилмент: {order.fulfillmentCenter.name || order.fulfillmentCenter.fullName}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@ -561,9 +461,7 @@ export function SupplierOrderCard({ order }: SupplierOrderCardProps) {
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="text-white/90 text-sm mb-2 block">
|
||||
Причина отклонения заявки:
|
||||
</label>
|
||||
<label className="text-white/90 text-sm mb-2 block">Причина отклонения заявки:</label>
|
||||
<Textarea
|
||||
value={rejectReason}
|
||||
onChange={(e) => setRejectReason(e.target.value)}
|
||||
@ -585,11 +483,7 @@ export function SupplierOrderCard({ order }: SupplierOrderCardProps) {
|
||||
disabled={rejecting || !rejectReason.trim()}
|
||||
className="bg-red-500/20 hover:bg-red-500/30 text-red-300 border border-red-500/30"
|
||||
>
|
||||
{rejecting ? (
|
||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||
) : (
|
||||
<XCircle className="h-4 w-4 mr-2" />
|
||||
)}
|
||||
{rejecting ? <Loader2 className="h-4 w-4 mr-2 animate-spin" /> : <XCircle className="h-4 w-4 mr-2" />}
|
||||
Отклонить заявку
|
||||
</Button>
|
||||
</div>
|
||||
@ -597,5 +491,5 @@ export function SupplierOrderCard({ order }: SupplierOrderCardProps) {
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
@ -1,55 +1,37 @@
|
||||
"use client";
|
||||
'use client'
|
||||
|
||||
import { useMemo } from "react";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import {
|
||||
Clock,
|
||||
CheckCircle,
|
||||
Settings,
|
||||
Truck,
|
||||
Package,
|
||||
TrendingUp,
|
||||
Calendar,
|
||||
DollarSign,
|
||||
} from "lucide-react";
|
||||
import { Clock, CheckCircle, Settings, Truck, Package, TrendingUp, Calendar, DollarSign } from 'lucide-react'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
import { Card } from '@/components/ui/card'
|
||||
|
||||
interface SupplierOrderStatsProps {
|
||||
orders: Array<{
|
||||
id: string;
|
||||
status: string;
|
||||
totalAmount: number;
|
||||
totalItems: number;
|
||||
createdAt: string;
|
||||
}>;
|
||||
id: string
|
||||
status: string
|
||||
totalAmount: number
|
||||
totalItems: number
|
||||
createdAt: string
|
||||
}>
|
||||
}
|
||||
|
||||
export function SupplierOrderStats({ orders }: SupplierOrderStatsProps) {
|
||||
const stats = useMemo(() => {
|
||||
const pending = orders.filter((order) => order.status === "PENDING").length;
|
||||
const approved = orders.filter(
|
||||
(order) => order.status === "SUPPLIER_APPROVED"
|
||||
).length;
|
||||
const inProgress = orders.filter((order) =>
|
||||
["CONFIRMED", "LOGISTICS_CONFIRMED"].includes(order.status)
|
||||
).length;
|
||||
const shipping = orders.filter((order) =>
|
||||
["SHIPPED", "IN_TRANSIT"].includes(order.status)
|
||||
).length;
|
||||
const completed = orders.filter(
|
||||
(order) => order.status === "DELIVERED"
|
||||
).length;
|
||||
const pending = orders.filter((order) => order.status === 'PENDING').length
|
||||
const approved = orders.filter((order) => order.status === 'SUPPLIER_APPROVED').length
|
||||
const inProgress = orders.filter((order) => ['CONFIRMED', 'LOGISTICS_CONFIRMED'].includes(order.status)).length
|
||||
const shipping = orders.filter((order) => ['SHIPPED', 'IN_TRANSIT'].includes(order.status)).length
|
||||
const completed = orders.filter((order) => order.status === 'DELIVERED').length
|
||||
|
||||
const totalRevenue = orders
|
||||
.filter((order) => order.status === "DELIVERED")
|
||||
.reduce((sum, order) => sum + order.totalAmount, 0);
|
||||
.filter((order) => order.status === 'DELIVERED')
|
||||
.reduce((sum, order) => sum + order.totalAmount, 0)
|
||||
|
||||
const totalItems = orders.reduce((sum, order) => sum + order.totalItems, 0);
|
||||
const totalItems = orders.reduce((sum, order) => sum + order.totalItems, 0)
|
||||
|
||||
// Заявки за сегодня
|
||||
const today = new Date().toDateString();
|
||||
const todayOrders = orders.filter(
|
||||
(order) => new Date(order.createdAt).toDateString() === today
|
||||
).length;
|
||||
const today = new Date().toDateString()
|
||||
const todayOrders = orders.filter((order) => new Date(order.createdAt).toDateString() === today).length
|
||||
|
||||
return {
|
||||
pending,
|
||||
@ -61,8 +43,8 @@ export function SupplierOrderStats({ orders }: SupplierOrderStatsProps) {
|
||||
totalItems,
|
||||
todayOrders,
|
||||
total: orders.length,
|
||||
};
|
||||
}, [orders]);
|
||||
}
|
||||
}, [orders])
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4">
|
||||
@ -152,9 +134,7 @@ export function SupplierOrderStats({ orders }: SupplierOrderStatsProps) {
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-white/60 text-sm">Выручка (завершенные)</p>
|
||||
<p className="text-xl font-bold text-white">
|
||||
{stats.totalRevenue.toLocaleString()}₽
|
||||
</p>
|
||||
<p className="text-xl font-bold text-white">{stats.totalRevenue.toLocaleString()}₽</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
@ -167,12 +147,10 @@ export function SupplierOrderStats({ orders }: SupplierOrderStatsProps) {
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-white/60 text-sm">Всего товаров в заявках</p>
|
||||
<p className="text-xl font-bold text-white">
|
||||
{stats.totalItems.toLocaleString()} шт.
|
||||
</p>
|
||||
<p className="text-xl font-bold text-white">{stats.totalItems.toLocaleString()} шт.</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
@ -1,20 +1,6 @@
|
||||
"use client";
|
||||
'use client'
|
||||
|
||||
import { useState } from "react";
|
||||
import { useQuery, useMutation } from "@apollo/client";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import { GET_SUPPLY_ORDERS } from "@/graphql/queries";
|
||||
import {
|
||||
SUPPLIER_APPROVE_ORDER,
|
||||
SUPPLIER_REJECT_ORDER,
|
||||
SUPPLIER_SHIP_ORDER
|
||||
} from "@/graphql/mutations";
|
||||
import { toast } from "sonner";
|
||||
import { useQuery, useMutation } from '@apollo/client'
|
||||
import {
|
||||
Calendar,
|
||||
Package,
|
||||
@ -29,63 +15,74 @@ import {
|
||||
Building,
|
||||
Hash,
|
||||
AlertTriangle,
|
||||
} from "lucide-react";
|
||||
} from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card } from '@/components/ui/card'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { SUPPLIER_APPROVE_ORDER, SUPPLIER_REJECT_ORDER, SUPPLIER_SHIP_ORDER } from '@/graphql/mutations'
|
||||
import { GET_SUPPLY_ORDERS } from '@/graphql/queries'
|
||||
import { useAuth } from '@/hooks/useAuth'
|
||||
|
||||
interface SupplyOrder {
|
||||
id: string;
|
||||
organizationId: string;
|
||||
partnerId: string;
|
||||
deliveryDate: string;
|
||||
status: "PENDING" | "SUPPLIER_APPROVED" | "LOGISTICS_CONFIRMED" | "SHIPPED" | "DELIVERED" | "CANCELLED";
|
||||
totalAmount: number;
|
||||
totalItems: number;
|
||||
createdAt: string;
|
||||
id: string
|
||||
organizationId: string
|
||||
partnerId: string
|
||||
deliveryDate: string
|
||||
status: 'PENDING' | 'SUPPLIER_APPROVED' | 'LOGISTICS_CONFIRMED' | 'SHIPPED' | 'DELIVERED' | 'CANCELLED'
|
||||
totalAmount: number
|
||||
totalItems: number
|
||||
createdAt: string
|
||||
organization: {
|
||||
id: string;
|
||||
name?: string;
|
||||
fullName?: string;
|
||||
type: string;
|
||||
};
|
||||
id: string
|
||||
name?: string
|
||||
fullName?: string
|
||||
type: string
|
||||
}
|
||||
fulfillmentCenter?: {
|
||||
id: string;
|
||||
name?: string;
|
||||
fullName?: string;
|
||||
type: string;
|
||||
};
|
||||
id: string
|
||||
name?: string
|
||||
fullName?: string
|
||||
type: string
|
||||
}
|
||||
logisticsPartner?: {
|
||||
id: string;
|
||||
name?: string;
|
||||
fullName?: string;
|
||||
type: string;
|
||||
};
|
||||
id: string
|
||||
name?: string
|
||||
fullName?: string
|
||||
type: string
|
||||
}
|
||||
items: Array<{
|
||||
id: string;
|
||||
quantity: number;
|
||||
price: number;
|
||||
totalPrice: number;
|
||||
id: string
|
||||
quantity: number
|
||||
price: number
|
||||
totalPrice: number
|
||||
product: {
|
||||
id: string;
|
||||
name: string;
|
||||
article: string;
|
||||
description?: string;
|
||||
id: string
|
||||
name: string
|
||||
article: string
|
||||
description?: string
|
||||
category?: {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
};
|
||||
}>;
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
}
|
||||
}>
|
||||
}
|
||||
|
||||
export function SupplierOrdersContent() {
|
||||
const { user } = useAuth();
|
||||
const [expandedOrders, setExpandedOrders] = useState<Set<string>>(new Set());
|
||||
const [rejectReason, setRejectReason] = useState<string>("");
|
||||
const [showRejectModal, setShowRejectModal] = useState<string | null>(null);
|
||||
const { user } = useAuth()
|
||||
const [expandedOrders, setExpandedOrders] = useState<Set<string>>(new Set())
|
||||
const [rejectReason, setRejectReason] = useState<string>('')
|
||||
const [showRejectModal, setShowRejectModal] = useState<string | null>(null)
|
||||
|
||||
// Загружаем заказы поставок
|
||||
const { data, loading, error, refetch } = useQuery(GET_SUPPLY_ORDERS, {
|
||||
fetchPolicy: "cache-and-network",
|
||||
});
|
||||
fetchPolicy: 'cache-and-network',
|
||||
})
|
||||
|
||||
// Мутации для действий поставщика
|
||||
const [supplierApproveOrder] = useMutation(SUPPLIER_APPROVE_ORDER, {
|
||||
@ -93,153 +90,151 @@ export function SupplierOrdersContent() {
|
||||
awaitRefetchQueries: true,
|
||||
onCompleted: (data) => {
|
||||
if (data.supplierApproveOrder.success) {
|
||||
toast.success(data.supplierApproveOrder.message);
|
||||
toast.success(data.supplierApproveOrder.message)
|
||||
} else {
|
||||
toast.error(data.supplierApproveOrder.message);
|
||||
toast.error(data.supplierApproveOrder.message)
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error("Error approving order:", error);
|
||||
toast.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);
|
||||
toast.success(data.supplierRejectOrder.message)
|
||||
} else {
|
||||
toast.error(data.supplierRejectOrder.message);
|
||||
toast.error(data.supplierRejectOrder.message)
|
||||
}
|
||||
setShowRejectModal(null);
|
||||
setRejectReason("");
|
||||
setShowRejectModal(null)
|
||||
setRejectReason('')
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error("Error rejecting order:", error);
|
||||
toast.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);
|
||||
toast.success(data.supplierShipOrder.message)
|
||||
} else {
|
||||
toast.error(data.supplierShipOrder.message);
|
||||
toast.error(data.supplierShipOrder.message)
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error("Error shipping order:", error);
|
||||
toast.error("Ошибка при отправке заказа");
|
||||
console.error('Error shipping order:', error)
|
||||
toast.error('Ошибка при отправке заказа')
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
const toggleOrderExpansion = (orderId: string) => {
|
||||
const newExpanded = new Set(expandedOrders);
|
||||
const newExpanded = new Set(expandedOrders)
|
||||
if (newExpanded.has(orderId)) {
|
||||
newExpanded.delete(orderId);
|
||||
newExpanded.delete(orderId)
|
||||
} else {
|
||||
newExpanded.add(orderId);
|
||||
newExpanded.add(orderId)
|
||||
}
|
||||
setExpandedOrders(newExpanded);
|
||||
};
|
||||
setExpandedOrders(newExpanded)
|
||||
}
|
||||
|
||||
// Фильтруем заказы где текущая организация является поставщиком
|
||||
const supplierOrders: SupplyOrder[] = (data?.supplyOrders || []).filter(
|
||||
(order: SupplyOrder) => {
|
||||
const isSupplier = order.partnerId === user?.organization?.id;
|
||||
return isSupplier;
|
||||
}
|
||||
);
|
||||
const supplierOrders: SupplyOrder[] = (data?.supplyOrders || []).filter((order: SupplyOrder) => {
|
||||
const isSupplier = order.partnerId === user?.organization?.id
|
||||
return isSupplier
|
||||
})
|
||||
|
||||
const getStatusBadge = (status: SupplyOrder["status"]) => {
|
||||
const getStatusBadge = (status: SupplyOrder['status']) => {
|
||||
const statusMap = {
|
||||
PENDING: {
|
||||
label: "Ожидает одобрения",
|
||||
color: "bg-yellow-500/20 text-yellow-300 border-yellow-500/30",
|
||||
label: 'Ожидает одобрения',
|
||||
color: 'bg-yellow-500/20 text-yellow-300 border-yellow-500/30',
|
||||
icon: Clock,
|
||||
},
|
||||
SUPPLIER_APPROVED: {
|
||||
label: "Ожидает подтверждения логистики",
|
||||
color: "bg-blue-500/20 text-blue-300 border-blue-500/30",
|
||||
label: 'Ожидает подтверждения логистики',
|
||||
color: 'bg-blue-500/20 text-blue-300 border-blue-500/30',
|
||||
icon: Clock,
|
||||
},
|
||||
LOGISTICS_CONFIRMED: {
|
||||
label: "Готов к отправке",
|
||||
color: "bg-cyan-500/20 text-cyan-300 border-cyan-500/30",
|
||||
label: 'Готов к отправке',
|
||||
color: 'bg-cyan-500/20 text-cyan-300 border-cyan-500/30',
|
||||
icon: CheckCircle,
|
||||
},
|
||||
SHIPPED: {
|
||||
label: "Отправлено",
|
||||
color: "bg-orange-500/20 text-orange-300 border-orange-500/30",
|
||||
label: 'Отправлено',
|
||||
color: 'bg-orange-500/20 text-orange-300 border-orange-500/30',
|
||||
icon: Truck,
|
||||
},
|
||||
DELIVERED: {
|
||||
label: "Доставлено",
|
||||
color: "bg-green-500/20 text-green-300 border-green-500/30",
|
||||
label: 'Доставлено',
|
||||
color: 'bg-green-500/20 text-green-300 border-green-500/30',
|
||||
icon: Package,
|
||||
},
|
||||
CANCELLED: {
|
||||
label: "Отменено",
|
||||
color: "bg-red-500/20 text-red-300 border-red-500/30",
|
||||
label: 'Отменено',
|
||||
color: 'bg-red-500/20 text-red-300 border-red-500/30',
|
||||
icon: XCircle,
|
||||
},
|
||||
};
|
||||
const { label, color, icon: Icon } = statusMap[status];
|
||||
}
|
||||
const { label, color, icon: Icon } = statusMap[status]
|
||||
return (
|
||||
<Badge className={`${color} border flex items-center gap-1 text-xs`}>
|
||||
<Icon className="h-3 w-3" />
|
||||
{label}
|
||||
</Badge>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
const handleApproveOrder = async (orderId: string) => {
|
||||
await supplierApproveOrder({ variables: { id: orderId } });
|
||||
};
|
||||
await supplierApproveOrder({ variables: { id: orderId } })
|
||||
}
|
||||
|
||||
const handleRejectOrder = async (orderId: string) => {
|
||||
await supplierRejectOrder({
|
||||
variables: { id: orderId, reason: rejectReason || undefined },
|
||||
});
|
||||
};
|
||||
})
|
||||
}
|
||||
|
||||
const handleShipOrder = async (orderId: string) => {
|
||||
await supplierShipOrder({ variables: { id: orderId } });
|
||||
};
|
||||
await supplierShipOrder({ variables: { id: orderId } })
|
||||
}
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
return new Date(dateString).toLocaleDateString("ru-RU", {
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
year: "numeric",
|
||||
});
|
||||
};
|
||||
return new Date(dateString).toLocaleDateString('ru-RU', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
})
|
||||
}
|
||||
|
||||
const formatCurrency = (amount: number) => {
|
||||
return new Intl.NumberFormat("ru-RU", {
|
||||
style: "currency",
|
||||
currency: "RUB",
|
||||
}).format(amount);
|
||||
};
|
||||
return new Intl.NumberFormat('ru-RU', {
|
||||
style: 'currency',
|
||||
currency: 'RUB',
|
||||
}).format(amount)
|
||||
}
|
||||
|
||||
const getInitials = (name: string): string => {
|
||||
return name
|
||||
.split(" ")
|
||||
.split(' ')
|
||||
.map((word) => word.charAt(0))
|
||||
.join("")
|
||||
.join('')
|
||||
.toUpperCase()
|
||||
.slice(0, 2);
|
||||
};
|
||||
.slice(0, 2)
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<div className="text-white">Загрузка заказов...</div>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
if (error) {
|
||||
@ -247,7 +242,7 @@ export function SupplierOrdersContent() {
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<div className="text-red-300">Ошибка загрузки заказов: {error.message}</div>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
@ -255,12 +250,8 @@ export function SupplierOrdersContent() {
|
||||
{/* Заголовок */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-white mb-2">
|
||||
Заказы поставок
|
||||
</h1>
|
||||
<p className="text-white/60">
|
||||
Управление входящими заказами от фулфилмент-центров
|
||||
</p>
|
||||
<h1 className="text-2xl font-bold text-white mb-2">Заказы поставок</h1>
|
||||
<p className="text-white/60">Управление входящими заказами от фулфилмент-центров</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -274,7 +265,7 @@ export function SupplierOrdersContent() {
|
||||
<div>
|
||||
<p className="text-white/60 text-sm">Ожидают одобрения</p>
|
||||
<p className="text-xl font-bold text-white">
|
||||
{supplierOrders.filter(order => order.status === "PENDING").length}
|
||||
{supplierOrders.filter((order) => order.status === 'PENDING').length}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -288,7 +279,7 @@ export function SupplierOrdersContent() {
|
||||
<div>
|
||||
<p className="text-white/60 text-sm">Готово к отправке</p>
|
||||
<p className="text-xl font-bold text-white">
|
||||
{supplierOrders.filter(order => order.status === "LOGISTICS_CONFIRMED").length}
|
||||
{supplierOrders.filter((order) => order.status === 'LOGISTICS_CONFIRMED').length}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -302,7 +293,7 @@ export function SupplierOrdersContent() {
|
||||
<div>
|
||||
<p className="text-white/60 text-sm">В пути</p>
|
||||
<p className="text-xl font-bold text-white">
|
||||
{supplierOrders.filter(order => order.status === "SHIPPED").length}
|
||||
{supplierOrders.filter((order) => order.status === 'SHIPPED').length}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -316,7 +307,7 @@ export function SupplierOrdersContent() {
|
||||
<div>
|
||||
<p className="text-white/60 text-sm">Доставлено</p>
|
||||
<p className="text-xl font-bold text-white">
|
||||
{supplierOrders.filter(order => order.status === "DELIVERED").length}
|
||||
{supplierOrders.filter((order) => order.status === 'DELIVERED').length}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -329,12 +320,8 @@ export function SupplierOrdersContent() {
|
||||
<Card className="bg-white/10 backdrop-blur border-white/20 p-8">
|
||||
<div className="text-center">
|
||||
<Package className="h-12 w-12 text-white/40 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-semibold text-white mb-2">
|
||||
Нет заказов поставок
|
||||
</h3>
|
||||
<p className="text-white/60">
|
||||
Входящие заказы от фулфилмент-центров будут отображаться здесь
|
||||
</p>
|
||||
<h3 className="text-lg font-semibold text-white mb-2">Нет заказов поставок</h3>
|
||||
<p className="text-white/60">Входящие заказы от фулфилмент-центров будут отображаться здесь</p>
|
||||
</div>
|
||||
</Card>
|
||||
) : (
|
||||
@ -352,16 +339,14 @@ export function SupplierOrdersContent() {
|
||||
{/* Номер заказа */}
|
||||
<div className="flex items-center space-x-2">
|
||||
<Hash className="h-4 w-4 text-white/60" />
|
||||
<span className="text-white font-semibold">
|
||||
{order.id.slice(-8)}
|
||||
</span>
|
||||
<span className="text-white font-semibold">{order.id.slice(-8)}</span>
|
||||
</div>
|
||||
|
||||
{/* Заказчик */}
|
||||
<div className="flex items-center space-x-3 min-w-0">
|
||||
<Avatar className="w-8 h-8">
|
||||
<AvatarFallback className="bg-blue-500 text-white text-sm">
|
||||
{getInitials(order.organization.name || order.organization.fullName || "ФФ")}
|
||||
{getInitials(order.organization.name || order.organization.fullName || 'ФФ')}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="min-w-0">
|
||||
@ -369,7 +354,7 @@ export function SupplierOrdersContent() {
|
||||
{order.organization.name || order.organization.fullName}
|
||||
</h3>
|
||||
<p className="text-white/60 text-xs">
|
||||
{order.organization.type === "FULFILLMENT" ? "Фулфилмент" : "Организация"}
|
||||
{order.organization.type === 'FULFILLMENT' ? 'Фулфилмент' : 'Организация'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -378,15 +363,11 @@ export function SupplierOrdersContent() {
|
||||
<div className="hidden lg:flex items-center space-x-4">
|
||||
<div className="flex items-center space-x-1">
|
||||
<Calendar className="h-4 w-4 text-blue-400" />
|
||||
<span className="text-white text-sm">
|
||||
{formatDate(order.deliveryDate)}
|
||||
</span>
|
||||
<span className="text-white text-sm">{formatDate(order.deliveryDate)}</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-1">
|
||||
<Package className="h-4 w-4 text-green-400" />
|
||||
<span className="text-white text-sm">
|
||||
{order.totalItems} шт.
|
||||
</span>
|
||||
<span className="text-white text-sm">{order.totalItems} шт.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -396,13 +377,13 @@ export function SupplierOrdersContent() {
|
||||
{getStatusBadge(order.status)}
|
||||
|
||||
{/* Кнопки действий для поставщика */}
|
||||
{order.status === "PENDING" && (
|
||||
{order.status === 'PENDING' && (
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleApproveOrder(order.id);
|
||||
e.stopPropagation()
|
||||
handleApproveOrder(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"
|
||||
>
|
||||
@ -412,8 +393,8 @@ export function SupplierOrdersContent() {
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setShowRejectModal(order.id);
|
||||
e.stopPropagation()
|
||||
setShowRejectModal(order.id)
|
||||
}}
|
||||
className="bg-red-500/20 hover:bg-red-500/30 text-red-300 border border-red-500/30 text-xs px-3 py-1 h-7"
|
||||
>
|
||||
@ -423,12 +404,12 @@ export function SupplierOrdersContent() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{order.status === "LOGISTICS_CONFIRMED" && (
|
||||
{order.status === 'LOGISTICS_CONFIRMED' && (
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleShipOrder(order.id);
|
||||
e.stopPropagation()
|
||||
handleShipOrder(order.id)
|
||||
}}
|
||||
className="bg-orange-500/20 hover:bg-orange-500/30 text-orange-300 border border-orange-500/30 text-xs px-3 py-1 h-7"
|
||||
>
|
||||
@ -443,14 +424,12 @@ export function SupplierOrdersContent() {
|
||||
{expandedOrders.has(order.id) && (
|
||||
<>
|
||||
<Separator className="my-4 bg-white/10" />
|
||||
|
||||
|
||||
{/* Сумма заказа */}
|
||||
<div className="mb-4 p-3 bg-white/5 rounded">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-white/60">Общая сумма:</span>
|
||||
<span className="text-white font-semibold text-lg">
|
||||
{formatCurrency(order.totalAmount)}
|
||||
</span>
|
||||
<span className="text-white font-semibold text-lg">{formatCurrency(order.totalAmount)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -462,9 +441,7 @@ export function SupplierOrdersContent() {
|
||||
Логистическая компания
|
||||
</h4>
|
||||
<div className="bg-white/5 rounded p-3">
|
||||
<p className="text-white">
|
||||
{order.logisticsPartner.name || order.logisticsPartner.fullName}
|
||||
</p>
|
||||
<p className="text-white">{order.logisticsPartner.name || order.logisticsPartner.fullName}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@ -477,36 +454,20 @@ export function SupplierOrdersContent() {
|
||||
</h4>
|
||||
<div className="space-y-2">
|
||||
{order.items.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="bg-white/5 rounded p-3 flex items-center justify-between"
|
||||
>
|
||||
<div key={item.id} className="bg-white/5 rounded p-3 flex items-center justify-between">
|
||||
<div className="flex-1 min-w-0">
|
||||
<h5 className="text-white font-medium text-sm">
|
||||
{item.product.name}
|
||||
</h5>
|
||||
<p className="text-white/60 text-xs">
|
||||
Артикул: {item.product.article}
|
||||
</p>
|
||||
<h5 className="text-white font-medium text-sm">{item.product.name}</h5>
|
||||
<p className="text-white/60 text-xs">Артикул: {item.product.article}</p>
|
||||
{item.product.category && (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="bg-blue-500/20 text-blue-300 text-xs mt-1"
|
||||
>
|
||||
<Badge variant="secondary" className="bg-blue-500/20 text-blue-300 text-xs mt-1">
|
||||
{item.product.category.name}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-right flex-shrink-0 ml-4">
|
||||
<p className="text-white font-semibold">
|
||||
{item.quantity} шт.
|
||||
</p>
|
||||
<p className="text-white/60 text-xs">
|
||||
{formatCurrency(item.price)}
|
||||
</p>
|
||||
<p className="text-green-400 font-semibold text-sm">
|
||||
{formatCurrency(item.totalPrice)}
|
||||
</p>
|
||||
<p className="text-white font-semibold">{item.quantity} шт.</p>
|
||||
<p className="text-white/60 text-xs">{formatCurrency(item.price)}</p>
|
||||
<p className="text-green-400 font-semibold text-sm">{formatCurrency(item.totalPrice)}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
@ -524,12 +485,8 @@ export function SupplierOrdersContent() {
|
||||
{showRejectModal && (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
||||
<Card className="bg-gray-900 border-white/20 p-6 max-w-md w-full mx-4">
|
||||
<h3 className="text-white font-semibold text-lg mb-4">
|
||||
Отклонить заказ
|
||||
</h3>
|
||||
<p className="text-white/60 text-sm mb-4">
|
||||
Укажите причину отклонения заказа (необязательно):
|
||||
</p>
|
||||
<h3 className="text-white font-semibold text-lg mb-4">Отклонить заказ</h3>
|
||||
<p className="text-white/60 text-sm mb-4">Укажите причину отклонения заказа (необязательно):</p>
|
||||
<textarea
|
||||
value={rejectReason}
|
||||
onChange={(e) => setRejectReason(e.target.value)}
|
||||
@ -546,8 +503,8 @@ export function SupplierOrdersContent() {
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setShowRejectModal(null);
|
||||
setRejectReason("");
|
||||
setShowRejectModal(null)
|
||||
setRejectReason('')
|
||||
}}
|
||||
variant="outline"
|
||||
className="border-white/20 text-white hover:bg-white/10"
|
||||
@ -559,5 +516,5 @@ export function SupplierOrdersContent() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -1,12 +1,14 @@
|
||||
"use client";
|
||||
'use client'
|
||||
|
||||
import { Sidebar } from "@/components/dashboard/sidebar";
|
||||
import { useSidebar } from "@/hooks/useSidebar";
|
||||
import { SupplierOrdersTabs } from "./supplier-orders-tabs";
|
||||
import { Package, AlertTriangle } from "lucide-react";
|
||||
import { Package, AlertTriangle } from 'lucide-react'
|
||||
|
||||
import { Sidebar } from '@/components/dashboard/sidebar'
|
||||
import { useSidebar } from '@/hooks/useSidebar'
|
||||
|
||||
import { SupplierOrdersTabs } from './supplier-orders-tabs'
|
||||
|
||||
export function SupplierOrdersDashboard() {
|
||||
const { getSidebarMargin } = useSidebar();
|
||||
const { getSidebarMargin } = useSidebar()
|
||||
|
||||
return (
|
||||
<div className="h-screen flex overflow-hidden">
|
||||
@ -19,10 +21,7 @@ export function SupplierOrdersDashboard() {
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-white mb-2">Заявки</h1>
|
||||
<p className="text-white/60">
|
||||
Управление входящими заявками от заказчиков согласно правилам
|
||||
системы
|
||||
</p>
|
||||
<p className="text-white/60">Управление входящими заявками от заказчиков согласно правилам системы</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -31,5 +30,5 @@ export function SupplierOrdersDashboard() {
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
@ -1,31 +1,20 @@
|
||||
"use client";
|
||||
'use client'
|
||||
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import {
|
||||
Search,
|
||||
Filter,
|
||||
Calendar,
|
||||
DollarSign,
|
||||
Package,
|
||||
Building,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import { Search, Filter, Calendar, DollarSign, Package, Building, X } from 'lucide-react'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card } from '@/components/ui/card'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
|
||||
|
||||
interface SupplierOrdersSearchProps {
|
||||
searchQuery: string;
|
||||
onSearchChange: (value: string) => void;
|
||||
priceRange: { min: string; max: string };
|
||||
onPriceRangeChange: (range: { min: string; max: string }) => void;
|
||||
dateFilter: string;
|
||||
onDateFilterChange: (value: string) => void;
|
||||
searchQuery: string
|
||||
onSearchChange: (value: string) => void
|
||||
priceRange: { min: string; max: string }
|
||||
onPriceRangeChange: (range: { min: string; max: string }) => void
|
||||
dateFilter: string
|
||||
onDateFilterChange: (value: string) => void
|
||||
}
|
||||
|
||||
export function SupplierOrdersSearch({
|
||||
@ -36,12 +25,12 @@ export function SupplierOrdersSearch({
|
||||
dateFilter,
|
||||
onDateFilterChange,
|
||||
}: SupplierOrdersSearchProps) {
|
||||
const hasActiveFilters = priceRange.min || priceRange.max || dateFilter;
|
||||
const hasActiveFilters = priceRange.min || priceRange.max || dateFilter
|
||||
|
||||
const clearFilters = () => {
|
||||
onPriceRangeChange({ min: "", max: "" });
|
||||
onDateFilterChange("");
|
||||
};
|
||||
onPriceRangeChange({ min: '', max: '' })
|
||||
onDateFilterChange('')
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="glass-card border-white/10 p-4">
|
||||
@ -64,15 +53,13 @@ export function SupplierOrdersSearch({
|
||||
<Button
|
||||
variant="ghost"
|
||||
className={`glass-secondary border-white/20 ${
|
||||
hasActiveFilters ? "border-blue-400/50 bg-blue-500/10" : ""
|
||||
hasActiveFilters ? 'border-blue-400/50 bg-blue-500/10' : ''
|
||||
}`}
|
||||
>
|
||||
<Filter className="h-4 w-4 mr-2" />
|
||||
Фильтры
|
||||
{hasActiveFilters && (
|
||||
<span className="ml-2 bg-blue-500/20 text-blue-300 px-2 py-1 rounded text-xs">
|
||||
Активны
|
||||
</span>
|
||||
<span className="ml-2 bg-blue-500/20 text-blue-300 px-2 py-1 rounded text-xs">Активны</span>
|
||||
)}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
@ -81,12 +68,7 @@ export function SupplierOrdersSearch({
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-white font-semibold">Фильтры поиска</h4>
|
||||
{hasActiveFilters && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={clearFilters}
|
||||
className="text-white/60 hover:text-white"
|
||||
>
|
||||
<Button variant="ghost" size="sm" onClick={clearFilters} className="text-white/60 hover:text-white">
|
||||
<X className="h-4 w-4 mr-1" />
|
||||
Очистить
|
||||
</Button>
|
||||
@ -164,9 +146,7 @@ export function SupplierOrdersSearch({
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
onDateFilterChange(new Date().toISOString().split("T")[0])
|
||||
}
|
||||
onClick={() => onDateFilterChange(new Date().toISOString().split('T')[0])}
|
||||
className="text-xs h-7 px-2"
|
||||
>
|
||||
Сегодня
|
||||
@ -175,9 +155,9 @@ export function SupplierOrdersSearch({
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
const weekAgo = new Date();
|
||||
weekAgo.setDate(weekAgo.getDate() - 7);
|
||||
onDateFilterChange(weekAgo.toISOString().split("T")[0]);
|
||||
const weekAgo = new Date()
|
||||
weekAgo.setDate(weekAgo.getDate() - 7)
|
||||
onDateFilterChange(weekAgo.toISOString().split('T')[0])
|
||||
}}
|
||||
className="text-xs h-7 px-2"
|
||||
>
|
||||
@ -194,17 +174,17 @@ export function SupplierOrdersSearch({
|
||||
<span className="text-white/60">Активные фильтры:</span>
|
||||
{dateFilter && (
|
||||
<span className="bg-blue-500/20 text-blue-300 px-2 py-1 rounded border border-blue-400/30">
|
||||
📅 {new Date(dateFilter).toLocaleDateString("ru-RU")}
|
||||
📅 {new Date(dateFilter).toLocaleDateString('ru-RU')}
|
||||
</span>
|
||||
)}
|
||||
{(priceRange.min || priceRange.max) && (
|
||||
<span className="bg-green-500/20 text-green-300 px-2 py-1 rounded border border-green-400/30">
|
||||
💰 {priceRange.min || "0"}₽ — {priceRange.max || "∞"}₽
|
||||
💰 {priceRange.min || '0'}₽ — {priceRange.max || '∞'}₽
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
@ -1,184 +1,164 @@
|
||||
"use client";
|
||||
'use client'
|
||||
|
||||
import { useState, useMemo } from "react";
|
||||
import { useQuery } from "@apollo/client";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { GET_SUPPLY_ORDERS } from "@/graphql/queries";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import { SupplierOrderCard } from "./supplier-order-card";
|
||||
import { SupplierOrderStats } from "./supplier-order-stats";
|
||||
import { SupplierOrdersSearch } from "./supplier-orders-search";
|
||||
import {
|
||||
Clock,
|
||||
CheckCircle,
|
||||
Settings,
|
||||
Truck,
|
||||
Package,
|
||||
Calendar,
|
||||
Search,
|
||||
} from "lucide-react";
|
||||
import { useQuery } from '@apollo/client'
|
||||
import { Clock, CheckCircle, Settings, Truck, Package, Calendar, Search } from 'lucide-react'
|
||||
import { useState, useMemo } from 'react'
|
||||
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card } from '@/components/ui/card'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { GET_SUPPLY_ORDERS } from '@/graphql/queries'
|
||||
import { useAuth } from '@/hooks/useAuth'
|
||||
|
||||
import { SupplierOrderCard } from './supplier-order-card'
|
||||
import { SupplierOrderStats } from './supplier-order-stats'
|
||||
import { SupplierOrdersSearch } from './supplier-orders-search'
|
||||
|
||||
interface SupplyOrder {
|
||||
id: string;
|
||||
organizationId: string;
|
||||
partnerId: string;
|
||||
deliveryDate: string;
|
||||
id: string
|
||||
organizationId: string
|
||||
partnerId: string
|
||||
deliveryDate: string
|
||||
status:
|
||||
| "PENDING"
|
||||
| "SUPPLIER_APPROVED"
|
||||
| "CONFIRMED"
|
||||
| "LOGISTICS_CONFIRMED"
|
||||
| "SHIPPED"
|
||||
| "IN_TRANSIT"
|
||||
| "DELIVERED"
|
||||
| "CANCELLED";
|
||||
totalAmount: number;
|
||||
totalItems: number;
|
||||
createdAt: string;
|
||||
| 'PENDING'
|
||||
| 'SUPPLIER_APPROVED'
|
||||
| 'CONFIRMED'
|
||||
| 'LOGISTICS_CONFIRMED'
|
||||
| 'SHIPPED'
|
||||
| 'IN_TRANSIT'
|
||||
| 'DELIVERED'
|
||||
| 'CANCELLED'
|
||||
totalAmount: number
|
||||
totalItems: number
|
||||
createdAt: string
|
||||
organization: {
|
||||
id: string;
|
||||
name?: string;
|
||||
fullName?: string;
|
||||
type: string;
|
||||
inn?: string;
|
||||
};
|
||||
id: string
|
||||
name?: string
|
||||
fullName?: string
|
||||
type: string
|
||||
inn?: string
|
||||
}
|
||||
partner?: {
|
||||
id: string;
|
||||
name?: string;
|
||||
fullName?: string;
|
||||
inn?: string;
|
||||
address?: string;
|
||||
phones?: string[];
|
||||
emails?: string[];
|
||||
};
|
||||
id: string
|
||||
name?: string
|
||||
fullName?: string
|
||||
inn?: string
|
||||
address?: string
|
||||
phones?: string[]
|
||||
emails?: string[]
|
||||
}
|
||||
fulfillmentCenter?: {
|
||||
id: string;
|
||||
name?: string;
|
||||
fullName?: string;
|
||||
type: string;
|
||||
};
|
||||
id: string
|
||||
name?: string
|
||||
fullName?: string
|
||||
type: string
|
||||
}
|
||||
logisticsPartner?: {
|
||||
id: string;
|
||||
name?: string;
|
||||
fullName?: string;
|
||||
type: string;
|
||||
};
|
||||
id: string
|
||||
name?: string
|
||||
fullName?: string
|
||||
type: string
|
||||
}
|
||||
items: Array<{
|
||||
id: string;
|
||||
quantity: number;
|
||||
price: number;
|
||||
totalPrice: number;
|
||||
id: string
|
||||
quantity: number
|
||||
price: number
|
||||
totalPrice: number
|
||||
product: {
|
||||
id: string;
|
||||
name: string;
|
||||
article: string;
|
||||
description?: string;
|
||||
id: string
|
||||
name: string
|
||||
article: string
|
||||
description?: string
|
||||
category?: {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
};
|
||||
}>;
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
}
|
||||
}>
|
||||
}
|
||||
|
||||
export function SupplierOrdersTabs() {
|
||||
const { user } = useAuth();
|
||||
const [activeTab, setActiveTab] = useState("new");
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [dateFilter, setDateFilter] = useState("");
|
||||
const [priceRange, setPriceRange] = useState({ min: "", max: "" });
|
||||
const { user } = useAuth()
|
||||
const [activeTab, setActiveTab] = useState('new')
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [dateFilter, setDateFilter] = useState('')
|
||||
const [priceRange, setPriceRange] = useState({ min: '', max: '' })
|
||||
|
||||
// Загружаем заказы поставок
|
||||
const { data, loading, error } = useQuery(GET_SUPPLY_ORDERS, {
|
||||
fetchPolicy: "cache-and-network",
|
||||
});
|
||||
fetchPolicy: 'cache-and-network',
|
||||
})
|
||||
|
||||
// Фильтруем заказы где текущая организация является поставщиком
|
||||
const supplierOrders: SupplyOrder[] = useMemo(() => {
|
||||
return (data?.supplyOrders || []).filter(
|
||||
(order: SupplyOrder) => order.partnerId === user?.organization?.id
|
||||
);
|
||||
}, [data?.supplyOrders, user?.organization?.id]);
|
||||
return (data?.supplyOrders || []).filter((order: SupplyOrder) => order.partnerId === user?.organization?.id)
|
||||
}, [data?.supplyOrders, user?.organization?.id])
|
||||
|
||||
// Фильтрация заказов по поисковому запросу
|
||||
const filteredOrders = useMemo(() => {
|
||||
let filtered = supplierOrders;
|
||||
let filtered = supplierOrders
|
||||
|
||||
// Поиск по номеру заявки, заказчику, товарам, ИНН
|
||||
if (searchQuery) {
|
||||
const query = searchQuery.toLowerCase();
|
||||
const query = searchQuery.toLowerCase()
|
||||
filtered = filtered.filter(
|
||||
(order) =>
|
||||
order.id.toLowerCase().includes(query) ||
|
||||
(order.organization.name || "").toLowerCase().includes(query) ||
|
||||
(order.organization.fullName || "").toLowerCase().includes(query) ||
|
||||
(order.organization.inn || "").toLowerCase().includes(query) ||
|
||||
order.items.some((item) =>
|
||||
item.product.name.toLowerCase().includes(query)
|
||||
)
|
||||
);
|
||||
(order.organization.name || '').toLowerCase().includes(query) ||
|
||||
(order.organization.fullName || '').toLowerCase().includes(query) ||
|
||||
(order.organization.inn || '').toLowerCase().includes(query) ||
|
||||
order.items.some((item) => item.product.name.toLowerCase().includes(query)),
|
||||
)
|
||||
}
|
||||
|
||||
// Фильтр по диапазону цены
|
||||
if (priceRange.min || priceRange.max) {
|
||||
filtered = filtered.filter((order) => {
|
||||
if (priceRange.min && order.totalAmount < parseFloat(priceRange.min))
|
||||
return false;
|
||||
if (priceRange.max && order.totalAmount > parseFloat(priceRange.max))
|
||||
return false;
|
||||
return true;
|
||||
});
|
||||
if (priceRange.min && order.totalAmount < parseFloat(priceRange.min)) return false
|
||||
if (priceRange.max && order.totalAmount > parseFloat(priceRange.max)) return false
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}, [supplierOrders, searchQuery, priceRange]);
|
||||
return filtered
|
||||
}, [supplierOrders, searchQuery, priceRange])
|
||||
|
||||
// Разделение заказов по статусам согласно правилам
|
||||
const ordersByStatus = useMemo(() => {
|
||||
return {
|
||||
new: filteredOrders.filter((order) => order.status === "PENDING"),
|
||||
approved: filteredOrders.filter(
|
||||
(order) => order.status === "SUPPLIER_APPROVED"
|
||||
),
|
||||
inProgress: filteredOrders.filter((order) =>
|
||||
["CONFIRMED", "LOGISTICS_CONFIRMED"].includes(order.status)
|
||||
),
|
||||
shipping: filteredOrders.filter((order) =>
|
||||
["SHIPPED", "IN_TRANSIT"].includes(order.status)
|
||||
),
|
||||
completed: filteredOrders.filter((order) => order.status === "DELIVERED"),
|
||||
new: filteredOrders.filter((order) => order.status === 'PENDING'),
|
||||
approved: filteredOrders.filter((order) => order.status === 'SUPPLIER_APPROVED'),
|
||||
inProgress: filteredOrders.filter((order) => ['CONFIRMED', 'LOGISTICS_CONFIRMED'].includes(order.status)),
|
||||
shipping: filteredOrders.filter((order) => ['SHIPPED', 'IN_TRANSIT'].includes(order.status)),
|
||||
completed: filteredOrders.filter((order) => order.status === 'DELIVERED'),
|
||||
all: filteredOrders,
|
||||
};
|
||||
}, [filteredOrders]);
|
||||
}
|
||||
}, [filteredOrders])
|
||||
|
||||
const getTabBadgeCount = (tabKey: string) => {
|
||||
return ordersByStatus[tabKey as keyof typeof ordersByStatus]?.length || 0;
|
||||
};
|
||||
return ordersByStatus[tabKey as keyof typeof ordersByStatus]?.length || 0
|
||||
}
|
||||
|
||||
const getCurrentOrders = () => {
|
||||
return ordersByStatus[activeTab as keyof typeof ordersByStatus] || [];
|
||||
};
|
||||
return ordersByStatus[activeTab as keyof typeof ordersByStatus] || []
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<div className="text-white/60">Загрузка заявок...</div>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<div className="text-red-400">
|
||||
Ошибка загрузки заявок: {error.message}
|
||||
</div>
|
||||
<div className="text-red-400">Ошибка загрузки заявок: {error.message}</div>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
@ -197,10 +177,8 @@ export function SupplierOrdersTabs() {
|
||||
>
|
||||
<Clock className="h-4 w-4 mr-2" />
|
||||
Новые
|
||||
{getTabBadgeCount("new") > 0 && (
|
||||
<Badge className="ml-2 bg-red-500/20 text-red-300 border-red-400/30">
|
||||
{getTabBadgeCount("new")}
|
||||
</Badge>
|
||||
{getTabBadgeCount('new') > 0 && (
|
||||
<Badge className="ml-2 bg-red-500/20 text-red-300 border-red-400/30">{getTabBadgeCount('new')}</Badge>
|
||||
)}
|
||||
</TabsTrigger>
|
||||
|
||||
@ -210,9 +188,9 @@ export function SupplierOrdersTabs() {
|
||||
>
|
||||
<CheckCircle className="h-4 w-4 mr-2" />
|
||||
Одобренные
|
||||
{getTabBadgeCount("approved") > 0 && (
|
||||
{getTabBadgeCount('approved') > 0 && (
|
||||
<Badge className="ml-2 bg-green-500/20 text-green-300 border-green-400/30">
|
||||
{getTabBadgeCount("approved")}
|
||||
{getTabBadgeCount('approved')}
|
||||
</Badge>
|
||||
)}
|
||||
</TabsTrigger>
|
||||
@ -222,9 +200,9 @@ export function SupplierOrdersTabs() {
|
||||
className="h-9 bg-white/8 border-white/20 rounded-lg font-medium ml-4 data-[state=active]:bg-white/15 data-[state=active]:text-white"
|
||||
>
|
||||
<Settings className="h-4 w-4 mr-2" />В работе
|
||||
{getTabBadgeCount("inProgress") > 0 && (
|
||||
{getTabBadgeCount('inProgress') > 0 && (
|
||||
<Badge className="ml-2 bg-blue-500/20 text-blue-300 border-blue-400/30">
|
||||
{getTabBadgeCount("inProgress")}
|
||||
{getTabBadgeCount('inProgress')}
|
||||
</Badge>
|
||||
)}
|
||||
</TabsTrigger>
|
||||
@ -235,9 +213,9 @@ export function SupplierOrdersTabs() {
|
||||
>
|
||||
<Truck className="h-4 w-4 mr-2" />
|
||||
Отгрузка
|
||||
{getTabBadgeCount("shipping") > 0 && (
|
||||
{getTabBadgeCount('shipping') > 0 && (
|
||||
<Badge className="ml-2 bg-orange-500/20 text-orange-300 border-orange-400/30">
|
||||
{getTabBadgeCount("shipping")}
|
||||
{getTabBadgeCount('shipping')}
|
||||
</Badge>
|
||||
)}
|
||||
</TabsTrigger>
|
||||
@ -248,9 +226,9 @@ export function SupplierOrdersTabs() {
|
||||
>
|
||||
<Package className="h-4 w-4 mr-2" />
|
||||
Завершенные
|
||||
{getTabBadgeCount("completed") > 0 && (
|
||||
{getTabBadgeCount('completed') > 0 && (
|
||||
<Badge className="ml-2 bg-emerald-500/20 text-emerald-300 border-emerald-400/30">
|
||||
{getTabBadgeCount("completed")}
|
||||
{getTabBadgeCount('completed')}
|
||||
</Badge>
|
||||
)}
|
||||
</TabsTrigger>
|
||||
@ -260,10 +238,8 @@ export function SupplierOrdersTabs() {
|
||||
className="h-9 bg-white/8 border-white/20 rounded-lg font-medium ml-4 data-[state=active]:bg-white/15 data-[state=active]:text-white"
|
||||
>
|
||||
Все заявки
|
||||
{getTabBadgeCount("all") > 0 && (
|
||||
<Badge className="ml-2 bg-white/20 text-white/70 border-white/30">
|
||||
{getTabBadgeCount("all")}
|
||||
</Badge>
|
||||
{getTabBadgeCount('all') > 0 && (
|
||||
<Badge className="ml-2 bg-white/20 text-white/70 border-white/30">{getTabBadgeCount('all')}</Badge>
|
||||
)}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
@ -287,12 +263,12 @@ export function SupplierOrdersTabs() {
|
||||
<div className="text-center py-12">
|
||||
<Package className="h-12 w-12 text-white/40 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-semibold text-white mb-2">
|
||||
{activeTab === "new" ? "Нет новых заявок" : "Заявки не найдены"}
|
||||
{activeTab === 'new' ? 'Нет новых заявок' : 'Заявки не найдены'}
|
||||
</h3>
|
||||
<p className="text-white/60">
|
||||
{activeTab === "new"
|
||||
? "Новые заявки от заказчиков будут отображаться здесь"
|
||||
: "Попробуйте изменить фильтры поиска"}
|
||||
{activeTab === 'new'
|
||||
? 'Новые заявки от заказчиков будут отображаться здесь'
|
||||
: 'Попробуйте изменить фильтры поиска'}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
@ -305,5 +281,5 @@ export function SupplierOrdersTabs() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
Reference in New Issue
Block a user