feat: dodaj display_code polje za putne naloge (MT070626 format)
- Dodano polje 'display_code' na WorkOrder model (format: 2 inicijala + ddmmyy) - Migracije za kreiranje polja i backfill postojećih naloga - Ažuriran WorkOrderSerializer za validaciju i serijalizaciju display_code - Backend tasks za auto-generiranje display_code pri kreiranju - Frontend komponente ažurirane za prikaz display_code umjesto UUID: * FleetDashboardShell tablica putnih naloga * WorkOrderDetailModal * TaskCreateModal * Sve PDF stranice - displayIds.js utility za formatiranjeDisplay ID-eva - Admin interface prikazuje display_code
This commit is contained in:
@@ -25,6 +25,7 @@ import {
|
||||
$serviceRecords,
|
||||
createWorkOrder,
|
||||
fetchFleetDashboardData,
|
||||
fetchTeamMembers,
|
||||
ensureVehiclesCatalog,
|
||||
triggerOfflineSync,
|
||||
updateWorkOrder,
|
||||
@@ -32,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 { formatEntityCode } from '../../lib/displayIds';
|
||||
import { formatWorkOrderDisplayCode } from '../../lib/displayIds';
|
||||
import { $tasks, fetchTasks, getStatusLabel, isTaskActive, updateTask } from '../../stores/taskStore';
|
||||
import AnimatedDataTable from '../ui/AnimatedDataTable';
|
||||
import AnimatedPage from '../ui/AnimatedPage';
|
||||
@@ -75,6 +76,15 @@ function formatTaskCraneLabel(task, cranes) {
|
||||
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 '-';
|
||||
@@ -121,6 +131,8 @@ export default function FleetDashboardShell({ initialSection = 'dashboard', page
|
||||
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);
|
||||
@@ -360,6 +372,7 @@ export default function FleetDashboardShell({ initialSection = 'dashboard', page
|
||||
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) => (
|
||||
@@ -370,6 +383,42 @@ export default function FleetDashboardShell({ initialSection = 'dashboard', page
|
||||
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 = (() => {
|
||||
@@ -381,14 +430,22 @@ export default function FleetDashboardShell({ initialSection = 'dashboard', page
|
||||
}
|
||||
return sorted;
|
||||
})();
|
||||
const filteredByServicer = (() => {
|
||||
if (!isSupervisorNotServiser || selectedServicerId === 'all') {
|
||||
return scoped;
|
||||
}
|
||||
return scoped.filter(
|
||||
(task) => String(task?.assigned_to || '') === String(selectedServicerId)
|
||||
);
|
||||
})();
|
||||
if (taskFilter === 'active') {
|
||||
return scoped.filter((task) => isTaskActive(task));
|
||||
return filteredByServicer.filter((task) => isTaskActive(task));
|
||||
}
|
||||
if (taskFilter === 'inactive') {
|
||||
return scoped.filter((task) => !isTaskActive(task));
|
||||
return filteredByServicer.filter((task) => !isTaskActive(task));
|
||||
}
|
||||
return scoped;
|
||||
}, [tasks, taskFilter, canViewAllTasks, canToggleTaskScope, taskScope, user?.id]);
|
||||
return filteredByServicer;
|
||||
}, [tasks, taskFilter, canViewAllTasks, canToggleTaskScope, taskScope, user?.id, isSupervisorNotServiser, selectedServicerId]);
|
||||
const selectedTaskServiceRecords = useMemo(() => {
|
||||
if (!selectedTask?.id) return [];
|
||||
return [...serviceRecords]
|
||||
@@ -435,7 +492,7 @@ export default function FleetDashboardShell({ initialSection = 'dashboard', page
|
||||
showToast('Putni nalog je spremljen.', 'success');
|
||||
await fetchFleetDashboardData({ silent: true });
|
||||
} catch (error) {
|
||||
showToast('Kreiranje naloga nije uspjelo.', 'error');
|
||||
showToast(error?.message || 'Kreiranje naloga nije uspjelo.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -446,7 +503,7 @@ export default function FleetDashboardShell({ initialSection = 'dashboard', page
|
||||
await fetchFleetDashboardData({ silent: true });
|
||||
await fetchTasks();
|
||||
} catch (error) {
|
||||
showToast('Ažuriranje naloga nije uspjelo.', 'error');
|
||||
showToast(error?.message || 'Ažuriranje naloga nije uspjelo.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -595,6 +652,23 @@ export default function FleetDashboardShell({ initialSection = 'dashboard', page
|
||||
<div className="flex items-center justify-between border-b border-border-hairline px-4 py-3">
|
||||
<h3 className="font-semibold text-text-main">Radni zadaci</h3>
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
{hasMounted && isSupervisorNotServiser && (
|
||||
<label className="flex items-center gap-2 text-text-main">
|
||||
<span>Serviser</span>
|
||||
<select
|
||||
value={selectedServicerId}
|
||||
onChange={(event) => setSelectedServicerId(event.target.value)}
|
||||
className="rounded-md border border-border-hairline bg-canvas-elevated px-2 py-1 text-xs text-text-main focus:border-indigo-400 focus:outline-none"
|
||||
>
|
||||
<option value="all">Svi serviseri</option>
|
||||
{supervisorServicers.map((servicer) => (
|
||||
<option key={servicer.id} value={servicer.id}>
|
||||
{servicer.full_name || servicer.email}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
)}
|
||||
{canToggleTaskScope && (
|
||||
<>
|
||||
<button
|
||||
@@ -765,7 +839,7 @@ export default function FleetDashboardShell({ initialSection = 'dashboard', page
|
||||
className="cursor-pointer px-4 py-3 font-medium text-text-main"
|
||||
onClick={() => openWorkOrderDetails(workOrder)}
|
||||
>
|
||||
{formatEntityCode('PN', workOrder.id)}
|
||||
{formatWorkOrderDisplayCode(workOrder)}
|
||||
</td>
|
||||
<td
|
||||
className="px-4 py-3"
|
||||
@@ -912,7 +986,7 @@ export default function FleetDashboardShell({ initialSection = 'dashboard', page
|
||||
<AnimatedDataTable
|
||||
columns={[
|
||||
{ key: 'date', label: 'Datum', className: 'px-4 py-3' },
|
||||
{ key: 'crane', label: 'SN', className: 'px-4 py-3' },
|
||||
{ key: 'crane', label: 'DIZALICA', className: 'px-4 py-3' },
|
||||
{ key: 'description', label: 'Opis', className: 'px-4 py-3' },
|
||||
{ key: 'cost', label: 'Trošak', className: 'px-4 py-3' },
|
||||
{ key: 'files', label: 'Datoteke', className: 'px-4 py-3' },
|
||||
@@ -927,7 +1001,7 @@ export default function FleetDashboardShell({ initialSection = 'dashboard', page
|
||||
{JSON.stringify(group, null, 2)}
|
||||
</pre> */}
|
||||
<td className="px-4 py-3">{formatDate(record.service_date)}</td>
|
||||
<td className="px-4 py-3">{record.crane_serial_number || '-'}</td>
|
||||
<td className="px-4 py-3">{formatServiceRecordCraneLabel(record, cranes)}</td>
|
||||
<td className="max-w-md truncate px-4 py-3" title={record.description || ''}>
|
||||
{record.description || '-'}
|
||||
</td>
|
||||
|
||||
Reference in New Issue
Block a user