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