Обновлены категории товаров с "Упаковка" на "Расходники" в различных компонентах и моделях. Добавлены уведомления о непринятых поставках и обновлены соответствующие GraphQL запросы и резолверы для поддержки новых данных. Оптимизирована логика отображения и обработки данных в интерфейсе.

This commit is contained in:
Veronika Smirnova
2025-07-28 13:19:19 +03:00
parent a1d1fcdd43
commit ac67b1e1ec
17 changed files with 1542 additions and 512 deletions

View File

@ -1,162 +1,190 @@
"use client"
"use client";
import { useState } from 'react'
import { useQuery, useMutation } from '@apollo/client'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from '@/components/ui/alert-dialog'
import { GET_CATEGORIES } from '@/graphql/queries'
import { CREATE_CATEGORY, UPDATE_CATEGORY, DELETE_CATEGORY } from '@/graphql/mutations'
import { Plus, Edit, Trash2, Package } from 'lucide-react'
import { toast } from 'sonner'
import { useState } from "react";
import { useQuery, useMutation } from "@apollo/client";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { GET_CATEGORIES } from "@/graphql/queries";
import {
CREATE_CATEGORY,
UPDATE_CATEGORY,
DELETE_CATEGORY,
} from "@/graphql/mutations";
import { Plus, Edit, Trash2, Package } from "lucide-react";
import { toast } from "sonner";
interface Category {
id: string
name: string
createdAt: string
updatedAt: string
id: string;
name: string;
createdAt: string;
updatedAt: string;
}
export function CategoriesSection() {
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false)
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false)
const [editingCategory, setEditingCategory] = useState<Category | null>(null)
const [newCategoryName, setNewCategoryName] = useState('')
const [editCategoryName, setEditCategoryName] = useState('')
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
const [editingCategory, setEditingCategory] = useState<Category | null>(null);
const [newCategoryName, setNewCategoryName] = useState("");
const [editCategoryName, setEditCategoryName] = useState("");
const formatDate = (dateString: string) => {
try {
const date = new Date(dateString)
const date = new Date(dateString);
if (isNaN(date.getTime())) {
return 'Неизвестно'
return "Неизвестно";
}
return date.toLocaleDateString('ru-RU', {
day: '2-digit',
month: '2-digit',
year: 'numeric'
})
return date.toLocaleDateString("ru-RU", {
day: "2-digit",
month: "2-digit",
year: "numeric",
});
} catch (error) {
return 'Неизвестно'
return "Неизвестно";
}
}
};
const { data, loading, error, refetch } = useQuery(GET_CATEGORIES)
const [createCategory, { loading: creating }] = useMutation(CREATE_CATEGORY)
const [updateCategory, { loading: updating }] = useMutation(UPDATE_CATEGORY)
const [deleteCategory, { loading: deleting }] = useMutation(DELETE_CATEGORY)
const { data, loading, error, refetch } = useQuery(GET_CATEGORIES);
const [createCategory, { loading: creating }] = useMutation(CREATE_CATEGORY);
const [updateCategory, { loading: updating }] = useMutation(UPDATE_CATEGORY);
const [deleteCategory, { loading: deleting }] = useMutation(DELETE_CATEGORY);
const categories: Category[] = data?.categories || []
const categories: Category[] = data?.categories || [];
const handleCreateCategory = async () => {
if (!newCategoryName.trim()) {
toast.error('Введите название категории')
return
toast.error("Введите название категории");
return;
}
try {
const { data } = await createCategory({
variables: { input: { name: newCategoryName.trim() } }
})
variables: { input: { name: newCategoryName.trim() } },
});
if (data?.createCategory?.success) {
toast.success('Категория успешно создана')
setNewCategoryName('')
setIsCreateDialogOpen(false)
refetch()
toast.success("Категория успешно создана");
setNewCategoryName("");
setIsCreateDialogOpen(false);
refetch();
} else {
toast.error(data?.createCategory?.message || 'Ошибка при создании категории')
toast.error(
data?.createCategory?.message || "Ошибка при создании категории"
);
}
} catch (error) {
console.error('Error creating category:', error)
toast.error('Ошибка при создании категории')
console.error("Error creating category:", error);
toast.error("Ошибка при создании категории");
}
}
};
const handleEditCategory = (category: Category) => {
setEditingCategory(category)
setEditCategoryName(category.name)
setIsEditDialogOpen(true)
}
setEditingCategory(category);
setEditCategoryName(category.name);
setIsEditDialogOpen(true);
};
const handleUpdateCategory = async () => {
if (!editingCategory || !editCategoryName.trim()) {
toast.error('Введите название категории')
return
toast.error("Введите название категории");
return;
}
try {
const { data } = await updateCategory({
variables: {
id: editingCategory.id,
input: { name: editCategoryName.trim() }
}
})
variables: {
id: editingCategory.id,
input: { name: editCategoryName.trim() },
},
});
if (data?.updateCategory?.success) {
toast.success('Категория успешно обновлена')
setEditingCategory(null)
setEditCategoryName('')
setIsEditDialogOpen(false)
refetch()
toast.success("Категория успешно обновлена");
setEditingCategory(null);
setEditCategoryName("");
setIsEditDialogOpen(false);
refetch();
} else {
toast.error(data?.updateCategory?.message || 'Ошибка при обновлении категории')
toast.error(
data?.updateCategory?.message || "Ошибка при обновлении категории"
);
}
} catch (error) {
console.error('Error updating category:', error)
toast.error('Ошибка при обновлении категории')
console.error("Error updating category:", error);
toast.error("Ошибка при обновлении категории");
}
}
};
const handleDeleteCategory = async (categoryId: string) => {
try {
const { data } = await deleteCategory({
variables: { id: categoryId }
})
variables: { id: categoryId },
});
if (data?.deleteCategory) {
toast.success('Категория успешно удалена')
refetch()
toast.success("Категория успешно удалена");
refetch();
} else {
toast.error('Ошибка при удалении категории')
toast.error("Ошибка при удалении категории");
}
} catch (error) {
console.error('Error deleting category:', error)
const errorMessage = error instanceof Error ? error.message : 'Ошибка при удалении категории'
toast.error(errorMessage)
console.error("Error deleting category:", error);
const errorMessage =
error instanceof Error
? error.message
: "Ошибка при удалении категории";
toast.error(errorMessage);
}
}
};
const handleCreateBasicCategories = async () => {
const basicCategories = [
'Электроника',
'Одежда',
'Обувь',
'Дом и сад',
'Красота и здоровье',
'Спорт и отдых',
'Автотовары',
'Детские товары',
'Продукты питания',
'Книги и канцелярия'
]
"Электроника",
"Одежда",
"Обувь",
"Дом и сад",
"Красота и здоровье",
"Спорт и отдых",
"Автотовары",
"Детские товары",
"Продукты питания",
"Книги и канцелярия",
"Расходники",
];
try {
for (const categoryName of basicCategories) {
await createCategory({
variables: { input: { name: categoryName } }
})
variables: { input: { name: categoryName } },
});
}
toast.success('Базовые категории созданы')
refetch()
toast.success("Базовые категории созданы");
refetch();
} catch (error) {
console.error('Error creating basic categories:', error)
toast.error('Ошибка при создании категорий')
console.error("Error creating basic categories:", error);
toast.error("Ошибка при создании категорий");
}
}
};
if (loading) {
return (
@ -170,7 +198,7 @@ export function CategoriesSection() {
</div>
</Card>
</div>
)
);
}
if (error) {
@ -182,13 +210,17 @@ export function CategoriesSection() {
<Card className="glass-card border-white/10 p-6">
<div className="text-center">
<p className="text-white/70 mb-4">Ошибка загрузки категорий</p>
<Button onClick={() => refetch()} variant="outline" className="bg-white/10 text-white border-white/20">
<Button
onClick={() => refetch()}
variant="outline"
className="bg-white/10 text-white border-white/20"
>
Попробовать снова
</Button>
</div>
</Card>
</div>
)
);
}
return (
@ -196,12 +228,14 @@ export function CategoriesSection() {
<div className="flex items-center justify-between">
<div>
<h2 className="text-2xl font-bold text-white">Категории товаров</h2>
<p className="text-white/70 text-sm">Управление категориями для классификации товаров</p>
<p className="text-white/70 text-sm">
Управление категориями для классификации товаров
</p>
</div>
<div className="flex gap-2">
{categories.length === 0 && (
<Button
<Button
onClick={handleCreateBasicCategories}
variant="outline"
className="bg-white/10 hover:bg-white/20 text-white border-white/20"
@ -209,8 +243,11 @@ export function CategoriesSection() {
Создать базовые категории
</Button>
)}
<Dialog open={isCreateDialogOpen} onOpenChange={setIsCreateDialogOpen}>
<Dialog
open={isCreateDialogOpen}
onOpenChange={setIsCreateDialogOpen}
>
<DialogTrigger asChild>
<Button className="bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-700 hover:to-pink-700 text-white">
<Plus className="w-4 h-4 mr-2" />
@ -219,34 +256,40 @@ export function CategoriesSection() {
</DialogTrigger>
<DialogContent className="glass-card">
<DialogHeader>
<DialogTitle className="text-white">Создать новую категорию</DialogTitle>
<DialogTitle className="text-white">
Создать новую категорию
</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div>
<Label htmlFor="category-name" className="text-white">Название категории</Label>
<Label htmlFor="category-name" className="text-white">
Название категории
</Label>
<Input
id="category-name"
value={newCategoryName}
onChange={(e) => setNewCategoryName(e.target.value)}
placeholder="Введите название..."
className="glass-input text-white placeholder:text-white/50"
onKeyDown={(e) => e.key === 'Enter' && handleCreateCategory()}
onKeyDown={(e) =>
e.key === "Enter" && handleCreateCategory()
}
/>
</div>
<div className="flex justify-end gap-2">
<Button
variant="outline"
<Button
variant="outline"
onClick={() => setIsCreateDialogOpen(false)}
className="bg-white/10 text-white border-white/20"
>
Отмена
</Button>
<Button
<Button
onClick={handleCreateCategory}
disabled={creating}
className="bg-gradient-to-r from-purple-600 to-pink-600 text-white"
>
{creating ? 'Создание...' : 'Создать'}
{creating ? "Создание..." : "Создать"}
</Button>
</div>
</div>
@ -257,17 +300,21 @@ export function CategoriesSection() {
<Card className="glass-card border-white/10">
<CardHeader>
<CardTitle className="text-white">Список категорий ({categories.length})</CardTitle>
<CardTitle className="text-white">
Список категорий ({categories.length})
</CardTitle>
</CardHeader>
<CardContent>
{categories.length === 0 ? (
<div className="text-center py-12">
<Package className="h-16 w-16 text-white/40 mx-auto mb-4" />
<h3 className="text-lg font-medium text-white mb-2">Нет категорий</h3>
<h3 className="text-lg font-medium text-white mb-2">
Нет категорий
</h3>
<p className="text-white/60 text-sm mb-4">
Создайте категории для классификации товаров
</p>
<Button
<Button
onClick={handleCreateBasicCategories}
className="bg-gradient-to-r from-purple-600 to-pink-600 text-white"
>
@ -277,10 +324,15 @@ export function CategoriesSection() {
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{categories.map((category) => (
<div key={category.id} className="glass-card p-4 border border-white/10 rounded-lg">
<div
key={category.id}
className="glass-card p-4 border border-white/10 rounded-lg"
>
<div className="flex items-center justify-between">
<div>
<h4 className="font-medium text-white">{category.name}</h4>
<h4 className="font-medium text-white">
{category.name}
</h4>
<p className="text-white/60 text-xs">
Создано: {formatDate(category.createdAt)}
</p>
@ -306,22 +358,26 @@ export function CategoriesSection() {
</AlertDialogTrigger>
<AlertDialogContent className="glass-card">
<AlertDialogHeader>
<AlertDialogTitle className="text-white">Удалить категорию</AlertDialogTitle>
<AlertDialogTitle className="text-white">
Удалить категорию
</AlertDialogTitle>
<AlertDialogDescription className="text-white/70">
Вы уверены, что хотите удалить категорию &quot;{category.name}&quot;?
Это действие нельзя отменить. Если в категории есть товары, удаление будет невозможно.
Вы уверены, что хотите удалить категорию &quot;
{category.name}&quot;? Это действие нельзя
отменить. Если в категории есть товары, удаление
будет невозможно.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel className="bg-white/10 text-white border-white/20">
Отмена
</AlertDialogCancel>
<AlertDialogAction
<AlertDialogAction
onClick={() => handleDeleteCategory(category.id)}
className="bg-red-600 hover:bg-red-700 text-white"
disabled={deleting}
>
{deleting ? 'Удаление...' : 'Удалить'}
{deleting ? "Удаление..." : "Удалить"}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
@ -339,39 +395,43 @@ export function CategoriesSection() {
<Dialog open={isEditDialogOpen} onOpenChange={setIsEditDialogOpen}>
<DialogContent className="glass-card">
<DialogHeader>
<DialogTitle className="text-white">Редактировать категорию</DialogTitle>
<DialogTitle className="text-white">
Редактировать категорию
</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div>
<Label htmlFor="edit-category-name" className="text-white">Название категории</Label>
<Label htmlFor="edit-category-name" className="text-white">
Название категории
</Label>
<Input
id="edit-category-name"
value={editCategoryName}
onChange={(e) => setEditCategoryName(e.target.value)}
placeholder="Введите название..."
className="glass-input text-white placeholder:text-white/50"
onKeyDown={(e) => e.key === 'Enter' && handleUpdateCategory()}
onKeyDown={(e) => e.key === "Enter" && handleUpdateCategory()}
/>
</div>
<div className="flex justify-end gap-2">
<Button
variant="outline"
<Button
variant="outline"
onClick={() => setIsEditDialogOpen(false)}
className="bg-white/10 text-white border-white/20"
>
Отмена
</Button>
<Button
<Button
onClick={handleUpdateCategory}
disabled={updating}
className="bg-gradient-to-r from-purple-600 to-pink-600 text-white"
>
{updating ? 'Сохранение...' : 'Сохранить'}
{updating ? "Сохранение..." : "Сохранить"}
</Button>
</div>
</div>
</DialogContent>
</Dialog>
</div>
)
}
);
}

View File

@ -362,7 +362,7 @@ export function FulfillmentWarehouse2Demo() {
icon={Wrench}
current={warehouseStats.fulfillmentSupplies.current}
change={warehouseStats.fulfillmentSupplies.change}
description="Упаковка, этикетки"
description="Расходники, этикетки"
/>
<StatCard
title="Расходники селлеров"