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

+789
View File
@@ -0,0 +1,789 @@
/*
* Чаты (этап 2). Три колонки: список диалогов · переписка · о клиенте.
* Переписка — на кремовом фоне: клиент слева белым, команда справа сливовым, заметки — жёлтым «стикером».
*/
/* страница чатов занимает ровно экран: прокручиваются список и лента, а не страница */
.bo-layout--chats .bo-main {
padding-top: 20px;
padding-bottom: 20px;
height: 100dvh;
display: flex;
flex-direction: column;
}
.bo-layout--chats .bo-banner {
margin-bottom: 14px;
}
.bo-chat {
flex: 1;
display: grid;
grid-template-columns: minmax(300px, 360px) minmax(0, 1fr);
gap: 14px;
min-height: 0;
}
.bo-chat--side {
grid-template-columns: minmax(280px, 340px) minmax(0, 1fr) 300px;
}
.bo-chat__list,
.bo-chat__conv,
.bo-chat__side {
background: var(--bo-panel);
border-radius: var(--bo-r-panel);
min-height: 0;
min-width: 0;
display: flex;
flex-direction: column;
overflow: hidden;
}
.bo-chat__listhead {
padding: 18px 16px 10px;
display: flex;
flex-direction: column;
gap: 12px;
border-bottom: 1px solid var(--bc-line);
}
.bo-chat__title {
font-size: 30px;
}
.bo-chtabs {
display: flex;
padding: 4px;
gap: 4px;
border-radius: var(--bc-pill);
background: var(--bo-soft);
}
.bo-chtabs button {
flex: 1;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
height: 34px;
border: 0;
border-radius: var(--bc-pill);
background: none;
font: inherit;
font-size: 14px;
font-weight: 600;
color: var(--bc-ink-2);
cursor: pointer;
}
.bo-chtabs button[aria-pressed='true'] {
background: #fff;
color: var(--bc-ink);
box-shadow: 0 2px 8px rgba(30, 27, 26, 0.08);
}
.bo-chat__dot {
min-width: 20px;
height: 20px;
padding: 0 6px;
border-radius: var(--bc-pill);
background: var(--bc-accent);
color: #fff;
font-size: 11px;
font-weight: 700;
display: inline-grid;
place-items: center;
}
.bo-chat__search {
flex: none;
max-width: none;
}
.bo-chat__search .bo-input {
background: var(--bo-soft);
min-height: 40px;
}
.bo-chat__filters {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.bo-chat__filters .bo-pill {
height: 30px;
padding: 0 12px;
display: inline-flex;
align-items: center;
gap: 6px;
flex: none;
}
.bo-chat__pillnum {
font-weight: 700;
color: var(--bc-accent);
}
.bo-pill[aria-pressed='true'] .bo-chat__pillnum {
color: var(--bc-pink);
}
.bo-chat__items {
list-style: none;
margin: 0;
padding: 6px;
overflow-y: auto;
flex: 1;
}
.bo-chat__none {
padding: 32px 20px;
text-align: center;
color: var(--bc-ink-2);
}
.bo-thread {
display: flex;
gap: 12px;
padding: 12px 10px;
border-radius: 18px;
color: inherit;
text-decoration: none;
}
.bo-thread:hover {
background: var(--bo-soft);
}
.bo-thread[aria-current='true'] {
background: var(--bo-soft);
box-shadow: inset 3px 0 0 var(--bc-accent);
}
.bo-thread[data-closed] {
opacity: 0.62;
}
.bo-thread__body {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 3px;
}
.bo-thread__top {
display: flex;
align-items: center;
gap: 6px;
min-width: 0;
}
.bo-thread__top .bo-plan {
height: 20px;
padding: 0 7px;
font-size: 11px;
}
.bo-thread__name {
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
min-width: 0;
}
.bo-thread__time {
margin-left: auto;
font-size: 12px;
color: var(--bc-ink-3);
white-space: nowrap;
}
.bo-thread__last {
font-size: 14px;
color: var(--bc-ink-2);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.bo-thread__meta {
display: flex;
align-items: center;
gap: 6px;
margin-top: 3px;
min-height: 20px;
min-width: 0;
}
.bo-thread__meta .bo-nav__count {
margin-left: 0;
flex: none;
}
.bo-thread__who {
font-size: 12px;
color: var(--bc-ink-2);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
min-width: 0;
}
.bo-thread__who--none {
color: var(--bc-ink-3);
}
.bo-chip {
display: inline-flex;
align-items: center;
gap: 4px;
height: 20px;
padding: 0 8px;
border-radius: 7px;
font-size: 11px;
font-weight: 600;
white-space: nowrap;
background: var(--bo-soft);
color: var(--bc-ink-2);
}
.bo-chip--SUPPORT {
background: var(--bc-blue);
color: var(--bc-blue-ink);
}
.bo-chip--CONCIERGE {
background: var(--bc-pink);
color: var(--bc-plum);
}
.bo-chip--provider {
background: var(--bc-docs);
color: var(--bc-docs-ink);
}
.bo-wait-chip {
display: inline-flex;
align-items: center;
height: 20px;
padding: 0 8px;
border-radius: var(--bc-pill);
font-size: 11px;
font-weight: 600;
white-space: nowrap;
}
.bo-wait-chip--calm {
background: var(--bc-home);
color: var(--bc-home-ink);
}
.bo-wait-chip--warm {
background: var(--bc-perks);
color: var(--bc-perks-ink);
}
.bo-wait-chip--hot {
background: var(--bc-accent);
color: #fff;
}
/* пустое состояние справа */
.bo-chat__pick {
margin: auto;
padding: 32px;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
}
.bo-chat__pickicon {
width: 64px;
height: 64px;
border-radius: 22px;
display: grid;
place-items: center;
background: var(--bc-pink);
color: var(--bc-plum);
margin-bottom: 8px;
}
/* шапка переписки */
.bo-chat__conv {
container-type: inline-size;
}
.bo-conv__head {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 10px 12px;
padding: 14px 16px;
border-bottom: 1px solid var(--bc-line);
}
@container (max-width: 640px) {
.bo-composer__hint {
display: none;
}
}
.bo-conv__back {
display: none;
color: var(--bc-ink);
}
.bo-conv__who {
min-width: 0;
flex: 1 1 180px;
}
.bo-conv__name {
display: flex;
align-items: center;
gap: 8px;
font-family: var(--bc-font-head);
font-weight: 700;
font-size: 18px;
white-space: nowrap;
min-width: 0;
}
.bo-conv__name > span:first-child {
overflow: hidden;
text-overflow: ellipsis;
}
.bo-conv__sub {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 4px 10px;
font-size: 13px;
color: var(--bc-ink-3);
margin-top: 3px;
}
.bo-conv__actions {
display: flex;
align-items: center;
gap: 6px;
margin-left: auto;
}
.bo-iconbtn {
width: 36px;
height: 36px;
border-radius: 12px;
border: 0;
background: none;
color: var(--bc-ink-2);
display: inline-grid;
place-items: center;
cursor: pointer;
flex: none;
}
.bo-iconbtn:hover:not(:disabled),
.bo-iconbtn[aria-expanded='true'],
.bo-iconbtn[aria-pressed='true'] {
background: var(--bo-soft);
color: var(--bc-ink);
}
.bo-iconbtn:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.bo-assignee {
display: inline-flex;
align-items: center;
gap: 8px;
height: 34px;
padding: 0 12px 0 4px;
border-radius: var(--bc-pill);
border: 1.5px solid var(--bc-line);
background: none;
font: inherit;
font-size: 13px;
font-weight: 600;
cursor: pointer;
white-space: nowrap;
}
.bo-assignee .bo-avatar {
font-size: 10px;
}
.bo-conv__banner {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 16px;
font-size: 14px;
background: var(--bc-blue);
color: var(--bc-blue-ink);
}
.bo-conv__banner--bot {
background: var(--bc-learn);
color: var(--bc-learn-ink);
}
.bo-conv__closed {
padding: 8px 18px;
font-size: 13px;
color: var(--bc-ink-2);
background: var(--bc-bg);
text-align: center;
}
/* лента сообщений */
.bo-msgs {
flex: 1;
overflow-y: auto;
background: var(--bc-bg);
padding: 16px clamp(12px, 3vw, 28px) 20px;
display: flex;
flex-direction: column;
gap: 6px;
}
.bo-msgs__day {
align-self: center;
margin: 12px 0 6px;
padding: 3px 12px;
border-radius: var(--bc-pill);
background: #fff;
font-size: 12px;
font-weight: 600;
color: var(--bc-ink-3);
}
.bo-msg {
max-width: min(560px, 82%);
padding: 10px 14px 8px;
border-radius: 20px;
font-size: 15px;
line-height: 1.45;
overflow-wrap: anywhere;
}
.bo-msg--in {
align-self: flex-start;
background: #fff;
border-bottom-left-radius: 6px;
box-shadow: 0 1px 0 var(--bc-line);
}
.bo-msg--out {
align-self: flex-end;
background: var(--bc-plum);
color: #f6e9dc;
border-bottom-right-radius: 6px;
}
.bo-msg--note {
align-self: flex-end;
background: #fbefbd;
color: var(--bc-perks-ink);
border: 1.5px dashed rgba(74, 16, 40, 0.22);
border-bottom-right-radius: 6px;
}
.bo-msg__text {
white-space: pre-wrap;
}
.bo-msg__label {
display: flex;
align-items: center;
gap: 5px;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.02em;
margin-bottom: 4px;
opacity: 0.75;
}
.bo-msg__meta {
margin-top: 4px;
font-size: 11px;
opacity: 0.6;
text-align: right;
}
.bo-msg--in .bo-msg__meta {
text-align: left;
}
.bo-msg__err {
display: flex;
align-items: center;
gap: 6px;
margin-top: 6px;
padding: 5px 8px;
border-radius: 10px;
background: #fff;
color: var(--bc-danger);
font-size: 12px;
font-weight: 500;
}
.bo-msg__img {
display: block;
margin: -4px -8px 6px;
}
.bo-msg__img img {
display: block;
max-width: 100%;
max-height: 280px;
border-radius: 14px;
object-fit: cover;
}
.bo-msg__file {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 12px 8px 8px;
margin: 0 -6px 6px;
border-radius: 14px;
background: rgba(255, 255, 255, 0.14);
color: inherit;
text-decoration: none;
}
.bo-msg--in .bo-msg__file,
.bo-msg--note .bo-msg__file {
background: var(--bo-soft);
}
.bo-msg__file svg {
flex: none;
}
.bo-msg__fname {
display: block;
font-weight: 600;
font-size: 14px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.bo-sys {
align-self: center;
font-size: 12px;
color: var(--bc-ink-2);
text-align: center;
padding: 2px 8px;
}
/* поле ответа */
.bo-composer {
padding: 8px 14px 14px;
border-top: 1px solid var(--bc-line);
background: #fff;
}
.bo-composer__tabs {
display: flex;
align-items: center;
gap: 4px;
margin-bottom: 8px;
}
.bo-composer__tabs [role='tab'] {
display: inline-flex;
align-items: center;
gap: 5px;
height: 30px;
padding: 0 12px;
border: 0;
border-radius: var(--bc-pill);
background: none;
font: inherit;
font-size: 13px;
font-weight: 600;
color: var(--bc-ink-3);
cursor: pointer;
}
.bo-composer__tabs [role='tab'][aria-selected='true'] {
background: var(--bc-ink);
color: #fff;
}
.bo-composer--note .bo-composer__tabs [role='tab'][aria-selected='true'] {
background: var(--bc-perks);
color: var(--bc-perks-ink);
}
.bo-composer__tabs [role='tab']:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.bo-composer__hint {
font-size: 12px;
color: var(--bc-ink-3);
}
.bo-composer__box {
display: flex;
align-items: flex-end;
gap: 6px;
padding: 6px 6px 6px 14px;
border-radius: 22px;
border: 1.5px solid var(--bc-line);
background: #fff;
}
.bo-composer__box:focus-within {
border-color: var(--bc-accent);
}
.bo-composer--note .bo-composer__box {
background: #fdf6d8;
border-style: dashed;
border-color: rgba(74, 16, 40, 0.25);
}
.bo-composer__input {
flex: 1;
min-width: 0;
border: 0;
outline: none;
resize: none;
background: none;
font: inherit;
font-size: 15px;
line-height: 1.45;
padding: 7px 0;
max-height: 220px;
}
.bo-composer__tools {
display: flex;
align-items: center;
gap: 2px;
}
/* всплывающие меню */
.bo-pop-anchor {
position: relative;
display: inline-flex;
}
.bo-pop {
position: absolute;
top: calc(100% + 6px);
right: 0;
z-index: 50;
min-width: 220px;
max-width: min(340px, 86vw);
padding: 6px;
border-radius: 18px;
background: #fff;
box-shadow:
0 18px 50px rgba(30, 27, 26, 0.18),
0 0 0 1px var(--bc-line);
display: flex;
flex-direction: column;
gap: 2px;
}
.bo-pop--up {
top: auto;
bottom: calc(100% + 8px);
}
.bo-pop__item {
display: flex;
align-items: center;
gap: 10px;
min-height: 40px;
padding: 6px 12px;
border: 0;
border-radius: 12px;
background: none;
font: inherit;
font-size: 14px;
text-align: left;
color: var(--bc-ink);
cursor: pointer;
}
.bo-pop__item:hover {
background: var(--bo-soft);
}
.bo-pop__item[aria-current='true'] {
color: var(--bc-ink-3);
}
.bo-tpl {
width: 340px;
padding: 10px;
gap: 8px;
}
.bo-tpl__list {
max-height: 260px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 2px;
}
.bo-tpl__item {
display: flex;
flex-direction: column;
gap: 2px;
padding: 8px 10px;
border: 0;
border-radius: 12px;
background: none;
text-align: left;
font: inherit;
font-size: 14px;
cursor: pointer;
color: var(--bc-ink);
}
.bo-tpl__item span {
font-size: 13px;
color: var(--bc-ink-2);
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.bo-tpl__item:hover {
background: var(--bo-soft);
}
/* о клиенте */
.bo-chat__side {
padding: 16px 18px 20px;
overflow-y: auto;
}
.bo-side-panel__head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 14px;
}
.bo-minipass {
display: flex;
flex-direction: column;
gap: 2px;
padding: 14px 16px;
border-radius: 20px;
background: var(--bo-soft);
font-size: 13px;
}
.bo-minipass__plan {
font-family: var(--bc-font-head);
font-weight: 700;
font-size: 20px;
}
.bo-minipass--BAREV_PLUS {
background: var(--bc-pink);
color: var(--bc-plum);
}
.bo-minipass--ELITE {
background: var(--bc-plum);
color: var(--bc-perks);
}
.bo-side-panel__note {
margin: 0;
padding: 10px 12px;
border-radius: 14px;
background: #fbefbd;
color: var(--bc-perks-ink);
font-size: 13px;
white-space: pre-line;
}
.bo-side-panel__orders {
list-style: none;
margin: 0;
padding: 0;
display: grid;
gap: 8px;
font-size: 14px;
}
/* узкий десктоп: «о клиенте» — выезжающая панель поверх */
@media (max-width: 1559px) {
.bo-chat--side {
grid-template-columns: minmax(280px, 340px) minmax(0, 1fr);
}
.bo-chat__side {
position: fixed;
top: 12px;
right: 12px;
bottom: 12px;
width: min(340px, calc(100vw - 24px));
z-index: 45;
box-shadow: 0 30px 80px rgba(30, 27, 26, 0.28);
}
}
/* телефон: список и переписка — отдельными экранами */
@media (max-width: 860px) {
.bo-layout--chats .bo-main {
padding: 12px 10px calc(96px + env(safe-area-inset-bottom));
}
.bo-layout--thread .bo-main {
padding-bottom: 10px;
}
.bo-layout--thread .bo-tabbar,
.bo-layout--thread .bo-banner {
display: none;
}
.bo-chat,
.bo-chat--side {
grid-template-columns: minmax(0, 1fr);
}
.bo-chat--thread .bo-chat__list,
.bo-chat:not(.bo-chat--thread) .bo-chat__conv {
display: none;
}
.bo-conv__back {
display: inline-grid;
}
.bo-conv__head {
padding: 10px 10px 10px 12px;
gap: 8px;
}
.bo-conv__head > .bo-avatar {
display: none;
}
.bo-hide-sm {
display: none;
}
.bo-msg {
max-width: 88%;
}
.bo-composer {
padding: 6px 8px 10px;
}
.bo-tpl {
width: min(320px, calc(100vw - 40px));
}
}