diff --git a/apps/api/src/routes/sync.ts b/apps/api/src/routes/sync.ts index deaf30f..05e8e71 100644 --- a/apps/api/src/routes/sync.ts +++ b/apps/api/src/routes/sync.ts @@ -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) diff --git a/apps/web/src/routes/_auth/accounts.tsx b/apps/web/src/routes/_auth/accounts.tsx index 624b9fd..25ae4c7 100644 --- a/apps/web/src/routes/_auth/accounts.tsx +++ b/apps/web/src/routes/_auth/accounts.tsx @@ -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[] = [ { @@ -184,7 +207,7 @@ function AccountsPage() { } /> - + syncMut.mutate(a.id)} @@ -223,7 +246,21 @@ function AccountsPage() { Добавить} + actions={ +
+ {syncableCount > 0 ? ( + + ) : null} + +
+ } /> { + 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[] = [ { @@ -79,7 +109,18 @@ function TariffsPage() { return ( - + 0 ? ( + + ) : undefined + } + /> } empty={snapshot?.activeTariffs.length === 0} emptyTitle="Тарифы не загружены" - emptyDescription="Выполните синхронизацию аккаунта BILLmanager, чтобы загрузить тарифы" + emptyDescription="Синхронизация аккаунта BILLmanager загружает тарифы вместе с VPS и платежами" emptyAction={ - +
+ {syncableCount > 0 ? ( + + ) : null} + +
} > {(snap) => t.id} />}