From 223c2d3bd6e9bb8ce9271e5efde823baf46a533b Mon Sep 17 00:00:00 2001 From: Redsandyg Date: Wed, 18 Jun 2025 10:48:31 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D1=8B=20=D0=BD=D0=BE=D0=B2=D1=8B=D0=B5=20=D0=BA=D0=BE?= =?UTF-8?q?=D0=BC=D0=BF=D0=BE=D0=BD=D0=B5=D0=BD=D1=82=D1=8B=20=D0=B4=D0=BB?= =?UTF-8?q?=D1=8F=20=D1=83=D0=BF=D1=80=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D1=8F=20=D0=BA=D0=B0=D1=82=D0=B5=D0=B3=D0=BE=D1=80=D0=B8=D1=8F?= =?UTF-8?q?=D0=BC=D0=B8=20=D1=82=D0=BE=D0=B2=D0=B0=D1=80=D0=BE=D0=B2:=20Sa?= =?UTF-8?q?leCategoriesTable=20=D0=B8=20CategoryPage.=20=D0=9E=D0=B1=D0=BD?= =?UTF-8?q?=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD=20=D0=BA=D0=BE=D0=BC=D0=BF=D0=BE?= =?UTF-8?q?=D0=BD=D0=B5=D0=BD=D1=82=20Navigation=20=D0=B4=D0=BB=D1=8F=20?= =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F=20?= =?UTF-8?q?=D1=81=D1=81=D1=8B=D0=BB=D0=BA=D0=B8=20=D0=BD=D0=B0=20=D1=81?= =?UTF-8?q?=D1=82=D1=80=D0=B0=D0=BD=D0=B8=D1=86=D1=83=20=D0=BA=D0=B0=D1=82?= =?UTF-8?q?=D0=B5=D0=B3=D0=BE=D1=80=D0=B8=D0=B9.=20=D0=94=D0=BE=D0=B1?= =?UTF-8?q?=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B=20=D1=81=D1=82=D0=B8=D0=BB?= =?UTF-8?q?=D0=B8=20=D0=B4=D0=BB=D1=8F=20=D0=BD=D0=BE=D0=B2=D0=BE=D0=B9=20?= =?UTF-8?q?=D1=81=D1=82=D1=80=D0=B0=D0=BD=D0=B8=D1=86=D1=8B=20=D0=BA=D0=B0?= =?UTF-8?q?=D1=82=D0=B5=D0=B3=D0=BE=D1=80=D0=B8=D0=B9.=20=D0=A0=D0=B5?= =?UTF-8?q?=D0=B0=D0=BB=D0=B8=D0=B7=D0=BE=D0=B2=D0=B0=D0=BD=D0=B0=20=D0=BB?= =?UTF-8?q?=D0=BE=D0=B3=D0=B8=D0=BA=D0=B0=20=D0=BF=D0=BE=D0=BB=D1=83=D1=87?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D1=8F,=20=D1=81=D0=BE=D0=B7=D0=B4=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D1=8F=20=D0=B8=20=D1=80=D0=B5=D0=B4=D0=B0=D0=BA?= =?UTF-8?q?=D1=82=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D1=8F=20=D0=BA?= =?UTF-8?q?=D0=B0=D1=82=D0=B5=D0=B3=D0=BE=D1=80=D0=B8=D0=B9=20=D1=81=20?= =?UTF-8?q?=D0=B8=D1=81=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=D0=BC=20=D1=82=D0=BE=D0=BA=D0=B5=D0=BD=D0=B0?= =?UTF-8?q?=20=D0=B0=D0=B2=D1=82=D0=BE=D1=80=D0=B8=D0=B7=D0=B0=D1=86=D0=B8?= =?UTF-8?q?=D0=B8=20=D0=B8=D0=B7=20=D0=BA=D1=83=D0=BA=D0=B8.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/account/page.tsx | 1 + src/app/category/page.tsx | 15 ++ src/components/Navigation.tsx | 1 + src/components/SaleCategoriesTable.tsx | 239 +++++++++++++++++++++++++ src/styles/category.module.css | 19 ++ src/types/tokens.ts | 3 +- 6 files changed, 277 insertions(+), 1 deletion(-) create mode 100644 src/app/category/page.tsx create mode 100644 src/components/SaleCategoriesTable.tsx create mode 100644 src/styles/category.module.css diff --git a/src/app/account/page.tsx b/src/app/account/page.tsx index 72626eb..23ee9ef 100644 --- a/src/app/account/page.tsx +++ b/src/app/account/page.tsx @@ -26,6 +26,7 @@ import AccountNotifications from "../../components/AccountNotifications"; import AccountAgentTransactionSection from "../../components/AccountAgentTransactionSection"; import TabsNav from "../../components/TabsNav"; import AccountIntegration from "../../components/AccountIntegration"; +import Cookies from "js-cookie"; const initialNotifications = { emailNotifications: true, diff --git a/src/app/category/page.tsx b/src/app/category/page.tsx new file mode 100644 index 0000000..35e8361 --- /dev/null +++ b/src/app/category/page.tsx @@ -0,0 +1,15 @@ +"use client"; +import SaleCategoriesTable from "../../components/SaleCategoriesTable"; +import AuthGuard from "../../components/AuthGuard"; +import styles from "../../styles/category.module.css"; + +export default function CategoryPage() { + return ( + +
+

Категории товаров

+ +
+
+ ); +} \ No newline at end of file diff --git a/src/components/Navigation.tsx b/src/components/Navigation.tsx index 579fabb..a53a28d 100644 --- a/src/components/Navigation.tsx +++ b/src/components/Navigation.tsx @@ -18,6 +18,7 @@ const navItems: NavItem[] = [ { id: "home", label: "Дашборд", href: "/" }, { id: "stat", label: "Статистика", href: "/stat" }, { id: "billing", label: "Финансы", href: "/billing" }, + { id: "category", label: "Категории товаров", href: "/category" }, ]; const Navigation: React.FC = () => { diff --git a/src/components/SaleCategoriesTable.tsx b/src/components/SaleCategoriesTable.tsx new file mode 100644 index 0000000..5c9c138 --- /dev/null +++ b/src/components/SaleCategoriesTable.tsx @@ -0,0 +1,239 @@ +"use client"; +import React, { useMemo, useEffect, useState } from "react"; +import { + Box, + Button, + IconButton, + Tooltip +} from "@mui/material"; +import { MaterialReactTable, type MRT_ColumnDef, useMaterialReactTable, type MRT_Row, type MRT_TableOptions } from "material-react-table"; +import { Add as AddIcon, Edit as EditIcon, Save as SaveIcon, Cancel as CancelIcon } from "@mui/icons-material"; +import Cookies from "js-cookie"; + +interface SaleCategory { + id: number; + category: string; + description?: string; + perc: number; + create_dttm: string; + update_dttm: string; +} + +const SaleCategoriesTable: React.FC = () => { + const [categories, setCategories] = useState([]); + const [validationErrors, setValidationErrors] = useState>({}); + const [creationKey, setCreationKey] = useState(0); + + const fetchCategories = async () => { + try { + const token = Cookies.get("access_token"); + if (!token) return; + const res = await fetch("/api/account/category", { + method: "GET", + headers: { + "Content-Type": "application/json", + "Authorization": `Bearer ${token}` + }, + }); + if (!res.ok) throw new Error(`HTTP error! status: ${res.status}`); + const data: SaleCategory[] = await res.json(); + setCategories(data); + } catch (error) { + console.error("Failed to fetch categories:", error); + } + }; + + useEffect(() => { + fetchCategories(); + }, []); + + // CREATE + const handleCreateCategory: MRT_TableOptions["onCreatingRowSave"] = async ({ values, table }) => { + const token = Cookies.get("access_token"); + if (!token) return; + try { + const res = await fetch("/api/account/category", { + method: "POST", + headers: { + "Content-Type": "application/json", + "Authorization": `Bearer ${token}` + }, + body: JSON.stringify({ + category: values.category, + description: values.description, + perc: Number(values.perc) + }) + }); + if (!res.ok) { + if (res.status === 400) { + const data = await res.json(); + setValidationErrors((prev) => ({ ...prev, category: data.detail || "Категория с таким именем уже существует" })); + return; + } + throw new Error("Ошибка создания категории"); + } + await fetchCategories(); + table.setCreatingRow(null); + } catch (e) { + alert("Ошибка создания категории"); + } + }; + + // UPDATE + const handleSaveCategory: MRT_TableOptions["onEditingRowSave"] = async ({ values, table }) => { + const token = Cookies.get("access_token"); + if (!token) return; + try { + const res = await fetch("/api/account/category", { + method: "POST", + headers: { + "Content-Type": "application/json", + "Authorization": `Bearer ${token}` + }, + body: JSON.stringify({ + id: Number(values.id), + category: values.category, + description: values.description, + perc: Number(values.perc) + }) + }); + if (!res.ok) { + if (res.status === 400) { + const data = await res.json(); + setValidationErrors((prev) => ({ ...prev, category: data.detail || "Категория с таким именем уже существует" })); + return; + } + throw new Error("Ошибка обновления категории"); + } + await fetchCategories(); + table.setEditingRow(null); + } catch (e) { + alert("Ошибка обновления категории"); + } + }; + + // Валидация (минимальная) + const validateCategory = (values: Partial) => { + return { + category: !values.category ? "Обязательное поле" : undefined, + perc: values.perc === undefined || isNaN(Number(values.perc)) ? "Введите число" : undefined + }; + }; + + const columns = useMemo[]>( + () => [ + { + accessorKey: "id", + header: "", + size: 1, + enableEditing: false, + enableHiding: false, + enableColumnActions: false, + enableSorting: false, + enableColumnFilter: false, + Cell: () => null, + Edit: () => null, + muiTableBodyCellProps: { sx: { display: 'none' } }, + muiTableHeadCellProps: { sx: { display: 'none' } }, + }, + { + accessorKey: "category", + header: "Категория", + size: 200, + muiEditTextFieldProps: { + required: true, + error: !!validationErrors?.category, + helperText: validationErrors?.category, + onFocus: () => setValidationErrors((prev) => ({ ...prev, category: undefined })) + } + }, + { + accessorKey: "description", + header: "Описание", + size: 250, + muiEditTextFieldProps: { + onFocus: () => undefined + } + }, + { + accessorKey: "perc", + header: "% начисления", + size: 100, + muiEditTextFieldProps: { + required: true, + type: "number", + error: !!validationErrors?.perc, + helperText: validationErrors?.perc, + onFocus: () => setValidationErrors((prev) => ({ ...prev, perc: undefined })) + } + }, + { + accessorKey: "create_dttm", + header: "Создана", + size: 160, + enableEditing: false, + Cell: ({ cell }) => new Date(cell.getValue()).toLocaleString(), + }, + { + accessorKey: "update_dttm", + header: "Обновлена", + size: 160, + enableEditing: false, + Cell: ({ cell }) => new Date(cell.getValue()).toLocaleString(), + }, + ], + [validationErrors], + ); + + const table = useMaterialReactTable({ + columns, + data: categories, + createDisplayMode: "row", + editDisplayMode: "row", + enableEditing: true, + enableRowActions: false, + getRowId: (row) => String(row.id), + onCreatingRowSave: async (props) => { + const errors = validateCategory(props.values); + setValidationErrors(errors); + if (Object.values(errors).some(Boolean)) return; + await handleCreateCategory(props); + }, + onEditingRowSave: async (props) => { + const errors = validateCategory(props.values); + setValidationErrors(errors); + if (Object.values(errors).some(Boolean)) return; + props.values.id = Number(props.values.id); + await handleSaveCategory(props); + }, + onCreatingRowCancel: () => setValidationErrors({}), + onEditingRowCancel: () => setValidationErrors({}), + renderTopToolbarCustomActions: ({ table }) => ( + + ), + muiTableBodyCellProps: { sx: { fontSize: 14 } }, + muiTableHeadCellProps: { sx: { fontWeight: 700 } }, + initialState: { + pagination: { pageSize: 10, pageIndex: 0 }, + }, + }); + + useEffect(() => { + setValidationErrors({}); + }, [table.getState().editingRow]); + + return ; +}; + +export default SaleCategoriesTable; \ No newline at end of file diff --git a/src/styles/category.module.css b/src/styles/category.module.css new file mode 100644 index 0000000..3b6df69 --- /dev/null +++ b/src/styles/category.module.css @@ -0,0 +1,19 @@ +.categoryPage { + display: flex; + flex-direction: column; + gap: 32px; +} +.categoryTitle { + font-size: 28px; + font-weight: bold; + color: #111827; + margin-bottom: 8px; +} +@media (max-width: 600px) { + .categoryPage { + gap: 16px; + } + .categoryTitle { + font-size: 16px; + } +} \ No newline at end of file diff --git a/src/types/tokens.ts b/src/types/tokens.ts index 37d6de7..0e33b87 100644 --- a/src/types/tokens.ts +++ b/src/types/tokens.ts @@ -5,4 +5,5 @@ export interface Token { rawToken?: string; create_dttm: string; use_dttm?: string; -} \ No newline at end of file +} +