From 1d7d631fbd16dec4da05e820ef50e2124d2a482e Mon Sep 17 00:00:00 2001 From: mariomitte Date: Sun, 2 Aug 2026 23:06:33 +0200 Subject: [PATCH] Refactor dashboard sections: KPI cards, filters, fallbacks - Replace 4 static KPI cards with 5 interactive cards - 'Otvoreni nalozi' and 'Servisi danas' are now clickable links - Added context-aware subtotals (u kontekstu) when crane context is set - Replaced 'Upozorenja' (mileage-based, irrelevant for cranes) with 'Bez putnog naloga' - 'Dovrseni nalozi' now shows completion percentage - Added new card 'Moji zadaci danas' (tasks assigned to current user, today) - Added crane context filter toggle on the tasks table (Sve dizalice / Odabrani kontekst) - Service records page now shows last 10 records as fallback when no crane context is selected - Moved SyncLogPanel into a collapsible
element - Removed dead code: PAGE_MODE_TO_SECTION, activeSection/setActiveSection state, navbar:navigate event handler, and DashboardTopbar nav tab block (showSectionNav was always false; navigation is handled by Navbar.jsx) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/production-db-local-setup.md | 33 ++- .../components/dashboard/DashboardTopbar.jsx | 41 +--- .../dashboard/FleetDashboardShell.jsx | 226 ++++++++++++++---- frontend/src/stores/fleetDashboardStore.js | 13 +- 4 files changed, 211 insertions(+), 102 deletions(-) diff --git a/docs/production-db-local-setup.md b/docs/production-db-local-setup.md index 4f5bb3d..4d695d9 100644 --- a/docs/production-db-local-setup.md +++ b/docs/production-db-local-setup.md @@ -50,10 +50,29 @@ docker exec 004erpdb pg_dump \ -f /tmp/prod_dump.dump ``` +```bash +docker exec 004erpdb sh -c 'pg_dump + -U "$TVOJ_USER" + -d "$TVOJA_BAZA" + --no-owner + --no-acl + --exclude-table-data='log_*' + -Fc + -f /tmp/prod_dump.dump' +``` + --- ## Korak 2 — Kopiraj dump na lokalni PC +Iz docker servera na host server: + +```bash +docker cp 004erpdb:/tmp/prod_dump.dump /tmp/prod_dump.dump +``` + +```bash + Iz lokalnog PowerShell terminala: ```powershell @@ -103,13 +122,13 @@ docker exec 004erpdb dropdb -U TVOJ_USER TVOJA_BAZA --if-exists docker exec 004erpdb createdb -U TVOJ_USER TVOJA_BAZA # Restoriraj -docker exec 004erpdb pg_restore ` - -U TVOJ_USER ` - -d TVOJA_BAZA ` - --no-owner ` - --no-acl ` - -Fc ` - /tmp/prod_dump.dump +docker exec 004erpdb sh -c 'pg_restore + -U "$TVOJ_USER" + -d "$TVOJA_BAZA" + --no-owner + --no-acl + -Fc + -f /tmp/prod_dump.dump' ``` > ⚠️ Zamijeni `TVOJ_USER` i `TVOJA_BAZA` s vrijednostima iz lokalnog `.env` fajla: diff --git a/frontend/src/components/dashboard/DashboardTopbar.jsx b/frontend/src/components/dashboard/DashboardTopbar.jsx index 492e9fb..a9d6e37 100644 --- a/frontend/src/components/dashboard/DashboardTopbar.jsx +++ b/frontend/src/components/dashboard/DashboardTopbar.jsx @@ -1,32 +1,18 @@ -import { showToast } from '../../stores/toastStore'; import ServiceContextSelector from './ServiceContextSelector'; -const NAV_ITEMS = [ - { key: 'dashboard', label: 'Dashboard' }, - { key: 'work-orders', label: 'Putni nalozi' }, - { key: 'service-records', label: 'Servisni zapisi' }, - { key: 'cranes', label: 'Dizalice' }, - { key: 'clients', label: 'Klijenti' }, -]; - export default function DashboardTopbar({ - activeSection, - onSectionChange, onOpenWorkOrderModal, onOpenTaskModal, pendingSyncCount, onNewServiceRecord, - showSectionNav = true, }) { - const sectionTitle = NAV_ITEMS.find((item) => item.key === activeSection)?.label || 'Dashboard'; - return (
{/* ── Gornji red ───────────────────────────────────────── */}

Pregled sustava

-

{sectionTitle}

+

ERP

{pendingSyncCount > 0 && ( @@ -53,31 +39,6 @@ export default function DashboardTopbar({ {/* ── Selector bar: Kupac + Dizalica ───────────────────── */} - - {/* ── Navigacijski tabovi ──────────────────────────────── */} - {showSectionNav && ( - - )}
); } diff --git a/frontend/src/components/dashboard/FleetDashboardShell.jsx b/frontend/src/components/dashboard/FleetDashboardShell.jsx index 40b764c..0ffe395 100644 --- a/frontend/src/components/dashboard/FleetDashboardShell.jsx +++ b/frontend/src/components/dashboard/FleetDashboardShell.jsx @@ -136,17 +136,7 @@ function SortableCodeLabel({ title, direction, onToggle }) { ); } -const PAGE_MODE_TO_SECTION = { - dashboard: 'dashboard', - 'work-orders': 'work-orders', - 'service-records': 'service-records', - vehicles: 'cranes', - clients: 'clients', -}; - -export default function FleetDashboardShell({ initialSection = 'dashboard', pageMode = 'dashboard' }) { - const resolvedInitialSection = initialSection || PAGE_MODE_TO_SECTION[pageMode] || 'dashboard'; - const [activeSection, setActiveSection] = useState(resolvedInitialSection); +export default function FleetDashboardShell({ pageMode = 'dashboard' }) { const [isWorkOrderModalOpen, setIsWorkOrderModalOpen] = useState(false); const [isTaskModalOpen, setIsTaskModalOpen] = useState(false); const [selectedWorkOrder, setSelectedWorkOrder] = useState(null); @@ -185,6 +175,7 @@ export default function FleetDashboardShell({ initialSection = 'dashboard', page const [selectedCraneRowId, setSelectedCraneRowId] = useState(null); const [expandedServiceTaskId, setExpandedServiceTaskId] = useState(null); const [hasMounted, setHasMounted] = useState(false); + const [taskCraneFilter, setTaskCraneFilter] = useState('all'); useEffect(() => { setHasMounted(true); @@ -270,26 +261,9 @@ export default function FleetDashboardShell({ initialSection = 'dashboard', page }; }, [authReady]); - useEffect(() => { - function handleNavbarNavigate(event) { - const section = event?.detail; - if (typeof section === 'string' && section.length > 0) { - setActiveSection(section); - } - } - - window.addEventListener('navbar:navigate', handleNavbarNavigate); - return () => window.removeEventListener('navbar:navigate', handleNavbarNavigate); - }, []); - useEffect(() => { function handleNotificationOpenEntity(event) { const detail = event?.detail || {}; - const section = detail.section; - if (typeof section === 'string' && section.length > 0) { - setActiveSection(section); - } - if (detail.entityType === 'work_order' && detail.entityId) { if (detail.vehicleId) { const targetCrane = cranes.find((item) => String(item.id) === String(detail.craneId || detail.vehicleId)); @@ -352,6 +326,43 @@ export default function FleetDashboardShell({ initialSection = 'dashboard', page if (serial) return `SN ${serial}`; return hydratedContextCrane.registration_number || '-'; }, [hydratedContextCrane]); + // KPI: context-aware counts (null kad kontekst nije postavljen) + const contextStats = useMemo(() => { + if (!hydratedSelectedVehicleId) return null; + const ctxWoIds = new Set( + tasks + .filter((t) => String(t.vehicle || '') === String(hydratedSelectedVehicleId) && t.work_order) + .map((t) => String(t.work_order)) + ); + const ctxWorkOrders = workOrders.filter( + (o) => String(o.vehicle) === String(hydratedSelectedVehicleId) || ctxWoIds.has(String(o.id)) + ); + const today = new Date().toISOString().slice(0, 10); + return { + openWorkOrders: ctxWorkOrders.filter((o) => o.status !== 'closed').length, + servicesToday: tasks.filter( + (t) => String(t.vehicle || '') === String(hydratedSelectedVehicleId) + && t.scheduled_date === today + && (t.status === 'aktivan' || t.status === 'servis') + ).length, + }; + }, [tasks, workOrders, hydratedSelectedVehicleId]); + // KPI: moji zadaci planirani danas + const myTasksToday = useMemo(() => { + if (!user?.id || !hasMounted) return 0; + const today = new Date().toISOString().slice(0, 10); + return tasks.filter( + (t) => String(t.assigned_to || '') === String(user.id) + && t.scheduled_date === today + && isTaskActive(t) + ).length; + }, [tasks, user?.id, hasMounted]); + // Zadnji servisni zapisi za fallback prikaz (kad kontekst nije postavljen) + const recentServiceRecords = useMemo(() => ( + [...serviceRecords] + .sort((a, b) => String(b.service_date || '').localeCompare(String(a.service_date || ''))) + .slice(0, 10) + ), [serviceRecords]); const serviceRecordsForContext = useMemo(() => { if (!selectedVehicleId) return []; return [...serviceRecords] @@ -550,14 +561,23 @@ export default function FleetDashboardShell({ initialSection = 'dashboard', page return filteredByServicer; })(); - return [...filteredByStatus].sort((left, right) => ( + const filteredByCrane = (() => { + if (taskCraneFilter !== 'context' || !hydratedSelectedVehicleId) { + return filteredByStatus; + } + return filteredByStatus.filter( + (task) => String(task.vehicle || '') === String(hydratedSelectedVehicleId) + ); + })(); + + return [...filteredByCrane].sort((left, right) => ( compareWorkOrderCodesByDate( left?.work_order_label || '', right?.work_order_label || '', taskWorkOrderSortDirection ) )); - }, [tasks, taskFilter, canViewAllTasks, canToggleTaskScope, taskScope, user?.id, isSupervisorNotServiser, selectedServicerId, taskWorkOrderSortDirection]); + }, [tasks, taskFilter, canViewAllTasks, canToggleTaskScope, taskScope, user?.id, isSupervisorNotServiser, selectedServicerId, taskWorkOrderSortDirection, taskCraneFilter, hydratedSelectedVehicleId]); const selectedTaskServiceRecords = useMemo(() => { if (!selectedTask?.id) return []; return [...serviceRecords] @@ -734,9 +754,6 @@ export default function FleetDashboardShell({ initialSection = 'dashboard', page
{ try { const availableCranes = await ensureVehiclesCatalog(); @@ -769,30 +786,92 @@ export default function FleetDashboardShell({ initialSection = 'dashboard', page )} {isDashboardPage && ( -
- - + {hydratedSelectedVehicleId && contextStats !== null && ( +

{contextStats.servicesToday} u kontekstu

+ )} +
+ + {/* Aktivni zadaci bez putnog naloga */}
-

Upozorenja

-

{stats.warnings}

+

Bez putnog naloga

+

0 ? 'text-amber-600' : 'text-text-main'}`}> + {stats.tasksWithoutWorkOrder} +

+

aktivnih zadataka

+ + {/* Dovršeni nalozi s postotkom */}

Dovršeni nalozi

{stats.closedWorkOrders}

+ {stats.totalWorkOrders > 0 && ( +

+ {Math.round((stats.closedWorkOrders / stats.totalWorkOrders) * 100)}% od ukupno {stats.totalWorkOrders} +

+ )} +
+ + {/* Moji zadaci danas */} +
+

Moji zadaci danas

+

0 ? 'text-indigo-600' : 'text-text-main'}`}> + {hasMounted ? myTasksToday : '—'} +

+

planirani na mene

)} {isDashboardPage && (
-
+

Radni zadaci

-
+
+ {/* Crane context filter */} + {hasMounted && hydratedSelectedVehicleId && ( + <> + | + + + | + + )} {hasMounted && isSupervisorNotServiser && (
)} {isDashboardPage && ( -
- -
+
+ + Sync log + + +
+ +
+
)} {showWorkOrders && ( @@ -1131,9 +1216,50 @@ export default function FleetDashboardShell({ initialSection = 'dashboard', page {!hydratedSelectedVehicleId && ( -

- Odaberite dizalicu za prikaz servisnih zapisa. -

+
+

+ Odaberite dizalicu u servisnom kontekstu za prikaz zapisa po radnim zadacima. + Ispod su prikazani zadnji servisni zapisi. +

+ {recentServiceRecords.length === 0 ? ( +

Nema servisnih zapisa.

+ ) : ( + r.id} + rowClassName="hover:bg-canvas-deep" + renderRow={(record) => ( + <> + {formatDate(record.service_date)} + {formatServiceRecordCraneLabel(record, cranes)} + + {record.description || '-'} + + {formatCost(record.cost)} + + + + + )} + /> + )} +
)} {hydratedSelectedVehicleId && loading && (

diff --git a/frontend/src/stores/fleetDashboardStore.js b/frontend/src/stores/fleetDashboardStore.js index 12f729e..32621e1 100644 --- a/frontend/src/stores/fleetDashboardStore.js +++ b/frontend/src/stores/fleetDashboardStore.js @@ -464,16 +464,19 @@ export const $dashboardStats = computed( const servicesToday = tasks.filter( (t) => t.scheduled_date === today && (t.status === 'aktivan' || t.status === 'servis') ).length; - const warnings = serviceRecords.filter((record) => { - if (record.next_service_due_at == null || record.mileage == null) return false; - return Number(record.next_service_due_at) - Number(record.mileage) <= 1000; - }).length; + const totalWorkOrders = workOrders.length; + // Aktivni taskovi bez dodijeljenog putnog naloga + const tasksWithoutWorkOrder = tasks.filter( + (t) => (t.status === 'aktivan' || t.status === 'servis' || t.status === 'spreman_za_zavrsetak') + && !t.work_order + ).length; return { openWorkOrders, closedWorkOrders, + totalWorkOrders, servicesToday, - warnings, + tasksWithoutWorkOrder, }; } );