Добавлен заголовок и кнопка создания поставки в компонент FulfillmentDetailedSuppliesTab. Реализовано перенаправление на страницу создания поставки расходников. Оптимизированы стили и структура кода для улучшения пользовательского интерфейса.
This commit is contained in:
10
src/app/fulfillment-supplies/create-consumables/page.tsx
Normal file
10
src/app/fulfillment-supplies/create-consumables/page.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import { AuthGuard } from "@/components/auth-guard";
|
||||
import { CreateFulfillmentConsumablesSupplyPage } from "@/components/fulfillment-supplies/create-fulfillment-consumables-supply-page";
|
||||
|
||||
export default function CreateFulfillmentConsumablesSupplyPageRoute() {
|
||||
return (
|
||||
<AuthGuard>
|
||||
<CreateFulfillmentConsumablesSupplyPage />
|
||||
</AuthGuard>
|
||||
);
|
||||
}
|
@ -0,0 +1,769 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useQuery, useMutation } from "@apollo/client";
|
||||
import { Sidebar } from "@/components/dashboard/sidebar";
|
||||
import { useSidebar } from "@/hooks/useSidebar";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
ArrowLeft,
|
||||
Building2,
|
||||
MapPin,
|
||||
Phone,
|
||||
Mail,
|
||||
Star,
|
||||
Search,
|
||||
Package,
|
||||
Plus,
|
||||
Minus,
|
||||
ShoppingCart,
|
||||
Wrench,
|
||||
Box,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
GET_MY_COUNTERPARTIES,
|
||||
GET_ALL_PRODUCTS,
|
||||
GET_SUPPLY_ORDERS,
|
||||
} from "@/graphql/queries";
|
||||
import { CREATE_SUPPLY_ORDER } from "@/graphql/mutations";
|
||||
import { OrganizationAvatar } from "@/components/market/organization-avatar";
|
||||
import { toast } from "sonner";
|
||||
import Image from "next/image";
|
||||
|
||||
interface FulfillmentConsumableSupplier {
|
||||
id: string;
|
||||
inn: string;
|
||||
name?: string;
|
||||
fullName?: string;
|
||||
type: "FULFILLMENT" | "SELLER" | "LOGIST" | "WHOLESALE";
|
||||
address?: string;
|
||||
phones?: Array<{ value: string }>;
|
||||
emails?: Array<{ value: string }>;
|
||||
users?: Array<{ id: string; avatar?: string; managerName?: string }>;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
interface FulfillmentConsumableProduct {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
price: number;
|
||||
category?: { name: string };
|
||||
images: string[];
|
||||
mainImage?: string;
|
||||
organization: {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
stock?: number;
|
||||
unit?: string;
|
||||
}
|
||||
|
||||
interface SelectedFulfillmentConsumable {
|
||||
id: string;
|
||||
name: string;
|
||||
price: number;
|
||||
selectedQuantity: number;
|
||||
unit?: string;
|
||||
category?: string;
|
||||
supplierId: string;
|
||||
supplierName: string;
|
||||
}
|
||||
|
||||
export function CreateFulfillmentConsumablesSupplyPage() {
|
||||
const router = useRouter();
|
||||
const { getSidebarMargin } = useSidebar();
|
||||
const [selectedSupplier, setSelectedSupplier] =
|
||||
useState<FulfillmentConsumableSupplier | null>(null);
|
||||
const [selectedConsumables, setSelectedConsumables] = useState<
|
||||
SelectedFulfillmentConsumable[]
|
||||
>([]);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [productSearchQuery, setProductSearchQuery] = useState("");
|
||||
const [deliveryDate, setDeliveryDate] = useState("");
|
||||
const [isCreatingSupply, setIsCreatingSupply] = useState(false);
|
||||
|
||||
// Загружаем контрагентов-поставщиков расходников
|
||||
const { data: counterpartiesData, loading: counterpartiesLoading } = useQuery(
|
||||
GET_MY_COUNTERPARTIES
|
||||
);
|
||||
|
||||
// Загружаем товары для выбранного поставщика
|
||||
const { data: productsData, loading: productsLoading } = useQuery(
|
||||
GET_ALL_PRODUCTS,
|
||||
{
|
||||
skip: !selectedSupplier,
|
||||
variables: { search: productSearchQuery || null, category: null },
|
||||
}
|
||||
);
|
||||
|
||||
// Мутация для создания заказа поставки расходников
|
||||
const [createSupplyOrder] = useMutation(CREATE_SUPPLY_ORDER);
|
||||
|
||||
// Фильтруем только поставщиков расходников (оптовиков)
|
||||
const consumableSuppliers = (
|
||||
counterpartiesData?.myCounterparties || []
|
||||
).filter((org: FulfillmentConsumableSupplier) => org.type === "WHOLESALE");
|
||||
|
||||
// Фильтруем поставщиков по поисковому запросу
|
||||
const filteredSuppliers = consumableSuppliers.filter(
|
||||
(supplier: FulfillmentConsumableSupplier) =>
|
||||
supplier.name?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
supplier.fullName?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
supplier.inn?.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
|
||||
// Фильтруем товары по выбранному поставщику
|
||||
const supplierProducts = selectedSupplier
|
||||
? (productsData?.allProducts || []).filter(
|
||||
(product: FulfillmentConsumableProduct) =>
|
||||
product.organization.id === selectedSupplier.id
|
||||
)
|
||||
: [];
|
||||
|
||||
const formatCurrency = (amount: number) => {
|
||||
return new Intl.NumberFormat("ru-RU", {
|
||||
style: "currency",
|
||||
currency: "RUB",
|
||||
minimumFractionDigits: 0,
|
||||
}).format(amount);
|
||||
};
|
||||
|
||||
const renderStars = (rating: number = 4.5) => {
|
||||
return Array.from({ length: 5 }, (_, i) => (
|
||||
<Star
|
||||
key={i}
|
||||
className={`h-3 w-3 ${
|
||||
i < Math.floor(rating)
|
||||
? "text-yellow-400 fill-current"
|
||||
: "text-gray-400"
|
||||
}`}
|
||||
/>
|
||||
));
|
||||
};
|
||||
|
||||
const updateConsumableQuantity = (productId: string, quantity: number) => {
|
||||
const product = supplierProducts.find(
|
||||
(p: FulfillmentConsumableProduct) => p.id === productId
|
||||
);
|
||||
if (!product || !selectedSupplier) return;
|
||||
|
||||
setSelectedConsumables((prev) => {
|
||||
const existing = prev.find((p) => p.id === productId);
|
||||
|
||||
if (quantity === 0) {
|
||||
// Удаляем расходник если количество 0
|
||||
return prev.filter((p) => p.id !== productId);
|
||||
}
|
||||
|
||||
if (existing) {
|
||||
// Обновляем количество существующего расходника
|
||||
return prev.map((p) =>
|
||||
p.id === productId ? { ...p, selectedQuantity: quantity } : p
|
||||
);
|
||||
} else {
|
||||
// Добавляем новый расходник
|
||||
return [
|
||||
...prev,
|
||||
{
|
||||
id: product.id,
|
||||
name: product.name,
|
||||
price: product.price,
|
||||
selectedQuantity: quantity,
|
||||
unit: product.unit || "шт",
|
||||
category: product.category?.name || "Расходники",
|
||||
supplierId: selectedSupplier.id,
|
||||
supplierName:
|
||||
selectedSupplier.name || selectedSupplier.fullName || "Поставщик",
|
||||
},
|
||||
];
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const getSelectedQuantity = (productId: string): number => {
|
||||
const selected = selectedConsumables.find((p) => p.id === productId);
|
||||
return selected ? selected.selectedQuantity : 0;
|
||||
};
|
||||
|
||||
const getTotalAmount = () => {
|
||||
return selectedConsumables.reduce(
|
||||
(sum, consumable) => sum + consumable.price * consumable.selectedQuantity,
|
||||
0
|
||||
);
|
||||
};
|
||||
|
||||
const getTotalItems = () => {
|
||||
return selectedConsumables.reduce(
|
||||
(sum, consumable) => sum + consumable.selectedQuantity,
|
||||
0
|
||||
);
|
||||
};
|
||||
|
||||
const handleCreateSupply = async () => {
|
||||
if (
|
||||
!selectedSupplier ||
|
||||
selectedConsumables.length === 0 ||
|
||||
!deliveryDate
|
||||
) {
|
||||
toast.error("Заполните все обязательные поля");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsCreatingSupply(true);
|
||||
|
||||
try {
|
||||
const result = await createSupplyOrder({
|
||||
variables: {
|
||||
input: {
|
||||
partnerId: selectedSupplier.id,
|
||||
deliveryDate: deliveryDate,
|
||||
// Для фулфилмента не требуется выбор фулфилмент-центра, поставка идет на свой склад
|
||||
items: selectedConsumables.map((consumable) => ({
|
||||
productId: consumable.id,
|
||||
quantity: consumable.selectedQuantity,
|
||||
})),
|
||||
},
|
||||
},
|
||||
refetchQueries: [{ query: GET_SUPPLY_ORDERS }],
|
||||
});
|
||||
|
||||
if (result.data?.createSupplyOrder?.success) {
|
||||
toast.success("Заказ поставки расходников фулфилмента создан успешно!");
|
||||
// Очищаем форму
|
||||
setSelectedSupplier(null);
|
||||
setSelectedConsumables([]);
|
||||
setDeliveryDate("");
|
||||
setProductSearchQuery("");
|
||||
setSearchQuery("");
|
||||
|
||||
// Перенаправляем на страницу поставок фулфилмента
|
||||
router.push("/fulfillment-supplies");
|
||||
} else {
|
||||
toast.error(
|
||||
result.data?.createSupplyOrder?.message ||
|
||||
"Ошибка при создании заказа поставки"
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error creating fulfillment consumables supply:", error);
|
||||
toast.error("Ошибка при создании поставки расходников фулфилмента");
|
||||
} finally {
|
||||
setIsCreatingSupply(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-screen flex overflow-hidden">
|
||||
<Sidebar />
|
||||
<main
|
||||
className={`flex-1 ${getSidebarMargin()} overflow-auto transition-all duration-300`}
|
||||
>
|
||||
<div className="min-h-full w-full flex flex-col px-3 py-2">
|
||||
{/* Заголовок */}
|
||||
<div className="flex items-center justify-between mb-3 flex-shrink-0">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-white mb-1">
|
||||
Создание поставки расходников фулфилмента
|
||||
</h1>
|
||||
<p className="text-white/60 text-sm">
|
||||
Выберите поставщика и добавьте расходники в заказ для вашего
|
||||
фулфилмент-центра
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => router.push("/fulfillment-supplies")}
|
||||
className="text-white/60 hover:text-white hover:bg-white/10 text-sm"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4 mr-1" />
|
||||
Назад
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Основной контент с двумя блоками */}
|
||||
<div className="flex-1 flex gap-3 min-h-0">
|
||||
{/* Левая колонка - Поставщики и Расходники */}
|
||||
<div className="flex-1 flex flex-col gap-3 min-h-0">
|
||||
{/* Блок "Поставщики" */}
|
||||
<Card className="bg-gradient-to-r from-white/15 via-white/10 to-white/15 backdrop-blur-xl border border-white/30 shadow-2xl flex-shrink-0 sticky top-0 z-10 rounded-xl overflow-hidden">
|
||||
<div className="p-3 bg-gradient-to-r from-purple-500/10 to-pink-500/10">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<h2 className="text-lg font-bold text-white flex items-center flex-shrink-0 bg-gradient-to-r from-purple-400 to-pink-400 bg-clip-text text-transparent">
|
||||
<Building2 className="h-5 w-5 mr-3 text-purple-400" />
|
||||
Поставщики расходников
|
||||
</h2>
|
||||
<div className="relative flex-1 max-w-sm">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-purple-300 h-4 w-4 z-10" />
|
||||
<Input
|
||||
placeholder="Найти поставщика..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="bg-white/20 backdrop-blur border-white/30 text-white placeholder-white/50 pl-10 h-8 text-sm rounded-full shadow-inner focus:ring-2 focus:ring-purple-400/50 focus:border-purple-400/50 transition-all duration-300"
|
||||
/>
|
||||
</div>
|
||||
{selectedSupplier && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setSelectedSupplier(null)}
|
||||
className="text-white/70 hover:text-white hover:bg-white/20 text-sm h-8 px-3 flex-shrink-0 rounded-full transition-all duration-300 hover:scale-105"
|
||||
>
|
||||
✕ Сбросить
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="px-3 pb-3 h-24 overflow-hidden">
|
||||
{counterpartiesLoading ? (
|
||||
<div className="text-center py-4">
|
||||
<div className="animate-spin rounded-full h-6 w-6 border-2 border-purple-400 border-t-transparent mx-auto mb-2"></div>
|
||||
<p className="text-white/70 text-sm font-medium">
|
||||
Загружаем поставщиков...
|
||||
</p>
|
||||
</div>
|
||||
) : filteredSuppliers.length === 0 ? (
|
||||
<div className="text-center py-4">
|
||||
<div className="bg-gradient-to-br from-purple-500/20 to-pink-500/20 rounded-full p-3 w-fit mx-auto mb-2">
|
||||
<Building2 className="h-6 w-6 text-purple-300" />
|
||||
</div>
|
||||
<p className="text-white/70 text-sm font-medium">
|
||||
{searchQuery
|
||||
? "Поставщики не найдены"
|
||||
: "Добавьте поставщиков"}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex gap-2 h-full pt-1">
|
||||
{filteredSuppliers
|
||||
.slice(0, 7)
|
||||
.map(
|
||||
(supplier: FulfillmentConsumableSupplier, index) => (
|
||||
<Card
|
||||
key={supplier.id}
|
||||
className={`relative cursor-pointer transition-all duration-300 border flex-shrink-0 rounded-xl overflow-hidden group hover:scale-105 hover:shadow-xl ${
|
||||
selectedSupplier?.id === supplier.id
|
||||
? "bg-gradient-to-br from-orange-500/30 via-orange-400/20 to-orange-500/30 border-orange-400/60 shadow-lg shadow-orange-500/25"
|
||||
: "bg-gradient-to-br from-white/10 via-white/5 to-white/10 border-white/20 hover:from-white/20 hover:via-white/10 hover:to-white/20 hover:border-white/40"
|
||||
}`}
|
||||
style={{
|
||||
width: "calc((100% - 48px) / 7)", // 48px = 6 gaps * 8px each
|
||||
animationDelay: `${index * 100}ms`,
|
||||
}}
|
||||
onClick={() => setSelectedSupplier(supplier)}
|
||||
>
|
||||
<div className="flex flex-col items-center justify-center h-full p-2 space-y-1">
|
||||
<div className="relative">
|
||||
<OrganizationAvatar
|
||||
organization={{
|
||||
id: supplier.id,
|
||||
name:
|
||||
supplier.name ||
|
||||
supplier.fullName ||
|
||||
"Поставщик",
|
||||
fullName: supplier.fullName,
|
||||
users: (supplier.users || []).map(
|
||||
(user) => ({
|
||||
id: user.id,
|
||||
avatar: user.avatar,
|
||||
})
|
||||
),
|
||||
}}
|
||||
size="sm"
|
||||
/>
|
||||
{selectedSupplier?.id === supplier.id && (
|
||||
<div className="absolute -top-1 -right-1 bg-gradient-to-r from-orange-400 to-orange-500 rounded-full w-4 h-4 flex items-center justify-center shadow-lg animate-pulse">
|
||||
<span className="text-white text-xs font-bold">
|
||||
✓
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-center w-full space-y-0.5">
|
||||
<h3 className="text-white font-semibold text-xs truncate leading-tight group-hover:text-purple-200 transition-colors duration-300">
|
||||
{(
|
||||
supplier.name ||
|
||||
supplier.fullName ||
|
||||
"Поставщик"
|
||||
).slice(0, 10)}
|
||||
</h3>
|
||||
<div className="flex items-center justify-center space-x-1">
|
||||
<span className="text-yellow-400 text-sm animate-pulse">
|
||||
★
|
||||
</span>
|
||||
<span className="text-white/80 text-xs font-medium">
|
||||
4.5
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-full bg-white/10 rounded-full h-1 overflow-hidden">
|
||||
<div
|
||||
className="bg-gradient-to-r from-purple-400 to-pink-400 h-full rounded-full animate-pulse"
|
||||
style={{ width: "90%" }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Hover эффект */}
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-purple-500/0 to-pink-500/0 group-hover:from-purple-500/10 group-hover:to-pink-500/10 transition-all duration-300 pointer-events-none"></div>
|
||||
</Card>
|
||||
)
|
||||
)}
|
||||
{filteredSuppliers.length > 7 && (
|
||||
<div
|
||||
className="flex-shrink-0 flex flex-col items-center justify-center bg-gradient-to-br from-white/10 to-white/5 rounded-xl border border-white/20 text-white/70 hover:text-white transition-all duration-300 hover:scale-105"
|
||||
style={{ width: "calc((100% - 48px) / 7)" }}
|
||||
>
|
||||
<div className="text-lg font-bold text-purple-300">
|
||||
+{filteredSuppliers.length - 7}
|
||||
</div>
|
||||
<div className="text-xs">ещё</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Блок "Расходники" */}
|
||||
<Card className="bg-white/10 backdrop-blur border-white/20 flex-1 min-h-0 flex flex-col">
|
||||
<div className="p-3 border-b border-white/10 flex-shrink-0">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h2 className="text-lg font-semibold text-white flex items-center">
|
||||
<Wrench className="h-4 w-4 mr-2" />
|
||||
Расходники для фулфилмента
|
||||
{selectedSupplier && (
|
||||
<span className="text-white/60 text-xs font-normal ml-2 truncate">
|
||||
- {selectedSupplier.name || selectedSupplier.fullName}
|
||||
</span>
|
||||
)}
|
||||
</h2>
|
||||
</div>
|
||||
{selectedSupplier && (
|
||||
<div className="relative">
|
||||
<Search className="absolute left-2 top-1/2 transform -translate-y-1/2 text-white/40 h-3 w-3" />
|
||||
<Input
|
||||
placeholder="Поиск расходников..."
|
||||
value={productSearchQuery}
|
||||
onChange={(e) => setProductSearchQuery(e.target.value)}
|
||||
className="bg-white/10 border-white/20 text-white placeholder-white/40 pl-7 h-8 text-sm"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="p-3 flex-1 overflow-y-auto">
|
||||
{!selectedSupplier ? (
|
||||
<div className="text-center py-8">
|
||||
<Wrench className="h-8 w-8 text-white/40 mx-auto mb-3" />
|
||||
<p className="text-white/60 text-sm">
|
||||
Выберите поставщика для просмотра расходников
|
||||
</p>
|
||||
</div>
|
||||
) : productsLoading ? (
|
||||
<div className="text-center py-8">
|
||||
<div className="animate-spin rounded-full h-6 w-6 border-2 border-white border-t-transparent mx-auto mb-2"></div>
|
||||
<p className="text-white/60 text-sm">Загрузка...</p>
|
||||
</div>
|
||||
) : supplierProducts.length === 0 ? (
|
||||
<div className="text-center py-8">
|
||||
<Package className="h-8 w-8 text-white/40 mx-auto mb-3" />
|
||||
<p className="text-white/60 text-sm">
|
||||
Нет доступных расходников
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 xl:grid-cols-7 gap-3">
|
||||
{supplierProducts.map(
|
||||
(product: FulfillmentConsumableProduct, index) => {
|
||||
const selectedQuantity = getSelectedQuantity(
|
||||
product.id
|
||||
);
|
||||
return (
|
||||
<Card
|
||||
key={product.id}
|
||||
className={`relative bg-gradient-to-br from-white/10 via-white/5 to-white/10 backdrop-blur border border-white/20 p-3 rounded-xl overflow-hidden group hover:shadow-xl transition-all duration-300 ${
|
||||
selectedQuantity > 0
|
||||
? "ring-2 ring-green-400/50 bg-gradient-to-br from-green-500/20 via-green-400/10 to-green-500/20"
|
||||
: "hover:from-white/20 hover:via-white/10 hover:to-white/20 hover:border-white/40"
|
||||
}`}
|
||||
style={{
|
||||
animationDelay: `${index * 50}ms`,
|
||||
minHeight: "200px",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<div className="space-y-2 h-full flex flex-col">
|
||||
{/* Изображение товара */}
|
||||
<div className="aspect-square bg-white/5 rounded-lg overflow-hidden relative flex-shrink-0">
|
||||
{product.images &&
|
||||
product.images.length > 0 &&
|
||||
product.images[0] ? (
|
||||
<Image
|
||||
src={product.images[0]}
|
||||
alt={product.name}
|
||||
width={100}
|
||||
height={100}
|
||||
className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-300"
|
||||
/>
|
||||
) : product.mainImage ? (
|
||||
<Image
|
||||
src={product.mainImage}
|
||||
alt={product.name}
|
||||
width={100}
|
||||
height={100}
|
||||
className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-300"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full h-full flex items-center justify-center">
|
||||
<Wrench className="h-8 w-8 text-white/40" />
|
||||
</div>
|
||||
)}
|
||||
{selectedQuantity > 0 && (
|
||||
<div className="absolute top-2 right-2 bg-gradient-to-r from-green-400 to-green-500 rounded-full w-6 h-6 flex items-center justify-center shadow-lg animate-pulse">
|
||||
<span className="text-white text-xs font-bold">
|
||||
{selectedQuantity > 999
|
||||
? "999+"
|
||||
: selectedQuantity}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Информация о товаре */}
|
||||
<div className="space-y-1 flex-grow">
|
||||
<h3 className="text-white font-medium text-sm leading-tight line-clamp-2 group-hover:text-purple-200 transition-colors duration-300">
|
||||
{product.name}
|
||||
</h3>
|
||||
{product.category && (
|
||||
<Badge className="bg-purple-500/20 text-purple-300 border-purple-500/30 text-xs px-2 py-1">
|
||||
{product.category.name.slice(0, 10)}
|
||||
</Badge>
|
||||
)}
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-green-400 font-semibold text-sm">
|
||||
{formatCurrency(product.price)}
|
||||
</span>
|
||||
{product.stock && (
|
||||
<span className="text-white/60 text-xs">
|
||||
{product.stock}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Управление количеством */}
|
||||
<div className="flex flex-col items-center space-y-2 mt-auto">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
updateConsumableQuantity(
|
||||
product.id,
|
||||
Math.max(0, selectedQuantity - 1)
|
||||
)
|
||||
}
|
||||
className="h-6 w-6 p-0 text-white/60 hover:text-white hover:bg-white/20 rounded-full transition-all duration-300"
|
||||
disabled={selectedQuantity === 0}
|
||||
>
|
||||
<Minus className="h-3 w-3" />
|
||||
</Button>
|
||||
<Input
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
pattern="[0-9]*"
|
||||
value={
|
||||
selectedQuantity === 0
|
||||
? ""
|
||||
: selectedQuantity.toString()
|
||||
}
|
||||
onChange={(e) => {
|
||||
let inputValue = e.target.value;
|
||||
|
||||
// Удаляем все нецифровые символы
|
||||
inputValue = inputValue.replace(
|
||||
/[^0-9]/g,
|
||||
""
|
||||
);
|
||||
|
||||
// Удаляем ведущие нули
|
||||
inputValue = inputValue.replace(
|
||||
/^0+/,
|
||||
""
|
||||
);
|
||||
|
||||
// Если строка пустая после удаления нулей, устанавливаем 0
|
||||
const numericValue =
|
||||
inputValue === ""
|
||||
? 0
|
||||
: parseInt(inputValue);
|
||||
|
||||
// Ограничиваем значение максимумом 99999
|
||||
const clampedValue = Math.min(
|
||||
numericValue,
|
||||
99999
|
||||
);
|
||||
|
||||
updateConsumableQuantity(
|
||||
product.id,
|
||||
clampedValue
|
||||
);
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
// При потере фокуса, если поле пустое, устанавливаем 0
|
||||
if (e.target.value === "") {
|
||||
updateConsumableQuantity(
|
||||
product.id,
|
||||
0
|
||||
);
|
||||
}
|
||||
}}
|
||||
className="w-16 h-7 text-center text-sm bg-white/10 border-white/20 text-white rounded px-1 focus:ring-2 focus:ring-purple-400/50 focus:border-purple-400/50"
|
||||
placeholder="0"
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
updateConsumableQuantity(
|
||||
product.id,
|
||||
Math.min(selectedQuantity + 1, 99999)
|
||||
)
|
||||
}
|
||||
className="h-6 w-6 p-0 text-white/60 hover:text-white hover:bg-white/20 rounded-full transition-all duration-300"
|
||||
>
|
||||
<Plus className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{selectedQuantity > 0 && (
|
||||
<div className="text-center">
|
||||
<span className="text-green-400 font-bold text-sm bg-green-500/10 px-3 py-1 rounded-full">
|
||||
{formatCurrency(
|
||||
product.price * selectedQuantity
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Hover эффект */}
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-purple-500/0 to-pink-500/0 group-hover:from-purple-500/5 group-hover:to-pink-500/5 transition-all duration-300 pointer-events-none rounded-xl"></div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Правая колонка - Корзина */}
|
||||
<div className="w-72 flex-shrink-0">
|
||||
<Card className="bg-white/10 backdrop-blur border-white/20 p-3 sticky top-0">
|
||||
<h3 className="text-white font-semibold mb-3 flex items-center text-sm">
|
||||
<ShoppingCart className="h-4 w-4 mr-2" />
|
||||
Корзина ({getTotalItems()} шт)
|
||||
</h3>
|
||||
|
||||
{selectedConsumables.length === 0 ? (
|
||||
<div className="text-center py-6">
|
||||
<div className="bg-gradient-to-br from-purple-500/20 to-pink-500/20 rounded-full p-4 w-fit mx-auto mb-3">
|
||||
<ShoppingCart className="h-8 w-8 text-purple-300" />
|
||||
</div>
|
||||
<p className="text-white/60 text-sm font-medium mb-2">
|
||||
Корзина пуста
|
||||
</p>
|
||||
<p className="text-white/40 text-xs mb-3">
|
||||
Добавьте расходники для создания поставки
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2 mb-3 max-h-48 overflow-y-auto">
|
||||
{selectedConsumables.map((consumable) => (
|
||||
<div
|
||||
key={consumable.id}
|
||||
className="flex items-center justify-between p-2 bg-white/5 rounded-lg"
|
||||
>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-white text-xs font-medium truncate">
|
||||
{consumable.name}
|
||||
</p>
|
||||
<p className="text-white/60 text-xs">
|
||||
{formatCurrency(consumable.price)} ×{" "}
|
||||
{consumable.selectedQuantity}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-green-400 font-medium text-xs">
|
||||
{formatCurrency(
|
||||
consumable.price * consumable.selectedQuantity
|
||||
)}
|
||||
</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
updateConsumableQuantity(consumable.id, 0)
|
||||
}
|
||||
className="h-5 w-5 p-0 text-red-400 hover:text-red-300 hover:bg-red-500/10"
|
||||
>
|
||||
×
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="border-t border-white/20 pt-3">
|
||||
<div className="mb-3">
|
||||
<label className="text-white/60 text-xs mb-1 block">
|
||||
Дата поставки:
|
||||
</label>
|
||||
<Input
|
||||
type="date"
|
||||
value={deliveryDate}
|
||||
onChange={(e) => setDeliveryDate(e.target.value)}
|
||||
className="bg-white/10 border-white/20 text-white h-8 text-sm"
|
||||
min={new Date().toISOString().split("T")[0]}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<span className="text-white font-semibold text-sm">
|
||||
Итого:
|
||||
</span>
|
||||
<span className="text-green-400 font-bold text-lg">
|
||||
{formatCurrency(getTotalAmount())}
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
onClick={handleCreateSupply}
|
||||
disabled={
|
||||
isCreatingSupply ||
|
||||
!deliveryDate ||
|
||||
selectedConsumables.length === 0
|
||||
}
|
||||
className="w-full bg-gradient-to-r from-purple-500 to-pink-500 hover:from-purple-600 hover:to-pink-600 text-white disabled:opacity-50 h-8 text-sm"
|
||||
>
|
||||
{isCreatingSupply ? "Создание..." : "Создать поставку"}
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -6,6 +6,7 @@ import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { StatsCard } from "../../supplies/ui/stats-card";
|
||||
import { StatsGrid } from "../../supplies/ui/stats-grid";
|
||||
import { useRouter } from "next/navigation";
|
||||
import {
|
||||
Calendar,
|
||||
MapPin,
|
||||
@ -17,6 +18,7 @@ import {
|
||||
Box,
|
||||
Package2,
|
||||
Tags,
|
||||
Plus,
|
||||
} from "lucide-react";
|
||||
|
||||
// Типы данных для расходников ФФ детально
|
||||
@ -213,6 +215,7 @@ const mockFulfillmentConsumablesDetailed: FulfillmentConsumableSupply[] = [
|
||||
];
|
||||
|
||||
export function FulfillmentDetailedSuppliesTab() {
|
||||
const router = useRouter();
|
||||
const [expandedSupplies, setExpandedSupplies] = useState<Set<string>>(
|
||||
new Set()
|
||||
);
|
||||
@ -336,6 +339,25 @@ export function FulfillmentDetailedSuppliesTab() {
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Заголовок с кнопкой создания поставки */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-white mb-1">
|
||||
Наши расходники
|
||||
</h2>
|
||||
<p className="text-white/60 text-sm">
|
||||
Управление поставками расходников фулфилмента
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => router.push("/fulfillment-supplies/create-consumables")}
|
||||
className="bg-gradient-to-r from-purple-500 to-pink-500 hover:from-purple-600 hover:to-pink-600 text-white shadow-lg"
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Создать поставку
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Статистика расходников ФФ детально */}
|
||||
<StatsGrid>
|
||||
<StatsCard
|
||||
@ -427,7 +449,7 @@ export function FulfillmentDetailedSuppliesTab() {
|
||||
return (
|
||||
<React.Fragment key={supply.id}>
|
||||
{/* Основная строка поставки расходников ФФ детально */}
|
||||
<tr
|
||||
<tr
|
||||
className="border-b border-white/10 hover:bg-white/5 transition-colors bg-orange-500/10 cursor-pointer"
|
||||
onClick={() => toggleSupplyExpansion(supply.id)}
|
||||
>
|
||||
@ -488,7 +510,7 @@ export function FulfillmentDetailedSuppliesTab() {
|
||||
const isRouteExpanded = expandedRoutes.has(route.id);
|
||||
return (
|
||||
<React.Fragment key={route.id}>
|
||||
<tr
|
||||
<tr
|
||||
className="border-b border-white/10 hover:bg-white/5 transition-colors bg-blue-500/10 cursor-pointer"
|
||||
onClick={() => toggleRouteExpansion(route.id)}
|
||||
>
|
||||
@ -580,9 +602,11 @@ export function FulfillmentDetailedSuppliesTab() {
|
||||
expandedSuppliers.has(supplier.id);
|
||||
return (
|
||||
<React.Fragment key={supplier.id}>
|
||||
<tr
|
||||
<tr
|
||||
className="border-b border-white/10 hover:bg-white/5 transition-colors bg-green-500/10 cursor-pointer"
|
||||
onClick={() => toggleSupplierExpansion(supplier.id)}
|
||||
onClick={() =>
|
||||
toggleSupplierExpansion(supplier.id)
|
||||
}
|
||||
>
|
||||
<td className="p-4 pl-20">
|
||||
<div className="flex items-center space-x-2">
|
||||
@ -662,9 +686,13 @@ export function FulfillmentDetailedSuppliesTab() {
|
||||
);
|
||||
return (
|
||||
<React.Fragment key={consumable.id}>
|
||||
<tr
|
||||
<tr
|
||||
className="border-b border-white/10 hover:bg-white/5 transition-colors bg-yellow-500/10 cursor-pointer"
|
||||
onClick={() => toggleConsumableExpansion(consumable.id)}
|
||||
onClick={() =>
|
||||
toggleConsumableExpansion(
|
||||
consumable.id
|
||||
)
|
||||
}
|
||||
>
|
||||
<td className="p-4 pl-28">
|
||||
<div className="flex items-center space-x-2">
|
||||
|
Reference in New Issue
Block a user