From cf18ecb6ff3552d3758f4187e64096a960405f5c Mon Sep 17 00:00:00 2001 From: root Date: Mon, 25 May 2026 13:17:24 +0000 Subject: [PATCH] fix: funnel show all steps, center KPI, top nav, roles RU, admin users tab --- src/components/admin/AdminDashboard.jsx | 86 ++++----- src/components/admin/UserManagementPanel.jsx | 184 +++++++++++-------- src/constants/roles.js | 9 +- src/layouts/AppShell.jsx | 63 ++++--- src/pages/DashboardPage.jsx | 4 +- 5 files changed, 192 insertions(+), 154 deletions(-) diff --git a/src/components/admin/AdminDashboard.jsx b/src/components/admin/AdminDashboard.jsx index 4d676ec..f922cbd 100644 --- a/src/components/admin/AdminDashboard.jsx +++ b/src/components/admin/AdminDashboard.jsx @@ -110,19 +110,14 @@ export const AdminDashboard = () => { total: d.total || 0, delivered: d.delivered || 0, problems: d.problems || 0, })); - const confirmedSms1 = econ.confirmed_after_sms1 || 0; - const confirmedSms2 = econ.confirmed_after_sms2 || 0; - const confirmedManual = econ.confirmed_via_manual || 0; - const paidStorage = econ.paid_storage_count || 0; - const cancelled = econ.cancelled_count || 0; - + // Funnel: ALWAYS show all steps, even with 0 values const funnelSteps = [ - { label: 'Согласовано после SMS 1', value: confirmedSms1, color: '#22c55e' }, - { label: 'Согласовано после SMS 2', value: confirmedSms2, color: '#14b8a6' }, - { label: 'Согласовано вручную', value: confirmedManual, color: '#eab308' }, - { label: 'Платное хранение', value: paidStorage, color: '#06b6d4' }, - { label: 'Отмена', value: cancelled, color: '#ef4444' }, - ].filter(s => s.value > 0); + { label: 'Согласовано после 1-й SMS', value: econ.confirmed_after_sms1 || 0, color: '#22c55e' }, + { label: 'Согласовано после 2-й SMS', value: econ.confirmed_after_sms2 || 0, color: '#14b8a6' }, + { label: 'Согласовано вручную', value: econ.confirmed_via_manual || 0, color: '#eab308' }, + { label: 'Платное хранение', value: econ.paid_storage_count || 0, color: '#06b6d4' }, + { label: 'Отмена', value: econ.cancelled_count || 0, color: '#ef4444' }, + ]; // Responsive values const chartHeight = mobile ? 200 : 240; @@ -144,7 +139,7 @@ export const AdminDashboard = () => { - {/* KPI — 2 cols on mobile, auto-fit on desktop */} + {/* KPI — centered on mobile */}
{[ { label: 'Всего', val: totalGroups }, @@ -154,7 +149,7 @@ export const AdminDashboard = () => { { label: 'Проблемы', val: sv.problem }, { label: '% доставки', val: sv.delivery_rate != null ? sv.delivery_rate + '%' : '—' }, ].map((kpi, i) => ( - +
{kpi.label}
{kpi.val ?? '—'}
@@ -240,45 +235,40 @@ export const AdminDashboard = () => { )}
- {/* Воронка согласования */} + {/* Воронка согласования — ALL steps always visible */}

Воронка согласования

{totalGroups === 0 ? (
Нет данных
) : ( -
- {funnelSteps.length === 0 ? ( -
Нет завершённых согласований
- ) : ( -
- {funnelSteps.map((step, i) => { - const pct = totalGroups > 0 ? Math.round((step.value / totalGroups) * 100) : 0; - const widthPct = Math.max(15, (step.value / totalGroups) * 100); - return ( -
-
- {step.value} -
-
- - {pct}% - -
-
- {step.label} -
- {i < funnelSteps.length - 1 && ( -
- )} -
- ); - })} -
- )} +
+ {funnelSteps.map((step, i) => { + const pct = totalGroups > 0 ? Math.round((step.value / totalGroups) * 100) : 0; + const widthPct = step.value > 0 ? Math.max(15, (step.value / totalGroups) * 100) : 15; + return ( +
+
+ {step.value} +
+
0 ? step.color : 'var(--color-border, #334155)', + borderRadius: '4px', display: 'flex', alignItems: 'center', justifyContent: 'center', + transition: 'width 0.4s ease', minWidth: '40px', maxWidth: '100%', + opacity: step.value > 0 ? 1 : 0.5, + }}> + 0 ? '#fff' : 'var(--color-text-muted)', textShadow: step.value > 0 ? '0 1px 2px rgba(0,0,0,0.3)' : 'none' }}> + {pct}% + +
+
+ {step.label} +
+ {i < funnelSteps.length - 1 && ( +
+ )} +
+ ); + })}
{ + const [mobile, setMobile] = useState(false); + useEffect(() => { + const mq = window.matchMedia('(max-width: 640px)'); + setMobile(mq.matches); + const handler = (e) => setMobile(e.matches); + mq.addEventListener('change', handler); + return () => mq.removeEventListener('change', handler); + }, []); + return mobile; +}; + export default function UserManagementPanel() { const [users, setUsers] = useState([]); const [roles, setRoles] = useState([]); @@ -31,6 +52,7 @@ export default function UserManagementPanel() { const [addForm, setAddForm] = useState({ name: '', email: '', role: '' }); const [addSubmitting, setAddSubmitting] = useState(false); const [addError, setAddError] = useState(null); + const mobile = useIsMobile(); const client = createClient(supabaseUrl, supabaseAnonKey); const adminClient = supabaseServiceKey ? createClient(supabaseUrl, supabaseServiceKey) : null; @@ -131,6 +153,13 @@ export default function UserManagementPanel() { const fmtDate = (iso) => iso ? new Date(iso).toLocaleString('ru-RU') : '—'; + // Mobile: horizontal scroll for table + const mobileTableStyle = mobile ? { + overflowX: 'auto', + WebkitOverflowScrolling: 'touch', + msOverflowStyle: '-ms-autohiding-scrollbar', + } : {}; + return ( {/* Toolbar */} @@ -151,7 +180,7 @@ export default function UserManagementPanel() { fontSize: '0.85rem', }} > - {showAddForm ? '✕ Отмена' : '+ Добавить'} + {showAddForm ? 'Отмена' : '+ Добавить'}
@@ -186,7 +215,7 @@ export default function UserManagementPanel() { className="min-w-[140px]!" > - {ROLES.map((r) => )} + {ROLES.map((r) => )}
{addError && ( @@ -228,84 +257,89 @@ export default function UserManagementPanel() { {loading ? (
- Загрузка… + Загрузка&
) : ( -
- {/* Header */} -
- EmailИмяРольСоздан -
- - {users.length === 0 && ( -
- Нет пользователей +
+
+ {/* Header */} +
+ EmailИмяРольСоздан
- )} - {users.map((user) => ( -
- {user.email} - {user.name || '—'} - - {editingRoleId === user.id ? ( - - ) : ( - setEditingRoleId(user.id)} style={{ cursor: 'pointer' }}> - {getRoleName(user)} - - )} - - {fmtDate(user.created_at)} - -
- {deleteConfirmId === user.id ? ( - <> - - - - ) : ( - - )} + {users.length === 0 && ( +
+ Нет пользователей
-
- ))} + )} + + {users.map((user) => { + const rn = getRoleName(user); + return ( +
+ {user.email} + {user.name || '—'} + + {editingRoleId === user.id ? ( + + ) : ( + setEditingRoleId(user.id)} style={{ cursor: 'pointer' }}> + {ROLE_LABELS[rn] || rn} + + )} + + {fmtDate(user.created_at)} + +
+ {deleteConfirmId === user.id ? ( + <> + + + + ) : ( + + )} +
+
+ ); + })} +
)} diff --git a/src/constants/roles.js b/src/constants/roles.js index 8437ee0..1b5221f 100644 --- a/src/constants/roles.js +++ b/src/constants/roles.js @@ -4,6 +4,7 @@ export const ROLE_LABELS = { logistician: "Логист", driver: "Водитель-экспедитор", admin: "Администратор", + mega_admin: "Суперадмин", }; export const ROLE_PERMISSIONS = { @@ -32,4 +33,10 @@ export const ROLE_PERMISSIONS = { "Управление пользователями и ролями", "Логи, ошибки и история действий", ], -}; + mega_admin: [ + "Полный доступ ко всем разделам", + "Управление пользователями и ролями", + "Аналитика и автоматизация", + "Логи ошибок", + ], +}; \ No newline at end of file diff --git a/src/layouts/AppShell.jsx b/src/layouts/AppShell.jsx index ae1fbd0..3eba546 100644 --- a/src/layouts/AppShell.jsx +++ b/src/layouts/AppShell.jsx @@ -46,6 +46,7 @@ export const AppShell = ({ return (
+ {/* Desktop sidebar */}

@@ -85,7 +86,9 @@ export const AppShell = ({

-
+ {/* Main content area */} +
+ {/* Mobile header */}
@@ -96,7 +99,7 @@ export const AppShell = ({ {sectionMeta?.label || "Панель"}

- {user.name} · {ROLE_LABELS[user.role]} + {user.name} · {ROLE_LABELS[user.role] || user.role}

@@ -121,6 +124,33 @@ export const AppShell = ({
+ {/* Mobile tab navigation — STICKY TOP */} + {shouldShowMobileNav && ( +
+
+ {navItems.map((item) => ( + + ))} +
+
+ )} + + {/* Desktop header */}
@@ -144,7 +174,7 @@ export const AppShell = ({ />
{user.name}
-
{ROLE_LABELS[user.role]}
+
{ROLE_LABELS[user.role] || user.role}
{onOpenGuide ? (
- - {shouldShowMobileNav ? ( -
-
- {navItems.map((item) => ( - - ))} -
-
- ) : null}
); -}; +}; \ No newline at end of file diff --git a/src/pages/DashboardPage.jsx b/src/pages/DashboardPage.jsx index b66c750..e233ec7 100644 --- a/src/pages/DashboardPage.jsx +++ b/src/pages/DashboardPage.jsx @@ -35,6 +35,7 @@ export const DashboardPage = () => { const navigate = useNavigate(); const userRole = user?.role; const isMegaAdmin = userRole === "mega_admin"; + const isAdmin = userRole === "admin" || isMegaAdmin; const section = ROLE_SECTION[userRole] || ROLE_SECTION.manager; const [activeSection, setActiveSection] = React.useState(section.key); @@ -83,6 +84,7 @@ export const DashboardPage = () => { ? [ { key: "analytics", label: "Аналитика", description: "Статистика доставки.", badge: null }, { key: "orders", label: "Группы", description: "Реестр групп доставки.", badge: String(allOrderGroups.length || orderGroups.length || 0) }, + { key: "users", label: "Пользователи", description: "Управление пользователями.", badge: null }, ] : [ { key: section.key, label: section.label, description: section.description, badge: String(allOrderGroups.length || orderGroups.length || 0) }, @@ -154,4 +156,4 @@ export const DashboardPage = () => { {renderActiveSection()} ); -}; +}; \ No newline at end of file