diff --git a/apps/internal-ui/src/features/sales/clients/SalesClientRow.tsx b/apps/internal-ui/src/features/sales/clients/SalesClientRow.tsx
index 51515b6..afe6751 100644
--- a/apps/internal-ui/src/features/sales/clients/SalesClientRow.tsx
+++ b/apps/internal-ui/src/features/sales/clients/SalesClientRow.tsx
@@ -25,8 +25,11 @@ export function SalesClientRow({ client, menu, openClient, setMenu }: { client:
|
- {client.email}
- {client.phone}
+ {client.statusMeta.label}
+ |
+
+ {client.phone || "—"}
+ {client.username ? `@${client.username}` : ""}
|
diff --git a/apps/internal-ui/src/features/sales/clients/SalesClientsFilters.tsx b/apps/internal-ui/src/features/sales/clients/SalesClientsFilters.tsx
index a225bf9..4387096 100644
--- a/apps/internal-ui/src/features/sales/clients/SalesClientsFilters.tsx
+++ b/apps/internal-ui/src/features/sales/clients/SalesClientsFilters.tsx
@@ -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 (
-
+
{clients.dropdown !== null && }
0 || clients.dropdown === "products"}
@@ -29,7 +29,8 @@ export function SalesClientsFilters({ clients }: { clients: SalesClientsState })
onToggleOption={(code) => clients.toggleChannel(code)}
/>
-
+
+
diff --git a/apps/internal-ui/src/features/sales/clients/SalesClientsHeader.tsx b/apps/internal-ui/src/features/sales/clients/SalesClientsHeader.tsx
index e577c34..a614dc3 100644
--- a/apps/internal-ui/src/features/sales/clients/SalesClientsHeader.tsx
+++ b/apps/internal-ui/src/features/sales/clients/SalesClientsHeader.tsx
@@ -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 (
- Клиенты
- Контакты отдела продаж · показано {shownCount} из 248
+ Контакты
+ Лиды и клиенты отдела продаж · показано {shownCount} из {totalCount}
Столбцы
diff --git a/apps/internal-ui/src/features/sales/clients/SalesClientsPagination.tsx b/apps/internal-ui/src/features/sales/clients/SalesClientsPagination.tsx
index 701970f..4ced827 100644
--- a/apps/internal-ui/src/features/sales/clients/SalesClientsPagination.tsx
+++ b/apps/internal-ui/src/features/sales/clients/SalesClientsPagination.tsx
@@ -1,5 +1,5 @@
import { TablePagination } from "../../../shared/ui-controls";
-export function SalesClientsPagination({ shownCount }: { shownCount: number }) {
- return ;
+export function SalesClientsPagination({ shownCount, totalCount }: { shownCount: number; totalCount: number }) {
+ return ;
}
diff --git a/apps/internal-ui/src/features/sales/clients/SalesClientsTable.tsx b/apps/internal-ui/src/features/sales/clients/SalesClientsTable.tsx
index 092ec62..8fea067 100644
--- a/apps/internal-ui/src/features/sales/clients/SalesClientsTable.tsx
+++ b/apps/internal-ui/src/features/sales/clients/SalesClientsTable.tsx
@@ -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 &&
{clients.rows.length === 0 && }
-
+
>
);
@@ -48,7 +49,7 @@ function SalesClientsEmpty() {
return (
- Клиенты не найдены
+ Контакты не найдены
Измените условия фильтра или сбросьте их.
);
diff --git a/apps/internal-ui/src/features/sales/clients/model.ts b/apps/internal-ui/src/features/sales/clients/model.ts
index e9ad7ae..7a82eff 100644
--- a/apps/internal-ui/src/features/sales/clients/model.ts
+++ b/apps/internal-ui/src/features/sales/clients/model.ts
@@ -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 & {
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;
+
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;
-// Реальный клиент с бэкенда (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, " ")}`,
};
}
diff --git a/apps/internal-ui/src/features/sales/clients/styles.css b/apps/internal-ui/src/features/sales/clients/styles.css
index 664b14d..66a4374 100644
--- a/apps/internal-ui/src/features/sales/clients/styles.css
+++ b/apps/internal-ui/src/features/sales/clients/styles.css
@@ -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;
diff --git a/apps/internal-ui/src/features/sales/clients/useSalesClients.ts b/apps/internal-ui/src/features/sales/clients/useSalesClients.ts
index 1116e2f..82bffa6 100644
--- a/apps/internal-ui/src/features/sales/clients/useSalesClients.ts
+++ b/apps/internal-ui/src/features/sales/clients/useSalesClients.ts
@@ -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;
@@ -10,7 +10,8 @@ export function useSalesClients(salesClients: SalesClient[]) {
const [productFilter, setProductFilter] = useState([]);
const [channelFilter, setChannelFilter] = useState([]);
const [openOnly, setOpenOnly] = useState(false);
- const [buyerOnly, setBuyerOnly] = useState(false);
+ // Фильтр по статусу контакта: null — все, иначе только лиды/клиенты.
+ const [statusFilter, setStatusFilter] = useState(null);
const [sortKey, setSortKey] = useState("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;
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,
diff --git a/apps/internal-ui/src/features/sales/dialogs/context/ClientContext.tsx b/apps/internal-ui/src/features/sales/dialogs/context/ClientContext.tsx
index 1fc460e..527f9f8 100644
--- a/apps/internal-ui/src/features/sales/dialogs/context/ClientContext.tsx
+++ b/apps/internal-ui/src/features/sales/dialogs/context/ClientContext.tsx
@@ -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 ;
}
diff --git a/apps/internal-ui/src/layout/SalesSidebar.tsx b/apps/internal-ui/src/layout/SalesSidebar.tsx
index e2a9ec6..ac5eb87 100644
--- a/apps/internal-ui/src/layout/SalesSidebar.tsx
+++ b/apps/internal-ui/src/layout/SalesSidebar.tsx
@@ -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 (
diff --git a/apps/internal-ui/src/layout/Sidebar.tsx b/apps/internal-ui/src/layout/Sidebar.tsx
index e75b137..d1315d8 100644
--- a/apps/internal-ui/src/layout/Sidebar.tsx
+++ b/apps/internal-ui/src/layout/Sidebar.tsx
@@ -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 },
diff --git a/apps/internal-ui/src/layout/TopBar.tsx b/apps/internal-ui/src/layout/TopBar.tsx
index b5298ea..a8193b0 100644
--- a/apps/internal-ui/src/layout/TopBar.tsx
+++ b/apps/internal-ui/src/layout/TopBar.tsx
@@ -18,7 +18,7 @@ export function TopBar({ route, user, currentEmployee, currentProduct, currentAg
{route === "employeeDetail" && <>/{currentEmployee?.fullName || currentEmployee?.email || routes[route]}>}
{route === "productDetail" && <>/{currentProduct?.name || routes[route]}>}
{isSalesWorkspace && route !== "salesClientDetail" && route !== "salesOrderDetail" && <>/{routes[route]}>}
- {route === "salesClientDetail" && <>//{routes[route]}>}
+ {route === "salesClientDetail" && <>//{routes[route]}>}
{route === "salesOrderDetail" && <>//{routes[route]}>}
{isSupportWorkspace && <>/{routes[route]}>}
{isAi && <>/{routes[route]}>}
diff --git a/apps/internal-ui/src/routes.ts b/apps/internal-ui/src/routes.ts
index 4ee7dcb..88418dd 100644
--- a/apps/internal-ui/src/routes.ts
+++ b/apps/internal-ui/src/routes.ts
@@ -8,8 +8,8 @@ export const routes: Record = {
productDetail: "Продукты",
products: "Продукты",
profile: "Профиль",
- salesClientDetail: "Клиент",
- salesClients: "Клиенты",
+ salesClientDetail: "Контакт",
+ salesClients: "Контакты",
salesDialogs: "Диалоги",
salesOrderDetail: "Заказ",
salesOrders: "Продажи",
|