fix: funnel show all steps, center KPI, top nav, roles RU, admin users tab
This commit is contained in:
parent
0d5fb1b79a
commit
cf18ecb6ff
|
|
@ -110,19 +110,14 @@ export const AdminDashboard = () => {
|
||||||
total: d.total || 0, delivered: d.delivered || 0, problems: d.problems || 0,
|
total: d.total || 0, delivered: d.delivered || 0, problems: d.problems || 0,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const confirmedSms1 = econ.confirmed_after_sms1 || 0;
|
// Funnel: ALWAYS show all steps, even with 0 values
|
||||||
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;
|
|
||||||
|
|
||||||
const funnelSteps = [
|
const funnelSteps = [
|
||||||
{ label: 'Согласовано после SMS 1', value: confirmedSms1, color: '#22c55e' },
|
{ label: 'Согласовано после 1-й SMS', value: econ.confirmed_after_sms1 || 0, color: '#22c55e' },
|
||||||
{ label: 'Согласовано после SMS 2', value: confirmedSms2, color: '#14b8a6' },
|
{ label: 'Согласовано после 2-й SMS', value: econ.confirmed_after_sms2 || 0, color: '#14b8a6' },
|
||||||
{ label: 'Согласовано вручную', value: confirmedManual, color: '#eab308' },
|
{ label: 'Согласовано вручную', value: econ.confirmed_via_manual || 0, color: '#eab308' },
|
||||||
{ label: 'Платное хранение', value: paidStorage, color: '#06b6d4' },
|
{ label: 'Платное хранение', value: econ.paid_storage_count || 0, color: '#06b6d4' },
|
||||||
{ label: 'Отмена', value: cancelled, color: '#ef4444' },
|
{ label: 'Отмена', value: econ.cancelled_count || 0, color: '#ef4444' },
|
||||||
].filter(s => s.value > 0);
|
];
|
||||||
|
|
||||||
// Responsive values
|
// Responsive values
|
||||||
const chartHeight = mobile ? 200 : 240;
|
const chartHeight = mobile ? 200 : 240;
|
||||||
|
|
@ -144,7 +139,7 @@ export const AdminDashboard = () => {
|
||||||
<SegmentedTabs items={PERIOD_OPTIONS} activeKey={period} onChange={setPeriod} />
|
<SegmentedTabs items={PERIOD_OPTIONS} activeKey={period} onChange={setPeriod} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* KPI — 2 cols on mobile, auto-fit on desktop */}
|
{/* KPI — centered on mobile */}
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: mobile ? '1fr 1fr' : `repeat(auto-fit, minmax(${kpiMin}, 1fr))`, gap: '0.4rem' }}>
|
<div style={{ display: 'grid', gridTemplateColumns: mobile ? '1fr 1fr' : `repeat(auto-fit, minmax(${kpiMin}, 1fr))`, gap: '0.4rem' }}>
|
||||||
{[
|
{[
|
||||||
{ label: 'Всего', val: totalGroups },
|
{ label: 'Всего', val: totalGroups },
|
||||||
|
|
@ -154,7 +149,7 @@ export const AdminDashboard = () => {
|
||||||
{ label: 'Проблемы', val: sv.problem },
|
{ label: 'Проблемы', val: sv.problem },
|
||||||
{ label: '% доставки', val: sv.delivery_rate != null ? sv.delivery_rate + '%' : '—' },
|
{ label: '% доставки', val: sv.delivery_rate != null ? sv.delivery_rate + '%' : '—' },
|
||||||
].map((kpi, i) => (
|
].map((kpi, i) => (
|
||||||
<Panel key={i} style={{ padding: mobile ? '0.4rem 0.6rem' : '0.5rem 0.75rem' }}>
|
<Panel key={i} style={{ padding: mobile ? '0.4rem 0.6rem' : '0.5rem 0.75rem', textAlign: 'center' }}>
|
||||||
<div style={{ fontSize: fontSize.xs, color: 'var(--color-text-muted)', marginBottom: '0.05rem' }}>{kpi.label}</div>
|
<div style={{ fontSize: fontSize.xs, color: 'var(--color-text-muted)', marginBottom: '0.05rem' }}>{kpi.label}</div>
|
||||||
<div style={{ fontSize: mobile ? '1rem' : '1.2rem', fontWeight: 700, color: 'var(--color-text)' }}>{kpi.val ?? '—'}</div>
|
<div style={{ fontSize: mobile ? '1rem' : '1.2rem', fontWeight: 700, color: 'var(--color-text)' }}>{kpi.val ?? '—'}</div>
|
||||||
</Panel>
|
</Panel>
|
||||||
|
|
@ -240,31 +235,28 @@ export const AdminDashboard = () => {
|
||||||
)}
|
)}
|
||||||
</Panel>
|
</Panel>
|
||||||
|
|
||||||
{/* Воронка согласования */}
|
{/* Воронка согласования — ALL steps always visible */}
|
||||||
<Panel style={{ padding: mobile ? '0.75rem' : '1rem' }}>
|
<Panel style={{ padding: mobile ? '0.75rem' : '1rem' }}>
|
||||||
<h3 style={{ fontSize: fontSize.l, fontWeight: 600, marginBottom: '0.5rem', color: 'var(--color-text)' }}>Воронка согласования</h3>
|
<h3 style={{ fontSize: fontSize.l, fontWeight: 600, marginBottom: '0.5rem', color: 'var(--color-text)' }}>Воронка согласования</h3>
|
||||||
{totalGroups === 0 ? (
|
{totalGroups === 0 ? (
|
||||||
<div style={{ color: 'var(--color-text-muted)', textAlign: 'center', padding: '1rem' }}>Нет данных</div>
|
<div style={{ color: 'var(--color-text-muted)', textAlign: 'center', padding: '1rem' }}>Нет данных</div>
|
||||||
) : (
|
|
||||||
<div>
|
|
||||||
{funnelSteps.length === 0 ? (
|
|
||||||
<div style={{ color: 'var(--color-text-muted)', textAlign: 'center', padding: '1rem' }}>Нет завершённых согласований</div>
|
|
||||||
) : (
|
) : (
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '0', padding: '0.4rem 0' }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '0', padding: '0.4rem 0' }}>
|
||||||
{funnelSteps.map((step, i) => {
|
{funnelSteps.map((step, i) => {
|
||||||
const pct = totalGroups > 0 ? Math.round((step.value / totalGroups) * 100) : 0;
|
const pct = totalGroups > 0 ? Math.round((step.value / totalGroups) * 100) : 0;
|
||||||
const widthPct = Math.max(15, (step.value / totalGroups) * 100);
|
const widthPct = step.value > 0 ? Math.max(15, (step.value / totalGroups) * 100) : 15;
|
||||||
return (
|
return (
|
||||||
<div key={i} style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '1px', width: '100%' }}>
|
<div key={i} style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '1px', width: '100%' }}>
|
||||||
<div style={{ fontSize: mobile ? '0.8rem' : '0.85rem', fontWeight: 700, color: 'var(--color-text)', textAlign: 'center' }}>
|
<div style={{ fontSize: mobile ? '0.8rem' : '0.85rem', fontWeight: 700, color: 'var(--color-text)', textAlign: 'center' }}>
|
||||||
{step.value}
|
{step.value}
|
||||||
</div>
|
</div>
|
||||||
<div style={{
|
<div style={{
|
||||||
width: widthPct + '%', height: mobile ? '28px' : '32px', background: step.color,
|
width: widthPct + '%', height: mobile ? '28px' : '32px', background: step.value > 0 ? step.color : 'var(--color-border, #334155)',
|
||||||
borderRadius: '4px', display: 'flex', alignItems: 'center', justifyContent: 'center',
|
borderRadius: '4px', display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||||
transition: 'width 0.4s ease', minWidth: '40px', maxWidth: '100%',
|
transition: 'width 0.4s ease', minWidth: '40px', maxWidth: '100%',
|
||||||
|
opacity: step.value > 0 ? 1 : 0.5,
|
||||||
}}>
|
}}>
|
||||||
<span style={{ fontSize: mobile ? '0.6rem' : '0.7rem', fontWeight: 600, color: '#fff', textShadow: '0 1px 2px rgba(0,0,0,0.3)' }}>
|
<span style={{ fontSize: mobile ? '0.6rem' : '0.7rem', fontWeight: 600, color: step.value > 0 ? '#fff' : 'var(--color-text-muted)', textShadow: step.value > 0 ? '0 1px 2px rgba(0,0,0,0.3)' : 'none' }}>
|
||||||
{pct}%
|
{pct}%
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -277,8 +269,6 @@ export const AdminDashboard = () => {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div style={{
|
<div style={{
|
||||||
display: 'grid', gridTemplateColumns: mobile ? '1fr 1fr' : '1fr 1fr 1fr', gap: '0.5rem',
|
display: 'grid', gridTemplateColumns: mobile ? '1fr 1fr' : '1fr 1fr 1fr', gap: '0.5rem',
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,15 @@ const supabaseServiceKey = import.meta.env.VITE_SUPABASE_SERVICE_ROLE_KEY;
|
||||||
|
|
||||||
const ROLES = ['admin', 'driver', 'logistician', 'manager', 'mega_admin', 'production_lead'];
|
const ROLES = ['admin', 'driver', 'logistician', 'manager', 'mega_admin', 'production_lead'];
|
||||||
|
|
||||||
|
const ROLE_LABELS = {
|
||||||
|
mega_admin: 'Суперадмин',
|
||||||
|
admin: 'Администратор',
|
||||||
|
manager: 'Менеджер',
|
||||||
|
production_lead: 'Начальник производства',
|
||||||
|
logistician: 'Логист',
|
||||||
|
driver: 'Водитель-экспедитор',
|
||||||
|
};
|
||||||
|
|
||||||
const ROLE_TONES = {
|
const ROLE_TONES = {
|
||||||
mega_admin: 'danger',
|
mega_admin: 'danger',
|
||||||
admin: 'warning',
|
admin: 'warning',
|
||||||
|
|
@ -20,6 +29,18 @@ const ROLE_TONES = {
|
||||||
driver: 'accent',
|
driver: 'accent',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const useIsMobile = () => {
|
||||||
|
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() {
|
export default function UserManagementPanel() {
|
||||||
const [users, setUsers] = useState([]);
|
const [users, setUsers] = useState([]);
|
||||||
const [roles, setRoles] = useState([]);
|
const [roles, setRoles] = useState([]);
|
||||||
|
|
@ -31,6 +52,7 @@ export default function UserManagementPanel() {
|
||||||
const [addForm, setAddForm] = useState({ name: '', email: '', role: '' });
|
const [addForm, setAddForm] = useState({ name: '', email: '', role: '' });
|
||||||
const [addSubmitting, setAddSubmitting] = useState(false);
|
const [addSubmitting, setAddSubmitting] = useState(false);
|
||||||
const [addError, setAddError] = useState(null);
|
const [addError, setAddError] = useState(null);
|
||||||
|
const mobile = useIsMobile();
|
||||||
|
|
||||||
const client = createClient(supabaseUrl, supabaseAnonKey);
|
const client = createClient(supabaseUrl, supabaseAnonKey);
|
||||||
const adminClient = supabaseServiceKey ? createClient(supabaseUrl, supabaseServiceKey) : null;
|
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') : '—';
|
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 (
|
return (
|
||||||
<Panel>
|
<Panel>
|
||||||
{/* Toolbar */}
|
{/* Toolbar */}
|
||||||
|
|
@ -151,7 +180,7 @@ export default function UserManagementPanel() {
|
||||||
fontSize: '0.85rem',
|
fontSize: '0.85rem',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{showAddForm ? '✕ Отмена' : '+ Добавить'}
|
{showAddForm ? 'Отмена' : '+ Добавить'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -186,7 +215,7 @@ export default function UserManagementPanel() {
|
||||||
className="min-w-[140px]!"
|
className="min-w-[140px]!"
|
||||||
>
|
>
|
||||||
<option value="">Выберите роль…</option>
|
<option value="">Выберите роль…</option>
|
||||||
{ROLES.map((r) => <option key={r} value={r}>{r}</option>)}
|
{ROLES.map((r) => <option key={r} value={r}>{ROLE_LABELS[r] || r}</option>)}
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
{addError && (
|
{addError && (
|
||||||
|
|
@ -228,14 +257,15 @@ export default function UserManagementPanel() {
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div style={{ textAlign: 'center', padding: '2rem', color: 'var(--color-text-muted, #94a3b8)' }}>
|
<div style={{ textAlign: 'center', padding: '2rem', color: 'var(--color-text-muted, #94a3b8)' }}>
|
||||||
Загрузка…
|
Загрузка&
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '0' }}>
|
<div style={mobileTableStyle}>
|
||||||
|
<div style={{ minWidth: mobile ? '600px' : 'auto' }}>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div style={{
|
<div style={{
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
gridTemplateColumns: '2fr 2fr 1.2fr 1fr auto',
|
gridTemplateColumns: '2fr 1.5fr 1.2fr 1fr auto',
|
||||||
gap: '0.5rem',
|
gap: '0.5rem',
|
||||||
padding: '0.5rem 0.75rem',
|
padding: '0.5rem 0.75rem',
|
||||||
fontSize: '0.8rem',
|
fontSize: '0.8rem',
|
||||||
|
|
@ -251,12 +281,14 @@ export default function UserManagementPanel() {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{users.map((user) => (
|
{users.map((user) => {
|
||||||
|
const rn = getRoleName(user);
|
||||||
|
return (
|
||||||
<div
|
<div
|
||||||
key={user.id}
|
key={user.id}
|
||||||
style={{
|
style={{
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
gridTemplateColumns: '2fr 2fr 1.2fr 1fr auto',
|
gridTemplateColumns: '2fr 1.5fr 1.2fr 1fr auto',
|
||||||
gap: '0.5rem',
|
gap: '0.5rem',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
padding: '0.6rem 0.75rem',
|
padding: '0.6rem 0.75rem',
|
||||||
|
|
@ -269,17 +301,17 @@ export default function UserManagementPanel() {
|
||||||
|
|
||||||
{editingRoleId === user.id ? (
|
{editingRoleId === user.id ? (
|
||||||
<Select
|
<Select
|
||||||
value={getRoleName(user)}
|
value={rn}
|
||||||
onChange={(e) => handleRoleChange(user.id, e.target.value)}
|
onChange={(e) => handleRoleChange(user.id, e.target.value)}
|
||||||
onBlur={() => setEditingRoleId(null)}
|
onBlur={() => setEditingRoleId(null)}
|
||||||
autoFocus
|
autoFocus
|
||||||
className="text-xs! py-1!"
|
className="text-xs! py-1!"
|
||||||
>
|
>
|
||||||
{ROLES.map((r) => <option key={r} value={r}>{r}</option>)}
|
{ROLES.map((r) => <option key={r} value={r}>{ROLE_LABELS[r] || r}</option>)}
|
||||||
</Select>
|
</Select>
|
||||||
) : (
|
) : (
|
||||||
<span onClick={() => setEditingRoleId(user.id)} style={{ cursor: 'pointer' }}>
|
<span onClick={() => setEditingRoleId(user.id)} style={{ cursor: 'pointer' }}>
|
||||||
<Badge tone={ROLE_TONES[getRoleName(user)] || 'neutral'}>{getRoleName(user)}</Badge>
|
<Badge tone={ROLE_TONES[rn] || 'neutral'}>{ROLE_LABELS[rn] || rn}</Badge>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -305,7 +337,9 @@ export default function UserManagementPanel() {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Panel>
|
</Panel>
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ export const ROLE_LABELS = {
|
||||||
logistician: "Логист",
|
logistician: "Логист",
|
||||||
driver: "Водитель-экспедитор",
|
driver: "Водитель-экспедитор",
|
||||||
admin: "Администратор",
|
admin: "Администратор",
|
||||||
|
mega_admin: "Суперадмин",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ROLE_PERMISSIONS = {
|
export const ROLE_PERMISSIONS = {
|
||||||
|
|
@ -32,4 +33,10 @@ export const ROLE_PERMISSIONS = {
|
||||||
"Управление пользователями и ролями",
|
"Управление пользователями и ролями",
|
||||||
"Логи, ошибки и история действий",
|
"Логи, ошибки и история действий",
|
||||||
],
|
],
|
||||||
|
mega_admin: [
|
||||||
|
"Полный доступ ко всем разделам",
|
||||||
|
"Управление пользователями и ролями",
|
||||||
|
"Аналитика и автоматизация",
|
||||||
|
"Логи ошибок",
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
@ -46,6 +46,7 @@ export const AppShell = ({
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen px-3 py-4 sm:px-4 md:px-6 md:py-8">
|
<div className="min-h-screen px-3 py-4 sm:px-4 md:px-6 md:py-8">
|
||||||
<div className="mx-auto max-w-[1540px] space-y-4 xl:grid xl:grid-cols-[220px_1fr] xl:gap-8 xl:space-y-0">
|
<div className="mx-auto max-w-[1540px] space-y-4 xl:grid xl:grid-cols-[220px_1fr] xl:gap-8 xl:space-y-0">
|
||||||
|
{/* Desktop sidebar */}
|
||||||
<Panel className="hidden h-fit flex-col gap-5 p-4 xl:flex">
|
<Panel className="hidden h-fit flex-col gap-5 p-4 xl:flex">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs uppercase tracking-[0.24em] text-[var(--color-text-muted)]">
|
<p className="text-xs uppercase tracking-[0.24em] text-[var(--color-text-muted)]">
|
||||||
|
|
@ -85,7 +86,9 @@ export const AppShell = ({
|
||||||
</div>
|
</div>
|
||||||
</Panel>
|
</Panel>
|
||||||
|
|
||||||
<div className="min-w-0 space-y-5 pb-24 xl:space-y-8 xl:pb-0">
|
{/* Main content area */}
|
||||||
|
<div className="min-w-0 space-y-5 pb-20 xl:space-y-8 xl:pb-0">
|
||||||
|
{/* Mobile header */}
|
||||||
<Panel className="p-4 xl:hidden">
|
<Panel className="p-4 xl:hidden">
|
||||||
<div className="flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
|
<div className="flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
|
||||||
<div className="min-w-0 flex-1 space-y-1">
|
<div className="min-w-0 flex-1 space-y-1">
|
||||||
|
|
@ -96,7 +99,7 @@ export const AppShell = ({
|
||||||
{sectionMeta?.label || "Панель"}
|
{sectionMeta?.label || "Панель"}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-sm leading-6 text-[var(--color-text-muted)]">
|
<p className="text-sm leading-6 text-[var(--color-text-muted)]">
|
||||||
{user.name} · {ROLE_LABELS[user.role]}
|
{user.name} · {ROLE_LABELS[user.role] || user.role}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap items-center justify-end gap-2 md:flex-shrink-0">
|
<div className="flex flex-wrap items-center justify-end gap-2 md:flex-shrink-0">
|
||||||
|
|
@ -121,6 +124,33 @@ export const AppShell = ({
|
||||||
</div>
|
</div>
|
||||||
</Panel>
|
</Panel>
|
||||||
|
|
||||||
|
{/* Mobile tab navigation — STICKY TOP */}
|
||||||
|
{shouldShowMobileNav && (
|
||||||
|
<div className="sticky inset-x-0 top-0 z-40 -mx-3 -mt-4 border-b border-[var(--color-border)] bg-[var(--color-surface)] px-3 py-2 backdrop-blur xl:hidden sm:-mx-4 md:-mx-6">
|
||||||
|
<div className="flex gap-1 overflow-x-auto" style={{ WebkitOverflowScrolling: 'touch', scrollbarWidth: 'none' }}>
|
||||||
|
{navItems.map((item) => (
|
||||||
|
<button
|
||||||
|
key={item.key}
|
||||||
|
className={[
|
||||||
|
"flex flex-shrink-0 items-center gap-1.5 rounded-[14px] px-3 py-2 text-sm transition",
|
||||||
|
activeSection === item.key
|
||||||
|
? "bg-[var(--color-accent)] text-[var(--color-accent-contrast)]"
|
||||||
|
: "bg-[var(--color-surface-strong)] text-[var(--color-text-muted)]",
|
||||||
|
].join(" ")}
|
||||||
|
onClick={() => onSectionChange(item.key)}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<span className="truncate font-medium">{item.label}</span>
|
||||||
|
{item.badge ? (
|
||||||
|
<Badge tone={activeSection === item.key ? "neutral" : "accent"}>{item.badge}</Badge>
|
||||||
|
) : null}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Desktop header */}
|
||||||
<Panel className="hidden p-4 md:p-5 xl:block">
|
<Panel className="hidden p-4 md:p-5 xl:block">
|
||||||
<div className="flex flex-wrap items-center justify-between gap-4">
|
<div className="flex flex-wrap items-center justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -144,7 +174,7 @@ export const AppShell = ({
|
||||||
/>
|
/>
|
||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
<div className="text-sm font-medium">{user.name}</div>
|
<div className="text-sm font-medium">{user.name}</div>
|
||||||
<div className="text-sm text-[var(--color-text-muted)]">{ROLE_LABELS[user.role]}</div>
|
<div className="text-sm text-[var(--color-text-muted)]">{ROLE_LABELS[user.role] || user.role}</div>
|
||||||
</div>
|
</div>
|
||||||
{onOpenGuide ? (
|
{onOpenGuide ? (
|
||||||
<Button size="sm" variant="ghost" onClick={onOpenGuide} aria-label="Справка">
|
<Button size="sm" variant="ghost" onClick={onOpenGuide} aria-label="Справка">
|
||||||
|
|
@ -160,31 +190,6 @@ export const AppShell = ({
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{shouldShowMobileNav ? (
|
|
||||||
<div className="fixed inset-x-0 bottom-0 z-40 border-t border-[var(--color-border)] bg-[var(--color-surface)] px-3 py-3 backdrop-blur xl:hidden">
|
|
||||||
<div className="mx-auto flex max-w-[1540px] gap-2 overflow-x-auto">
|
|
||||||
{navItems.map((item) => (
|
|
||||||
<button
|
|
||||||
key={item.key}
|
|
||||||
className={[
|
|
||||||
"flex min-w-[120px] flex-1 items-center justify-center gap-2 rounded-[18px] px-3 py-3 text-sm transition",
|
|
||||||
activeSection === item.key
|
|
||||||
? "bg-[var(--color-accent)] text-[var(--color-accent-contrast)]"
|
|
||||||
: "bg-[var(--color-surface)] text-[var(--color-text-muted)]",
|
|
||||||
].join(" ")}
|
|
||||||
onClick={() => onSectionChange(item.key)}
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<span className="truncate font-medium">{item.label}</span>
|
|
||||||
{item.badge ? (
|
|
||||||
<Badge tone={activeSection === item.key ? "neutral" : "accent"}>{item.badge}</Badge>
|
|
||||||
) : null}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
@ -35,6 +35,7 @@ export const DashboardPage = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const userRole = user?.role;
|
const userRole = user?.role;
|
||||||
const isMegaAdmin = userRole === "mega_admin";
|
const isMegaAdmin = userRole === "mega_admin";
|
||||||
|
const isAdmin = userRole === "admin" || isMegaAdmin;
|
||||||
const section = ROLE_SECTION[userRole] || ROLE_SECTION.manager;
|
const section = ROLE_SECTION[userRole] || ROLE_SECTION.manager;
|
||||||
const [activeSection, setActiveSection] = React.useState(section.key);
|
const [activeSection, setActiveSection] = React.useState(section.key);
|
||||||
|
|
||||||
|
|
@ -83,6 +84,7 @@ export const DashboardPage = () => {
|
||||||
? [
|
? [
|
||||||
{ key: "analytics", label: "Аналитика", description: "Статистика доставки.", badge: null },
|
{ key: "analytics", label: "Аналитика", description: "Статистика доставки.", badge: null },
|
||||||
{ key: "orders", label: "Группы", description: "Реестр групп доставки.", badge: String(allOrderGroups.length || orderGroups.length || 0) },
|
{ 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) },
|
{ key: section.key, label: section.label, description: section.description, badge: String(allOrderGroups.length || orderGroups.length || 0) },
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue