From 55422ec65aff4a3732037040c283f733b6490096 Mon Sep 17 00:00:00 2001 From: root Date: Fri, 12 Jun 2026 07:45:19 +0000 Subject: [PATCH] feat: add skeleton loading states across all pages - New Loading.jsx component library (Skeleton, SkeletonPanel, SkeletonPage, SkeletonTable, Spinner, LoadingBlock) - Dashboard: SkeletonTable/SkeletonPage during isLoading - OrdersTable, LogisticsReadinessBoard, DriverDeliveryPlanner: show skeleton instead of empty state - ChatTimeline, DeliverySlotsPicker, GroupDetailPage: skeleton while loading - AdminDashboard, StopWordsPanel, UserManagementPanel, ErrorLogPanel, ActionLogPanel: skeleton during initial load - NotificationSettings: skeleton for push toggle and notification preferences - ClientDeliveryPage: skeleton bars instead of text-only loading --- src/components/UI/Loading.jsx | 166 ++++++++++++++++++ src/components/admin/ActionLogPanel.jsx | 9 +- src/components/admin/AdminDashboard.jsx | 43 ++++- src/components/admin/ErrorLogPanel.jsx | 7 +- src/components/admin/StopWordsPanel.jsx | 7 +- src/components/admin/UserManagementPanel.jsx | 14 +- src/components/chat/ChatTimeline.jsx | 16 +- src/components/client/DeliverySlotsPicker.jsx | 14 ++ .../driver/DriverDeliveryPlanner.jsx | 7 +- .../logistics/LogisticsReadinessBoard.jsx | 7 +- .../notifications/NotificationSettings.jsx | 42 +++++ src/components/orders/OrdersTable.jsx | 6 + src/pages/ClientDeliveryPage.jsx | 14 +- src/pages/DashboardPage.jsx | 37 +++- src/pages/GroupDetailPage.jsx | 18 +- 15 files changed, 386 insertions(+), 21 deletions(-) create mode 100644 src/components/UI/Loading.jsx diff --git a/src/components/UI/Loading.jsx b/src/components/UI/Loading.jsx new file mode 100644 index 0000000..d822ef4 --- /dev/null +++ b/src/components/UI/Loading.jsx @@ -0,0 +1,166 @@ +import React from "react"; +import { cn } from "../../lib/cn"; + +/** + * Skeleton bar — a pulsing placeholder that mimics content shape. + * + * @param {'text'|'heading'|'avatar'|'card'|'table-row'} variant - Preset sizes + * @param {string} [className] - Additional CSS classes + * @param {number} [lines=1] - Number of skeleton lines (for text variant) + */ +export const Skeleton = ({ variant = "text", className, lines = 1 }) => { + const variantClasses = { + text: "h-4 rounded-lg", + heading: "h-6 rounded-lg", + avatar: "h-10 w-10 rounded-full", + card: "h-28 rounded-[24px]", + "table-row": "h-14 rounded-[16px]", + }; + + if (lines > 1) { + return ( +
+ {Array.from({ length: lines }).map((_, i) => ( +
+ ))} +
+ ); + } + + return ( +
+ ); +}; + +/** + * Skeleton block that mimics a Panel with header + body lines. + * + * @param {number} [lines=4] - Number of body lines + * @param {string} [className] + */ +export const SkeletonPanel = ({ lines = 4, className }) => ( +
+ +
+ {Array.from({ length: lines }).map((_, i) => ( + + ))} +
+
+); + +/** + * Full-page skeleton loader with title bar + content panels. + * + * @param {number} [panels=2] - Number of skeleton panels + * @param {string} [className] + */ +export const SkeletonPage = ({ panels = 2, className }) => ( +
+
+ + +
+ {Array.from({ length: panels }).map((_, i) => ( + + ))} +
+); + +/** + * Skeleton that mimics a table with header + rows. + * + * @param {number} [rows=4] - Number of skeleton rows + * @param {number} [cols=4] - Number of columns + * @param {string} [className] + */ +export const SkeletonTable = ({ rows = 4, cols = 4, className }) => ( +
+ {/* Header */} +
+ {Array.from({ length: cols }).map((_, i) => ( + + ))} +
+ {/* Rows */} + {Array.from({ length: rows }).map((_, rowIdx) => ( +
+ {Array.from({ length: cols }).map((_, colIdx) => ( + + ))} +
+ ))} +
+); + +/** + * Inline spinner — a compact rotating indicator for buttons and small areas. + * + * @param {string} [size='sm'] - 'xs' | 'sm' | 'md' | 'lg' + * @param {string} [className] + */ +export const Spinner = ({ size = "sm", className }) => { + const sizeClasses = { + xs: "h-3 w-3 border-[1.5px]", + sm: "h-4 w-4 border-2", + md: "h-6 w-6 border-2", + lg: "h-8 w-8 border-[3px]", + }; + + return ( + + ); +}; + +/** + * Centered loading block with spinner + optional label. + * + * @param {string} [label='Загрузка...'] + * @param {string} [size] - Spinner size + * @param {string} [className] + */ +export const LoadingBlock = ({ label = "Загрузка...", size = "md", className }) => ( +
+ + {label && {label}} +
+); \ No newline at end of file diff --git a/src/components/admin/ActionLogPanel.jsx b/src/components/admin/ActionLogPanel.jsx index 7f54bda..ce50db7 100644 --- a/src/components/admin/ActionLogPanel.jsx +++ b/src/components/admin/ActionLogPanel.jsx @@ -1,6 +1,7 @@ import React, { useState, useEffect, useMemo, useCallback } from "react"; import { Panel } from "../UI/Panel"; import { Badge } from "../UI/Badge"; +import { Skeleton } from "../UI/Loading"; import { fetchActionLogs, getActionLabel } from "../../services/supabase/actionLogService"; import { useNavigate } from "react-router-dom"; import { useAuth } from "../../context/AuthContext"; @@ -351,7 +352,13 @@ export const ActionLogPanel = ({ orderGroupId = null }) => {
- {loading &&
Загрузка...
} + {loading && !filteredLogs.length && ( +
+ {Array.from({ length: 5 }).map((_, i) => ( + + ))} +
+ )} ); }; \ No newline at end of file diff --git a/src/components/admin/AdminDashboard.jsx b/src/components/admin/AdminDashboard.jsx index ca0bd26..875ea66 100644 --- a/src/components/admin/AdminDashboard.jsx +++ b/src/components/admin/AdminDashboard.jsx @@ -1,3 +1,9 @@ +/** + * @file AdminDashboard.jsx + * @description Admin analytics dashboard. Displays KPI cards, status pie chart, + * daily trend line, confirmation funnel, SMS stats, and driver performance + * bar chart. Supports period selection (1d/7d/30d/all) and mobile layout. + */ import React, { useState, useEffect } from 'react'; import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, @@ -6,10 +12,12 @@ import { import { Panel } from '../UI/Panel'; import { Badge } from '../UI/Badge'; import { SegmentedTabs } from '../UI/SegmentedTabs'; +import { Skeleton } from '../UI/Loading'; import { useAdminStats } from '../../hooks/useAdminStats'; import { usePickupStats } from '../../hooks/usePickupStats'; import { PickupStatsPanel } from './PickupStatsPanel'; +// ── Mobile Detection Hook ─────────────────────────────────────────────────── const useIsMobile = () => { const [mobile, setMobile] = useState(false); useEffect(() => { @@ -22,6 +30,7 @@ const useIsMobile = () => { return mobile; }; +// ── Status Colour & Label Maps ───────────────────────────────────────────── const STATUS_COLORS = { pending_confirmation: '#94a3b8', manual_confirmation_required: '#eab308', @@ -50,6 +59,7 @@ const STATUS_LABELS = { pickup: 'Самовывоз', }; +// ── Period Selector Options ──────────────────────────────────────────────── const PERIOD_OPTIONS = [ { key: '1d', label: 'Сегодня' }, { key: '7d', label: '7 дней' }, @@ -57,6 +67,7 @@ const PERIOD_OPTIONS = [ { key: 'all', label: 'Все' }, ]; +// ── Custom Recharts Tooltip ───────────────────────────────────────────────── const CustomTooltip = ({ active, payload, label: tooltipLabel }) => { if (!active || !payload?.length) return null; return ( @@ -74,17 +85,37 @@ const CustomTooltip = ({ active, payload, label: tooltipLabel }) => { ); }; +// ── AdminDashboard Component ─────────────────────────────────────────────── export const AdminDashboard = () => { + // ── State & Hooks ───────────────────────────────────────────────────────── const [period, setPeriod] = useState('7d'); const mobile = useIsMobile(); const { stats, statusDist, dailyTrend, driverStats, economics, isLoading, error, refetch } = useAdminStats(period); const { stats: pickupStats, isLoading: pickupLoading } = usePickupStats(period); + // ── Loading / Error States ───────────────────────────────────────────────── if (isLoading) { return ( - -
Загрузка...
-
+
+
+ + +
+
+ {Array.from({ length: 6 }).map((_, i) => ( + + + + + ))} +
+ + +
+ +
+
+
); } if (error) { @@ -95,6 +126,7 @@ export const AdminDashboard = () => { ); } + // ── Data Preparation ────────────────────────────────────────────────────── const sv = stats || {}; const totalGroups = sv.total || 0; const econ = economics || {}; @@ -105,6 +137,7 @@ export const AdminDashboard = () => { status: s.delivery_status, })).filter(d => d.value > 0); + // ── Trend & Driver Data ─────────────────────────────────────────────────── const trendData = (dailyTrend || []).map(d => ({ date: d.date ? new Date(d.date).toLocaleDateString('ru-RU', { day: '2-digit', month: '2-digit' }) : '', delivered: d.delivered || 0, total: d.total || 0, problems: d.problems || 0, @@ -116,6 +149,7 @@ export const AdminDashboard = () => { })); // Funnel: ALWAYS show all steps, even with 0 values + // ── Funnel Data ──────────────────────────────────────────────────────────── const funnelSteps = [ { label: 'Согласовано после 1-й SMS', value: econ.confirmed_after_sms1 || 0, color: '#22c55e' }, { label: 'Согласовано после 2-й SMS', value: econ.confirmed_after_sms2 || 0, color: '#14b8a6' }, @@ -125,7 +159,7 @@ export const AdminDashboard = () => { { label: 'Отмена', value: econ.cancelled_count || 0, color: '#ef4444' }, ]; - // Responsive values + // ── Responsive Layout Values ────────────────────────────────────────────── const chartHeight = mobile ? 200 : 240; const kpiMin = mobile ? '80px' : '110px'; const chartGridCols = mobile ? '1fr' : '1fr 2fr'; @@ -133,6 +167,7 @@ export const AdminDashboard = () => { const fontSize = mobile ? { xs: '0.6rem', s: '0.7rem', m: '0.78rem', l: '0.85rem', xl: '1rem' } : { xs: '0.65rem', s: '0.68rem', m: '0.78rem', l: '0.85rem', xl: '1.1rem' }; + // ── Render ───────────────────────────────────────────────────────────────── return (
diff --git a/src/components/admin/ErrorLogPanel.jsx b/src/components/admin/ErrorLogPanel.jsx index c91f933..0503e12 100644 --- a/src/components/admin/ErrorLogPanel.jsx +++ b/src/components/admin/ErrorLogPanel.jsx @@ -2,6 +2,7 @@ import React, { useState, useEffect, useCallback, useRef } from 'react'; import { Panel } from '../UI/Panel'; import { Badge } from '../UI/Badge'; import { Select } from '../UI/Select'; +import { Skeleton } from '../UI/Loading'; import { supabase } from '../../supabaseClient'; @@ -274,7 +275,11 @@ export default function ErrorLogPanel() { )} {loading ? ( -
Загрузка…
+
+ {Array.from({ length: 5 }).map((_, i) => ( + + ))} +
) : (
{errors.length === 0 && ( diff --git a/src/components/admin/StopWordsPanel.jsx b/src/components/admin/StopWordsPanel.jsx index 9ffd338..96838f8 100644 --- a/src/components/admin/StopWordsPanel.jsx +++ b/src/components/admin/StopWordsPanel.jsx @@ -1,6 +1,7 @@ import React from "react"; import { Panel } from "../UI/Panel"; import { Button } from "../UI/Button"; +import { Skeleton } from "../UI/Loading"; import { supabase } from "../../supabaseClient"; export const StopWordsPanel = () => { @@ -162,7 +163,11 @@ export const StopWordsPanel = () => { )} {isLoading ? ( -

Загрузка...

+
+ {Array.from({ length: 6 }).map((_, i) => ( + + ))} +
) : !words.length ? (

Стоп-слов пока нет. Добавьте первое.

) : ( diff --git a/src/components/admin/UserManagementPanel.jsx b/src/components/admin/UserManagementPanel.jsx index 62dbc43..6199aaf 100644 --- a/src/components/admin/UserManagementPanel.jsx +++ b/src/components/admin/UserManagementPanel.jsx @@ -2,6 +2,7 @@ import React, { useState, useEffect, useCallback, useRef } from 'react'; import { Panel } from '../UI/Panel'; import { Badge } from '../UI/Badge'; import { Input } from '../UI/Input'; +import { Skeleton } from '../UI/Loading'; import { supabase, supabaseUrl } from '../../supabaseClient'; @@ -274,7 +275,18 @@ export default function UserManagementPanel() { const fmtDate = (iso) => iso ? new Date(iso).toLocaleString('ru-RU') : '—'; if (loading) { - return
Загрузка…
; + return ( + +
+ +
+ {Array.from({ length: 4 }).map((_, i) => ( + + ))} +
+
+
+ ); } return ( diff --git a/src/components/chat/ChatTimeline.jsx b/src/components/chat/ChatTimeline.jsx index 0c24f43..d297fc3 100644 --- a/src/components/chat/ChatTimeline.jsx +++ b/src/components/chat/ChatTimeline.jsx @@ -1,8 +1,22 @@ import React from "react"; import { formatDateTime } from "../../utils/formatters"; import { Badge } from "../UI/Badge"; +import { Skeleton } from "../UI/Loading"; + +export const ChatTimeline = ({ messages, isLoading = false }) => { + if (isLoading) { + return ( +
+ {Array.from({ length: 3 }).map((_, i) => ( +
+ + +
+ ))} +
+ ); + } -export const ChatTimeline = ({ messages }) => { if (!messages.length) { return (
diff --git a/src/components/client/DeliverySlotsPicker.jsx b/src/components/client/DeliverySlotsPicker.jsx index 4d4af7d..ecf39a0 100644 --- a/src/components/client/DeliverySlotsPicker.jsx +++ b/src/components/client/DeliverySlotsPicker.jsx @@ -1,6 +1,7 @@ import React from "react"; import { Button } from "../UI/Button"; import { Panel } from "../UI/Panel"; +import { Skeleton } from "../UI/Loading"; import { formatDeliveryDate, getDeliveryRelativeDayLabel } from "./deliveryDateFormatting"; const groupSlotsByDate = (slots) => { @@ -55,7 +56,20 @@ export const DeliverySlotsPicker = ({ onSelectSlot, selectedSlotId, referenceDate = new Date(), + isLoading = false, }) => { + if (isLoading) { + return ( + +
+ + + +
+
+ ); + } + if (!slots || !slots.length) { return ( diff --git a/src/components/driver/DriverDeliveryPlanner.jsx b/src/components/driver/DriverDeliveryPlanner.jsx index 0e12fc7..31f15af 100644 --- a/src/components/driver/DriverDeliveryPlanner.jsx +++ b/src/components/driver/DriverDeliveryPlanner.jsx @@ -13,6 +13,7 @@ import { Badge } from "../UI/Badge"; import { Input } from "../UI/Input"; import { Select } from "../UI/Select"; import { Panel } from "../UI/Panel"; +import { SkeletonPage } from "../UI/Loading"; const CHEVRON_DOWN = ( @@ -103,7 +104,7 @@ const countByStatus = (items) => { return result; }; -export const DriverDeliveryPlanner = ({ orderGroups = [], onOpenOrder, currentUser }) => { +export const DriverDeliveryPlanner = ({ orderGroups = [], onOpenOrder, currentUser, isLoading = false }) => { const [filters, setFilters] = React.useState({ selectedDate: "", deliveryStatus: "all", @@ -193,6 +194,10 @@ export const DriverDeliveryPlanner = ({ orderGroups = [], onOpenOrder, currentUs return summary; }, [groupedOrderGroups]); + if (isLoading) { + return ; + } + return (
diff --git a/src/components/logistics/LogisticsReadinessBoard.jsx b/src/components/logistics/LogisticsReadinessBoard.jsx index 8d2287e..081fe67 100644 --- a/src/components/logistics/LogisticsReadinessBoard.jsx +++ b/src/components/logistics/LogisticsReadinessBoard.jsx @@ -8,10 +8,11 @@ import { } from "../../services/orderGroupViews"; import { Badge } from "../UI/Badge"; import { Panel } from "../UI/Panel"; +import { SkeletonPage } from "../UI/Loading"; import { OrderFilters } from "../orders/OrderFilters"; import { formatDateTime } from "../../utils/formatters"; -export const LogisticsReadinessBoard = ({ orderGroups = [], onSelectSet, statusOptions = ORDER_GROUP_DISPLAY_STATUS_OPTIONS }) => { +export const LogisticsReadinessBoard = ({ orderGroups = [], onSelectSet, statusOptions = ORDER_GROUP_DISPLAY_STATUS_OPTIONS, isLoading = false }) => { const [filters, setFilters] = React.useState({ query: "", displayStatus: "all", city: "" }); const [collapsedSections, setCollapsedSections] = React.useState(new Set()); @@ -72,6 +73,10 @@ export const LogisticsReadinessBoard = ({ orderGroups = [], onSelectSet, statusO ); + if (isLoading) { + return ; + } + return (
diff --git a/src/components/notifications/NotificationSettings.jsx b/src/components/notifications/NotificationSettings.jsx index 2d8b367..c92ea84 100644 --- a/src/components/notifications/NotificationSettings.jsx +++ b/src/components/notifications/NotificationSettings.jsx @@ -3,6 +3,7 @@ import { useNotificationPreferences } from "../../hooks/useNotifications"; import { usePushNotifications } from "../../hooks/usePushNotifications"; import { Panel } from "../UI/Panel"; import { Bell, Settings } from "../UI/Icons"; +import { Skeleton } from "../UI/Loading"; const ALL_NOTIF_TYPES = [ { key: "order_status_change", label: "Изменение статуса", description: "Статус заказа или доставки изменился", roles: ["manager", "logistician", "driver", "admin", "mega_admin"] }, @@ -21,6 +22,47 @@ export function NotificationSettings({ userId, userRole, onBack }) { const visibleTypes = ALL_NOTIF_TYPES.filter((t) => t.roles.includes(role)); const loading = prefsLoading || pushLoading; + if (loading) { + return ( +
+
+ {onBack && ( + + )} +

Настройки уведомлений

+
+ +
+
+ + +
+ +
+
+ + +
+ {Array.from({ length: visibleTypes.length || 3 }).map((_, i) => ( +
+
+ + +
+ +
+ ))} +
+
+
+ ); + } + return (
diff --git a/src/components/orders/OrdersTable.jsx b/src/components/orders/OrdersTable.jsx index 3edecdd..eeb9e5b 100644 --- a/src/components/orders/OrdersTable.jsx +++ b/src/components/orders/OrdersTable.jsx @@ -1,6 +1,7 @@ import { formatDateTime } from "../../utils/formatters"; import { Badge } from "../UI/Badge"; import { Panel } from "../UI/Panel"; +import { SkeletonTable } from "../UI/Loading"; import { OrderFilters } from "./OrderFilters"; import { getOrderGroupDisplayStatusLabel, @@ -64,7 +65,12 @@ export const OrdersTable = ({ setFilters, statusOptions, cities = [], + isLoading = false, }) => { + if (isLoading) { + return ; + } + return (
diff --git a/src/pages/ClientDeliveryPage.jsx b/src/pages/ClientDeliveryPage.jsx index c6385b6..8816c41 100644 --- a/src/pages/ClientDeliveryPage.jsx +++ b/src/pages/ClientDeliveryPage.jsx @@ -7,6 +7,7 @@ import { OrderCompositionPanel } from "../components/client/OrderCompositionPane import { getInvitationReferenceLabel } from "../components/client/invitationReference"; import { DeliveryStateNotice } from "../components/client/DeliveryStateNotice"; import { Panel } from "../components/UI/Panel"; +import { Skeleton } from "../components/UI/Loading"; import { formatDeliveryDate } from "../components/client/deliveryDateFormatting"; import { confirmDeliveryChoice, @@ -316,7 +317,18 @@ export const ClientDeliveryPage = () => {

Доставка заказа

Загрузка страницы

-

Подтягиваем актуальные данные по заказу.

+
+ + + +
+
+ +
+ + + +
diff --git a/src/pages/DashboardPage.jsx b/src/pages/DashboardPage.jsx index 0ca584b..e542ab8 100644 --- a/src/pages/DashboardPage.jsx +++ b/src/pages/DashboardPage.jsx @@ -1,3 +1,9 @@ +/** + * @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"; @@ -10,6 +16,7 @@ import { StopWordsPanel } from "../components/admin/StopWordsPanel"; import { ActionLogPanel } from "../components/admin/ActionLogPanel"; import { SuggestionsPanel } from "../components/admin/SuggestionsPanel"; 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"; @@ -17,6 +24,7 @@ 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 }, @@ -27,6 +35,7 @@ const MEGA_ADMIN_NAV = [ { key: "suggestions", label: "Предложения", description: "Предложения сотрудников по улучшению.", badge: null }, ]; +// ── Role → Default Section Map ───────────────────────────────────────────── const ROLE_SECTION = { mega_admin: { key: "analytics", label: "Аналитика" }, admin: { key: "analytics", label: "Аналитика", description: "Статистика доставки." }, @@ -35,6 +44,7 @@ const ROLE_SECTION = { driver: { key: "deliveries", label: "Мои доставки", description: "Группы доставки по датам и статусам." }, }; +// ── Dashboard Component ──────────────────────────────────────────────────── export const DashboardPage = () => { const { user, signOut, isSessionLoading } = useAuth(); const location = useLocation(); @@ -55,6 +65,7 @@ export const DashboardPage = () => { } }; + // ── Notifications ───────────────────────────────────────────────────────── const { notifications, unreadCount, @@ -71,8 +82,10 @@ export const DashboardPage = () => { } }, [isSupported, isSubscribed, user?.id, subscribe]); + // ── PWA ──────────────────────────────────────────────────────────────────── const { isInstalled, isInstallAvailable, installApp: onInstallApp } = usePwaStatus(); + // ── Order Groups ───────────────────────────────────────────────────────── const { orderGroups, allOrderGroups, @@ -86,6 +99,7 @@ export const DashboardPage = () => { loadError, } = useOrderGroups(); + // ── Derived City List ───────────────────────────────────────────────────── const cities = React.useMemo(() => { const set = new Set(); for (const g of allOrderGroups) { @@ -94,6 +108,7 @@ export const DashboardPage = () => { return [...set].sort(); }, [allOrderGroups]); + // ── Navigation Builder ──────────────────────────────────────────────────── const openGroupPage = React.useCallback((groupId) => { navigate("/dashboard/group/" + groupId); }, [navigate]); @@ -120,6 +135,8 @@ export const DashboardPage = () => { ]; 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"]; @@ -137,6 +154,7 @@ const ALLOWED_DASHBOARD_ROLES = ["admin", "mega_admin", "manager", "logistician" return ; } + // ── Section Renderer ────────────────────────────────────────────────────── const renderActiveSection = () => { if (activeSection === "analytics") return
; @@ -146,10 +164,17 @@ const ALLOWED_DASHBOARD_ROLES = ["admin", "mega_admin", "manager", "logistician" if (activeSection === "action_log") return
; if (activeSection === "suggestions") return
; + if (isLoading) { + if (userRole === "driver") { + return ; + } + return ; + } + if (userRole === "driver") { return (
- +
); } @@ -163,17 +188,18 @@ const ALLOWED_DASHBOARD_ROLES = ["admin", "mega_admin", "manager", "logistician" } return (
- +
); } return (
- +
); }; + // ── Layout ──────────────────────────────────────────────────────────────── return ( - {isLoading && ( - - Загружаем данные... - - )} {loadError && ( Не удалось загрузить данные. Обратитесь к администратору. diff --git a/src/pages/GroupDetailPage.jsx b/src/pages/GroupDetailPage.jsx index 4beea6f..08461d8 100644 --- a/src/pages/GroupDetailPage.jsx +++ b/src/pages/GroupDetailPage.jsx @@ -1,8 +1,14 @@ +/** + * @file GroupDetailPage.jsx + * @description Detail view for a single order group. Reads groupId from URL + * params, loads drivers, and renders the OrderDetailPanel. + */ import React from "react"; import { Navigate, useNavigate, useParams, useLocation } from "react-router-dom"; import { OrderDetailPanel } from "../components/orders/OrderDetailPanel"; import { Button } from "../components/UI/Button"; import { Panel } from "../components/UI/Panel"; +import { SkeletonPanel } from "../components/UI/Loading"; import { useAuth } from "../context/AuthContext"; import { fetchDrivers } from "../services/supabase/userRepository"; import { useOrderGroups } from "../hooks/useOrderGroups"; @@ -10,6 +16,7 @@ import { useOrderGroups } from "../hooks/useOrderGroups"; const ALLOWED_ROLES = ["admin", "mega_admin", "manager", "logistician", "driver"]; export const GroupDetailPage = () => { + // ── Route Params & Auth ─────────────────────────────────────────────────── const { groupId } = useParams(); const navigate = useNavigate(); const location = useLocation(); @@ -25,8 +32,10 @@ export const GroupDetailPage = () => { isSavingDeliveryChoice, assignDriver, changeDeliveryStatus, + isLoading, } = useOrderGroups(); + // ── Drivers ──────────────────────────────────────────────────────────────── const [drivers, setDrivers] = React.useState([]); React.useEffect(() => { @@ -49,6 +58,11 @@ export const GroupDetailPage = () => { }, []); // ALL hooks must be called before any early return (Rules of Hooks) + const order = isLoading ? null : (allOrderGroups.find((g) => g.id === groupId) || + allOrderGroups.find((g) => g.id === selectedOrderGroupId) || + null); + + // Preserve the tab the user came from when going back const handleGoBack = React.useCallback(() => { if (window.history.length > 1) { navigate(-1); @@ -84,7 +98,9 @@ export const GroupDetailPage = () => {
- {order ? ( + {isLoading ? ( + + ) : order ? (