import { useQuery } from '@tanstack/react-query'; import { useTRPC } from '@barev/web'; import type { Me } from '../App.tsx'; import { useT, type BoKey } from '../i18n.tsx'; import { I, PlanBadge, Skeleton, sourceKeyLabel } from '../components/kit.tsx'; type Pair = { orders: number; subs: number }; const total = (p: Pair) => p.orders + p.subs; function greetingKey(): BoKey { const h = Number(new Intl.DateTimeFormat('en-GB', { hour: 'numeric', hour12: false, timeZone: 'Asia/Yerevan' }).format(new Date())); return h < 12 ? 'dash.morning' : h < 18 ? 'dash.day' : 'dash.evening'; } export function Dashboard({ me }: { me: Me }) { const { t } = useT(); const trpc = useTRPC(); const attention = useQuery({ ...trpc.bo.dashboard.attention.queryOptions(), refetchInterval: 30_000 }); const ov = useQuery({ ...trpc.bo.dashboard.overview.queryOptions(), refetchInterval: 60_000 }); const firstName = me.name.split(' ')[0]; return ( <>

{t(greetingKey(), { name: firstName })}

{!ov.data ? (
) : ( <> {ov.data.money && }
{ov.data.team && }
)} ); } // ───── требует внимания ───── type Att = { unassigned: number; unreadChats: number; webRequests: number; leads: number; appRequests: number; lowRatings: number; expiring: number }; const ATT: { key: keyof Att; href: string; urgent?: boolean; fill: string; ink: string }[] = [ { key: 'unassigned', href: '#/orders?group=new', urgent: true, fill: 'var(--bc-accent)', ink: '#fff' }, { key: 'unreadChats', href: '#/chats', urgent: true, fill: 'var(--bc-accent)', ink: '#fff' }, { key: 'webRequests', href: '#/requests', fill: 'var(--bc-blue)', ink: 'var(--bc-blue-ink)' }, { key: 'leads', href: '#/moves', fill: 'var(--bc-docs)', ink: 'var(--bc-docs-ink)' }, { key: 'appRequests', href: '#/requests', fill: 'var(--bc-learn)', ink: 'var(--bc-learn-ink)' }, { key: 'lowRatings', href: '#/providers', fill: 'var(--bc-pink)', ink: 'var(--bc-plum)' }, { key: 'expiring', href: '#/clients?expiring=7', fill: 'var(--bc-perks)', ink: 'var(--bc-perks-ink)' }, ]; function Attention({ data }: { data?: Att }) { const { t } = useT(); if (!data) return
; const items = ATT.filter((a) => data[a.key] > 0); return (
{items.length === 0 ? ( {t('dash.allClear')} ) : ( items.map((a) => { const external = a.href.startsWith('http'); return ( {data[a.key]} {t(`att.${a.key}`)} ); }) )}
); } // ───── деньги ───── function Delta({ cur, prev, label }: { cur: number; prev: number; label: string }) { if (prev <= 0) return null; const pct = Math.round(((cur - prev) / prev) * 100); const up = pct >= 0; return ( {Math.abs(pct)}% ); } function Money({ m }: { m: { today: Pair; yesterday: Pair; week: Pair; prevWeek: Pair; month: Pair; prevMonth: Pair; series: ({ day: string } & Pair)[] } }) { const { t, amd, amdShort, locale } = useT(); const periods = [ { label: t('dash.today'), cur: m.today, prev: m.yesterday, extra: t('dash.vsYesterday', { sum: amd(total(m.yesterday)) }) }, { label: t('dash.week'), cur: m.week, prev: m.prevWeek, extra: t('dash.vsPrev', { period: t('dash.week') }) }, { label: t('dash.month'), cur: m.month, prev: m.prevMonth, extra: t('dash.vsPrev', { period: t('dash.month') }) }, ]; const max = Math.max(1, ...m.series.map(total)); const dayFmt = new Intl.DateTimeFormat(locale === 'en' ? 'en-GB' : locale === 'hy' ? 'hy-AM' : 'ru-RU', { day: 'numeric', month: 'short' }); const ORDERS = 'var(--bc-pink)'; const SUBS = 'var(--bc-perks)'; return (
{periods.map((p) => (
{p.label}
{amd(total(p.cur))}
{t('dash.orders')} {amdShort(p.cur.orders)} {t('dash.subs')} {amdShort(p.cur.subs)}
))}
{m.series.map((d) => { const date = dayFmt.format(new Date(`${d.day}T12:00:00+04:00`)); return (
{d.orders > 0 && } {d.subs > 0 && }
); })}
{t('dash.chart')} {t('dash.orders')} {t('dash.subs')}
); } // ───── подписки ───── interface SubsData { plus: number; elite: number; gifts: number; expiring: { clientId: string; name: string; plan: 'BAREV_PLUS' | 'ELITE'; endsAt: Date; source: string }[]; giftClients: number; giftConverted: number; } function Subs({ s }: { s: SubsData }) { const { t, date } = useT(); const conv = s.giftClients ? Math.round((s.giftConverted / s.giftClients) * 100) : 0; return (

