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
+}
+