From 0ea92746e47d9b7e1bfc54aa71fc823ffd5f5e34 Mon Sep 17 00:00:00 2001 From: Denozordec Date: Sun, 28 Jun 2026 13:16:00 +0700 Subject: [PATCH] =?UTF-8?q?refactor(web):=20=D1=83=D0=BD=D0=B8=D1=84=D0=B8?= =?UTF-8?q?=D1=86=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D1=82=D1=8C=20UI=20=D0=BF?= =?UTF-8?q?=D0=BE=20=D0=BF=D0=B0=D1=82=D1=82=D0=B5=D1=80=D0=BD=D0=B0=D0=BC?= =?UTF-8?q?=20ReUI=20=D0=BD=D0=B0=20=D0=B2=D1=81=D0=B5=D1=85=2012=20=D1=81?= =?UTF-8?q?=D1=82=D1=80=D0=B0=D0=BD=D0=B8=D1=86=D0=B0=D1=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ввести CrudListPage, AnalyticsPage, RowActions и domain edit sheets с FormSheetRhf; убрать устаревшие TableCard/DataTableCard. Co-authored-by: Cursor --- .cursor/rules/frontend-ui-patterns.mdc | 7 +- apps/web/src/components/analytics-page.tsx | 69 +++ apps/web/src/components/crud-list-page.tsx | 62 +++ apps/web/src/components/data-grid-card.tsx | 2 +- apps/web/src/components/data-table-card.tsx | 2 - .../components/domain/account-edit-sheet.tsx | 119 ++++ .../components/domain/balance-entry-sheet.tsx | 103 ++++ .../components/domain/payment-edit-sheet.tsx | 112 ++++ .../components/domain/project-edit-sheet.tsx | 37 ++ .../components/domain/provider-edit-sheet.tsx | 97 ++++ .../src/components/domain/vps-edit-sheet.tsx | 334 ++++++++++++ apps/web/src/components/row-actions.tsx | 53 ++ apps/web/src/components/section-cards.tsx | 11 +- apps/web/src/components/table-card.tsx | 29 - apps/web/src/lib/api-client.ts | 6 + apps/web/src/lib/export-csv.ts | 30 ++ apps/web/src/lib/schemas.ts | 13 + apps/web/src/routes/_auth/accounts.tsx | 290 ++++------ apps/web/src/routes/_auth/balance.tsx | 194 +++---- apps/web/src/routes/_auth/dashboard.tsx | 27 +- apps/web/src/routes/_auth/payments.tsx | 221 +++----- apps/web/src/routes/_auth/projects.tsx | 117 ++-- apps/web/src/routes/_auth/providers.tsx | 194 +++---- apps/web/src/routes/_auth/reports.tsx | 132 +++-- apps/web/src/routes/_auth/resources.tsx | 143 ++--- apps/web/src/routes/_auth/settings.tsx | 510 +++++++++--------- apps/web/src/routes/_auth/sync-journal.tsx | 82 +-- apps/web/src/routes/_auth/tariffs.tsx | 86 +-- apps/web/src/routes/_auth/vps.tsx | 342 +----------- packages/shared/src/contracts/project.ts | 7 + packages/shared/src/index.ts | 1 + 31 files changed, 2002 insertions(+), 1430 deletions(-) create mode 100644 apps/web/src/components/analytics-page.tsx create mode 100644 apps/web/src/components/crud-list-page.tsx delete mode 100644 apps/web/src/components/data-table-card.tsx create mode 100644 apps/web/src/components/domain/account-edit-sheet.tsx create mode 100644 apps/web/src/components/domain/balance-entry-sheet.tsx create mode 100644 apps/web/src/components/domain/payment-edit-sheet.tsx create mode 100644 apps/web/src/components/domain/project-edit-sheet.tsx create mode 100644 apps/web/src/components/domain/provider-edit-sheet.tsx create mode 100644 apps/web/src/components/domain/vps-edit-sheet.tsx create mode 100644 apps/web/src/components/row-actions.tsx delete mode 100644 apps/web/src/components/table-card.tsx create mode 100644 apps/web/src/lib/export-csv.ts create mode 100644 packages/shared/src/contracts/project.ts diff --git a/.cursor/rules/frontend-ui-patterns.mdc b/.cursor/rules/frontend-ui-patterns.mdc index d259ada..716e158 100644 --- a/.cursor/rules/frontend-ui-patterns.mdc +++ b/.cursor/rules/frontend-ui-patterns.mdc @@ -29,12 +29,11 @@ apps/web/src/components/ ← shared + domain + layout empty-state.tsx query-state.tsx confirm-dialog.tsx - data-table-card.tsx + data-grid-card.tsx section-cards.tsx status-badge.tsx form-sheet.tsx ← Sheet + RHF FormProvider form-field.tsx ← Field + Controller + aria-invalid - table-card.tsx ← Card + Table wrapper loading-button.tsx ← Button + Spinner + label swap section-cards-skeleton.tsx table-skeleton.tsx @@ -49,14 +48,14 @@ apps/web/src/components/ ← shared + domain + layout | Page wrapper | `PageShell` | — | | Page title | `PageHeader` | — | | Stat metrics | `SectionCards` | `Card` | -| Data list | `DataTableCard` | `Table`, `InputGroup` | +| Data list | `DataGridCard` | `Table`, `InputGroup` | | Empty | `EmptyState` | `Empty` | | Loading / Error | `QueryState` | `Skeleton`, `Alert` | | Status | `StatusBadge` | `Badge` | | Create/Edit | `FormSheet` + `*-edit-sheet.tsx` | `Sheet`, `Field` | | Form field | `FormField` | `Field`, `Input`, `Select` | | Submit button | `LoadingButton` | `Button`, `Spinner` | -| Table wrapper | `TableCard` | `Table`, `Card` | +| Table wrapper | `DataGridCard` | `Table`, `Card` | | Delete confirm | `ConfirmDialog` | `AlertDialog` | | List row | — | `Item variant="outline" size="sm"` | | Nav | `AppSidebar` | `Sidebar` | diff --git a/apps/web/src/components/analytics-page.tsx b/apps/web/src/components/analytics-page.tsx new file mode 100644 index 0000000..a65ef7c --- /dev/null +++ b/apps/web/src/components/analytics-page.tsx @@ -0,0 +1,69 @@ +import type { ReactNode } from 'react' +import { ServerIcon } from 'lucide-react' +import { PageShell } from './page-shell' +import { PageHeader } from './page-header' +import { QueryState } from './query-state' +import { EmptyState } from './empty-state' +import { SectionCardsSkeleton } from './skeletons' + +interface AnalyticsPageProps { + title: string + description?: string + actions?: ReactNode + data: T | undefined + isLoading: boolean + isError: boolean + error?: unknown + onRetry?: () => void + /** Показать empty, если нет данных для аналитики (например, 0 VPS). */ + analyticsEmpty?: boolean + emptyTitle?: string + emptyDescription?: string + emptyAction?: ReactNode + skeleton?: ReactNode + children: (data: T) => ReactNode +} + +export function AnalyticsPage({ + title, + description, + actions, + data, + isLoading, + isError, + error, + onRetry, + analyticsEmpty, + emptyTitle = 'Нет данных для аналитики', + emptyDescription = 'Добавьте VPS или дождитесь синхронизации с BILLmanager', + emptyAction, + skeleton, + children, +}: AnalyticsPageProps) { + return ( + + + } + > + {(snap) => + analyticsEmpty ? ( + } + title={emptyTitle} + description={emptyDescription} + action={emptyAction} + /> + ) : ( + children(snap) + ) + } + + + ) +} diff --git a/apps/web/src/components/crud-list-page.tsx b/apps/web/src/components/crud-list-page.tsx new file mode 100644 index 0000000..8b3c1f5 --- /dev/null +++ b/apps/web/src/components/crud-list-page.tsx @@ -0,0 +1,62 @@ +import type { ReactNode } from 'react' +import { PageShell } from './page-shell' +import { PageHeader } from './page-header' +import { QueryState } from './query-state' +import { TableSkeleton } from './skeletons' + +interface CrudListPageProps { + title: string + description?: string + actions?: ReactNode + data: T | undefined + isLoading: boolean + isError: boolean + error?: unknown + onRetry?: () => void + empty?: boolean + emptyTitle?: string + emptyDescription?: string + emptyAction?: ReactNode + skeleton?: ReactNode + sheet?: ReactNode + children: (data: T) => ReactNode +} + +export function CrudListPage({ + title, + description, + actions, + data, + isLoading, + isError, + error, + onRetry, + empty, + emptyTitle, + emptyDescription, + emptyAction, + skeleton, + sheet, + children, +}: CrudListPageProps) { + return ( + + + } + empty={empty} + emptyTitle={emptyTitle} + emptyDescription={emptyDescription} + emptyAction={emptyAction} + > + {children} + + {sheet} + + ) +} diff --git a/apps/web/src/components/data-grid-card.tsx b/apps/web/src/components/data-grid-card.tsx index add10e0..3d699b6 100644 --- a/apps/web/src/components/data-grid-card.tsx +++ b/apps/web/src/components/data-grid-card.tsx @@ -195,7 +195,7 @@ export function DataGridCard({ return ( -
+
{title ? {title} : null} {description ?

{description}

: null}
diff --git a/apps/web/src/components/data-table-card.tsx b/apps/web/src/components/data-table-card.tsx deleted file mode 100644 index 8987823..0000000 --- a/apps/web/src/components/data-table-card.tsx +++ /dev/null @@ -1,2 +0,0 @@ -/** @deprecated Используйте DataGridCard. Тип колонок — data-grid-types. */ -export type { DataTableColumn } from './data-grid-types' diff --git a/apps/web/src/components/domain/account-edit-sheet.tsx b/apps/web/src/components/domain/account-edit-sheet.tsx new file mode 100644 index 0000000..dfcbae0 --- /dev/null +++ b/apps/web/src/components/domain/account-edit-sheet.tsx @@ -0,0 +1,119 @@ +import { FormSheetRhf } from '@/components/form-sheet-rhf' +import { FormField } from '@/components/form-field' +import { Input } from '@cfdm/ui/components/input' +import { Textarea } from '@cfdm/ui/components/textarea' +import { SelectField } from '@/components/select-field' +import type { ZodType } from 'zod' +import { providerAccountSchema, type ProviderAccountFormValues } from '@/lib/schemas' +import type { BillingMode, Provider } from '@/types/entities' +import { billingModeLabel } from '@/lib/format' + +const EMPTY: ProviderAccountFormValues = { + providerId: '', + name: '', + login: '', + apiCredentials: '', + billingMode: 'monthly', + balanceAlertBelow: '', + notes: '', +} + +interface ProviderAccountEditSheetProps { + open: boolean + onOpenChange: (open: boolean) => void + defaultValues: ProviderAccountFormValues + providers: Provider[] + onSubmit: (values: ProviderAccountFormValues) => void + submitting?: boolean +} + +export function providerAccountFormDefaults( + edit?: Partial | null, + fallbackProviderId = '', +): ProviderAccountFormValues { + if (!edit) { + return { ...EMPTY, providerId: fallbackProviderId } + } + return { + ...EMPTY, + ...edit, + balanceAlertBelow: edit.balanceAlertBelow ?? '', + } +} + +export function ProviderAccountEditSheet({ + open, + onOpenChange, + defaultValues, + providers, + onSubmit, + submitting, +}: ProviderAccountEditSheetProps) { + const isEdit = Boolean(defaultValues.id) + + return ( + } + defaultValues={defaultValues} + onSubmit={onSubmit} + submitting={submitting} + > + {(form) => { + const { register, formState: { errors }, watch, setValue } = form + return ( + <> + + setValue('providerId', v ?? '', { shouldValidate: true })} + options={providers.map((p) => ({ value: p.id, label: p.name }))} + /> + + + + + + + + + + + + setValue('billingMode', (v ?? 'monthly') as BillingMode)} + options={[ + { value: 'monthly', label: billingModeLabel('monthly') }, + { value: 'daily', label: billingModeLabel('daily') }, + ]} + /> + + + + + +