diff --git a/backend/modules/fleet/views.py b/backend/modules/fleet/views.py index 6333f65..9f8f161 100644 --- a/backend/modules/fleet/views.py +++ b/backend/modules/fleet/views.py @@ -3055,10 +3055,17 @@ def _request_monthly_archive_generation(*, request, archive_type): .first() ) if existing_pending: - return { - 'status': 'processing', - 'generated_archive_id': str(existing_pending.pk), - } + stale_pending_threshold = timezone.now() - timedelta(minutes=3) + if existing_pending.created_at and existing_pending.created_at < stale_pending_threshold: + existing_pending.is_active = False + existing_pending.status = 'failed' + existing_pending.error_message = 'ZIP zahtjev je ostao predugo u pending statusu; pokrece se novi zahtjev.' + existing_pending.save(update_fields=['is_active', 'status', 'error_message', 'updated_at']) + else: + return { + 'status': 'processing', + 'generated_archive_id': str(existing_pending.pk), + } generated_archive = GeneratedFleetArchive.objects.create( requested_by=request.user, @@ -3080,6 +3087,7 @@ def _request_monthly_archive_generation(*, request, archive_type): stage='requested', year=year, month=month, + generated_archive=generated_archive, ) try: diff --git a/frontend/src/components/Navbar.jsx b/frontend/src/components/Navbar.jsx index c732828..2e2c03f 100644 --- a/frontend/src/components/Navbar.jsx +++ b/frontend/src/components/Navbar.jsx @@ -4,7 +4,7 @@ import AuthWidget from './AuthWidget'; import UserDisplay from './ui/UserDisplay'; import { useEffect, useRef, useState } from 'preact/hooks'; import { useSpring, animated } from '@react-spring/web'; -import { hydrateAuthFromStorage } from '../stores/authStore'; +import { hydrateAuthFromStorage, loadCurrentUser, $user, $accessToken } from '../stores/authStore'; const ITEMS = [ { id: 'dashboard', label: 'Dashboard', href: '/' }, @@ -21,8 +21,12 @@ function normalizePath(p) { } function getActiveIndex(path) { - const idx = ITEMS.findIndex((item) => item.href === path); - return idx >= 0 ? idx : 0; + // Exact match (e.g. '/' → Dashboard) + const exact = ITEMS.findIndex((item) => item.href === path); + if (exact >= 0) return exact; + // Prefix match for sub-paths (e.g. '/putni-nalozi/racuni' → Putni nalozi) + const prefix = ITEMS.findIndex((item) => item.href !== '/' && path.startsWith(item.href)); + return prefix >= 0 ? prefix : 0; } export default function Navbar({ minimal = false }) { @@ -55,6 +59,10 @@ export default function Navbar({ minimal = false }) { // Mount: izmjeri početnu poziciju bez animacije, pa uključi animaciju za buduće navigacije useEffect(() => { hydrateAuthFromStorage(); + // Učitaj korisnika ako token postoji ali $user još nije popunjen + if (!$user.get() && $accessToken.get()) { + loadCurrentUser(); + } const timer = setTimeout(() => { measureIndicator(); diff --git a/frontend/src/components/dashboard/TaskCalendarWidget.jsx b/frontend/src/components/dashboard/TaskCalendarWidget.jsx index 294eb4a..8cfcb36 100644 --- a/frontend/src/components/dashboard/TaskCalendarWidget.jsx +++ b/frontend/src/components/dashboard/TaskCalendarWidget.jsx @@ -367,10 +367,10 @@ export default function TaskCalendarWidget({ tasks = [], notes = [], workOrders async function handleDownloadAllTasksArchive() { if (downloadingAllTasks) return; + setBulkDownloadOpen(false); setDownloadingAllTasks(true); try { await downloadMonthlyServiceTasksArchive(viewYear, viewMonth + 1); - setBulkDownloadOpen(false); } finally { setDownloadingAllTasks(false); } @@ -378,10 +378,10 @@ export default function TaskCalendarWidget({ tasks = [], notes = [], workOrders async function handleDownloadAllWorkOrdersArchive() { if (downloadingAllWorkOrders) return; + setBulkDownloadOpen(false); setDownloadingAllWorkOrders(true); try { await downloadMonthlyWorkOrdersArchive(viewYear, viewMonth + 1); - setBulkDownloadOpen(false); } finally { setDownloadingAllWorkOrders(false); } @@ -655,23 +655,25 @@ export default function TaskCalendarWidget({ tasks = [], notes = [], workOrders {MONTH_NAMES[viewMonth]} {viewYear} - - {reportType === 'servicer' && ( +
diff --git a/frontend/src/components/dashboard/TaskCreateModal.jsx b/frontend/src/components/dashboard/TaskCreateModal.jsx
index 41b21e8..86d05ef 100644
--- a/frontend/src/components/dashboard/TaskCreateModal.jsx
+++ b/frontend/src/components/dashboard/TaskCreateModal.jsx
@@ -2,6 +2,7 @@
import { createTask, fetchTaskTemplates, getStatusLabel } from '../../stores/taskStore';
import { formatPurposeLabel, formatWorkOrderDisplayCode } from '../../lib/displayIds';
import ModalShell from '../ui/ModalShell';
+import { updateVehicle } from '../../stores/fleetDashboardStore';
const STATUS_OPTIONS = ['aktivan', 'servis', 'zavrsen', 'neaktivan'];
@@ -32,6 +33,10 @@ export default function TaskCreateModal({ open, workOrders = [], contextCrane =
const [templateLoading, setTemplateLoading] = useState(false);
const [templateId, setTemplateId] = useState('');
const [templates, setTemplates] = useState([]);
+ const [craneDataModalOpen, setCraneDataModalOpen] = useState(false);
+ const [craneDataForm, setCraneDataForm] = useState({ superstructure_working_hours: '', chassis_working_hours: '', current_mileage: '' });
+ const [savingCraneData, setSavingCraneData] = useState(false);
+ const [craneDataError, setCraneDataError] = useState('');
const selectedTemplate = useMemo(
() => templates.find((template) => String(template.id) === String(templateId)) || null,
@@ -51,6 +56,10 @@ export default function TaskCreateModal({ open, workOrders = [], contextCrane =
setTemplateId('');
setTemplates([]);
setTemplateLoading(false);
+ setCraneDataModalOpen(false);
+ setCraneDataForm({ superstructure_working_hours: '', chassis_working_hours: '', current_mileage: '' });
+ setSavingCraneData(false);
+ setCraneDataError('');
return;
}
// Auto-pre-select today's open work order if exactly one exists for this crane
@@ -76,6 +85,60 @@ export default function TaskCreateModal({ open, workOrders = [], contextCrane =
}));
}, [selectedTemplate]);
+ function openCraneDataModal() {
+ if (!contextCrane?.id) {
+ setCraneDataError('Dizalica nije odabrana u servisnom kontekstu.');
+ return;
+ }
+ setCraneDataForm({
+ superstructure_working_hours: String(contextCrane?.superstructure_working_hours ?? ''),
+ chassis_working_hours: String(contextCrane?.chassis_working_hours ?? ''),
+ current_mileage: String(contextCrane?.current_mileage ?? ''),
+ });
+ setCraneDataError('');
+ setCraneDataModalOpen(true);
+ }
+
+ async function handleSaveCraneData() {
+ if (!contextCrane?.id) {
+ setCraneDataError('Dizalica nije odabrana u servisnom kontekstu.');
+ return;
+ }
+ const superstructure = craneDataForm.superstructure_working_hours === '' ? undefined : Number(craneDataForm.superstructure_working_hours);
+ const chassis = craneDataForm.chassis_working_hours === '' ? undefined : Number(craneDataForm.chassis_working_hours);
+ const mileage = craneDataForm.current_mileage === '' ? undefined : Number(craneDataForm.current_mileage);
+ if (superstructure !== undefined && (Number.isNaN(superstructure) || superstructure < 0)) {
+ setCraneDataError('Radni sati nadogradnje moraju biti pozitivan broj.');
+ return;
+ }
+ if (chassis !== undefined && (Number.isNaN(chassis) || chassis < 0)) {
+ setCraneDataError('Radni sati podvozja moraju biti pozitivan broj.');
+ return;
+ }
+ if (mileage !== undefined && (Number.isNaN(mileage) || mileage < 0)) {
+ setCraneDataError('Kilometraza mora biti pozitivan broj.');
+ return;
+ }
+ const payload = {};
+ if (superstructure !== undefined) payload.superstructure_working_hours = superstructure;
+ if (chassis !== undefined) payload.chassis_working_hours = chassis;
+ if (mileage !== undefined) payload.current_mileage = mileage;
+ if (Object.keys(payload).length === 0) {
+ setCraneDataError('Unesite barem jednu vrijednost za azuriranje.');
+ return;
+ }
+ setSavingCraneData(true);
+ setCraneDataError('');
+ try {
+ await updateVehicle(contextCrane.id, payload);
+ setCraneDataModalOpen(false);
+ } catch (err) {
+ setCraneDataError(err?.message || 'Azuriranje podataka dizalice nije uspjelo.');
+ } finally {
+ setSavingCraneData(false);
+ }
+ }
+
if (!open) return null;
function setField(name, value) {
@@ -156,6 +219,7 @@ export default function TaskCreateModal({ open, workOrders = [], contextCrane =
}
return (
+ <>
Podaci odabrane dizalice {contextCrane.registration_number || '-'} / {contextCrane.make || '-'} {contextCrane.model || ''}
+ Azurirajte radne sate i kilometrazu za dizalicu: {contextCrane?.registration_number || "-"}
+
Ažurirajte radne sate i kilometražu za dizalicu: {getTaskCraneDisplay(task)}
diff --git a/frontend/src/components/dashboard/WorkOrderInvoicesPdfPage.jsx b/frontend/src/components/dashboard/WorkOrderInvoicesPdfPage.jsx
index 9c1cd74..fe54e7c 100644
--- a/frontend/src/components/dashboard/WorkOrderInvoicesPdfPage.jsx
+++ b/frontend/src/components/dashboard/WorkOrderInvoicesPdfPage.jsx
@@ -321,22 +321,24 @@ export default function WorkOrderInvoicesPdfPage() {
{workOrder ? `Dizalica: ${workOrder.crane_label || workOrder.crane || '-'}` : 'Pregled računa prije kreiranja PDF-a.'}
+
+ Izmijeni podatke dizalice
+