From fe739b14c03106860725d3a69a09f97b5cb2ac89 Mon Sep 17 00:00:00 2001 From: mariomitte Date: Fri, 31 Jul 2026 13:31:58 +0200 Subject: [PATCH] feat: dodaj mjesecni izvjestaj servisera i troskova u kalendar widget - Dva nova Django API endpointa (GET /api/fleet/reports/monthly-servicer/ i /api/fleet/reports/monthly-costs/) koji generiraju DOCX u A4 landscape formatu - TaskCalendarWidget prosiren s report panelom: dva gumba iznad kalendara (Izvjestaj servisera / Izvjestaj troskova) sirenjem drawer-a na 680 px - Preview tablice s podacima iz postojeceg tasks/workOrders store-a - downloadMonthlyServiserReport i downloadMonthlyCostsReport u store-u - TaskCalendarPortal prosljeduje activeWorkOrders prop widgetu Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- backend/modules/fleet/urls.py | 3 + backend/modules/fleet/views.py | 248 ++++++++ .../dashboard/TaskCalendarWidget.jsx | 566 ++++++++++++------ .../components/layout/TaskCalendarPortal.jsx | 1 + frontend/src/stores/fleetDashboardStore.js | 28 + 5 files changed, 672 insertions(+), 174 deletions(-) diff --git a/backend/modules/fleet/urls.py b/backend/modules/fleet/urls.py index 0c91d44..a3b7388 100644 --- a/backend/modules/fleet/urls.py +++ b/backend/modules/fleet/urls.py @@ -3,6 +3,7 @@ from django.urls import path from .views import ( CraneViewSet, VehicleViewSet, WorkOrderViewSet, VehicleServiceRecordViewSet, WorkOrderInvoiceViewSet, ServiceContextNoteViewSet, VehicleNotificationViewSet, VehicleServicePhotoViewSet, VehicleServiceAttachmentViewSet, pusher_auth, + monthly_servicer_report_docx, monthly_costs_report_docx, ) router = DefaultRouter() @@ -20,4 +21,6 @@ urlpatterns = router.urls urlpatterns += [ path('pusher-auth/', pusher_auth, name='pusher-auth'), + path('reports/monthly-servicer/', monthly_servicer_report_docx, name='monthly-servicer-report'), + path('reports/monthly-costs/', monthly_costs_report_docx, name='monthly-costs-report'), ] \ No newline at end of file diff --git a/backend/modules/fleet/views.py b/backend/modules/fleet/views.py index 0501caf..593e612 100644 --- a/backend/modules/fleet/views.py +++ b/backend/modules/fleet/views.py @@ -2108,6 +2108,254 @@ def _dispatch_work_order_email_background(task_kwargs): return 'thread' +_MONTH_NAMES_HR = [ + 'Siječanj', 'Veljača', 'Ožujak', 'Travanj', 'Svibanj', 'Lipanj', + 'Srpanj', 'Kolovoz', 'Rujan', 'Listopad', 'Studeni', 'Prosinac', +] + + +@api_view(['GET']) +@permission_classes([permissions.IsAuthenticated]) +def monthly_servicer_report_docx(request): + """Download monthly servicer report as DOCX (landscape table).""" + from datetime import date as _dt_date + try: + year = int(request.query_params.get('year', _dt_date.today().year)) + month = int(request.query_params.get('month', _dt_date.today().month)) + if not (1 <= month <= 12): + raise ValueError() + except (ValueError, TypeError): + return Response({'detail': 'Nevažeći year/month parametar.'}, status=400) + + try: + from modules.task_management.models import Task as _TaskModel + except ImportError: + return Response({'detail': 'Task model nije dostupan.'}, status=500) + + tasks_qs = ( + _TaskModel.objects + .filter( + assigned_to=request.user, + is_active=True, + scheduled_date__year=year, + scheduled_date__month=month, + ) + .select_related('work_order', 'work_order__vehicle', 'work_order__vehicle__client') + .order_by('scheduled_date', 'created_at') + ) + + rows = [] + for task in tasks_qs: + wo = task.work_order + datum = task.scheduled_date.strftime('%d.%m.%Y') if task.scheduled_date else '-' + opis_posla = task.title or '-' + br_dizalice = '-' + komitent = '-' + mjesto_rada = '-' + pocetak_rada = '-' + kraj_rada = '-' + redovan_rad = '-' + prekovremeni = '0' + radni_nalog = '-' + + if wo: + vehicle = getattr(wo, 'vehicle', None) + if vehicle: + sn = str(getattr(vehicle, 'crane_serial_number', '') or '').strip() + br_dizalice = sn or '-' + client_obj = getattr(vehicle, 'client', None) + komitent = str(getattr(client_obj, 'name', '') or '').strip() or '-' + mjesto_rada = wo.location or '-' + ts = wo.travel_start_at + te = wo.travel_end_at + if ts: + pocetak_rada = timezone.localtime(ts).strftime('%H:%M') + if te: + kraj_rada = timezone.localtime(te).strftime('%H:%M') + if ts and te and te > ts: + delta_h = (te - ts).total_seconds() / 3600.0 + reg = min(delta_h, 8.0) + ovt = max(0.0, delta_h - 8.0) + redovan_rad = f"{reg:.2f}".rstrip('0').rstrip('.') + prekovremeni = f"{ovt:.2f}".rstrip('0').rstrip('.') if ovt > 0 else '0' + radni_nalog = str(wo.display_code or '').strip() or '-' + + rows.append([datum, opis_posla, br_dizalice, komitent, mjesto_rada, + pocetak_rada, kraj_rada, redovan_rad, prekovremeni, radni_nalog]) + + from docx import Document as _DocxDoc + from docx.shared import Pt, Cm + from docx.enum.text import WD_ALIGN_PARAGRAPH + from docx.enum.section import WD_ORIENT + + doc = _DocxDoc() + for section in doc.sections: + section.orientation = WD_ORIENT.LANDSCAPE + section.page_width = Cm(29.7) + section.page_height = Cm(21.0) + section.left_margin = Cm(1.5) + section.right_margin = Cm(1.5) + section.top_margin = Cm(1.5) + section.bottom_margin = Cm(1.5) + + month_label = _MONTH_NAMES_HR[month - 1] + servicer_name = _user_display_name(request.user) or getattr(request.user, 'username', str(request.user)) + + p_title = doc.add_paragraph() + p_title.alignment = WD_ALIGN_PARAGRAPH.CENTER + r_title = p_title.add_run('MJESEČNI IZVJEŠTAJ SERVISERA') + r_title.bold = True + r_title.font.size = Pt(14) + + p_sub = doc.add_paragraph() + p_sub.alignment = WD_ALIGN_PARAGRAPH.CENTER + r_sub = p_sub.add_run(f"{servicer_name} — {month_label} {year}") + r_sub.font.size = Pt(11) + + headers = ['DATUM', 'OPIS POSLA', 'BR. DIZALICE', 'KOMITENT', 'MJESTO RADA', + 'POČETAK RADA', 'KRAJ RADA', 'REDOVAN RAD (h)', 'PREKOVREMENI (h)', 'RADNI NALOG'] + col_widths = [Cm(2.2), Cm(5.5), Cm(2.5), Cm(3.5), Cm(3.5), + Cm(2.2), Cm(2.2), Cm(2.4), Cm(2.4), Cm(2.4)] + + table = doc.add_table(rows=1 + len(rows), cols=len(headers)) + table.style = 'Table Grid' + + hdr_cells = table.rows[0].cells + for i, (hdr, w) in enumerate(zip(headers, col_widths)): + hdr_cells[i].width = w + hdr_cells[i].text = hdr + if hdr_cells[i].paragraphs[0].runs: + run_h = hdr_cells[i].paragraphs[0].runs[0] + run_h.bold = True + run_h.font.size = Pt(8) + + for ri, row_data in enumerate(rows): + data_cells = table.rows[ri + 1].cells + for ci, (val, w) in enumerate(zip(row_data, col_widths)): + data_cells[ci].width = w + data_cells[ci].text = str(val) + if data_cells[ci].paragraphs[0].runs: + data_cells[ci].paragraphs[0].runs[0].font.size = Pt(8) + + buf = BytesIO() + doc.save(buf) + buf.seek(0) + + month_key = f"{year}-{month:02d}" + safe_name = re.sub(r'[^\w\-]', '_', servicer_name) + fname = f"{safe_name}.izvjestaj-servisera.{month_key}.docx" + resp = HttpResponse( + buf.read(), + content_type='application/vnd.openxmlformats-officedocument.wordprocessingml.document', + ) + resp['Content-Disposition'] = f'attachment; filename="{fname}"' + return resp + + +@api_view(['GET']) +@permission_classes([permissions.IsAuthenticated]) +def monthly_costs_report_docx(request): + """Download monthly servicer costs (invoices) report as DOCX.""" + from datetime import date as _dt_date + try: + year = int(request.query_params.get('year', _dt_date.today().year)) + month = int(request.query_params.get('month', _dt_date.today().month)) + if not (1 <= month <= 12): + raise ValueError() + except (ValueError, TypeError): + return Response({'detail': 'Nevažeći year/month parametar.'}, status=400) + + invoices_qs = ( + WorkOrderInvoice.objects + .filter( + work_order__work_order_tasks__assigned_to=request.user, + work_order__work_order_tasks__is_active=True, + datum__year=year, + datum__month=month, + ) + .select_related('work_order') + .distinct() + .order_by('datum', 'naziv_racuna') + ) + + rows = [] + for inv in invoices_qs: + wo = inv.work_order + rows.append([ + inv.datum.strftime('%d.%m.%Y') if inv.datum else '-', + inv.naziv_racuna or '-', + inv.lokacija or '-', + inv.opis or '-', + str(getattr(wo, 'display_code', '') or '').strip() or '-', + ]) + + from docx import Document as _DocxDoc + from docx.shared import Pt, Cm + from docx.enum.text import WD_ALIGN_PARAGRAPH + from docx.enum.section import WD_ORIENT + + doc = _DocxDoc() + for section in doc.sections: + section.orientation = WD_ORIENT.LANDSCAPE + section.page_width = Cm(29.7) + section.page_height = Cm(21.0) + section.left_margin = Cm(1.5) + section.right_margin = Cm(1.5) + section.top_margin = Cm(1.5) + section.bottom_margin = Cm(1.5) + + month_label = _MONTH_NAMES_HR[month - 1] + servicer_name = _user_display_name(request.user) or getattr(request.user, 'username', str(request.user)) + + p_title = doc.add_paragraph() + p_title.alignment = WD_ALIGN_PARAGRAPH.CENTER + r_title = p_title.add_run('MJESEČNI IZVJEŠTAJ TROŠKOVA SERVISERA') + r_title.bold = True + r_title.font.size = Pt(14) + + p_sub = doc.add_paragraph() + p_sub.alignment = WD_ALIGN_PARAGRAPH.CENTER + r_sub = p_sub.add_run(f"{servicer_name} — {month_label} {year}") + r_sub.font.size = Pt(11) + + headers = ['DATUM', 'NAZIV RAČUNA', 'LOKACIJA', 'OPIS', 'RADNI NALOG'] + col_widths = [Cm(2.5), Cm(6.0), Cm(4.0), Cm(8.0), Cm(3.0)] + + table = doc.add_table(rows=1 + len(rows), cols=len(headers)) + table.style = 'Table Grid' + + hdr_cells = table.rows[0].cells + for i, (hdr, w) in enumerate(zip(headers, col_widths)): + hdr_cells[i].width = w + hdr_cells[i].text = hdr + if hdr_cells[i].paragraphs[0].runs: + run_h = hdr_cells[i].paragraphs[0].runs[0] + run_h.bold = True + run_h.font.size = Pt(9) + + for ri, row_data in enumerate(rows): + data_cells = table.rows[ri + 1].cells + for ci, (val, w) in enumerate(zip(row_data, col_widths)): + data_cells[ci].width = w + data_cells[ci].text = str(val) + if data_cells[ci].paragraphs[0].runs: + data_cells[ci].paragraphs[0].runs[0].font.size = Pt(9) + + buf = BytesIO() + doc.save(buf) + buf.seek(0) + + month_key = f"{year}-{month:02d}" + safe_name = re.sub(r'[^\w\-]', '_', servicer_name) + fname = f"{safe_name}.troskovi-servisera.{month_key}.docx" + resp = HttpResponse( + buf.read(), + content_type='application/vnd.openxmlformats-officedocument.wordprocessingml.document', + ) + resp['Content-Disposition'] = f'attachment; filename="{fname}"' + return resp + + @api_view(['POST']) @permission_classes([permissions.IsAuthenticated]) def pusher_auth(request): diff --git a/frontend/src/components/dashboard/TaskCalendarWidget.jsx b/frontend/src/components/dashboard/TaskCalendarWidget.jsx index fbfa404..599e5d1 100644 --- a/frontend/src/components/dashboard/TaskCalendarWidget.jsx +++ b/frontend/src/components/dashboard/TaskCalendarWidget.jsx @@ -1,11 +1,12 @@ -import { useMemo, useState } from 'preact/hooks'; +import { useMemo, useState } from 'preact/hooks'; import { getStatusLabel } from '../../stores/taskStore'; +import { downloadMonthlyServiserReport, downloadMonthlyCostsReport } from '../../stores/fleetDashboardStore'; import LeftSlideDrawer from '../layout/LeftSlideDrawer'; import { getTaskCraneKind, getTaskCraneOwner, getTaskCraneSerial } from '../../lib/taskCraneDisplay'; -const WEEKDAY_LABELS = ['Po', 'Ut', 'Sr', 'Če', 'Pe', 'Su', 'Ne']; +const WEEKDAY_LABELS = ['Po', 'Ut', 'Sr', 'ÄŚe', 'Pe', 'Su', 'Ne']; const MONTH_NAMES = [ - 'Siječanj', 'Veljača', 'Ožujak', 'Travanj', 'Svibanj', 'Lipanj', + 'SijeÄŤanj', 'VeljaÄŤa', 'OĹľujak', 'Travanj', 'Svibanj', 'Lipanj', 'Srpanj', 'Kolovoz', 'Rujan', 'Listopad', 'Studeni', 'Prosinac', ]; function toIsoDateKey(dateStr) { @@ -19,12 +20,25 @@ function formatDayLabel(year, month, day) { return `${String(day).padStart(2, '0')}.${String(month + 1).padStart(2, '0')}.${year}`; } -export default function TaskCalendarWidget({ tasks = [], notes = [], onTaskClick }) { +function formatTime(isoStr) { + if (!isoStr) return '-'; + const d = new Date(isoStr); + if (Number.isNaN(d.getTime())) return '-'; + return d.toLocaleTimeString('hr-HR', { hour: '2-digit', minute: '2-digit' }); +} + +const SERVICER_HEADERS = ['DATUM', 'OPIS POSLA', 'BR. DIZALICE', 'KOMITENT', 'MJESTO RADA', 'POÄŚETAK', 'KRAJ', 'REDOVAN (h)', 'PREKOVR. (h)', 'RADNI NALOG']; + +export default function TaskCalendarWidget({ tasks = [], notes = [], workOrders = [], onTaskClick }) { const [viewYear, setViewYear] = useState(() => new Date().getFullYear()); const [viewMonth, setViewMonth] = useState(() => new Date().getMonth()); const [selectedDay, setSelectedDay] = useState(null); + const [reportType, setReportType] = useState(null); // 'servicer' | 'costs' | null + const [downloading, setDownloading] = useState(false); + + const reportOpen = reportType !== null; + const panelWidth = reportOpen ? 680 : 300; - // Map: "YYYY-MM-DD" -> task[] const tasksByDate = useMemo(() => { const map = {}; for (const task of tasks) { @@ -47,11 +61,10 @@ export default function TaskCalendarWidget({ tasks = [], notes = [], onTaskClick return map; }, [notes]); - // Calendar grid cells for current month const calendarDays = useMemo(() => { const firstDay = new Date(viewYear, viewMonth, 1); - let startDow = firstDay.getDay(); // 0=Sun - startDow = (startDow + 6) % 7; // shift to Mon=0 + let startDow = firstDay.getDay(); + startDow = (startDow + 6) % 7; const daysInMonth = new Date(viewYear, viewMonth + 1, 0).getDate(); const cells = []; for (let i = 0; i < startDow; i++) cells.push(null); @@ -59,6 +72,74 @@ export default function TaskCalendarWidget({ tasks = [], notes = [], onTaskClick return cells; }, [viewYear, viewMonth]); + // Rows for servicer report preview table (current month tasks) + const servicerRows = useMemo(() => { + if (reportType !== 'servicer') return []; + return tasks + .filter((t) => { + const d = t.scheduled_date ? new Date(t.scheduled_date) : null; + return d && !Number.isNaN(d.getTime()) && d.getFullYear() === viewYear && d.getMonth() === viewMonth; + }) + .sort((a, b) => (a.scheduled_date || '').localeCompare(b.scheduled_date || '')) + .map((task) => { + const wo = workOrders.find((w) => String(w.id) === String(task.work_order)); + let start = '-', end = '-', regular = '-', overtime = '0'; + if (wo?.travel_start_at) start = formatTime(wo.travel_start_at); + if (wo?.travel_end_at) end = formatTime(wo.travel_end_at); + if (wo?.travel_start_at && wo?.travel_end_at) { + const deltaH = (new Date(wo.travel_end_at) - new Date(wo.travel_start_at)) / 3_600_000; + if (deltaH > 0) { + const reg = Math.min(deltaH, 8); + const ovt = Math.max(0, deltaH - 8); + regular = reg % 1 === 0 ? String(reg) : reg.toFixed(2); + overtime = ovt > 0 ? (ovt % 1 === 0 ? String(ovt) : ovt.toFixed(2)) : '0'; + } + } + const datum = task.scheduled_date + ? new Date(task.scheduled_date).toLocaleDateString('hr-HR') + : '-'; + return [ + datum, + task.title || '-', + getTaskCraneSerial(task), + getTaskCraneOwner(task), + wo?.location || '-', + start, + end, + regular, + overtime, + task.work_order_label || '-', + ]; + }); + }, [reportType, tasks, workOrders, viewYear, viewMonth]); + + // Costs report rows: aggregate invoices from work orders linked to tasks in current month + const costsRows = useMemo(() => { + if (reportType !== 'costs') return []; + // Collect work order IDs from tasks in current month + const woIds = new Set( + tasks + .filter((t) => { + const d = t.scheduled_date ? new Date(t.scheduled_date) : null; + return d && !Number.isNaN(d.getTime()) && d.getFullYear() === viewYear && d.getMonth() === viewMonth && t.work_order; + }) + .map((t) => String(t.work_order)) + ); + // Filter work orders with invoices + return workOrders + .filter((wo) => woIds.has(String(wo.id)) && Array.isArray(wo.invoices) && wo.invoices.length > 0) + .flatMap((wo) => + wo.invoices.map((inv) => [ + inv.datum ? new Date(inv.datum).toLocaleDateString('hr-HR') : '-', + inv.naziv_racuna || '-', + inv.lokacija || '-', + inv.opis || '-', + wo.display_code || '-', + ]) + ) + .sort((a, b) => (a[0] || '').localeCompare(b[0] || '')); + }, [reportType, tasks, workOrders, viewYear, viewMonth]); + const selectedDayKey = selectedDay ? `${viewYear}-${String(viewMonth + 1).padStart(2, '0')}-${String(selectedDay).padStart(2, '0')}` : null; @@ -86,199 +167,336 @@ export default function TaskCalendarWidget({ tasks = [], notes = [], onTaskClick closePanel?.(); } + function toggleReport(type) { + setReportType((prev) => (prev === type ? null : type)); + } + + async function handleDownload() { + if (downloading) return; + setDownloading(true); + try { + if (reportType === 'servicer') { + await downloadMonthlyServiserReport(viewYear, viewMonth + 1); + } else { + await downloadMonthlyCostsReport(viewYear, viewMonth + 1); + } + } finally { + setDownloading(false); + } + } + const totalScheduled = Object.values(tasksByDate).flat().length; const totalNoteReminders = Object.values(notesByDate).flat().length; return ( {({ close }) => ( - <> +
- {/* Month navigation */} -
- - - {MONTH_NAMES[viewMonth]} {viewYear} - - -
+ {/* Report toggle buttons */} +
+ + +
- {/* Calendar grid */} -
-
- {WEEKDAY_LABELS.map((label) => ( -
- {label} + {/* Month navigation */} +
+ + + {MONTH_NAMES[viewMonth]} {viewYear} + + +
+ + {/* Calendar grid */} +
+
+ {WEEKDAY_LABELS.map((label) => ( +
+ {label} +
+ ))}
- ))} -
-
- {calendarDays.map((day, idx) => { - if (!day) return
; - const key = `${viewYear}-${String(viewMonth + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`; - const hasTasks = Boolean(tasksByDate[key]?.length); - const hasNotes = Boolean(notesByDate[key]?.length); - const taskCount = tasksByDate[key]?.length ?? 0; - const noteCount = notesByDate[key]?.length ?? 0; - const isToday = - today.getFullYear() === viewYear && - today.getMonth() === viewMonth && - today.getDate() === day; - const isSelected = selectedDay === day; - return ( - + ); + })} +
+
+ + {/* Divider */} +
+ + {/* Task list for selected day OR hint */} +
+ {selectedDay ? ( + <> +

+ {formatDayLabel(viewYear, viewMonth, selectedDay)} +

+ {selectedDayTasks.length === 0 && selectedDayNotes.length === 0 ? ( +

Nema događaja za odabrani datum.

+ ) : ( +
+ {selectedDayNotes.length > 0 && ( +
+

Bilješke

+
    + {selectedDayNotes.map((note) => ( +
  • + {note.note} + + {note.task_title || note.work_order_label || 'Servisni kontekst'} + +
  • + ))} +
+
)} - + {selectedDayTasks.length > 0 && ( +
+

Zadaci

+
    + {selectedDayTasks.map((task) => ( +
  • + +
  • + ))} +
+
+ )} +
)} - - ); - })} -
-
- - {/* Divider */} -
- - {/* Task list for selected day OR hint */} -
- {selectedDay ? ( - <> -

- {formatDayLabel(viewYear, viewMonth, selectedDay)} -

- {selectedDayTasks.length === 0 && selectedDayNotes.length === 0 ? ( -

Nema događaja za odabrani datum.

+ ) : ( -
- {selectedDayNotes.length > 0 && ( -
-

Bilješke

-
    - {selectedDayNotes.map((note) => ( -
  • - {note.note} - - {note.task_title || note.work_order_label || 'Servisni kontekst'} - -
  • - ))} -
-
+ <> +

+ Odaberite datum u kalendaru za prikaz zadataka. +

+ {totalScheduled > 0 && ( +

+ {totalScheduled} zadatak/a s planiranim datumom. +

)} - {selectedDayTasks.length > 0 && ( -
-

Zadaci

-
    - {selectedDayTasks.map((task) => ( -
  • - -
  • - ))} -
-
+ {totalNoteReminders > 0 && ( +

+ {totalNoteReminders} bilješki/podsjetnika u kalendaru. +

)} + {totalScheduled === 0 && ( +

+ Nijedan zadatak nema postavljen planirani datum. +

+ )} + + )} +
+
+ + {/* ── Report panel (visible when reportType is set) ── */} + {reportOpen && ( +
+ {/* Report header */} +
+
+ + {reportType === 'servicer' ? '📊 Izvještaj servisera' : '💰 Izvještaj troškova'} + + + {MONTH_NAMES[viewMonth]} {viewYear} +
- )} - - ) : ( - <> -

- Odaberite datum u kalendaru za prikaz zadataka. + +

+ + {/* Report info note */} +

+ {reportType === 'servicer' + ? 'Pregled podataka za odabrani mjesec. Preuzmi DOCX za točan i formatiran izvještaj.' + : 'Pregled troškova za odabrani mjesec iz putnih naloga. Preuzmi DOCX za točan izvještaj.' + }

- {totalScheduled > 0 && ( -

- {totalScheduled} zadatak/a s planiranim datumom. -

- )} - {totalNoteReminders > 0 && ( -

- {totalNoteReminders} bilješki/podsjetnika u kalendaru. -

- )} - {totalScheduled === 0 && ( -

- Nijedan zadatak nema postavljen planirani datum. -

- )} - + + {/* Table */} +
+ {reportType === 'servicer' && ( + servicerRows.length === 0 ? ( +

+ Nema zadataka za {MONTH_NAMES[viewMonth]} {viewYear}. +

+ ) : ( + + + + {SERVICER_HEADERS.map((h) => ( + + ))} + + + + {servicerRows.map((row, i) => ( + + {row.map((cell, j) => ( + + ))} + + ))} + +
+ {h} +
+ {cell} +
+ ) + )} + {reportType === 'costs' && ( + costsRows.length === 0 ? ( +

+ Nema troškova za {MONTH_NAMES[viewMonth]} {viewYear}. +
+ Napomena: prikaz ovisi o dostupnim podacima raÄŤuna u putnim nalozima. +

+ ) : ( + + + + {['DATUM', 'NAZIV RAÄŚUNA', 'LOKACIJA', 'OPIS', 'RADNI NALOG'].map((h) => ( + + ))} + + + + {costsRows.map((row, i) => ( + + {row.map((cell, j) => ( + + ))} + + ))} + +
+ {h} +
+ {cell} +
+ ) + )} +
+
)}
- )} ); diff --git a/frontend/src/components/layout/TaskCalendarPortal.jsx b/frontend/src/components/layout/TaskCalendarPortal.jsx index 8494961..05dfc8d 100644 --- a/frontend/src/components/layout/TaskCalendarPortal.jsx +++ b/frontend/src/components/layout/TaskCalendarPortal.jsx @@ -103,6 +103,7 @@ export default function TaskCalendarPortal() {