feat: dodaj ishodište i PDF račune za putni nalog
Some checks failed
ERP CI/CD Pipeline / test (push) Has been cancelled
ERP CI/CD Pipeline / Deploy (server git pull + compose) (push) Has been cancelled

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:
mariomitte
2026-07-19 19:54:15 +02:00
parent cbcbf0b3af
commit d35eaeccf4
12 changed files with 175 additions and 14 deletions

View File

@@ -44,7 +44,7 @@ class ServiceAttachmentInline(admin.TabularInline):
class WorkOrderInline(admin.TabularInline): class WorkOrderInline(admin.TabularInline):
model = WorkOrder model = WorkOrder
extra = 0 extra = 0
fields = ('display_code', 'creator', 'date', 'start_mileage', 'end_mileage', 'location', 'travel_start_at', 'travel_end_at', 'purpose', 'status') fields = ('display_code', 'creator', 'date', 'start_mileage', 'end_mileage', 'origin_location', 'location', 'travel_start_at', 'travel_end_at', 'purpose', 'status')
readonly_fields = ('date',) readonly_fields = ('date',)
@@ -176,7 +176,7 @@ class CraneAdmin(VehicleAdmin):
@admin.register(WorkOrder) @admin.register(WorkOrder)
class WorkOrderAdmin(admin.ModelAdmin): class WorkOrderAdmin(admin.ModelAdmin):
list_display = ('display_code', 'id', 'vehicle', 'creator', 'date', 'location', 'travel_start_at', 'travel_end_at', 'start_mileage', 'end_mileage', 'distance_display', 'status') list_display = ('display_code', 'id', 'vehicle', 'creator', 'date', 'origin_location', 'location', 'travel_start_at', 'travel_end_at', 'start_mileage', 'end_mileage', 'distance_display', 'status')
list_filter = ('status', 'date', 'vehicle') list_filter = ('status', 'date', 'vehicle')
search_fields = ('display_code', 'vehicle__registration_number', 'creator__username', 'purpose') search_fields = ('display_code', 'vehicle__registration_number', 'creator__username', 'purpose')
readonly_fields = ('date',) readonly_fields = ('date',)

View File

@@ -0,0 +1,16 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('fleet', '0026_backfill_workorder_display_code'),
]
operations = [
migrations.AddField(
model_name='workorder',
name='origin_location',
field=models.CharField(blank=True, max_length=255, verbose_name='Lokacija ishodišta'),
),
]

View File

@@ -0,0 +1,18 @@
from django.db import migrations, models
import modules.fleet.models
class Migration(migrations.Migration):
dependencies = [
('fleet', '0027_workorder_origin_location'),
]
operations = [
migrations.AlterField(
model_name='workorderinvoice',
name='image',
field=models.FileField(blank=True, null=True, upload_to=modules.fleet.models._work_order_invoice_upload_to, verbose_name='Datoteka računa'),
),
]

View File

