prvi kod za live uporabu
Some checks failed
ERP CI Pipeline / test (push) Has been cancelled

This commit is contained in:
mariomitte
2026-07-09 21:21:17 +02:00
parent 857bb65d52
commit 5d39e048ba
239 changed files with 25151 additions and 0 deletions

View File

@@ -0,0 +1,239 @@
import { useMemo } from 'preact/hooks';
import ModalShell from '../ui/ModalShell';
import { downloadWorkOrderInvoicesPdf, downloadWorkOrderPdf, downloadWorkOrderServiceRecordsPdf } from '../../stores/fleetDashboardStore';
function formatTimestamp(value) {
if (!value) return '';
const date = new Date(value);
if (Number.isNaN(date.getTime())) return '';
return new Intl.DateTimeFormat('hr-HR', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
}).format(date);
}
function detectNotificationKind(notification) {
const title = String(notification?.title || '').toLowerCase();
if (title.includes('servisni kontekst')) return 'service_context';
if (title.includes('pdf')) return 'work_order_pdf';
if (title.includes('putni nalog kreiran')) return 'work_order_created';
if (title.includes('novi putni nalog')) return 'work_order_assigned';
if (title.includes('putni nalog zatvoren')) return 'work_order_closed';
if (title.includes('putni nalog obrisan')) return 'work_order_deleted';
if (title.includes('servis zabilježen')) return 'service_record_created';
if (title.includes('servis na vašem vozilu')) return 'service_record_vehicle_update';
if (title.includes('uskoro servis')) return 'service_due_warning';
return 'generic';
}
function getEntityAction(notification, meta) {
const metadata = notification?.metadata || {};
if (metadata.entity_type === 'service_context' && metadata.vehicle_id) {
return {
label: 'Otvori servisni kontekst',
run() {
window.dispatchEvent(new CustomEvent('notification:open-entity', {
detail: {
entityType: 'service_context',
vehicleId: metadata.vehicle_id,
section: metadata.section || meta.section,
},
}));
},
};
}
if (
metadata.entity_type === 'work_order_pdf' &&
metadata.stage === 'completed' &&
metadata.work_order_id
) {
return {
label: metadata.pdf_type === 'invoices'
? 'Preuzmi PDF računa'
: metadata.pdf_type === 'service_records'
? 'Preuzmi PDF servisnih zapisa'
: 'Preuzmi PDF putnog naloga',
run() {
if (metadata.pdf_type === 'invoices') {
return downloadWorkOrderInvoicesPdf(metadata.work_order_id);
}
if (metadata.pdf_type === 'service_records') {
return downloadWorkOrderServiceRecordsPdf(metadata.work_order_id);
}
return downloadWorkOrderPdf(metadata.work_order_id);
},
};
}
if (metadata.entity_type === 'work_order' && metadata.work_order_id) {
return {
label: 'Otvori detalje naloga',
run() {
window.dispatchEvent(new CustomEvent('notification:open-entity', {
detail: {
entityType: 'work_order',
entityId: metadata.work_order_id,
vehicleId: metadata.vehicle_id || null,
section: metadata.section || meta.section,
},
}));
},
};
}
if (metadata.entity_type === 'service_record' && metadata.service_record_id) {
return {
label: 'Otvori detalje servisa',
run() {
window.dispatchEvent(new CustomEvent('notification:open-entity', {
detail: {
entityType: 'service_record',
entityId: metadata.service_record_id,
vehicleId: metadata.vehicle_id || null,
section: metadata.section || meta.section,
},
}));
},
};
}
return {
label: `Otvori ${meta.category}`,
run() {
window.dispatchEvent(new CustomEvent('navbar:navigate', { detail: meta.section }));
},
};
}
function getNotificationTypeMeta(kind) {
if (kind.startsWith('work_order_pdf')) {
return {
category: 'PDF dokumenti',
section: 'work-orders',
recommendation: 'Možete odmah preuzeti generirani PDF dokument.',
};
}
if (kind.startsWith('work_order')) {
return {
category: 'Putni nalozi',
section: 'work-orders',
recommendation: 'Provjerite status naloga i povezano vozilo.',
};
}
if (kind === 'service_context') {
return {
category: 'Servisni kontekst',
section: 'service-records',
recommendation: 'Kontekst je postavljen i spreman za rad na servisnim zapisima.',
};
}
if (kind.startsWith('service_')) {
return {
category: 'Servisni zapisi',
section: 'service-records',
recommendation: 'Provjerite detalje servisa i priloge.',
};
}
return {
category: 'Sustav',
section: 'dashboard',
recommendation: 'Pregledajte dashboard za dodatni kontekst.',
};
}
function levelClass(level) {
if (level === 'critical') return 'bg-red-100 text-red-700';
if (level === 'warning') return 'bg-amber-100 text-amber-700';
return 'bg-indigo-100 text-indigo-700';
}
export default function NotificationDetailModal({ open, notification, onClose }) {
const kind = useMemo(() => detectNotificationKind(notification), [notification]);
const meta = useMemo(() => getNotificationTypeMeta(kind), [kind]);
const action = useMemo(() => getEntityAction(notification, meta), [notification, meta]);
const showPdfLink = Boolean(
notification?.metadata?.entity_type === 'work_order_pdf' &&
notification?.metadata?.stage === 'completed' &&
notification?.metadata?.work_order_id
);
if (!open || !notification) {
return null;
}
return (
<ModalShell
onClose={onClose}
overlayClassName="z-50 p-4"
contentClassName="flex items-center justify-center min-h-full"
panelClassName="max-h-[calc(100vh-2rem)] w-full max-w-2xl rounded-xl border border-border-hairline bg-canvas-elevated shadow-xl overflow-y-auto"
>
<div>
<div className="flex items-center justify-between border-b border-border-hairline px-5 py-4">
<div>
<h3 className="text-lg font-semibold text-text-main">Detalji notifikacije</h3>
<p className="text-xs text-text-muted">{formatTimestamp(notification.created_at)}</p>
</div>
<button
type="button"
onClick={onClose}
className="rounded-md px-2 py-1 text-sm text-text-muted hover:bg-canvas-deep"
aria-label="Zatvori"
>
</button>
</div>
<div className="space-y-4 px-5 py-4">
<div className="flex items-center gap-2">
<span className={`rounded-full px-2.5 py-1 text-xs font-semibold ${levelClass(notification.level)}`}>
{notification.level || 'info'}
</span>
<span className="rounded-full bg-canvas-base px-2.5 py-1 text-xs font-semibold text-text-muted">
{meta.category}
</span>
</div>
<div className="rounded-lg border border-border-hairline bg-canvas-base p-3">
<p className="text-sm font-semibold text-text-main">{notification.title || 'Obavijest'}</p>
<p className="mt-2 text-sm text-text-muted">{notification.message || '-'}</p>
{showPdfLink && (
<button
type="button"
onClick={() => action.run()}
className="mt-2 text-sm font-medium text-indigo-600 underline underline-offset-2 hover:text-indigo-700"
>
{action.label}
</button>
)}
</div>
<div className="rounded-lg border border-border-hairline bg-canvas-base p-3">
<p className="text-xs uppercase tracking-wide text-text-muted">Preporuka</p>
<p className="mt-1 text-sm text-text-main">{meta.recommendation}</p>
</div>
<div className="flex justify-end gap-2 border-t border-border-hairline pt-3">
<button
type="button"
onClick={() => {
action.run();
onClose?.();
}}
className="rounded-lg bg-indigo-600 px-4 py-2 text-sm font-semibold text-white hover:bg-indigo-700"
>
{action.label}
</button>
<button
type="button"
onClick={onClose}
className="rounded-lg border border-border-hairline px-4 py-2 text-sm font-medium text-text-main hover:bg-canvas-deep"
>
Zatvori
</button>
</div>
</div>
</div>
</ModalShell>
);
}