Backoffice stage 2: chats (support + concierge, files)
- ChatThread per client/provider and channel; messages get files, internal notes, source (bot/mini app) and delivery errors; reply templates. Migration moves old concierge messages into threads. - Routing: subscribers write to the concierge, others to support; /support and /concierge in the client bot; staff can take, transfer, close and move threads. - Telegram: photos/documents from clients and providers, replies with the channel header, files sent from storage, notifications only to the assignee or the channel's staff, "Reply" into a specific thread. - New admin: /chats with list, conversation, notes, files, templates, client panel; "Message" button on the client card; dashboard counts threads awaiting reply. - Client mini app: in-app chat screen sharing the same conversation. - Old admin keeps working on the same data (notes hidden there). - Tests for routing, notes, assign/close/move, lists, recipients; separate test DB via TEST_DB_NAME; dev:chats-demo script. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
d452824c62
commit
23ffb2bf1d
46 files changed
+4176
-144
No files matched your search
@@ -554,7 +554,7 @@ describe('приложение: переезд, Elite, заявки операт
|
||||
const clientId = await onboardedClient(ctx, 504);
|
||||
await addClientMessage(ctx, clientId, 'Как продлить визу?');
|
||||
expect((await listConversations(ctx))[0]).toMatchObject({ clientId, unread: 1 });
|
||||
await conversation(ctx, clientId);
|
||||
await conversation(ctx, { clientId });
|
||||
expect((await listConversations(ctx))[0]!.unread).toBe(0);
|
||||
const admin = await db.adminUser.create({ data: { email: 'op@test', name: 'Оператор', passwordHash: 'x' } });
|
||||
const msg = await replyToClient(ctx, { clientId, adminUserId: admin.id, text: 'Подскажем завтра утром' });
|
||||
@@ -582,12 +582,12 @@ describe('шаблоны переезда и переписка с исполн
|
||||
it('исполнитель пишет боту — операторам уведомление; ответ уходит исполнителю', async () => {
|
||||
const { ctx, events } = makeCtx(NOW);
|
||||
const provider = await db.provider.findFirstOrThrow({ where: { userId: { not: null } } });
|
||||
const msg = await addProviderMessage(ctx, provider.id, 'Клиент не открывает дверь');
|
||||
expect(events).toContainEqual({ type: 'concierge.message', messageId: msg!.id });
|
||||
const msg = (await addProviderMessage(ctx, provider.id, 'Клиент не открывает дверь'))!.message;
|
||||
expect(events).toContainEqual({ type: 'concierge.message', messageId: msg.id });
|
||||
const admin = await db.adminUser.create({ data: { email: 'op2@test', name: 'Оператор', passwordHash: 'x' } });
|
||||
const reply = await replyTo(ctx, { providerId: provider.id, adminUserId: admin.id, text: 'Звоним клиенту' });
|
||||
expect(reply).toMatchObject({ providerId: provider.id, author: 'OPERATOR' });
|
||||
expect((await db.conciergeMessage.findUniqueOrThrow({ where: { id: msg!.id } })).readAt).not.toBeNull();
|
||||
expect((await db.conciergeMessage.findUniqueOrThrow({ where: { id: msg.id } })).readAt).not.toBeNull();
|
||||
const unlinked = await db.provider.create({ data: { name: 'Без бота', role: 'CLEANER', languages: ['ru'], inviteCode: 'X-NOBOT' } });
|
||||
expect(await code(replyTo(ctx, { providerId: unlinked.id, adminUserId: admin.id, text: 'привет' }))).toBe('PROVIDER_NOT_LINKED');
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user