diff --git a/.agents/skills/reui/SKILL.md b/.agents/skills/reui/SKILL.md index c84e016..082d38f 100644 --- a/.agents/skills/reui/SKILL.md +++ b/.agents/skills/reui/SKILL.md @@ -41,6 +41,7 @@ pnpm dlx shadcn@latest add @reui/filters | filters | `VpsFiltersToolbar` | | autocomplete | `AutoCompleteInput` | | date-selector | `lib/date-selector-i18n.ts` | +| color-picker | напрямую в domain forms (`project-edit-sheet`) | ## Матрица выбора @@ -61,3 +62,4 @@ pnpm dlx shadcn@latest add @reui/filters - `@/components/ui/*` → `@cfdm/ui/components/*` - Не класть ReUI в `packages/ui` - Semantic colors: `variant="success"` — не `bg-emerald-*` +- `color-picker`: hex только в preset data, не в Tailwind `className` diff --git a/.cursor/rules/frontend-shadcn.mdc b/.cursor/rules/frontend-shadcn.mdc index a2e01d4..15c0483 100644 --- a/.cursor/rules/frontend-shadcn.mdc +++ b/.cursor/rules/frontend-shadcn.mdc @@ -51,12 +51,11 @@ Monorepo layout — [`frontend-monorepo.mdc`](frontend-monorepo.mdc). MCP workfl | `EmptyState` | `empty-state.tsx` | | `QueryState` | `query-state.tsx` | | `ConfirmDialog` | `confirm-dialog.tsx` | -| `DataTableCard` | `data-table-card.tsx` | +| `DataGridCard` | `data-grid-card.tsx` | | `SectionCards` | `section-cards.tsx` | | `StatusBadge` | `status-badge.tsx` | | `FormSheet` | `form-sheet.tsx` | | `FormField` | `form-field.tsx` | -| `TableCard` | `table-card.tsx` | | `LoadingButton` | `loading-button.tsx` | | `SectionCardsSkeleton` | `section-cards-skeleton.tsx` | | `TableSkeleton` | `table-skeleton.tsx` | diff --git a/.cursor/rules/reui-mcp.mdc b/.cursor/rules/reui-mcp.mdc index 69b891e..81aa90b 100644 --- a/.cursor/rules/reui-mcp.mdc +++ b/.cursor/rules/reui-mcp.mdc @@ -28,6 +28,7 @@ ReUI — first-class shadcn registry с enterprise-компонентами (Dat | Number field со stepper | `@reui` | `@/components/reui/number-field` | | Autocomplete | `@reui` | `@/components/reui/autocomplete` → `AutoCompleteInput` | | Date selector / range | `@reui` | `@/components/reui/date-selector` | +| Color picker (формы) | `@reui` | `@/components/reui/color-picker` — hex только в swatch data | | Semantic badge (success/info/warning) | `@reui` | `@/components/reui/badge` или `StatusBadge` | **Простые списки** — shadcn `Table`. **Сложные data-списки** — `DataGridCard` (ReUI data-grid), не shadcn Data Table. diff --git a/apps/web/src/components/data-grid-card.tsx b/apps/web/src/components/data-grid-card.tsx index bbb187e..d3e582d 100644 --- a/apps/web/src/components/data-grid-card.tsx +++ b/apps/web/src/components/data-grid-card.tsx @@ -27,7 +27,7 @@ import { DataGridPagination } from '@/components/reui/data-grid/data-grid-pagina import { DataGridColumnHeader } from '@/components/reui/data-grid/data-grid-column-header' import { DataGridColumnVisibility } from '@/components/reui/data-grid/data-grid-column-visibility' import { EmptyState } from './empty-state' -import type { DataTableColumn } from './data-grid-types' +import type { DataGridColumn } from './data-grid-types' const PAGINATION_LABELS = { rowsPerPageLabel: 'Строк на странице', @@ -59,8 +59,8 @@ export interface DataGridColumnVisibilityOption { label: string } -export function dataTableColumnVisibilityOptions( - cols: DataTableColumn[], +export function dataGridColumnVisibilityOptions( + cols: DataGridColumn[], ): DataGridColumnVisibilityOption[] { return cols .filter((c) => c.enableHiding !== false) @@ -390,9 +390,9 @@ export function DataGridCard({ ) } -/** Хелпер для конвертации DataTableColumn → ColumnDef с DataGridColumnHeader. */ -export function columnDefFromDataTable( - cols: DataTableColumn[], +/** Хелпер для конвертации DataGridColumn → ColumnDef с DataGridColumnHeader. */ +export function columnDefFromDataGrid( + cols: DataGridColumn[], ): ColumnDef[] { return cols.map((c) => { const title = resolveHeaderTitle(c.header, c.headerTitle) @@ -429,5 +429,8 @@ export function columnDefFromDataTable( }) } +/** @deprecated Используйте columnDefFromDataGrid */ +export const columnDefFromDataTable = columnDefFromDataGrid + /** re-export flexRender для удобства использования в колонках. */ export { flexRender } diff --git a/apps/web/src/components/data-grid-types.ts b/apps/web/src/components/data-grid-types.ts index 8a29011..f1a98a4 100644 --- a/apps/web/src/components/data-grid-types.ts +++ b/apps/web/src/components/data-grid-types.ts @@ -1,7 +1,7 @@ import type { ReactNode } from 'react' import type { LucideIcon } from 'lucide-react' -export interface DataTableColumn { +export interface DataGridColumn { key: string header: ReactNode cell: (row: T, index: number) => ReactNode @@ -14,6 +14,9 @@ export interface DataTableColumn { enableHiding?: boolean } +/** @deprecated Используйте DataGridColumn */ +export type DataTableColumn = DataGridColumn + /** Унифицированные классы колонок для DataGridCard. */ export const COL = { num: 'w-28 text-right tabular-nums', diff --git a/apps/web/src/components/reui/date-selector.tsx b/apps/web/src/components/reui/date-selector.tsx index 535643b..ad70a8a 100644 --- a/apps/web/src/components/reui/date-selector.tsx +++ b/apps/web/src/components/reui/date-selector.tsx @@ -19,7 +19,7 @@ import { import { DayButton } from "react-day-picker" import type { DateRange } from "react-day-picker" -import { useIsMobile } from "@/hooks/use-mobile" +import { useIsMobile } from "@cfdm/ui/hooks/use-mobile" import { cn } from "@cfdm/ui/lib/utils" import { Button } from "@cfdm/ui/components/button" import { Calendar, CalendarDayButton } from "@cfdm/ui/components/calendar" diff --git a/apps/web/src/components/status-badge.tsx b/apps/web/src/components/status-badge.tsx index 9f51708..5b19e6e 100644 --- a/apps/web/src/components/status-badge.tsx +++ b/apps/web/src/components/status-badge.tsx @@ -1,18 +1,19 @@ -import { Badge } from '@cfdm/ui/components/badge' import type { ComponentProps } from 'react' +import { Badge } from '@/components/reui/badge' + type BadgeVariant = NonNullable['variant']> const STATUS_VARIANT: Record = { - active: 'default', - ok: 'default', - paid: 'default', + active: 'success', + ok: 'success', + paid: 'success', paused: 'secondary', archived: 'outline', error: 'destructive', - running: 'secondary', - overdue: 'destructive', - stale: 'destructive', + running: 'info', + overdue: 'warning', + stale: 'warning', } export function StatusBadge({ status, label }: { status: string; label?: string }) { diff --git a/apps/web/src/components/vps-filters-toolbar.tsx b/apps/web/src/components/vps-filters-toolbar.tsx index 128d6c5..6c458ce 100644 --- a/apps/web/src/components/vps-filters-toolbar.tsx +++ b/apps/web/src/components/vps-filters-toolbar.tsx @@ -10,13 +10,24 @@ import { PopoverContent, PopoverTrigger, } from '@cfdm/ui/components/popover' -import { Slider } from '@cfdm/ui/components/slider' import { PlusIcon } from 'lucide-react' import { ListFiltersBar, type FilterChip } from '@/components/list-filters-bar' import type { DataGridColumnVisibilityOption } from '@/components/data-grid-card' import type { VisibilityState } from '@tanstack/react-table' +import { + DateSelector, + type DateSelectorFilterType, + type DateSelectorValue, +} from '@/components/reui/date-selector' +import { + NumberField, + NumberFieldDecrement, + NumberFieldGroup, + NumberFieldIncrement, + NumberFieldInput, +} from '@/components/reui/number-field' import { Filters, createFilter, @@ -24,6 +35,7 @@ import { type FilterFieldConfig, type FilterI18nConfig, type FilterOption, + type CustomRendererProps, } from '@/components/reui/filters' import { type VpsFiltersState, @@ -34,6 +46,7 @@ import { saveFilterPresets, type VpsFilterPreset, } from '@/components/vps-filters' +import { RU_DATE_SELECTOR_I18N } from '@/lib/date-selector-i18n' import { vpsStatusLabel, tariffTypeLabel, environmentLabel } from '@/lib/format' import { CountryFlag } from '@/components/country-flag' import type { Provider, ProviderAccount, Vps } from '@/types/entities' @@ -54,6 +67,47 @@ interface VpsFiltersToolbarProps { onColumnVisibilityChange?: (columnId: string, visible: boolean) => void } +function toDateSelectorValue(values: string[], operator: string): DateSelectorValue { + return { + period: 'day', + operator: (operator as DateSelectorFilterType) || 'before', + startDate: values[0] ? new Date(values[0]) : undefined, + endDate: values[1] ? new Date(values[1]) : undefined, + } +} + +function fromDateSelectorValue(value: DateSelectorValue): string[] { + const out: string[] = [] + if (value.startDate) out.push(value.startDate.toISOString().slice(0, 10)) + if (value.endDate) out.push(value.endDate.toISOString().slice(0, 10)) + return out +} + +function renderMinNumberField( + min: number, + max: number, + values: number[], + onChange: (v: number[]) => void, +) { + return ( +
+ onChange([v ?? 0])} + min={min} + max={max} + size="sm" + > + + + + + + +
+ ) +} + const RU_I18N: FilterI18nConfig = { addFilter: 'Фильтр', searchFields: 'Поиск поля…', @@ -135,6 +189,13 @@ function stateToFilters(state: VpsFiltersState): (Filter | Filter('minVcpu', 'is', [state.minVcpu])) if (state.minRamGb != null) out.push(createFilter('minRamGb', 'is', [state.minRamGb])) if (state.minDiskGb != null) out.push(createFilter('minDiskGb', 'is', [state.minDiskGb])) + if (state.paidUntilStart) { + const values = + state.paidUntilEnd && state.paidUntilOperator === 'between' + ? [state.paidUntilStart, state.paidUntilEnd] + : [state.paidUntilStart] + out.push(createFilter('paidUntil', state.paidUntilOperator ?? 'before', values)) + } return out } @@ -160,6 +221,12 @@ function filtersToState(filters: Filter[], base: VpsFiltersState): VpsFiltersSta case 'minVcpu': next.minVcpu = (f.values[0] as number) ?? null; break case 'minRamGb': next.minRamGb = (f.values[0] as number) ?? null; break case 'minDiskGb': next.minDiskGb = (f.values[0] as number) ?? null; break + case 'paidUntil': { + next.paidUntilOperator = (f.operator as typeof next.paidUntilOperator) ?? 'before' + next.paidUntilStart = (f.values[0] as string) ?? null + next.paidUntilEnd = (f.values[1] as string) ?? null + break + } } } return next @@ -255,25 +322,39 @@ export function VpsFiltersToolbar({ { key: 'monitoring', label: 'Мониторинг', type: 'multiselect' as const, options: onOffOpts, defaultOperator: 'is_any_of' }, { key: 'backup', label: 'Бэкап', type: 'multiselect' as const, options: onOffOpts, defaultOperator: 'is_any_of' }, { key: 'project', label: 'Проект', type: 'multiselect' as const, options: projectOpts, searchable: true, defaultOperator: 'is_any_of' }, + { + key: 'paidUntil', + label: 'Оплачено до', + type: 'custom' as const, + defaultOperator: 'before', + operators: [ + { value: 'before', label: 'до' }, + { value: 'after', label: 'после' }, + { value: 'between', label: 'между' }, + { value: 'is', label: '=' }, + ], + customRenderer: ({ values, onChange: onCh, operator }: CustomRendererProps) => ( +
+ onCh(fromDateSelectorValue(v))} + allowRange={operator === 'between'} + presetMode={operator as DateSelectorFilterType} + i18n={RU_DATE_SELECTOR_I18N} + showTwoMonths={operator === 'between'} + dayDateFormat="dd.MM.yyyy" + /> +
+ ), + }, { key: 'minVcpu', label: 'vCPU ≥', type: 'custom' as const, defaultOperator: 'is', operators: [{ value: 'is', label: '≥' }], - customRenderer: ({ values, onChange: onCh }: { values: number[]; onChange: (v: number[]) => void; operator: string }) => ( -
- onCh([typeof v === 'number' ? v : (v[0] ?? 0)])} - className="flex-1" - /> - {values[0] ?? 0} -
- ), + customRenderer: ({ values, onChange: onCh }: CustomRendererProps) => + renderMinNumberField(0, 32, values, onCh), }, { key: 'minRamGb', @@ -281,19 +362,8 @@ export function VpsFiltersToolbar({ type: 'custom' as const, defaultOperator: 'is', operators: [{ value: 'is', label: '≥' }], - customRenderer: ({ values, onChange: onCh }: { values: number[]; onChange: (v: number[]) => void; operator: string }) => ( -
- onCh([typeof v === 'number' ? v : (v[0] ?? 0)])} - className="flex-1" - /> - {values[0] ?? 0} GB -
- ), + customRenderer: ({ values, onChange: onCh }: CustomRendererProps) => + renderMinNumberField(0, 256, values, onCh), }, { key: 'minDiskGb', @@ -301,19 +371,8 @@ export function VpsFiltersToolbar({ type: 'custom' as const, defaultOperator: 'is', operators: [{ value: 'is', label: '≥' }], - customRenderer: ({ values, onChange: onCh }: { values: number[]; onChange: (v: number[]) => void; operator: string }) => ( -
- onCh([typeof v === 'number' ? v : (v[0] ?? 0)])} - className="flex-1" - /> - {values[0] ?? 0} GB -
- ), + customRenderer: ({ values, onChange: onCh }: CustomRendererProps) => + renderMinNumberField(0, 2000, values, onCh), }, ] }, [providers, providerAccounts, vps, countryOptions, cityOptions, projectNameOptions]) @@ -434,6 +493,28 @@ export function VpsFiltersToolbar({ onRemove: () => onChange({ ...filters, minDiskGb: null }), }) } + if (filters.paidUntilStart) { + const op = filters.paidUntilOperator ?? 'before' + const start = new Date(filters.paidUntilStart).toLocaleDateString('ru-RU') + const end = filters.paidUntilEnd + ? new Date(filters.paidUntilEnd).toLocaleDateString('ru-RU') + : null + const label = + op === 'between' && end + ? `Оплачено до: ${start} — ${end}` + : `Оплачено до ${op === 'after' ? 'после' : op === 'is' ? '' : ''} ${start}` + out.push({ + id: 'paidUntil', + label: label.trim(), + onRemove: () => + onChange({ + ...filters, + paidUntilOperator: null, + paidUntilStart: null, + paidUntilEnd: null, + }), + }) + } if (filters.groupByProject) { out.push({ id: 'groupByProject', diff --git a/apps/web/src/components/vps-filters.tsx b/apps/web/src/components/vps-filters.tsx index 806943d..5da6b6f 100644 --- a/apps/web/src/components/vps-filters.tsx +++ b/apps/web/src/components/vps-filters.tsx @@ -1,4 +1,8 @@ import type { Vps } from '@/types/entities' +import type { PaidUntilContext } from '@/lib/paid-until' +import { getPaidUntilDate } from '@/lib/paid-until' + +export type PaidUntilFilterOperator = 'is' | 'before' | 'after' | 'between' /** * Состояние фильтров VPS. @@ -21,6 +25,9 @@ export interface VpsFiltersState { minVcpu: number | null minRamGb: number | null minDiskGb: number | null + paidUntilOperator: PaidUntilFilterOperator | null + paidUntilStart: string | null + paidUntilEnd: string | null project: string[] groupByProject: boolean tableCompact: boolean @@ -42,6 +49,9 @@ export function buildDefaultVpsFilters(): VpsFiltersState { minVcpu: null, minRamGb: null, minDiskGb: null, + paidUntilOperator: null, + paidUntilStart: null, + paidUntilEnd: null, project: [], groupByProject: false, tableCompact: false, @@ -91,6 +101,41 @@ const matchesNumberGte = ( return Number(item ?? 0) >= threshold } +function startOfDay(d: Date): Date { + const next = new Date(d) + next.setHours(0, 0, 0, 0) + return next +} + +function matchesPaidUntil( + itemDate: Date | null, + operator: string, + values: string[], +): boolean { + if (values.length === 0) return true + if (!itemDate) return false + const itemDay = startOfDay(itemDate) + const start = values[0] ? startOfDay(new Date(values[0])) : null + const end = values[1] ? startOfDay(new Date(values[1])) : null + + switch (operator) { + case 'is': + return start ? itemDay.getTime() === start.getTime() : true + case 'before': + return start ? itemDay.getTime() < start.getTime() : true + case 'after': + return start ? itemDay.getTime() > start.getTime() : true + case 'between': + if (start && end) { + return itemDay.getTime() >= start.getTime() && itemDay.getTime() <= end.getTime() + } + if (start) return itemDay.getTime() >= start.getTime() + return true + default: + return true + } +} + interface ActiveFilter { field: string operator: string @@ -105,6 +150,7 @@ interface ActiveFilter { export function applyVpsFilters( items: Vps[], filters: VpsFiltersState | ActiveFilter[], + paidUntilCtx?: PaidUntilContext, ): Vps[] { // Если передан state — конвертируем в active filters const activeFilters: ActiveFilter[] = Array.isArray(filters) @@ -189,6 +235,15 @@ export function applyVpsFilters( case 'minDiskGb': if (!matchesNumberGte(item.diskGb, f.values as number[])) return false break + case 'paidUntil': { + const itemDate = paidUntilCtx + ? getPaidUntilDate(item, paidUntilCtx) + : item.paidUntil + ? new Date(item.paidUntil) + : null + if (!matchesPaidUntil(itemDate, f.operator, f.values as string[])) return false + break + } } } return true @@ -213,6 +268,15 @@ export function stateToActiveFilters(state: VpsFiltersState): ActiveFilter[] { if (state.minVcpu != null) out.push({ field: 'minVcpu', operator: 'gte', values: [state.minVcpu] }) if (state.minRamGb != null) out.push({ field: 'minRamGb', operator: 'gte', values: [state.minRamGb] }) if (state.minDiskGb != null) out.push({ field: 'minDiskGb', operator: 'gte', values: [state.minDiskGb] }) + if (state.paidUntilStart) { + out.push({ + field: 'paidUntil', + operator: state.paidUntilOperator ?? 'before', + values: state.paidUntilEnd + ? [state.paidUntilStart, state.paidUntilEnd] + : [state.paidUntilStart], + }) + } return out } @@ -233,6 +297,7 @@ export function countActiveFilters(filters: VpsFiltersState): number { if (filters.minVcpu != null) n++ if (filters.minRamGb != null) n++ if (filters.minDiskGb != null) n++ + if (filters.paidUntilStart) n++ return n } diff --git a/apps/web/src/hooks/use-mobile.ts b/apps/web/src/hooks/use-mobile.ts deleted file mode 100644 index 2b0fe1d..0000000 --- a/apps/web/src/hooks/use-mobile.ts +++ /dev/null @@ -1,19 +0,0 @@ -import * as React from "react" - -const MOBILE_BREAKPOINT = 768 - -export function useIsMobile() { - const [isMobile, setIsMobile] = React.useState(undefined) - - React.useEffect(() => { - const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`) - const onChange = () => { - setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) - } - mql.addEventListener("change", onChange) - setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) - return () => mql.removeEventListener("change", onChange) - }, []) - - return !!isMobile -} diff --git a/apps/web/src/lib/custom-fields.tsx b/apps/web/src/lib/custom-fields.tsx index c49f1f4..ca395c2 100644 --- a/apps/web/src/lib/custom-fields.tsx +++ b/apps/web/src/lib/custom-fields.tsx @@ -14,11 +14,11 @@ import { formatCustomFieldValue, parseCustomData, } from '@cfdm/shared/contracts/custom-fields' -import type { DataTableColumn } from '@/components/data-grid-types' +import type { DataGridColumn } from '@/components/data-grid-types' export function buildCustomFieldColumns( defs: CustomFieldDef[], -): DataTableColumn[] { +): DataGridColumn[] { return defs.map((def) => ({ key: `custom_${def.key}`, header: def.label, diff --git a/apps/web/src/routes/_auth/accounts.tsx b/apps/web/src/routes/_auth/accounts.tsx index 64acf1d..ceed17c 100644 --- a/apps/web/src/routes/_auth/accounts.tsx +++ b/apps/web/src/routes/_auth/accounts.tsx @@ -21,8 +21,8 @@ import { snapshotQueryOptions } from '@/queries/snapshot' import { api, ApiError } from '@/lib/api-client' import { Button } from '@cfdm/ui/components/button' import { Badge } from '@cfdm/ui/components/badge' -import { DataGridCard, columnDefFromDataTable } from '@/components/data-grid-card' -import type { DataTableColumn } from '@/components/data-grid-types' +import { DataGridCard, columnDefFromDataGrid } from '@/components/data-grid-card' +import type { DataGridColumn } from '@/components/data-grid-types' import { dataGridCellStack } from '@/components/data-grid-cells' import { CrudListPage } from '@/components/crud-list-page' import { RowActions } from '@/components/row-actions' @@ -251,7 +251,7 @@ function AccountsPage() { ] }, [snapshot, syncableCount, healthCtx, filters, health]) - const columns: DataTableColumn[] = [ + const columns: DataGridColumn[] = [ { key: 'name', header: 'Аккаунт', @@ -436,7 +436,7 @@ function AccountsPage() { ) : null} {health ? : null} a.id} pinLastColumn diff --git a/apps/web/src/routes/_auth/audit.tsx b/apps/web/src/routes/_auth/audit.tsx index c21bfc9..ce19db0 100644 --- a/apps/web/src/routes/_auth/audit.tsx +++ b/apps/web/src/routes/_auth/audit.tsx @@ -6,8 +6,8 @@ import { api } from '@/lib/api-client' import { PageShell } from '@/components/page-shell' import { PageHeader } from '@/components/page-header' import { QueryState } from '@/components/query-state' -import { DataGridCard, columnDefFromDataTable } from '@/components/data-grid-card' -import type { DataTableColumn } from '@/components/data-grid-types' +import { DataGridCard, columnDefFromDataGrid } from '@/components/data-grid-card' +import type { DataGridColumn } from '@/components/data-grid-types' import { Button } from '@cfdm/ui/components/button' import { Badge } from '@cfdm/ui/components/badge' @@ -36,7 +36,7 @@ function AuditPage() { queryFn: () => api.fetchAuditLog(200), }) - const columns: DataTableColumn[] = [ + const columns: DataGridColumn[] = [ { key: 'createdAt', header: 'Время', @@ -97,7 +97,7 @@ function AuditPage() { > {(rows) => ( r.id} pageSize={25} diff --git a/apps/web/src/routes/_auth/balance.tsx b/apps/web/src/routes/_auth/balance.tsx index 74dacd6..c983f49 100644 --- a/apps/web/src/routes/_auth/balance.tsx +++ b/apps/web/src/routes/_auth/balance.tsx @@ -20,8 +20,8 @@ import { snapshotQueryOptions, ratesQueryOptions } from '@/queries/snapshot' import { api, ApiError } from '@/lib/api-client' import { Button } from '@cfdm/ui/components/button' import { Badge } from '@cfdm/ui/components/badge' -import { DataGridCard, columnDefFromDataTable } from '@/components/data-grid-card' -import type { DataTableColumn } from '@/components/data-grid-types' +import { DataGridCard, columnDefFromDataGrid } from '@/components/data-grid-card' +import type { DataGridColumn } from '@/components/data-grid-types' import { dataGridCellStack } from '@/components/data-grid-cells' import { CrudListPage } from '@/components/crud-list-page' import { SectionCards } from '@/components/section-cards' @@ -73,7 +73,7 @@ function BalancePage() { const providerById = snapshot ? providerByIdMap(snapshot.providers) : new Map() - const columns: DataTableColumn[] = [ + const columns: DataGridColumn[] = [ { key: 'date', header: 'Дата', @@ -233,7 +233,7 @@ function BalancePage() { ]} /> r.id} pinLastColumn diff --git a/apps/web/src/routes/_auth/dashboard.tsx b/apps/web/src/routes/_auth/dashboard.tsx index dd29069..e61e991 100644 --- a/apps/web/src/routes/_auth/dashboard.tsx +++ b/apps/web/src/routes/_auth/dashboard.tsx @@ -22,8 +22,8 @@ import { PageShell } from '@/components/page-shell' import { PageHeader } from '@/components/page-header' import { SectionCards } from '@/components/section-cards' import { QueryState } from '@/components/query-state' -import { DataGridCard, columnDefFromDataTable } from '@/components/data-grid-card' -import type { DataTableColumn } from '@/components/data-grid-types' +import { DataGridCard, columnDefFromDataGrid } from '@/components/data-grid-card' +import type { DataGridColumn } from '@/components/data-grid-types' import { dataGridCellStack } from '@/components/data-grid-cells' import { SectionCardsSkeleton, TableSkeleton } from '@/components/skeletons' import { Button } from '@cfdm/ui/components/button' @@ -101,7 +101,7 @@ function DashboardPage() { const atRisk = buildAtRiskAccounts(snap.providerAccounts, snap.providers, snap.syncLog ?? []) const baseCur = snap.settings[0]?.baseCurrency ?? 'RUB' - const issueColumns: DataTableColumn[] = [ + const issueColumns: DataGridColumn[] = [ { key: 'title', header: 'Проблема', @@ -134,7 +134,7 @@ function DashboardPage() { }, ] - const vpsColumns: DataTableColumn[] = [ + const vpsColumns: DataGridColumn[] = [ { key: 'ip', header: 'IP / DNS', @@ -174,7 +174,7 @@ function DashboardPage() { }, ] - const riskColumns: DataTableColumn[] = [ + const riskColumns: DataGridColumn[] = [ { key: 'name', header: 'Аккаунт', @@ -319,7 +319,7 @@ function DashboardPage() { i.key} emptyTitle="Проблем не найдено" @@ -336,7 +336,7 @@ function DashboardPage() { Все VPS } - columns={columnDefFromDataTable(vpsColumns)} + columns={columnDefFromDataGrid(vpsColumns)} data={activeVps.slice(0, 8)} rowId={(v) => v.id} pagination={false} @@ -347,7 +347,7 @@ function DashboardPage() { r.id} emptyTitle="Рисков нет" diff --git a/apps/web/src/routes/_auth/payments.tsx b/apps/web/src/routes/_auth/payments.tsx index 749c020..d23cbff 100644 --- a/apps/web/src/routes/_auth/payments.tsx +++ b/apps/web/src/routes/_auth/payments.tsx @@ -15,8 +15,8 @@ import { toast } from 'sonner' import { snapshotQueryOptions, ratesQueryOptions } from '@/queries/snapshot' import { api, ApiError } from '@/lib/api-client' import { Button } from '@cfdm/ui/components/button' -import { DataGridCard, columnDefFromDataTable } from '@/components/data-grid-card' -import type { DataTableColumn } from '@/components/data-grid-types' +import { DataGridCard, columnDefFromDataGrid } from '@/components/data-grid-card' +import type { DataGridColumn } from '@/components/data-grid-types' import { dataGridCellStack } from '@/components/data-grid-cells' import { CrudListPage } from '@/components/crud-list-page' import { SectionCards } from '@/components/section-cards' @@ -93,7 +93,7 @@ function PaymentsPage() { const providerById = snapshot ? providerByIdMap(snapshot.providers) : new Map() - const columns: DataTableColumn[] = [ + const columns: DataGridColumn[] = [ { key: 'date', header: 'Дата', @@ -247,7 +247,7 @@ function PaymentsPage() { ]} /> p.id} pinLastColumn diff --git a/apps/web/src/routes/_auth/projects.tsx b/apps/web/src/routes/_auth/projects.tsx index 92189a3..cffced3 100644 --- a/apps/web/src/routes/_auth/projects.tsx +++ b/apps/web/src/routes/_auth/projects.tsx @@ -6,8 +6,8 @@ import { toast } from 'sonner' import { snapshotQueryOptions } from '@/queries/snapshot' import { api, ApiError } from '@/lib/api-client' -import { DataGridCard, columnDefFromDataTable } from '@/components/data-grid-card' -import type { DataTableColumn } from '@/components/data-grid-types' +import { DataGridCard, columnDefFromDataGrid } from '@/components/data-grid-card' +import type { DataGridColumn } from '@/components/data-grid-types' import { CrudListPage } from '@/components/crud-list-page' import { RowActions } from '@/components/row-actions' import { Button } from '@cfdm/ui/components/button' @@ -79,7 +79,7 @@ function ProjectsPage() { setOpen(true) } - const columns: DataTableColumn[] = [ + const columns: DataGridColumn[] = [ { key: 'name', header: 'Проект', @@ -167,7 +167,7 @@ function ProjectsPage() { > {() => ( r.id} pinLastColumn diff --git a/apps/web/src/routes/_auth/providers.tsx b/apps/web/src/routes/_auth/providers.tsx index c0386df..a6ba026 100644 --- a/apps/web/src/routes/_auth/providers.tsx +++ b/apps/web/src/routes/_auth/providers.tsx @@ -8,8 +8,8 @@ import { snapshotQueryOptions } from '@/queries/snapshot' import { api, ApiError } from '@/lib/api-client' import { Button } from '@cfdm/ui/components/button' import { Badge } from '@cfdm/ui/components/badge' -import { DataGridCard, columnDefFromDataTable } from '@/components/data-grid-card' -import type { DataTableColumn } from '@/components/data-grid-types' +import { DataGridCard, columnDefFromDataGrid } from '@/components/data-grid-card' +import type { DataGridColumn } from '@/components/data-grid-types' import { dataGridCellWithIcon } from '@/components/data-grid-cells' import { CrudListPage } from '@/components/crud-list-page' import { RowActions } from '@/components/row-actions' @@ -74,7 +74,7 @@ function ProvidersPage() { setOpen(true) } - const columns: DataTableColumn[] = [ + const columns: DataGridColumn[] = [ { key: 'name', header: 'Хостер', @@ -152,7 +152,7 @@ function ProvidersPage() { > {(snap) => ( p.id} pinLastColumn diff --git a/apps/web/src/routes/_auth/sync-journal.tsx b/apps/web/src/routes/_auth/sync-journal.tsx index c825d1b..f498be4 100644 --- a/apps/web/src/routes/_auth/sync-journal.tsx +++ b/apps/web/src/routes/_auth/sync-journal.tsx @@ -4,8 +4,8 @@ import { HistoryIcon, UserRoundIcon, CheckCircle2Icon, XCircleIcon, LoaderIcon } import { snapshotQueryOptions } from '@/queries/snapshot' import { CrudListPage } from '@/components/crud-list-page' -import { DataGridCard, columnDefFromDataTable } from '@/components/data-grid-card' -import type { DataTableColumn } from '@/components/data-grid-types' +import { DataGridCard, columnDefFromDataGrid } from '@/components/data-grid-card' +import type { DataGridColumn } from '@/components/data-grid-types' import { StatusBadge } from '@/components/status-badge' import { dataGridCellStack } from '@/components/data-grid-cells' import { Button } from '@cfdm/ui/components/button' @@ -29,7 +29,7 @@ function statusIcon(status: SyncLogRow['status']) { function SyncJournalPage() { const { data: snapshot, isLoading, isError, error, refetch } = useQuery(snapshotQueryOptions()) - const columns: DataTableColumn[] = [ + const columns: DataGridColumn[] = [ { key: 'started', header: 'Запуск', @@ -99,7 +99,7 @@ function SyncJournalPage() { > {(snap) => ( r.id} dense diff --git a/apps/web/src/routes/_auth/tariffs.tsx b/apps/web/src/routes/_auth/tariffs.tsx index 2a48929..760e722 100644 --- a/apps/web/src/routes/_auth/tariffs.tsx +++ b/apps/web/src/routes/_auth/tariffs.tsx @@ -7,8 +7,8 @@ import { snapshotQueryOptions } from '@/queries/snapshot' import { api, ApiError } from '@/lib/api-client' import { Alert, AlertDescription, AlertTitle } from '@cfdm/ui/components/alert' import { Badge } from '@cfdm/ui/components/badge' -import { DataGridCard, columnDefFromDataTable } from '@/components/data-grid-card' -import type { DataTableColumn } from '@/components/data-grid-types' +import { DataGridCard, columnDefFromDataGrid } from '@/components/data-grid-card' +import type { DataGridColumn } from '@/components/data-grid-types' import { dataGridCellStack } from '@/components/data-grid-cells' import { CrudListPage } from '@/components/crud-list-page' import { Button } from '@cfdm/ui/components/button' @@ -62,7 +62,7 @@ function TariffsPage() { onError: (e: unknown) => toast.error(e instanceof ApiError ? e.message : 'Ошибка загрузки тарифов'), }) - const columns: DataTableColumn[] = [ + const columns: DataGridColumn[] = [ { key: 'name', header: 'Тариф', @@ -169,7 +169,7 @@ function TariffsPage() { ) : null} t.id} /> diff --git a/apps/web/src/routes/_auth/vps.$vpsId.tsx b/apps/web/src/routes/_auth/vps.$vpsId.tsx index 50c12e1..6616d40 100644 --- a/apps/web/src/routes/_auth/vps.$vpsId.tsx +++ b/apps/web/src/routes/_auth/vps.$vpsId.tsx @@ -18,8 +18,8 @@ import { Button } from '@cfdm/ui/components/button' import { Badge } from '@cfdm/ui/components/badge' import { Card, CardContent, CardHeader, CardTitle } from '@cfdm/ui/components/card' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@cfdm/ui/components/tabs' -import { DataGridCard, columnDefFromDataTable } from '@/components/data-grid-card' -import type { DataTableColumn } from '@/components/data-grid-types' +import { DataGridCard, columnDefFromDataGrid } from '@/components/data-grid-card' +import type { DataGridColumn } from '@/components/data-grid-types' import { getPaidUntilDate } from '@/lib/paid-until' import { effectiveVpsTariffCurrency, @@ -93,7 +93,7 @@ function VpsDetailPage() { .filter(({ value }) => value !== undefined && value !== null && value !== '') }, [vps, customFieldDefs]) - const paymentColumns: DataTableColumn[] = [ + const paymentColumns: DataGridColumn[] = [ { key: 'date', header: 'Дата', cell: (p) => {p.date} }, { key: 'type', header: 'Тип', cell: (p) => paymentTypeLabel(p.type) }, { @@ -231,7 +231,7 @@ function VpsDetailPage() { p.id} emptyTitle="Платежей нет" diff --git a/apps/web/src/routes/_auth/vps.tsx b/apps/web/src/routes/_auth/vps.tsx index 668f4e8..999fac0 100644 --- a/apps/web/src/routes/_auth/vps.tsx +++ b/apps/web/src/routes/_auth/vps.tsx @@ -11,9 +11,9 @@ import { PageShell } from '@/components/page-shell' import { PageHeader } from '@/components/page-header' import { Button } from '@cfdm/ui/components/button' import { Badge } from '@cfdm/ui/components/badge' -import { DataGridCard, columnDefFromDataTable, loadStoredColumnVisibility, dataTableColumnVisibilityOptions } from '@/components/data-grid-card' +import { DataGridCard, columnDefFromDataGrid, loadStoredColumnVisibility, dataGridColumnVisibilityOptions } from '@/components/data-grid-card' import type { VisibilityState } from '@tanstack/react-table' -import type { DataTableColumn } from '@/components/data-grid-types' +import type { DataGridColumn } from '@/components/data-grid-types' import { dataGridCellStack, dataGridCellWithFlag } from '@/components/data-grid-cells' import { CountryFlag } from '@/components/country-flag' import { QueryState } from '@/components/query-state' @@ -196,17 +196,20 @@ function VpsPage() { }, [snapshot?.serverProjects, projectNameOptions]) const filteredVps = useMemo(() => { - let rows = applyVpsFilters(snapshot?.vps ?? [], filters) - if (!health || !snapshot) return rows const now = new Date() + const paidUntilCtx = snapshot + ? { + vps: snapshot.vps, + providerAccounts: snapshot.providerAccounts, + payments: snapshot.payments, + balanceLedger: snapshot.balanceLedger, + now, + } + : undefined + let rows = applyVpsFilters(snapshot?.vps ?? [], filters, paidUntilCtx) + if (!health || !snapshot) return rows const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()) - const ctx = { - vps: snapshot.vps, - providerAccounts: snapshot.providerAccounts, - payments: snapshot.payments, - balanceLedger: snapshot.balanceLedger, - now, - } + const ctx = paidUntilCtx! if (health === 'no-rate') { rows = rows.filter((v) => { if (v.status !== 'active') return false @@ -319,8 +322,8 @@ function VpsPage() { }) } - const columns: DataTableColumn[] = useMemo(() => { - const base: DataTableColumn[] = [ + const columns: DataGridColumn[] = useMemo(() => { + const base: DataGridColumn[] = [ { key: 'ip', header: 'IP / DNS', @@ -483,7 +486,7 @@ function VpsPage() { ]) const columnVisibilityOptions = useMemo( - () => dataTableColumnVisibilityOptions(columns), + () => dataGridColumnVisibilityOptions(columns), [columns], ) @@ -582,7 +585,7 @@ function VpsPage() { v.id} emptyTitle="VPS не найдены" diff --git a/packages/ui/src/styles/globals.css b/packages/ui/src/styles/globals.css index 602d5b3..c5fe0af 100644 --- a/packages/ui/src/styles/globals.css +++ b/packages/ui/src/styles/globals.css @@ -48,6 +48,8 @@ --warning-foreground: var(--color-yellow-900); --invert: var(--color-zinc-900); --invert-foreground: var(--color-zinc-50); + --focus: var(--color-blue-500); + --focus-foreground: var(--color-blue-900); } .dark { @@ -91,6 +93,8 @@ --warning-foreground: var(--color-yellow-600); --invert: var(--color-zinc-700); --invert-foreground: var(--color-zinc-50); + --focus: var(--color-blue-500); + --focus-foreground: var(--color-blue-600); } @theme inline { @@ -138,6 +142,8 @@ --color-success-foreground: var(--success-foreground); --color-success: var(--success); --color-destructive-foreground: var(--destructive-foreground); + --color-focus: var(--focus); + --color-focus-foreground: var(--focus-foreground); } @layer base {