@@ -285,6 +285,7 @@ class WorkOrder(BaseModel):
blank=True, blank=True,
verbose_name=_("Vozilo servisera - trošak goriva (EUR)"), verbose_name=_("Vozilo servisera - trošak goriva (EUR)"),
) )
origin_location = models.CharField(max_length=255, blank=True, verbose_name=_("Lokacija ishodišta"))
location = models.CharField(max_length=255, blank=True, verbose_name=_("Lokacija odredišta")) location = models.CharField(max_length=255, blank=True, verbose_name=_("Lokacija odredišta"))
travel_start_at = models.DateTimeField(null=True, blank=True, verbose_name=_("Početak puta")) travel_start_at = models.DateTimeField(null=True, blank=True, verbose_name=_("Početak puta"))
travel_end_at = models.DateTimeField(null=True, blank=True, verbose_name=_("Kraj puta")) travel_end_at = models.DateTimeField(null=True, blank=True, verbose_name=_("Kraj puta"))
@@ -351,11 +352,11 @@ class WorkOrderInvoice(BaseModel):
lokacija = models.CharField(max_length=255, blank=True, verbose_name=_("Lokacija")) lokacija = models.CharField(max_length=255, blank=True, verbose_name=_("Lokacija"))
datum = models.DateField(default=timezone.now, verbose_name=_("Datum")) datum = models.DateField(default=timezone.now, verbose_name=_("Datum"))
opis = models.TextField(blank=True, verbose_name=_("Opis")) opis = models.TextField(blank=True, verbose_name=_("Opis"))
image = models.ImageField( image = models.FileField(
upload_to=_work_order_invoice_upload_to, upload_to=_work_order_invoice_upload_to,
null=True, null=True,
blank=True, blank=True,
verbose_name=_("Slika računa"), verbose_name=_("Datoteka računa"),
) )
created_by = models.ForeignKey( created_by = models.ForeignKey(
settings.AUTH_USER_MODEL, settings.AUTH_USER_MODEL,

View File

@@ -1,5 +1,6 @@
from rest_framework import serializers from rest_framework import serializers
import re import re
from pathlib import Path
from django.urls import reverse from django.urls import reverse
from django.utils import timezone from django.utils import timezone
from .models import ( from .models import (
@@ -104,7 +105,7 @@ class WorkOrderSerializer(serializers.ModelSerializer):
'servicer_vehicle_registration', 'servicer_vehicle_make_model', 'servicer_vehicle_registration', 'servicer_vehicle_make_model',
'servicer_vehicle_start_mileage', 'servicer_vehicle_end_mileage', 'servicer_vehicle_start_mileage', 'servicer_vehicle_end_mileage',
'servicer_vehicle_fuel_cost', 'servicer_vehicle_fuel_cost',
'location', 'travel_start_at', 'travel_end_at', 'origin_location', 'location', 'travel_start_at', 'travel_end_at',
'purpose', 'notes', 'status' 'purpose', 'notes', 'status'
] ]
read_only_fields = ['id', 'date'] read_only_fields = ['id', 'date']
@@ -119,6 +120,7 @@ class WorkOrderSerializer(serializers.ModelSerializer):
'servicer_vehicle_start_mileage': {'required': False, 'allow_null': True}, 'servicer_vehicle_start_mileage': {'required': False, 'allow_null': True},
'servicer_vehicle_end_mileage': {'required': False, 'allow_null': True}, 'servicer_vehicle_end_mileage': {'required': False, 'allow_null': True},
'servicer_vehicle_fuel_cost': {'required': False, 'allow_null': True}, 'servicer_vehicle_fuel_cost': {'required': False, 'allow_null': True},
'origin_location': {'required': False, 'allow_blank': True},
'location': {'required': False, 'allow_blank': True}, 'location': {'required': False, 'allow_blank': True},
'travel_start_at': {'required': False, 'allow_null': True}, 'travel_start_at': {'required': False, 'allow_null': True},
'travel_end_at': {'required': False, 'allow_null': True}, 'travel_end_at': {'required': False, 'allow_null': True},
@@ -211,6 +213,7 @@ class WorkOrderSerializer(serializers.ModelSerializer):
class WorkOrderInvoiceSerializer(serializers.ModelSerializer): class WorkOrderInvoiceSerializer(serializers.ModelSerializer):
image_url = serializers.SerializerMethodField() image_url = serializers.SerializerMethodField()
image_content_type = serializers.SerializerMethodField()
class Meta: class Meta:
model = WorkOrderInvoice model = WorkOrderInvoice
@@ -223,6 +226,7 @@ class WorkOrderInvoiceSerializer(serializers.ModelSerializer):
'opis', 'opis',
'image', 'image',
'image_url', 'image_url',
'image_content_type',
'created_by', 'created_by',
'created_at', 'created_at',
] ]
@@ -238,6 +242,31 @@ class WorkOrderInvoiceSerializer(serializers.ModelSerializer):
reverse('work-order-invoice-image', kwargs={'pk': obj.pk}) reverse('work-order-invoice-image', kwargs={'pk': obj.pk})
) )
def get_image_content_type(self, obj):
if not obj.image:
return None
suffix = Path(obj.image.name or '').suffix.lower()
if suffix == '.pdf':
return 'application/pdf'
if suffix in {'.jpg', '.jpeg'}:
return 'image/jpeg'
if suffix == '.png':
return 'image/png'
if suffix == '.webp':
return 'image/webp'
if suffix == '.gif':
return 'image/gif'
return 'application/octet-stream'
def validate_image(self, value):
if not value:
return value
suffix = Path(getattr(value, 'name', '')).suffix.lower()
allowed_suffixes = {'.jpg', '.jpeg', '.png', '.webp', '.gif', '.pdf'}
if suffix not in allowed_suffixes:
raise serializers.ValidationError('Datoteka računa mora biti slika (.jpg, .jpeg, .png, .webp, .gif) ili PDF (.pdf).')
return value
class WorkOrderPhotoSerializer(serializers.ModelSerializer): class WorkOrderPhotoSerializer(serializers.ModelSerializer):
class Meta: class Meta:

