fix: sortiranje putnih naloga i upload modal u detaljima
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>
This commit is contained in:
@@ -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 (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onToggle}
|
||||
className="inline-flex items-center gap-1 rounded px-1 py-0.5 text-left hover:bg-canvas-base"
|
||||
title="Sortiraj po datumu iz koda (godina, mjesec, dan)"
|
||||
>
|
||||
<span>{title}</span>
|
||||
<span className={direction === 'asc' ? 'text-indigo-700' : 'text-text-muted/50'}>↑</span>
|
||||
<span className={direction === 'desc' ? 'text-indigo-700' : 'text-text-muted/50'}>↓</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
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: (
|
||||
<SortableCodeLabel
|
||||
title="Putni nalog"
|
||||
direction={workOrderCodeSortDirection}
|
||||
onToggle={() => 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: (
|
||||
<SortableCodeLabel
|
||||
title="Putni nalog"
|
||||
direction={taskWorkOrderSortDirection}
|
||||
onToggle={() => 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 });
|
||||
}}
|
||||
/>
|
||||
|
||||
<WorkOrderPhotoUpload
|
||||
@@ -1241,10 +1323,9 @@ export default function FleetDashboardShell({ initialSection = 'dashboard', page
|
||||
onOpenPhotoUpload={(record) => {
|
||||
const targetId = record?.id ?? selectedServiceRecord?.id;
|
||||
if (!targetId) return;
|
||||
setSelectedServiceRecord(null);
|
||||
setServiceRecordBackTask(null);
|
||||
setPhotoUpload({ open: true, serviceRecordId: targetId });
|
||||
}}
|
||||
filesRefreshToken={serviceRecordFilesRefreshToken}
|
||||
onBack={serviceRecordBackTask
|
||||
? () => {
|
||||
setSelectedServiceRecord(null);
|
||||
|
||||
@@ -134,7 +134,7 @@ export default function ServicePhotoUpload({ open, serviceRecordId, onClose, onS
|
||||
return (
|
||||
<ModalShell
|
||||
onClose={handleCancel}
|
||||
overlayClassName="z-50 p-4"
|
||||
overlayClassName="z-[70] p-4"
|
||||
contentClassName="flex items-center justify-center min-h-full"
|
||||
panelClassName={`w-full max-w-lg rounded-xl border border-border-hairline bg-canvas-elevated shadow-xl ${isOffline ? 'pointer-events-none opacity-50' : ''}`}
|
||||
>
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user