feat: dodaj display_code polje za putne naloge (MT070626 format)
Some checks failed
ERP CI/CD Pipeline / test (push) Has been cancelled
ERP CI/CD Pipeline / Deploy (server git pull + compose) (push) Has been cancelled

- 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:
mariomitte
2026-07-17 23:53:47 +02:00
parent ef8718a4cf
commit bf8e25efe7
19 changed files with 377 additions and 54 deletions

View File

@@ -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>