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:
zabudko-ruandClaude Opus 5.5 committed 2026-10-21 01:50:37 +04:00
1 parent d452824c62
commit 23ffb2bf1d
46 files changed
+4176 -144

No files matched your search

+30 -4
View File
@@ -442,7 +442,9 @@
"PROVIDER_NOT_LINKED": "The provider hasn’t opened the work bot via the invite yet — nowhere to send messages.",
"CARD_EXPIRED": "The QR is out of date or invalid — ask the client to reopen the card.",
"OFFER_NOT_AVAILABLE": "This offer isn’t available at the client’s level.",
"OFFER_LIMIT_REACHED": "The limit for this offer is already used up."
"OFFER_LIMIT_REACHED": "The limit for this offer is already used up.",
"CONCIERGE_SUBSCRIBERS_ONLY": "The personal concierge is for Barev+ and Barev Elite members. For now, write to support — we will help too.",
"ASSIGNEE_NO_ACCESS": "This team member has no access to this channel."
},
"client": {
"nav": {
@@ -654,6 +656,20 @@
"used_ONCE": "already used",
"used_WEEK": "again next week",
"used_MONTH": "again next month"
},
"chat": {
"title": "Chat with Barev Club",
"support": "Support",
"concierge": "Personal concierge",
"hint": "This is the same conversation as in the bot chat: replies arrive here and in Telegram.",
"empty": "Tell us how we can help — we’ll reply here.",
"placeholder": "Message…",
"send": "Send",
"attach": "Attach a photo or file",
"conciergeOnly": "The personal concierge comes with Barev+ and Barev Elite",
"team": "Barev Club",
"fileTooBig": "The file is larger than 10 MB",
"file": "File"
}
},
"pro": {
@@ -815,7 +831,12 @@
"cmdHelp": "Help",
"webRequestOrdered": "Request No. {{request}} received and booked as order No. {{order}} for {{date}}. Track the status in the app.",
"webRequestLinked": "Request No. {{request}} received. An operator will contact you to confirm the time.",
"conciergeReceived": "Passed to the concierge — we'll reply here, in this chat."
"conciergeReceived": "Passed to the concierge — we'll reply here, in this chat.",
"supportReceived": "Passed to support — we'll reply here, in this chat.",
"channelSupport": "You are now writing to support. Just send a message, photo or file.",
"channelConcierge": "You are now writing to your personal concierge. Just send a message, photo or file.",
"cmdSupport": "Write to support",
"cmdConcierge": "Write to the concierge (Barev+)"
},
"pro": {
"notLinked": "This bot is for Barev Club specialists. Ask the operator for an invite link.",
@@ -844,7 +865,11 @@
"newRequest": "App request #{{number}}",
"newWebRequest": "Site request #{{number}}",
"loginOk": "Sign-in confirmed — go back to your browser.",
"loginExpired": "The sign-in link has expired — reload the admin page."
"loginExpired": "The sign-in link has expired — reload the admin page.",
"channelSupport": "Support",
"channelConcierge": "Concierge",
"file": "file",
"chatAssigned": "{{by}} passed you a conversation: {{name}}"
},
"partner": {
"valid": "{{name}} — {{level}}, card is valid",
@@ -948,7 +973,8 @@
"openPlan": "Open the app",
"concierge": "Barev Club concierge",
"subscriptionActivated": "{{plan}} is on until {{date}}. Discounts are already in service prices.",
"operator": "Barev Club operator"
"operator": "Barev Club operator",
"support": "Barev Club support"
},
"admin": {
"nav": {
+30 -4
View File
@@ -442,7 +442,9 @@
"PROVIDER_NOT_LINKED": "El profesional aún no ha abierto el bot con la invitación: no hay dónde escribirle.",
"CARD_EXPIRED": "El QR caducó o no es válido: pide al cliente que vuelva a abrir la tarjeta.",
"OFFER_NOT_AVAILABLE": "Esta oferta no está disponible en el nivel del cliente.",
"OFFER_LIMIT_REACHED": "Ya se agotó el límite de esta oferta."
"OFFER_LIMIT_REACHED": "Ya se agotó el límite de esta oferta.",
"CONCIERGE_SUBSCRIBERS_ONLY": "El conserje personal es para miembros de Barev+ y Barev Elite. De momento, escribe a soporte: también te ayudaremos.",
"ASSIGNEE_NO_ACCESS": "Esta persona del equipo no tiene acceso a este canal."
},
"client": {
"nav": {
@@ -654,6 +656,20 @@
"used_ONCE": "ya usado",
"used_WEEK": "de nuevo la próxima semana",
"used_MONTH": "de nuevo el próximo mes"
},
"chat": {
"title": "Chat con Barev Club",
"support": "Soporte",
"concierge": "Conserje personal",
"hint": "Es la misma conversación que en el chat del bot: la respuesta llegará aquí y a Telegram.",
"empty": "Cuéntanos en qué podemos ayudarte: te responderemos aquí.",
"placeholder": "Mensaje…",
"send": "Enviar",
"attach": "Adjuntar foto o archivo",
"conciergeOnly": "El conserje personal está incluido en Barev+ y Barev Elite",
"team": "Barev Club",
"fileTooBig": "El archivo supera los 10 MB",
"file": "Archivo"
}
},
"pro": {
@@ -815,7 +831,12 @@
"cmdHelp": "Ayuda",
"webRequestOrdered": "Solicitud n.º {{request}} recibida y registrada como pedido n.º {{order}} para el {{date}}. El estado está en la app.",
"webRequestLinked": "Solicitud n.º {{request}} recibida. Un operador te contactará para confirmar la hora.",
"conciergeReceived": "Lo hemos pasado al conserje: te responderemos aquí, en este chat."
"conciergeReceived": "Lo hemos pasado al conserje: te responderemos aquí, en este chat.",
"supportReceived": "Lo hemos pasado a soporte: te responderemos aquí, en este chat.",
"channelSupport": "Ahora escribes a soporte. Envía un mensaje, una foto o un archivo.",
"channelConcierge": "Ahora escribes a tu conserje personal. Envía un mensaje, una foto o un archivo.",
"cmdSupport": "Escribir a soporte",
"cmdConcierge": "Escribir al conserje (Barev+)"
},
"pro": {
"notLinked": "Este bot es para los especialistas de Barev Club. Pide al operador un enlace de invitación.",
@@ -844,7 +865,11 @@
"newRequest": "Solicitud de la app n.º {{number}}",
"newWebRequest": "Solicitud de la web n.º {{number}}",
"loginOk": "Inicio de sesión confirmado: vuelve al navegador.",
"loginExpired": "El enlace de acceso ha caducado: recarga la página de administración."
"loginExpired": "El enlace de acceso ha caducado: recarga la página de administración.",
"channelSupport": "Soporte",
"channelConcierge": "Conserje",
"file": "archivo",
"chatAssigned": "{{by}} te ha pasado una conversación: {{name}}"
},
"partner": {
"valid": "{{name}} — {{level}}, tarjeta válida",
@@ -948,7 +973,8 @@
"openPlan": "Abrir la app",
"concierge": "Conserje de Barev Club",
"subscriptionActivated": "{{plan}} activo hasta el {{date}}. Los descuentos ya están en los precios.",
"operator": "Operador de Barev Club"
"operator": "Operador de Barev Club",
"support": "Soporte de Barev Club"
},
"admin": {
"nav": {
+30 -4
View File
@@ -442,7 +442,9 @@
"PROVIDER_NOT_LINKED": "Կատարողը դեռ չի բացել աշխատանքային բոտը հրավերով՝ գրելու տեղ չկա։",
"CARD_EXPIRED": "QR-ը հնացել է կամ անվավեր է՝ խնդրեք հաճախորդին նորից բացել քարտը։",
"OFFER_NOT_AVAILABLE": "Այս առաջարկը հասանելի չէ հաճախորդի մակարդակում։",
"OFFER_LIMIT_REACHED": "Այս առաջարկի սահմանաչափն արդեն սպառված է։"
"OFFER_LIMIT_REACHED": "Այս առաջարկի սահմանաչափն արդեն սպառված է։",
"CONCIERGE_SUBSCRIBERS_ONLY": "Անձնական կոնսիերժը Barev+ և Barev Elite բաժանորդների համար է։ Առայժմ գրեք աջակցությանը՝ մենք էլ կօգնենք։",
"ASSIGNEE_NO_ACCESS": "Այս աշխատակիցը մուտք չունի այս ալիքին։"
},
"client": {
"nav": {
@@ -654,6 +656,20 @@
"used_ONCE": "արդեն ստացել եք",
"used_WEEK": "կրկին հաջորդ շաբաթից",
"used_MONTH": "կրկին հաջորդ ամսից"
},
"chat": {
"title": "Զրույց Barev Club-ի հետ",
"support": "Աջակցություն",
"concierge": "Անձնական կոնսիերժ",
"hint": "Սա նույն զրույցն է, ինչ բոտի չատում․ պատասխանը կգա և՛ այստեղ, և՛ Telegram-ում։",
"empty": "Գրեք, թե ինչով օգնել՝ կպատասխանենք այստեղ։",
"placeholder": "Հաղորդագրություն…",
"send": "Ուղարկել",
"attach": "Կցել լուսանկար կամ ֆայլ",
"conciergeOnly": "Անձնական կոնսիերժը՝ Barev+ և Barev Elite-ում",
"team": "Barev Club",
"fileTooBig": "Ֆայլը 10 ՄԲ-ից մեծ է",
"file": "Ֆայլ"
}
},
"pro": {
@@ -815,7 +831,12 @@
"cmdHelp": "Օգնություն",
"webRequestOrdered": "Հայտ №{{request}}-ն ընդունված է և ձևակերպված որպես պատվեր №{{order}}՝ {{date}}։ Կարգավիճակը՝ հավելվածում։",
"webRequestLinked": "Հայտ №{{request}}-ն ընդունված է։ Օպերատորը կկապվի քեզ հետ և կհաստատի ժամը։",
"conciergeReceived": "Փոխանցեցինք կոնսիերժին՝ կպատասխանենք այստեղ, այս չատում։"
"conciergeReceived": "Փոխանցեցինք կոնսիերժին՝ կպատասխանենք այստեղ, այս չատում։",
"supportReceived": "Փոխանցեցինք աջակցությանը՝ կպատասխանենք այստեղ, այս չատում։",
"channelSupport": "Այժմ գրում եք աջակցությանը։ Պարզապես ուղարկեք հաղորդագրություն, լուսանկար կամ ֆայլ։",
"channelConcierge": "Այժմ գրում եք ձեր անձնական կոնսիերժին։ Պարզապես ուղարկեք հաղորդագրություն, լուսանկար կամ ֆայլ։",
"cmdSupport": "Գրել աջակցությանը",
"cmdConcierge": "Գրել կոնսիերժին (Barev+)"
},
"pro": {
"notLinked": "Այս բոտը Barev Club-ի կատարողների համար է։ Խնդրեք օպերատորից հրավերի հղում։",
@@ -844,7 +865,11 @@
"newRequest": "Հայտ հավելվածից №{{number}}",
"newWebRequest": "Հայտ կայքից №{{number}}",
"loginOk": "Մուտքը հաստատված է — վերադարձեք բրաուզեր։",
"loginExpired": "Մուտքի հղումը հնացել է — թարմացրեք ադմին էջը։"
"loginExpired": "Մուտքի հղումը հնացել է — թարմացրեք ադմին էջը։",
"channelSupport": "Աջակցություն",
"channelConcierge": "Կոնսիերժ",
"file": "ֆայլ",
"chatAssigned": "{{by}}-ը ձեզ փոխանցեց զրույցը՝ {{name}}"
},
"partner": {
"valid": "{{name}} — {{level}}, քարտը գործում է",
@@ -948,7 +973,8 @@
"openPlan": "Բացել հավելվածը",
"concierge": "Barev Club կոնսիերժ",
"subscriptionActivated": "{{plan}}-ը միացված է մինչև {{date}}։ Զեղչերն արդեն ծառայությունների գներում են։",
"operator": "Barev Club օպերատոր"
"operator": "Barev Club օպերատոր",
"support": "Barev Club աջակցություն"
},
"admin": {
"nav": {
+30 -4
View File
@@ -442,7 +442,9 @@
"PROVIDER_NOT_LINKED": "Исполнитель ещё не открыл рабочий бот по инвайту — написать ему пока некуда.",
"CARD_EXPIRED": "QR устарел или недействителен — попросите клиента открыть карту заново.",
"OFFER_NOT_AVAILABLE": "Это предложение недоступно на уровне клиента.",
"OFFER_LIMIT_REACHED": "Лимит по этому предложению уже исчерпан."
"OFFER_LIMIT_REACHED": "Лимит по этому предложению уже исчерпан.",
"CONCIERGE_SUBSCRIBERS_ONLY": "Личный консьерж — для подписчиков Barev+ и Barev Elite. Пока пишите в поддержку — мы тоже поможем.",
"ASSIGNEE_NO_ACCESS": "У этого сотрудника нет доступа к этому каналу."
},
"client": {
"nav": {
@@ -654,6 +656,20 @@
"used_ONCE": "уже получено",
"used_WEEK": "снова со следующей недели",
"used_MONTH": "снова со следующего месяца"
},
"chat": {
"title": "Чат с Barev Club",
"support": "Поддержка",
"concierge": "Личный консьерж",
"hint": "Это та же переписка, что в чате с ботом: ответ придёт и сюда, и в Telegram.",
"empty": "Напишите, чем помочь, — ответим здесь.",
"placeholder": "Сообщение…",
"send": "Отправить",
"attach": "Прикрепить фото или файл",
"conciergeOnly": "Личный консьерж — в Barev+ и Barev Elite",
"team": "Barev Club",
"fileTooBig": "Файл больше 10 МБ",
"file": "Файл"
}
},
"pro": {
@@ -815,7 +831,12 @@
"cmdHelp": "Помощь",
"webRequestOrdered": "Заявка №{{request}} принята и оформлена как заказ №{{order}} на {{date}}. Статус — в приложении.",
"webRequestLinked": "Заявка №{{request}} принята. Оператор свяжется с тобой и подтвердит время.",
"conciergeReceived": "Передали консьержу — ответим здесь, в этом чате."
"conciergeReceived": "Передали консьержу — ответим здесь, в этом чате.",
"supportReceived": "Передали в поддержку — ответим здесь, в этом чате.",
"channelSupport": "Теперь вы пишете в поддержку. Просто отправьте сообщение, фото или файл.",
"channelConcierge": "Теперь вы пишете личному консьержу. Просто отправьте сообщение, фото или файл.",
"cmdSupport": "Написать в поддержку",
"cmdConcierge": "Написать консьержу (Barev+)"
},
"pro": {
"notLinked": "Этот бот — для исполнителей Barev Club. Попроси у оператора ссылку-приглашение.",
@@ -844,7 +865,11 @@
"newRequest": "Заявка из приложения №{{number}}",
"newWebRequest": "Заявка с сайта №{{number}}",
"loginOk": "Вход в админку подтверждён — вернитесь в браузер.",
"loginExpired": "Ссылка для входа устарела — обновите страницу админки."
"loginExpired": "Ссылка для входа устарела — обновите страницу админки.",
"channelSupport": "Поддержка",
"channelConcierge": "Консьерж",
"file": "файл",
"chatAssigned": "{{by}} передал(а) вам диалог: {{name}}"
},
"partner": {
"valid": "{{name}} — {{level}}, карта действует",
@@ -948,7 +973,8 @@
"openPlan": "Открыть приложение",
"concierge": "Консьерж Barev Club",
"subscriptionActivated": "{{plan}} включён до {{date}}. Скидки уже в ценах услуг.",
"operator": "Оператор Barev Club"
"operator": "Оператор Barev Club",
"support": "Поддержка Barev Club"
},
"admin": {
"nav": {
+30 -4
View File
@@ -442,7 +442,9 @@
"PROVIDER_NOT_LINKED": "服务人员尚未通过邀请打开工作机器人,暂时无法发送消息。",
"CARD_EXPIRED": "二维码已过期或无效——请客户重新打开会员卡。",
"OFFER_NOT_AVAILABLE": "该优惠不适用于客户当前等级。",
"OFFER_LIMIT_REACHED": "该优惠次数已用完。"
"OFFER_LIMIT_REACHED": "该优惠次数已用完。",
"CONCIERGE_SUBSCRIBERS_ONLY": "专属礼宾仅面向 Barev+ 和 Barev Elite 会员。目前请联系客服,我们同样会帮你。",
"ASSIGNEE_NO_ACCESS": "该成员无权访问此频道。"
},
"client": {
"nav": {
@@ -654,6 +656,20 @@
"used_ONCE": "已使用",
"used_WEEK": "下周可再次使用",
"used_MONTH": "下月可再次使用"
},
"chat": {
"title": "与 Barev Club 聊天",
"support": "客服",
"concierge": "专属礼宾",
"hint": "这与机器人聊天是同一个对话:回复会同时出现在这里和 Telegram。",
"empty": "告诉我们需要什么帮助,我们会在这里回复。",
"placeholder": "输入消息…",
"send": "发送",
"attach": "附加照片或文件",
"conciergeOnly": "专属礼宾包含在 Barev+ 和 Barev Elite 中",
"team": "Barev Club",
"fileTooBig": "文件超过 10 MB",
"file": "文件"
}
},
"pro": {
@@ -815,7 +831,12 @@
"cmdHelp": "帮助",
"webRequestOrdered": "申请 {{request}} 号已收到,并已生成订单 {{order}} 号,时间:{{date}}。状态请在应用中查看。",
"webRequestLinked": "申请 {{request}} 号已收到。客服会联系你确认时间。",
"conciergeReceived": "已转给礼宾,我们会在这个聊天中回复你。"
"conciergeReceived": "已转给礼宾,我们会在这个聊天中回复你。",
"supportReceived": "已转给客服,我们会在这个聊天中回复你。",
"channelSupport": "现在你在给客服写信。直接发送消息、照片或文件即可。",
"channelConcierge": "现在你在给专属礼宾写信。直接发送消息、照片或文件即可。",
"cmdSupport": "联系客服",
"cmdConcierge": "联系礼宾(Barev+)"
},
"pro": {
"notLinked": "此机器人供 Barev Club 服务人员使用。请向客服索取邀请链接。",
@@ -844,7 +865,11 @@
"newRequest": "应用申请 #{{number}}",
"newWebRequest": "网站申请 #{{number}}",
"loginOk": "登录已确认,请返回浏览器。",
"loginExpired": "登录链接已过期,请刷新管理页面。"
"loginExpired": "登录链接已过期,请刷新管理页面。",
"channelSupport": "客服",
"channelConcierge": "礼宾",
"file": "文件",
"chatAssigned": "{{by}} 将对话转交给你:{{name}}"
},
"partner": {
"valid": "{{name}} — {{level}},会员卡有效",
@@ -948,7 +973,8 @@
"openPlan": "打开应用",
"concierge": "Barev Club 礼宾",
"subscriptionActivated": "{{plan}} 已开通,有效期至 {{date}}。服务价格已含折扣。",
"operator": "Barev Club 客服"
"operator": "Barev Club 客服",
"support": "Barev Club 客服支持"
},
"admin": {
"nav": {