Files
BarevClub/apps/admin/src/pages/WebRequests.tsx
T
2026-10-25 22:55:29 +04:00

155 lines
6.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { formatAmd } from '@barev/i18n';
import { Badge, Button, Chip } from '@barev/ui';
import { confirmDialog, errorText, useI18n, useToast, useTRPC } from '@barev/web';
import { useState } from 'react';
const STATUSES = ['NEW', 'LINKED', 'ORDERED', 'CANCELLED'] as const;
type Status = (typeof STATUSES)[number];
const STATUS_TONE: Record<Status, string> = {
NEW: 'var(--bc-perks)',
LINKED: 'var(--bc-pink)',
ORDERED: 'var(--bc-home)',
CANCELLED: 'var(--bc-line)',
};
/** Заявки с сайта (barevclub.com/clean): появляются сразу после отправки формы, список обновляется сам */
export function WebRequests() {
const { t, dt, locale } = useI18n();
const trpc = useTRPC();
const qc = useQueryClient();
const toast = useToast();
const [status, setStatus] = useState<Status | undefined>();
const list = useQuery({ ...trpc.admin.webRequests.list.queryOptions({ status }), refetchInterval: 15_000 });
const update = useMutation(
trpc.admin.webRequests.update.mutationOptions({
onSuccess: () => {
toast(t('admin.saved'));
void qc.invalidateQueries();
},
onError: (e) => toast(errorText(t, e), 'error'),
}),
);
return (
<>
<div className="bc-between">
<h1 className="bc-h2">{t('admin.nav.webRequests')}</h1>
<div className="bc-chips">
<Chip pressed={!status} onClick={() => setStatus(undefined)}>
{t('admin.webRequests.all')}
</Chip>
{STATUSES.map((s) => (
<Chip key={s} pressed={status === s} onClick={() => setStatus(s)}>
{t(`admin.webRequests.status_${s}`)}
</Chip>
))}
</div>
</div>
{list.data?.length === 0 ? (
<p className="bc-muted">{t('admin.webRequests.empty')}</p>
) : (
<table className="grid">
<thead>
<tr>
<th>{t('admin.webRequests.number')}</th>
<th>{t('admin.webRequests.when')}</th>
<th>{t('admin.webRequests.what')}</th>
<th>{t('admin.webRequests.address')}</th>
<th>{t('admin.webRequests.contact')}</th>
<th>{t('admin.webRequests.price')}</th>
<th>{t('admin.webRequests.status')}</th>
<th>{t('admin.webRequests.note')}</th>
</tr>
</thead>
<tbody>
{list.data?.map((r) => {
const p = r.params as { cleaningType?: string; rooms?: number; bathrooms?: number; extras?: Record<string, number> };
const extras = Object.entries(p.extras ?? {});
return (
<tr key={r.id}>
<td>
<b>{r.number}</b>
<div className="bc-small bc-muted">{t('admin.webRequests.created', { date: dt(r.createdAt, { day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit' }) })}</div>
</td>
<td className="bc-small">{dt(r.preferredAt, { weekday: 'short', day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit' })}</td>
<td>
<b>{t(`params.cleaning.cleaningType.${p.cleaningType ?? 'regular'}`)}</b>
<div className="bc-small">
{t('params.cleaning.rooms.count', { count: p.rooms ?? 1 })}, {t('params.cleaning.bathrooms.count', { count: p.bathrooms ?? 1 })}
</div>
{extras.length > 0 && (
<div className="bc-small bc-muted">{extras.map(([code, qty]) => `${t(`extras.${code}.name`)}${qty > 1 ? ` × ${qty}` : ''}`).join(', ')}</div>
)}
</td>
<td className="bc-small">
{t(`districts.${r.district.code}`)}
<div>{r.addressText}</div>
{r.note && <div className="bc-muted">{r.note}</div>}
</td>
<td>
{r.name && <b>{r.name}</b>}
<div>
<a href={`tel:${r.phone}`}>{r.phone}</a>
</div>
{r.client && (
<div className="bc-small bc-muted">
{r.client.user.username ? `@${r.client.user.username}` : `${r.client.user.firstName ?? ''} ${r.client.user.lastName ?? ''}`}
</div>
)}
<div className="bc-small bc-muted">{r.locale.toUpperCase()}</div>
</td>
<td className="mono">{formatAmd(locale, r.totalPrice)}</td>
<td>
<Badge style={{ background: STATUS_TONE[r.status] }}>{t(`admin.webRequests.status_${r.status}`)}</Badge>
{r.order && (
<div className="bc-small" style={{ marginTop: 6 }}>
{t('admin.webRequests.order', { number: r.order.number })}
</div>
)}
<div style={{ marginTop: 8 }}>
{r.status === 'CANCELLED' ? (
<Button size="sm" variant="light" onClick={() => update.mutate({ id: r.id, status: 'REOPEN' })}>
{t('admin.webRequests.reopen')}
</Button>
) : (
r.status !== 'ORDERED' && (
<Button
size="sm"
variant="light"
onClick={async () => (await confirmDialog(t('admin.webRequests.cancelConfirm', { number: r.number }))) && update.mutate({ id: r.id, status: 'CANCELLED' })}
>
{t('admin.webRequests.cancel')}
</Button>
)
)}
</div>
</td>
<td style={{ minWidth: 200 }}>
<NoteEditor initial={r.adminNote ?? ''} onSave={(adminNote) => update.mutate({ id: r.id, adminNote })} />
</td>
</tr>
);
})}
</tbody>
</table>
)}
</>
);
}
function NoteEditor({ initial, onSave }: { initial: string; onSave: (v: string) => void }) {
const { t } = useI18n();
const [value, setValue] = useState(initial);
return (
<div className="bc-stack" style={{ gap: 6 }}>
<textarea className="bc-input" style={{ minHeight: 60 }} value={value} maxLength={1000} placeholder={t('admin.webRequests.notePlaceholder')} onChange={(e) => setValue(e.target.value)} />
{value !== initial && (
<Button size="sm" variant="outline" onClick={() => onSave(value)}>
{t('admin.webRequests.saveNote')}
</Button>
)}
</div>
);
}