feat: dodaj display_code polje za putne naloge (MT070626 format)
- Dodano polje 'display_code' na WorkOrder model (format: 2 inicijala + ddmmyy) - Migracije za kreiranje polja i backfill postojećih naloga - Ažuriran WorkOrderSerializer za validaciju i serijalizaciju display_code - Backend tasks za auto-generiranje display_code pri kreiranju - Frontend komponente ažurirane za prikaz display_code umjesto UUID: * FleetDashboardShell tablica putnih naloga * WorkOrderDetailModal * TaskCreateModal * Sve PDF stranice - displayIds.js utility za formatiranjeDisplay ID-eva - Admin interface prikazuje display_code
This commit is contained in:
@@ -66,7 +66,7 @@ class TeamMemberSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CustomUser
|
model = CustomUser
|
||||||
fields = ['id', 'email', 'full_name', 'is_team_member']
|
fields = ['id', 'email', 'full_name', 'is_team_member', 'is_serviser']
|
||||||
read_only_fields = fields
|
read_only_fields = fields
|
||||||
|
|
||||||
def get_full_name(self, obj):
|
def get_full_name(self, obj):
|
||||||
|
|||||||
@@ -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 = ('creator', 'date', 'start_mileage', 'end_mileage', 'location', 'travel_start_at', 'travel_end_at', 'purpose', 'status')
|
fields = ('display_code', 'creator', 'date', 'start_mileage', 'end_mileage', 'location', 'travel_start_at', 'travel_end_at', 'purpose', 'status')
|
||||||
readonly_fields = ('date',)
|
readonly_fields = ('date',)
|
||||||
|
|
||||||
|
|
||||||
@@ -176,9 +176,9 @@ class CraneAdmin(VehicleAdmin):
|
|||||||
|
|
||||||
@admin.register(WorkOrder)
|
@admin.register(WorkOrder)
|
||||||
class WorkOrderAdmin(admin.ModelAdmin):
|
class WorkOrderAdmin(admin.ModelAdmin):
|
||||||
list_display = ('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', '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 = ('vehicle__registration_number', 'creator__username', 'purpose')
|
search_fields = ('display_code', 'vehicle__registration_number', 'creator__username', 'purpose')
|
||||||
readonly_fields = ('date',)
|
readonly_fields = ('date',)
|
||||||
ordering = ('-date',)
|
ordering = ('-date',)
|
||||||
inlines = (WorkOrderInvoiceInline, WorkOrderPhotoInline)
|
inlines = (WorkOrderInvoiceInline, WorkOrderPhotoInline)
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# Generated by Django 5.2.15 on 2026-07-17 21:55
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('fleet', '0024_alter_workorderphoto_created_at_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='workorder',
|
||||||
|
name='display_code',
|
||||||
|
field=models.CharField(
|
||||||
|
blank=True,
|
||||||
|
db_index=True,
|
||||||
|
help_text='Format: MT070626 (2 slova + 6 znamenki).',
|
||||||
|
max_length=8,
|
||||||
|
verbose_name='Prikaz putnog naloga',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
def backfill_workorder_display_code(apps, schema_editor):
|
||||||
|
WorkOrder = apps.get_model('fleet', 'WorkOrder')
|
||||||
|
for work_order in WorkOrder.objects.select_related('creator').all():
|
||||||
|
current_code = str(getattr(work_order, 'display_code', '') or '').strip().upper()
|
||||||
|
if current_code:
|
||||||
|
continue
|
||||||
|
creator = getattr(work_order, 'creator', None)
|
||||||
|
first_name = str(getattr(creator, 'first_name', '') or '').strip()
|
||||||
|
last_name = str(getattr(creator, 'last_name', '') or '').strip()
|
||||||
|
if not first_name or not last_name:
|
||||||
|
continue
|
||||||
|
order_date = getattr(work_order, 'date', None)
|
||||||
|
if not order_date:
|
||||||
|
continue
|
||||||
|
display_code = f"{first_name[0].upper()}{last_name[0].upper()}{order_date.strftime('%d%m%y')}"
|
||||||
|
WorkOrder.objects.filter(pk=work_order.pk).update(display_code=display_code)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('fleet', '0025_workorder_display_code'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(backfill_workorder_display_code, migrations.RunPython.noop),
|
||||||
|
]
|
||||||
@@ -287,6 +287,13 @@ class WorkOrder(BaseModel):
|
|||||||
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"))
|
||||||
|
display_code = models.CharField(
|
||||||
|
max_length=8,
|
||||||
|
blank=True,
|
||||||
|
db_index=True,
|
||||||
|
verbose_name=_("Prikaz putnog naloga"),
|
||||||
|
help_text=_("Format: MT070626 (2 slova + 6 znamenki)."),
|
||||||
|
)
|
||||||
purpose = models.CharField(max_length=50, choices=PURPOSE_CHOICES, blank=True, verbose_name=_("Svrha"))
|
purpose = models.CharField(max_length=50, choices=PURPOSE_CHOICES, blank=True, verbose_name=_("Svrha"))
|
||||||
notes = models.TextField(blank=True, verbose_name=_("Bilješke"))
|
notes = models.TextField(blank=True, verbose_name=_("Bilješke"))
|
||||||
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='open', verbose_name=_("Status"))
|
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='open', verbose_name=_("Status"))
|
||||||
@@ -322,6 +329,12 @@ class WorkOrder(BaseModel):
|
|||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
super().clean()
|
super().clean()
|
||||||
|
if self.display_code:
|
||||||
|
self.display_code = str(self.display_code).strip().upper()
|
||||||
|
if not re.fullmatch(r"[A-Z]{2}[0-9]{6}", self.display_code):
|
||||||
|
raise ValidationError({
|
||||||
|
"display_code": _("Prikaz putnog naloga mora biti u formatu MT070626 (2 slova + 6 znamenki).")
|
||||||
|
})
|
||||||
if self.travel_start_at and self.travel_end_at and self.travel_end_at < self.travel_start_at:
|
if self.travel_start_at and self.travel_end_at and self.travel_end_at < self.travel_start_at:
|
||||||
raise ValidationError({"travel_end_at": _("Kraj puta ne može biti prije početka puta.")})
|
raise ValidationError({"travel_end_at": _("Kraj puta ne može biti prije početka puta.")})
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
import re
|
import re
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from django.utils import timezone
|
||||||
from .models import (
|
from .models import (
|
||||||
Crane,
|
Crane,
|
||||||
Vehicle,
|
Vehicle,
|
||||||
@@ -98,7 +99,7 @@ class WorkOrderSerializer(serializers.ModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = WorkOrder
|
model = WorkOrder
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'vehicle', 'crane', 'crane_label', 'creator', 'date',
|
'id', 'display_code', 'vehicle', 'crane', 'crane_label', 'creator', 'date',
|
||||||
'start_mileage', 'end_mileage',
|
'start_mileage', 'end_mileage',
|
||||||
'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',
|
||||||
@@ -112,6 +113,7 @@ class WorkOrderSerializer(serializers.ModelSerializer):
|
|||||||
'creator': {'required': False},
|
'creator': {'required': False},
|
||||||
'start_mileage': {'required': False, 'allow_null': True},
|
'start_mileage': {'required': False, 'allow_null': True},
|
||||||
'end_mileage': {'required': False, 'allow_null': True},
|
'end_mileage': {'required': False, 'allow_null': True},
|
||||||
|
'display_code': {'required': False, 'allow_blank': False},
|
||||||
'servicer_vehicle_registration': {'required': False, 'allow_blank': True},
|
'servicer_vehicle_registration': {'required': False, 'allow_blank': True},
|
||||||
'servicer_vehicle_make_model': {'required': False, 'allow_blank': True},
|
'servicer_vehicle_make_model': {'required': False, 'allow_blank': True},
|
||||||
'servicer_vehicle_start_mileage': {'required': False, 'allow_null': True},
|
'servicer_vehicle_start_mileage': {'required': False, 'allow_null': True},
|
||||||
@@ -146,6 +148,14 @@ class WorkOrderSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
if user and 'creator' not in validated_data:
|
if user and 'creator' not in validated_data:
|
||||||
validated_data['creator'] = user
|
validated_data['creator'] = user
|
||||||
|
if not validated_data.get('display_code'):
|
||||||
|
creator = validated_data.get('creator')
|
||||||
|
generated_display_code = self._generate_display_code(creator, timezone.localdate())
|
||||||
|
if not generated_display_code:
|
||||||
|
raise serializers.ValidationError({
|
||||||
|
'display_code': 'Serviser mora imati ime i prezime za automatsko generiranje oznake. Unesite oznaku ručno u formatu MT070626.'
|
||||||
|
})
|
||||||
|
validated_data['display_code'] = generated_display_code
|
||||||
|
|
||||||
return services.VehicleService.create_work_order(validated_data)
|
return services.VehicleService.create_work_order(validated_data)
|
||||||
|
|
||||||
@@ -182,6 +192,22 @@ class WorkOrderSerializer(serializers.ModelSerializer):
|
|||||||
raise serializers.ValidationError({'servicer_vehicle_fuel_cost': 'Trošak goriva ne može biti negativan.'})
|
raise serializers.ValidationError({'servicer_vehicle_fuel_cost': 'Trošak goriva ne može biti negativan.'})
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
|
def validate_display_code(self, value):
|
||||||
|
normalized = str(value or '').strip().upper()
|
||||||
|
if not re.fullmatch(r"[A-Z]{2}[0-9]{6}", normalized):
|
||||||
|
raise serializers.ValidationError('Prikaz putnog naloga mora biti u formatu MT070626 (2 slova + 6 znamenki).')
|
||||||
|
return normalized
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _generate_display_code(user, target_date):
|
||||||
|
if user is None:
|
||||||
|
return None
|
||||||
|
first_name = str(getattr(user, 'first_name', '') or '').strip()
|
||||||
|
last_name = str(getattr(user, 'last_name', '') or '').strip()
|
||||||
|
if not first_name or not last_name:
|
||||||
|
return None
|
||||||
|
return f"{first_name[0].upper()}{last_name[0].upper()}{target_date.strftime('%d%m%y')}"
|
||||||
|
|
||||||
|
|
||||||
class WorkOrderInvoiceSerializer(serializers.ModelSerializer):
|
class WorkOrderInvoiceSerializer(serializers.ModelSerializer):
|
||||||
image_url = serializers.SerializerMethodField()
|
image_url = serializers.SerializerMethodField()
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ class VehicleService:
|
|||||||
Task.objects.filter(
|
Task.objects.filter(
|
||||||
is_active=True,
|
is_active=True,
|
||||||
work_order=instance,
|
work_order=instance,
|
||||||
status='servis',
|
status__in=('aktivan', 'servis'),
|
||||||
).update(status='spreman_za_zavrsetak')
|
).update(status='spreman_za_zavrsetak')
|
||||||
|
|
||||||
# Ako je nalog upravo zatvoren, obavijestimo kreatora/servisera
|
# Ako je nalog upravo zatvoren, obavijestimo kreatora/servisera
|
||||||
|
|||||||
@@ -18,6 +18,13 @@ logger = logging.getLogger(__name__)
|
|||||||
register_unicode_fonts()
|
register_unicode_fonts()
|
||||||
|
|
||||||
|
|
||||||
|
def _work_order_display_code(work_order):
|
||||||
|
normalized = str(getattr(work_order, 'display_code', '') or '').strip().upper()
|
||||||
|
if normalized:
|
||||||
|
return normalized
|
||||||
|
return 'NALOG'
|
||||||
|
|
||||||
|
|
||||||
@shared_task(bind=True, max_retries=3, default_retry_delay=60)
|
@shared_task(bind=True, max_retries=3, default_retry_delay=60)
|
||||||
def send_notification_email(self, notification_id):
|
def send_notification_email(self, notification_id):
|
||||||
"""
|
"""
|
||||||
@@ -95,6 +102,7 @@ def _build_work_order_invoices_pdf(work_order):
|
|||||||
chassis_hours = str(getattr(work_order.vehicle, "chassis_working_hours", "-") or "-")
|
chassis_hours = str(getattr(work_order.vehicle, "chassis_working_hours", "-") or "-")
|
||||||
mileage = str(getattr(work_order.vehicle, "current_mileage", "-") or "-")
|
mileage = str(getattr(work_order.vehicle, "current_mileage", "-") or "-")
|
||||||
generated_date = timezone.localtime(timezone.now()).strftime("%d.%m.%Y")
|
generated_date = timezone.localtime(timezone.now()).strftime("%d.%m.%Y")
|
||||||
|
display_code = _work_order_display_code(work_order)
|
||||||
|
|
||||||
def draw_header_footer(page_num):
|
def draw_header_footer(page_num):
|
||||||
draw_standard_header_footer(
|
draw_standard_header_footer(
|
||||||
@@ -107,7 +115,7 @@ def _build_work_order_invoices_pdf(work_order):
|
|||||||
upgrade_hours=upgrade_hours,
|
upgrade_hours=upgrade_hours,
|
||||||
chassis_hours=chassis_hours,
|
chassis_hours=chassis_hours,
|
||||||
mileage=mileage,
|
mileage=mileage,
|
||||||
work_order_number=str(work_order.pk),
|
work_order_number=display_code,
|
||||||
generated_date=generated_date,
|
generated_date=generated_date,
|
||||||
report_title="Računi putnog naloga",
|
report_title="Računi putnog naloga",
|
||||||
page_size=A4,
|
page_size=A4,
|
||||||
@@ -122,7 +130,7 @@ def _build_work_order_invoices_pdf(work_order):
|
|||||||
y -= 8
|
y -= 8
|
||||||
|
|
||||||
pdf.setFont("Vera-Bold", 13)
|
pdf.setFont("Vera-Bold", 13)
|
||||||
pdf.drawString(margin, y, f"Računi putnog naloga WO-{work_order.pk}")
|
pdf.drawString(margin, y, f"Računi putnog naloga {display_code}")
|
||||||
y -= 20
|
y -= 20
|
||||||
pdf.setFont("Vera", 10)
|
pdf.setFont("Vera", 10)
|
||||||
pdf.drawString(margin, y, f"Datum naloga: {work_order.date}")
|
pdf.drawString(margin, y, f"Datum naloga: {work_order.date}")
|
||||||
@@ -202,7 +210,7 @@ def build_work_order_invoices_pdf_task(work_order_id):
|
|||||||
|
|
||||||
pdf_bytes = _build_work_order_invoices_pdf(work_order)
|
pdf_bytes = _build_work_order_invoices_pdf(work_order)
|
||||||
pdf_b64 = base64.b64encode(pdf_bytes).decode('ascii')
|
pdf_b64 = base64.b64encode(pdf_bytes).decode('ascii')
|
||||||
filename = f"{work_order.pk}.work-order-invoices.pdf"
|
filename = f"{_work_order_display_code(work_order)}.work-order-invoices.pdf"
|
||||||
return {
|
return {
|
||||||
"filename": filename,
|
"filename": filename,
|
||||||
"pdf_base64": pdf_b64,
|
"pdf_base64": pdf_b64,
|
||||||
@@ -259,7 +267,7 @@ def build_work_order_pdf_cached_task(generated_pdf_id):
|
|||||||
else:
|
else:
|
||||||
pdf_bytes = _build_work_order_pdf(work_order)
|
pdf_bytes = _build_work_order_pdf(work_order)
|
||||||
|
|
||||||
filename = generated.filename or f"{work_order.pk}.{generated.pdf_type}.pdf"
|
filename = generated.filename or f"{_work_order_display_code(work_order)}.{generated.pdf_type}.pdf"
|
||||||
generated.file.save(filename, ContentFile(pdf_bytes), save=False)
|
generated.file.save(filename, ContentFile(pdf_bytes), save=False)
|
||||||
generated.status = 'ready'
|
generated.status = 'ready'
|
||||||
generated.error_message = ''
|
generated.error_message = ''
|
||||||
@@ -269,7 +277,7 @@ def build_work_order_pdf_cached_task(generated_pdf_id):
|
|||||||
NotificationService.create_notification(
|
NotificationService.create_notification(
|
||||||
recipient=generated.requested_by,
|
recipient=generated.requested_by,
|
||||||
title="PDF spreman",
|
title="PDF spreman",
|
||||||
message=f"PDF dokument je uspješno generiran za WO-{work_order.pk}.",
|
message=f"PDF dokument je uspješno generiran za putni nalog {_work_order_display_code(work_order)}.",
|
||||||
level="success",
|
level="success",
|
||||||
send_email=False,
|
send_email=False,
|
||||||
metadata={
|
metadata={
|
||||||
@@ -294,7 +302,7 @@ def build_work_order_pdf_cached_task(generated_pdf_id):
|
|||||||
NotificationService.create_notification(
|
NotificationService.create_notification(
|
||||||
recipient=generated.requested_by,
|
recipient=generated.requested_by,
|
||||||
title="Greška kod PDF-a",
|
title="Greška kod PDF-a",
|
||||||
message=f"Generiranje PDF dokumenta nije uspjelo za WO-{work_order.pk}.",
|
message=f"Generiranje PDF dokumenta nije uspjelo za putni nalog {_work_order_display_code(work_order)}.",
|
||||||
level="warning",
|
level="warning",
|
||||||
send_email=False,
|
send_email=False,
|
||||||
metadata={
|
metadata={
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ from django.test import TestCase
|
|||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from modules.fleet.services import VehicleService
|
from modules.fleet.services import VehicleService
|
||||||
from modules.fleet.models import Vehicle, WorkOrder, VehicleServiceRecord
|
from modules.fleet.models import Vehicle, WorkOrder, VehicleServiceRecord
|
||||||
|
from modules.fleet.serializers import WorkOrderSerializer
|
||||||
from modules.task_management.models import Task
|
from modules.task_management.models import Task
|
||||||
|
from rest_framework.test import APIRequestFactory
|
||||||
|
|
||||||
|
|
||||||
class FleetServiceTests(TestCase):
|
class FleetServiceTests(TestCase):
|
||||||
@@ -73,3 +75,57 @@ class FleetServiceTests(TestCase):
|
|||||||
VehicleService.update_work_order(work_order, {"status": "closed"})
|
VehicleService.update_work_order(work_order, {"status": "closed"})
|
||||||
task.refresh_from_db()
|
task.refresh_from_db()
|
||||||
self.assertEqual(task.status, "spreman_za_zavrsetak")
|
self.assertEqual(task.status, "spreman_za_zavrsetak")
|
||||||
|
|
||||||
|
def test_closing_work_order_moves_active_task_to_ready_for_completion(self):
|
||||||
|
work_order = WorkOrder.objects.create(
|
||||||
|
vehicle=self.vehicle,
|
||||||
|
creator=self.user,
|
||||||
|
purpose="kontrola",
|
||||||
|
status="open",
|
||||||
|
start_mileage=5000,
|
||||||
|
end_mileage=5200,
|
||||||
|
)
|
||||||
|
task = Task.objects.create(
|
||||||
|
title="Preventiva",
|
||||||
|
description="",
|
||||||
|
status="aktivan",
|
||||||
|
assigned_to=self.user,
|
||||||
|
vehicle=self.vehicle,
|
||||||
|
work_order=work_order,
|
||||||
|
)
|
||||||
|
|
||||||
|
VehicleService.update_work_order(work_order, {"status": "closed"})
|
||||||
|
task.refresh_from_db()
|
||||||
|
self.assertEqual(task.status, "spreman_za_zavrsetak")
|
||||||
|
|
||||||
|
def test_work_order_serializer_autogenerates_display_code(self):
|
||||||
|
self.user.first_name = "Mario"
|
||||||
|
self.user.last_name = "Tkalac"
|
||||||
|
self.user.save(update_fields=["first_name", "last_name"])
|
||||||
|
|
||||||
|
request = APIRequestFactory().post('/api/fleet/work-orders/', {})
|
||||||
|
request.user = self.user
|
||||||
|
serializer = WorkOrderSerializer(
|
||||||
|
data={
|
||||||
|
"vehicle": str(self.vehicle.id),
|
||||||
|
"purpose": "kontrola",
|
||||||
|
},
|
||||||
|
context={"request": request},
|
||||||
|
)
|
||||||
|
self.assertTrue(serializer.is_valid(), serializer.errors)
|
||||||
|
work_order = serializer.save()
|
||||||
|
self.assertRegex(work_order.display_code, r'^MT[0-9]{6}$')
|
||||||
|
|
||||||
|
def test_work_order_serializer_rejects_invalid_display_code(self):
|
||||||
|
request = APIRequestFactory().post('/api/fleet/work-orders/', {})
|
||||||
|
request.user = self.user
|
||||||
|
serializer = WorkOrderSerializer(
|
||||||
|
data={
|
||||||
|
"vehicle": str(self.vehicle.id),
|
||||||
|
"purpose": "kontrola",
|
||||||
|
"display_code": "BAD-CODE",
|
||||||
|
},
|
||||||
|
context={"request": request},
|
||||||
|
)
|
||||||
|
self.assertFalse(serializer.is_valid())
|
||||||
|
self.assertIn("display_code", serializer.errors)
|
||||||
@@ -142,17 +142,26 @@ def _parse_format(value):
|
|||||||
GENERATED_PDF_TTL_HOURS = 24
|
GENERATED_PDF_TTL_HOURS = 24
|
||||||
|
|
||||||
|
|
||||||
|
def _work_order_display_code(work_order):
|
||||||
|
normalized = str(getattr(work_order, 'display_code', '') or '').strip().upper()
|
||||||
|
if normalized:
|
||||||
|
return normalized
|
||||||
|
return 'NALOG'
|
||||||
|
|
||||||
|
|
||||||
def _pdf_filename(work_order, pdf_type):
|
def _pdf_filename(work_order, pdf_type):
|
||||||
|
display_code = _work_order_display_code(work_order)
|
||||||
if pdf_type == 'invoices':
|
if pdf_type == 'invoices':
|
||||||
return f"{work_order.pk}.work-order-invoices.pdf"
|
return f"{display_code}.work-order-invoices.pdf"
|
||||||
if pdf_type == 'service_records':
|
if pdf_type == 'service_records':
|
||||||
return f"{work_order.pk}.work-order-service-records.pdf"
|
return f"{display_code}.work-order-service-records.pdf"
|
||||||
return f"{work_order.pk}.work-order.pdf"
|
return f"{display_code}.work-order.pdf"
|
||||||
|
|
||||||
|
|
||||||
def _get_cached_pdf(work_order, pdf_type):
|
def _get_cached_pdf(work_order, pdf_type):
|
||||||
now = timezone.now()
|
now = timezone.now()
|
||||||
return (
|
expected_filename = _pdf_filename(work_order, pdf_type)
|
||||||
|
candidates = list(
|
||||||
GeneratedWorkOrderPdf.objects
|
GeneratedWorkOrderPdf.objects
|
||||||
.filter(
|
.filter(
|
||||||
is_active=True,
|
is_active=True,
|
||||||
@@ -164,8 +173,11 @@ def _get_cached_pdf(work_order, pdf_type):
|
|||||||
.exclude(file='')
|
.exclude(file='')
|
||||||
.exclude(file__isnull=True)
|
.exclude(file__isnull=True)
|
||||||
.order_by('-created_at')
|
.order_by('-created_at')
|
||||||
.first()
|
|
||||||
)
|
)
|
||||||
|
for candidate in candidates:
|
||||||
|
if str(candidate.filename or '').strip() == expected_filename:
|
||||||
|
return candidate
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _cleanup_expired_generated_pdfs():
|
def _cleanup_expired_generated_pdfs():
|
||||||
@@ -203,17 +215,18 @@ def _notify_pdf_request(*, user, work_order, doc_type, stage, generated_pdf=None
|
|||||||
else:
|
else:
|
||||||
doc_label = "računa putnog naloga"
|
doc_label = "računa putnog naloga"
|
||||||
|
|
||||||
|
display_code = _work_order_display_code(work_order)
|
||||||
if stage == 'requested':
|
if stage == 'requested':
|
||||||
title = f"Zahtjev za PDF {doc_label}"
|
title = f"Zahtjev za PDF {doc_label}"
|
||||||
message = f"Zaprimljen je zahtjev za generiranje PDF dokumenta za WO-{work_order.pk}."
|
message = f"Zaprimljen je zahtjev za generiranje PDF dokumenta za putni nalog {display_code}."
|
||||||
level = "info"
|
level = "info"
|
||||||
elif stage == 'failed':
|
elif stage == 'failed':
|
||||||
title = f"Greška kod PDF-a ({doc_label})"
|
title = f"Greška kod PDF-a ({doc_label})"
|
||||||
message = f"Generiranje PDF dokumenta nije uspjelo za WO-{work_order.pk}."
|
message = f"Generiranje PDF dokumenta nije uspjelo za putni nalog {display_code}."
|
||||||
level = "warning"
|
level = "warning"
|
||||||
else:
|
else:
|
||||||
title = f"PDF spreman ({doc_label})"
|
title = f"PDF spreman ({doc_label})"
|
||||||
message = f"PDF dokument je uspješno generiran za WO-{work_order.pk}."
|
message = f"PDF dokument je uspješno generiran za putni nalog {display_code}."
|
||||||
level = "success"
|
level = "success"
|
||||||
|
|
||||||
metadata = {
|
metadata = {
|
||||||
@@ -337,6 +350,7 @@ def _build_work_order_pdf(work_order):
|
|||||||
invoices = list(work_order.invoices.filter(is_active=True).order_by('-datum', '-created_at'))
|
invoices = list(work_order.invoices.filter(is_active=True).order_by('-datum', '-created_at'))
|
||||||
now_local = timezone.localtime(timezone.now())
|
now_local = timezone.localtime(timezone.now())
|
||||||
tz = timezone.get_current_timezone()
|
tz = timezone.get_current_timezone()
|
||||||
|
display_code = _work_order_display_code(work_order)
|
||||||
|
|
||||||
def _fmt_date(value):
|
def _fmt_date(value):
|
||||||
if not value:
|
if not value:
|
||||||
@@ -437,7 +451,7 @@ def _build_work_order_pdf(work_order):
|
|||||||
_paragraph("N A L O G<br/>ZA SLUŽBENO PUTOVANJE", bold=True, size=11, align=1),
|
_paragraph("N A L O G<br/>ZA SLUŽBENO PUTOVANJE", bold=True, size=11, align=1),
|
||||||
_paragraph(
|
_paragraph(
|
||||||
f"Mjesto i datum:<br/><b>{place_label} {_fmt_date(now_local.date())}</b><br/>"
|
f"Mjesto i datum:<br/><b>{place_label} {_fmt_date(now_local.date())}</b><br/>"
|
||||||
f"Broj naloga:<br/><b>{work_order.pk}</b>",
|
f"Broj naloga:<br/><b>{display_code}</b>",
|
||||||
size=8.8,
|
size=8.8,
|
||||||
),
|
),
|
||||||
]]
|
]]
|
||||||
@@ -686,7 +700,7 @@ def _build_work_order_service_records_pdf(work_order):
|
|||||||
upgrade_hours_str = str(getattr(vehicle, 'superstructure_working_hours', '-') or '-')
|
upgrade_hours_str = str(getattr(vehicle, 'superstructure_working_hours', '-') or '-')
|
||||||
chassis_hours_str = str(getattr(vehicle, 'chassis_working_hours', '-') or '-')
|
chassis_hours_str = str(getattr(vehicle, 'chassis_working_hours', '-') or '-')
|
||||||
mileage_str = str(vehicle.current_mileage or '-')
|
mileage_str = str(vehicle.current_mileage or '-')
|
||||||
nalog_str = str(work_order.pk)
|
nalog_str = _work_order_display_code(work_order)
|
||||||
generated_date_str = timezone.localtime(timezone.now()).strftime('%d.%m.%Y')
|
generated_date_str = timezone.localtime(timezone.now()).strftime('%d.%m.%Y')
|
||||||
|
|
||||||
HEADER_H = 86
|
HEADER_H = 86
|
||||||
@@ -1064,7 +1078,7 @@ def _build_service_record_pdf(service_record):
|
|||||||
upgrade_hours_str = str(getattr(vehicle, 'superstructure_working_hours', '-') or '-')
|
upgrade_hours_str = str(getattr(vehicle, 'superstructure_working_hours', '-') or '-')
|
||||||
chassis_hours_str = str(getattr(vehicle, 'chassis_working_hours', '-') or '-')
|
chassis_hours_str = str(getattr(vehicle, 'chassis_working_hours', '-') or '-')
|
||||||
mileage_str = str(vehicle.current_mileage or '-')
|
mileage_str = str(vehicle.current_mileage or '-')
|
||||||
nalog_str = str(getattr(work_order, 'pk', '-') or '-')
|
nalog_str = _work_order_display_code(work_order) if work_order else '-'
|
||||||
generated_date_str = timezone.localtime(timezone.now()).strftime('%d.%m.%Y')
|
generated_date_str = timezone.localtime(timezone.now()).strftime('%d.%m.%Y')
|
||||||
|
|
||||||
HEADER_H = 86
|
HEADER_H = 86
|
||||||
@@ -1487,7 +1501,7 @@ class WorkOrderViewSet(viewsets.ModelViewSet):
|
|||||||
pdf_bytes = _build_work_order_pdf(work_order)
|
pdf_bytes = _build_work_order_pdf(work_order)
|
||||||
_notify_pdf_request(user=request.user, work_order=work_order, doc_type='work_order', stage='completed')
|
_notify_pdf_request(user=request.user, work_order=work_order, doc_type='work_order', stage='completed')
|
||||||
response = HttpResponse(pdf_bytes, content_type='application/pdf')
|
response = HttpResponse(pdf_bytes, content_type='application/pdf')
|
||||||
response['Content-Disposition'] = f'attachment; filename="{work_order.pk}.work-order.pdf"'
|
response['Content-Disposition'] = f'attachment; filename="{_pdf_filename(work_order, "work_order")}"'
|
||||||
return response
|
return response
|
||||||
|
|
||||||
@action(detail=True, methods=['post'], url_path='pdf-request')
|
@action(detail=True, methods=['post'], url_path='pdf-request')
|
||||||
@@ -1508,7 +1522,7 @@ class WorkOrderViewSet(viewsets.ModelViewSet):
|
|||||||
pdf_bytes = _build_work_order_service_records_pdf(work_order)
|
pdf_bytes = _build_work_order_service_records_pdf(work_order)
|
||||||
_notify_pdf_request(user=request.user, work_order=work_order, doc_type='service_records', stage='completed')
|
_notify_pdf_request(user=request.user, work_order=work_order, doc_type='service_records', stage='completed')
|
||||||
response = HttpResponse(pdf_bytes, content_type='application/pdf')
|
response = HttpResponse(pdf_bytes, content_type='application/pdf')
|
||||||
response['Content-Disposition'] = f'attachment; filename="{work_order.pk}.work-order-service-records.pdf"'
|
response['Content-Disposition'] = f'attachment; filename="{_pdf_filename(work_order, "service_records")}"'
|
||||||
return response
|
return response
|
||||||
|
|
||||||
@action(detail=True, methods=['post'], url_path='service-records-pdf-request')
|
@action(detail=True, methods=['post'], url_path='service-records-pdf-request')
|
||||||
@@ -1545,7 +1559,7 @@ class WorkOrderViewSet(viewsets.ModelViewSet):
|
|||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
raise DRFValidationError({"detail": "Neispravan PDF sadržaj računa."})
|
raise DRFValidationError({"detail": "Neispravan PDF sadržaj računa."})
|
||||||
|
|
||||||
filename = payload.get('filename') or f"{work_order.pk}.work-order-invoices.pdf"
|
filename = payload.get('filename') or _pdf_filename(work_order, 'invoices')
|
||||||
_notify_pdf_request(user=request.user, work_order=work_order, doc_type='invoices', stage='completed')
|
_notify_pdf_request(user=request.user, work_order=work_order, doc_type='invoices', stage='completed')
|
||||||
response = HttpResponse(pdf_bytes, content_type='application/pdf')
|
response = HttpResponse(pdf_bytes, content_type='application/pdf')
|
||||||
response['Content-Disposition'] = f'attachment; filename="{filename}"'
|
response['Content-Disposition'] = f'attachment; filename="{filename}"'
|
||||||
@@ -1600,7 +1614,7 @@ class WorkOrderViewSet(viewsets.ModelViewSet):
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>Računi putnog naloga WO-{work_order.pk}</title>
|
<title>Računi putnog naloga {_work_order_display_code(work_order)}</title>
|
||||||
<style>
|
<style>
|
||||||
body {{ font-family: Arial, sans-serif; margin: 24px; color: #111827; background: #f8fafc; }}
|
body {{ font-family: Arial, sans-serif; margin: 24px; color: #111827; background: #f8fafc; }}
|
||||||
.card {{ background: #fff; border: 1px solid #e5e7eb; border-radius: 12px; padding: 16px; margin-bottom: 16px; }}
|
.card {{ background: #fff; border: 1px solid #e5e7eb; border-radius: 12px; padding: 16px; margin-bottom: 16px; }}
|
||||||
@@ -1614,7 +1628,7 @@ class WorkOrderViewSet(viewsets.ModelViewSet):
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h1>Putni nalog WO-{work_order.pk} — Računi</h1>
|
<h1>Putni nalog {_work_order_display_code(work_order)} — Računi</h1>
|
||||||
<p class="muted">Dizalica: {escape(work_order.vehicle.registration_number)} | Datum naloga: {escape(str(work_order.date))}</p>
|
<p class="muted">Dizalica: {escape(work_order.vehicle.registration_number)} | Datum naloga: {escape(str(work_order.date))}</p>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<a href="{escape(pdf_url)}">Preuzmi PDF putnog naloga</a>
|
<a href="{escape(pdf_url)}">Preuzmi PDF putnog naloga</a>
|
||||||
@@ -1693,13 +1707,13 @@ class WorkOrderViewSet(viewsets.ModelViewSet):
|
|||||||
raise DRFValidationError({"recipient": "Nije pronađena email adresa za slanje."})
|
raise DRFValidationError({"recipient": "Nije pronađena email adresa za slanje."})
|
||||||
|
|
||||||
pdf_bytes = _build_work_order_pdf(work_order)
|
pdf_bytes = _build_work_order_pdf(work_order)
|
||||||
subject = _first_non_empty(request.data.get('subject')) or f"Putni nalog WO-{work_order.pk}"
|
subject = _first_non_empty(request.data.get('subject')) or f"Putni nalog {_work_order_display_code(work_order)}"
|
||||||
body = _first_non_empty(request.data.get('message')) or "U prilogu je PDF putnog naloga."
|
body = _first_non_empty(request.data.get('message')) or "U prilogu je PDF putnog naloga."
|
||||||
_send_document_email(
|
_send_document_email(
|
||||||
recipient=recipient,
|
recipient=recipient,
|
||||||
subject=subject,
|
subject=subject,
|
||||||
body=body,
|
body=body,
|
||||||
filename=f"{work_order.pk}.work-order.pdf",
|
filename=_pdf_filename(work_order, 'work_order'),
|
||||||
pdf_bytes=pdf_bytes,
|
pdf_bytes=pdf_bytes,
|
||||||
)
|
)
|
||||||
return Response({"sent": True, "recipient": recipient}, status=status.HTTP_200_OK)
|
return Response({"sent": True, "recipient": recipient}, status=status.HTTP_200_OK)
|
||||||
|
|||||||
@@ -155,7 +155,10 @@ class TaskSerializer(serializers.ModelSerializer):
|
|||||||
def get_work_order_label(self, obj):
|
def get_work_order_label(self, obj):
|
||||||
if not obj.work_order_id:
|
if not obj.work_order_id:
|
||||||
return None
|
return None
|
||||||
return f"PN-{str(obj.work_order_id).split('-')[0].upper()}"
|
display_code = str(getattr(obj.work_order, 'display_code', '') or '').strip().upper()
|
||||||
|
if display_code:
|
||||||
|
return display_code
|
||||||
|
return None
|
||||||
|
|
||||||
def get_assigned_to_name(self, obj):
|
def get_assigned_to_name(self, obj):
|
||||||
if not obj.assigned_to_id:
|
if not obj.assigned_to_id:
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import {
|
|||||||
$serviceRecords,
|
$serviceRecords,
|
||||||
createWorkOrder,
|
createWorkOrder,
|
||||||
fetchFleetDashboardData,
|
fetchFleetDashboardData,
|
||||||
|
fetchTeamMembers,
|
||||||
ensureVehiclesCatalog,
|
ensureVehiclesCatalog,
|
||||||
triggerOfflineSync,
|
triggerOfflineSync,
|
||||||
updateWorkOrder,
|
updateWorkOrder,
|
||||||
@@ -32,7 +33,7 @@ import {
|
|||||||
import { showToast } from '../../stores/toastStore';
|
import { showToast } from '../../stores/toastStore';
|
||||||
import { $accessToken, $authReady, $user, hydrateAuthFromStorage, validateToken } from '../../stores/authStore';
|
import { $accessToken, $authReady, $user, hydrateAuthFromStorage, validateToken } from '../../stores/authStore';
|
||||||
import { cacheCraneSerial, cacheCraneSerialsFromVehicles } from '../../stores/craneSerialStore';
|
import { cacheCraneSerial, cacheCraneSerialsFromVehicles } from '../../stores/craneSerialStore';
|
||||||
import { formatEntityCode } from '../../lib/displayIds';
|
import { formatWorkOrderDisplayCode } from '../../lib/displayIds';
|
||||||
import { $tasks, fetchTasks, getStatusLabel, isTaskActive, updateTask } from '../../stores/taskStore';
|
import { $tasks, fetchTasks, getStatusLabel, isTaskActive, updateTask } from '../../stores/taskStore';
|
||||||
import AnimatedDataTable from '../ui/AnimatedDataTable';
|
import AnimatedDataTable from '../ui/AnimatedDataTable';
|
||||||
import AnimatedPage from '../ui/AnimatedPage';
|
import AnimatedPage from '../ui/AnimatedPage';
|
||||||
@@ -75,6 +76,15 @@ function formatTaskCraneLabel(task, cranes) {
|
|||||||
return model ? `${primary} / ${model}` : primary;
|
return model ? `${primary} / ${model}` : primary;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatServiceRecordCraneLabel(record, cranes) {
|
||||||
|
const crane = cranes.find((item) => String(item.id) === String(record?.vehicle || ''));
|
||||||
|
const serial = String(record?.crane_serial_number || crane?.crane_serial_number || '').trim();
|
||||||
|
const model = [crane?.make, crane?.model].filter(Boolean).join(' ').trim();
|
||||||
|
const registration = String(record?.vehicle_registration || crane?.registration_number || '').trim();
|
||||||
|
const primary = serial ? `SN ${serial}` : (registration || '-');
|
||||||
|
return model ? `${primary} / ${model}` : primary;
|
||||||
|
}
|
||||||
|
|
||||||
function formatCost(value) {
|
function formatCost(value) {
|
||||||
const numeric = Number(value);
|
const numeric = Number(value);
|
||||||
if (Number.isNaN(numeric)) return '-';
|
if (Number.isNaN(numeric)) return '-';
|
||||||
@@ -121,6 +131,8 @@ export default function FleetDashboardShell({ initialSection = 'dashboard', page
|
|||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const [taskFilter, setTaskFilter] = useState('active');
|
const [taskFilter, setTaskFilter] = useState('active');
|
||||||
const [taskScope, setTaskScope] = useState('all');
|
const [taskScope, setTaskScope] = useState('all');
|
||||||
|
const [supervisorServicers, setSupervisorServicers] = useState([]);
|
||||||
|
const [selectedServicerId, setSelectedServicerId] = useState('all');
|
||||||
const [workOrderScope, setWorkOrderScope] = useState('context');
|
const [workOrderScope, setWorkOrderScope] = useState('context');
|
||||||
const [selectedCraneRowId, setSelectedCraneRowId] = useState(null);
|
const [selectedCraneRowId, setSelectedCraneRowId] = useState(null);
|
||||||
const [expandedServiceTaskId, setExpandedServiceTaskId] = useState(null);
|
const [expandedServiceTaskId, setExpandedServiceTaskId] = useState(null);
|
||||||
@@ -360,6 +372,7 @@ export default function FleetDashboardShell({ initialSection = 'dashboard', page
|
|||||||
const isTeamMember = Boolean(user?.is_team_member);
|
const isTeamMember = Boolean(user?.is_team_member);
|
||||||
const isServiser = Boolean(user?.is_serviser);
|
const isServiser = Boolean(user?.is_serviser);
|
||||||
const isCustomer = Boolean(user?.is_kupac);
|
const isCustomer = Boolean(user?.is_kupac);
|
||||||
|
const isSupervisorNotServiser = isTeamMember && !isServiser;
|
||||||
const canViewAllTasks = isTeamMember && (isServiser || !isCustomer);
|
const canViewAllTasks = isTeamMember && (isServiser || !isCustomer);
|
||||||
const canToggleTaskScope = isTeamMember && isServiser;
|
const canToggleTaskScope = isTeamMember && isServiser;
|
||||||
const isTaskAssignedToCurrentUser = (task) => (
|
const isTaskAssignedToCurrentUser = (task) => (
|
||||||
@@ -370,6 +383,42 @@ export default function FleetDashboardShell({ initialSection = 'dashboard', page
|
|||||||
setTaskScope(canViewAllTasks ? 'all' : 'mine');
|
setTaskScope(canViewAllTasks ? 'all' : 'mine');
|
||||||
}, [canViewAllTasks]);
|
}, [canViewAllTasks]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!authReady || !isSupervisorNotServiser) {
|
||||||
|
setSupervisorServicers([]);
|
||||||
|
setSelectedServicerId('all');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let isActive = true;
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const members = await fetchTeamMembers();
|
||||||
|
if (!isActive) return;
|
||||||
|
const servicers = members.filter((member) => Boolean(member?.is_serviser));
|
||||||
|
setSupervisorServicers(servicers);
|
||||||
|
} catch {
|
||||||
|
if (isActive) {
|
||||||
|
setSupervisorServicers([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
return () => {
|
||||||
|
isActive = false;
|
||||||
|
};
|
||||||
|
}, [authReady, isSupervisorNotServiser]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedServicerId === 'all') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const servicerExists = supervisorServicers.some(
|
||||||
|
(member) => String(member?.id || '') === String(selectedServicerId)
|
||||||
|
);
|
||||||
|
if (!servicerExists) {
|
||||||
|
setSelectedServicerId('all');
|
||||||
|
}
|
||||||
|
}, [selectedServicerId, supervisorServicers]);
|
||||||
|
|
||||||
const dashboardTasks = useMemo(() => {
|
const dashboardTasks = useMemo(() => {
|
||||||
const sorted = [...tasks].sort((a, b) => String(b.created_at || '').localeCompare(String(a.created_at || '')));
|
const sorted = [...tasks].sort((a, b) => String(b.created_at || '').localeCompare(String(a.created_at || '')));
|
||||||
const scoped = (() => {
|
const scoped = (() => {
|
||||||
@@ -381,14 +430,22 @@ export default function FleetDashboardShell({ initialSection = 'dashboard', page
|
|||||||
}
|
}
|
||||||
return sorted;
|
return sorted;
|
||||||
})();
|
})();
|
||||||
|
const filteredByServicer = (() => {
|
||||||
|
if (!isSupervisorNotServiser || selectedServicerId === 'all') {
|
||||||
|
return scoped;
|
||||||
|
}
|
||||||
|
return scoped.filter(
|
||||||
|
(task) => String(task?.assigned_to || '') === String(selectedServicerId)
|
||||||
|
);
|
||||||
|
})();
|
||||||
if (taskFilter === 'active') {
|
if (taskFilter === 'active') {
|
||||||
return scoped.filter((task) => isTaskActive(task));
|
return filteredByServicer.filter((task) => isTaskActive(task));
|
||||||
}
|
}
|
||||||
if (taskFilter === 'inactive') {
|
if (taskFilter === 'inactive') {
|
||||||
return scoped.filter((task) => !isTaskActive(task));
|
return filteredByServicer.filter((task) => !isTaskActive(task));
|
||||||
}
|
}
|
||||||
return scoped;
|
return filteredByServicer;
|
||||||
}, [tasks, taskFilter, canViewAllTasks, canToggleTaskScope, taskScope, user?.id]);
|
}, [tasks, taskFilter, canViewAllTasks, canToggleTaskScope, taskScope, user?.id, isSupervisorNotServiser, selectedServicerId]);
|
||||||
const selectedTaskServiceRecords = useMemo(() => {
|
const selectedTaskServiceRecords = useMemo(() => {
|
||||||
if (!selectedTask?.id) return [];
|
if (!selectedTask?.id) return [];
|
||||||
return [...serviceRecords]
|
return [...serviceRecords]
|
||||||
@@ -435,7 +492,7 @@ export default function FleetDashboardShell({ initialSection = 'dashboard', page
|
|||||||
showToast('Putni nalog je spremljen.', 'success');
|
showToast('Putni nalog je spremljen.', 'success');
|
||||||
await fetchFleetDashboardData({ silent: true });
|
await fetchFleetDashboardData({ silent: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showToast('Kreiranje naloga nije uspjelo.', 'error');
|
showToast(error?.message || 'Kreiranje naloga nije uspjelo.', 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -446,7 +503,7 @@ export default function FleetDashboardShell({ initialSection = 'dashboard', page
|
|||||||
await fetchFleetDashboardData({ silent: true });
|
await fetchFleetDashboardData({ silent: true });
|
||||||
await fetchTasks();
|
await fetchTasks();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showToast('Ažuriranje naloga nije uspjelo.', 'error');
|
showToast(error?.message || 'Ažuriranje naloga nije uspjelo.', 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -595,6 +652,23 @@ export default function FleetDashboardShell({ initialSection = 'dashboard', page
|
|||||||
<div className="flex items-center justify-between border-b border-border-hairline px-4 py-3">
|
<div className="flex items-center justify-between border-b border-border-hairline px-4 py-3">
|
||||||
<h3 className="font-semibold text-text-main">Radni zadaci</h3>
|
<h3 className="font-semibold text-text-main">Radni zadaci</h3>
|
||||||
<div className="flex items-center gap-2 text-xs">
|
<div className="flex items-center gap-2 text-xs">
|
||||||
|
{hasMounted && isSupervisorNotServiser && (
|
||||||
|
<label className="flex items-center gap-2 text-text-main">
|
||||||
|
<span>Serviser</span>
|
||||||
|
<select
|
||||||
|
value={selectedServicerId}
|
||||||
|
onChange={(event) => setSelectedServicerId(event.target.value)}
|
||||||
|
className="rounded-md border border-border-hairline bg-canvas-elevated px-2 py-1 text-xs text-text-main focus:border-indigo-400 focus:outline-none"
|
||||||
|
>
|
||||||
|
<option value="all">Svi serviseri</option>
|
||||||
|
{supervisorServicers.map((servicer) => (
|
||||||
|
<option key={servicer.id} value={servicer.id}>
|
||||||
|
{servicer.full_name || servicer.email}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
{canToggleTaskScope && (
|
{canToggleTaskScope && (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
@@ -765,7 +839,7 @@ export default function FleetDashboardShell({ initialSection = 'dashboard', page
|
|||||||
className="cursor-pointer px-4 py-3 font-medium text-text-main"
|
className="cursor-pointer px-4 py-3 font-medium text-text-main"
|
||||||
onClick={() => openWorkOrderDetails(workOrder)}
|
onClick={() => openWorkOrderDetails(workOrder)}
|
||||||
>
|
>
|
||||||
{formatEntityCode('PN', workOrder.id)}
|
{formatWorkOrderDisplayCode(workOrder)}
|
||||||
</td>
|
</td>
|
||||||
<td
|
<td
|
||||||
className="px-4 py-3"
|
className="px-4 py-3"
|
||||||
@@ -912,7 +986,7 @@ export default function FleetDashboardShell({ initialSection = 'dashboard', page
|
|||||||
<AnimatedDataTable
|
<AnimatedDataTable
|
||||||
columns={[
|
columns={[
|
||||||
{ key: 'date', label: 'Datum', className: 'px-4 py-3' },
|
{ key: 'date', label: 'Datum', className: 'px-4 py-3' },
|
||||||
{ key: 'crane', label: 'SN', className: 'px-4 py-3' },
|
{ key: 'crane', label: 'DIZALICA', className: 'px-4 py-3' },
|
||||||
{ key: 'description', label: 'Opis', className: 'px-4 py-3' },
|
{ key: 'description', label: 'Opis', className: 'px-4 py-3' },
|
||||||
{ key: 'cost', label: 'Trošak', className: 'px-4 py-3' },
|
{ key: 'cost', label: 'Trošak', className: 'px-4 py-3' },
|
||||||
{ key: 'files', label: 'Datoteke', className: 'px-4 py-3' },
|
{ key: 'files', label: 'Datoteke', className: 'px-4 py-3' },
|
||||||
@@ -927,7 +1001,7 @@ export default function FleetDashboardShell({ initialSection = 'dashboard', page
|
|||||||
{JSON.stringify(group, null, 2)}
|
{JSON.stringify(group, null, 2)}
|
||||||
</pre> */}
|
</pre> */}
|
||||||
<td className="px-4 py-3">{formatDate(record.service_date)}</td>
|
<td className="px-4 py-3">{formatDate(record.service_date)}</td>
|
||||||
<td className="px-4 py-3">{record.crane_serial_number || '-'}</td>
|
<td className="px-4 py-3">{formatServiceRecordCraneLabel(record, cranes)}</td>
|
||||||
<td className="max-w-md truncate px-4 py-3" title={record.description || ''}>
|
<td className="max-w-md truncate px-4 py-3" title={record.description || ''}>
|
||||||
{record.description || '-'}
|
{record.description || '-'}
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useMemo, useState } from 'preact/hooks';
|
import { useEffect, useMemo, useState } from 'preact/hooks';
|
||||||
import { createTask, fetchTaskTemplates, getStatusLabel } from '../../stores/taskStore';
|
import { createTask, fetchTaskTemplates, getStatusLabel } from '../../stores/taskStore';
|
||||||
import { formatEntityCode, formatPurposeLabel } from '../../lib/displayIds';
|
import { formatPurposeLabel, formatWorkOrderDisplayCode } from '../../lib/displayIds';
|
||||||
import ModalShell from '../ui/ModalShell';
|
import ModalShell from '../ui/ModalShell';
|
||||||
|
|
||||||
const STATUS_OPTIONS = ['aktivan', 'servis', 'zavrsen', 'neaktivan'];
|
const STATUS_OPTIONS = ['aktivan', 'servis', 'zavrsen', 'neaktivan'];
|
||||||
@@ -25,6 +25,10 @@ export default function TaskCreateModal({ open, workOrders = [], contextCrane =
|
|||||||
() => templates.find((template) => String(template.id) === String(templateId)) || null,
|
() => templates.find((template) => String(template.id) === String(templateId)) || null,
|
||||||
[templates, templateId]
|
[templates, templateId]
|
||||||
);
|
);
|
||||||
|
const availableWorkOrders = useMemo(
|
||||||
|
() => workOrders.filter((workOrder) => String(workOrder?.status || '').toLowerCase() !== 'closed'),
|
||||||
|
[workOrders]
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) {
|
if (!open) {
|
||||||
@@ -235,9 +239,9 @@ export default function TaskCreateModal({ open, workOrders = [], contextCrane =
|
|||||||
className="rounded-lg border border-border-hairline bg-canvas-base px-3 py-2 text-text-main disabled:opacity-50"
|
className="rounded-lg border border-border-hairline bg-canvas-base px-3 py-2 text-text-main disabled:opacity-50"
|
||||||
>
|
>
|
||||||
<option value="">— Bez povezanog naloga —</option>
|
<option value="">— Bez povezanog naloga —</option>
|
||||||
{workOrders.map((workOrder) => (
|
{availableWorkOrders.map((workOrder) => (
|
||||||
<option key={workOrder.id} value={String(workOrder.id)}>
|
<option key={workOrder.id} value={String(workOrder.id)}>
|
||||||
{formatEntityCode('PN', workOrder.id)} • {workOrder.craneLabel || workOrder.craneInfo?.registration_number || '-'} • {formatPurposeLabel(workOrder.purpose) || '-'}
|
{formatWorkOrderDisplayCode(workOrder)} • {workOrder.craneLabel || workOrder.craneInfo?.registration_number || '-'} • {formatPurposeLabel(workOrder.purpose) || '-'}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
@@ -246,7 +250,7 @@ export default function TaskCreateModal({ open, workOrders = [], contextCrane =
|
|||||||
Kod template unosa nalog nije obavezan i backend ga neće automatski povezati.
|
Kod template unosa nalog nije obavezan i backend ga neće automatski povezati.
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{workOrders.length === 0 && (
|
{availableWorkOrders.length === 0 && (
|
||||||
<span className="text-[11px] text-amber-600">
|
<span className="text-[11px] text-amber-600">
|
||||||
Nema dostupnih naloga za odabranu dizalicu.
|
Nema dostupnih naloga za odabranu dizalicu.
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useMemo, useState } from 'preact/hooks';
|
import { useEffect, useMemo, useState } from 'preact/hooks';
|
||||||
import ModalShell from '../ui/ModalShell';
|
import ModalShell from '../ui/ModalShell';
|
||||||
import { formatEntityCode } from '../../lib/displayIds';
|
import { formatWorkOrderDisplayCode } from '../../lib/displayIds';
|
||||||
import { getStatusLabel } from '../../stores/taskStore';
|
import { getStatusLabel } from '../../stores/taskStore';
|
||||||
|
|
||||||
const STATUS_OPTIONS = ['aktivan', 'servis', 'spreman_za_zavrsetak', 'zavrsen', 'neaktivan'];
|
const STATUS_OPTIONS = ['aktivan', 'servis', 'spreman_za_zavrsetak', 'zavrsen', 'neaktivan'];
|
||||||
@@ -301,7 +301,7 @@ export default function TaskServiceRecordsModal({
|
|||||||
{currentWorkOrder && !showWorkOrderEditor ? (
|
{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">
|
<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">
|
<p className="text-sm text-text-main">
|
||||||
{formatEntityCode('PN', currentWorkOrder.id)} • {currentWorkOrder.craneLabel || currentWorkOrder.craneInfo?.registration_number || '-'} • {currentWorkOrder.status || '-'}
|
{formatWorkOrderDisplayCode(currentWorkOrder)} • {currentWorkOrder.craneLabel || currentWorkOrder.craneInfo?.registration_number || '-'} • {currentWorkOrder.status || '-'}
|
||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -322,7 +322,7 @@ export default function TaskServiceRecordsModal({
|
|||||||
<option value="">— Odaberi putni nalog —</option>
|
<option value="">— Odaberi putni nalog —</option>
|
||||||
{sortedWorkOrders.map((workOrder) => (
|
{sortedWorkOrders.map((workOrder) => (
|
||||||
<option key={workOrder.id} value={String(workOrder.id)}>
|
<option key={workOrder.id} value={String(workOrder.id)}>
|
||||||
{formatEntityCode('PN', workOrder.id)} • {workOrder.craneLabel || workOrder.craneInfo?.registration_number || '-'} • {workOrder.status || '-'}
|
{formatWorkOrderDisplayCode(workOrder)} • {workOrder.craneLabel || workOrder.craneInfo?.registration_number || '-'} • {workOrder.status || '-'}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
sendWorkOrderEmail,
|
sendWorkOrderEmail,
|
||||||
} from '../../stores/fleetDashboardStore';
|
} from '../../stores/fleetDashboardStore';
|
||||||
import { showToast } from '../../stores/toastStore';
|
import { showToast } from '../../stores/toastStore';
|
||||||
import { formatEntityCode } from '../../lib/displayIds';
|
import { formatWorkOrderDisplayCode } from '../../lib/displayIds';
|
||||||
|
|
||||||
const PURPOSE_OPTIONS = [
|
const PURPOSE_OPTIONS = [
|
||||||
{ value: 'defektaza', label: 'Defektaža' },
|
{ value: 'defektaza', label: 'Defektaža' },
|
||||||
@@ -23,6 +23,7 @@ export default function WorkOrderDetailModal({ open, workOrder, onClose, onSubmi
|
|||||||
const [isEditing, setIsEditing] = useState(false);
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
const [form, setForm] = useState({
|
const [form, setForm] = useState({
|
||||||
has_travel_order: false,
|
has_travel_order: false,
|
||||||
|
display_code: '',
|
||||||
status: 'open',
|
status: 'open',
|
||||||
start_mileage: '',
|
start_mileage: '',
|
||||||
end_mileage: '',
|
end_mileage: '',
|
||||||
@@ -92,6 +93,7 @@ export default function WorkOrderDetailModal({ open, workOrder, onClose, onSubmi
|
|||||||
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.location || workOrder.start_mileage != null || workOrder.end_mileage != null),
|
||||||
|
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),
|
||||||
@@ -308,10 +310,20 @@ export default function WorkOrderDetailModal({ open, workOrder, onClose, onSubmi
|
|||||||
setError('Svrha naloga je obavezna.');
|
setError('Svrha naloga je obavezna.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const displayCode = String(form.display_code || '').trim().toUpperCase();
|
||||||
|
if (!displayCode) {
|
||||||
|
setError('Prikaz putnog naloga je obavezan.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!/^[A-Z]{2}[0-9]{6}$/.test(displayCode)) {
|
||||||
|
setError('Prikaz putnog naloga mora biti u formatu MT070626.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
try {
|
try {
|
||||||
await onSubmit(workOrder, {
|
await onSubmit(workOrder, {
|
||||||
|
display_code: displayCode,
|
||||||
status: form.status,
|
status: form.status,
|
||||||
start_mileage: startMileage,
|
start_mileage: startMileage,
|
||||||
end_mileage: endMileage,
|
end_mileage: endMileage,
|
||||||
@@ -430,7 +442,7 @@ export default function WorkOrderDetailModal({ open, workOrder, onClose, onSubmi
|
|||||||
<div>
|
<div>
|
||||||
<div className="flex items-center justify-between border-b border-border-hairline px-5 py-4">
|
<div className="flex items-center justify-between border-b border-border-hairline px-5 py-4">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-semibold text-text-main">Uredi putni nalog {formatEntityCode('PN', workOrder.id)}</h3>
|
<h3 className="text-lg font-semibold text-text-main">Uredi putni nalog {formatWorkOrderDisplayCode(workOrder)}</h3>
|
||||||
<p className="text-sm text-text-muted">Dizalica: {craneLabel}</p>
|
<p className="text-sm text-text-muted">Dizalica: {craneLabel}</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@@ -491,6 +503,20 @@ 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">Status</h4>
|
<h4 className="mb-2 text-sm font-semibold text-text-main">Status</h4>
|
||||||
|
<label className="mb-3 flex flex-col gap-1 text-sm">
|
||||||
|
<span className="font-medium text-text-main">Prikaz putnog naloga *</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={form.display_code}
|
||||||
|
onInput={(event) => setForm({ ...form, display_code: event.currentTarget.value.toUpperCase() })}
|
||||||
|
className="rounded-lg border border-border-hairline bg-canvas-base px-3 py-2 text-text-main disabled:bg-canvas-deep"
|
||||||
|
pattern="[A-Za-z]{2}[0-9]{6}"
|
||||||
|
maxLength={8}
|
||||||
|
placeholder="MT070626"
|
||||||
|
disabled={!isEditing}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
<label className="flex flex-col gap-1 text-sm">
|
<label className="flex flex-col gap-1 text-sm">
|
||||||
<span className="font-medium text-text-main">Status *</span>
|
<span className="font-medium text-text-main">Status *</span>
|
||||||
<select
|
<select
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
} from '../../stores/fleetDashboardStore';
|
} from '../../stores/fleetDashboardStore';
|
||||||
import { $accessToken, $authReady, hydrateAuthFromStorage } from '../../stores/authStore';
|
import { $accessToken, $authReady, hydrateAuthFromStorage } from '../../stores/authStore';
|
||||||
import { fetchNotifications, connectNotifications } from '../../stores/notificationStore';
|
import { fetchNotifications, connectNotifications } from '../../stores/notificationStore';
|
||||||
import { formatEntityCode } from '../../lib/displayIds';
|
import { formatWorkOrderDisplayCode } from '../../lib/displayIds';
|
||||||
import { resolveMediaUrl, useAuthenticatedMediaSources } from './WorkOrderImageCarousel';
|
import { resolveMediaUrl, useAuthenticatedMediaSources } from './WorkOrderImageCarousel';
|
||||||
import TaskWorkHoursTableModal from './TaskWorkHoursTableModal';
|
import TaskWorkHoursTableModal from './TaskWorkHoursTableModal';
|
||||||
|
|
||||||
@@ -122,7 +122,7 @@ export default function WorkOrderInvoicesPdfPage() {
|
|||||||
if (!workOrder) {
|
if (!workOrder) {
|
||||||
return 'Računi putnog naloga';
|
return 'Računi putnog naloga';
|
||||||
}
|
}
|
||||||
return `${formatEntityCode('PN', workOrder.id)}`;
|
return formatWorkOrderDisplayCode(workOrder);
|
||||||
}, [workOrder]);
|
}, [workOrder]);
|
||||||
|
|
||||||
const mediaPaths = useMemo(() => {
|
const mediaPaths = useMemo(() => {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useEffect, useState } from 'preact/hooks';
|
|||||||
import { useStore } from '@nanostores/preact';
|
import { useStore } from '@nanostores/preact';
|
||||||
import ModalShell from '../ui/ModalShell';
|
import ModalShell from '../ui/ModalShell';
|
||||||
import { $user } from '../../stores/authStore';
|
import { $user } from '../../stores/authStore';
|
||||||
|
import { generateWorkOrderDisplayCode } from '../../lib/displayIds';
|
||||||
|
|
||||||
const PURPOSE_OPTIONS = [
|
const PURPOSE_OPTIONS = [
|
||||||
{ value: 'defektaza', label: 'Defektaža' },
|
{ value: 'defektaza', label: 'Defektaža' },
|
||||||
@@ -11,6 +12,7 @@ const PURPOSE_OPTIONS = [
|
|||||||
|
|
||||||
const INITIAL_FORM = {
|
const INITIAL_FORM = {
|
||||||
has_travel_order: false,
|
has_travel_order: false,
|
||||||
|
display_code: '',
|
||||||
start_mileage: '',
|
start_mileage: '',
|
||||||
end_mileage: '',
|
end_mileage: '',
|
||||||
servicer_vehicle_registration: '',
|
servicer_vehicle_registration: '',
|
||||||
@@ -54,6 +56,7 @@ export default function WorkOrderModal({ open, selectedCrane, onClose, onSubmit
|
|||||||
|
|
||||||
setForm((prev) => ({
|
setForm((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
|
display_code: generateWorkOrderDisplayCode(user?.first_name, user?.last_name, new Date()),
|
||||||
start_mileage: selectedCrane ? String(selectedCrane.current_mileage ?? '') : prev.start_mileage,
|
start_mileage: selectedCrane ? String(selectedCrane.current_mileage ?? '') : prev.start_mileage,
|
||||||
servicer_vehicle_registration: user?.assigned_vehicle?.registration_number || '',
|
servicer_vehicle_registration: user?.assigned_vehicle?.registration_number || '',
|
||||||
servicer_vehicle_make_model: `${user?.assigned_vehicle?.make || ''} ${user?.assigned_vehicle?.model || ''}`.trim(),
|
servicer_vehicle_make_model: `${user?.assigned_vehicle?.make || ''} ${user?.assigned_vehicle?.model || ''}`.trim(),
|
||||||
@@ -116,6 +119,15 @@ export default function WorkOrderModal({ open, selectedCrane, onClose, onSubmit
|
|||||||
setErrorMessage('Svrha naloga je obavezna.');
|
setErrorMessage('Svrha naloga je obavezna.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const displayCode = String(form.display_code || '').trim().toUpperCase();
|
||||||
|
if (!displayCode) {
|
||||||
|
setErrorMessage('Prikaz putnog naloga je obavezan. Provjerite da serviser ima ime i prezime ili unesite oznaku ručno.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!/^[A-Z]{2}[0-9]{6}$/.test(displayCode)) {
|
||||||
|
setErrorMessage('Prikaz putnog naloga mora biti u formatu MT070626.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let startMileage = null;
|
let startMileage = null;
|
||||||
let endMileage = null;
|
let endMileage = null;
|
||||||
@@ -191,6 +203,7 @@ export default function WorkOrderModal({ open, selectedCrane, onClose, onSubmit
|
|||||||
try {
|
try {
|
||||||
await onSubmit({
|
await onSubmit({
|
||||||
vehicle: selectedCrane.id,
|
vehicle: selectedCrane.id,
|
||||||
|
display_code: displayCode,
|
||||||
start_mileage: startMileage,
|
start_mileage: startMileage,
|
||||||
end_mileage: endMileage,
|
end_mileage: endMileage,
|
||||||
servicer_vehicle_registration: form.servicer_vehicle_registration.trim(),
|
servicer_vehicle_registration: form.servicer_vehicle_registration.trim(),
|
||||||
@@ -233,6 +246,20 @@ export default function WorkOrderModal({ open, selectedCrane, onClose, onSubmit
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form onSubmit={submit} className="max-h-[calc(100vh-8rem)] space-y-4 overflow-y-auto px-5 py-4">
|
<form onSubmit={submit} className="max-h-[calc(100vh-8rem)] space-y-4 overflow-y-auto px-5 py-4">
|
||||||
|
<label className="flex flex-col gap-1 text-sm">
|
||||||
|
<span className="font-medium text-text-main">Prikaz putnog naloga *</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={form.display_code}
|
||||||
|
onInput={(event) => setForm({ ...form, display_code: event.currentTarget.value.toUpperCase() })}
|
||||||
|
className="rounded-lg border border-border-hairline bg-canvas-base px-3 py-2 text-text-main"
|
||||||
|
placeholder="MT070626"
|
||||||
|
pattern="[A-Za-z]{2}[0-9]{6}"
|
||||||
|
maxLength={8}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
<label className="flex flex-col gap-1 text-sm">
|
<label className="flex flex-col gap-1 text-sm">
|
||||||
<span className="font-medium text-text-main">Status *</span>
|
<span className="font-medium text-text-main">Status *</span>
|
||||||
<select
|
<select
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ const isDev = import.meta.env.DEV;
|
|||||||
<div class="absolute inset-0 bg-[linear-gradient(to_right,var(--color-text-main)_0.03_1px,transparent_1px),linear-gradient(to_bottom,var(--color-text-main)_0.03_1px,transparent_1px)] bg-[size:64px_64px] opacity-[0.4] dark:opacity-[0.02] pointer-events-none z-0"></div>
|
<div class="absolute inset-0 bg-[linear-gradient(to_right,var(--color-text-main)_0.03_1px,transparent_1px),linear-gradient(to_bottom,var(--color-text-main)_0.03_1px,transparent_1px)] bg-[size:64px_64px] opacity-[0.4] dark:opacity-[0.02] pointer-events-none z-0"></div>
|
||||||
|
|
||||||
{!isAuthPage && <NetworkGuard client:only="preact" />}
|
{!isAuthPage && <NetworkGuard client:only="preact" />}
|
||||||
<Toast client:load />
|
<Toast client:only="preact" />
|
||||||
{!isAuthPage && isDev && <ToastTrigger client:load />}
|
{!isAuthPage && isDev && <ToastTrigger client:load />}
|
||||||
{!isAuthPage && <Navbar client:load minimal={minimalNav} />}
|
{!isAuthPage && <Navbar client:load minimal={minimalNav} />}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,24 @@ export function formatEntityCode(prefix, id, size = 8) {
|
|||||||
return `${prefix}-${short}`;
|
return `${prefix}-${short}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function generateWorkOrderDisplayCode(firstName, lastName, dateValue = new Date()) {
|
||||||
|
const firstInitial = String(firstName || '').trim().charAt(0).toUpperCase();
|
||||||
|
const lastInitial = String(lastName || '').trim().charAt(0).toUpperCase();
|
||||||
|
if (!firstInitial || !lastInitial) return '';
|
||||||
|
const parsedDate = dateValue instanceof Date ? dateValue : new Date(dateValue);
|
||||||
|
if (Number.isNaN(parsedDate.getTime())) return '';
|
||||||
|
const day = String(parsedDate.getDate()).padStart(2, '0');
|
||||||
|
const month = String(parsedDate.getMonth() + 1).padStart(2, '0');
|
||||||
|
const year = String(parsedDate.getFullYear()).slice(-2);
|
||||||
|
return `${firstInitial}${lastInitial}${day}${month}${year}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatWorkOrderDisplayCode(workOrder) {
|
||||||
|
const displayCode = String(workOrder?.display_code || '').trim().toUpperCase();
|
||||||
|
if (displayCode) return displayCode;
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
|
|
||||||
export function formatPurposeLabel(value) {
|
export function formatPurposeLabel(value) {
|
||||||
const normalized = String(value || '').trim().toLowerCase();
|
const normalized = String(value || '').trim().toLowerCase();
|
||||||
if (!normalized) return '';
|
if (!normalized) return '';
|
||||||
|
|||||||
Reference in New Issue
Block a user