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
@@ -226,6 +226,10 @@ model Client {
|
||||
leads Lead[]
|
||||
requests ClientRequest[]
|
||||
concierge ConciergeMessage[]
|
||||
chatThreads ChatThread[]
|
||||
/// куда сейчас пишет клиент: SUPPORT или CONCIERGE (переключается командой бота или командой поддержки);
|
||||
/// null — по умолчанию: подписчик → консьерж, остальные → поддержка
|
||||
chatChannel ChatChannel?
|
||||
|
||||
@@index([agencyId])
|
||||
}
|
||||
@@ -298,6 +302,7 @@ model Provider {
|
||||
|
||||
@@index([role, active])
|
||||
concierge ConciergeMessage[]
|
||||
chatThreads ChatThread[]
|
||||
}
|
||||
|
||||
/// Пресет ставок исполнителя: для каждой строки цены — фикс или % (см. packages/core/src/payout.ts)
|
||||
@@ -855,6 +860,8 @@ model AdminUser {
|
||||
audit AuditLog[]
|
||||
loginRequests AdminLoginRequest[]
|
||||
segments SavedSegment[]
|
||||
chatThreads ChatThread[]
|
||||
chatTemplates ChatTemplate[]
|
||||
}
|
||||
|
||||
/// Вход в новую админку через Telegram: страница показывает ссылку t.me/<рабочий бот>?start=lg-<code>,
|
||||
@@ -1110,19 +1117,41 @@ enum ConciergeAuthor {
|
||||
CLIENT
|
||||
PROVIDER
|
||||
OPERATOR
|
||||
/// ИИ-консьерж (на будущее): отвечает сам, человек может перехватить диалог
|
||||
BOT
|
||||
/// служебная строка в ленте: «взял диалог», «передал», «закрыл»
|
||||
SYSTEM
|
||||
}
|
||||
|
||||
/// Переписка из админки: с клиентом (клиентский бот) или с исполнителем (рабочий бот); ответ приходит в тот же чат с ботом
|
||||
/// Сообщение в чате. Клиент пишет боту или в мини-аппе — всё попадает в один диалог (ChatThread).
|
||||
/// Ответ команды приходит клиенту в чат с ботом (клиентский бот, для исполнителя — рабочий).
|
||||
model ConciergeMessage {
|
||||
id String @id @default(cuid())
|
||||
/// диалог; у старых сообщений до этапа 2 проставлен миграцией
|
||||
threadId String?
|
||||
thread ChatThread? @relation(fields: [threadId], references: [id], onDelete: Cascade)
|
||||
clientId String?
|
||||
client Client? @relation(fields: [clientId], references: [id])
|
||||
providerId String?
|
||||
provider Provider? @relation(fields: [providerId], references: [id])
|
||||
author ConciergeAuthor
|
||||
/// кто из операторов ответил
|
||||
/// кто из команды написал (author = OPERATOR / SYSTEM)
|
||||
adminUserId String?
|
||||
text String
|
||||
/// внутренняя заметка команды — клиент её не видит
|
||||
internal Boolean @default(false)
|
||||
/// откуда пришло: BOT | MINIAPP | ADMIN
|
||||
source String @default("BOT")
|
||||
/// файл: ключ в приватном хранилище (загрузила команда / мини-апп)…
|
||||
fileKey String?
|
||||
/// …или file_id в Telegram (прислал клиент боту); бот — client | pro
|
||||
tgFileId String?
|
||||
tgBot String?
|
||||
fileName String?
|
||||
mime String?
|
||||
size Int?
|
||||
/// не удалось доставить в Telegram (клиент заблокировал бота и т.п.)
|
||||
deliveryError String?
|
||||
/// оператор прочитал сообщение клиента
|
||||
readAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
@@ -1130,4 +1159,58 @@ model ConciergeMessage {
|
||||
@@index([clientId, createdAt])
|
||||
@@index([providerId, createdAt])
|
||||
@@index([author, readAt])
|
||||
@@index([threadId, createdAt])
|
||||
}
|
||||
|
||||
enum ChatChannel {
|
||||
/// поддержка — все клиенты и исполнители
|
||||
SUPPORT
|
||||
/// консьерж — только подписчики Barev+ / Elite
|
||||
CONCIERGE
|
||||
}
|
||||
|
||||
enum ChatStatus {
|
||||
OPEN
|
||||
CLOSED
|
||||
}
|
||||
|
||||
/// Диалог: один на собеседника в каждом канале. Закрытый диалог открывается заново, когда клиент пишет снова.
|
||||
model ChatThread {
|
||||
id String @id @default(cuid())
|
||||
channel ChatChannel
|
||||
clientId String?
|
||||
client Client? @relation(fields: [clientId], references: [id])
|
||||
providerId String?
|
||||
provider Provider? @relation(fields: [providerId], references: [id])
|
||||
status ChatStatus @default(OPEN)
|
||||
/// кто из команды ведёт диалог
|
||||
assigneeId String?
|
||||
assignee AdminUser? @relation(fields: [assigneeId], references: [id])
|
||||
/// диалог ведёт ИИ-консьерж (задел на будущее); человек может перехватить
|
||||
botActive Boolean @default(false)
|
||||
lastMessageAt DateTime @default(now())
|
||||
/// с какого момента клиент ждёт ответа; null — ответили
|
||||
waitingSince DateTime?
|
||||
closedAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
messages ConciergeMessage[]
|
||||
|
||||
@@unique([clientId, channel])
|
||||
@@unique([providerId, channel])
|
||||
@@index([channel, status, lastMessageAt])
|
||||
@@index([assigneeId, status])
|
||||
}
|
||||
|
||||
/// Шаблон быстрого ответа в чате. {name} подставляется именем клиента.
|
||||
model ChatTemplate {
|
||||
id String @id @default(cuid())
|
||||
/// для какого канала; null — для обоих
|
||||
channel ChatChannel?
|
||||
title String
|
||||
text String
|
||||
createdById String?
|
||||
createdBy AdminUser? @relation(fields: [createdById], references: [id])
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
Reference in new issue
Block a user