From cbcbf0b3aff970a48c949cceccb7b1b757193c3e Mon Sep 17 00:00:00 2001 From: mariomitte Date: Sun, 19 Jul 2026 19:16:19 +0200 Subject: [PATCH] fix: sortiranje putnih naloga i upload modal u detaljima MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dodaje sortiranje stupca Putni nalog (↑/↓) u tablicama gdje je prikazan work order kod i uspoređuje kodove u formatu MTDDMMYY po godini, mjesecu i danu. Popravlja tok uređivanja servisnog zapisa tako da upload fotografija ostaje u višem overlayju bez zatvaranja parent modala te nakon uspješnog uploada osvježava listu datoteka u detaljima. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../dashboard/FleetDashboardShell.jsx | 115 +++++++++++++++--- .../components/fleet/ServicePhotoUpload.jsx | 2 +- .../fleet/ServiceRecordDetailModal.jsx | 3 +- frontend/src/lib/displayIds.js | 26 ++++ 4 files changed, 127 insertions(+), 19 deletions(-) diff --git a/frontend/src/components/dashboard/FleetDashboardShell.jsx b/frontend/src/components/dashboard/FleetDashboardShell.jsx index bd80e42..9d820a9 100644 --- a/frontend/src/components/dashboard/FleetDashboardShell.jsx +++ b/frontend/src/components/dashboard/FleetDashboardShell.jsx @@ -33,7 +33,7 @@ import { 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 { formatWorkOrderDisplayCode, parseWorkOrderDisplayCode } from '../../lib/displayIds'; import { $tasks, fetchTasks, getStatusLabel, isTaskActive, updateTask } from '../../stores/taskStore'; import AnimatedDataTable from '../ui/AnimatedDataTable'; import AnimatedPage from '../ui/AnimatedPage'; @@ -98,6 +98,44 @@ function normalizeComparableId(value) { return value ?? ''; } +function normalizeWorkOrderCodeForSort(value) { + return String(value || '').trim().toUpperCase().split(/\s+/)[0] || ''; +} + +function compareWorkOrderCodesByDate(leftCode, rightCode, direction = 'asc') { + const left = parseWorkOrderDisplayCode(normalizeWorkOrderCodeForSort(leftCode)); + const right = parseWorkOrderDisplayCode(normalizeWorkOrderCodeForSort(rightCode)); + if (left.valid && right.valid) { + if (left.sortKey !== right.sortKey) { + return direction === 'asc' ? left.sortKey - right.sortKey : right.sortKey - left.sortKey; + } + return left.normalized.localeCompare(right.normalized); + } + if (left.valid !== right.valid) { + return left.valid ? -1 : 1; + } + return String(leftCode || '').localeCompare(String(rightCode || '')); +} + +function nextSortDirection(direction) { + return direction === 'asc' ? 'desc' : 'asc'; +} + +function SortableCodeLabel({ title, direction, onToggle }) { + return ( + + ); +} + const PAGE_MODE_TO_SECTION = { dashboard: 'dashboard', 'work-orders': 'work-orders', @@ -120,6 +158,7 @@ export default function FleetDashboardShell({ initialSection = 'dashboard', page const [selectedServiceRecord, setSelectedServiceRecord] = useState(null); // photoUpload: { open: bool, serviceRecordId: string|null } const [photoUpload, setPhotoUpload] = useState({ open: false, serviceRecordId: null }); + const [serviceRecordFilesRefreshToken, setServiceRecordFilesRefreshToken] = useState(0); const [workOrderPhotoUpload, setWorkOrderPhotoUpload] = useState({ open: false, workOrderId: null }); const loading = useStore($dashboardLoading); const error = useStore($dashboardError); @@ -141,6 +180,8 @@ export default function FleetDashboardShell({ initialSection = 'dashboard', page const [supervisorServicers, setSupervisorServicers] = useState([]); const [selectedServicerId, setSelectedServicerId] = useState('all'); const [workOrderScope, setWorkOrderScope] = useState('context'); + const [workOrderCodeSortDirection, setWorkOrderCodeSortDirection] = useState('desc'); + const [taskWorkOrderSortDirection, setTaskWorkOrderSortDirection] = useState('desc'); const [selectedCraneRowId, setSelectedCraneRowId] = useState(null); const [expandedServiceTaskId, setExpandedServiceTaskId] = useState(null); const [hasMounted, setHasMounted] = useState(false); @@ -368,14 +409,22 @@ export default function FleetDashboardShell({ initialSection = 'dashboard', page )); }); + const codeSorted = [...roleScoped].sort((left, right) => ( + compareWorkOrderCodesByDate( + formatWorkOrderDisplayCode(left), + formatWorkOrderDisplayCode(right), + workOrderCodeSortDirection + ) + )); + if (workOrderScope === 'all') { - return roleScoped; + return codeSorted; } if (!hydratedSelectedVehicleId) { return []; } - return roleScoped.filter((order) => String(order.vehicle) === String(hydratedSelectedVehicleId)); - }, [workOrders, workOrderScope, hydratedSelectedVehicleId, user?.id, user?.is_serviser, user?.is_team_member]); + return codeSorted.filter((order) => String(order.vehicle) === String(hydratedSelectedVehicleId)); + }, [workOrders, workOrderScope, hydratedSelectedVehicleId, user?.id, user?.is_serviser, user?.is_team_member, workOrderCodeSortDirection]); const pageSize = 10; const totalPages = Math.max(1, Math.ceil(scopedWorkOrders.length / pageSize)); const latestWorkOrders = useMemo( @@ -466,14 +515,24 @@ export default function FleetDashboardShell({ initialSection = 'dashboard', page (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 filteredByStatus = (() => { + if (taskFilter === 'active') { + return filteredByServicer.filter((task) => isTaskActive(task)); + } + if (taskFilter === 'inactive') { + return filteredByServicer.filter((task) => !isTaskActive(task)); + } + return filteredByServicer; + })(); + + return [...filteredByStatus].sort((left, right) => ( + compareWorkOrderCodesByDate( + left?.work_order_label || '', + right?.work_order_label || '', + taskWorkOrderSortDirection + ) + )); + }, [tasks, taskFilter, canViewAllTasks, canToggleTaskScope, taskScope, user?.id, isSupervisorNotServiser, selectedServicerId, taskWorkOrderSortDirection]); const selectedTaskServiceRecords = useMemo(() => { if (!selectedTask?.id) return []; return [...serviceRecords] @@ -587,7 +646,17 @@ export default function FleetDashboardShell({ initialSection = 'dashboard', page } const workOrderColumns = [ - { key: 'code', label: 'Putni nalog', className: 'px-4 py-3' }, + { + key: 'code', + label: ( + setWorkOrderCodeSortDirection((prev) => nextSortDirection(prev))} + /> + ), + 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' }, @@ -609,7 +678,17 @@ export default function FleetDashboardShell({ initialSection = 'dashboard', page { 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: 'workOrder', + label: ( + setTaskWorkOrderSortDirection((prev) => nextSortDirection(prev))} + /> + ), + className: 'px-4 py-3', + }, { key: 'assigned', label: 'Dodijeljeno', className: 'px-4 py-3' }, ]; const isDashboardPage = pageMode === 'dashboard'; @@ -1217,7 +1296,10 @@ export default function FleetDashboardShell({ initialSection = 'dashboard', page open={photoUpload.open} serviceRecordId={photoUpload.serviceRecordId} onClose={() => setPhotoUpload({ open: false, serviceRecordId: null })} - onSuccess={() => fetchFleetDashboardData({ silent: true })} + onSuccess={() => { + setServiceRecordFilesRefreshToken((prev) => prev + 1); + fetchFleetDashboardData({ silent: true }); + }} /> { const targetId = record?.id ?? selectedServiceRecord?.id; if (!targetId) return; - setSelectedServiceRecord(null); - setServiceRecordBackTask(null); setPhotoUpload({ open: true, serviceRecordId: targetId }); }} + filesRefreshToken={serviceRecordFilesRefreshToken} onBack={serviceRecordBackTask ? () => { setSelectedServiceRecord(null); diff --git a/frontend/src/components/fleet/ServicePhotoUpload.jsx b/frontend/src/components/fleet/ServicePhotoUpload.jsx index dde82da..250b512 100644 --- a/frontend/src/components/fleet/ServicePhotoUpload.jsx +++ b/frontend/src/components/fleet/ServicePhotoUpload.jsx @@ -134,7 +134,7 @@ export default function ServicePhotoUpload({ open, serviceRecordId, onClose, onS return ( diff --git a/frontend/src/components/fleet/ServiceRecordDetailModal.jsx b/frontend/src/components/fleet/ServiceRecordDetailModal.jsx index 5af354f..b039a22 100644 --- a/frontend/src/components/fleet/ServiceRecordDetailModal.jsx +++ b/frontend/src/components/fleet/ServiceRecordDetailModal.jsx @@ -35,6 +35,7 @@ export default function ServiceRecordDetailModal({ onClose, onUpdated, onOpenPhotoUpload = null, + filesRefreshToken = 0, onBack = null, backLabel = 'Natrag', }) { @@ -117,7 +118,7 @@ export default function ServiceRecordDetailModal({ return () => { cancelled = true; }; - }, [open, serviceRecord?.id]); + }, [open, serviceRecord?.id, filesRefreshToken]); useEffect(() => { if (!emailModalOpen) return; diff --git a/frontend/src/lib/displayIds.js b/frontend/src/lib/displayIds.js index 5ce4fda..e2e2504 100644 --- a/frontend/src/lib/displayIds.js +++ b/frontend/src/lib/displayIds.js @@ -30,6 +30,32 @@ export function formatWorkOrderDisplayCode(workOrder) { return '-'; } +const WORK_ORDER_CODE_PATTERN = /^([A-Z]{2})(\d{2})(\d{2})(\d{2})$/; + +export function parseWorkOrderDisplayCode(value) { + const normalized = String(value || '').trim().toUpperCase(); + const match = normalized.match(WORK_ORDER_CODE_PATTERN); + if (!match) { + return { valid: false, normalized }; + } + const day = Number(match[2]); + const month = Number(match[3]); + const year = 2000 + Number(match[4]); + if (day < 1 || day > 31 || month < 1 || month > 12) { + return { valid: false, normalized }; + } + const sortKey = year * 10000 + month * 100 + day; + return { + valid: true, + normalized, + prefix: match[1], + day, + month, + year, + sortKey, + }; +} + export function formatPurposeLabel(value) { const normalized = String(value || '').trim().toLowerCase(); if (!normalized) return '';