This commit is contained in:
258
frontend/src/components/dashboard/WorkOrderInvoicesPdfPage.jsx
Normal file
258
frontend/src/components/dashboard/WorkOrderInvoicesPdfPage.jsx
Normal file
@@ -0,0 +1,258 @@
|
||||
import { useEffect, useMemo, useState } from 'preact/hooks';
|
||||
import { useStore } from '@nanostores/preact';
|
||||
import {
|
||||
downloadWorkOrderPdf,
|
||||
downloadWorkOrderInvoicesPdf,
|
||||
downloadWorkOrderServiceRecordsPdf,
|
||||
fetchWorkOrderById,
|
||||
fetchWorkOrderInvoices,
|
||||
fetchWorkOrderTaskServiceContext,
|
||||
} from '../../stores/fleetDashboardStore';
|
||||
import { $accessToken, $authReady, hydrateAuthFromStorage } from '../../stores/authStore';
|
||||
import { fetchNotifications, connectNotifications } from '../../stores/notificationStore';
|
||||
import { formatEntityCode } from '../../lib/displayIds';
|
||||
import { resolveMediaUrl } from './WorkOrderImageCarousel';
|
||||
|
||||
function readWorkOrderIdFromQuery() {
|
||||
if (typeof window === 'undefined') {
|
||||
return '';
|
||||
}
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
return params.get('workOrderId') || '';
|
||||
}
|
||||
|
||||
function InvoiceImagePreview({ imageUrl, alt }) {
|
||||
const [broken, setBroken] = useState(false);
|
||||
|
||||
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 (
|
||||
<a href={imageUrl} target="_blank" rel="noopener noreferrer" className="inline-flex">
|
||||
<img
|
||||
src={imageUrl}
|
||||
alt={alt}
|
||||
onError={() => setBroken(true)}
|
||||
className="h-28 w-28 rounded-lg border border-border-hairline object-cover"
|
||||
/>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
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('');
|
||||
|
||||
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] = await Promise.all([
|
||||
fetchWorkOrderTaskServiceContext(workOrderId),
|
||||
fetchWorkOrderById(workOrderId),
|
||||
fetchWorkOrderInvoices(workOrderId),
|
||||
]);
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
setTaskContext(contextPayload);
|
||||
setWorkOrder(order);
|
||||
setInvoices(invoiceItems);
|
||||
} 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 `${formatEntityCode('PN', workOrder.id)}`;
|
||||
}, [workOrder]);
|
||||
|
||||
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>
|
||||
</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">Task i povezani servisni zapisi</h2>
|
||||
<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>
|
||||
</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={resolveMediaUrl(photo.optimized_url || photo.image_url)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex"
|
||||
>
|
||||
<img
|
||||
src={resolveMediaUrl(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>
|
||||
<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>
|
||||
</div>
|
||||
{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) => (
|
||||
<article key={invoice.id} className="rounded-lg border border-border-hairline bg-canvas-base p-3">
|
||||
<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={resolveMediaUrl(invoice.image)}
|
||||
alt={`Račun ${invoice.naziv_racuna}`}
|
||||
/>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user