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
@@ -1,6 +1,6 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Center, Icon, Spinner } from '@barev/ui';
|
||||
import { I18nProvider, match, openTelegram, pickLocale, telegramLanguage, useI18n, useLocation, useTelegramBack, useTRPC } from '@barev/web';
|
||||
import { I18nProvider, match, pickLocale, telegramLanguage, useI18n, useLocation, useTelegramBack, useTRPC } from '@barev/web';
|
||||
import type { ReactNode } from 'react';
|
||||
import { Consent } from './screens/Consent.tsx';
|
||||
import { Docs } from './screens/Docs.tsx';
|
||||
@@ -15,6 +15,7 @@ import { Orders } from './screens/Orders.tsx';
|
||||
import { Perks } from './screens/Perks.tsx';
|
||||
import { Profile } from './screens/Profile.tsx';
|
||||
import { Services } from './screens/Services.tsx';
|
||||
import { Chat } from './screens/Chat.tsx';
|
||||
|
||||
const ROOTS = ['/', '/services', '/perks', '/profile', '/docs'];
|
||||
|
||||
@@ -38,7 +39,7 @@ export function App({ hasInitData }: { hasInitData: boolean }) {
|
||||
) : !me.data.segment ? (
|
||||
<Segment />
|
||||
) : (
|
||||
<Routes segment={me.data.segment} conciergeUrl={me.data.conciergeUrl} />
|
||||
<Routes segment={me.data.segment} />
|
||||
)}
|
||||
</I18nProvider>
|
||||
);
|
||||
@@ -48,7 +49,7 @@ export function App({ hasInitData }: { hasInitData: boolean }) {
|
||||
* Два сценария: MOVING — план переезда, документы, консьерж; LIVING — услуги со скидками, партнёры, заказы.
|
||||
* Остальные экраны (услуги, подписка, заказы) доступны из обоих.
|
||||
*/
|
||||
function Routes({ segment, conciergeUrl }: { segment: 'MOVING' | 'LIVING'; conciergeUrl: string }) {
|
||||
function Routes({ segment }: { segment: 'MOVING' | 'LIVING' }) {
|
||||
const { path } = useLocation();
|
||||
useTelegramBack(path, ROOTS);
|
||||
let screen: ReactNode;
|
||||
@@ -63,18 +64,19 @@ function Routes({ segment, conciergeUrl }: { segment: 'MOVING' | 'LIVING'; conci
|
||||
else if (path === '/perks') screen = <Perks />;
|
||||
else if (path === '/profile') screen = <Profile />;
|
||||
else if (path === '/membership') screen = <Membership />;
|
||||
else if (path === '/chat') screen = <Chat />;
|
||||
else screen = segment === 'MOVING' ? <Move /> : <Home />;
|
||||
const withNav = ROOTS.includes(path) || path === '/orders';
|
||||
return (
|
||||
<>
|
||||
{screen}
|
||||
{withNav && (segment === 'MOVING' ? <MoveNav path={path} conciergeUrl={conciergeUrl} /> : <BottomNav path={path} />)}
|
||||
{withNav && (segment === 'MOVING' ? <MoveNav path={path} /> : <BottomNav path={path} />)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
/** Меню переезда: План / Документы / Консьерж (чат с ботом) / Услуги / Профиль */
|
||||
function MoveNav({ path, conciergeUrl }: { path: string; conciergeUrl: string }) {
|
||||
function MoveNav({ path }: { path: string }) {
|
||||
const { t } = useI18n();
|
||||
const item = (href: string, icon: Parameters<typeof Icon>[0]['name'], label: string) => (
|
||||
<a href={`#${href}`} aria-current={path === href ? 'page' : undefined}>
|
||||
@@ -86,9 +88,9 @@ function MoveNav({ path, conciergeUrl }: { path: string; conciergeUrl: string })
|
||||
<nav className="bc-bottomnav" aria-label={t('client.nav.label')}>
|
||||
{item('/', 'globe', t('client.nav.plan'))}
|
||||
{item('/docs', 'docs', t('client.nav.docs'))}
|
||||
<button type="button" className="bc-bottomnav__plus" onClick={() => openTelegram(conciergeUrl)} aria-label={t('client.concierge.title')}>
|
||||
<a href="#/chat" className="bc-bottomnav__plus" aria-label={t('client.concierge.title')}>
|
||||
<Icon name="chat" size={24} strokeWidth={2.4} />
|
||||
</button>
|
||||
</a>
|
||||
{item('/services', 'grid', t('client.nav.services'))}
|
||||
{item('/profile', 'user', t('client.nav.profile'))}
|
||||
</nav>
|
||||
|
||||
Reference in new issue
Block a user