supersam/src/pages/DashboardPage.jsx

230 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* @file DashboardPage.jsx
* @description Main dashboard page. Dispatches to role-specific sections
* (analytics, orders, logistics, driver deliveries, admin panels).
* Manages navigation, notifications, PWA status, and order-group state.
*/
import React from "react";
import { Navigate, useNavigate, useSearchParams, useLocation } from "react-router-dom";
import { DriverDeliveryPlanner } from "../components/driver/DriverDeliveryPlanner";
import { isOrderGroupVisibleToDriver } from "../services/orderGroupViews";
import { LogisticsReadinessBoard } from "../components/logistics/LogisticsReadinessBoard";
import { AdminDashboard } from "../components/admin/AdminDashboard";
import UserManagementPanel from "../components/admin/UserManagementPanel";
import ErrorLogPanel from "../components/admin/ErrorLogPanel";
import { StopWordsPanel } from "../components/admin/StopWordsPanel";
import { ActionLogPanel } from "../components/admin/ActionLogPanel";
import { SuggestionsPanel } from "../components/admin/SuggestionsPanel";
import { SmsCampaignPanel } from "../components/admin/SmsCampaignPanel";
import { Panel } from "../components/UI/Panel";
import { SkeletonPage, SkeletonTable } from "../components/UI/Loading";
import { useAuth } from "../context/AuthContext";
import { useNotifications } from "../hooks/useNotifications";
import { usePushNotifications } from "../hooks/usePushNotifications";
import { usePwaStatus } from "../hooks/usePwaStatus";
import { useOrderGroups } from "../hooks/useOrderGroups";
import { AppShell } from "../layouts/AppShell";
// ── Navigation Config ─────────────────────────────────────────────────────
const MEGA_ADMIN_NAV = [
{ key: "analytics", label: "Аналитика", description: "Статистика доставки, графики и показатели.", badge: null },
{ key: "orders", label: "Группы", description: "Реестр групп доставки.", badge: null },
{ key: "users", label: "Пользователи", description: "Управление пользователями и ролями.", badge: null },
{ key: "errors", label: "Ошибки", description: "Журнал ошибок приложения.", badge: null },
{ key: "stop_words", label: "Стоп-слова", description: "Слова, исключаемые из клиентской карточки.", badge: null },
{ key: "action_log", label: "Журнал", description: "Журнал действий сотрудников.", badge: null },
{ key: "suggestions", label: "Предложения", description: "Предложения сотрудников по улучшению.", badge: null },
{ key: "sms_campaign", label: "SMS-кампании", description: "Логи и настройки SMS-рассылок.", badge: null },
];
// ── Role → Default Section Map ─────────────────────────────────────────────
const ROLE_SECTION = {
mega_admin: { key: "analytics", label: "Аналитика" },
admin: { key: "analytics", label: "Аналитика", description: "Статистика доставки." },
manager: { key: "orders", label: "Группы", description: "Реестр групп доставки, поиск и просмотр карточки." },
logistician: { key: "logistics", label: "Логистика", description: "Группы доставки по готовности к уведомлению." },
driver: { key: "deliveries", label: "Мои доставки", description: "Группы доставки по датам и статусам." },
};
// ── Dashboard Component ────────────────────────────────────────────────────
export const DashboardPage = () => {
const { user, signOut, isSessionLoading } = useAuth();
const location = useLocation();
const navigate = useNavigate();
const [searchParams, setSearchParams] = useSearchParams();
const userRole = user?.role;
const isMegaAdmin = userRole === "mega_admin";
const isAdmin = userRole === "admin" || isMegaAdmin;
const section = ROLE_SECTION[userRole] || ROLE_SECTION.manager;
// Active section from URL, fallback to role default
const activeSection = searchParams.get("tab") || section.key;
const setActiveSection = (key) => {
if (key === section.key) {
setSearchParams({}, { replace: true }); // default tab → clean URL
} else {
setSearchParams({ tab: key }, { replace: true });
}
};
// ── Notifications ─────────────────────────────────────────────────────────
const {
notifications,
unreadCount,
isLoading: notifLoading,
markAsRead: markNotificationRead,
markAllAsRead: markAllNotificationsRead,
} = useNotifications(user?.id);
const { isSupported, isSubscribed, subscribe } = usePushNotifications(user?.id);
React.useEffect(() => {
if (isSupported && !isSubscribed && user?.id && Notification.permission === "granted") {
subscribe();
}
}, [isSupported, isSubscribed, user?.id, subscribe]);
// ── PWA ────────────────────────────────────────────────────────────────────
const { isInstalled, isInstallAvailable, installApp: onInstallApp } = usePwaStatus();
// ── Order Groups ─────────────────────────────────────────────────────────
const {
orderGroups,
allOrderGroups,
filteredOrderGroups,
selectedOrderGroupId,
setSelectedOrderGroupId,
filters,
setFilters,
statusOptions,
isLoading,
loadError,
} = useOrderGroups();
// ── Derived City List ─────────────────────────────────────────────────────
const cities = React.useMemo(() => {
const set = new Set();
for (const g of allOrderGroups) {
if (g.city) set.add(g.city);
}
return [...set].sort();
}, [allOrderGroups]);
// ── Driver order count (for badge) ────────────────────────────────────────
const driverOrderCount = React.useMemo(() => {
if (userRole !== "driver" || !user) return 0;
return allOrderGroups.filter((g) => {
const isVisible = isOrderGroupVisibleToDriver(g);
const isAssignedToMe = g.assignedDriverId === user.id;
return isVisible && isAssignedToMe;
}).length;
}, [allOrderGroups, user, userRole]);
// ── Navigation Builder ────────────────────────────────────────────────────
const openGroupPage = React.useCallback((groupId) => {
navigate("/dashboard/group/" + groupId);
}, [navigate]);
const navItems = isMegaAdmin
? MEGA_ADMIN_NAV
: userRole === "admin"
? [
{ key: "analytics", label: "Аналитика", description: "Статистика доставки.", badge: null },
{ key: "orders", label: "Группы", description: "Реестр групп доставки.", badge: String(allOrderGroups.length || orderGroups.length || 0) },
{ key: "users", label: "Пользователи", description: "Управление пользователями.", badge: null },
{ key: "stop_words", label: "Стоп-слова", description: "Слова, исключаемые из карточки.", badge: null },
{ key: "errors", label: "Ошибки", description: "Журнал ошибок приложения.", badge: null },
{ key: "action_log", label: "Журнал", description: "Журнал действий сотрудников.", badge: null },
{ key: "suggestions", label: "Предложения", description: "Предложения сотрудников по улучшению.", badge: null },
]
: userRole === "logistician"
? [
{ key: "logistics", label: "Логистика", description: "Группы доставки по готовности к уведомлению.", badge: String(allOrderGroups.length || orderGroups.length || 0) },
]
: [
{ key: section.key, label: section.label, description: section.description, badge: String(userRole === "driver" ? driverOrderCount : (allOrderGroups.length || orderGroups.length || 0)) },
{ key: "suggestions", label: "Предложения", description: "Предложить улучшение.", badge: null },
];
const activeSectionMeta = navItems.find((n) => n.key === activeSection) || navItems[0];
// ── Auth Guard ────────────────────────────────────────────────────────────
const isGuideOpen = false;
const ALLOWED_DASHBOARD_ROLES = ["admin", "mega_admin", "manager", "logistician", "driver"];
// Wait for session restore before deciding redirect
if (isSessionLoading) {
return null;
}
if (!user) {
return <Navigate to={`/login?redirect=${encodeURIComponent(location.pathname + location.search)}`} replace />;
}
if (!ALLOWED_DASHBOARD_ROLES.includes(userRole)) {
return <Navigate to="/forbidden" replace />;
}
// ── Section Renderer ──────────────────────────────────────────────────────
const renderActiveSection = () => {
if (activeSection === "analytics") return <div className="space-y-6 xl:space-y-8"><AdminDashboard /></div>;
if (activeSection === "users") return <div className="space-y-6 xl:space-y-8"><UserManagementPanel /></div>;
if (activeSection === "stop_words") return <div className="space-y-6 xl:space-y-8"><StopWordsPanel /></div>;
if (activeSection === "errors") return <div className="space-y-6 xl:space-y-8"><ErrorLogPanel /></div>;
if (activeSection === "action_log") return <div className="space-y-6 xl:space-y-8"><ActionLogPanel /></div>;
if (activeSection === "suggestions") return <div className="space-y-6 xl:space-y-8"><SuggestionsPanel /></div>;
if (activeSection === "sms_campaign") return <div className="space-y-6 xl:space-y-8"><SmsCampaignPanel /></div>;
if (isLoading) {
if (userRole === "driver") {
return <SkeletonPage panels={3} />;
}
return <SkeletonTable rows={6} cols={5} />;
}
if (userRole === "driver") {
return (
<div className="space-y-6 xl:space-y-8">
<DriverDeliveryPlanner orderGroups={allOrderGroups} onOpenOrder={openGroupPage} currentUser={user} isLoading={isLoading} />
</div>
);
}
// All roles (logistician, admin, mega_admin, manager) — same grouped board
return (
<div className="space-y-6 xl:space-y-8">
<LogisticsReadinessBoard orderGroups={allOrderGroups} onSelectSet={openGroupPage} statusOptions={statusOptions} isLoading={isLoading} />
</div>
);
};
// ── Layout ────────────────────────────────────────────────────────────────
return (
<AppShell
user={user}
onSignOut={signOut}
onInstallApp={onInstallApp}
isInstalled={isInstalled}
isInstallAvailable={isInstallAvailable}
onOpenGuide={undefined}
isGuideOpen={false}
navItems={navItems}
activeSection={activeSection}
onSectionChange={setActiveSection}
sectionMeta={activeSectionMeta}
notifications={notifications}
unreadCount={unreadCount}
onMarkNotificationRead={markNotificationRead}
onMarkAllNotificationsRead={markAllNotificationsRead}
>
{loadError && (
<Panel className="border border-dashed border-[var(--color-danger)] bg-[var(--color-surface-strong)] p-4 text-sm text-[var(--color-danger)]">
Не удалось загрузить данные. Обратитесь к администратору.
</Panel>
)}
{renderActiveSection()}
</AppShell>
);
};