import { useStore } from '@nanostores/preact'; import { useEffect, useMemo, useState } from 'preact/hooks'; import { animated, useSpring, useTransition } from '@react-spring/web'; import DashboardTopbar from './DashboardTopbar'; import WorkOrderModal from './WorkOrderModal'; import TaskCreateModal from './TaskCreateModal'; import TaskServiceRecordsModal from './TaskServiceRecordsModal'; import WorkOrderDetailModal from './WorkOrderDetailModal'; import TaskCalendarWidget from './TaskCalendarWidget'; import SyncLogPanel from './SyncLogPanel'; import ServiceRecordForm from '../fleet/ServiceRecordForm'; import ServicePhotoUpload from '../fleet/ServicePhotoUpload'; import WorkOrderPhotoUpload from '../fleet/WorkOrderPhotoUpload'; import ServiceRecordDetailModal from '../fleet/ServiceRecordDetailModal'; import ServiceRecordCreateButton from '../fleet/ServiceRecordCreateButton'; import ClientsSection from '../crm/ClientsSection'; import { $clients } from '../../stores/clientStore'; import { $selectedClientId, $selectedVehicleId, hydrateServiceContext, setServiceContext } from '../../stores/serviceContextStore'; import { $dashboardError, $dashboardLoading, $offlineQueueCount, $dashboardStats, $cranes, $workOrdersWithVehicle, $serviceRecords, createWorkOrder, fetchFleetDashboardData, fetchTeamMembers, ensureVehiclesCatalog, triggerOfflineSync, updateWorkOrder, } from '../../stores/fleetDashboardStore'; import { showToast } from '../../stores/toastStore'; import { $accessToken, $authReady, $user, hydrateAuthFromStorage, validateToken } from '../../stores/authStore'; import { cacheCraneSerial, cacheCraneSerialsFromVehicles } from '../../stores/craneSerialStore'; import { formatWorkOrderDisplayCode } from '../../lib/displayIds'; import { $tasks, fetchTasks, getStatusLabel, isTaskActive, updateTask } from '../../stores/taskStore'; import AnimatedDataTable from '../ui/AnimatedDataTable'; import AnimatedPage from '../ui/AnimatedPage'; function AnimatedStatusBadge({ status }) { const normalizedStatus = status === 'closed' ? 'closed' : 'open'; const springStyle = useSpring({ backgroundColor: normalizedStatus === 'closed' ? 'rgba(16, 185, 129, 0.16)' : 'rgba(99, 102, 241, 0.16)', color: normalizedStatus === 'closed' ? 'rgb(4, 120, 87)' : 'rgb(67, 56, 202)', config: { tension: 240, friction: 24 }, }); return ( {normalizedStatus} ); } function formatDateTime(value) { if (!value) return '-'; const parsed = new Date(value); if (Number.isNaN(parsed.getTime())) return '-'; return parsed.toLocaleString('hr-HR'); } function formatDate(value) { if (!value) return '-'; const parsed = new Date(value); if (Number.isNaN(parsed.getTime())) return value; return parsed.toLocaleDateString('hr-HR'); } function formatTaskCraneLabel(task, cranes) { const crane = cranes.find((item) => String(item.id) === String(task?.vehicle || '')); const serial = String(crane?.crane_serial_number || '').trim(); const model = [crane?.make, crane?.model].filter(Boolean).join(' ').trim(); const registration = String(task?.vehicle_registration || crane?.registration_number || '').trim(); const primary = serial ? `SN ${serial}` : (registration || '-'); return model ? `${primary} / ${model}` : primary; } function formatServiceRecordCraneLabel(record, cranes) { const crane = cranes.find((item) => String(item.id) === String(record?.vehicle || '')); const serial = String(record?.crane_serial_number || crane?.crane_serial_number || '').trim(); const model = [crane?.make, crane?.model].filter(Boolean).join(' ').trim(); const registration = String(record?.vehicle_registration || crane?.registration_number || '').trim(); const primary = serial ? `SN ${serial}` : (registration || '-'); return model ? `${primary} / ${model}` : primary; } function formatCost(value) { const numeric = Number(value); if (Number.isNaN(numeric)) return '-'; return `${numeric.toFixed(2)} EUR`; } 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); const [isWorkOrderModalOpen, setIsWorkOrderModalOpen] = useState(false); const [isTaskModalOpen, setIsTaskModalOpen] = useState(false); const [selectedWorkOrder, setSelectedWorkOrder] = useState(null); const [selectedTask, setSelectedTask] = useState(null); const [serviceRecordBackTask, setServiceRecordBackTask] = useState(null); const [serviceRecordFormBackTask, setServiceRecordFormBackTask] = useState(null); const [serviceRecordOpen, setServiceRecordOpen] = useState(false); const [serviceRecordCrane, setServiceRecordCrane] = useState(null); const [selectedServiceRecord, setSelectedServiceRecord] = useState(null); // photoUpload: { open: bool, serviceRecordId: string|null } const [photoUpload, setPhotoUpload] = useState({ open: false, serviceRecordId: null }); const [workOrderPhotoUpload, setWorkOrderPhotoUpload] = useState({ open: false, workOrderId: null }); const loading = useStore($dashboardLoading); const error = useStore($dashboardError); const stats = useStore($dashboardStats); const workOrders = useStore($workOrdersWithVehicle); const serviceRecords = useStore($serviceRecords); const tasks = useStore($tasks); const cranes = useStore($cranes); const clients = useStore($clients); const offlineQueueCount = useStore($offlineQueueCount); const user = useStore($user); const token = useStore($accessToken); const authReady = useStore($authReady); const selectedClientId = useStore($selectedClientId); const selectedVehicleId = useStore($selectedVehicleId); const [currentPage, setCurrentPage] = useState(1); const [taskFilter, setTaskFilter] = useState('active'); const [taskScope, setTaskScope] = useState('all'); const [supervisorServicers, setSupervisorServicers] = useState([]); const [selectedServicerId, setSelectedServicerId] = useState('all'); const [workOrderScope, setWorkOrderScope] = useState('context'); const [selectedCraneRowId, setSelectedCraneRowId] = useState(null); const [expandedServiceTaskId, setExpandedServiceTaskId] = useState(null); const [hasMounted, setHasMounted] = useState(false); useEffect(() => { setHasMounted(true); hydrateAuthFromStorage(); hydrateServiceContext(); }, []); useEffect(() => { const nextSection = String(resolvedInitialSection || 'dashboard'); setActiveSection(nextSection); }, [resolvedInitialSection]); useEffect(() => { if (typeof window === 'undefined') return; const syncContextFromStorage = () => { const persistedClientId = localStorage.getItem('topbar_selected_client'); const persistedVehicleId = localStorage.getItem('topbar_selected_vehicle'); if (!persistedVehicleId) { return; } if (String(selectedVehicleId || '') === String(persistedVehicleId)) { return; } setServiceContext(persistedClientId, persistedVehicleId); }; syncContextFromStorage(); window.addEventListener('storage', syncContextFromStorage); window.addEventListener('focus', syncContextFromStorage); window.addEventListener('service-context:changed', syncContextFromStorage); window.addEventListener('astro:page-load', syncContextFromStorage); return () => { window.removeEventListener('storage', syncContextFromStorage); window.removeEventListener('focus', syncContextFromStorage); window.removeEventListener('service-context:changed', syncContextFromStorage); window.removeEventListener('astro:page-load', syncContextFromStorage); }; }, [selectedVehicleId]); useEffect(() => { if (authReady && !token) { window.location.href = '/login'; } }, [authReady, token]); function openWorkOrderDetails(workOrder) { if (!workOrder) { return; } const targetCraneId = workOrder.crane || workOrder.vehicle; const craneFromCatalog = cranes.find((item) => String(item.id) === String(targetCraneId)) || null; const serialFromContext = craneFromCatalog?.crane_serial_number || workOrder.craneInfo?.crane_serial_number; cacheCraneSerial(targetCraneId, serialFromContext); setSelectedWorkOrder({ ...workOrder, craneInfo: craneFromCatalog || workOrder.craneInfo || null, }); } useEffect(() => { cacheCraneSerialsFromVehicles(cranes); }, [cranes]); useEffect(() => { if (!authReady) return; let active = true; (async () => { await validateToken(); if (!active) return; await Promise.all([ fetchFleetDashboardData(), fetchTasks(), ]); })(); return () => { active = false; }; }, [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)); if (targetCrane) { setServiceContext(targetCrane.client || null, targetCrane.id); } } const targetWorkOrder = workOrders.find((item) => String(item.id) === String(detail.entityId)); if (targetWorkOrder) { openWorkOrderDetails(targetWorkOrder); } else { showToast('Traženi putni nalog nije pronađen u trenutnom prikazu.', 'warning'); } return; } if (detail.entityType === 'service_record' && detail.entityId) { if (detail.vehicleId) { const targetCrane = cranes.find((item) => String(item.id) === String(detail.craneId || detail.vehicleId)); if (targetCrane) { setServiceContext(targetCrane.client || null, targetCrane.id); } } const targetServiceRecord = serviceRecords.find((item) => String(item.id) === String(detail.entityId)); if (targetServiceRecord) { setServiceRecordBackTask(null); setSelectedServiceRecord(targetServiceRecord); } else { showToast('Traženi servisni zapis nije pronađen u trenutnom prikazu.', 'warning'); } return; } if (detail.entityType === 'service_context' && detail.vehicleId) { const targetCrane = cranes.find((item) => String(item.id) === String(detail.vehicleId)); if (targetCrane) { setServiceContext(targetCrane.client || null, targetCrane.id); setSelectedCraneRowId(targetCrane.id); } } } window.addEventListener('notification:open-entity', handleNotificationOpenEntity); return () => window.removeEventListener('notification:open-entity', handleNotificationOpenEntity); }, [serviceRecords, cranes, workOrders]); const contextCrane = useMemo(() => ( cranes.find((item) => String(item.id) === String(selectedVehicleId)) || null ), [cranes, selectedVehicleId]); const hydratedSelectedVehicleId = hasMounted ? selectedVehicleId : null; const hydratedContextCrane = hasMounted ? contextCrane : null; const selectedContextClientName = useMemo(() => ( clients.find((client) => String(client.id) === String(selectedClientId))?.name || contextCrane?.client_name || null ), [clients, selectedClientId, contextCrane]); const contextCraneLabel = useMemo(() => { if (!hydratedContextCrane) return null; const serial = String(hydratedContextCrane.crane_serial_number || '').trim(); if (serial) return `SN ${serial}`; return hydratedContextCrane.registration_number || '-'; }, [hydratedContextCrane]); const serviceRecordsForContext = useMemo(() => { if (!selectedVehicleId) return []; return [...serviceRecords] .filter((record) => String(record.vehicle) === String(selectedVehicleId)) .sort((a, b) => String(b.service_date || '').localeCompare(String(a.service_date || ''))); }, [serviceRecords, selectedVehicleId]); const serviceTaskGroups = useMemo(() => { if (!selectedVehicleId) return []; const contextTasks = [...tasks] .filter((task) => String(task.vehicle || '') === String(selectedVehicleId)) .sort((a, b) => String(b.created_at || '').localeCompare(String(a.created_at || ''))); const groups = contextTasks.map((task) => ({ id: String(task.id), title: task.title || 'Radni zadatak', status: task.status || '-', task, records: serviceRecordsForContext.filter((record) => String(record.task || '') === String(task.id)), })); const unassignedRecords = serviceRecordsForContext.filter((record) => !record.task); if (unassignedRecords.length > 0) { groups.push({ id: 'unassigned-task', title: 'Bez radnog zadatka', status: '-', task: null, records: unassignedRecords, }); } return groups; }, [selectedVehicleId, tasks, serviceRecordsForContext]); const serviceTaskTransitions = useTransition(serviceTaskGroups, { keys: (group) => group.id, from: { opacity: 0, transform: 'translate3d(0,8px,0)' }, enter: { opacity: 1, transform: 'translate3d(0,0,0)' }, leave: { opacity: 0, transform: 'translate3d(0,-8px,0)' }, trail: 30, config: { tension: 230, friction: 26 }, }); const scopedWorkOrders = useMemo(() => { const sorted = [...workOrders].sort((a, b) => String(b.date || '').localeCompare(String(a.date || ''))); if (workOrderScope === 'all') { return sorted; } if (!hydratedSelectedVehicleId) { return []; } return sorted.filter((order) => String(order.vehicle) === String(hydratedSelectedVehicleId)); }, [workOrders, workOrderScope, hydratedSelectedVehicleId]); const pageSize = 10; const totalPages = Math.max(1, Math.ceil(scopedWorkOrders.length / pageSize)); const latestWorkOrders = useMemo( () => scopedWorkOrders.slice((currentPage - 1) * pageSize, currentPage * pageSize), [scopedWorkOrders, currentPage] ); const serviceRecordWorkOrders = useMemo(() => { const craneId = serviceRecordCrane?.id || contextCrane?.id; if (!craneId) return []; return workOrders .filter((order) => String(order.vehicle) === String(craneId)) .sort((a, b) => String(b.date || '').localeCompare(String(a.date || ''))); }, [workOrders, serviceRecordCrane, contextCrane]); const serviceRecordTasks = useMemo(() => { const craneId = serviceRecordCrane?.id || contextCrane?.id; if (!craneId) return []; return tasks .filter((task) => isTaskActive(task)) .filter((task) => task.vehicle && String(task.vehicle) === String(craneId)) .sort((a, b) => String(b.created_at || '').localeCompare(String(a.created_at || ''))); }, [tasks, serviceRecordCrane, contextCrane]); const isTeamMember = Boolean(user?.is_team_member); const isServiser = Boolean(user?.is_serviser); const isCustomer = Boolean(user?.is_kupac); const isSupervisorNotServiser = isTeamMember && !isServiser; const canViewAllTasks = isTeamMember && (isServiser || !isCustomer); const canToggleTaskScope = isTeamMember && isServiser; const isTaskAssignedToCurrentUser = (task) => ( Boolean(user?.id) && String(task?.assigned_to || '') === String(user.id) ); useEffect(() => { setTaskScope(canViewAllTasks ? 'all' : 'mine'); }, [canViewAllTasks]); useEffect(() => { if (!authReady || !isSupervisorNotServiser) { setSupervisorServicers([]); setSelectedServicerId('all'); return; } let isActive = true; (async () => { try { const members = await fetchTeamMembers(); if (!isActive) return; const servicers = members.filter((member) => Boolean(member?.is_serviser)); setSupervisorServicers(servicers); } catch { if (isActive) { setSupervisorServicers([]); } } })(); return () => { isActive = false; }; }, [authReady, isSupervisorNotServiser]); useEffect(() => { if (selectedServicerId === 'all') { return; } const servicerExists = supervisorServicers.some( (member) => String(member?.id || '') === String(selectedServicerId) ); if (!servicerExists) { setSelectedServicerId('all'); } }, [selectedServicerId, supervisorServicers]); const dashboardTasks = useMemo(() => { const sorted = [...tasks].sort((a, b) => String(b.created_at || '').localeCompare(String(a.created_at || ''))); const scoped = (() => { if (!canViewAllTasks) { return sorted.filter((task) => isTaskAssignedToCurrentUser(task)); } if (canToggleTaskScope && taskScope === 'mine') { return sorted.filter((task) => isTaskAssignedToCurrentUser(task)); } return sorted; })(); const filteredByServicer = (() => { if (!isSupervisorNotServiser || selectedServicerId === 'all') { return scoped; } return scoped.filter( (task) => String(task?.assigned_to || '') === String(selectedServicerId) ); })(); if (taskFilter === 'active') { return filteredByServicer.filter((task) => isTaskActive(task)); } if (taskFilter === 'inactive') { return filteredByServicer.filter((task) => !isTaskActive(task)); } return filteredByServicer; }, [tasks, taskFilter, canViewAllTasks, canToggleTaskScope, taskScope, user?.id, isSupervisorNotServiser, selectedServicerId]); const selectedTaskServiceRecords = useMemo(() => { if (!selectedTask?.id) return []; return [...serviceRecords] .filter((record) => String(record.task || '') === String(selectedTask.id)) .sort((a, b) => String(b.service_date || '').localeCompare(String(a.service_date || ''))); }, [serviceRecords, selectedTask?.id]); const selectedTaskWorkOrders = useMemo(() => { if (!selectedTask) return []; const byVehicle = selectedTask.vehicle ? workOrders.filter((order) => String(order.vehicle) === String(selectedTask.vehicle)) : workOrders; return [...byVehicle].sort((a, b) => String(b.date || '').localeCompare(String(a.date || ''))); }, [selectedTask, workOrders]); useEffect(() => { if (currentPage > totalPages) { setCurrentPage(totalPages); } }, [currentPage, totalPages]); useEffect(() => { setCurrentPage(1); }, [workOrderScope, hydratedSelectedVehicleId]); useEffect(() => { if (!expandedServiceTaskId) return; const exists = serviceTaskGroups.some((group) => String(group.id) === String(expandedServiceTaskId)); if (!exists) { setExpandedServiceTaskId(null); } }, [expandedServiceTaskId, serviceTaskGroups]); useEffect(() => { if (!selectedCraneRowId) return; const exists = cranes.some((item) => String(item.id) === String(selectedCraneRowId)); if (!exists) { setSelectedCraneRowId(null); } }, [cranes, selectedCraneRowId]); async function handleCreateWorkOrder(formData) { try { await createWorkOrder(formData); showToast('Putni nalog je spremljen.', 'success'); await fetchFleetDashboardData({ silent: true }); } catch (error) { showToast(error?.message || 'Kreiranje naloga nije uspjelo.', 'error'); } } async function handleUpdateWorkOrder(workOrder, changes) { try { await updateWorkOrder(workOrder.id, changes); showToast('Nalog je uspješno ažuriran.', 'success'); await fetchFleetDashboardData({ silent: true }); await fetchTasks(); } catch (error) { showToast(error?.message || 'Ažuriranje naloga nije uspjelo.', 'error'); } } function openServiceRecordForCrane(crane) { setServiceRecordFormBackTask(null); setServiceRecordCrane(crane ?? null); setServiceRecordOpen(true); } async function handleSetServiceContext() { if (!selectedCraneRowId) { showToast('Prvo označite redak dizalice.', 'warning'); return; } const selectedCrane = cranes.find((item) => String(item.id) === String(selectedCraneRowId)); handleSetServiceContextForCrane(selectedCrane); } function handleSetServiceContextForCraneId(craneId) { const selectedCrane = cranes.find((item) => String(item.id) === String(craneId)); handleSetServiceContextForCrane(selectedCrane); } function handleSetServiceContextForCrane(selectedCrane) { if (!selectedCrane) { showToast('Odabrana dizalica više nije dostupna.', 'warning'); return; } setSelectedCraneRowId(selectedCrane.id); setServiceContext(selectedCrane.client || null, selectedCrane.id); showToast('Servisni kontekst je postavljen.', 'success'); } async function openTaskModalFromContext() { if (!selectedClientId || !selectedVehicleId) { showToast('Odaberite kupca i dizalicu u Servisnom kontekstu prije kreiranja radnog zadatka.', 'warning'); return; } try { const availableCranes = await ensureVehiclesCatalog(); const selectedFromContext = availableCranes.find( (item) => String(item.id) === String(selectedVehicleId) ); if (!selectedFromContext) { showToast('Odabrana dizalica nije dostupna u trenutnom kontekstu.', 'warning'); return; } } catch { showToast('Ne mogu dohvatiti dizalice. Provjerite mrežu ili prijavu.', 'error'); return; } setIsTaskModalOpen(true); } const workOrderColumns = [ { key: 'code', label: 'Putni nalog', className: 'px-4 py-3' }, { key: 'vehicle', label: 'Dizalica', className: 'px-4 py-3' }, { key: 'serialNumber', label: 'SN', className: 'px-4 py-3' }, { key: 'createdAt', label: 'Kreirano', className: 'px-4 py-3' }, { key: 'status', label: 'Status', className: 'px-4 py-3' }, { key: 'creator', label: 'Creator', className: 'px-4 py-3' }, { key: 'actions', label: 'Akcije', className: 'px-4 py-3' }, ]; const vehicleColumns = [ { key: 'registration', label: 'Registracija', className: 'px-4 py-3' }, { key: 'serialNumber', label: 'SN', className: 'px-4 py-3' }, { key: 'model', label: 'Marka / Model', className: 'px-4 py-3' }, { key: 'client', label: 'Kupac', className: 'px-4 py-3' }, { key: 'mileage', label: 'Kilometraža', className: 'px-4 py-3' }, { key: 'status', label: 'Status', className: 'px-4 py-3' }, ]; const taskColumns = [ { key: 'title', label: 'Radni zadatak', className: 'px-4 py-3' }, { key: 'status', label: 'Status', className: 'px-4 py-3' }, { key: 'scheduledDate', label: 'Planirano', className: 'px-4 py-3' }, { key: 'createdAt', label: 'Vrijeme izrade', className: 'px-4 py-3' }, { key: 'vehicle', label: 'Dizalica', className: 'px-4 py-3' }, { key: 'workOrder', label: 'Putni nalog', className: 'px-4 py-3' }, { key: 'assigned', label: 'Dodijeljeno', className: 'px-4 py-3' }, ]; const isDashboardPage = pageMode === 'dashboard'; const showWorkOrders = pageMode === 'work-orders'; const showServiceRecords = pageMode === 'service-records'; const showClients = pageMode === 'clients'; const showVehicles = pageMode === 'vehicles'; return ( {hasMounted && ( setSelectedTask(task)} /> )}
{ try { const availableCranes = await ensureVehiclesCatalog(); const selectedFromContext = availableCranes.find( (item) => String(item.id) === String(selectedVehicleId) ); if (!selectedFromContext) { showToast('Odaberite dizalicu u Servisnom kontekstu prije kreiranja naloga.', 'warning'); return; } } catch { showToast('Ne mogu dohvatiti dizalice. Provjerite mrežu ili prijavu.', 'error'); return; } setIsWorkOrderModalOpen(true); }} onOpenTaskModal={openTaskModalFromContext} onNewServiceRecord={({ craneId }) => { const crane = cranes.find((item) => String(item.id) === String(craneId)); openServiceRecordForCrane(crane ?? null); }} pendingSyncCount={offlineQueueCount} />
{error && (
{error}
)} {isDashboardPage && (

Otvoreni nalozi

{stats.openWorkOrders}

Servisi danas

{stats.servicesToday}

Upozorenja

{stats.warnings}

Dovršeni nalozi

{stats.closedWorkOrders}

)} {isDashboardPage && (

Radni zadaci

{hasMounted && isSupervisorNotServiser && ( )} {canToggleTaskScope && ( <> )}
task.id} loading={loading} loadingMessage="Učitavanje radnih zadataka..." emptyMessage="Nema radnih zadataka za odabrani filter." rowClassName={(task) => { const status = String(task?.status || '').toLowerCase(); if (status === 'hitno') { return 'bg-amber-50 hover:bg-amber-100'; } return 'hover:bg-canvas-deep'; }} renderRow={(task) => ( <> {getStatusLabel(task.status || '-')} {task.scheduled_date ? ( {formatDate(task.scheduled_date)} ) : ( - )} {formatDate(task.created_at)} {formatTaskCraneLabel(task, cranes)} {task.work_order_label || '-'} {task.assigned_to_name || '-'} )} />
)} {isDashboardPage && (
)} {showWorkOrders && (

Putni nalozi dizalica

{workOrderScope === 'context' ? (selectedContextClientName && contextCraneLabel ? `Kontekst: ${selectedContextClientName} / ${contextCraneLabel}` : 'Odaberite kupca i dizalicu u "Servisni kontekst" traci.') : 'Prikaz svih putnih naloga.'}

workOrder.id} loading={loading} loadingMessage="Učitavanje..." emptyMessage={ workOrderScope === 'context' ? (!hydratedSelectedVehicleId ? 'Odaberite dizalicu za prikaz putnih naloga iz servisnog konteksta.' : 'Nema dostupnih putnih naloga za odabrani servisni kontekst.') : 'Nema dostupnih putnih naloga.' } renderRow={(workOrder) => ( <> openWorkOrderDetails(workOrder)} > {formatWorkOrderDisplayCode(workOrder)} {[workOrder.craneInfo?.make, workOrder.craneInfo?.model].filter(Boolean).join(' ') || workOrder.craneLabel || workOrder.craneInfo?.registration_number || `#${workOrder.crane || workOrder.vehicle}`} {workOrder.craneInfo?.crane_serial_number || '-'} {formatDate(workOrder.date)} {workOrder.creator || '-'} )} />
Prikaz {scopedWorkOrders.length ? (currentPage - 1) * pageSize + 1 : 0} - {scopedWorkOrders.length ? Math.min(currentPage * pageSize, scopedWorkOrders.length) : 0} od {scopedWorkOrders.length}
{currentPage}/{totalPages}
)} {showServiceRecords && (

Servisni zapisi

{selectedContextClientName && contextCraneLabel ? `Kontekst: ${selectedContextClientName} / ${contextCraneLabel}` : 'Odaberite kupca i dizalicu u "Servisni kontekst" traci.'}

{hydratedSelectedVehicleId && hydratedContextCrane && ( openServiceRecordForCrane(hydratedContextCrane)} /> )}
{!hydratedSelectedVehicleId && (

Odaberite dizalicu za prikaz servisnih zapisa.

)} {hydratedSelectedVehicleId && loading && (

Učitavanje...

)} {hydratedSelectedVehicleId && !loading && serviceTaskGroups.length === 0 && (

Nema grupiranih radnih zadataka za odabranu dizalicu.

)} {hydratedSelectedVehicleId && !loading && serviceTaskGroups.length > 0 && (
{serviceTaskTransitions((style, group) => { const isOpen = String(expandedServiceTaskId || '') === String(group.id); return ( {isOpen && (
{group.records.length === 0 ? (

Nema servisnih zapisa za odabrani radni zadatak.

) : ( record.id} rowClassName="hover:bg-canvas-deep" renderRow={(record) => ( <> {/*
                                                                    {JSON.stringify(group, null, 2)}
                                                                    
*/} {formatDate(record.service_date)} {formatServiceRecordCraneLabel(record, cranes)} {record.description || '-'} {formatCost(record.cost)} {record.files_count ?? (record.photos_count ?? 0)} )} /> )}
)}
); })}
)}
)} {showClients && ( <> {/* ── Tablica klijenata i njihovih dizalica ───────── */}
)} {showVehicles && (

Vozila i dizalice

vehicle.id} rowClassName="hover:bg-canvas-deep" isRowSelected={(vehicle) => String(vehicle.id) === String(selectedCraneRowId)} loading={loading} loadingMessage="Učitavanje vozila..." emptyMessage="Nema dostupnih vozila." renderRow={(vehicle) => ( <> setSelectedCraneRowId(vehicle.id)} > {vehicle.registration_number || '-'} setSelectedCraneRowId(vehicle.id)} > {vehicle.crane_serial_number || '-'} setSelectedCraneRowId(vehicle.id)} > {vehicle.make || '-'} {vehicle.model || ''} setSelectedCraneRowId(vehicle.id)} > {vehicle.client_name || '-'} setSelectedCraneRowId(vehicle.id)} > {vehicle.current_mileage ?? '-'} setSelectedCraneRowId(vehicle.id)} > {vehicle.is_active ? 'Aktivno' : 'Neaktivno'} )} />
)}
setIsWorkOrderModalOpen(false)} onSubmit={handleCreateWorkOrder} /> setIsTaskModalOpen(false)} onSuccess={() => fetchFleetDashboardData({ silent: true })} /> setSelectedWorkOrder(null)} onSubmit={handleUpdateWorkOrder} /> {/* Modali su na root razini div-a — van
i
— kako bi fixed pozicioniranje i z-index radili ispravno */} { setServiceRecordOpen(false); setServiceRecordCrane(null); setServiceRecordFormBackTask(null); }} onSuccess={() => fetchFleetDashboardData({ silent: true })} onBack={serviceRecordFormBackTask ? () => { setServiceRecordOpen(false); setServiceRecordCrane(null); setSelectedTask(serviceRecordFormBackTask); setServiceRecordFormBackTask(null); } : null} backLabel="Natrag na radni zadatak" /> setPhotoUpload({ open: false, serviceRecordId: null })} onSuccess={() => fetchFleetDashboardData({ silent: true })} /> setWorkOrderPhotoUpload({ open: false, workOrderId: null })} onSuccess={() => fetchFleetDashboardData({ silent: true })} /> { setSelectedServiceRecord(null); setServiceRecordBackTask(null); }} onUpdated={(updatedRecord) => { setSelectedServiceRecord(updatedRecord); fetchFleetDashboardData({ silent: true }); }} onBack={serviceRecordBackTask ? () => { setSelectedServiceRecord(null); setSelectedTask(serviceRecordBackTask); } : null} backLabel="Natrag na popis servisnih zapisa" /> setSelectedTask(null)} onOpenServiceRecord={(record) => { setServiceRecordBackTask(selectedTask); setSelectedTask(null); setSelectedServiceRecord(record); }} onAssignWorkOrder={async (task, workOrderId) => { const updatedTask = await updateTask(task.id, { work_order: workOrderId }); setSelectedTask(updatedTask); return updatedTask; }} onUpdateTask={async (task, changes) => { const updatedTask = await updateTask(task.id, changes); setSelectedTask(updatedTask); await fetchFleetDashboardData({ silent: true }); return updatedTask; }} onCreateServiceRecord={(task) => { const taskCrane = cranes.find((item) => String(item.id) === String(task?.vehicle)); setSelectedTask(null); setServiceRecordFormBackTask(task); setServiceRecordCrane(taskCrane ?? null); setServiceRecordOpen(true); }} /> ); }