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