feat: vise taskova po putnom nalogu i izmjena podataka dizalice
- task_management/views.py: dodaj ?work_order= filter u get_queryset
(jedini razlog zasto putni nalog nije mogao imati vise taskova bio je
nedostatak backend filtera; model uvijek podrzavao vise taskova)
- fleet/serializers.py: ukloni current_mileage iz read_only_fields
kako bi se moglo azurirati putem PATCH /fleet/cranes/{id}/
- fleetDashboardStore.js: dodaj fetchTasksByWorkOrder(workOrderId)
koji dohvaca GET tasks/tasks/?work_order={id}
- WorkOrderDetailModal.jsx:
* import TaskCreateModal, fetchTasksByWorkOrder, updateVehicle, getStatusLabel
* nova sekcija 'Zadaci putnog naloga' s popisom i gumbom '+ Dodaj zadatak'
* gumb '🔧 Izmijeni podatke dizalice' otvara CraneDataModal
* CraneDataModal: unos superstructure_working_hours, chassis_working_hours,
current_mileage; poziva updateVehicle; prikazuje trenutne vrijednosti
kao placeholder
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={openCraneDataModal}
|
||||
className="rounded-md border border-border-hairline px-3 py-2 text-xs font-medium text-text-main hover:bg-canvas-deep"
|
||||
>
|
||||
🔧 Izmijeni podatke dizalice
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
@@ -540,6 +634,33 @@ export default function WorkOrderDetailModal({ open, workOrder, onClose, onSubmi
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border border-border-hairline bg-canvas-base p-3">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<h4 className="text-sm font-semibold text-text-main">Zadaci putnog naloga ({workOrderTasks.length})</h4>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => 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
|
||||
</button>
|
||||
</div>
|
||||
{workOrderTasks.length === 0 ? (
|
||||
<p className="text-xs text-text-muted">Nema zadataka za ovaj putni nalog.</p>
|
||||
) : (
|
||||
<ul className="space-y-1">
|
||||
{workOrderTasks.map((task) => (
|
||||
<li key={task.id} className="flex items-center justify-between rounded-lg bg-canvas-deep px-3 py-2 text-sm">
|
||||
<span className="font-medium text-text-main">{task.title}</span>
|
||||
<span className="rounded-full bg-canvas-base px-2 py-0.5 text-[11px] text-text-muted border border-border-hairline">
|
||||
{getStatusLabel(task.status)}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border border-border-hairline bg-canvas-base p-3">
|
||||
<h4 className="mb-2 text-sm font-semibold text-text-main">Status</h4>
|
||||
<label className="mb-3 flex flex-col gap-1 text-sm">
|
||||
@@ -1165,6 +1286,99 @@ export default function WorkOrderDetailModal({ open, workOrder, onClose, onSubmi
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<TaskCreateModal
|
||||
open={addTaskModalOpen}
|
||||
workOrders={[workOrder]}
|
||||
contextCrane={craneVehicle || { id: craneVehicleId, registration_number: craneLabel }}
|
||||
onClose={() => setAddTaskModalOpen(false)}
|
||||
onSuccess={async () => {
|
||||
const tasks = await fetchTasksByWorkOrder(workOrder.id).catch(() => []);
|
||||
setWorkOrderTasks(tasks);
|
||||
}}
|
||||
/>
|
||||
{craneDataModalOpen && (
|
||||
<ModalShell
|
||||
onClose={() => 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"
|
||||
>
|
||||
<div>
|
||||
<div className="flex items-center justify-between border-b border-border-hairline px-4 py-3">
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold text-text-main">Izmijeni podatke dizalice</h4>
|
||||
<p className="text-xs text-text-muted">{craneLabel}</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setCraneDataModalOpen(false)}
|
||||
className="rounded-md px-2 py-1 text-sm text-text-muted hover:bg-canvas-deep"
|
||||
aria-label="Zatvori"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
<div className="space-y-4 px-4 py-4">
|
||||
<label className="flex flex-col gap-1 text-sm">
|
||||
<span className="font-medium text-text-main">Radni sati superstrukture</span>
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
value={craneDataForm.superstructure_working_hours}
|
||||
onInput={(e) => 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')}
|
||||
/>
|
||||
</label>
|
||||
<label className="flex flex-col gap-1 text-sm">
|
||||
<span className="font-medium text-text-main">Radni sati podvozja</span>
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
value={craneDataForm.chassis_working_hours}
|
||||
onInput={(e) => 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')}
|
||||
/>
|
||||
</label>
|
||||
<label className="flex flex-col gap-1 text-sm">
|
||||
<span className="font-medium text-text-main">Prijeđeni kilometri dizalice</span>
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
value={craneDataForm.current_mileage}
|
||||
onInput={(e) => 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')}
|
||||
/>
|
||||
</label>
|
||||
{craneDataError && (
|
||||
<div className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700">
|
||||
{craneDataError}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex justify-end gap-2 border-t border-border-hairline px-4 py-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => 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
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSaveCraneData}
|
||||
disabled={savingCraneData}
|
||||
className="rounded-md bg-indigo-600 px-3 py-2 text-xs font-semibold text-white hover:bg-indigo-700 disabled:opacity-60"
|
||||
>
|
||||
{savingCraneData ? 'Spremam…' : 'Spremi podatke'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</ModalShell>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
|
||||
Reference in New Issue
Block a user