View File

@@ -6,6 +6,7 @@ from django.utils import timezone
from django.core.files.base import ContentFile from django.core.files.base import ContentFile
import logging import logging
from io import BytesIO from io import BytesIO
from pathlib import Path
import base64 import base64
from PIL import Image, UnidentifiedImageError from PIL import Image, UnidentifiedImageError
from reportlab.lib.pagesizes import A4 from reportlab.lib.pagesizes import A4
@@ -165,6 +166,11 @@ def _build_work_order_invoices_pdf(work_order):
pdf.drawString(margin, y, "slika: Nema slike.") pdf.drawString(margin, y, "slika: Nema slike.")
continue continue
suffix = Path(invoice.image.name or '').suffix.lower()
if suffix == '.pdf':
pdf.drawString(margin, y, "prilog: PDF račun (otvorite datoteku računa za pregled).")
continue
try: try:
invoice.image.open('rb') invoice.image.open('rb')
with Image.open(invoice.image) as source: with Image.open(invoice.image) as source:

View File

@@ -22,6 +22,15 @@ def create_test_image(filename='test.jpg', size=(40, 40), color='red'):
) )
def create_test_pdf(filename='test.pdf'):
content = b'%PDF-1.4\n1 0 obj<</Type/Catalog>>endobj\ntrailer<</Root 1 0 R>>\n%%EOF'
return SimpleUploadedFile(
name=filename,
content=content,
content_type='application/pdf',
)
class WorkOrderImagesEndpointTests(TestCase): class WorkOrderImagesEndpointTests(TestCase):
def setUp(self): def setUp(self):
user_model = get_user_model() user_model = get_user_model()
@@ -138,3 +147,23 @@ class WorkOrderImagesEndpointTests(TestCase):
image_response = self.client.get(image_url) image_response = self.client.get(image_url)
self.assertEqual(image_response.status_code, 200) self.assertEqual(image_response.status_code, 200)
self.assertTrue(image_response['Content-Type'].startswith('image/')) self.assertTrue(image_response['Content-Type'].startswith('image/'))
def test_work_order_invoice_serializer_supports_pdf_file(self):
invoice = WorkOrderInvoice.objects.create(
work_order=self.work_order,
naziv_racuna='PDF račun',
image=create_test_pdf('invoice.pdf'),
created_by=self.user,
)
list_url = f"{reverse('work-order-invoice-list')}?work_order_id={self.work_order.pk}"
list_response = self.client.get(list_url)
self.assertEqual(list_response.status_code, 200)
self.assertEqual(len(list_response.data), 1)
image_url = list_response.data[0].get('image_url')
self.assertIsNotNone(image_url)
self.assertIn(f'/api/fleet/work-order-invoices/{invoice.pk}/image/', image_url)
self.assertEqual(list_response.data[0].get('image_content_type'), 'application/pdf')
file_response = self.client.get(image_url)
self.assertEqual(file_response.status_code, 200)
self.assertEqual(file_response['Content-Type'], 'application/pdf')

