This commit is contained in:
199
frontend/src/components/dashboard/TaskServiceRecordsModal.jsx
Normal file
199
frontend/src/components/dashboard/TaskServiceRecordsModal.jsx
Normal file
@@ -0,0 +1,199 @@
|
||||
import { useEffect, useMemo, useState } from 'preact/hooks';
|
||||
import ModalShell from '../ui/ModalShell';
|
||||
import { formatEntityCode } from '../../lib/displayIds';
|
||||
|
||||
function formatDate(value) {
|
||||
if (!value) return '-';
|
||||
const parsed = new Date(value);
|
||||
if (Number.isNaN(parsed.getTime())) return value;
|
||||
return parsed.toLocaleDateString('hr-HR');
|
||||
}
|
||||
|
||||
function formatCost(value) {
|
||||
const numeric = Number(value);
|
||||
if (Number.isNaN(numeric)) return '-';
|
||||
return `${numeric.toFixed(2)} EUR`;
|
||||
}
|
||||
|
||||
export default function TaskServiceRecordsModal({
|
||||
open,
|
||||
task,
|
||||
records = [],
|
||||
workOrders = [],
|
||||
onClose,
|
||||
onOpenServiceRecord,
|
||||
onAssignWorkOrder,
|
||||
onCreateServiceRecord,
|
||||
}) {
|
||||
const [showWorkOrderEditor, setShowWorkOrderEditor] = useState(false);
|
||||
const [selectedWorkOrderId, setSelectedWorkOrderId] = useState('');
|
||||
const [assigningWorkOrder, setAssigningWorkOrder] = useState(false);
|
||||
const [assignError, setAssignError] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || typeof window === 'undefined') return;
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}, [open]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!task) return;
|
||||
setSelectedWorkOrderId(task.work_order ? String(task.work_order) : '');
|
||||
setShowWorkOrderEditor(!task.work_order);
|
||||
setAssignError('');
|
||||
setAssigningWorkOrder(false);
|
||||
}, [task?.id, task?.work_order]);
|
||||
|
||||
const currentWorkOrder = useMemo(
|
||||
() => workOrders.find((item) => String(item.id) === String(task?.work_order || selectedWorkOrderId)) || null,
|
||||
[workOrders, task?.work_order, selectedWorkOrderId]
|
||||
);
|
||||
|
||||
const sortedWorkOrders = useMemo(
|
||||
() => [...workOrders].sort((a, b) => String(b.date || '').localeCompare(String(a.date || ''))),
|
||||
[workOrders]
|
||||
);
|
||||
|
||||
const handleWorkOrderChange = async (event) => {
|
||||
const nextValue = String(event.currentTarget.value || '');
|
||||
setSelectedWorkOrderId(nextValue);
|
||||
setAssignError('');
|
||||
setAssigningWorkOrder(true);
|
||||
try {
|
||||
await onAssignWorkOrder?.(task, nextValue || null);
|
||||
setShowWorkOrderEditor(false);
|
||||
} catch (error) {
|
||||
setAssignError(error?.message || 'Neuspješno spremanje putnog naloga za radni zadatak.');
|
||||
} finally {
|
||||
setAssigningWorkOrder(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!open || !task) return null;
|
||||
|
||||
return (
|
||||
<ModalShell
|
||||
onClose={onClose}
|
||||
overlayClassName="z-50 overflow-y-auto p-4 pt-2"
|
||||
contentClassName="flex items-start justify-center min-h-full"
|
||||
panelClassName="w-full max-w-4xl rounded-xl border border-border-hairline bg-canvas-elevated shadow-xl max-h-[calc(100vh-6rem)] overflow-hidden"
|
||||
>
|
||||
<div className="w-full flex flex-col h-full">
|
||||
<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">Radni zadatak detalji</h3>
|
||||
<p className="text-xs text-text-muted">
|
||||
Radni zadatak: {task.title || '-'} • Status: {task.status || '-'} • Zapisa: {records.length}
|
||||
</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="max-h-[calc(100vh-12rem)] overflow-y-auto p-4">
|
||||
<div className="space-y-4">
|
||||
<div className="rounded-lg border border-border-hairline bg-canvas-base p-3">
|
||||
<h4 className="mb-2 text-sm font-semibold text-text-main">Putni nalozi</h4>
|
||||
{currentWorkOrder && !showWorkOrderEditor ? (
|
||||
<div className="flex flex-wrap items-center justify-between gap-2 rounded border border-border-hairline bg-canvas-elevated px-3 py-2">
|
||||
<p className="text-sm text-text-main">
|
||||
{formatEntityCode('PN', currentWorkOrder.id)} • {currentWorkOrder.craneLabel || currentWorkOrder.craneInfo?.registration_number || '-'} • {currentWorkOrder.status || '-'}
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowWorkOrderEditor(true)}
|
||||
className="rounded border border-border-hairline px-2 py-1 text-xs text-text-main hover:bg-canvas-base"
|
||||
>
|
||||
Izmijeni putni nalog
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
<select
|
||||
value={selectedWorkOrderId}
|
||||
onChange={handleWorkOrderChange}
|
||||
disabled={assigningWorkOrder}
|
||||
className="w-full rounded-lg border border-border-hairline bg-canvas-elevated px-3 py-2 text-sm text-text-main disabled:opacity-60"
|
||||
>
|
||||
<option value="">— Odaberi putni nalog —</option>
|
||||
{sortedWorkOrders.map((workOrder) => (
|
||||
<option key={workOrder.id} value={String(workOrder.id)}>
|
||||
{formatEntityCode('PN', workOrder.id)} • {workOrder.craneLabel || workOrder.craneInfo?.registration_number || '-'} • {workOrder.status || '-'}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="text-xs text-text-muted">
|
||||
{assigningWorkOrder ? 'Spremanje odabira putnog naloga...' : 'Odabir se automatski sprema u bazu.'}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{assignError && (
|
||||
<div className="mt-2 rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700">
|
||||
{assignError}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border border-border-hairline bg-canvas-base p-3">
|
||||
<div className="mb-2 flex items-center justify-between gap-2">
|
||||
<h4 className="text-sm font-semibold text-text-main">Povezani servisni zapisi</h4>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onCreateServiceRecord?.(task)}
|
||||
className="rounded-lg bg-emerald-600 px-3 py-1.5 text-xs font-semibold text-white hover:bg-emerald-700"
|
||||
>
|
||||
+ Servisni zapis
|
||||
</button>
|
||||
</div>
|
||||
{records.length === 0 ? (
|
||||
<div className="rounded-lg border border-border-hairline bg-canvas-elevated px-4 py-3 text-sm text-text-muted">
|
||||
Nema povezanih servisnih zapisa za odabrani radni zadatak.
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-hidden rounded-lg border border-border-hairline">
|
||||
<table className="w-full table-auto text-sm">
|
||||
<thead className="bg-canvas-base text-left text-text-muted">
|
||||
<tr>
|
||||
<th className="px-3 py-2">Datum</th>
|
||||
<th className="px-3 py-2">Opis</th>
|
||||
<th className="px-3 py-2">Trošak</th>
|
||||
<th className="px-3 py-2">Datoteke</th>
|
||||
<th className="px-3 py-2">Akcija</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{records.map((record) => (
|
||||
<tr key={record.id} className="border-t border-border-hairline">
|
||||
<td className="px-3 py-2">{formatDate(record.service_date)}</td>
|
||||
<td className="max-w-[420px] truncate px-3 py-2" title={record.description || ''}>
|
||||
{record.description || '-'}
|
||||
</td>
|
||||
<td className="px-3 py-2">{formatCost(record.cost)}</td>
|
||||
<td className="px-3 py-2">{record.files_count ?? (record.photos_count ?? 0)}</td>
|
||||
<td className="px-3 py-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onOpenServiceRecord?.(record)}
|
||||
className="rounded border border-border-hairline px-2 py-1 text-xs text-text-main hover:bg-canvas-base"
|
||||
>
|
||||
Detalji
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ModalShell>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user