feat: dodaj DOCX akcije i napomene na racunima
- dodaje DOCX preuzimanje za putni nalog, servisne zapise i racune na /putni-nalozi/racuni\n- uvodi modal za uredjivanje teksta napomene izvjestaja servisera\n- poboljsava update work order toka i upsert lokalnog stanja\n- ukljucuje TaskCalendar migraciju, dev watch stabilizaciju i deploy upute Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -68,7 +68,12 @@ export default defineConfig({
|
||||
},
|
||||
watch: {
|
||||
usePolling: true,
|
||||
interval: 100,
|
||||
interval: 1000,
|
||||
awaitWriteFinish: {
|
||||
stabilityThreshold: 1200,
|
||||
pollInterval: 200,
|
||||
},
|
||||
ignored: ['**/.git/**', '**/node_modules/**', '**/.astro/**', '**/dist/**'],
|
||||
},
|
||||
}
|
||||
: {
|
||||
|
||||
@@ -1,22 +1,28 @@
|
||||
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 { resolveMediaUrl, useAuthenticatedMediaSources } from './WorkOrderImageCarousel';
|
||||
import TaskWorkHoursTableModal from './TaskWorkHoursTableModal';
|
||||
import WorkOrderAdditionalCostsModal from './WorkOrderAdditionalCostsModal';
|
||||
import WorkOrderServiceNotesModal from './WorkOrderServiceNotesModal';
|
||||
|
||||
function readWorkOrderIdFromQuery() {
|
||||
if (typeof window === 'undefined') {
|
||||
@@ -77,6 +83,9 @@ export default function WorkOrderInvoicesPdfPage() {
|
||||
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('');
|
||||
|
||||
useEffect(() => {
|
||||
hydrateAuthFromStorage();
|
||||
@@ -207,6 +216,24 @@ export default function WorkOrderInvoicesPdfPage() {
|
||||
}
|
||||
};
|
||||
|
||||
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">
|
||||
@@ -225,6 +252,14 @@ export default function WorkOrderInvoicesPdfPage() {
|
||||
>
|
||||
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>
|
||||
|
||||
@@ -244,6 +279,14 @@ export default function WorkOrderInvoicesPdfPage() {
|
||||
>
|
||||
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)}
|
||||
@@ -252,6 +295,14 @@ export default function WorkOrderInvoicesPdfPage() {
|
||||
>
|
||||
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>}
|
||||
@@ -331,6 +382,14 @@ export default function WorkOrderInvoicesPdfPage() {
|
||||
>
|
||||
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 && (
|
||||
@@ -385,6 +444,17 @@ export default function WorkOrderInvoicesPdfPage() {
|
||||
onClose={() => setEditingAdditionalCosts(false)}
|
||||
onSave={handleSaveAdditionalCostsTable}
|
||||
/>
|
||||
<WorkOrderServiceNotesModal
|
||||
open={editingServiceReportNote}
|
||||
workOrder={workOrder}
|
||||
saving={savingServiceReportNote}
|
||||
error={serviceReportNoteError}
|
||||
onClose={() => {
|
||||
setEditingServiceReportNote(false);
|
||||
setServiceReportNoteError('');
|
||||
}}
|
||||
onSave={handleSaveServiceReportNote}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
import { useEffect, useState } from 'preact/hooks';
|
||||
import ModalShell from '../ui/ModalShell';
|
||||
|
||||
export default function WorkOrderServiceNotesModal({
|
||||
open,
|
||||
workOrder,
|
||||
saving = false,
|
||||
error = '',
|
||||
onClose,
|
||||
onSave,
|
||||
}) {
|
||||
const [notes, setNotes] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
setNotes('');
|
||||
return;
|
||||
}
|
||||
setNotes(String(workOrder?.notes || ''));
|
||||
}, [open, workOrder?.id, workOrder?.notes]);
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
const handleSave = async () => {
|
||||
await onSave?.(notes);
|
||||
};
|
||||
|
||||
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-2xl 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">UREDI TEKST NAPOMENE</h3>
|
||||
<p className="text-xs text-text-muted">
|
||||
Tekst se prikazuje u PDF-u "Izvještaj servisera" na prvoj stranici.
|
||||
</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-2 px-5 py-4">
|
||||
{error ? (
|
||||
<div className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700">
|
||||
{error}
|
||||
</div>
|
||||
) : null}
|
||||
<label htmlFor="work-order-notes" className="text-sm font-medium text-text-main">
|
||||
Napomena
|
||||
</label>
|
||||
<textarea
|
||||
id="work-order-notes"
|
||||
value={notes}
|
||||
onInput={(event) => setNotes(event.currentTarget.value)}
|
||||
rows={8}
|
||||
className="w-full rounded-lg border border-border-hairline bg-canvas-base px-3 py-2 text-sm text-text-main focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="Unesite tekst napomene za PDF izvještaj servisera..."
|
||||
/>
|
||||
</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 napomenu'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</ModalShell>
|
||||
);
|
||||
}
|
||||
@@ -257,6 +257,16 @@ function replaceWorkOrderItem(workOrderId, nextValue) {
|
||||
);
|
||||
}
|
||||
|
||||
function upsertWorkOrderItem(workOrderId, nextValue) {
|
||||
const current = $workOrders.get();
|
||||
const exists = current.some((item) => String(item.id) === String(workOrderId));
|
||||
if (exists) {
|
||||
replaceWorkOrderItem(workOrderId, nextValue);
|
||||
return;
|
||||
}
|
||||
$workOrders.set([nextValue, ...current]);
|
||||
}
|
||||
|
||||
function appendLocalDraft(entity, payload, tempId) {
|
||||
const base = {
|
||||
...payload,
|
||||
@@ -547,15 +557,12 @@ export async function createWorkOrder(data) {
|
||||
|
||||
export async function updateWorkOrder(workOrderId, changes) {
|
||||
const existing = $workOrders.get().find((item) => String(item.id) === String(workOrderId));
|
||||
if (!existing) {
|
||||
throw new Error('Work order nije pronađen u lokalnom stanju.');
|
||||
}
|
||||
|
||||
const canSendNow = isBrowser() && navigator.onLine && !!$accessToken.get();
|
||||
if (canSendNow) {
|
||||
try {
|
||||
const response = await api.patch(`fleet/work-orders/${workOrderId}/`, changes);
|
||||
replaceWorkOrderItem(workOrderId, response);
|
||||
upsertWorkOrderItem(workOrderId, response);
|
||||
await persistDashboardCache();
|
||||
await logSync(
|
||||
{ entity: 'work_orders', endpoint: `fleet/work-orders/${workOrderId}/`, method: 'PATCH' },
|
||||
@@ -575,6 +582,10 @@ export async function updateWorkOrder(workOrderId, changes) {
|
||||
}
|
||||
}
|
||||
|
||||
if (!existing) {
|
||||
throw new Error('Work order nije dostupan offline. Otvorite dashboard ili pokušajte ponovno kada ste online.');
|
||||
}
|
||||
|
||||
const optimistic = {
|
||||
...existing,
|
||||
...changes,
|
||||
@@ -849,6 +860,51 @@ export async function downloadWorkOrderServiceRecordsPdf(workOrderId) {
|
||||
return payload;
|
||||
}
|
||||
|
||||
export async function downloadWorkOrderDocx(workOrderId) {
|
||||
if (!workOrderId) {
|
||||
throw new Error('Work order ID je obavezan.');
|
||||
}
|
||||
try {
|
||||
const blob = await api.get(`fleet/work-orders/${encodeURIComponent(workOrderId)}/docx/`, { responseType: 'blob' });
|
||||
saveBlobToFile(blob, `${workOrderId}.work-order.docx`);
|
||||
showToast('DOCX putnog naloga je preuzet.', 'success');
|
||||
} catch (err) {
|
||||
const msg = err?.message || 'Preuzimanje DOCX putnog naloga nije uspjelo.';
|
||||
showToast(msg, 'error');
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
export async function downloadWorkOrderServiceRecordsDocx(workOrderId) {
|
||||
if (!workOrderId) {
|
||||
throw new Error('Work order ID je obavezan.');
|
||||
}
|
||||
try {
|
||||
const blob = await api.get(`fleet/work-orders/${encodeURIComponent(workOrderId)}/service-records-docx/`, { responseType: 'blob' });
|
||||
saveBlobToFile(blob, `${workOrderId}.work-order-service-records.docx`);
|
||||
showToast('DOCX servisnih zapisa je preuzet.', 'success');
|
||||
} catch (err) {
|
||||
const msg = err?.message || 'Preuzimanje DOCX servisnih zapisa nije uspjelo.';
|
||||
showToast(msg, 'error');
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
export async function downloadWorkOrderInvoicesDocx(workOrderId) {
|
||||
if (!workOrderId) {
|
||||
throw new Error('Work order ID je obavezan.');
|
||||
}
|
||||
try {
|
||||
const blob = await api.get(`fleet/work-orders/${encodeURIComponent(workOrderId)}/invoices-docx/`, { responseType: 'blob' });
|
||||
saveBlobToFile(blob, `${workOrderId}.work-order-invoices.docx`);
|
||||
showToast('DOCX računa putnog naloga je preuzet.', 'success');
|
||||
} catch (err) {
|
||||
const msg = err?.message || 'Preuzimanje DOCX računa nije uspjelo.';
|
||||
showToast(msg, 'error');
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
export async function downloadGeneratedPdfByUrl(downloadUrl, filename = 'document.pdf') {
|
||||
if (!downloadUrl) {
|
||||
showToast('Nedostaje URL za preuzimanje PDF-a.', 'error');
|
||||
|
||||
Reference in New Issue
Block a user