fix(sync): загрузка тарифов при обычной синхронизации аккаунтов
Docker / build (push) Has been cancelled
Docker / build (push) Has been cancelled
Обычный синк больше не пропускает тарифы; добавлены кнопки «Синхронизировать все» и «Загрузить тарифы», исправлен dropdown на странице аккаунтов. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -73,7 +73,7 @@ export const syncRoutes: FastifyPluginAsync = async (app) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const onlyTariffs = Boolean(req.body?.onlyTariffs)
|
const onlyTariffs = Boolean(req.body?.onlyTariffs)
|
||||||
const opts = onlyTariffs ? { skipVpsPayments: true } : { skipTariffs: true }
|
const opts = onlyTariffs ? { skipVpsPayments: true } : {}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await runBillmanagerAccountSync(syncRow, opts)
|
const result = await runBillmanagerAccountSync(syncRow, opts)
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ import {
|
|||||||
import { accountBalanceApi, accountBalanceCurrency } from '@/lib/account'
|
import { accountBalanceApi, accountBalanceCurrency } from '@/lib/account'
|
||||||
|
|
||||||
import type { ProviderAccount, BillingMode } from '@/types/entities'
|
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'
|
import { billingModeLabel, formatCurrency } from '@/lib/format'
|
||||||
|
|
||||||
export const Route = createFileRoute('/_auth/accounts')({
|
export const Route = createFileRoute('/_auth/accounts')({
|
||||||
@@ -98,9 +98,29 @@ function AccountsPage() {
|
|||||||
})
|
})
|
||||||
const syncMut = useMutation({
|
const syncMut = useMutation({
|
||||||
mutationFn: (id: string) => api.syncAccount(id),
|
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: () => {
|
onSuccess: () => {
|
||||||
void queryClient.invalidateQueries({ queryKey: ['snapshot'] })
|
void queryClient.invalidateQueries({ queryKey: ['snapshot'] })
|
||||||
toast.success('Синк запущен')
|
toast.success('Синхронизация всех аккаунтов завершена')
|
||||||
},
|
},
|
||||||
onError: (e: unknown) => toast.error(e instanceof ApiError ? e.message : 'Ошибка синка'),
|
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 providerById = snapshot ? providerByIdMap(snapshot.providers) : new Map()
|
||||||
|
const syncableCount = snapshot
|
||||||
|
? billmanagerSyncableAccounts(snapshot.providerAccounts, snapshot.providers).length
|
||||||
|
: 0
|
||||||
|
|
||||||
const columns: DataTableColumn<ProviderAccount>[] = [
|
const columns: DataTableColumn<ProviderAccount>[] = [
|
||||||
{
|
{
|
||||||
@@ -184,7 +207,7 @@ function AccountsPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent align="end" className="w-auto min-w-44">
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
disabled={!canSync || (syncMut.isPending && syncMut.variables === a.id)}
|
disabled={!canSync || (syncMut.isPending && syncMut.variables === a.id)}
|
||||||
onClick={() => syncMut.mutate(a.id)}
|
onClick={() => syncMut.mutate(a.id)}
|
||||||
@@ -223,7 +246,21 @@ function AccountsPage() {
|
|||||||
<PageHeader
|
<PageHeader
|
||||||
title="Аккаунты хостеров"
|
title="Аккаунты хостеров"
|
||||||
description="Аккаунты провайдеров с API-доступом"
|
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
|
<QueryState
|
||||||
data={snapshot}
|
data={snapshot}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { createFileRoute, Link } from '@tanstack/react-router'
|
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 { snapshotQueryOptions } from '@/queries/snapshot'
|
||||||
|
import { api, ApiError } from '@/lib/api-client'
|
||||||
import { PageShell } from '@/components/page-shell'
|
import { PageShell } from '@/components/page-shell'
|
||||||
import { PageHeader } from '@/components/page-header'
|
import { PageHeader } from '@/components/page-header'
|
||||||
import { Badge } from '@cfdm/ui/components/badge'
|
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 { QueryState } from '@/components/query-state'
|
||||||
import { TableSkeleton } from '@/components/skeletons'
|
import { TableSkeleton } from '@/components/skeletons'
|
||||||
import { Button } from '@cfdm/ui/components/button'
|
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 type { ActiveTariff } from '@/types/entities'
|
||||||
import { providerByIdMap, accountSelectLabel } from '@/lib/billmanager'
|
import { providerByIdMap, accountSelectLabel, billmanagerSyncableAccounts } from '@/lib/billmanager'
|
||||||
import { formatCurrency } from '@/lib/format'
|
import { formatCurrency } from '@/lib/format'
|
||||||
|
|
||||||
export const Route = createFileRoute('/_auth/tariffs')({
|
export const Route = createFileRoute('/_auth/tariffs')({
|
||||||
@@ -24,8 +26,36 @@ export const Route = createFileRoute('/_auth/tariffs')({
|
|||||||
})
|
})
|
||||||
|
|
||||||
function TariffsPage() {
|
function TariffsPage() {
|
||||||
|
const queryClient = useQueryClient()
|
||||||
const { data: snapshot, isLoading, isError, error, refetch } = useQuery(snapshotQueryOptions())
|
const { data: snapshot, isLoading, isError, error, refetch } = useQuery(snapshotQueryOptions())
|
||||||
const providerById = snapshot ? providerByIdMap(snapshot.providers) : new Map()
|
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>[] = [
|
const columns: DataTableColumn<ActiveTariff>[] = [
|
||||||
{
|
{
|
||||||
@@ -79,7 +109,18 @@ function TariffsPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PageShell>
|
<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
|
<QueryState
|
||||||
data={snapshot}
|
data={snapshot}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
@@ -89,9 +130,19 @@ function TariffsPage() {
|
|||||||
skeleton={<TableSkeleton />}
|
skeleton={<TableSkeleton />}
|
||||||
empty={snapshot?.activeTariffs.length === 0}
|
empty={snapshot?.activeTariffs.length === 0}
|
||||||
emptyTitle="Тарифы не загружены"
|
emptyTitle="Тарифы не загружены"
|
||||||
emptyDescription="Выполните синхронизацию аккаунта BILLmanager, чтобы загрузить тарифы"
|
emptyDescription="Синхронизация аккаунта BILLmanager загружает тарифы вместе с VPS и платежами"
|
||||||
emptyAction={
|
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} />}
|
{(snap) => <DataGridCard columns={columnDefFromDataTable(columns)} data={snap.activeTariffs} rowId={(t) => t.id} />}
|
||||||
|
|||||||
Reference in New Issue
Block a user