Dodana je tablica DODATNI TROŠKOVI za putni nalog (backend model + API + frontend modal) s izračunom ukupnog iznosa za isplatu. Upload računa sada odmah upisuje redak u dodatne troškove, a asinkroni OCR (Celery) naknadno pokušava popuniti broj računa i iznos iz slike/PDF-a. Uvedena je invalidacija PDF cache-a nakon promjena računa/dodatnih troškova kako bi generirani PDF uvijek prikazivao najnovije podatke. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
187 lines
9.8 KiB
JavaScript
187 lines
9.8 KiB
JavaScript
import { useEffect, useMemo, useState } from 'preact/hooks';
|
|
import ModalShell from '../ui/ModalShell';
|
|
|
|
const EMPTY_ROW = {
|
|
naziv: '',
|
|
broj_racuna: '',
|
|
ukupan_iznos: '',
|
|
prilog: '',
|
|
source_invoice_id: '',
|
|
};
|
|
|
|
function parseAmount(value) {
|
|
const normalized = String(value || '').trim().replace('€', '').replace(/\s+/g, '').replace(',', '.');
|
|
const parsed = Number(normalized);
|
|
return Number.isFinite(parsed) ? parsed : 0;
|
|
}
|
|
|
|
export default function WorkOrderAdditionalCostsModal({
|
|
open,
|
|
workOrder,
|
|
tableData,
|
|
saving = false,
|
|
onClose,
|
|
onSave,
|
|
}) {
|
|
const [rows, setRows] = useState([]);
|
|
|
|
useEffect(() => {
|
|
if (!open) {
|
|
setRows([]);
|
|
return;
|
|
}
|
|
const incoming = Array.isArray(tableData?.data?.rows) ? tableData.data.rows : [];
|
|
setRows(incoming.length > 0 ? incoming.map((row) => ({ ...EMPTY_ROW, ...(row || {}) })) : [{ ...EMPTY_ROW }]);
|
|
}, [open, tableData]);
|
|
|
|
const total = useMemo(() => {
|
|
const value = rows.reduce((sum, row) => sum + parseAmount(row.ukupan_iznos), 0);
|
|
return value.toFixed(2);
|
|
}, [rows]);
|
|
|
|
if (!open) return null;
|
|
|
|
const updateRowField = (index, field, value) => {
|
|
setRows((prev) => prev.map((row, i) => (i === index ? { ...row, [field]: value } : row)));
|
|
};
|
|
|
|
const addRow = () => setRows((prev) => [...prev, { ...EMPTY_ROW }]);
|
|
const removeRow = (index) => setRows((prev) => prev.filter((_, i) => i !== index));
|
|
|
|
const handleSave = async () => {
|
|
const payloadRows = rows.map((row) => ({
|
|
naziv: String(row.naziv || '').trim(),
|
|
broj_racuna: String(row.broj_racuna || '').trim(),
|
|
ukupan_iznos: String(row.ukupan_iznos || '').trim(),
|
|
prilog: String(row.prilog || '').trim(),
|
|
source_invoice_id: String(row.source_invoice_id || '').trim(),
|
|
}));
|
|
await onSave?.({ rows: payloadRows });
|
|
};
|
|
|
|
return (
|
|
<ModalShell
|
|
onClose={onClose}
|
|
overlayClassName="z-[70] overflow-y-auto p-4 pt-16"
|
|
contentClassName="flex min-h-full items-start justify-center"
|
|
panelClassName="w-full max-w-5xl rounded-xl border border-border-hairline bg-canvas-elevated shadow-xl overflow-hidden max-h-[calc(100vh-2rem)]"
|
|
>
|
|
<div className="flex w-full flex-col">
|
|
<div className="flex items-center justify-between border-b border-border-hairline px-5 py-3">
|
|
<div>
|
|
<h3 className="text-base font-semibold text-text-main">DODATNI TROŠKOVI</h3>
|
|
<p className="text-xs text-text-muted">{workOrder?.display_code || workOrder?.id || '-'}</p>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onClick={onClose}
|
|
aria-label="Zatvori"
|
|
className="ml-4 rounded-lg p-1.5 text-gray-400 hover:bg-gray-100 hover:text-gray-600 dark:hover:bg-gray-700"
|
|
>
|
|
✕
|
|
</button>
|
|
</div>
|
|
|
|
<div className="space-y-4 overflow-y-auto px-5 py-4">
|
|
<div className="flex flex-wrap gap-2">
|
|
<button
|
|
type="button"
|
|
onClick={addRow}
|
|
className="inline-flex items-center gap-1 rounded-lg border border-gray-300 bg-white px-3 py-1.5 text-xs font-medium text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200 dark:hover:bg-gray-700"
|
|
>
|
|
+ Dodaj redak
|
|
</button>
|
|
</div>
|
|
|
|
<div className="overflow-auto rounded-xl border border-gray-200 shadow-sm dark:border-gray-700">
|
|
<table className="min-w-[900px] w-full bg-white text-xs dark:bg-gray-900">
|
|
<thead>
|
|
<tr className="border-b border-gray-200 bg-gray-50 text-left dark:border-gray-700 dark:bg-gray-800">
|
|
<th className="w-8 px-2 py-2.5 text-center text-gray-400">#</th>
|
|
<th className="px-2 py-2.5 font-semibold text-gray-700 dark:text-gray-200">NAZIV</th>
|
|
<th className="px-2 py-2.5 font-semibold text-gray-700 dark:text-gray-200">Broj računa</th>
|
|
<th className="px-2 py-2.5 font-semibold text-gray-700 dark:text-gray-200">Ukupan iznos</th>
|
|
<th className="px-2 py-2.5 font-semibold text-gray-700 dark:text-gray-200">Prilog</th>
|
|
<th className="px-2 py-2.5 text-gray-400">Akcija</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{rows.map((row, index) => (
|
|
<tr key={index} className="border-t border-gray-100 dark:border-gray-700">
|
|
<td className="px-2 py-1.5 text-center text-[11px] text-gray-400">{index + 1}</td>
|
|
<td className="px-1 py-1">
|
|
<input
|
|
type="text"
|
|
value={row.naziv}
|
|
onInput={(event) => updateRowField(index, 'naziv', event.currentTarget.value)}
|
|
className="block w-full rounded-lg border border-gray-300 bg-gray-50 px-2.5 py-2 text-xs text-gray-900"
|
|
/>
|
|
</td>
|
|
<td className="px-1 py-1">
|
|
<input
|
|
type="text"
|
|
value={row.broj_racuna}
|
|
onInput={(event) => updateRowField(index, 'broj_racuna', event.currentTarget.value)}
|
|
className="block w-full rounded-lg border border-gray-300 bg-gray-50 px-2.5 py-2 text-xs text-gray-900"
|
|
/>
|
|
</td>
|
|
<td className="px-1 py-1">
|
|
<input
|
|
type="text"
|
|
value={row.ukupan_iznos}
|
|
onInput={(event) => updateRowField(index, 'ukupan_iznos', event.currentTarget.value)}
|
|
className="block w-full rounded-lg border border-gray-300 bg-gray-50 px-2.5 py-2 text-xs text-gray-900"
|
|
/>
|
|
</td>
|
|
<td className="px-1 py-1">
|
|
<input
|
|
type="text"
|
|
value={row.prilog}
|
|
onInput={(event) => updateRowField(index, 'prilog', event.currentTarget.value)}
|
|
className="block w-full rounded-lg border border-gray-300 bg-gray-50 px-2.5 py-2 text-xs text-gray-900"
|
|
/>
|
|
</td>
|
|
<td className="px-1 py-1">
|
|
<button
|
|
type="button"
|
|
onClick={() => removeRow(index)}
|
|
className="rounded-md border border-red-200 px-2 py-1 text-[11px] font-medium text-red-600 hover:bg-red-50"
|
|
>
|
|
Obriši
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
<tr className="border-t-2 border-gray-300 bg-gray-100 text-xs font-bold dark:border-gray-600 dark:bg-gray-800">
|
|
<td colSpan={3} className="px-3 py-2 text-gray-700 dark:text-gray-200">UKUPNO ZA ISPLATU</td>
|
|
<td className="px-2 py-2 text-right text-gray-800 dark:text-gray-100">{total} €</td>
|
|
<td colSpan={2} />
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-end gap-3 border-t border-border-hairline px-5 py-3">
|
|
<button
|
|
type="button"
|
|
onClick={onClose}
|
|
disabled={saving}
|
|
className="rounded-lg border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 disabled:opacity-50"
|
|
>
|
|
Odustani
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={handleSave}
|
|
disabled={saving}
|
|
className="rounded-lg bg-blue-600 px-5 py-2 text-sm font-semibold text-white hover:bg-blue-700 disabled:opacity-60"
|
|
>
|
|
{saving ? 'Spremam…' : 'Spremi dodatne troškove'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</ModalShell>
|
|
);
|
|
}
|