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:
@@ -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',)
|
||||||
|
|||||||
@@ -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'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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 }));
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 : '',
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
Reference in New Issue
Block a user