fix: sync multi-day work hours accounting
Some checks failed
ERP CI/CD Pipeline / test (push) Has been cancelled
ERP CI/CD Pipeline / Deploy (server git pull + compose) (push) Has been cancelled

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.
This commit is contained in:
mariomitte
2026-08-08 01:32:17 +02:00
parent 38f1786469
commit 181cab1f45
5 changed files with 362 additions and 87 deletions

View File

@@ -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/',

View File

@@ -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',

View File

@@ -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

View File

@@ -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",

View File

@@ -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 [];