From b6eefd34376bc9118b5feddef1c19212a399b1b6 Mon Sep 17 00:00:00 2001 From: nurdotnet <278048682+nurdotnet@users.noreply.github.com> Date: Tue, 21 Apr 2026 19:28:26 +0500 Subject: [PATCH] =?UTF-8?q?phase1c:=20web=20UI=20=E2=80=94=20sidebar=20lay?= =?UTF-8?q?out=20+=20list/form=20pages=20for=20catalog?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Shared components: - AppLayout with grouped sidebar nav (Главное / Каталог / Контрагенты / Склады / Справочники) - DataTable, Pagination, SearchBar, Button, Modal, Field (TextInput/TextArea/Select/Checkbox) - PageHeader, useCatalogList + useCatalogMutations hooks (TanStack Query) - types.ts with enums as const objects (erasableSyntaxOnly-compatible) Dashboard: 4 stat cards (products/counterparties/stores/retail-points counts) + roadmap List + edit-in-modal for 9 entities: - Countries, Currencies (read-only) - VatRates, UnitsOfMeasure, PriceTypes, Stores, RetailPoints, ProductGroups, Counterparties (full CRUD) - Products: list + filters; full form deferred to next commit All pages: search, pagination, row-click → modal edit, create button, delete with confirm. Counterparty form uses Country lookup; RetailPoint form uses Store lookup. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/food-market.web/src/App.tsx | 25 ++- .../src/components/AppLayout.tsx | 102 +++++++++ src/food-market.web/src/components/Button.tsx | 39 ++++ .../src/components/DataTable.tsx | 72 +++++++ src/food-market.web/src/components/Field.tsx | 58 +++++ src/food-market.web/src/components/Modal.tsx | 44 ++++ .../src/components/PageHeader.tsx | 21 ++ .../src/components/Pagination.tsx | 36 ++++ .../src/components/SearchBar.tsx | 22 ++ src/food-market.web/src/lib/types.ts | 56 +++++ src/food-market.web/src/lib/useCatalog.ts | 45 ++++ .../src/pages/CounterpartiesPage.tsx | 198 ++++++++++++++++++ .../src/pages/CountriesPage.tsx | 31 +++ .../src/pages/CurrenciesPage.tsx | 33 +++ .../src/pages/DashboardPage.tsx | 129 +++++------- .../src/pages/PriceTypesPage.tsx | 100 +++++++++ .../src/pages/ProductGroupsPage.tsx | 108 ++++++++++ .../src/pages/ProductsPage.tsx | 66 ++++++ .../src/pages/RetailPointsPage.tsx | 150 +++++++++++++ src/food-market.web/src/pages/StoresPage.tsx | 134 ++++++++++++ .../src/pages/UnitsOfMeasurePage.tsx | 118 +++++++++++ .../src/pages/VatRatesPage.tsx | 115 ++++++++++ 22 files changed, 1628 insertions(+), 74 deletions(-) create mode 100644 src/food-market.web/src/components/AppLayout.tsx create mode 100644 src/food-market.web/src/components/Button.tsx create mode 100644 src/food-market.web/src/components/DataTable.tsx create mode 100644 src/food-market.web/src/components/Field.tsx create mode 100644 src/food-market.web/src/components/Modal.tsx create mode 100644 src/food-market.web/src/components/PageHeader.tsx create mode 100644 src/food-market.web/src/components/Pagination.tsx create mode 100644 src/food-market.web/src/components/SearchBar.tsx create mode 100644 src/food-market.web/src/lib/types.ts create mode 100644 src/food-market.web/src/lib/useCatalog.ts create mode 100644 src/food-market.web/src/pages/CounterpartiesPage.tsx create mode 100644 src/food-market.web/src/pages/CountriesPage.tsx create mode 100644 src/food-market.web/src/pages/CurrenciesPage.tsx create mode 100644 src/food-market.web/src/pages/PriceTypesPage.tsx create mode 100644 src/food-market.web/src/pages/ProductGroupsPage.tsx create mode 100644 src/food-market.web/src/pages/ProductsPage.tsx create mode 100644 src/food-market.web/src/pages/RetailPointsPage.tsx create mode 100644 src/food-market.web/src/pages/StoresPage.tsx create mode 100644 src/food-market.web/src/pages/UnitsOfMeasurePage.tsx create mode 100644 src/food-market.web/src/pages/VatRatesPage.tsx diff --git a/src/food-market.web/src/App.tsx b/src/food-market.web/src/App.tsx index d450e02..5b6a948 100644 --- a/src/food-market.web/src/App.tsx +++ b/src/food-market.web/src/App.tsx @@ -3,6 +3,17 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { ReactQueryDevtools } from '@tanstack/react-query-devtools' import { LoginPage } from '@/pages/LoginPage' import { DashboardPage } from '@/pages/DashboardPage' +import { CountriesPage } from '@/pages/CountriesPage' +import { CurrenciesPage } from '@/pages/CurrenciesPage' +import { VatRatesPage } from '@/pages/VatRatesPage' +import { UnitsOfMeasurePage } from '@/pages/UnitsOfMeasurePage' +import { PriceTypesPage } from '@/pages/PriceTypesPage' +import { StoresPage } from '@/pages/StoresPage' +import { RetailPointsPage } from '@/pages/RetailPointsPage' +import { ProductGroupsPage } from '@/pages/ProductGroupsPage' +import { CounterpartiesPage } from '@/pages/CounterpartiesPage' +import { ProductsPage } from '@/pages/ProductsPage' +import { AppLayout } from '@/components/AppLayout' import { ProtectedRoute } from '@/components/ProtectedRoute' const queryClient = new QueryClient({ @@ -21,7 +32,19 @@ export default function App() { } /> }> - } /> + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> diff --git a/src/food-market.web/src/components/AppLayout.tsx b/src/food-market.web/src/components/AppLayout.tsx new file mode 100644 index 0000000..410e794 --- /dev/null +++ b/src/food-market.web/src/components/AppLayout.tsx @@ -0,0 +1,102 @@ +import { NavLink, Outlet } from 'react-router-dom' +import { useQuery } from '@tanstack/react-query' +import { api } from '@/lib/api' +import { logout } from '@/lib/auth' +import { cn } from '@/lib/utils' +import { + LayoutDashboard, Package, FolderTree, Ruler, Percent, Tag, + Users, Warehouse, Store as StoreIcon, Globe, Coins, LogOut, +} from 'lucide-react' + +interface MeResponse { + sub: string + name: string + email: string + roles: string[] + orgId: string +} + +const nav = [ + { group: 'Главное', items: [ + { to: '/', icon: LayoutDashboard, label: 'Dashboard', end: true }, + ]}, + { group: 'Каталог', items: [ + { to: '/catalog/products', icon: Package, label: 'Товары' }, + { to: '/catalog/product-groups', icon: FolderTree, label: 'Группы' }, + { to: '/catalog/units', icon: Ruler, label: 'Ед. измерения' }, + { to: '/catalog/vat-rates', icon: Percent, label: 'Ставки НДС' }, + { to: '/catalog/price-types', icon: Tag, label: 'Типы цен' }, + ]}, + { group: 'Контрагенты', items: [ + { to: '/catalog/counterparties', icon: Users, label: 'Контрагенты' }, + ]}, + { group: 'Склады', items: [ + { to: '/catalog/stores', icon: Warehouse, label: 'Склады' }, + { to: '/catalog/retail-points', icon: StoreIcon, label: 'Точки продаж' }, + ]}, + { group: 'Справочники', items: [ + { to: '/catalog/countries', icon: Globe, label: 'Страны' }, + { to: '/catalog/currencies', icon: Coins, label: 'Валюты' }, + ]}, +] as const + +export function AppLayout() { + const { data: me } = useQuery({ + queryKey: ['me'], + queryFn: async () => (await api.get('/api/me')).data, + staleTime: 5 * 60 * 1000, + }) + + return ( +
+ + +
+ +
+
+ ) +} diff --git a/src/food-market.web/src/components/Button.tsx b/src/food-market.web/src/components/Button.tsx new file mode 100644 index 0000000..af4f780 --- /dev/null +++ b/src/food-market.web/src/components/Button.tsx @@ -0,0 +1,39 @@ +import type { ButtonHTMLAttributes, ReactNode } from 'react' +import { cn } from '@/lib/utils' + +type Variant = 'primary' | 'secondary' | 'ghost' | 'danger' +type Size = 'sm' | 'md' + +interface ButtonProps extends ButtonHTMLAttributes { + variant?: Variant + size?: Size + children: ReactNode +} + +const variants: Record = { + primary: 'bg-violet-600 hover:bg-violet-700 text-white', + secondary: 'bg-white dark:bg-slate-800 text-slate-700 dark:text-slate-200 border border-slate-200 dark:border-slate-700 hover:bg-slate-50 dark:hover:bg-slate-700', + ghost: 'text-slate-600 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-800', + danger: 'bg-red-600 hover:bg-red-700 text-white', +} + +const sizes: Record = { + sm: 'px-2.5 py-1 text-xs', + md: 'px-3.5 py-1.5 text-sm', +} + +export function Button({ variant = 'primary', size = 'md', className, children, ...rest }: ButtonProps) { + return ( + + ) +} diff --git a/src/food-market.web/src/components/DataTable.tsx b/src/food-market.web/src/components/DataTable.tsx new file mode 100644 index 0000000..23e974c --- /dev/null +++ b/src/food-market.web/src/components/DataTable.tsx @@ -0,0 +1,72 @@ +import type { ReactNode } from 'react' +import { cn } from '@/lib/utils' + +export interface Column { + header: string + cell: (row: T) => ReactNode + className?: string + width?: string +} + +interface DataTableProps { + rows: T[] + columns: Column[] + rowKey: (row: T) => string + onRowClick?: (row: T) => void + empty?: ReactNode + isLoading?: boolean +} + +export function DataTable({ rows, columns, rowKey, onRowClick, empty, isLoading }: DataTableProps) { + return ( +
+ + + + {columns.map((c, i) => ( + + ))} + + + + {isLoading ? ( + + + + ) : rows.length === 0 ? ( + + + + ) : ( + rows.map((row) => ( + onRowClick?.(row)} + className={cn( + 'border-t border-slate-100 dark:border-slate-800', + onRowClick && 'cursor-pointer hover:bg-slate-50 dark:hover:bg-slate-800/30' + )} + > + {columns.map((c, i) => ( + + ))} + + )) + )} + +
+ {c.header} +
+ Загрузка… +
+ {empty ?? 'Нет данных'} +
+ {c.cell(row)} +
+
+ ) +} diff --git a/src/food-market.web/src/components/Field.tsx b/src/food-market.web/src/components/Field.tsx new file mode 100644 index 0000000..fad4374 --- /dev/null +++ b/src/food-market.web/src/components/Field.tsx @@ -0,0 +1,58 @@ +import type { InputHTMLAttributes, SelectHTMLAttributes, ReactNode, TextareaHTMLAttributes } from 'react' +import { cn } from '@/lib/utils' + +interface FieldProps { + label: string + error?: string + children: ReactNode + className?: string +} + +export function Field({ label, error, children, className }: FieldProps) { + return ( + + ) +} + +const inputClass = 'w-full rounded-md border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-900 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-violet-500 disabled:opacity-60' + +export function TextInput(props: InputHTMLAttributes) { + return +} + +export function TextArea(props: TextareaHTMLAttributes) { + return