import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useToast, useTRPC } from '@barev/web'; import { useEffect, useState } from 'react'; import type { Me } from '../App.tsx'; import { errText, useT, type BoKey } from '../i18n.tsx'; import { Avatar, I, PlanBadge, Skeleton, sourceLabel } from '../components/kit.tsx'; import { actionLabel } from './Dashboard.tsx'; import { SubDialog, type SubMode, type SubTarget } from './SubDialog.tsx'; const FLOWS = ['CLEANING', 'REPAIR', 'TRANSFER', 'LANGUAGE', 'CAREER', 'DOCUMENTS']; const DAY = 86_400_000; export function ClientCard({ id, me }: { id: string; me: Me }) { const { t, amd, date, dateTime, shared } = useT(); const trpc = useTRPC(); const c = useQuery(trpc.bo.clients.get.queryOptions({ id })); const [dlg, setDlg] = useState<{ mode: SubMode; sub?: SubTarget } | null>(null); const isSuper = me.roles.includes('SUPERADMIN'); const canManage = isSuper || me.roles.includes('SUPPORT'); if (c.isError) return ( <> {t('card.back')}
{errText(t, c.error)}
); if (!c.data) return (
); const d = c.data; const current = d.subscriptions.find((s) => s.active && s.plan === 'ELITE') ?? d.subscriptions.find((s) => s.active); const statusText = (status: string) => { for (const f of FLOWS) { const k = `status.${f}.${status}`; const v = shared(k); if (v !== k) return v; } return status; }; return ( <> {t('card.back')}

{d.name}

{d.username && @{d.username}} {d.phone && {d.phone}} {t('card.since', { date: date(d.createdAt) })}
{d.username && ( {t('card.telegram')} )}
{/* посадочный талон текущей подписки */}
{current ? t(`plan.${current.plan}`) : t('card.noSub')}
{current ? ( <> {t(`subSource.${current.source}`)} {current.grantedBy && {t('card.grantedBy', { name: current.grantedBy })}} {current.agency && {t('card.giftFrom', { name: current.agency })}} {current.payMethod && {t(`pay.${current.payMethod}`)}} ) : ( {t('card.noSubHint')} )}
{current ? (
{date(current.endsAt)}
{t('card.daysLeft', { n: Math.max(0, Math.ceil((current.endsAt.getTime() - Date.now()) / DAY)) })}
) : ( )}
{t('card.cardNo', { code: d.cardCode })}
{canManage && (
{current && ( <> )}
)}
{d.subscriptions.length > 0 && (

{t('card.history')}

{d.subscriptions.map((s) => { const upcoming = s.status === 'ACTIVE' && s.startsAt.getTime() > Date.now(); const off = !s.active && !upcoming; return (
{date(s.startsAt)} — {date(s.endsAt)} {s.amount != null && {amd(s.amount)}}
{[ upcoming ? t('card.upcoming', { date: date(s.startsAt) }) : t(`subStatus.${s.status}`), t(`subSource.${s.source}`), s.grantedBy && t('card.grantedBy', { name: s.grantedBy }), s.agency && t('card.giftFrom', { name: s.agency }), s.payMethod && t(`pay.${s.payMethod}`), s.refundAmount ? t('card.refunded', { sum: amd(s.refundAmount) }) : null, s.cancelledAt && t('card.cancelledAt', { date: date(s.cancelledAt) }), ] .filter(Boolean) .join(', ')}
{s.note &&
{s.note}
}
{canManage && s.status === 'ACTIVE' && !s.active && upcoming && ( )} {isSuper && s.amount != null && s.amount > (s.refundAmount ?? 0) && ( )}
); })}
)}

{t('card.orders')}

{d.orders.length === 0 ? (

{t('card.noOrders')}

) : (
    {d.orders.map((o) => (
  • №{o.number} {shared(`services.${o.service}.name`)} {dateTime(o.scheduledAt)} {o.provider && `, ${o.provider}`} {o.rating && ` ★ ${o.rating}`} {statusText(o.status)} {o.total != null && ( {amd(o.total)} {shared(`payment.${o.paymentStatus}`)} )}
  • ))}
)}
{d.money && (
{t('card.money')}
{amd(d.money.total)}
{t('card.fromSubs')} {amd(d.money.subs)} {t('card.fromOrders')} {amd(d.money.orders)}
)}

{t('card.profile')}

{t('card.source')}
{sourceLabel(t, d)}
{d.agency && ( <>
{t('card.agency')}
{d.agency.name} {d.agencyAttachedAt && ({date(d.agencyAttachedAt)})}
)}
{t('card.language')}
{d.locale.toUpperCase()}
{d.segment && ( <>
{t('card.segment')}
{t(`segment.${d.segment}`)}
)}
{t('card.consent')}
{d.consentAt ? date(d.consentAt) : t('ui.no')}

{t('card.stats')}

{( [ ['cardUsages', d.counts.cardUsages], ['concierge', d.counts.concierge], ['requests', d.counts.requests], ['leads', d.counts.leads], ] as [string, number][] ).map(([k, n]) => (
{n} {t(`stat.${k}` as BoKey)}
))}

{t('card.activity')}

{d.history.length === 0 ? (

{t('card.noActivity')}

) : (
    {d.history.map((h) => (
  • {dateTime(h.at)} {h.by ?? '—'} {actionLabel(t, h.action)}
  • ))}
)}
{dlg && setDlg(null)} />} ); } function TagsAndNote({ id, tags, note }: { id: string; tags: string[]; note: string }) { const { t } = useT(); const trpc = useTRPC(); const qc = useQueryClient(); const toast = useToast(); const [draftTag, setDraftTag] = useState(''); const [text, setText] = useState(note); useEffect(() => { setText(note); }, [note]); const facets = useQuery(trpc.bo.clients.facets.queryOptions()); const refresh = () => { qc.invalidateQueries({ queryKey: trpc.bo.clients.get.queryKey({ id }) }); qc.invalidateQueries({ queryKey: trpc.bo.clients.facets.queryKey() }); qc.invalidateQueries({ queryKey: trpc.bo.clients.list.queryKey() }); }; const setTags = useMutation(trpc.bo.clients.setTags.mutationOptions({ onSuccess: refresh, onError: (e) => toast(errText(t, e), 'error') })); const setNote = useMutation( trpc.bo.clients.setNote.mutationOptions({ onSuccess: () => { refresh(); toast(t('card.saved')); }, onError: (e) => toast(errText(t, e), 'error'), }), ); const add = () => { const v = draftTag.trim(); if (v && !tags.includes(v)) setTags.mutate({ id, tags: [...tags, v] }); setDraftTag(''); }; const suggestions = (facets.data?.tags ?? []).map((x) => x.tag).filter((x) => !tags.includes(x)); return (

{t('card.tags')}

{tags.map((tg) => ( {tg} ))}
{ e.preventDefault(); add(); }} > setDraftTag(e.target.value)} placeholder={t('card.addTag')} maxLength={40} style={{ minHeight: 36, padding: '6px 12px' }} /> {suggestions.map((s) => (

{t('card.note')}