diff --git a/backend/modules/fleet/serializers.py b/backend/modules/fleet/serializers.py index 74cb1cc..74fc6c4 100644 --- a/backend/modules/fleet/serializers.py +++ b/backend/modules/fleet/serializers.py @@ -38,7 +38,7 @@ class VehicleSerializer(serializers.ModelSerializer): 'is_company_vehicle', 'client', 'client_name', 'assigned_servicer', 'is_active', ] - read_only_fields = ['id', 'current_mileage', 'client_name'] + read_only_fields = ['id', 'client_name'] def get_client_name(self, obj): if obj.client_id: diff --git a/backend/modules/task_management/views.py b/backend/modules/task_management/views.py index 7e4c8c8..9ebc347 100644 --- a/backend/modules/task_management/views.py +++ b/backend/modules/task_management/views.py @@ -30,6 +30,9 @@ class TaskViewSet(viewsets.ModelViewSet): scheduled_date = self.request.query_params.get('scheduled_date') if scheduled_date: qs = qs.filter(scheduled_date=scheduled_date) + work_order_id = self.request.query_params.get('work_order') + if work_order_id: + qs = qs.filter(work_order_id=work_order_id) return qs def perform_create(self, serializer): diff --git a/frontend/src/components/dashboard/WorkOrderDetailModal.jsx b/frontend/src/components/dashboard/WorkOrderDetailModal.jsx index 9040ef8..b8f6393 100644 --- a/frontend/src/components/dashboard/WorkOrderDetailModal.jsx +++ b/frontend/src/components/dashboard/WorkOrderDetailModal.jsx @@ -2,15 +2,19 @@ import { useEffect, useMemo, useState } from 'preact/hooks'; import { useStore } from '@nanostores/preact'; import WorkOrderImageCarousel from './WorkOrderImageCarousel'; import WorkOrderPhotoUpload from '../fleet/WorkOrderPhotoUpload'; +import TaskCreateModal from './TaskCreateModal'; import ModalShell from '../ui/ModalShell'; import { createWorkOrderInvoice, + fetchTasksByWorkOrder, fetchTeamMembers, fetchWorkOrderInvoices, fetchWorkOrderImages, openWorkOrderInvoicesPdfPage, sendWorkOrderEmail, + updateVehicle, } from '../../stores/fleetDashboardStore'; +import { getStatusLabel } from '../../stores/taskStore'; import { showToast } from '../../stores/toastStore'; import { formatWorkOrderDisplayCode } from '../../lib/displayIds'; import { $serviceNotes, $serviceNotesLoading, fetchServiceNotes } from '../../stores/serviceNotesStore'; @@ -81,6 +85,12 @@ export default function WorkOrderDetailModal({ open, workOrder, onClose, onSubmi const [selectedLocation, setSelectedLocation] = useState(null); const [searchingLocation, setSearchingLocation] = useState(false); const [mapError, setMapError] = useState(''); + const [workOrderTasks, setWorkOrderTasks] = useState([]); + const [addTaskModalOpen, setAddTaskModalOpen] = useState(false); + 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 craneLabel = useMemo(() => { if (!workOrder) return '-'; @@ -168,6 +178,12 @@ export default function WorkOrderDetailModal({ open, workOrder, onClose, onSubmi setSelectedLocation(null); setSearchingLocation(false); setMapError(''); + setWorkOrderTasks([]); + setAddTaskModalOpen(false); + setCraneDataModalOpen(false); + setCraneDataForm({ superstructure_working_hours: '', chassis_working_hours: '', current_mileage: '' }); + setSavingCraneData(false); + setCraneDataError(''); }, [open, workOrder]); useEffect(() => { @@ -251,6 +267,23 @@ export default function WorkOrderDetailModal({ open, workOrder, onClose, onSubmi }; }, [open, workOrder?.id]); + useEffect(() => { + if (!open || !workOrder?.id) { + setWorkOrderTasks([]); + return; + } + let cancelled = false; + (async () => { + try { + const tasks = await fetchTasksByWorkOrder(workOrder.id); + if (!cancelled) setWorkOrderTasks(tasks); + } catch { + if (!cancelled) setWorkOrderTasks([]); + } + })(); + return () => { cancelled = true; }; + }, [open, workOrder?.id]); + const selectedMapEmbedUrl = (() => { if (!selectedLocation) return null; const lat = Number(selectedLocation.lat); @@ -265,6 +298,60 @@ export default function WorkOrderDetailModal({ open, workOrder, onClose, onSubmi return null; } + const craneVehicleId = workOrder.vehicle || workOrder.crane; + const craneVehicle = workOrder.craneInfo || null; + + const openCraneDataModal = () => { + setCraneDataForm({ + superstructure_working_hours: String(craneVehicle?.superstructure_working_hours ?? ''), + chassis_working_hours: String(craneVehicle?.chassis_working_hours ?? ''), + current_mileage: String(craneVehicle?.current_mileage ?? ''), + }); + setCraneDataError(''); + setCraneDataModalOpen(true); + }; + + const handleSaveCraneData = async () => { + if (!craneVehicleId) { + setCraneDataError('Dizalica nije pronađena.'); + 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 superstrukture 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('Kilometraža 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('Nije unesena niti jedna vrijednost za ažuriranje.'); + return; + } + setSavingCraneData(true); + setCraneDataError(''); + try { + await updateVehicle(craneVehicleId, payload); + showToast('Podaci dizalice su uspješno ažurirani.', 'success'); + setCraneDataModalOpen(false); + } catch (err) { + setCraneDataError(err?.message || 'Ažuriranje podataka dizalice nije uspjelo.'); + } finally { + setSavingCraneData(false); + } + }; + const searchLocation = async () => { const query = locationQuery.trim(); if (!query) { @@ -522,6 +609,13 @@ export default function WorkOrderDetailModal({ open, workOrder, onClose, onSubmi > Dodaj račun + + 🔧 Izmijeni podatke dizalice + { @@ -540,6 +634,33 @@ export default function WorkOrderDetailModal({ open, workOrder, onClose, onSubmi )} + + + Zadaci putnog naloga ({workOrderTasks.length}) + setAddTaskModalOpen(true)} + className="rounded-md border border-border-hairline px-2 py-1 text-xs font-medium text-text-main hover:bg-canvas-deep" + > + + Dodaj zadatak + + + {workOrderTasks.length === 0 ? ( + Nema zadataka za ovaj putni nalog. + ) : ( + + {workOrderTasks.map((task) => ( + + {task.title} + + {getStatusLabel(task.status)} + + + ))} + + )} + + Status @@ -1165,6 +1286,99 @@ export default function WorkOrderDetailModal({ open, workOrder, onClose, onSubmi } }} /> + setAddTaskModalOpen(false)} + onSuccess={async () => { + const tasks = await fetchTasksByWorkOrder(workOrder.id).catch(() => []); + setWorkOrderTasks(tasks); + }} + /> + {craneDataModalOpen && ( + setCraneDataModalOpen(false)} + overlayClassName="z-[60] overflow-y-auto p-4 pt-16" + contentClassName="flex items-start justify-center min-h-full" + panelClassName="w-full max-w-md rounded-xl border border-border-hairline bg-canvas-elevated shadow-xl" + > + + + + Izmijeni podatke dizalice + {craneLabel} + + setCraneDataModalOpen(false)} + className="rounded-md px-2 py-1 text-sm text-text-muted hover:bg-canvas-deep" + aria-label="Zatvori" + > + ✕ + + + + + Radni sati superstrukture + setCraneDataForm((p) => ({ ...p, superstructure_working_hours: e.currentTarget.value }))} + className="rounded-lg border border-border-hairline bg-canvas-base px-3 py-2 text-text-main" + placeholder={String(craneVehicle?.superstructure_working_hours ?? '0')} + /> + + + Radni sati podvozja + setCraneDataForm((p) => ({ ...p, chassis_working_hours: e.currentTarget.value }))} + className="rounded-lg border border-border-hairline bg-canvas-base px-3 py-2 text-text-main" + placeholder={String(craneVehicle?.chassis_working_hours ?? '0')} + /> + + + Prijeđeni kilometri dizalice + setCraneDataForm((p) => ({ ...p, current_mileage: e.currentTarget.value }))} + className="rounded-lg border border-border-hairline bg-canvas-base px-3 py-2 text-text-main" + placeholder={String(craneVehicle?.current_mileage ?? '0')} + /> + + {craneDataError && ( + + {craneDataError} + + )} + + + setCraneDataModalOpen(false)} + className="rounded-md border border-border-hairline px-3 py-2 text-xs font-medium text-text-main hover:bg-canvas-deep" + disabled={savingCraneData} + > + Odustani + + + {savingCraneData ? 'Spremam…' : 'Spremi podatke'} + + + + + )} > ); } diff --git a/frontend/src/stores/fleetDashboardStore.js b/frontend/src/stores/fleetDashboardStore.js index 8519770..a1d52e6 100644 --- a/frontend/src/stores/fleetDashboardStore.js +++ b/frontend/src/stores/fleetDashboardStore.js @@ -721,6 +721,14 @@ export async function fetchWorkOrderById(workOrderId) { return await api.get(`fleet/work-orders/${encodeURIComponent(workOrderId)}/`); } +export async function fetchTasksByWorkOrder(workOrderId) { + if (!workOrderId) { + return []; + } + const data = await api.get(`tasks/tasks/?work_order=${encodeURIComponent(workOrderId)}`); + return Array.isArray(data) ? data : (data?.results ?? []); +} + export async function fetchWorkOrderTaskServiceContext(workOrderId) { if (!workOrderId) { return { tasks: [], additional_costs_table: null };
Nema zadataka za ovaj putni nalog.
{craneLabel}