Chats: show which order a provider writes about, or that there is no order

- Provider messages store the order that was current when they wrote: a started
  order first, otherwise the nearest assigned one within -4h..+12h; none — "no order".
- Admin chat: order tag above each provider message, "now / next" order strip in
  the conversation header, order number (or "no order") in the thread list.
- Telegram notification to the team gets an order line or "no current order".
- Test for the order-picking rule; demo shows both cases.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
zabudko-ruandClaude Opus 5.5 committed 2026-10-21 02:02:31 +04:00
1 parent 23ffb2bf1d
commit 2772837e29
14 files changed
+321 -13

No files matched your search

+80
View File
@@ -169,6 +169,8 @@ type ListItem = {
waitingSince: Date | null;
lastMessageAt: Date;
last: { author: string; text: string; file: string | null } | null;
/** исполнитель: номер заказа последнего сообщения, null — без заказа */
lastOrder?: number | null;
unread: number;
};
@@ -204,6 +206,7 @@ function ThreadList({ items, loading, activeId, showChannel, meId, filtered }: {
</span>
<span className="bo-thread__meta">
{showChannel && <span className={`bo-chip bo-chip--${it.channel}`}>{t(`channel.${it.channel}`)}</span>}
{it.lastOrder !== undefined && <span className="bo-chip">{it.lastOrder ? t('chat.order', { n: it.lastOrder }) : t('chat.noOrder')}</span>}
{it.waitingSince && <span className={`bo-wait-chip bo-wait-chip--${urgency(it.waitingSince)}`}>{t('chat.waiting', { t: ago(t, it.waitingSince) })}</span>}
{it.botActive && (
<span className="bo-chip">
@@ -334,6 +337,7 @@ function Conversation({ id, me, channels, side, onToggleSide }: { id: string; me
</div>
</header>
{d.providerId && <ProviderOrders current={d.currentOrder} next={d.nextOrder} />}
{d.botActive && (
<div className="bo-conv__banner bo-conv__banner--bot">
<I name="bot" size={18} />
@@ -379,6 +383,7 @@ type Message = {
param: string | null;
deliveryError: string | null;
createdAt: Date;
order?: OrderBrief | null;
};
function Messages({ messages, locale, peerName }: { messages: Message[]; locale: Locale; peerName: string }) {
@@ -430,6 +435,7 @@ function Messages({ messages, locale, peerName }: { messages: Message[]; locale:
<Fragment key={m.id}>
{sep}
<div className={`bo-msg bo-msg--${kind}`}>
{m.author === 'PROVIDER' && m.order !== undefined && <OrderTag order={m.order} />}
{kind === 'note' && (
<div className="bo-msg__label">
<I name="lock" size={13} />
@@ -478,6 +484,80 @@ function FileView({ id, file }: { id: string; file: { name: string | null; mime:
);
}
// ───── заказ исполнителя ─────
type OrderBrief = {
id: string;
number: number;
service: string;
flow: string;
status: string;
scheduledAt: Date;
district: string | null;
address: string | null;
isOnline: boolean;
clientId: string;
clientName: string;
};
/** «Заказ №19 · Клининг · ср, 21 окт., 14:00 · Кентрон» */
function useOrderText() {
const { t, shared, locale } = useT();
return (o: OrderBrief, full = false) =>
[
t('chat.order', { n: o.number }),
shared(`services.${o.service}.name`),
formatDateTime(locale as Locale, o.scheduledAt, { weekday: 'short', day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit' }),
o.isOnline ? null : full && o.address ? o.address : o.district ? shared(`districts.${o.district}`) : null,
full ? shared(`status.${o.flow}.${o.status}`) : null,
full && o.clientName ? t('chat.orderClient', { name: o.clientName }) : null,
]
.filter(Boolean)
.join(' · ');
}
/** Над сообщением исполнителя: по какому заказу он писал, или что заказа не было */
function OrderTag({ order }: { order: OrderBrief | null }) {
const { t } = useT();
const text = useOrderText();
if (!order)
return (
<div className="bo-ordertag bo-ordertag--none">
<I name="orders" size={13} />
{t('chat.wroteNoOrder')}
</div>
);
return (
<a className="bo-ordertag" href={`#/clients/${order.clientId}`} title={t('chat.orderClient', { name: order.clientName })}>
<I name="orders" size={13} />
{text(order)}
</a>
);
}
/** Шапка диалога с исполнителем: что у него сейчас и что дальше */
function ProviderOrders({ current, next }: { current: OrderBrief | null; next: OrderBrief | null }) {
const { t } = useT();
const text = useOrderText();
return (
<div className={`bo-conv__banner bo-conv__banner--order${current ? '' : ' is-free'}`}>
<I name="orders" size={18} />
<span className="bo-grow" style={{ minWidth: 0 }}>
{current ? (
<>
<b>{t('chat.orderNow')}:</b> {text(current, true)}
</>
) : (
<b>{t('chat.noOrderNow')}</b>
)}
<span className="bo-conv__next">
{next ? `${t('chat.orderNext')}: ${text(next)}` : t('chat.orderNoNext')}
</span>
</span>
</div>
);
}
const fmtSize = (n: number) => (n < 1024 ? `${n} B` : n < 1024 * 1024 ? `${Math.round(n / 1024)} KB` : `${(n / 1024 / 1024).toFixed(1)} MB`);
// ───── поле ответа ─────