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:
1 parent
23ffb2bf1d
commit
2772837e29
14 files changed
+321
-13
No files matched your search
@@ -379,6 +379,46 @@
|
||||
background: var(--bc-learn);
|
||||
color: var(--bc-learn-ink);
|
||||
}
|
||||
/* исполнитель: текущий заказ в шапке и заказ над каждым его сообщением */
|
||||
.bo-conv__banner--order {
|
||||
background: var(--bc-docs);
|
||||
color: var(--bc-docs-ink);
|
||||
align-items: flex-start;
|
||||
}
|
||||
.bo-conv__banner--order.is-free {
|
||||
background: var(--bo-soft);
|
||||
color: var(--bc-ink);
|
||||
}
|
||||
.bo-conv__banner--order svg {
|
||||
flex: none;
|
||||
margin-top: 2px;
|
||||
}
|
||||
.bo-conv__next {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
opacity: 0.75;
|
||||
}
|
||||
.bo-ordertag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
max-width: 100%;
|
||||
margin: -2px 0 6px;
|
||||
padding: 3px 9px;
|
||||
border-radius: var(--bc-pill);
|
||||
background: var(--bc-docs);
|
||||
color: var(--bc-docs-ink);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.bo-ordertag--none {
|
||||
background: var(--bo-soft);
|
||||
color: var(--bc-ink-2);
|
||||
}
|
||||
.bo-conv__closed {
|
||||
padding: 8px 18px;
|
||||
font-size: 13px;
|
||||
|
||||
@@ -386,6 +386,14 @@ const ru = {
|
||||
'chat.notLinked': 'Рабочий бот не открыт',
|
||||
'chat.details': 'Подробнее',
|
||||
'chat.more': 'Ещё действия',
|
||||
'chat.order': 'Заказ №{n}',
|
||||
'chat.noOrder': 'Без заказа',
|
||||
'chat.noOrderNow': 'Сейчас без заказа',
|
||||
'chat.orderNow': 'Сейчас',
|
||||
'chat.orderNext': 'Ближайший',
|
||||
'chat.orderNoNext': 'Ближайших заказов нет',
|
||||
'chat.orderClient': 'клиент {name}',
|
||||
'chat.wroteNoOrder': 'писал(а) без заказа',
|
||||
'err.CONCIERGE_SUBSCRIBERS_ONLY': 'Консьерж — только для подписчиков. Сначала выдайте клиенту подписку.',
|
||||
'err.ASSIGNEE_NO_ACCESS': 'У этого сотрудника нет доступа к этому каналу.',
|
||||
'err.PROVIDER_NOT_LINKED': 'Исполнитель ещё не открыл рабочий бот — написать ему пока некуда.',
|
||||
@@ -784,6 +792,14 @@ const en: Dict = {
|
||||
'chat.notLinked': 'Work bot not opened',
|
||||
'chat.details': 'Details',
|
||||
'chat.more': 'More actions',
|
||||
'chat.order': 'Order #{n}',
|
||||
'chat.noOrder': 'No order',
|
||||
'chat.noOrderNow': 'No current order',
|
||||
'chat.orderNow': 'Now',
|
||||
'chat.orderNext': 'Next',
|
||||
'chat.orderNoNext': 'No upcoming orders',
|
||||
'chat.orderClient': 'client {name}',
|
||||
'chat.wroteNoOrder': 'wrote without an order',
|
||||
'err.CONCIERGE_SUBSCRIBERS_ONLY': 'The concierge is for members only. Give the client a subscription first.',
|
||||
'err.ASSIGNEE_NO_ACCESS': 'This team member has no access to this channel.',
|
||||
'err.PROVIDER_NOT_LINKED': 'The provider hasn’t opened the work bot yet.',
|
||||
@@ -1179,6 +1195,14 @@ const hy: Dict = {
|
||||
'chat.notLinked': 'Աշխատանքային բոտը բացված չէ',
|
||||
'chat.details': 'Մանրամասներ',
|
||||
'chat.more': 'Այլ գործողություններ',
|
||||
'chat.order': 'Պատվեր №{n}',
|
||||
'chat.noOrder': 'Առանց պատվերի',
|
||||
'chat.noOrderNow': 'Այժմ պատվեր չկա',
|
||||
'chat.orderNow': 'Այժմ',
|
||||
'chat.orderNext': 'Հաջորդը',
|
||||
'chat.orderNoNext': 'Մոտակա պատվերներ չկան',
|
||||
'chat.orderClient': 'հաճախորդ {name}',
|
||||
'chat.wroteNoOrder': 'գրել է առանց պատվերի',
|
||||
'err.CONCIERGE_SUBSCRIBERS_ONLY': 'Կոնսիերժը միայն բաժանորդների համար է։ Նախ տրամադրեք բաժանորդագրություն։',
|
||||
'err.ASSIGNEE_NO_ACCESS': 'Այս աշխատակիցը մուտք չունի այս ալիքին։',
|
||||
'err.PROVIDER_NOT_LINKED': 'Կատարողը դեռ չի բացել աշխատանքային բոտը։',
|
||||
|
||||
@@ -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`);
|
||||
|
||||
// ───── поле ответа ─────
|
||||
|
||||
Reference in new issue
Block a user