From 181cab1f455ff68ebca80d9deeea621e791d0450 Mon Sep 17 00:00:00 2001 From: mariomitte Date: Sat, 8 Aug 2026 01:32:17 +0200 Subject: [PATCH] fix: sync multi-day work hours accounting Aggregate task work-hour rows into travel-cost calculations and monthly servicer reporting, and keep the frontend calendar aligned with the task work-hours source. --- .../tests/test_work_order_images_endpoint.py | 70 +++++- backend/modules/fleet/views.py | 210 +++++++++++++----- .../modules/task_management/serializers.py | 9 +- .../task_management/tests/test_serializers.py | 18 +- .../dashboard/TaskCalendarWidget.jsx | 142 +++++++++--- 5 files changed, 362 insertions(+), 87 deletions(-) diff --git a/backend/modules/fleet/tests/test_work_order_images_endpoint.py b/backend/modules/fleet/tests/test_work_order_images_endpoint.py index 3a7a563..4701fc3 100644 --- a/backend/modules/fleet/tests/test_work_order_images_endpoint.py +++ b/backend/modules/fleet/tests/test_work_order_images_endpoint.py @@ -24,8 +24,9 @@ from modules.fleet.models import ( GeneratedFleetArchive, VehicleNotification, ) -from modules.task_management.models import Task +from modules.task_management.models import Task, TaskWorkHoursTable from modules.fleet.tasks import build_monthly_archive_cached_task +from modules.fleet.views import _build_monthly_servicer_report_rows def create_test_image(filename='test.jpg', size=(40, 40), color='red'): @@ -467,6 +468,73 @@ class WorkOrderImagesEndpointTests(TestCase): self.assertIn('MT150726.work-order.pdf', names) self.assertTrue(any(name.startswith('Racuni/MT150726/') for name in names), names) + def test_work_order_pdf_travel_expense_uses_task_work_hours_table(self): + TaskWorkHoursTable.objects.create( + task=self.task, + data={ + 'rows': [ + { + 'date': '24.12.2033', + 'work_time_from': '08:00', + 'work_time_to': '16:00', + 'travel_hours': '2', + 'work_hours': '6', + }, + { + 'date': '25.12.2033', + 'work_time_from': '09:00', + 'work_time_to': '15:00', + 'travel_hours': '1,5', + 'work_hours': '4', + }, + ] + }, + ) + + response = self.client.get(f'/api/fleet/work-orders/{self.work_order.pk}/pdf/') + self.assertEqual(response.status_code, 200, response.content) + self.assertEqual(response['Content-Type'], 'application/pdf') + + reader = PdfReader(BytesIO(response.content)) + text = '\n'.join((page.extract_text() or '') for page in reader.pages) + self.assertIn('24.12.2033.', text) + self.assertIn('25.12.2033.', text) + self.assertIn('13,5', text) + self.assertIn('0,6', text) + + def test_monthly_servicer_report_rows_use_task_work_hours_table_hours(self): + TaskWorkHoursTable.objects.create( + task=self.task, + data={ + 'rows': [ + { + 'date': '24.12.2033', + 'work_time_from': '08:00', + 'work_time_to': '16:00', + 'travel_hours': '2', + 'work_hours': '6', + }, + { + 'date': '25.12.2033', + 'work_time_from': '09:00', + 'work_time_to': '15:00', + 'travel_hours': '1,5', + 'work_hours': '4', + }, + ] + }, + ) + + rows = _build_monthly_servicer_report_rows(self.user, 2033, 12) + row_by_date = {row['date']: row for row in rows if row['source'] == 'task'} + self.assertIn(date(2033, 12, 24), row_by_date) + self.assertIn(date(2033, 12, 25), row_by_date) + self.assertEqual(row_by_date[date(2033, 12, 24)]['redovan_rad'], '8') + self.assertEqual(row_by_date[date(2033, 12, 24)]['prekovremeni'], '8') + self.assertEqual(row_by_date[date(2033, 12, 25)]['redovan_rad'], '8') + self.assertEqual(row_by_date[date(2033, 12, 25)]['prekovremeni'], '5.5') + + def test_monthly_service_tasks_archive_request_creates_ready_download_with_notification(self): response = self.client.post( '/api/fleet/reports/monthly-service-tasks-archive-request/', diff --git a/backend/modules/fleet/views.py b/backend/modules/fleet/views.py index 9f8f161..dcf4ed5 100644 --- a/backend/modules/fleet/views.py +++ b/backend/modules/fleet/views.py @@ -9,7 +9,7 @@ import threading import zipfile import re from collections import OrderedDict -from datetime import timedelta +from datetime import datetime, timedelta from pathlib import Path from decimal import Decimal, InvalidOperation from django.contrib.auth import get_user_model @@ -162,6 +162,110 @@ def _work_order_related_tasks_queryset(work_order): 'assigned_to', 'vehicle', 'work_order', 'work_hours_table' ).order_by('-created_at') + +def _parse_report_date_value(value): + text = str(value or '').strip() + if not text: + return None + for fmt in ('%Y-%m-%d', '%d.%m.%Y.', '%d.%m.%Y'): + try: + return datetime.strptime(text, fmt).date() + except ValueError: + continue + return None + + +def _parse_report_time_value(value): + text = str(value or '').strip() + if not text: + return None + for fmt in ('%H:%M', '%H:%M:%S'): + try: + return datetime.strptime(text, fmt).time() + except ValueError: + continue + return None + + +def _parse_report_decimal_value(value): + if value in (None, ''): + return Decimal('0.00') + try: + return Decimal(str(value).strip().replace(',', '.')) + except (InvalidOperation, TypeError, ValueError): + return Decimal('0.00') + + +def _task_work_hours_entries(task): + table_data = getattr(getattr(task, 'work_hours_table', None), 'data', None) + rows = table_data.get('rows', []) if isinstance(table_data, dict) else [] + if not isinstance(rows, list): + rows = [] + + entries = [] + work_order = getattr(task, 'work_order', None) + fallback_date = getattr(task, 'scheduled_date', None) + task_vehicle = getattr(task, 'vehicle', None) + task_client = getattr(task_vehicle, 'client', None) + + for raw_row in rows: + if not isinstance(raw_row, dict): + continue + row_date = _parse_report_date_value(raw_row.get('date')) or fallback_date + if row_date is None: + continue + + start_text = str(raw_row.get('work_time_from') or raw_row.get('travel_time_from') or '').strip() + end_text = str(raw_row.get('work_time_to') or raw_row.get('travel_time_to') or '').strip() + start_time = _parse_report_time_value(start_text) + end_time = _parse_report_time_value(end_text) + start_dt = datetime.combine(row_date, start_time) if start_time else None + end_dt = datetime.combine(row_date, end_time) if end_time else None + if start_dt and end_dt and end_dt <= start_dt: + end_dt += timedelta(days=1) + + work_hours = _parse_report_decimal_value(raw_row.get('work_hours')) + travel_hours = _parse_report_decimal_value(raw_row.get('travel_hours')) + entries.append({ + 'date': row_date, + 'date_label': row_date.strftime('%d.%m.%Y.'), + 'start_dt': start_dt, + 'end_dt': end_dt, + 'work_hours': work_hours, + 'travel_hours': travel_hours, + 'total_hours': work_hours + travel_hours, + 'title': str(getattr(task, 'title', '') or '').strip(), + 'serial': str(getattr(task_vehicle, 'crane_serial_number', '') or '').strip(), + 'client': str(getattr(task_client, 'name', '') or '').strip(), + 'location': str(getattr(work_order, 'location', '') or '').strip(), + 'work_order_label': str(getattr(work_order, 'display_code', '') or '').strip(), + }) + + if entries: + return entries + + if work_order and work_order.travel_start_at and work_order.travel_end_at and work_order.travel_end_at > work_order.travel_start_at: + start_dt = timezone.localtime(work_order.travel_start_at) + end_dt = timezone.localtime(work_order.travel_end_at) + fallback_date = fallback_date or start_dt.date() + travel_hours = Decimal(str((end_dt - start_dt).total_seconds() / 3600.0)).quantize(Decimal('0.01')) + return [{ + 'date': fallback_date, + 'date_label': fallback_date.strftime('%d.%m.%Y.') if fallback_date else '-', + 'start_dt': start_dt, + 'end_dt': end_dt, + 'work_hours': Decimal('0.00'), + 'travel_hours': travel_hours, + 'total_hours': travel_hours, + 'title': str(getattr(task, 'title', '') or '').strip(), + 'serial': str(getattr(task_vehicle, 'crane_serial_number', '') or '').strip(), + 'client': str(getattr(task_client, 'name', '') or '').strip(), + 'location': str(getattr(work_order, 'location', '') or '').strip(), + 'work_order_label': str(getattr(work_order, 'display_code', '') or '').strip(), + }] + + return [] + def _can_access_service_record(user, service_record): service_record_id = getattr(service_record, 'pk', service_record) return _service_records_queryset_for_user(user).filter(pk=service_record_id).exists() @@ -600,10 +704,32 @@ def _build_work_order_pdf(work_order): ) creator_residence = (getattr(creator, 'residence', None) or '').strip() or "-" creator_work_position = (getattr(creator, 'work_position', None) or '').strip() or creator_occupation + related_tasks = list(_work_order_related_tasks_queryset(work_order)) + trip_entries = [] + for task in related_tasks: + trip_entries.extend(_task_work_hours_entries(task)) + travel_start = work_order.travel_start_at travel_end = work_order.travel_end_at + trip_start_date = travel_start.date() if travel_start else getattr(work_order, 'date', None) + trip_end_date = travel_end.date() if travel_end else getattr(work_order, 'date', None) travel_hours = _hours_between(travel_start, travel_end) - daily_qty = round(travel_hours / 8.0, 1) if travel_hours > 0 else 0.0 + + if trip_entries: + entry_dates = [entry['date'] for entry in trip_entries if entry.get('date')] + start_candidates = [entry['start_dt'] for entry in trip_entries if entry.get('start_dt')] + end_candidates = [entry['end_dt'] for entry in trip_entries if entry.get('end_dt')] + total_hours = sum((entry['total_hours'] for entry in trip_entries), Decimal('0.00')) + travel_hours = float(total_hours) + if entry_dates: + trip_start_date = min(entry_dates) + trip_end_date = max(entry_dates) + if start_candidates: + travel_start = min(start_candidates) + if end_candidates: + travel_end = max(end_candidates) + + daily_qty = round(travel_hours / 24.0, 1) if travel_hours > 0 else 0.0 daily_rate = 0.0 daily_total = daily_qty * daily_rate transport_total = float(work_order.servicer_vehicle_fuel_cost or 0.0) @@ -785,9 +911,9 @@ def _build_work_order_pdf(work_order): ["OBRAČUN PUTNIH TROŠKOVA", "", "", "", "", "", "", ""], ["ODLAZAK Datum", "ODLAZAK Vrijeme", "POVRATAK Datum", "POVRATAK Vrijeme", "Broj sati", "Količina dnevnica", "Iznos dnevnice", "Ukupan iznos"], [ - _fmt_date(travel_start.date() if travel_start else work_order.date), + _fmt_date(trip_start_date or work_order.date), _fmt_time(travel_start), - _fmt_date(travel_end.date() if travel_end else work_order.date), + _fmt_date(trip_end_date or work_order.date), _fmt_time(travel_end), str(travel_hours).replace('.', ','), str(daily_qty).replace('.', ','), @@ -2348,6 +2474,7 @@ def _build_monthly_servicer_report_rows(user, year, month): 'work_order', 'work_order__vehicle', 'work_order__vehicle__client', + 'work_hours_table', ) .order_by('scheduled_date', 'created_at') ) @@ -2358,9 +2485,10 @@ def _build_monthly_servicer_report_rows(user, year, month): entry_date__month=month, ).order_by('entry_date') - tasks_by_date = OrderedDict() + entries_by_date = OrderedDict() for task in tasks_qs: - tasks_by_date.setdefault(task.scheduled_date, []).append(task) + for entry in _task_work_hours_entries(task): + entries_by_date.setdefault(entry['date'], []).append(entry) manual_by_date = { entry.entry_date: entry @@ -2371,8 +2499,8 @@ def _build_monthly_servicer_report_rows(user, year, month): days_in_month = monthrange(year, month)[1] for day in range(1, days_in_month + 1): current_date = _date(year, month, day) - day_tasks = tasks_by_date.get(current_date, []) - if day_tasks: + day_entries = entries_by_date.get(current_date, []) + if day_entries: titles = OrderedDict() serials = OrderedDict() clients = OrderedDict() @@ -2380,53 +2508,31 @@ def _build_monthly_servicer_report_rows(user, year, month): work_orders = OrderedDict() start_values = [] end_values = [] - counted_work_order_ids = set() - total_hours = 0.0 + total_hours = Decimal('0.00') - for task in day_tasks: - title = str(task.title or '').strip() - if title: - titles[title] = title - - vehicle = getattr(task, 'vehicle', None) - work_order = getattr(task, 'work_order', None) - if work_order and getattr(work_order, 'vehicle', None): - vehicle = work_order.vehicle - - serial_value = str(getattr(vehicle, 'crane_serial_number', '') or '').strip() - if serial_value: - serials[serial_value] = serial_value - - client_name = str(getattr(getattr(vehicle, 'client', None), 'name', '') or '').strip() - if client_name: - clients[client_name] = client_name - - location = str(getattr(work_order, 'location', '') or '').strip() - if location: - locations[location] = location - - display_code = str(getattr(work_order, 'display_code', '') or '').strip() - if display_code: - work_orders[display_code] = display_code - - if work_order and work_order.travel_start_at and work_order.travel_end_at and work_order.travel_end_at > work_order.travel_start_at and str(work_order.id) not in counted_work_order_ids: - counted_work_order_ids.add(str(work_order.id)) - start_values.append((work_order.id, timezone.localtime(work_order.travel_start_at))) - end_values.append((work_order.id, timezone.localtime(work_order.travel_end_at))) - total_hours += (work_order.travel_end_at - work_order.travel_start_at).total_seconds() / 3600.0 + for entry in day_entries: + if entry.get('title'): + titles[entry['title']] = entry['title'] + if entry.get('serial'): + serials[entry['serial']] = entry['serial'] + if entry.get('client'): + clients[entry['client']] = entry['client'] + if entry.get('location'): + locations[entry['location']] = entry['location'] + if entry.get('work_order_label'): + work_orders[entry['work_order_label']] = entry['work_order_label'] + if entry.get('start_dt'): + start_values.append(entry['start_dt']) + if entry.get('end_dt'): + end_values.append(entry['end_dt']) + total_hours += entry.get('total_hours', Decimal('0.00')) start_label = '-' end_label = '-' if start_values: - start_label = min(value for _, value in start_values).strftime('%H:%M') + start_label = min(start_values).strftime('%H:%M') if end_values: - end_label = max(value for _, value in end_values).strftime('%H:%M') - - regular_hours = '-' - overtime_hours = '0' - if total_hours > 0: - regular_hours = _format_report_hours(min(total_hours, 8.0), default='0') - overtime_hours = _format_report_hours(max(total_hours - 8.0, 0.0), default='0') + end_label = max(end_values).strftime('%H:%M') rows.append({ 'date': current_date, @@ -2437,8 +2543,8 @@ def _build_monthly_servicer_report_rows(user, year, month): 'mjesto_rada': ', '.join(locations.values()) or '-', 'pocetak_rada': start_label, 'kraj_rada': end_label, - 'redovan_rad': regular_hours, - 'prekovremeni': overtime_hours, + 'redovan_rad': '8', + 'prekovremeni': _format_report_hours(total_hours, default='0'), 'radni_nalog': ', '.join(value for value in work_orders.values() if value) or '-', 'source': 'task', }) @@ -2471,7 +2577,7 @@ def _build_monthly_servicer_report_rows(user, year, month): 'mjesto_rada': '-', 'pocetak_rada': '-', 'kraj_rada': '-', - 'redovan_rad': '-', + 'redovan_rad': '0', 'prekovremeni': '0', 'radni_nalog': '-', 'source': 'empty', diff --git a/backend/modules/task_management/serializers.py b/backend/modules/task_management/serializers.py index 1fb2ba9..e3cf646 100644 --- a/backend/modules/task_management/serializers.py +++ b/backend/modules/task_management/serializers.py @@ -91,6 +91,7 @@ class TaskSerializer(serializers.ModelSerializer): vehicle_owner_name = serializers.SerializerMethodField() vehicle_asset_type = serializers.SerializerMethodField() crane_serial_number = serializers.SerializerMethodField() + work_hours_table = serializers.SerializerMethodField() template_id = serializers.UUIDField(write_only=True, required=False, allow_null=True) auto_close_work_order = serializers.BooleanField(write_only=True, required=False) @@ -100,7 +101,7 @@ class TaskSerializer(serializers.ModelSerializer): 'id', 'title', 'description', 'status', 'assigned_to', 'assigned_to_name', 'vehicle', 'vehicle_registration', 'vehicle_make', 'vehicle_model', - 'vehicle_owner_name', 'vehicle_asset_type', 'crane_serial_number', + 'vehicle_owner_name', 'vehicle_asset_type', 'crane_serial_number', 'work_hours_table', 'work_order', 'work_order_label', 'scheduled_date', 'service_report_note', @@ -212,3 +213,9 @@ class TaskSerializer(serializers.ModelSerializer): if not obj.vehicle_id: return None return obj.vehicle.crane_serial_number or None + + def get_work_hours_table(self, obj): + table = getattr(obj, 'work_hours_table', None) + if not table: + return None + return TaskWorkHoursTableSerializer(table).data diff --git a/backend/modules/task_management/tests/test_serializers.py b/backend/modules/task_management/tests/test_serializers.py index af6e466..7266980 100644 --- a/backend/modules/task_management/tests/test_serializers.py +++ b/backend/modules/task_management/tests/test_serializers.py @@ -2,7 +2,7 @@ from django.test import TestCase from modules.task_management.serializers import TaskSerializer from rest_framework.exceptions import ValidationError from django.contrib.auth import get_user_model -from modules.task_management.models import Task +from modules.task_management.models import Task, TaskWorkHoursTable from modules.fleet.models import Vehicle, WorkOrder from modules.crm.models import Client @@ -86,6 +86,22 @@ class TaskSerializerTests(TestCase): assert serializer.data['vehicle_owner_name'] == 'Klijent zadatka' assert serializer.data['vehicle_asset_type'] == 'crane' + def test_work_hours_table_is_included_in_serialized_output(self): + task = Task.objects.create(title="Task work hours", assigned_to=self.user, vehicle=self.vehicle) + TaskWorkHoursTable.objects.create( + task=task, + data={ + 'rows': [ + {'date': '12.07.2026', 'work_hours': '8', 'travel_hours': '2'}, + ] + }, + ) + + serializer = TaskSerializer(instance=task) + assert serializer.data['work_hours_table']['data']['rows'][0]['date'] == '12.07.2026' + assert serializer.data['work_hours_table']['data']['rows'][0]['work_hours'] == '8' + assert serializer.data['work_hours_table']['data']['rows'][0]['travel_hours'] == '2' + def test_scheduled_date_accepts_valid_date(self): serializer = TaskSerializer( data={"title": "Task mit datum", "status": "aktivan", diff --git a/frontend/src/components/dashboard/TaskCalendarWidget.jsx b/frontend/src/components/dashboard/TaskCalendarWidget.jsx index 8cfcb36..c3f871b 100644 --- a/frontend/src/components/dashboard/TaskCalendarWidget.jsx +++ b/frontend/src/components/dashboard/TaskCalendarWidget.jsx @@ -24,6 +24,10 @@ function toDateKey(value) { const text = String(value || '').trim(); if (!text) return null; if (/^\d{4}-\d{2}-\d{2}$/.test(text)) return text; + const croatian = text.match(/^(\d{2})\.(\d{2})\.(\d{4})\.?$/); + if (croatian) { + return `${croatian[3]}-${croatian[2]}-${croatian[1]}`; + } const parsed = new Date(text); if (Number.isNaN(parsed.getTime())) return null; return `${parsed.getFullYear()}-${String(parsed.getMonth() + 1).padStart(2, '0')}-${String(parsed.getDate()).padStart(2, '0')}`; @@ -77,6 +81,83 @@ function joinUnique(values) { return Array.from(new Set(values.filter(Boolean).map((item) => String(item).trim()).filter(Boolean))).join(', '); } +function parseReportHours(value) { + if (value == null || value === '') return 0; + const numeric = Number(String(value).replace(',', '.')); + return Number.isFinite(numeric) ? numeric : 0; +} + +function parseReportDateTime(dateKey, value) { + if (value == null || value === '') return null; + if (value instanceof Date) { + return new Date(value.getTime()); + } + const text = String(value).trim(); + if (!text) return null; + const timeMatch = text.match(/^(\d{1,2}):(\d{2})(?::(\d{2}))?$/); + if (dateKey && timeMatch) { + const hours = String(timeMatch[1]).padStart(2, '0'); + const minutes = timeMatch[2]; + const seconds = timeMatch[3] || '00'; + return new Date(`${dateKey}T${hours}:${minutes}:${seconds}`); + } + const parsed = new Date(text); + return Number.isNaN(parsed.getTime()) ? null : parsed; +} + +function getTaskWorkHoursEntries(task, workOrdersById) { + const tableData = task?.work_hours_table?.data ?? task?.work_hours_table ?? null; + const rows = Array.isArray(tableData?.rows) ? tableData.rows : []; + const workOrder = workOrdersById.get(String(task?.work_order || '')); + const base = { + title: task?.title || '', + serial: getTaskCraneSerial(task) || '', + client: getTaskCraneOwner(task) || '', + location: workOrder?.location || '', + workOrderLabel: task?.work_order_label || '', + }; + const entries = []; + + for (const row of rows) { + if (!row || typeof row !== 'object') continue; + const dateKey = toDateKey(row.date) || toDateKey(task?.scheduled_date); + if (!dateKey) continue; + const startAt = parseReportDateTime(dateKey, row.work_time_from || row.travel_time_from || ''); + const endAt = parseReportDateTime(dateKey, row.work_time_to || row.travel_time_to || ''); + entries.push({ + ...base, + dateKey, + startAt, + endAt, + totalHours: parseReportHours(row.work_hours) + parseReportHours(row.travel_hours), + }); + } + + if (entries.length > 0) { + return entries; + } + + const travelStart = workOrder?.travel_start_at; + const travelEnd = workOrder?.travel_end_at; + if (travelStart && travelEnd) { + const startAt = parseReportDateTime(null, travelStart); + const endAt = parseReportDateTime(null, travelEnd); + const dateKey = toDateKey(task?.scheduled_date) || toDateKey(travelStart); + const totalHours = startAt && endAt ? Math.max(0, (endAt.getTime() - startAt.getTime()) / 3600000) : 0; + if (dateKey) { + entries.push({ + ...base, + dateKey, + startAt, + endAt, + totalHours, + }); + } + } + + return entries; +} + export default function TaskCalendarWidget({ tasks = [], notes = [], workOrders = [], onTaskClick }) { const [viewYear, setViewYear] = useState(() => new Date().getFullYear()); const [viewMonth, setViewMonth] = useState(() => new Date().getMonth()); @@ -196,6 +277,17 @@ export default function TaskCalendarWidget({ tasks = [], notes = [], workOrders }; }, [reportOpen, reportType, viewYear, viewMonth]); + const reportTaskEntriesByDate = useMemo(() => { + const map = {}; + for (const task of tasks) { + for (const entry of getTaskWorkHoursEntries(task, workOrdersById)) { + if (!map[entry.dateKey]) map[entry.dateKey] = []; + map[entry.dateKey].push(entry); + } + } + return map; + }, [tasks, workOrdersById]); + const servicerRows = useMemo(() => { if (reportType !== 'servicer') return []; const rows = []; @@ -204,42 +296,28 @@ export default function TaskCalendarWidget({ tasks = [], notes = [], workOrders for (let day = 1; day <= daysInMonth; day += 1) { const key = `${monthPrefix}-${String(day).padStart(2, '0')}`; - const dayTasks = tasksByDate[key] ?? []; + const dayEntries = reportTaskEntriesByDate[key] ?? []; const manualEntry = manualEntriesByDate[key]; - if (dayTasks.length > 0) { + if (dayEntries.length > 0) { const titles = []; const serials = []; const clients = []; const locations = []; const workOrderLabels = []; - const workStartTimes = []; - const workEndTimes = []; - const countedWorkOrders = new Set(); + const startTimes = []; + const endTimes = []; let totalHours = 0; - for (const task of dayTasks) { - if (task?.title) titles.push(task.title); - serials.push(getTaskCraneSerial(task)); - clients.push(getTaskCraneOwner(task)); - if (task?.work_order_label) workOrderLabels.push(task.work_order_label); - - const workOrder = workOrdersById.get(String(task.work_order || '')); - if (workOrder?.location) locations.push(workOrder.location); - if ( - workOrder?.travel_start_at - && workOrder?.travel_end_at - && !countedWorkOrders.has(String(workOrder.id)) - ) { - const startAt = new Date(workOrder.travel_start_at); - const endAt = new Date(workOrder.travel_end_at); - if (!Number.isNaN(startAt.getTime()) && !Number.isNaN(endAt.getTime()) && endAt > startAt) { - countedWorkOrders.add(String(workOrder.id)); - workStartTimes.push(startAt); - workEndTimes.push(endAt); - totalHours += (endAt.getTime() - startAt.getTime()) / 3600000; - } - } + for (const entry of dayEntries) { + if (entry.title) titles.push(entry.title); + if (entry.serial) serials.push(entry.serial); + if (entry.client) clients.push(entry.client); + if (entry.location) locations.push(entry.location); + if (entry.workOrderLabel) workOrderLabels.push(entry.workOrderLabel); + if (entry.startAt) startTimes.push(entry.startAt); + if (entry.endAt) endTimes.push(entry.endAt); + totalHours += Number(entry.totalHours || 0); } rows.push({ @@ -252,10 +330,10 @@ export default function TaskCalendarWidget({ tasks = [], notes = [], workOrders joinUnique(serials) || '-', joinUnique(clients) || '-', joinUnique(locations) || '-', - workStartTimes.length ? formatTime(new Date(Math.min(...workStartTimes.map((item) => item.getTime()))).toISOString()) : '-', - workEndTimes.length ? formatTime(new Date(Math.max(...workEndTimes.map((item) => item.getTime()))).toISOString()) : '-', - totalHours > 0 ? formatHourValue(Math.min(totalHours, 8), '0') : '-', - totalHours > 8 ? formatHourValue(totalHours - 8, '0') : '0', + startTimes.length ? formatTime(new Date(Math.min(...startTimes.map((item) => item.getTime())))) : '-', + endTimes.length ? formatTime(new Date(Math.max(...endTimes.map((item) => item.getTime())))) : '-', + '8', + formatHourValue(totalHours, '0'), joinUnique(workOrderLabels) || '-', ], }); @@ -292,7 +370,7 @@ export default function TaskCalendarWidget({ tasks = [], notes = [], workOrders } return rows; - }, [manualEntriesByDate, reportType, tasksByDate, viewMonth, viewYear, workOrdersById]); + }, [manualEntriesByDate, reportTaskEntriesByDate, reportType, viewMonth, viewYear]); const costsRows = useMemo(() => { if (reportType !== 'costs') return [];