- admin.py: thumbnail_preview u WorkOrderInvoiceInline i WorkOrderInvoiceAdmin prikazuje slike/PDF veze putem optimiziranog image endpointa - serializers.py: image_url sada uključuje ?w=1280&q=80&fmt=jpeg za ne-PDF slike - views.py: doda _compress_image_for_pdf() helper, zamijeni ImageReader(photo.image) s kompresiranom JPEG verzijom na oba mjesta u PDF generiranju - tasks.py: kompresija invoice slike u JPEG u memoriji prije ImageReader poziva - WorkOrderInvoicesPdfPage.jsx: * InvoiceImagePreview prikazuje 'Učitavanje...' dok se blob dohvaća (ne 401 fallback) * uklonjen resolveMediaUrl fallback koji uzrokuje 401 greške * svaka invoice kartica je klikabilna i otvara InvoiceDetailModal * InvoiceDetailModal prikazuje detalje i uvećanu sliku/PDF vezu Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
555 lines
28 KiB
JavaScript
555 lines
28 KiB
JavaScript
import { useEffect, useMemo, useState } from 'preact/hooks';
|
|
import { useStore } from '@nanostores/preact';
|
|
import {
|
|
downloadWorkOrderDocx,
|
|
downloadWorkOrderPdf,
|
|
downloadWorkOrderInvoicesDocx,
|
|
downloadWorkOrderInvoicesPdf,
|
|
downloadWorkOrderServiceRecordsDocx,
|
|
downloadWorkOrderServiceRecordsPdf,
|
|
fetchWorkOrderAdditionalCostsTable,
|
|
fetchWorkOrderById,
|
|
fetchWorkOrderInvoices,
|
|
fetchWorkOrderTaskServiceContext,
|
|
updateTaskWorkHoursTable,
|
|
updateWorkOrder,
|
|
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';
|
|
import WorkOrderServiceNotesModal from './WorkOrderServiceNotesModal';
|
|
|
|
function readWorkOrderIdFromQuery() {
|
|
if (typeof window === 'undefined') {
|
|
return '';
|
|
}
|
|
const params = new URLSearchParams(window.location.search);
|
|
return params.get('workOrderId') || '';
|
|
}
|
|
|
|
function InvoiceImagePreview({ imageUrl, rawUrl, alt, contentType, onClick }) {
|
|
const [broken, setBroken] = useState(false);
|
|
const isPdf = String(contentType || '').toLowerCase() === 'application/pdf';
|
|
const isProtected = typeof rawUrl === 'string' && /\/api\/fleet\//i.test(rawUrl);
|
|
const isLoading = isProtected && !imageUrl && !broken;
|
|
|
|
if (isPdf && imageUrl) {
|
|
return (
|
|
<a
|
|
href={imageUrl}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="flex h-28 w-28 items-center justify-center rounded-lg border border-border-hairline bg-canvas-deep text-center text-[11px] text-text-main hover:bg-canvas-base"
|
|
>
|
|
Otvori PDF
|
|
</a>
|
|
);
|
|
}
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="flex h-28 w-28 items-center justify-center rounded-lg border border-border-hairline bg-canvas-deep text-center text-[11px] text-text-muted">
|
|
<span className="animate-pulse">Učitavanje…</span>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!imageUrl || broken) {
|
|
return (
|
|
<div className="flex h-28 w-28 items-center justify-center rounded-lg border border-border-hairline bg-canvas-deep text-center text-[11px] text-text-muted">
|
|
Bez slike
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<button
|
|
type="button"
|
|
onClick={onClick}
|
|
className="inline-flex cursor-pointer focus:outline-none"
|
|
aria-label={`Otvori pregled: ${alt}`}
|
|
>
|
|
<img
|
|
src={imageUrl}
|
|
alt={alt}
|
|
onError={() => setBroken(true)}
|
|
className="h-28 w-28 rounded-lg border border-border-hairline object-cover hover:opacity-90 transition-opacity"
|
|
/>
|
|
</button>
|
|
);
|
|
}
|
|
|
|
function InvoiceDetailModal({ invoice, imageUrl, contentType, onClose }) {
|
|
const isPdf = String(contentType || '').toLowerCase() === 'application/pdf';
|
|
|
|
useEffect(() => {
|
|
const handleKey = (e) => { if (e.key === 'Escape') onClose(); };
|
|
window.addEventListener('keydown', handleKey);
|
|
return () => window.removeEventListener('keydown', handleKey);
|
|
}, [onClose]);
|
|
|
|
return (
|
|
<div
|
|
className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-4"
|
|
onClick={(e) => { if (e.target === e.currentTarget) onClose(); }}
|
|
>
|
|
<div className="relative flex max-h-[95vh] w-full max-w-3xl flex-col overflow-auto rounded-xl bg-canvas-base shadow-2xl">
|
|
<div className="flex items-center justify-between border-b border-border-hairline px-5 py-3">
|
|
<h2 className="text-base font-semibold text-text-main">{invoice.naziv_racuna || 'Račun'}</h2>
|
|
<button
|
|
type="button"
|
|
onClick={onClose}
|
|
className="rounded p-1 text-text-muted hover:text-text-main focus:outline-none"
|
|
aria-label="Zatvori"
|
|
>
|
|
✕
|
|
</button>
|
|
</div>
|
|
<div className="space-y-1 px-5 py-3 text-sm text-text-main border-b border-border-hairline">
|
|
<div><span className="font-semibold">Naziv:</span> {invoice.naziv_racuna || '-'}</div>
|
|
<div><span className="font-semibold">Lokacija:</span> {invoice.lokacija || '-'}</div>
|
|
<div><span className="font-semibold">Datum:</span> {invoice.datum || '-'}</div>
|
|
<div><span className="font-semibold">Opis:</span> {invoice.opis || '-'}</div>
|
|
</div>
|
|
<div className="flex flex-1 items-center justify-center p-5">
|
|
{isPdf && imageUrl ? (
|
|
<a
|
|
href={imageUrl}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="rounded-lg bg-indigo-600 px-4 py-2 text-sm font-semibold text-white hover:bg-indigo-700"
|
|
>
|
|
Otvori PDF u novom tabu
|
|
</a>
|
|
) : imageUrl ? (
|
|
<img
|
|
src={imageUrl}
|
|
alt={invoice.naziv_racuna}
|
|
className="max-h-[60vh] max-w-full rounded-lg object-contain shadow"
|
|
/>
|
|
) : (
|
|
<p className="text-sm text-text-muted">Slika nije dostupna.</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default function WorkOrderInvoicesPdfPage() {
|
|
const authReady = useStore($authReady);
|
|
const token = useStore($accessToken);
|
|
const [workOrderId, setWorkOrderId] = useState('');
|
|
const [workOrder, setWorkOrder] = useState(null);
|
|
const [invoices, setInvoices] = useState([]);
|
|
const [taskContext, setTaskContext] = useState({ tasks: [] });
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState('');
|
|
const [editingTask, setEditingTask] = useState(null);
|
|
const [savingWorkHours, setSavingWorkHours] = useState(false);
|
|
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 [invoiceDetailModal, setInvoiceDetailModal] = useState(null);
|
|
|
|
useEffect(() => {
|
|
hydrateAuthFromStorage();
|
|
const id = readWorkOrderIdFromQuery();
|
|
setWorkOrderId(id);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (authReady) {
|
|
fetchNotifications();
|
|
connectNotifications();
|
|
}
|
|
}, [authReady]);
|
|
|
|
useEffect(() => {
|
|
if (!workOrderId) {
|
|
setLoading(false);
|
|
setError('Nedostaje workOrderId u URL-u.');
|
|
return;
|
|
}
|
|
if (!authReady) {
|
|
return;
|
|
}
|
|
if (!token) {
|
|
setLoading(false);
|
|
setError('Sesija nije aktivna. Prijavite se ponovno.');
|
|
return;
|
|
}
|
|
let cancelled = false;
|
|
setLoading(true);
|
|
setError('');
|
|
|
|
(async () => {
|
|
try {
|
|
const [contextPayload, order, invoiceItems, additionalCostsPayload] = await Promise.all([
|
|
fetchWorkOrderTaskServiceContext(workOrderId),
|
|
fetchWorkOrderById(workOrderId),
|
|
fetchWorkOrderInvoices(workOrderId),
|
|
fetchWorkOrderAdditionalCostsTable(workOrderId),
|
|
]);
|
|
if (cancelled) {
|
|
return;
|
|
}
|
|
setTaskContext(contextPayload);
|
|
setWorkOrder(order);
|
|
setInvoices(invoiceItems);
|
|
setAdditionalCostsTable(additionalCostsPayload || contextPayload?.additional_costs_table || { data: { rows: [] }, total_for_payout: '0.00' });
|
|
} catch (err) {
|
|
if (!cancelled) {
|
|
setError(err?.message || 'Neuspješno dohvaćanje podataka o računima.');
|
|
}
|
|
} finally {
|
|
if (!cancelled) {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
})();
|
|
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, [workOrderId, authReady, token]);
|
|
|
|
const title = useMemo(() => {
|
|
if (!workOrder) {
|
|
return 'Računi putnog naloga';
|
|
}
|
|
return formatWorkOrderDisplayCode(workOrder);
|
|
}, [workOrder]);
|
|
|
|
const mediaPaths = useMemo(() => {
|
|
const paths = [];
|
|
if (Array.isArray(taskContext.tasks)) {
|
|
taskContext.tasks.forEach((task) => {
|
|
if (!Array.isArray(task?.service_records)) return;
|
|
task.service_records.forEach((record) => {
|
|
if (!Array.isArray(record?.photos)) return;
|
|
record.photos.forEach((photo) => {
|
|
const candidate = photo?.optimized_url || photo?.image_url;
|
|
if (candidate) {
|
|
paths.push(candidate);
|
|
}
|
|
});
|
|
});
|
|
});
|
|
}
|
|
if (Array.isArray(invoices)) {
|
|
invoices.forEach((invoice) => {
|
|
const invoiceImage = invoice?.image_url || invoice?.image;
|
|
if (invoiceImage) {
|
|
paths.push(invoiceImage);
|
|
}
|
|
});
|
|
}
|
|
return paths;
|
|
}, [taskContext.tasks, invoices]);
|
|
|
|
const { getAuthenticatedMediaSrc } = useAuthenticatedMediaSources(mediaPaths);
|
|
|
|
const handleSaveWorkHoursTable = async (tableData) => {
|
|
if (!editingTask?.id) return;
|
|
setSavingWorkHours(true);
|
|
try {
|
|
const response = await updateTaskWorkHoursTable(editingTask.id, tableData);
|
|
setTaskContext((prev) => ({
|
|
...prev,
|
|
tasks: (Array.isArray(prev.tasks) ? prev.tasks : []).map((task) => (
|
|
String(task.id) === String(editingTask.id)
|
|
? { ...task, work_hours_table: response?.data ?? null }
|
|
: task
|
|
)),
|
|
}));
|
|
setEditingTask(null);
|
|
} finally {
|
|
setSavingWorkHours(false);
|
|
}
|
|
};
|
|
|
|
const handleSaveAdditionalCostsTable = async (tableData) => {
|
|
if (!workOrderId) return;
|
|
setSavingAdditionalCosts(true);
|
|
try {
|
|
const response = await updateWorkOrderAdditionalCostsTable(workOrderId, tableData);
|
|
setAdditionalCostsTable(response || { data: { rows: [] }, total_for_payout: '0.00' });
|
|
setEditingAdditionalCosts(false);
|
|
} finally {
|
|
setSavingAdditionalCosts(false);
|
|
}
|
|
};
|
|
|
|
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 (
|
|
<section className="mx-auto w-full max-w-5xl space-y-4 p-4 sm:p-6">
|
|
<div className="rounded-xl border border-border-hairline bg-canvas-elevated p-4">
|
|
<div className="flex flex-wrap items-center justify-between gap-3">
|
|
<div>
|
|
<h1 className="text-xl font-semibold text-text-main">{title}</h1>
|
|
<p className="text-sm text-text-muted">
|
|
{workOrder ? `Dizalica: ${workOrder.crane_label || workOrder.crane || '-'}` : 'Pregled računa prije kreiranja PDF-a.'}
|
|
</p>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onClick={() => downloadWorkOrderPdf(workOrderId)}
|
|
disabled={!workOrderId}
|
|
className="rounded-lg bg-indigo-600 px-4 py-2 text-sm font-semibold text-white hover:bg-indigo-700 disabled:opacity-60"
|
|
>
|
|
Preuzmi PDF putnog naloga
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => downloadWorkOrderDocx(workOrderId)}
|
|
disabled={!workOrderId}
|
|
className="rounded-lg border border-border-hairline px-4 py-2 text-sm font-semibold text-text-main hover:bg-canvas-deep disabled:opacity-60"
|
|
>
|
|
Preuzmi DOCX putnog naloga
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="rounded-xl border border-border-hairline bg-canvas-elevated p-4">
|
|
<div className="mb-3 flex flex-wrap items-center justify-between gap-2">
|
|
<h2 className="text-lg font-semibold text-text-main">Servisni zapisi</h2>
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
const firstTask = Array.isArray(taskContext.tasks) ? taskContext.tasks[0] : null;
|
|
if (!firstTask) return;
|
|
setEditingTask(firstTask);
|
|
}}
|
|
disabled={!Array.isArray(taskContext.tasks) || taskContext.tasks.length === 0}
|
|
className="rounded-lg border border-border-hairline px-4 py-2 text-sm font-semibold text-text-main hover:bg-canvas-deep disabled:opacity-60"
|
|
>
|
|
Uredi tablicu radnih sati
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => setEditingServiceReportNote(true)}
|
|
disabled={!workOrderId}
|
|
className="rounded-lg border border-border-hairline px-4 py-2 text-sm font-semibold text-text-main hover:bg-canvas-deep disabled:opacity-60"
|
|
>
|
|
Uredi tekst napomene
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => downloadWorkOrderServiceRecordsPdf(workOrderId)}
|
|
disabled={!workOrderId}
|
|
className="rounded-lg bg-indigo-600 px-4 py-2 text-sm font-semibold text-white hover:bg-indigo-700 disabled:opacity-60"
|
|
>
|
|
Preuzmi PDF servisnih zapisa
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => downloadWorkOrderServiceRecordsDocx(workOrderId)}
|
|
disabled={!workOrderId}
|
|
className="rounded-lg border border-border-hairline px-4 py-2 text-sm font-semibold text-text-main hover:bg-canvas-deep disabled:opacity-60"
|
|
>
|
|
Preuzmi DOCX servisnih zapisa
|
|
</button>
|
|
</div>
|
|
</div>
|
|
{loading && <p className="text-sm text-text-muted">Učitavanje task konteksta...</p>}
|
|
{!loading && !error && (!Array.isArray(taskContext.tasks) || taskContext.tasks.length === 0) && (
|
|
<p className="text-sm text-text-muted">Nema taskova povezanih s ovim putnim nalogom.</p>
|
|
)}
|
|
{!loading && !error && Array.isArray(taskContext.tasks) && taskContext.tasks.length > 0 && (
|
|
<div className="space-y-4">
|
|
{taskContext.tasks.map((task) => (
|
|
<article key={task.id} className="rounded-lg border border-border-hairline bg-canvas-base p-3">
|
|
<div className="space-y-1 text-sm text-text-main">
|
|
<div><span className="font-semibold">Radni zadatak:</span> {task.title || '-'}</div>
|
|
<div><span className="font-semibold">Status:</span> {task.status || '-'}</div>
|
|
<div><span className="font-semibold">Dodijeljeno:</span> {task.assigned_to_name || '-'}</div>
|
|
<div><span className="font-semibold">Dizalica:</span> {task.vehicle_registration || '-'}</div>
|
|
</div>
|
|
<div className="mt-3 space-y-3">
|
|
{!Array.isArray(task.service_records) || task.service_records.length === 0 ? (
|
|
<p className="text-xs text-text-muted">Nema povezanih servisnih zapisa.</p>
|
|
) : (
|
|
task.service_records.map((record) => (
|
|
<div key={record.id} className="rounded border border-border-hairline px-3 py-2">
|
|
<div className="space-y-1 text-xs text-text-main">
|
|
<div><span className="font-semibold">Servis:</span> {record.description || '-'}</div>
|
|
<div><span className="font-semibold">Datum:</span> {record.service_date || '-'}</div>
|
|
<div><span className="font-semibold">Trošak:</span> {record.cost || '-'} EUR</div>
|
|
<div><span className="font-semibold">KM:</span> {record.mileage ?? '-'}</div>
|
|
</div>
|
|
<div className="mt-2 flex flex-wrap gap-2">
|
|
{Array.isArray(record.photos) && record.photos.length > 0 ? (
|
|
record.photos.map((photo) => (
|
|
<a
|
|
key={photo.id}
|
|
href={getAuthenticatedMediaSrc(photo.optimized_url || photo.image_url)}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="inline-flex"
|
|
>
|
|
<img
|
|
src={getAuthenticatedMediaSrc(photo.optimized_url || photo.image_url)}
|
|
alt={`Servisna slika ${photo.id}`}
|
|
className="h-20 w-20 rounded border border-border-hairline object-cover"
|
|
/>
|
|
</a>
|
|
))
|
|
) : (
|
|
<span className="text-xs text-text-muted">Bez slika</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
))
|
|
)}
|
|
</div>
|
|
</article>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="rounded-xl border border-border-hairline bg-canvas-elevated p-4">
|
|
<div className="mb-3 flex flex-wrap items-center justify-between gap-2">
|
|
<h2 className="text-lg font-semibold text-text-main">Računi</h2>
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
<button
|
|
type="button"
|
|
onClick={() => setEditingAdditionalCosts(true)}
|
|
disabled={!workOrderId}
|
|
className="rounded-lg border border-border-hairline px-4 py-2 text-sm font-semibold text-text-main hover:bg-canvas-deep disabled:opacity-60"
|
|
>
|
|
Uredi dodatne troškove
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => downloadWorkOrderInvoicesPdf(workOrderId)}
|
|
disabled={!workOrderId}
|
|
className="rounded-lg bg-indigo-600 px-4 py-2 text-sm font-semibold text-white hover:bg-indigo-700 disabled:opacity-60"
|
|
>
|
|
Preuzmi račune putnog naloga
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => downloadWorkOrderInvoicesDocx(workOrderId)}
|
|
disabled={!workOrderId}
|
|
className="rounded-lg border border-border-hairline px-4 py-2 text-sm font-semibold text-text-main hover:bg-canvas-deep disabled:opacity-60"
|
|
>
|
|
Preuzmi DOCX računa
|
|
</button>
|
|
</div>
|
|
</div>
|
|
{!loading && !error && (
|
|
<p className="mb-3 text-sm text-text-muted">
|
|
Ukupno dodatni troškovi: <span className="font-semibold text-text-main">{additionalCostsTable?.total_for_payout || '0.00'} €</span>
|
|
</p>
|
|
)}
|
|
{loading && <p className="text-sm text-text-muted">Učitavanje računa...</p>}
|
|
{!loading && error && (
|
|
<div className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700">
|
|
{error}
|
|
</div>
|
|
)}
|
|
{!loading && !error && invoices.length === 0 && (
|
|
<p className="text-sm text-text-muted">Za ovaj putni nalog nema unesenih računa.</p>
|
|
)}
|
|
{!loading && !error && invoices.length > 0 && (
|
|
<div className="space-y-3">
|
|
{invoices.map((invoice) => {
|
|
const rawUrl = invoice.image_url || invoice.image || '';
|
|
const blobUrl = getAuthenticatedMediaSrc(rawUrl);
|
|
return (
|
|
<article
|
|
key={invoice.id}
|
|
className="cursor-pointer rounded-lg border border-border-hairline bg-canvas-base p-3 hover:bg-canvas-deep transition-colors"
|
|
onClick={() => setInvoiceDetailModal(invoice)}
|
|
role="button"
|
|
tabIndex={0}
|
|
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') setInvoiceDetailModal(invoice); }}
|
|
>
|
|
<div className="grid gap-3 sm:grid-cols-[1fr_auto] sm:items-start">
|
|
<div className="space-y-1 text-sm text-text-main">
|
|
<div><span className="font-semibold">naziv_računa:</span> {invoice.naziv_racuna}</div>
|
|
<div><span className="font-semibold">lokacija:</span> {invoice.lokacija || '-'}</div>
|
|
<div><span className="font-semibold">datum:</span> {invoice.datum || '-'}</div>
|
|
<div><span className="font-semibold">opis:</span> {invoice.opis || '-'}</div>
|
|
</div>
|
|
<InvoiceImagePreview
|
|
imageUrl={blobUrl}
|
|
rawUrl={rawUrl}
|
|
alt={`Račun ${invoice.naziv_racuna}`}
|
|
contentType={invoice.image_content_type}
|
|
onClick={(e) => { e.stopPropagation(); setInvoiceDetailModal(invoice); }}
|
|
/>
|
|
</div>
|
|
</article>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
<TaskWorkHoursTableModal
|
|
open={!!editingTask}
|
|
task={editingTask}
|
|
defaultLocation={workOrder?.location || ''}
|
|
saving={savingWorkHours}
|
|
onClose={() => setEditingTask(null)}
|
|
onSave={handleSaveWorkHoursTable}
|
|
/>
|
|
<WorkOrderAdditionalCostsModal
|
|
open={editingAdditionalCosts}
|
|
workOrder={workOrder}
|
|
tableData={additionalCostsTable}
|
|
saving={savingAdditionalCosts}
|
|
onClose={() => setEditingAdditionalCosts(false)}
|
|
onSave={handleSaveAdditionalCostsTable}
|
|
/>
|
|
<WorkOrderServiceNotesModal
|
|
open={editingServiceReportNote}
|
|
workOrder={workOrder}
|
|
saving={savingServiceReportNote}
|
|
error={serviceReportNoteError}
|
|
onClose={() => {
|
|
setEditingServiceReportNote(false);
|
|
setServiceReportNoteError('');
|
|
}}
|
|
onSave={handleSaveServiceReportNote}
|
|
/>
|
|
{invoiceDetailModal && (
|
|
<InvoiceDetailModal
|
|
invoice={invoiceDetailModal}
|
|
imageUrl={getAuthenticatedMediaSrc(invoiceDetailModal.image_url || invoiceDetailModal.image || '')}
|
|
contentType={invoiceDetailModal.image_content_type}
|
|
onClose={() => setInvoiceDetailModal(null)}
|
|
/>
|
|
)}
|
|
</section>
|
|
);
|
|
}
|