feat: dodaj ishodište i PDF račune za putni nalog
Dodano je origin_location polje kroz backend model, serializer, admin i UI modal za kreiranje/uređivanje putnog naloga (default: Zagreb). Računi putnog naloga sada prihvaćaju slike i PDF datoteke, uz backend validaciju formata, podršku za serving PDF-a i prilagodbu preview prikaza. Za greške uploada računa dodan je error toast kako bi poruke validacije bile vidljive korisniku odmah u sučelju. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -32,6 +32,7 @@ export default function WorkOrderDetailModal({ open, workOrder, onClose, onSubmi
|
||||
servicer_vehicle_start_mileage: '',
|
||||
servicer_vehicle_end_mileage: '',
|
||||
servicer_vehicle_fuel_cost: '',
|
||||
origin_location: 'Zagreb',
|
||||
location: '',
|
||||
travel_start_at: '',
|
||||
travel_end_at: '',
|
||||
@@ -92,11 +93,12 @@ export default function WorkOrderDetailModal({ open, workOrder, onClose, onSubmi
|
||||
if (!open || !workOrder) return;
|
||||
setIsEditing(false);
|
||||
setForm({
|
||||
has_travel_order: !!(workOrder.travel_start_at || workOrder.travel_end_at || workOrder.location || workOrder.start_mileage != null || workOrder.end_mileage != null),
|
||||
has_travel_order: !!(workOrder.travel_start_at || workOrder.travel_end_at || workOrder.origin_location || workOrder.location || workOrder.start_mileage != null || workOrder.end_mileage != null),
|
||||
display_code: workOrder.display_code || '',
|
||||
status: workOrder.status || 'open',
|
||||
start_mileage: String(workOrder.start_mileage ?? ''),
|
||||
end_mileage: workOrder.end_mileage == null ? '' : String(workOrder.end_mileage),
|
||||
origin_location: workOrder.origin_location || 'Zagreb',
|
||||
location: workOrder.location || '',
|
||||
travel_start_at: workOrder.travel_start_at ? String(workOrder.travel_start_at).slice(0, 16) : '',
|
||||
travel_end_at: workOrder.travel_end_at ? String(workOrder.travel_end_at).slice(0, 16) : '',
|
||||
@@ -332,6 +334,7 @@ export default function WorkOrderDetailModal({ open, workOrder, onClose, onSubmi
|
||||
servicer_vehicle_start_mileage: form.servicer_vehicle_start_mileage === '' ? null : Number(form.servicer_vehicle_start_mileage),
|
||||
servicer_vehicle_end_mileage: form.servicer_vehicle_end_mileage === '' ? null : Number(form.servicer_vehicle_end_mileage),
|
||||
servicer_vehicle_fuel_cost: form.servicer_vehicle_fuel_cost === '' ? null : Number(form.servicer_vehicle_fuel_cost),
|
||||
origin_location: form.origin_location.trim() || 'Zagreb',
|
||||
location: form.location.trim(),
|
||||
travel_start_at: travelStart ? travelStart.toISOString() : null,
|
||||
travel_end_at: travelEnd ? travelEnd.toISOString() : null,
|
||||
@@ -599,6 +602,16 @@ export default function WorkOrderDetailModal({ open, workOrder, onClose, onSubmi
|
||||
<div className="rounded-xl border border-border-hairline bg-canvas-base p-3">
|
||||
<h4 className="mb-2 text-sm font-semibold text-text-main">Putni podaci</h4>
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<label className="sm:col-span-2 flex flex-col gap-1 text-sm">
|
||||
<span className="font-medium text-text-main">Lokacija ishodišta</span>
|
||||
<input
|
||||
type="text"
|
||||
value={form.origin_location}
|
||||
onInput={(event) => setForm({ ...form, origin_location: event.currentTarget.value })}
|
||||
className="rounded-lg border border-border-hairline bg-canvas-base px-3 py-2 text-text-main disabled:bg-canvas-deep"
|
||||
disabled={!isEditing}
|
||||
/>
|
||||
</label>
|
||||
<label className="sm:col-span-2 flex flex-col gap-1 text-sm">
|
||||
<span className="font-medium text-text-main">Lokacija odredišta {form.has_travel_order ? '*' : ''}</span>
|
||||
<div className="flex gap-2">
|
||||
@@ -658,6 +671,7 @@ export default function WorkOrderDetailModal({ open, workOrder, onClose, onSubmi
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
has_travel_order: checked,
|
||||
origin_location: checked ? (prev.origin_location || 'Zagreb') : 'Zagreb',
|
||||
location: checked ? prev.location : '',
|
||||
travel_start_at: checked ? prev.travel_start_at : '',
|
||||
travel_end_at: checked ? prev.travel_end_at : '',
|
||||
@@ -980,10 +994,10 @@ export default function WorkOrderDetailModal({ open, workOrder, onClose, onSubmi
|
||||
/>
|
||||
</label>
|
||||
<label className="flex flex-col gap-1 text-sm sm:col-span-2">
|
||||
<span className="font-medium text-text-main">slika_računa</span>
|
||||
<span className="font-medium text-text-main">slika_računa / pdf_računa</span>
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
accept="image/*,.pdf,application/pdf"
|
||||
onChange={(event) => {
|
||||
const file = event.currentTarget.files && event.currentTarget.files[0] ? event.currentTarget.files[0] : null;
|
||||
setInvoiceForm((prev) => ({ ...prev, imageFile: file }));
|
||||
|
||||
@@ -23,8 +23,22 @@ function readWorkOrderIdFromQuery() {
|
||||
return params.get('workOrderId') || '';
|
||||
}
|
||||
|
||||
function InvoiceImagePreview({ imageUrl, alt }) {
|
||||
function InvoiceImagePreview({ imageUrl, alt, contentType }) {
|
||||
const [broken, setBroken] = useState(false);
|
||||
const isPdf = String(contentType || '').toLowerCase() === 'application/pdf';
|
||||
|
||||
if (isPdf && imageUrl) {
|
||||
return (
|
||||
<a
|
||||
href={imageUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
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-main hover:bg-canvas-base"
|
||||
>
|
||||
Otvori PDF
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
if (!imageUrl || broken) {
|
||||
return (
|
||||
@@ -307,6 +321,7 @@ export default function WorkOrderInvoicesPdfPage() {
|
||||
<InvoiceImagePreview
|
||||
imageUrl={getAuthenticatedMediaSrc(invoice.image_url || invoice.image) || resolveMediaUrl(invoice.image_url || invoice.image)}
|
||||
alt={`Račun ${invoice.naziv_racuna}`}
|
||||
contentType={invoice.image_content_type}
|
||||
/>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
@@ -20,6 +20,7 @@ const INITIAL_FORM = {
|
||||
servicer_vehicle_start_mileage: '',
|
||||
servicer_vehicle_end_mileage: '',
|
||||
servicer_vehicle_fuel_cost: '',
|
||||
origin_location: 'Zagreb',
|
||||
location: '',
|
||||
travel_start_at: '',
|
||||
travel_end_at: '',
|
||||
@@ -211,6 +212,7 @@ export default function WorkOrderModal({ open, selectedCrane, onClose, onSubmit
|
||||
servicer_vehicle_start_mileage: servicerStartMileage,
|
||||
servicer_vehicle_end_mileage: servicerEndMileage,
|
||||
servicer_vehicle_fuel_cost: servicerFuelCost,
|
||||
origin_location: form.origin_location.trim() || 'Zagreb',
|
||||
location: form.has_travel_order ? form.location.trim() : '',
|
||||
travel_start_at: travelStart ? travelStart.toISOString() : null,
|
||||
travel_end_at: travelEnd ? travelEnd.toISOString() : null,
|
||||
@@ -393,6 +395,18 @@ export default function WorkOrderModal({ open, selectedCrane, onClose, onSubmit
|
||||
</p>
|
||||
)}
|
||||
|
||||
<label className="sm:col-span-2 flex flex-col gap-1 text-sm">
|
||||
<span className="font-medium text-text-main">Lokacija ishodišta</span>
|
||||
<input
|
||||
type="text"
|
||||
value={form.origin_location}
|
||||
onInput={(event) => setForm({ ...form, origin_location: event.currentTarget.value })}
|
||||
className="rounded-lg border border-border-hairline bg-canvas-base px-3 py-2 text-text-main disabled:bg-canvas-deep"
|
||||
placeholder="Npr. Zagreb"
|
||||
disabled={!form.has_travel_order}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label className="sm:col-span-2 flex flex-col gap-1 text-sm">
|
||||
<span className="font-medium text-text-main">Lokacija odredišta {form.has_travel_order ? '*' : ''}</span>
|
||||
<div className="flex gap-2">
|
||||
@@ -458,6 +472,7 @@ export default function WorkOrderModal({ open, selectedCrane, onClose, onSubmit
|
||||
has_travel_order: checked,
|
||||
start_mileage: checked ? (prev.start_mileage || String(selectedCrane?.current_mileage ?? '')) : '',
|
||||
end_mileage: checked ? prev.end_mileage : '',
|
||||
origin_location: checked ? (prev.origin_location || 'Zagreb') : 'Zagreb',
|
||||
location: checked ? prev.location : '',
|
||||
travel_start_at: checked ? prev.travel_start_at : '',
|
||||
travel_end_at: checked ? prev.travel_end_at : '',
|
||||
|
||||
@@ -747,9 +747,14 @@ export async function createWorkOrderInvoice(workOrderId, data = {}, signal = nu
|
||||
formData.append('image', data.imageFile);
|
||||
}
|
||||
const options = signal ? { signal } : {};
|
||||
const response = await api.post('fleet/work-order-invoices/', formData, options);
|
||||
showToast('Račun je uspješno spremljen.', 'success');
|
||||
return response;
|
||||
try {
|
||||
const response = await api.post('fleet/work-order-invoices/', formData, options);
|
||||
showToast('Račun je uspješno spremljen.', 'success');
|
||||
return response;
|
||||
} catch (error) {
|
||||
showToast(error?.message || 'Neuspješno spremanje računa.', 'error');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function _refreshNotificationsAsync() {
|
||||
|
||||
Reference in New Issue
Block a user