mirror of
https://github.com/dartdavros/chatballs.git
synced 2026-10-05 17:14:59 +03:00
feat(ui): раздел Контакты, статусы Лид/Клиент, фиксы бейджа и запроса контакта
- Клиенты → Контакты: сайдбар, крошки, роуты, тексты detail-страницы - таблица: колонка Статус (Лид/Клиент — из оплаченных заказов), реальные телефон и @логин вместо заглушек, фильтры Лиды/Клиенты, поиск по имени/телефону/логину, реальные счётчики вместо фейковых 248 - сайдбар компании: группа «Инструменты» с неактивной «Доской» - fix: бейдж «Диалоги» считает только очередь (PAUSED) — взятые оператором диалоги без ответа больше не подсвечиваются - fix: состояние «Контакт запрошен» сбрасывается при смене диалога Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b3e7d9e60e
commit
2e542a4a49
20 files changed
+91
-41
No files matched your search
@@ -47,7 +47,8 @@ def clients_overview(organization_id: int) -> list[dict]:
|
||||
Prefetch(
|
||||
"conversations",
|
||||
queryset=Conversation.objects.select_related("channel", "channel__product", "connection").order_by("-last_activity_at"),
|
||||
)
|
||||
),
|
||||
"identities",
|
||||
)
|
||||
paid_by_contact = {
|
||||
row["contact_id"]: row
|
||||
@@ -78,6 +79,11 @@ def clients_overview(organization_id: int) -> list[dict]:
|
||||
"id": contact.id,
|
||||
"cid": f"CUS-{contact.id}",
|
||||
"name": contact.name or "Гость",
|
||||
"phone": contact.phone,
|
||||
# Первый непустой @логин среди identity каналов (остальные — в карточке).
|
||||
"username": next((identity.username for identity in contact.identities.all() if identity.username), ""),
|
||||
# Статус выводится из данных: есть оплаченный заказ — клиент, иначе лид.
|
||||
"status": "client" if paid else "lead",
|
||||
"channels": sorted(channels),
|
||||
"products": sorted(products),
|
||||
"openDialogs": open_dialogs,
|
||||
|
||||
@@ -73,7 +73,10 @@ def sales_overview_stats(organization_id: int, period: str) -> dict:
|
||||
|
||||
open_qs = Conversation.objects.filter(organization_id=organization_id, lifecycle=LifecycleState.OPEN)
|
||||
open_dialogs = open_qs.count()
|
||||
waiting = open_qs.filter(expected_responder=ExpectedResponder.OPERATOR).count()
|
||||
# «Ждут оператора» = очередь: диалоги, которые никто не взял (PAUSED).
|
||||
# Взятые оператором (HUMAN), но ещё без ответа, очередью не считаются —
|
||||
# иначе бейдж «Диалоги» показывает число при полностью разобранном inbox.
|
||||
waiting = open_qs.filter(control_mode=ControlMode.PAUSED).count()
|
||||
ops = {
|
||||
"openDialogs": open_dialogs,
|
||||
"activeNow": open_qs.filter(last_activity_at__gte=now - _ACTIVE_WINDOW).count(),
|
||||
|
||||
@@ -11,15 +11,15 @@ export function SalesClientsPage({ openClient }: { openClient: (id: number) => v
|
||||
|
||||
return (
|
||||
<>
|
||||
<SalesClientsHeader shownCount={clients.rows.length} />
|
||||
<SalesClientsHeader shownCount={clients.rows.length} totalCount={data.length} />
|
||||
{loading ? (
|
||||
<LoadingState />
|
||||
) : error ? (
|
||||
<EmptyState title="Не удалось загрузить клиентов" />
|
||||
<EmptyState title="Не удалось загрузить контакты" />
|
||||
) : (
|
||||
<>
|
||||
<SalesClientsFilters clients={clients} />
|
||||
<SalesClientsTable clients={clients} openClient={openClient} />
|
||||
<SalesClientsTable clients={clients} openClient={openClient} totalCount={data.length} />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -18,7 +18,7 @@ export function SalesClientDetailPage({ contactId, setRoute }: { contactId: numb
|
||||
const { client, loading, error } = useClientDetail(contactId);
|
||||
|
||||
if (loading) return <LoadingState />;
|
||||
if (error || !client) return <EmptyState title="Не удалось загрузить клиента" />;
|
||||
if (error || !client) return <EmptyState title="Не удалось загрузить контакт" />;
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { RouteKey } from "../../../types";
|
||||
import type { ClientDetailVm } from "./model";
|
||||
|
||||
export function SalesClientDialogsTab({ dialogs, setRoute }: { dialogs: ClientDetailVm["dialogs"]; setRoute: (route: RouteKey) => void }) {
|
||||
if (dialogs.length === 0) return <EmptyState title="У клиента ещё нет диалогов" />;
|
||||
if (dialogs.length === 0) return <EmptyState title="У контакта ещё нет диалогов" />;
|
||||
return (
|
||||
<div className="sales-client-list-card">
|
||||
{dialogs.map((dialog) => (
|
||||
|
||||
@@ -30,7 +30,7 @@ export function SalesClientHeader({ client, setRoute }: { client: ClientDetailVm
|
||||
</div>
|
||||
<div className="sales-client-detail-actions">
|
||||
<Button className="sales-client-primary" icon="message" variant="primary" onClick={() => setRoute("salesDialogs")}>Открыть диалог</Button>
|
||||
<button className={`sales-client-more ${menuOpen ? "active" : ""}`} type="button" aria-label="Действия клиента" onClick={() => setMenuOpen((value) => !value)}><Icon name="more" size={18} /></button>
|
||||
<button className={`sales-client-more ${menuOpen ? "active" : ""}`} type="button" aria-label="Действия контакта" onClick={() => setMenuOpen((value) => !value)}><Icon name="more" size={18} /></button>
|
||||
{menuOpen && <SalesClientMenu />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { EmptyState } from "../../../shared/ui";
|
||||
import type { ClientDetailVm } from "./model";
|
||||
|
||||
export function SalesClientOrdersTab({ orders }: { orders: ClientDetailVm["orders"] }) {
|
||||
if (orders.length === 0) return <EmptyState title="У клиента ещё нет заказов" />;
|
||||
if (orders.length === 0) return <EmptyState title="У контакта ещё нет заказов" />;
|
||||
return (
|
||||
<div className="sales-client-table-card">
|
||||
<table className="sales-client-detail-table">
|
||||
|
||||
@@ -25,8 +25,11 @@ export function SalesClientRow({ client, menu, openClient, setMenu }: { client:
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div className={`sales-client-contact ${client.anon ? "muted" : ""}`}>{client.email}</div>
|
||||
<small className="sales-client-phone">{client.phone}</small>
|
||||
<span className="sales-client-status" style={{ background: client.statusMeta.bg, color: client.statusMeta.color }}>{client.statusMeta.label}</span>
|
||||
</td>
|
||||
<td>
|
||||
<div className={`sales-client-contact ${client.phone ? "" : "muted"}`}>{client.phone || "—"}</div>
|
||||
<small className="sales-client-phone">{client.username ? `@${client.username}` : ""}</small>
|
||||
</td>
|
||||
<td>
|
||||
<div className="sales-client-tags">
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Icon } from "../../../shared/icons";
|
||||
import { SearchInput } from "../../../shared/ui-controls";
|
||||
import { channelOptions, productOptions } from "./model";
|
||||
import { channelOptions, productOptions, statusMap } from "./model";
|
||||
import type { SalesClientsState } from "./useSalesClients";
|
||||
|
||||
export function SalesClientsFilters({ clients }: { clients: SalesClientsState }) {
|
||||
return (
|
||||
<div className="sales-clients-filterbar">
|
||||
<SearchInput className="sales-clients-search" value={clients.query} onChange={clients.setQuery} placeholder="Поиск по имени или email…" />
|
||||
<SearchInput className="sales-clients-search" value={clients.query} onChange={clients.setQuery} placeholder="Поиск по имени, телефону или логину…" />
|
||||
{clients.dropdown !== null && <button className="sales-clients-dd-scrim" type="button" aria-label="Закрыть фильтр" onClick={clients.closeDropdown} />}
|
||||
<SalesFilterDropdown
|
||||
active={clients.productFilter.length > 0 || clients.dropdown === "products"}
|
||||
@@ -29,7 +29,8 @@ export function SalesClientsFilters({ clients }: { clients: SalesClientsState })
|
||||
onToggleOption={(code) => clients.toggleChannel(code)}
|
||||
/>
|
||||
<button className={`sales-clients-chip ${clients.openOnly ? "active" : ""}`} type="button" onClick={clients.toggleOpenOnly}><span />С открытым диалогом</button>
|
||||
<button className={`sales-clients-chip ${clients.buyerOnly ? "active" : ""}`} type="button" onClick={clients.toggleBuyerOnly}><Icon name="check" size={13} />Есть покупка</button>
|
||||
<button className={`sales-clients-chip ${clients.statusFilter === "lead" ? "active" : ""}`} type="button" onClick={() => clients.toggleStatus("lead")}><span style={{ background: statusMap.lead.color }} />Лиды</button>
|
||||
<button className={`sales-clients-chip ${clients.statusFilter === "client" ? "active" : ""}`} type="button" onClick={() => clients.toggleStatus("client")}><span style={{ background: statusMap.client.color }} />Клиенты</button>
|
||||
<div className="sales-clients-filter-spacer" />
|
||||
<button className="sales-clients-reset" type="button" onClick={clients.reset}>Сбросить</button>
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { ActionButton } from "../../../shared/ui-controls";
|
||||
|
||||
export function SalesClientsHeader({ shownCount }: { shownCount: number }) {
|
||||
export function SalesClientsHeader({ shownCount, totalCount }: { shownCount: number; totalCount: number }) {
|
||||
return (
|
||||
<div className="sales-clients-header">
|
||||
<div>
|
||||
<h1>Клиенты</h1>
|
||||
<p>Контакты отдела продаж · показано <b>{shownCount}</b> из 248</p>
|
||||
<h1>Контакты</h1>
|
||||
<p>Лиды и клиенты отдела продаж · показано <b>{shownCount}</b> из {totalCount}</p>
|
||||
</div>
|
||||
<div className="sales-clients-header-actions">
|
||||
<ActionButton icon="columns">Столбцы</ActionButton>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { TablePagination } from "../../../shared/ui-controls";
|
||||
|
||||
export function SalesClientsPagination({ shownCount }: { shownCount: number }) {
|
||||
return <TablePagination className="sales-clients-pagination" shown={shownCount} total={248} pages={[1, 2, 3, "ellipsis", 25]} pageSizeLabel="10 / стр." />;
|
||||
export function SalesClientsPagination({ shownCount, totalCount }: { shownCount: number; totalCount: number }) {
|
||||
return <TablePagination className="sales-clients-pagination" shown={shownCount} total={totalCount} pages={[1]} pageSizeLabel={`${Math.max(shownCount, 1)} / стр.`} />;
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import { SalesClientsPagination } from "./SalesClientsPagination";
|
||||
import type { ClientSortKey } from "./model";
|
||||
import type { SalesClientsState } from "./useSalesClients";
|
||||
|
||||
export function SalesClientsTable({ clients, openClient }: { clients: SalesClientsState; openClient: (id: number) => void }) {
|
||||
export function SalesClientsTable({ clients, openClient, totalCount }: { clients: SalesClientsState; openClient: (id: number) => void; totalCount: number }) {
|
||||
return (
|
||||
<>
|
||||
{clients.menu && <button className="sales-clients-menu-scrim" type="button" aria-label="Закрыть меню" onClick={() => clients.setMenu(null)} />}
|
||||
@@ -13,8 +13,9 @@ export function SalesClientsTable({ clients, openClient }: { clients: SalesClien
|
||||
<table className="sales-clients-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>КЛИЕНТ</th>
|
||||
<th>КОНТАКТЫ</th>
|
||||
<th>КОНТАКТ</th>
|
||||
<th>СТАТУС</th>
|
||||
<th>ТЕЛЕФОН / ЛОГИН</th>
|
||||
<th>КАНАЛЫ</th>
|
||||
<th>ПРОДУКТЫ</th>
|
||||
<SortableTh label="ПОСЛ. ДИАЛОГ" sortKey="last" clients={clients} />
|
||||
@@ -30,7 +31,7 @@ export function SalesClientsTable({ clients, openClient }: { clients: SalesClien
|
||||
</table>
|
||||
</div>
|
||||
{clients.rows.length === 0 && <SalesClientsEmpty />}
|
||||
<SalesClientsPagination shownCount={clients.rows.length} />
|
||||
<SalesClientsPagination shownCount={clients.rows.length} totalCount={totalCount} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
@@ -48,7 +49,7 @@ function SalesClientsEmpty() {
|
||||
return (
|
||||
<div className="sales-clients-empty">
|
||||
<div><Icon name="search" size={22} /></div>
|
||||
<strong>Клиенты не найдены</strong>
|
||||
<strong>Контакты не найдены</strong>
|
||||
<span>Измените условия фильтра или сбросьте их.</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -3,14 +3,17 @@ export type ClientChannelCode = "MAX" | "TG" | "WEB";
|
||||
export type ClientSortKey = "last" | "open" | "orders" | "total";
|
||||
export type ClientDropdown = "products" | "channels";
|
||||
|
||||
export type ClientStatus = "lead" | "client";
|
||||
|
||||
export type SalesClient = {
|
||||
id: number;
|
||||
name: string;
|
||||
initials: string;
|
||||
avatarBg: string;
|
||||
cid: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
username: string;
|
||||
status: ClientStatus;
|
||||
anon?: boolean;
|
||||
channels: ClientChannelCode[];
|
||||
products: ClientProductCode[];
|
||||
@@ -27,10 +30,17 @@ export type SalesClientRowVm = Omit<SalesClient, "channels" | "products"> & {
|
||||
openColor: string;
|
||||
totalColor: string;
|
||||
totalLabel: string;
|
||||
statusMeta: { label: string; color: string; bg: string };
|
||||
channels: Array<{ label: string; full: string; color: string; bg: string }>;
|
||||
products: Array<{ name: string; color: string; bg: string }>;
|
||||
};
|
||||
|
||||
// Статус контакта: лид (писал, не покупал) / клиент (есть оплаченный заказ).
|
||||
export const statusMap = {
|
||||
lead: { label: "Лид", color: "#d48806", bg: "#fff7e6" },
|
||||
client: { label: "Клиент", color: "#389e0d", bg: "#f6ffed" },
|
||||
} satisfies Record<ClientStatus, { label: string; color: string; bg: string }>;
|
||||
|
||||
export const channelMap = {
|
||||
MAX: { label: "MAX", full: "MAX", color: "#6b5be0", bg: "#f2f0ff" },
|
||||
TG: { label: "TG", full: "Telegram", color: "#2f8fd0", bg: "#eaf6fd" },
|
||||
@@ -60,11 +70,14 @@ const statusDot = {
|
||||
closed: "#bfbfbf",
|
||||
} satisfies Record<SalesClient["mode"], string>;
|
||||
|
||||
// Реальный клиент с бэкенда (conversations/clients.py).
|
||||
// Реальный контакт с бэкенда (conversations/clients.py).
|
||||
export type ApiClient = {
|
||||
id: number;
|
||||
cid: string;
|
||||
name: string;
|
||||
phone: string;
|
||||
username: string;
|
||||
status: ClientStatus;
|
||||
channels: ClientChannelCode[];
|
||||
products: ClientProductCode[];
|
||||
openDialogs: number;
|
||||
@@ -106,8 +119,9 @@ export function toSalesClient(api: ApiClient): SalesClient {
|
||||
initials: initialsOf(api.name),
|
||||
avatarBg: isGuest ? "#8c8c8c" : avatarColor(api.cid),
|
||||
cid: api.cid,
|
||||
email: isGuest ? "без контакта" : "—",
|
||||
phone: "идентификация по каналу",
|
||||
phone: api.phone,
|
||||
username: api.username,
|
||||
status: api.status,
|
||||
anon: isGuest,
|
||||
channels: api.channels,
|
||||
products: api.products,
|
||||
@@ -128,6 +142,7 @@ export function toSalesClientRow(client: SalesClient): SalesClientRowVm {
|
||||
lastDot: statusDot[client.mode],
|
||||
openColor: client.openDialogs > 0 ? "#d48806" : "#bfbfbf",
|
||||
totalColor: client.total > 0 ? "#262626" : "#bfbfbf",
|
||||
statusMeta: statusMap[client.status],
|
||||
totalLabel: client.total === 0 ? "—" : `₽${client.total.toLocaleString("ru-RU").replace(/\u00a0/g, " ")}`,
|
||||
};
|
||||
}
|
||||
@@ -348,6 +348,16 @@
|
||||
font-size: 11.5px;
|
||||
}
|
||||
|
||||
/* Статус контакта: Лид / Клиент (цвета — statusMap в model.ts). */
|
||||
.sales-client-status {
|
||||
display: inline-block;
|
||||
padding: 3px 10px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.sales-client-tags,
|
||||
.sales-client-products {
|
||||
display: flex;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
import type { ClientChannelCode, ClientDropdown, ClientProductCode, ClientSortKey, SalesClient } from "./model";
|
||||
import type { ClientChannelCode, ClientDropdown, ClientProductCode, ClientSortKey, ClientStatus, SalesClient } from "./model";
|
||||
import { toSalesClientRow } from "./model";
|
||||
|
||||
export type SalesClientsState = ReturnType<typeof useSalesClients>;
|
||||
@@ -10,7 +10,8 @@ export function useSalesClients(salesClients: SalesClient[]) {
|
||||
const [productFilter, setProductFilter] = useState<ClientProductCode[]>([]);
|
||||
const [channelFilter, setChannelFilter] = useState<ClientChannelCode[]>([]);
|
||||
const [openOnly, setOpenOnly] = useState(false);
|
||||
const [buyerOnly, setBuyerOnly] = useState(false);
|
||||
// Фильтр по статусу контакта: null — все, иначе только лиды/клиенты.
|
||||
const [statusFilter, setStatusFilter] = useState<ClientStatus | null>(null);
|
||||
const [sortKey, setSortKey] = useState<ClientSortKey>("last");
|
||||
const [sortDir, setSortDir] = useState<"asc" | "desc">("asc");
|
||||
const [menu, setMenu] = useState<{ id: string; left: number; top: number } | null>(null);
|
||||
@@ -19,17 +20,17 @@ export function useSalesClients(salesClients: SalesClient[]) {
|
||||
const rows = useMemo(() => {
|
||||
const normalizedQuery = query.trim().toLowerCase();
|
||||
const filtered = salesClients.filter((client) => {
|
||||
return (!normalizedQuery || client.name.toLowerCase().includes(normalizedQuery) || client.email.toLowerCase().includes(normalizedQuery))
|
||||
return (!normalizedQuery || client.name.toLowerCase().includes(normalizedQuery) || client.phone.includes(normalizedQuery) || client.username.toLowerCase().includes(normalizedQuery))
|
||||
&& (productFilter.length === 0 || client.products.some((product) => productFilter.includes(product)))
|
||||
&& (channelFilter.length === 0 || client.channels.some((channel) => channelFilter.includes(channel)))
|
||||
&& (!openOnly || client.openDialogs > 0)
|
||||
&& (!buyerOnly || client.orders > 0);
|
||||
&& (statusFilter === null || client.status === statusFilter);
|
||||
});
|
||||
const key = { last: "last", open: "openDialogs", orders: "orders", total: "total" } satisfies Record<ClientSortKey, keyof typeof salesClients[number]>;
|
||||
return [...filtered]
|
||||
.sort((left, right) => sortDir === "asc" ? Number(left[key[sortKey]]) - Number(right[key[sortKey]]) : Number(right[key[sortKey]]) - Number(left[key[sortKey]]))
|
||||
.map(toSalesClientRow);
|
||||
}, [salesClients, buyerOnly, channelFilter, openOnly, productFilter, query, sortDir, sortKey]);
|
||||
}, [salesClients, statusFilter, channelFilter, openOnly, productFilter, query, sortDir, sortKey]);
|
||||
|
||||
function setQuery(value: string) {
|
||||
setQueryState(value);
|
||||
@@ -60,7 +61,7 @@ export function useSalesClients(salesClients: SalesClient[]) {
|
||||
setProductFilter([]);
|
||||
setChannelFilter([]);
|
||||
setOpenOnly(false);
|
||||
setBuyerOnly(false);
|
||||
setStatusFilter(null);
|
||||
setMenu(null);
|
||||
setDropdown(null);
|
||||
}
|
||||
@@ -71,7 +72,7 @@ export function useSalesClients(salesClients: SalesClient[]) {
|
||||
productFilter,
|
||||
channelFilter,
|
||||
openOnly,
|
||||
buyerOnly,
|
||||
statusFilter,
|
||||
dropdown,
|
||||
menu,
|
||||
setMenu,
|
||||
@@ -81,7 +82,8 @@ export function useSalesClients(salesClients: SalesClient[]) {
|
||||
toggleDropdown,
|
||||
closeDropdown: () => setDropdown(null),
|
||||
toggleOpenOnly: () => { setOpenOnly((current) => !current); setMenu(null); setDropdown(null); },
|
||||
toggleBuyerOnly: () => { setBuyerOnly((current) => !current); setMenu(null); setDropdown(null); },
|
||||
// Повторный клик по активному статусу снимает фильтр (возврат к «все»).
|
||||
toggleStatus: (status: ClientStatus) => { setStatusFilter((current) => (current === status ? null : status)); setMenu(null); setDropdown(null); },
|
||||
sortBy,
|
||||
sortArrow: (key: ClientSortKey) => sortKey === key ? (sortDir === "asc" ? "↑" : "↓") : "",
|
||||
reset,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { channelMeta } from "../../../conversations/data";
|
||||
import { FieldRow } from "../../../conversations/FieldRow";
|
||||
@@ -19,6 +19,13 @@ export function ClientContext({ dialog, detail }: { dialog: ConversationListItem
|
||||
const [justRequested, setJustRequested] = useState(false);
|
||||
const [requestError, setRequestError] = useState(false);
|
||||
|
||||
// Локальное состояние кнопки принадлежит конкретному диалогу — при переключении сбрасываем.
|
||||
useEffect(() => {
|
||||
setRequesting(false);
|
||||
setJustRequested(false);
|
||||
setRequestError(false);
|
||||
}, [detail?.id]);
|
||||
|
||||
if (!dialog) {
|
||||
return <div className="sales-client-context"><p className="sales-context-muted">Выберите диалог</p></div>;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ export function SalesSidebar({ route, user, setRoute, waitingCount = 0 }: { rout
|
||||
const nav = [
|
||||
{ key: "salesOverview" as const, label: "Обзор", icon: "grid" as const },
|
||||
{ key: "salesDialogs" as const, label: "Диалоги", icon: "message" as const, badge: waitingCount > 0 ? String(waitingCount) : undefined },
|
||||
{ key: "salesClients" as const, label: "Клиенты", icon: "team" as const },
|
||||
{ key: "salesClients" as const, label: "Контакты", icon: "team" as const },
|
||||
{ key: "salesOrders" as const, label: "Продажи", icon: "cart" as const },
|
||||
];
|
||||
return (
|
||||
|
||||
@@ -9,6 +9,8 @@ export function Sidebar({ route, user, setRoute }: { route: RouteKey; user: Sess
|
||||
{ key: "departments" as const, label: "Отделы", icon: "building" as const },
|
||||
{ key: "employees" as const, label: "Сотрудники", icon: "team" as const },
|
||||
{ key: "products" as const, label: "Продукты", icon: "box" as const },
|
||||
{ label: "ИНСТРУМЕНТЫ", group: true },
|
||||
{ label: "Доска", icon: "columns" as const, disabled: true },
|
||||
{ label: "ПЛАТФОРМА", group: true },
|
||||
{ key: "aiAgents" as const, label: "AI", icon: "robot" as const },
|
||||
{ key: "integrations" as const, label: "Интеграции", icon: "plug" as const },
|
||||
|
||||
@@ -18,7 +18,7 @@ export function TopBar({ route, user, currentEmployee, currentProduct, currentAg
|
||||
{route === "employeeDetail" && <><button type="button" onClick={() => setRoute("employees")}>Сотрудники</button><i>/</i><strong>{currentEmployee?.fullName || currentEmployee?.email || routes[route]}</strong></>}
|
||||
{route === "productDetail" && <><button type="button" onClick={() => setRoute("products")}>Продукты</button><i>/</i><strong>{currentProduct?.name || routes[route]}</strong></>}
|
||||
{isSalesWorkspace && route !== "salesClientDetail" && route !== "salesOrderDetail" && <><button type="button" onClick={() => setRoute("salesOverview")}>Продажи</button><i>/</i><strong>{routes[route]}</strong></>}
|
||||
{route === "salesClientDetail" && <><button type="button" onClick={() => setRoute("salesOverview")}>Продажи</button><i>/</i><button type="button" onClick={() => setRoute("salesClients")}>Клиенты</button><i>/</i><strong>{routes[route]}</strong></>}
|
||||
{route === "salesClientDetail" && <><button type="button" onClick={() => setRoute("salesOverview")}>Продажи</button><i>/</i><button type="button" onClick={() => setRoute("salesClients")}>Контакты</button><i>/</i><strong>{routes[route]}</strong></>}
|
||||
{route === "salesOrderDetail" && <><button type="button" onClick={() => setRoute("salesOverview")}>Продажи</button><i>/</i><button type="button" onClick={() => setRoute("salesOrders")}>Продажи</button><i>/</i><strong className="topbar-mono">{routes[route]}</strong></>}
|
||||
{isSupportWorkspace && <><button type="button" onClick={() => setRoute("supportOverview")}>Поддержка</button><i>/</i><strong>{routes[route]}</strong></>}
|
||||
{isAi && <><button type="button" onClick={() => setRoute("aiAgents")}>AI</button><i>/</i><strong>{routes[route]}</strong></>}
|
||||
|
||||
@@ -8,8 +8,8 @@ export const routes: Record<RouteKey, string> = {
|
||||
productDetail: "Продукты",
|
||||
products: "Продукты",
|
||||
profile: "Профиль",
|
||||
salesClientDetail: "Клиент",
|
||||
salesClients: "Клиенты",
|
||||
salesClientDetail: "Контакт",
|
||||
salesClients: "Контакты",
|
||||
salesDialogs: "Диалоги",
|
||||
salesOrderDetail: "Заказ",
|
||||
salesOrders: "Продажи",
|
||||
|
||||
Reference in new issue
Block a user