View File

@@ -3,6 +3,7 @@
from io import BytesIO from io import BytesIO
import base64 import base64
from datetime import timedelta from datetime import timedelta
from pathlib import Path
from PIL import Image, UnidentifiedImageError from PIL import Image, UnidentifiedImageError
from django.conf import settings from django.conf import settings
@@ -401,6 +402,7 @@ def _build_work_order_pdf(work_order):
transport_total = float(work_order.servicer_vehicle_fuel_cost or 0.0) transport_total = float(work_order.servicer_vehicle_fuel_cost or 0.0)
grand_total = daily_total + transport_total grand_total = daily_total + transport_total
place_label = (work_order.location or 'Zagreb').split(',')[0].strip() or 'Zagreb' place_label = (work_order.location or 'Zagreb').split(',')[0].strip() or 'Zagreb'
origin_label = (work_order.origin_location or 'Zagreb').split(',')[0].strip() or 'Zagreb'
invoice_names = [inv.naziv_racuna for inv in invoices[:5] if inv.naziv_racuna] invoice_names = [inv.naziv_racuna for inv in invoices[:5] if inv.naziv_racuna]
attachments_text = ', '.join(invoice_names) if invoice_names else '-' attachments_text = ', '.join(invoice_names) if invoice_names else '-'
assigned_servicer_vehicle = ( assigned_servicer_vehicle = (
@@ -598,7 +600,7 @@ def _build_work_order_pdf(work_order):
y, y,
[ [
["RELACIJA od", "RELACIJA do", "Vrsta prijevoznog sredstva", "Razred [km]", "Iznos za prijevoz", "Ukupan iznos"], ["RELACIJA od", "RELACIJA do", "Vrsta prijevoznog sredstva", "Razred [km]", "Iznos za prijevoz", "Ukupan iznos"],
[place_label or '-', work_order.location or '-', f"{servicer_vehicle_label}, {servicer_vehicle_registration}", str(work_order.distance or 0), _fmt_eur(transport_total), _fmt_eur(transport_total)], [origin_label or '-', work_order.location or '-', f"{servicer_vehicle_label}, {servicer_vehicle_registration}", str(work_order.distance or 0), _fmt_eur(transport_total), _fmt_eur(transport_total)],
["", "", "", "", "", ""], ["", "", "", "", "", ""],
["", "", "", "", "", ""], ["", "", "", "", "", ""],
], ],
@@ -1601,7 +1603,7 @@ class WorkOrderViewSet(viewsets.ModelViewSet):
<td>{escape(invoice.lokacija or '-')}</td> <td>{escape(invoice.lokacija or '-')}</td>
<td>{escape(datum)}</td> <td>{escape(datum)}</td>
<td>{escape(invoice.opis or '-')}</td> <td>{escape(invoice.opis or '-')}</td>
<td>{f'<a href="{escape(image_url)}" target="_blank" rel="noopener noreferrer">Otvori sliku</a>' if image_url else '-'}</td> <td>{f'<a href="{escape(image_url)}" target="_blank" rel="noopener noreferrer">Otvori dokument</a>' if image_url else '-'}</td>
</tr> </tr>
""" """
) )
@@ -1791,6 +1793,17 @@ class WorkOrderInvoiceViewSet(viewsets.ModelViewSet):
if not invoice.image: if not invoice.image:
raise DRFValidationError({"detail": "Slika računa nije dostupna."}) raise DRFValidationError({"detail": "Slika računa nije dostupna."})
suffix = Path(invoice.image.name or '').suffix.lower()
if suffix == '.pdf':
invoice.image.open('rb')
try:
response = HttpResponse(invoice.image.read(), content_type='application/pdf')
finally:
invoice.image.close()
response['Cache-Control'] = 'private, max-age=86400'
response['Content-Disposition'] = f'inline; filename="invoice-{invoice.pk}.pdf"'
return response
width = _parse_positive_int( width = _parse_positive_int(
request.query_params.get('w'), request.query_params.get('w'),
field_name='w', field_name='w',

View File

@@ -32,6 +32,7 @@ export default function WorkOrderDetailModal({ open, workOrder, onClose, onSubmi
servicer_vehicle_start_mileage: '', servicer_vehicle_start_mileage: '',
servicer_vehicle_end_mileage: '', servicer_vehicle_end_mileage: '',
servicer_vehicle_fuel_cost: '', servicer_vehicle_fuel_cost: '',
origin_location: 'Zagreb',
location: '', location: '',
travel_start_at: '', travel_start_at: '',
travel_end_at: '', travel_end_at: '',
@@ -92,11 +93,12 @@ export default function WorkOrderDetailModal({ open, workOrder, onClose, onSubmi
if (!open || !workOrder) return; if (!open || !workOrder) return;
setIsEditing(false); setIsEditing(false);
setForm({ 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 || '', display_code: workOrder.display_code || '',
status: workOrder.status || 'open', status: workOrder.status || 'open',
start_mileage: String(workOrder.start_mileage ?? ''), start_mileage: String(workOrder.start_mileage ?? ''),
end_mileage: workOrder.end_mileage == null ? '' : String(workOrder.end_mileage), end_mileage: workOrder.end_mileage == null ? '' : String(workOrder.end_mileage),
origin_location: workOrder.origin_location || 'Zagreb',
location: workOrder.location || '', location: workOrder.location || '',
travel_start_at: workOrder.travel_start_at ? String(workOrder.travel_start_at).slice(0, 16) : '', 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) : '', 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_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_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), 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(), location: form.location.trim(),
travel_start_at: travelStart ? travelStart.toISOString() : null, travel_start_at: travelStart ? travelStart.toISOString() : null,
travel_end_at: travelEnd ? travelEnd.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"> <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> <h4 className="mb-2 text-sm font-semibold text-text-main">Putni podaci</h4>
<div className="grid gap-4 sm:grid-cols-2"> <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"> <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> <span className="font-medium text-text-main">Lokacija odredišta {form.has_travel_order ? '*' : ''}</span>
<div className="flex gap-2"> <div className="flex gap-2">
@@ -658,6 +671,7 @@ export default function WorkOrderDetailModal({ open, workOrder, onClose, onSubmi
setForm((prev) => ({ setForm((prev) => ({
...prev, ...prev,
has_travel_order: checked, has_travel_order: checked,
origin_location: checked ? (prev.origin_location || 'Zagreb') : 'Zagreb',
location: checked ? prev.location : '', location: checked ? prev.location : '',
travel_start_at: checked ? prev.travel_start_at : '', travel_start_at: checked ? prev.travel_start_at : '',
travel_end_at: checked ? prev.travel_end_at : '', travel_end_at: checked ? prev.travel_end_at : '',
@@ -980,10 +994,10 @@ export default function WorkOrderDetailModal({ open, workOrder, onClose, onSubmi
/> />
</label> </label>
<label className="flex flex-col gap-1 text-sm sm:col-span-2"> <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 <input
type="file" type="file"
accept="image/*" accept="image/*,.pdf,application/pdf"
onChange={(event) => { onChange={(event) => {
const file = event.currentTarget.files && event.currentTarget.files[0] ? event.currentTarget.files[0] : null; const file = event.currentTarget.files && event.currentTarget.files[0] ? event.currentTarget.files[0] : null;
setInvoiceForm((prev) => ({ ...prev, imageFile: file })); setInvoiceForm((prev) => ({ ...prev, imageFile: file }));

View File

@@ -23,8 +23,22 @@ function readWorkOrderIdFromQuery() {
return params.get('workOrderId') || ''; return params.get('workOrderId') || '';
} }
function InvoiceImagePreview({ imageUrl, alt }) { function InvoiceImagePreview({ imageUrl, alt, contentType }) {
const [broken, setBroken] = useState(false); 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) { if (!imageUrl || broken) {
return ( return (
@@ -307,6 +321,7 @@ export default function WorkOrderInvoicesPdfPage() {
<InvoiceImagePreview <InvoiceImagePreview
imageUrl={getAuthenticatedMediaSrc(invoice.image_url || invoice.image) || resolveMediaUrl(invoice.image_url || invoice.image)} imageUrl={getAuthenticatedMediaSrc(invoice.image_url || invoice.image) || resolveMediaUrl(invoice.image_url || invoice.image)}
alt={`Račun ${invoice.naziv_racuna}`} alt={`Račun ${invoice.naziv_racuna}`}
contentType={invoice.image_content_type}
/> />
</div> </div>
</article> </article>

View File

@@ -20,6 +20,7 @@ const INITIAL_FORM = {
servicer_vehicle_start_mileage: '', servicer_vehicle_start_mileage: '',
servicer_vehicle_end_mileage: '', servicer_vehicle_end_mileage: '',
servicer_vehicle_fuel_cost: '', servicer_vehicle_fuel_cost: '',
origin_location: 'Zagreb',
location: '', location: '',
travel_start_at: '', travel_start_at: '',
travel_end_at: '', travel_end_at: '',
@@ -211,6 +212,7 @@ export default function WorkOrderModal({ open, selectedCrane, onClose, onSubmit
servicer_vehicle_start_mileage: servicerStartMileage, servicer_vehicle_start_mileage: servicerStartMileage,
servicer_vehicle_end_mileage: servicerEndMileage, servicer_vehicle_end_mileage: servicerEndMileage,
servicer_vehicle_fuel_cost: servicerFuelCost, servicer_vehicle_fuel_cost: servicerFuelCost,
origin_location: form.origin_location.trim() || 'Zagreb',
location: form.has_travel_order ? form.location.trim() : '', location: form.has_travel_order ? form.location.trim() : '',
travel_start_at: travelStart ? travelStart.toISOString() : null, travel_start_at: travelStart ? travelStart.toISOString() : null,
travel_end_at: travelEnd ? travelEnd.toISOString() : null, travel_end_at: travelEnd ? travelEnd.toISOString() : null,
@@ -393,6 +395,18 @@ export default function WorkOrderModal({ open, selectedCrane, onClose, onSubmit
</p> </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"> <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> <span className="font-medium text-text-main">Lokacija odredišta {form.has_travel_order ? '*' : ''}</span>
<div className="flex gap-2"> <div className="flex gap-2">
@@ -458,6 +472,7 @@ export default function WorkOrderModal({ open, selectedCrane, onClose, onSubmit
has_travel_order: checked, has_travel_order: checked,
start_mileage: checked ? (prev.start_mileage || String(selectedCrane?.current_mileage ?? '')) : '', start_mileage: checked ? (prev.start_mileage || String(selectedCrane?.current_mileage ?? '')) : '',
end_mileage: checked ? prev.end_mileage : '', end_mileage: checked ? prev.end_mileage : '',
origin_location: checked ? (prev.origin_location || 'Zagreb') : 'Zagreb',
location: checked ? prev.location : '', location: checked ? prev.location : '',
travel_start_at: checked ? prev.travel_start_at : '', travel_start_at: checked ? prev.travel_start_at : '',
travel_end_at: checked ? prev.travel_end_at : '', travel_end_at: checked ? prev.travel_end_at : '',

View File

@@ -747,9 +747,14 @@ export async function createWorkOrderInvoice(workOrderId, data = {}, signal = nu
formData.append('image', data.imageFile); formData.append('image', data.imageFile);
} }
const options = signal ? { signal } : {}; const options = signal ? { signal } : {};
const response = await api.post('fleet/work-order-invoices/', formData, options); try {
showToast('Račun je uspješno spremljen.', 'success'); const response = await api.post('fleet/work-order-invoices/', formData, options);
return response; 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() { async function _refreshNotificationsAsync() {