diff --git a/backend/modules/fleet/views.py b/backend/modules/fleet/views.py index 50a92ca..77e6881 100644 --- a/backend/modules/fleet/views.py +++ b/backend/modules/fleet/views.py @@ -900,17 +900,25 @@ def _build_work_order_service_records_pdf(work_order): FOOTER_H = 72 MARGIN = 28 + related_tasks = list(_work_order_related_tasks_queryset(work_order)) + # Collect service records for all task vehicles (supports cross-crane work orders) + task_vehicle_ids = list({task.vehicle_id for task in related_tasks if task.vehicle_id}) service_rows = list( VehicleServiceRecord.objects.filter( is_active=True, - vehicle_id=work_order.vehicle_id, + vehicle_id__in=task_vehicle_ids if task_vehicle_ids else [work_order.vehicle_id], ) .select_related('performed_by', 'task') .prefetch_related('photos') .order_by('service_date', 'created_at') ) - related_tasks = list(_work_order_related_tasks_queryset(work_order)) - notes_text = str(work_order.notes or '').strip() + # Per-task notes; fall back to work_order.notes + task_note_parts = [ + str(task.service_report_note or '').strip() + for task in related_tasks + if str(task.service_report_note or '').strip() + ] + notes_text = "\n".join(task_note_parts) if task_note_parts else str(work_order.notes or '').strip() buffer = BytesIO() @@ -1812,9 +1820,8 @@ def _build_work_order_service_records_docx_bytes(work_order): client_name = getattr(getattr(vehicle, 'client', None), 'name', None) or '-' servicer_name = _user_display_name(work_order.creator) or '-' completion_label = "Da ☒ Ne ☐" if work_order.status == 'closed' else "Da ☐ Ne ☒" - notes_text = str(work_order.notes or '').strip() or '-' - related_tasks = list(_work_order_related_tasks_queryset(work_order)) + related_tasks = list(_work_order_related_tasks_queryset(work_order).select_related('work_hours_table')) normalized_rows = [] for task in related_tasks: table_data = getattr(getattr(task, 'work_hours_table', None), 'data', None) @@ -1847,10 +1854,13 @@ def _build_work_order_service_records_docx_bytes(work_order): 'vehicle_km': str(row.get('vehicle_km', '') or '').strip() or '-', }) + # Collect service records per task, filtering by each task's own vehicle + # (supports cross-crane work orders where each task may have a different crane). + task_vehicle_ids = list({task.vehicle_id for task in related_tasks if task.vehicle_id}) service_rows = list( VehicleServiceRecord.objects.filter( is_active=True, - vehicle_id=work_order.vehicle_id, + vehicle_id__in=task_vehicle_ids, ) .select_related('performed_by', 'task') .prefetch_related('photos') @@ -1861,6 +1871,14 @@ def _build_work_order_service_records_docx_bytes(work_order): for task in related_tasks } + # Build notes: use per-task service_report_note; fall back to work_order.notes + task_note_parts = [ + str(task.service_report_note or '').strip() + for task in related_tasks + if str(task.service_report_note or '').strip() + ] + notes_text = "\n".join(task_note_parts) if task_note_parts else (str(work_order.notes or '').strip() or '-') + if len(doc.tables) >= 4: info_table = doc.tables[0] _set_docx_cell_text(info_table, 1, 0, client_name) @@ -1893,7 +1911,6 @@ def _build_work_order_service_records_docx_bytes(work_order): target_row.cells[3].text = entry['changed_at'] details_table = doc.tables[3] - issue_text = str(work_order.notes or '-').strip() or '-' repair_lines = [] for task in related_tasks: task_records = records_by_task.get(task.id, []) @@ -1904,7 +1921,7 @@ def _build_work_order_service_records_docx_bytes(work_order): description = str(record.description or '-').strip() or '-' repair_lines.append(f"- {description}") repair_text = "\n".join(repair_lines) if repair_lines else '-' - _set_docx_cell_text(details_table, 0, 0, f"Kvar: {issue_text}") + _set_docx_cell_text(details_table, 0, 0, f"Kvar: {notes_text}") _set_docx_cell_text(details_table, 1, 0, f"Popravak: {repair_text}") else: raise DRFValidationError({"detail": "DOCX template ima neočekivanu strukturu (nedostaju tablice)."}) @@ -2922,7 +2939,10 @@ class WorkOrderViewSet(viewsets.ModelViewSet): payload = [] for task in tasks: records_payload = [] - for record in task.service_records.filter(is_active=True, vehicle_id=work_order.vehicle_id).all(): + # Filter records by the task's own vehicle (not work_order.vehicle) to support + # cross-crane work orders where each task may belong to a different crane. + task_vehicle_id = task.vehicle_id + for record in task.service_records.filter(is_active=True, vehicle_id=task_vehicle_id).all(): photos_payload = [] for photo in record.photos.filter(is_active=True).all(): if not photo.image: @@ -2950,6 +2970,7 @@ class WorkOrderViewSet(viewsets.ModelViewSet): 'title': task.title, 'status': task.status, 'description': task.description, + 'service_report_note': task.service_report_note or '', 'assigned_to_name': _user_display_name(task.assigned_to), 'vehicle_registration': getattr(task.vehicle, 'registration_number', None), 'work_hours_table': getattr(getattr(task, 'work_hours_table', None), 'data', None), diff --git a/backend/modules/task_management/migrations/0012_task_service_report_note.py b/backend/modules/task_management/migrations/0012_task_service_report_note.py new file mode 100644 index 0000000..1c0a704 --- /dev/null +++ b/backend/modules/task_management/migrations/0012_task_service_report_note.py @@ -0,0 +1,21 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('task_management', '0011_taskcalendar'), + ] + + operations = [ + migrations.AddField( + model_name='task', + name='service_report_note', + field=models.TextField( + blank=True, + default='', + verbose_name='Napomena izvještaja servisera', + help_text='Tekst napomene za PDF izvještaj servisera za ovaj zadatak.', + ), + ), + ] diff --git a/backend/modules/task_management/models.py b/backend/modules/task_management/models.py index b88cae2..8a220e4 100644 --- a/backend/modules/task_management/models.py +++ b/backend/modules/task_management/models.py @@ -54,6 +54,12 @@ class Task(BaseModel): verbose_name=_("Planirani datum izvršenja"), help_text=_("Datum kada je planirano izvršenje radnog zadatka."), ) + service_report_note = models.TextField( + blank=True, + default='', + verbose_name=_("Napomena izvještaja servisera"), + help_text=_("Tekst napomene za PDF izvještaj servisera za ovaj zadatak."), + ) class Meta: ordering = ['-created_at'] diff --git a/backend/modules/task_management/serializers.py b/backend/modules/task_management/serializers.py index be87226..1fb2ba9 100644 --- a/backend/modules/task_management/serializers.py +++ b/backend/modules/task_management/serializers.py @@ -103,6 +103,7 @@ class TaskSerializer(serializers.ModelSerializer): 'vehicle_owner_name', 'vehicle_asset_type', 'crane_serial_number', 'work_order', 'work_order_label', 'scheduled_date', + 'service_report_note', 'template_id', 'auto_close_work_order', 'created_at', ] @@ -114,6 +115,7 @@ class TaskSerializer(serializers.ModelSerializer): 'work_order': {'required': False, 'allow_null': True}, 'vehicle': {'required': False, 'allow_null': True}, 'scheduled_date': {'required': False, 'allow_null': True}, + 'service_report_note': {'required': False, 'allow_blank': True}, } def validate_title(self, value): diff --git a/frontend/src/components/dashboard/TaskWorkHoursTableModal.jsx b/frontend/src/components/dashboard/TaskWorkHoursTableModal.jsx index 983656d..b0a0627 100644 --- a/frontend/src/components/dashboard/TaskWorkHoursTableModal.jsx +++ b/frontend/src/components/dashboard/TaskWorkHoursTableModal.jsx @@ -137,6 +137,7 @@ const FB_MUTED = [ export default function TaskWorkHoursTableModal({ open, task, + workOrder = null, defaultLocation = '', saving = false, onClose, @@ -337,6 +338,39 @@ export default function TaskWorkHoursTableModal({
+ {/* ── read-only work order travel data ── */} + {workOrder && ( +
+

Podaci iz putnog naloga (samo za prikaz)

+
+ {workOrder.origin_location && ( +
Ishodište: {workOrder.origin_location}
+ )} + {workOrder.location && ( +
Odredište: {workOrder.location}
+ )} + {workOrder.travel_start_at && ( +
Polazak: {new Date(workOrder.travel_start_at).toLocaleString('hr-HR', { dateStyle: 'short', timeStyle: 'short' })}
+ )} + {workOrder.travel_end_at && ( +
Povratak: {new Date(workOrder.travel_end_at).toLocaleString('hr-HR', { dateStyle: 'short', timeStyle: 'short' })}
+ )} + {workOrder.start_mileage != null && ( +
KM početak: {workOrder.start_mileage}
+ )} + {workOrder.end_mileage != null && ( +
KM kraj: {workOrder.end_mileage}
+ )} + {workOrder.distance != null && ( +
Prijeđeno km: {workOrder.distance}
+ )} + {workOrder.servicer_vehicle_registration && ( +
Vozilo servisera: {workOrder.servicer_vehicle_registration} {workOrder.servicer_vehicle_make_model || ''}
+ )} +
+
+ )} + {/* ── info banner ── */}
Put (od–do): unesi Put od/do → Sati puta se računaju automatski; Km vozila se popunjava ručno; polja Vr. rada su blokirana. diff --git a/frontend/src/components/dashboard/WorkOrderInvoicesPdfPage.jsx b/frontend/src/components/dashboard/WorkOrderInvoicesPdfPage.jsx index 9bec321..fbf02b5 100644 --- a/frontend/src/components/dashboard/WorkOrderInvoicesPdfPage.jsx +++ b/frontend/src/components/dashboard/WorkOrderInvoicesPdfPage.jsx @@ -12,13 +12,12 @@ import { fetchWorkOrderInvoices, fetchWorkOrderTaskServiceContext, updateTaskWorkHoursTable, - updateWorkOrder, + updateTaskServiceReportNote, updateWorkOrderAdditionalCostsTable, } from '../../stores/fleetDashboardStore'; import { $accessToken, $authReady, hydrateAuthFromStorage } from '../../stores/authStore'; import { fetchNotifications, connectNotifications } from '../../stores/notificationStore'; import { formatWorkOrderDisplayCode } from '../../lib/displayIds'; -import { showToast } from '../../stores/toastStore'; import { useAuthenticatedMediaSources } from './WorkOrderImageCarousel'; import TaskWorkHoursTableModal from './TaskWorkHoursTableModal'; import WorkOrderAdditionalCostsModal from './WorkOrderAdditionalCostsModal'; @@ -155,9 +154,9 @@ export default function WorkOrderInvoicesPdfPage() { const [additionalCostsTable, setAdditionalCostsTable] = useState({ data: { rows: [] }, total_for_payout: '0.00' }); const [editingAdditionalCosts, setEditingAdditionalCosts] = useState(false); const [savingAdditionalCosts, setSavingAdditionalCosts] = useState(false); - const [editingServiceReportNote, setEditingServiceReportNote] = useState(false); - const [savingServiceReportNote, setSavingServiceReportNote] = useState(false); - const [serviceReportNoteError, setServiceReportNoteError] = useState(''); + const [editingTaskNote, setEditingTaskNote] = useState(null); + const [savingTaskNote, setSavingTaskNote] = useState(false); + const [taskNoteError, setTaskNoteError] = useState(''); const [invoiceDetailModal, setInvoiceDetailModal] = useState(null); useEffect(() => { @@ -277,6 +276,29 @@ export default function WorkOrderInvoicesPdfPage() { } }; + const handleSaveTaskServiceReportNote = async (note) => { + if (!editingTaskNote?.id) return; + setSavingTaskNote(true); + setTaskNoteError(''); + try { + const updated = await updateTaskServiceReportNote(editingTaskNote.id, note); + setTaskContext((prev) => ({ + ...prev, + tasks: (Array.isArray(prev.tasks) ? prev.tasks : []).map((task) => ( + String(task.id) === String(editingTaskNote.id) + ? { ...task, service_report_note: updated?.service_report_note ?? note } + : task + )), + })); + setEditingTaskNote(null); + } catch (err) { + const message = err?.message || 'Spremanje napomene nije uspjelo.'; + setTaskNoteError(message); + } finally { + setSavingTaskNote(false); + } + }; + const handleSaveAdditionalCostsTable = async (tableData) => { if (!workOrderId) return; setSavingAdditionalCosts(true); @@ -289,24 +311,6 @@ export default function WorkOrderInvoicesPdfPage() { } }; - const handleSaveServiceReportNote = async (notes) => { - if (!workOrderId) return; - setSavingServiceReportNote(true); - setServiceReportNoteError(''); - try { - const updated = await updateWorkOrder(workOrderId, { notes: String(notes || '') }); - setWorkOrder(updated); - setEditingServiceReportNote(false); - showToast('Napomena za izvještaj servisera je spremljena.', 'success'); - } catch (err) { - const message = err?.message || 'Spremanje napomene nije uspjelo.'; - setServiceReportNoteError(message); - showToast(message, 'error'); - } finally { - setSavingServiceReportNote(false); - } - }; - return (
@@ -340,26 +344,6 @@ export default function WorkOrderInvoicesPdfPage() {

Servisni zapisi

- - + +
-
+
{!Array.isArray(task.service_records) || task.service_records.length === 0 ? (

Nema povezanih servisnih zapisa.

) : ( @@ -517,6 +524,7 @@ export default function WorkOrderInvoicesPdfPage() { setEditingTask(null)} @@ -531,15 +539,15 @@ export default function WorkOrderInvoicesPdfPage() { onSave={handleSaveAdditionalCostsTable} /> { - setEditingServiceReportNote(false); - setServiceReportNoteError(''); + setEditingTaskNote(null); + setTaskNoteError(''); }} - onSave={handleSaveServiceReportNote} + onSave={handleSaveTaskServiceReportNote} /> {invoiceDetailModal && (

UREDI TEKST NAPOMENE

- Tekst se prikazuje u PDF-u "Izvještaj servisera" na prvoj stranici. + {task?.title + ? `Zadatak: ${task.title}` + : 'Tekst se prikazuje u PDF-u "Izvještaj servisera".'}