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')}
{/* посадочный талон текущей подписки */}
{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}
))}
{t('card.note')}
);
}