From c4e8add07b9007e4a21c5bce3afdb018199a18d8 Mon Sep 17 00:00:00 2001 From: ochenstarik Date: Tue, 22 Sep 2026 00:20:37 +0700 Subject: [PATCH] feat: refine CRM workspace and responsive hierarchy --- src/app/layouts/AppShell.tsx | 4 +- .../configurationAndCrmInteractions.test.tsx | 8 +- src/features/crm/CrmModule.tsx | 114 ++++++++---------- src/features/crm/components/CrmTableView.tsx | 64 +++++----- 4 files changed, 87 insertions(+), 103 deletions(-) diff --git a/src/app/layouts/AppShell.tsx b/src/app/layouts/AppShell.tsx index 5c8aa2e..ad6e3eb 100644 --- a/src/app/layouts/AppShell.tsx +++ b/src/app/layouts/AppShell.tsx @@ -103,7 +103,9 @@ export const AppShell: React.FC = () => { {/* Right Command Panel (Only on full Desktop, not on communications which has its own full right card) */} - {!isEffectiveMobile && !isEffectiveTablet && !isCommunicationsPage && } + {!isEffectiveMobile && !isEffectiveTablet && width >= 1536 && !isCommunicationsPage && ( + + )} {/* Mobile Bottom Navigation (Always present on mobile) */} diff --git a/src/features/__tests__/configurationAndCrmInteractions.test.tsx b/src/features/__tests__/configurationAndCrmInteractions.test.tsx index 2a91724..78e0580 100644 --- a/src/features/__tests__/configurationAndCrmInteractions.test.tsx +++ b/src/features/__tests__/configurationAndCrmInteractions.test.tsx @@ -255,16 +255,12 @@ describe('Configuration Engine & CRM Advanced Interactions', () => { expect(deal.id).toBeDefined(); render( - + ); - // 2. Switch to Deals Kanban tab - const dealsTab = screen.getByRole('button', { name: /Воронка сделок/i }); - fireEvent.click(dealsTab); - - // 3. Verify stage columns are rendered + // 2. Verify stage columns are rendered from the route-owned workspace. expect(screen.getByText('Квалификация')).toBeInTheDocument(); expect(screen.getByText('Выиграна')).toBeInTheDocument(); diff --git a/src/features/crm/CrmModule.tsx b/src/features/crm/CrmModule.tsx index 4bfe65a..218fff2 100644 --- a/src/features/crm/CrmModule.tsx +++ b/src/features/crm/CrmModule.tsx @@ -1,5 +1,5 @@ import React, { useState } from 'react'; -import { useLocation } from 'react-router-dom'; +import { useLocation, useNavigate } from 'react-router-dom'; import { ulid } from 'ulidx'; import { useCrm } from '../../services/hooks/useCrm'; import { useOrders } from '../../services/hooks/useOrders'; @@ -10,7 +10,7 @@ import { Deal, DealStage } from '../../entities/crm/deal'; import { Lead } from '../../entities/crm/lead'; import { OrderItem } from '../../entities/commerce/order'; import { MoneyAmount } from '../../entities/types'; -import { MaturityBadge } from '../../shared/ui/MaturityBadge'; +import { Button } from '../../shared/ui/Button'; import { useToast } from '../../shared/ui/useToast'; import { CsvImportWizard } from './CsvImportWizard'; import { DEAL_STAGES } from './constants'; @@ -26,6 +26,7 @@ import { UploadCloud } from 'lucide-react'; export const CrmModule: React.FC = () => { const location = useLocation(); + const navigate = useNavigate(); const { showToast } = useToast(); const { @@ -50,14 +51,12 @@ export const CrmModule: React.FC = () => { const dealMeta = useMetadata('deal'); const leadMeta = useMetadata('lead'); - const [activeTab, setActiveTab] = useState<'deals' | 'contacts' | 'companies' | 'leads' | 'import'>(() => { - const p = location.pathname; - if (p.includes('/crm/deals')) return 'deals'; - if (p.includes('/crm/leads')) return 'leads'; - if (p.includes('/crm/companies')) return 'companies'; - if (p.includes('/crm/import')) return 'import'; - return 'contacts'; - }); + const activeTab: 'deals' | 'contacts' | 'companies' | 'leads' | 'import' = + location.pathname.includes('/crm/deals') ? 'deals' + : location.pathname.includes('/crm/leads') ? 'leads' + : location.pathname.includes('/crm/companies') ? 'companies' + : location.pathname.includes('/crm/import') ? 'import' + : 'contacts'; const [search, setSearch] = useState(''); // Modals state @@ -76,16 +75,6 @@ export const CrmModule: React.FC = () => { const [selectedContact, setSelectedContact] = useState(null); const [selectedCompany, setSelectedCompany] = useState(null); - // Synchronize active tab with URL path if navigated externally - const p = location.pathname; - if (p !== '/crm' && p !== '/crm/') { - if (p.includes('/crm/deals') && activeTab !== 'deals') setActiveTab('deals'); - else if (p.includes('/crm/leads') && activeTab !== 'leads') setActiveTab('leads'); - else if (p.includes('/crm/companies') && activeTab !== 'companies') setActiveTab('companies'); - else if (p.includes('/crm/import') && activeTab !== 'import') setActiveTab('import'); - else if (p.includes('/crm/contacts') || p === '/crm') setActiveTab('contacts'); - } - // Deal stage transition handler const handleStageChange = async (dealId: string, nextStage: DealStage) => { const deal = deals.find((d) => d.id === dealId); @@ -210,54 +199,51 @@ export const CrmModule: React.FC = () => { } }; - const navTabs = [ - { id: 'deals', label: `Воронка сделок (${deals.length})` }, - { id: 'contacts', label: `Контакты (${contacts.length})` }, - { id: 'companies', label: `Компании (${companies.length})` }, - { id: 'leads', label: `Лиды (${leads.length})` }, - ] as const; + const pageMeta = { + deals: { + title: 'Воронка сделок', + description: `${deals.length} сделок — от первого контакта до заказа`, + }, + contacts: { + title: 'Клиенты и контакты', + description: `${contacts.length} контактов в единой клиентской базе`, + }, + companies: { + title: 'Компании', + description: `${companies.length} организаций, поставщиков и партнёров`, + }, + leads: { + title: 'Лиды', + description: `${leads.length} обращений ожидают обработки`, + }, + import: { + title: 'Импорт данных', + description: 'Загрузка клиентской базы из CSV с предварительной проверкой', + }, + }[activeTab]; return ( -
-
-
-
-

- CRM: Клиенты, Воронка продаж & Сделки -

- -
-

- Сквозной процесс: Лид → Контакт/Компания → Сделка (Kanban) → Выиграна → Заказ в Коммерции -

+
+
+
+

+ {pageMeta.title} +

+

{pageMeta.description}

- -
- {navTabs.map((t) => ( - - ))} - -
-
+ + Импорт CSV + Импорт + + )} + {activeTab === 'deals' && ( = ({ if (activeTab === 'contacts') { return (
-
-
+
+
= ({
-
- +
+
- + - - + + @@ -97,10 +97,10 @@ export const CrmTableView: React.FC = ({ - - - - + + + @@ -134,8 +134,8 @@ export const CrmTableView: React.FC = ({ if (activeTab === 'companies') { return (
-
-
+
+
= ({
-
-
ФИО / КонтактКомпанияКомпания ТелефонEmailРолиEmailРоли Действие
{comp?.name || '—'}{contact.phone || '—'}{contact.email || '—'} + {comp?.name || '—'}{contact.phone || '—'}{contact.email || '—'}
{contact.roles?.map((r) => ( @@ -118,7 +118,7 @@ export const CrmTableView: React.FC = ({ onSelectContact(contact); }} > - Карточка 360 + Карточка
+
+
- - + + - + @@ -187,10 +187,10 @@ export const CrmTableView: React.FC = ({ - - + + - @@ -223,8 +223,8 @@ export const CrmTableView: React.FC = ({ if (activeTab === 'leads') { return (
-
-
+
+
= ({
-
-
Название компанииИННГородИННГород ТелефонРолиРоли Действие
{company.inn || '—'}{company.address || '—'}{company.inn || '—'}{company.address || '—'} {company.phone || '—'} +
{company.roles?.map((r) => ( @@ -208,7 +208,7 @@ export const CrmTableView: React.FC = ({ onSelectCompany(company); }} > - Карточка 360 + Карточка
+
+
- - + + @@ -268,8 +268,8 @@ export const CrmTableView: React.FC = ({ .map((lead) => ( - - + @@ -302,7 +302,7 @@ export const CrmTableView: React.FC = ({ data-testid="convert-lead-btn" onClick={() => onConvertLead(lead)} > - Конвертировать в сделку + В сделку ) : (
Название лидаКонтактное лицоКонтактыКонтактное лицоКонтакты Оценка бюджета Статус Конвертация
{lead.title}{lead.contactName} + {lead.contactName}
{lead.phone}
{lead.email}