Apps, web, i18n, UI, partners, payouts, backoffice, bariki, prod infra
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
45d7512d2b
commit
3489e0b816
319 files changed
+45483
-121
No files matched your search
@@ -0,0 +1,154 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user