{t('dash.subsTitle')}

{t('nav.clients')}
{s.plus}
{s.elite}
{t('dash.conversion')}
{conv}%

{t('dash.gifts', { n: s.gifts })}. {t('dash.conversionHint', { paid: s.giftConverted, total: s.giftClients })}.

{t('dash.expiring')}

{s.expiring.length === 0 ? (

{t('dash.noExpiring')}

) : (
    {s.expiring.map((e) => (
  • {e.name} {date(e.endsAt)}
  • ))}
)}
); } // ───── рост ───── interface GrowthData { today: number; week: number; month: number; prevMonth: number; sources: { key: string; count: number }[]; funnel: { leads: number; clients: number; paying: number }; } function Growth({ g }: { g: GrowthData }) { const { t } = useT(); const maxSrc = Math.max(1, ...g.sources.map((s) => s.count)); const steps = [ { key: 'funnel.leads' as const, n: g.funnel.leads, fill: 'var(--bc-blue)', ink: 'var(--bc-blue-ink)' }, { key: 'funnel.clients' as const, n: g.funnel.clients, fill: 'var(--bc-learn)', ink: 'var(--bc-learn-ink)' }, { key: 'funnel.paying' as const, n: g.funnel.paying, fill: 'var(--bc-home)', ink: 'var(--bc-home-ink)' }, ]; const maxStep = Math.max(1, ...steps.map((s) => s.n)); return (

{t('dash.growth')}

{t('dash.newClients')}
{g.month} {t('dash.month')} {g.week} {' '} {t('dash.week')} {g.today} {' '} {t('dash.today')}

{t('dash.prevMonth', { n: g.prevMonth })}

{t('dash.sources')}

{g.sources.length === 0 ? (

{t('dash.empty')}

) : ( g.sources.slice(0, 5).map((s) => (
{sourceKeyLabel(t, s.key)} {s.count}
)) )}

{t('dash.funnel')}

{steps.map((s) => (
{t(s.key)} {s.n}
))}
); } // ───── команда ───── interface TeamData { topProviders: { id: string; name: string; avg: number; count: number }[]; bottomProviders: { id: string; name: string; avg: number; count: number }[]; topPartners: { id: string; name: string; scans: number }[]; } function TeamBlock({ team }: { team: TeamData }) { const { t } = useT(); const prov = (p: TeamData['topProviders'][number]) => (
  • {p.name} {t('dash.ratings', { n: p.count })} ★ {p.avg.toFixed(1)}
  • ); return (

    {t('dash.team')}

    {t('dash.topProviders')}

    {team.topProviders.length ?
      {team.topProviders.map(prov)}
    :

    {t('dash.empty')}

    } {team.bottomProviders.length > 0 && ( <>

    {t('dash.bottomProviders')}

      {team.bottomProviders.map(prov)}
    )}

    {t('dash.topPartners')}

    {team.topPartners.length ? (
      {team.topPartners.map((p) => (
    • {p.name} {t('dash.scans', { n: p.scans })}
    • ))}
    ) : (

    {t('dash.empty')}

    )}

    {t('dash.salesSoon')}

    ); } // ───── лента ───── type FeedItem = | { kind: 'subscription'; at: Date; clientId: string; who: string; plan: 'BAREV_PLUS' | 'ELITE'; source: string; amount: number | null } | { kind: 'payment'; at: Date; clientId: string; who: string; service: string; number: number; amount: number | null } | { kind: 'action'; at: Date; by: string | null; action: string; entityId: string | null }; function Feed({ items, wide }: { items: FeedItem[]; wide: boolean }) { const { t, dateTime, amd, shared } = useT(); return (

    {t('dash.feed')}

    {items.length === 0 ? (

    {t('dash.empty')}

    ) : (
      {items.map((f, i) => (
    • {dateTime(f.at)} {f.kind === 'subscription' && ( <> {f.who} · {t(`subSource.${f.source}`)} {f.amount != null && {amd(f.amount)}} )} {f.kind === 'payment' && ( <> {f.who} · {shared(`services.${f.service}.name`)} · {t('feed.payment', { number: f.number })} {f.amount != null && {amd(f.amount)}} )} {f.kind === 'action' && ( {f.by ?? '—'} {actionLabel(t, f.action)} )}
    • ))}
    )}
    ); } export function actionLabel(t: ReturnType['t'], action: string) { const key = `audit.${action}`; const tr = t(key); return tr === key ? action : tr; }