fix(sync): загрузка тарифов при обычной синхронизации аккаунтов
Docker / build (push) Has been cancelled

Обычный синк больше не пропускает тарифы; добавлены кнопки «Синхронизировать все» и «Загрузить тарифы», исправлен dropdown на странице аккаунтов.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Denozordec
2026-06-28 00:45:52 +07:00
co-authored by Cursor
parent a53192207d
commit 921feb926f
3 changed files with 99 additions and 11 deletions
+1 -1
View File
@@ -73,7 +73,7 @@ export const syncRoutes: FastifyPluginAsync = async (app) => {
}
const onlyTariffs = Boolean(req.body?.onlyTariffs)
const opts = onlyTariffs ? { skipVpsPayments: true } : { skipTariffs: true }
const opts = onlyTariffs ? { skipVpsPayments: true } : {}
try {
const result = await runBillmanagerAccountSync(syncRow, opts)
+41 -4
View File
@@ -31,7 +31,7 @@ import {
import { accountBalanceApi, accountBalanceCurrency } from '@/lib/account'
import type { ProviderAccount, BillingMode } from '@/types/entities'
import { providerByIdMap, accountBillmanagerUiReady } from '@/lib/billmanager'
import { providerByIdMap, accountBillmanagerUiReady, billmanagerSyncableAccounts } from '@/lib/billmanager'
import { billingModeLabel, formatCurrency } from '@/lib/format'
export const Route = createFileRoute('/_auth/accounts')({
@@ -98,9 +98,29 @@ function AccountsPage() {
})
const syncMut = useMutation({
mutationFn: (id: string) => api.syncAccount(id),
onSuccess: (data) => {
void queryClient.invalidateQueries({ queryKey: ['snapshot'] })
const synced = (data as { synced?: { vpsCount?: number; paymentsCount?: number; tariffsCount?: number } })
?.synced
const parts: string[] = []
if (synced?.vpsCount != null) parts.push(`VPS ${synced.vpsCount}`)
if (synced?.paymentsCount) parts.push(`платежи ${synced.paymentsCount}`)
if (synced?.tariffsCount) parts.push(`тарифы ${synced.tariffsCount}`)
toast.success(parts.length ? `Синк: ${parts.join(', ')}` : 'Синк завершён')
},
onError: (e: unknown) => toast.error(e instanceof ApiError ? e.message : 'Ошибка синка'),
})
const syncAllMut = useMutation({
mutationFn: async () => {
if (!snapshot) return
const accounts = billmanagerSyncableAccounts(snapshot.providerAccounts, snapshot.providers)
for (const a of accounts) {
await api.syncAccount(a.id)
}
},
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ['snapshot'] })
toast.success('Синк запущен')
toast.success('Синхронизация всех аккаунтов завершена')
},
onError: (e: unknown) => toast.error(e instanceof ApiError ? e.message : 'Ошибка синка'),
})
@@ -122,6 +142,9 @@ function AccountsPage() {
}
const providerById = snapshot ? providerByIdMap(snapshot.providers) : new Map()
const syncableCount = snapshot
? billmanagerSyncableAccounts(snapshot.providerAccounts, snapshot.providers).length
: 0
const columns: DataTableColumn<ProviderAccount>[] = [
{
@@ -184,7 +207,7 @@ function AccountsPage() {
</Button>
}
/>
<DropdownMenuContent align="end">
<DropdownMenuContent align="end" className="w-auto min-w-44">
<DropdownMenuItem
disabled={!canSync || (syncMut.isPending && syncMut.variables === a.id)}
onClick={() => syncMut.mutate(a.id)}
@@ -223,7 +246,21 @@ function AccountsPage() {
<PageHeader
title="Аккаунты хостеров"
description="Аккаунты провайдеров с API-доступом"
actions={<Button onClick={openCreate}><PlusIcon data-icon="inline-start" />Добавить</Button>}
actions={
<div className="flex flex-wrap gap-2">
{syncableCount > 0 ? (
<Button
variant="outline"
disabled={syncAllMut.isPending}
onClick={() => syncAllMut.mutate()}
>
<RefreshCwIcon data-icon="inline-start" />
Синхронизировать все
</Button>
) : null}
<Button onClick={openCreate}><PlusIcon data-icon="inline-start" />Добавить</Button>
</div>
}
/>
<QueryState
data={snapshot}
+57 -6
View File
@@ -1,7 +1,9 @@
import { createFileRoute, Link } from '@tanstack/react-router'
import { useQuery } from '@tanstack/react-query'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { toast } from 'sonner'
import { snapshotQueryOptions } from '@/queries/snapshot'
import { api, ApiError } from '@/lib/api-client'
import { PageShell } from '@/components/page-shell'
import { PageHeader } from '@/components/page-header'
import { Badge } from '@cfdm/ui/components/badge'
@@ -11,10 +13,10 @@ import { dataGridCellStack } from '@/components/data-grid-cells'
import { QueryState } from '@/components/query-state'
import { TableSkeleton } from '@/components/skeletons'
import { Button } from '@cfdm/ui/components/button'
import { ServerIcon, UserRoundIcon, CpuIcon, CoinsIcon, HardDriveIcon } from 'lucide-react'
import { ServerIcon, UserRoundIcon, CpuIcon, CoinsIcon, HardDriveIcon, RefreshCwIcon } from 'lucide-react'
import type { ActiveTariff } from '@/types/entities'
import { providerByIdMap, accountSelectLabel } from '@/lib/billmanager'
import { providerByIdMap, accountSelectLabel, billmanagerSyncableAccounts } from '@/lib/billmanager'
import { formatCurrency } from '@/lib/format'
export const Route = createFileRoute('/_auth/tariffs')({
@@ -24,8 +26,36 @@ export const Route = createFileRoute('/_auth/tariffs')({
})
function TariffsPage() {
const queryClient = useQueryClient()
const { data: snapshot, isLoading, isError, error, refetch } = useQuery(snapshotQueryOptions())
const providerById = snapshot ? providerByIdMap(snapshot.providers) : new Map()
const syncableCount = snapshot
? billmanagerSyncableAccounts(snapshot.providerAccounts, snapshot.providers).length
: 0
const syncTariffsMut = useMutation({
mutationFn: async () => {
if (!snapshot) return { tariffsCount: 0 }
const accounts = billmanagerSyncableAccounts(snapshot.providerAccounts, snapshot.providers)
let tariffsCount = 0
for (const a of accounts) {
const res = (await api.syncAccount(a.id)) as {
synced?: { tariffsCount?: number }
}
tariffsCount += res?.synced?.tariffsCount ?? 0
}
return { tariffsCount, accounts: accounts.length }
},
onSuccess: ({ tariffsCount, accounts }) => {
void queryClient.invalidateQueries({ queryKey: ['snapshot'] })
if (accounts === 0) {
toast.error('Нет аккаунтов BILLmanager с настроенным API')
return
}
toast.success(`Загружено тарифов: ${tariffsCount}`)
},
onError: (e: unknown) => toast.error(e instanceof ApiError ? e.message : 'Ошибка загрузки тарифов'),
})
const columns: DataTableColumn<ActiveTariff>[] = [
{
@@ -79,7 +109,18 @@ function TariffsPage() {
return (
<PageShell>
<PageHeader title="Активные тарифы" description="Тарифы, загруженные из BILLmanager vds.order" />
<PageHeader
title="Активные тарифы"
description="Тарифы, загруженные из BILLmanager vds.order"
actions={
syncableCount > 0 ? (
<Button variant="outline" disabled={syncTariffsMut.isPending} onClick={() => syncTariffsMut.mutate()}>
<RefreshCwIcon data-icon="inline-start" />
Загрузить тарифы
</Button>
) : undefined
}
/>
<QueryState
data={snapshot}
isLoading={isLoading}
@@ -89,9 +130,19 @@ function TariffsPage() {
skeleton={<TableSkeleton />}
empty={snapshot?.activeTariffs.length === 0}
emptyTitle="Тарифы не загружены"
emptyDescription="Выполните синхронизацию аккаунта BILLmanager, чтобы загрузить тарифы"
emptyDescription="Синхронизация аккаунта BILLmanager загружает тарифы вместе с VPS и платежами"
emptyAction={
<Button render={<Link to="/accounts" />}>Перейти к аккаунтам</Button>
<div className="flex flex-wrap justify-center gap-2">
{syncableCount > 0 ? (
<Button disabled={syncTariffsMut.isPending} onClick={() => syncTariffsMut.mutate()}>
<RefreshCwIcon data-icon="inline-start" />
Загрузить тарифы
</Button>
) : null}
<Button variant="outline" render={<Link to="/accounts" />}>
Перейти к аккаунтам
</Button>
</div>
}
>
{(snap) => <DataGridCard columns={columnDefFromDataTable(columns)} data={snap.activeTariffs} rowId={(t) => t.id} />}