From 5bbf0061cf94770262533380b581428cde854992 Mon Sep 17 00:00:00 2001 From: Denozordec Date: Fri, 31 Jul 2026 17:00:25 +0700 Subject: [PATCH] feat(data-grid): add sorting function support for DataGridColumn and update vps tariff calculations Enhanced DataGridColumn interface to include a sorting function option. Updated the frame-data-grid component to utilize the new sorting function. Introduced a new function for calculating comparable daily rates for VPS tariffs, improving the accuracy of monthly burn calculations. Updated the VpsPage to implement the new sorting function and daily rate logic. --- apps/web/src/components/data-grid-types.ts | 2 + .../components/reui-kit/frame-data-grid.tsx | 1 + apps/web/src/lib/format-tariff.test.ts | 75 +++++++++++++++++++ apps/web/src/lib/format.ts | 28 +++++-- apps/web/src/routes/_auth/vps.tsx | 19 ++++- 5 files changed, 118 insertions(+), 7 deletions(-) create mode 100644 apps/web/src/lib/format-tariff.test.ts diff --git a/apps/web/src/components/data-grid-types.ts b/apps/web/src/components/data-grid-types.ts index 2ddeae1..b8b7526 100644 --- a/apps/web/src/components/data-grid-types.ts +++ b/apps/web/src/components/data-grid-types.ts @@ -8,6 +8,8 @@ export interface DataGridColumn { icon?: LucideIcon sortable?: boolean sortValue?: (row: T) => string | number + /** TanStack sortingFn; для числовых sortValue — `'basic'`. */ + sortingFn?: 'auto' | 'alphanumeric' | 'basic' | 'text' | 'datetime' headerTitle?: string className?: string headerClassName?: string diff --git a/apps/web/src/components/reui-kit/frame-data-grid.tsx b/apps/web/src/components/reui-kit/frame-data-grid.tsx index 7d0c5ea..4dc756d 100644 --- a/apps/web/src/components/reui-kit/frame-data-grid.tsx +++ b/apps/web/src/components/reui-kit/frame-data-grid.tsx @@ -418,6 +418,7 @@ export function columnDefFromDataGrid( accessorFn: c.sortValue ? (row: T) => c.sortValue!(row) : (row: T) => (row as Record)[c.key] as string | number, + sortingFn: c.sortingFn ?? 'auto', } : {}), header: Icon diff --git a/apps/web/src/lib/format-tariff.test.ts b/apps/web/src/lib/format-tariff.test.ts new file mode 100644 index 0000000..8192d6a --- /dev/null +++ b/apps/web/src/lib/format-tariff.test.ts @@ -0,0 +1,75 @@ +import { describe, expect, it } from 'vitest' +import { + vpsTariffComparableDailyRate, + vpsTariffMonthlyBurn, + vpsTariffRateAmount, +} from './format' + +describe('vpsTariffComparableDailyRate', () => { + it('нормализует месячный тариф к суточной стоимости', () => { + expect( + vpsTariffComparableDailyRate({ + tariffType: 'monthly', + monthlyRate: 75, + dailyRate: null, + }), + ).toBeCloseTo(75 / 30, 6) + }) + + it('оставляет суточный тариф как есть', () => { + expect( + vpsTariffComparableDailyRate({ + tariffType: 'daily', + dailyRate: 5.59, + monthlyRate: 167.7, + }), + ).toBeCloseTo(5.59, 6) + }) + + it('сортирует 5,59/сутки дороже 75/мес', () => { + const daily = vpsTariffComparableDailyRate({ + tariffType: 'daily', + dailyRate: 5.59, + }) + const monthly = vpsTariffComparableDailyRate({ + tariffType: 'monthly', + monthlyRate: 75, + }) + expect(daily).toBeGreaterThan(monthly) + }) + + it('для daily без dailyRate берёт monthly/30', () => { + expect( + vpsTariffComparableDailyRate({ + tariffType: 'daily', + dailyRate: 0, + monthlyRate: 90, + }), + ).toBeCloseTo(3, 6) + }) +}) + +describe('vpsTariffMonthlyBurn', () => { + it('даёт месячный эквивалент суточного тарифа', () => { + expect( + vpsTariffMonthlyBurn({ tariffType: 'daily', dailyRate: 5.59 }), + ).toBeCloseTo(5.59 * 30, 6) + }) + + it('для месячного возвращает monthlyRate', () => { + expect( + vpsTariffMonthlyBurn({ tariffType: 'monthly', monthlyRate: 75 }), + ).toBeCloseTo(75, 6) + }) +}) + +describe('vpsTariffRateAmount', () => { + it('для отображения берёт ставку периода без нормализации', () => { + expect( + vpsTariffRateAmount({ tariffType: 'daily', dailyRate: 5.59, monthlyRate: 167 }), + ).toBe(5.59) + expect( + vpsTariffRateAmount({ tariffType: 'monthly', monthlyRate: 75, dailyRate: 2.5 }), + ).toBe(75) + }) +}) diff --git a/apps/web/src/lib/format.ts b/apps/web/src/lib/format.ts index d050b0d..8c6381c 100644 --- a/apps/web/src/lib/format.ts +++ b/apps/web/src/lib/format.ts @@ -151,16 +151,34 @@ export function vpsTariffRateAmount(vps: { return monthly ?? 0 } -/** Месячный burn-rate для сортировки и отчётов. */ +/** + * Сопоставимая суточная стоимость (единый знаменатель для сортировки). + * Месячный тариф → / 30; суточный → dailyRate (fallback: monthly/30). + */ +export function vpsTariffComparableDailyRate(vps: { + tariffType?: string | null + dailyRate?: number | string | null + monthlyRate?: number | string | null +}): number { + const daily = tariffRateNumber(vps.dailyRate) + const monthly = tariffRateNumber(vps.monthlyRate) + if (vps.tariffType === 'daily') { + if (daily != null && daily > 0) return daily + if (monthly != null && monthly > 0) return monthly / 30 + return 0 + } + if (monthly != null && monthly > 0) return monthly / 30 + if (daily != null && daily > 0) return daily + return 0 +} + +/** Месячный burn-rate для сортировки и отчётов (= comparable daily × 30). */ export function vpsTariffMonthlyBurn(vps: { tariffType?: string | null dailyRate?: number | string | null monthlyRate?: number | string | null }): number { - const daily = tariffRateNumber(vps.dailyRate) ?? 0 - const monthly = tariffRateNumber(vps.monthlyRate) ?? 0 - if (vps.tariffType === 'daily') return daily * 30 - return monthly + return vpsTariffComparableDailyRate(vps) * 30 } const ENVIRONMENT_LABELS: Record = { diff --git a/apps/web/src/routes/_auth/vps.tsx b/apps/web/src/routes/_auth/vps.tsx index 7865245..3f5571b 100644 --- a/apps/web/src/routes/_auth/vps.tsx +++ b/apps/web/src/routes/_auth/vps.tsx @@ -6,7 +6,7 @@ import { toast } from 'sonner' import { snapshotQueryOptions, ratesQueryOptions } from '@/queries/snapshot' import { api, ApiError } from '@/lib/api-client' import type { VpsFormValues } from '@/lib/schemas' -import { normalizeRatesPayload, effectiveVpsTariffCurrency, formatInProviderCurrency, vpsStatusLabel, tariffTypeLabel, vpsTariffRateAmount, vpsTariffMonthlyBurn } from '@/lib/format' +import { normalizeRatesPayload, effectiveVpsTariffCurrency, formatInProviderCurrency, vpsStatusLabel, tariffTypeLabel, vpsTariffRateAmount, vpsTariffComparableDailyRate, convertWithProviderRate } from '@/lib/format' import { PageShell } from '@/components/page-shell' import { PageHeader } from '@/components/page-header' import { Button } from '@cfdm/ui/components/button' @@ -405,6 +405,7 @@ function VpsPage() { key: 'specs', header: 'Ресурсы', icon: CpuIcon, + sortingFn: 'basic', sortValue: (v) => v.vcpu, cell: (v) => ( @@ -431,7 +432,21 @@ function VpsPage() { key: 'tariff', header: 'Тариф', icon: CreditCardIcon, - sortValue: (v) => vpsTariffMonthlyBurn(v), + // Единый знаменатель: суточная стоимость в базовой валюте (5,59/сутки > 75/мес). + sortingFn: 'basic', + sortValue: (v) => { + const provider = providerById.get(v.providerId) + const daily = vpsTariffComparableDailyRate(v) + if (!(daily > 0)) return 0 + const currency = effectiveVpsTariffCurrency(v, provider) + return convertWithProviderRate( + daily, + currency, + provider, + snapshot?.settings ?? null, + ratesData, + ).value + }, cell: (v) => { const provider = providerById.get(v.providerId) const currency = effectiveVpsTariffCurrency(v, provider)