Order card: 'Message provider' and 'Call' buttons, also for a pending offer

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:32:36 +04:00
1 parent e7de4f5893
commit a848bb13e2
5 files changed
+49 -7

No files matched your search

+4 -2
View File
@@ -196,7 +196,7 @@ export const boOrdersRouter = router({
client: { include: { user: true, subscriptions: { where: { status: 'ACTIVE' } } } }, client: { include: { user: true, subscriptions: { where: { status: 'ACTIVE' } } } },
history: { orderBy: { createdAt: 'asc' } }, history: { orderBy: { createdAt: 'asc' } },
checklist: { orderBy: { sortOrder: 'asc' } }, checklist: { orderBy: { sortOrder: 'asc' } },
offers: { orderBy: { createdAt: 'desc' }, include: { provider: { select: { name: true } }, company: { select: { name: true } } } }, offers: { orderBy: { createdAt: 'desc' }, include: { provider: { select: { id: true, name: true, phone: true } }, company: { select: { name: true } } } },
files: { where: { deletedAt: null }, select: { id: true, kind: true, mime: true, createdAt: true } }, files: { where: { deletedAt: null }, select: { id: true, kind: true, mime: true, createdAt: true } },
paymentMethod: true, paymentMethod: true,
rating: true, rating: true,
@@ -233,7 +233,9 @@ export const boOrdersRouter = router({
client: { id: o.clientId, name: name(o.client.user), username: o.client.user.username, phone: o.client.user.phone, plan: sub?.plan ?? null }, client: { id: o.clientId, name: name(o.client.user), username: o.client.user.username, phone: o.client.user.phone, plan: sub?.plan ?? null },
provider: o.provider ? { id: o.provider.id, name: o.provider.name, phone: o.provider.phone, username: o.provider.user?.username ?? null } : null, provider: o.provider ? { id: o.provider.id, name: o.provider.name, phone: o.provider.phone, username: o.provider.user?.username ?? null } : null,
company: o.company ? { id: o.company.id, name: o.company.name } : null, company: o.company ? { id: o.company.id, name: o.company.name } : null,
pending: pending ? { id: pending.id, name: pending.provider?.name ?? pending.company?.name ?? '', expiresAt: pending.expiresAt, auto: !pending.createdById, company: !pending.providerId } : null, pending: pending
? { id: pending.id, name: pending.provider?.name ?? pending.company?.name ?? '', providerId: pending.provider?.id ?? null, phone: pending.provider?.phone ?? null, expiresAt: pending.expiresAt, auto: !pending.createdById, company: !pending.providerId }
: null,
offers: o.offers.map((x) => ({ offers: o.offers.map((x) => ({
id: x.id, id: x.id,
name: x.provider?.name ?? x.company?.name ?? '', name: x.provider?.name ?? x.company?.name ?? '',
+1
View File
@@ -37,6 +37,7 @@ const PATHS = {
archive: 'M4 5h16v4H4zM5 9v10h14V9M10 13h4', archive: 'M4 5h16v4H4zM5 9v10h14V9M10 13h4',
bot: 'M6 9h12v10H6zM12 5v4M9.5 13.5v.01M14.5 13.5v.01M9 17h6M3 13v2M21 13v2', bot: 'M6 9h12v10H6zM12 5v4M9.5 13.5v.01M14.5 13.5v.01M9 17h6M3 13v2M21 13v2',
warn: 'M12 3l10 18H2zM12 10v5M12 18v.01', warn: 'M12 3l10 18H2zM12 10v5M12 18v.01',
phone: 'M5 4h4l2 5-2.5 1.5a11 11 0 0 0 5 5L15 13l5 2v4a2 2 0 0 1-2 2A16 16 0 0 1 3 6a2 2 0 0 1 2-2z',
} as const; } as const;
export type IconName = keyof typeof PATHS; export type IconName = keyof typeof PATHS;
+6
View File
@@ -433,6 +433,8 @@ const ru = {
'ord.client': 'Клиент', 'ord.client': 'Клиент',
'ord.writeClient': 'Написать клиенту', 'ord.writeClient': 'Написать клиенту',
'ord.writeProvider': 'Написать исполнителю', 'ord.writeProvider': 'Написать исполнителю',
'ord.call': 'Позвонить',
'ord.noPhone': 'Телефон исполнителя не указан — добавьте его в профиле',
'ord.money': 'Деньги', 'ord.money': 'Деньги',
'ord.base': 'Без скидки', 'ord.base': 'Без скидки',
'ord.discount': 'Скидка {pct}%', 'ord.discount': 'Скидка {pct}%',
@@ -1075,6 +1077,8 @@ const en: Dict = {
'ord.client': 'Client', 'ord.client': 'Client',
'ord.writeClient': 'Message the client', 'ord.writeClient': 'Message the client',
'ord.writeProvider': 'Message the provider', 'ord.writeProvider': 'Message the provider',
'ord.call': 'Call',
'ord.noPhone': 'No phone number — add it to the profile',
'ord.money': 'Money', 'ord.money': 'Money',
'ord.base': 'Before discount', 'ord.base': 'Before discount',
'ord.discount': 'Discount {pct}%', 'ord.discount': 'Discount {pct}%',
@@ -1714,6 +1718,8 @@ const hy: Dict = {
'ord.client': 'Հաճախորդ', 'ord.client': 'Հաճախորդ',
'ord.writeClient': 'Գրել հաճախորդին', 'ord.writeClient': 'Գրել հաճախորդին',
'ord.writeProvider': 'Գրել կատարողին', 'ord.writeProvider': 'Գրել կատարողին',
'ord.call': 'Զանգահարել',
'ord.noPhone': 'Հեռախոսահամարը նշված չէ',
'ord.money': 'Գումար', 'ord.money': 'Գումար',
'ord.base': 'Առանց զեղչի', 'ord.base': 'Առանց զեղչի',
'ord.discount': 'Զեղչ {pct}%', 'ord.discount': 'Զեղչ {pct}%',
+6
View File
@@ -352,6 +352,12 @@
} }
/* подбор исполнителя */ /* подбор исполнителя */
.bo-contact {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 12px;
}
.bo-pendingbox { .bo-pendingbox {
display: flex; display: flex;
align-items: center; align-items: center;
+32 -5
View File
@@ -419,6 +419,7 @@ function ProviderPanel({ order: d, onChanged, canChat }: { order: OrderData; onC
</div> </div>
{d.provider ? ( {d.provider ? (
<>
<div className="bo-person"> <div className="bo-person">
<Avatar name={d.provider.name} id={d.provider.id} size={40} /> <Avatar name={d.provider.name} id={d.provider.id} size={40} />
<a href={`#/providers/${d.provider.id}`} style={{ minWidth: 0, color: 'inherit', textDecoration: 'none' }}> <a href={`#/providers/${d.provider.id}`} style={{ minWidth: 0, color: 'inherit', textDecoration: 'none' }}>
@@ -427,13 +428,11 @@ function ProviderPanel({ order: d, onChanged, canChat }: { order: OrderData; onC
{[d.company?.name, d.provider.phone, d.provider.username && `@${d.provider.username}`].filter(Boolean).join(' · ') || '—'} {[d.company?.name, d.provider.phone, d.provider.username && `@${d.provider.username}`].filter(Boolean).join(' · ') || '—'}
</span> </span>
</a> </a>
{canChat && (
<button type="button" className="bo-iconbtn" title={t('ord.writeProvider')} aria-label={t('ord.writeProvider')} onClick={() => chat.mutate({ providerId: d.provider!.id })}>
<I name="chats" size={20} />
</button>
)}
</div> </div>
<ContactRow providerId={d.provider.id} phone={d.provider.phone} canChat={canChat} onChat={(id) => chat.mutate({ providerId: id })} busy={chat.isPending} />
</>
) : d.pending ? ( ) : d.pending ? (
<>
<div className="bo-pendingbox"> <div className="bo-pendingbox">
{d.pending.auto ? <I name="bot" size={20} /> : <I name="send" size={20} />} {d.pending.auto ? <I name="bot" size={20} /> : <I name="send" size={20} />}
<span className="bo-grow"> <span className="bo-grow">
@@ -443,6 +442,8 @@ function ProviderPanel({ order: d, onChanged, canChat }: { order: OrderData; onC
</span> </span>
</span> </span>
</div> </div>
{d.pending.providerId && <ContactRow providerId={d.pending.providerId} phone={d.pending.phone} canChat={canChat} onChat={(id) => chat.mutate({ providerId: id })} busy={chat.isPending} />}
</>
) : d.canAssign ? ( ) : d.canAssign ? (
<> <>
<div className="bo-assignbar"> <div className="bo-assignbar">
@@ -496,6 +497,32 @@ function ProviderPanel({ order: d, onChanged, canChat }: { order: OrderData; onC
); );
} }
/** Связаться с исполнителем: чат в рабочем боте и звонок */
function ContactRow({ providerId, phone, canChat, onChat, busy }: { providerId: string; phone: string | null; canChat: boolean; onChat: (id: string) => void; busy: boolean }) {
const { t } = useT();
return (
<div className="bo-contact">
{canChat && (
<button type="button" className="bo-btn bo-btn--sm" onClick={() => onChat(providerId)} disabled={busy}>
<I name="chats" size={16} />
{t('ord.writeProvider')}
</button>
)}
{phone ? (
<a className="bo-btn bo-btn--ghost bo-btn--sm" href={`tel:${phone.replace(/[^\d+]/g, '')}`}>
<I name="phone" size={16} />
{t('ord.call')}
</a>
) : (
<span className="bo-btn bo-btn--ghost bo-btn--sm" aria-disabled="true" title={t('ord.noPhone')} style={{ opacity: 0.45, cursor: 'default' }}>
<I name="phone" size={16} />
{t('ord.call')}
</span>
)}
</div>
);
}
// ───── диалоги ───── // ───── диалоги ─────
function RescheduleDialog({ order: d, onClose, onDone }: { order: OrderData; onClose: () => void; onDone: () => void }) { function RescheduleDialog({ order: d, onClose, onDone }: { order: OrderData; onClose: () => void; onDone: () => void }) {