feat: proširi prikaz dizalice i vlasnika u zadacima
Dodaj detalje dizalice (proizvođač, model i SN) te vlasnika u modal radnog zadatka i kalendar zadataka, uz zajednički frontend helper za DRY prikaz. Proširi Task API serializer dodatnim poljima za prikaz dizalice/vlasnika i ažuriraj testove serializera za nova izlazna polja. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -86,6 +86,11 @@ class TaskSerializer(serializers.ModelSerializer):
|
|||||||
assigned_to_name = serializers.SerializerMethodField()
|
assigned_to_name = serializers.SerializerMethodField()
|
||||||
work_order_label = serializers.SerializerMethodField()
|
work_order_label = serializers.SerializerMethodField()
|
||||||
vehicle_registration = serializers.SerializerMethodField()
|
vehicle_registration = serializers.SerializerMethodField()
|
||||||
|
vehicle_make = serializers.SerializerMethodField()
|
||||||
|
vehicle_model = serializers.SerializerMethodField()
|
||||||
|
vehicle_owner_name = serializers.SerializerMethodField()
|
||||||
|
vehicle_asset_type = serializers.SerializerMethodField()
|
||||||
|
crane_serial_number = serializers.SerializerMethodField()
|
||||||
template_id = serializers.UUIDField(write_only=True, required=False, allow_null=True)
|
template_id = serializers.UUIDField(write_only=True, required=False, allow_null=True)
|
||||||
auto_close_work_order = serializers.BooleanField(write_only=True, required=False)
|
auto_close_work_order = serializers.BooleanField(write_only=True, required=False)
|
||||||
|
|
||||||
@@ -94,7 +99,8 @@ class TaskSerializer(serializers.ModelSerializer):
|
|||||||
fields = [
|
fields = [
|
||||||
'id', 'title', 'description', 'status',
|
'id', 'title', 'description', 'status',
|
||||||
'assigned_to', 'assigned_to_name',
|
'assigned_to', 'assigned_to_name',
|
||||||
'vehicle', 'vehicle_registration',
|
'vehicle', 'vehicle_registration', 'vehicle_make', 'vehicle_model',
|
||||||
|
'vehicle_owner_name', 'vehicle_asset_type', 'crane_serial_number',
|
||||||
'work_order', 'work_order_label',
|
'work_order', 'work_order_label',
|
||||||
'scheduled_date',
|
'scheduled_date',
|
||||||
'template_id', 'auto_close_work_order',
|
'template_id', 'auto_close_work_order',
|
||||||
@@ -180,3 +186,28 @@ class TaskSerializer(serializers.ModelSerializer):
|
|||||||
if not obj.vehicle_id:
|
if not obj.vehicle_id:
|
||||||
return None
|
return None
|
||||||
return obj.vehicle.registration_number
|
return obj.vehicle.registration_number
|
||||||
|
|
||||||
|
def get_vehicle_make(self, obj):
|
||||||
|
if not obj.vehicle_id:
|
||||||
|
return None
|
||||||
|
return obj.vehicle.make or None
|
||||||
|
|
||||||
|
def get_vehicle_model(self, obj):
|
||||||
|
if not obj.vehicle_id:
|
||||||
|
return None
|
||||||
|
return obj.vehicle.model or None
|
||||||
|
|
||||||
|
def get_vehicle_owner_name(self, obj):
|
||||||
|
if not obj.vehicle_id or not obj.vehicle.client_id:
|
||||||
|
return None
|
||||||
|
return obj.vehicle.client.name
|
||||||
|
|
||||||
|
def get_vehicle_asset_type(self, obj):
|
||||||
|
if not obj.vehicle_id:
|
||||||
|
return None
|
||||||
|
return obj.vehicle.asset_type
|
||||||
|
|
||||||
|
def get_crane_serial_number(self, obj):
|
||||||
|
if not obj.vehicle_id:
|
||||||
|
return None
|
||||||
|
return obj.vehicle.crane_serial_number or None
|
||||||
|
|||||||
@@ -4,13 +4,23 @@ from rest_framework.exceptions import ValidationError
|
|||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from modules.task_management.models import Task
|
from modules.task_management.models import Task
|
||||||
from modules.fleet.models import Vehicle, WorkOrder
|
from modules.fleet.models import Vehicle, WorkOrder
|
||||||
|
from modules.crm.models import Client
|
||||||
|
|
||||||
|
|
||||||
class TaskSerializerTests(TestCase):
|
class TaskSerializerTests(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
self.user = User.objects.create_user(username="task-user-base", email="task-user-base@example.test", password="pass")
|
self.user = User.objects.create_user(username="task-user-base", email="task-user-base@example.test", password="pass")
|
||||||
self.vehicle = Vehicle.objects.create(registration_number="ZG-TSK-01", asset_type='crane')
|
self.client = Client.objects.create(name="Klijent zadatka")
|
||||||
|
self.vehicle = Vehicle.objects.create(
|
||||||
|
registration_number="ZG-TSK-01",
|
||||||
|
asset_type='crane',
|
||||||
|
make='Liebherr',
|
||||||
|
model='LTM',
|
||||||
|
crane_serial_number='123456',
|
||||||
|
client=self.client,
|
||||||
|
is_company_vehicle=False,
|
||||||
|
)
|
||||||
|
|
||||||
def test_validate_title_too_short_raises(self):
|
def test_validate_title_too_short_raises(self):
|
||||||
serializer = TaskSerializer(data={"title": "ab", "description": "", "status": "aktivan", "vehicle": str(self.vehicle.id)})
|
serializer = TaskSerializer(data={"title": "ab", "description": "", "status": "aktivan", "vehicle": str(self.vehicle.id)})
|
||||||
@@ -67,6 +77,15 @@ class TaskSerializerTests(TestCase):
|
|||||||
serializer = TaskSerializer(instance=task)
|
serializer = TaskSerializer(instance=task)
|
||||||
assert serializer.data['scheduled_date'] is None
|
assert serializer.data['scheduled_date'] is None
|
||||||
|
|
||||||
|
def test_vehicle_details_are_included_in_serialized_output(self):
|
||||||
|
task = Task.objects.create(title="Task crane details", assigned_to=self.user, vehicle=self.vehicle)
|
||||||
|
serializer = TaskSerializer(instance=task)
|
||||||
|
assert serializer.data['vehicle_make'] == 'Liebherr'
|
||||||
|
assert serializer.data['vehicle_model'] == 'LTM'
|
||||||
|
assert serializer.data['crane_serial_number'] == '123456'
|
||||||
|
assert serializer.data['vehicle_owner_name'] == 'Klijent zadatka'
|
||||||
|
assert serializer.data['vehicle_asset_type'] == 'crane'
|
||||||
|
|
||||||
def test_scheduled_date_accepts_valid_date(self):
|
def test_scheduled_date_accepts_valid_date(self):
|
||||||
serializer = TaskSerializer(
|
serializer = TaskSerializer(
|
||||||
data={"title": "Task mit datum", "status": "aktivan",
|
data={"title": "Task mit datum", "status": "aktivan",
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ class TaskViewSet(viewsets.ModelViewSet):
|
|||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
qs = Task.objects.filter(is_active=True).select_related(
|
qs = Task.objects.filter(is_active=True).select_related(
|
||||||
'assigned_to', 'vehicle', 'work_order', 'work_order__vehicle', 'work_hours_table'
|
'assigned_to', 'vehicle', 'vehicle__client', 'work_order', 'work_order__vehicle', 'work_hours_table'
|
||||||
)
|
)
|
||||||
vehicle_id = self.request.query_params.get('vehicle')
|
vehicle_id = self.request.query_params.get('vehicle')
|
||||||
if vehicle_id:
|
if vehicle_id:
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useMemo, useState } from 'preact/hooks';
|
import { useMemo, useState } from 'preact/hooks';
|
||||||
import { getStatusLabel } from '../../stores/taskStore';
|
import { getStatusLabel } from '../../stores/taskStore';
|
||||||
import LeftSlideDrawer from '../layout/LeftSlideDrawer';
|
import LeftSlideDrawer from '../layout/LeftSlideDrawer';
|
||||||
|
import { getTaskCraneKind, getTaskCraneOwner, getTaskCraneSerial } from '../../lib/taskCraneDisplay';
|
||||||
|
|
||||||
const WEEKDAY_LABELS = ['Po', 'Ut', 'Sr', 'Če', 'Pe', 'Su', 'Ne'];
|
const WEEKDAY_LABELS = ['Po', 'Ut', 'Sr', 'Če', 'Pe', 'Su', 'Ne'];
|
||||||
const MONTH_NAMES = [
|
const MONTH_NAMES = [
|
||||||
@@ -242,6 +243,9 @@ export default function TaskCalendarWidget({ tasks = [], notes = [], onTaskClick
|
|||||||
{task.assigned_to_name || '-'} •{' '}
|
{task.assigned_to_name || '-'} •{' '}
|
||||||
{getStatusLabel(task.status || '-')}
|
{getStatusLabel(task.status || '-')}
|
||||||
</span>
|
</span>
|
||||||
|
<span className="mt-0.5 block text-[11px] text-text-muted">
|
||||||
|
{getTaskCraneSerial(task)} • {getTaskCraneKind(task)} • {getTaskCraneOwner(task)}
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useEffect, useMemo, useState } from 'preact/hooks';
|
|||||||
import ModalShell from '../ui/ModalShell';
|
import ModalShell from '../ui/ModalShell';
|
||||||
import { formatWorkOrderDisplayCode } from '../../lib/displayIds';
|
import { formatWorkOrderDisplayCode } from '../../lib/displayIds';
|
||||||
import { getStatusLabel } from '../../stores/taskStore';
|
import { getStatusLabel } from '../../stores/taskStore';
|
||||||
|
import { getTaskCraneDisplay, getTaskCraneOwner } from '../../lib/taskCraneDisplay';
|
||||||
|
|
||||||
const STATUS_OPTIONS = ['aktivan', 'servis', 'spreman_za_zavrsetak', 'zavrsen', 'neaktivan'];
|
const STATUS_OPTIONS = ['aktivan', 'servis', 'spreman_za_zavrsetak', 'zavrsen', 'neaktivan'];
|
||||||
|
|
||||||
@@ -211,9 +212,13 @@ export default function TaskServiceRecordsModal({
|
|||||||
<dt className="text-xs text-text-muted">Dodijeljeno</dt>
|
<dt className="text-xs text-text-muted">Dodijeljeno</dt>
|
||||||
<dd className="text-text-main">{task.assigned_to_name || '-'}</dd>
|
<dd className="text-text-main">{task.assigned_to_name || '-'}</dd>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt className="text-xs text-text-muted">Vlasnik</dt>
|
||||||
|
<dd className="text-text-main">{getTaskCraneOwner(task)}</dd>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<dt className="text-xs text-text-muted">Dizalica</dt>
|
<dt className="text-xs text-text-muted">Dizalica</dt>
|
||||||
<dd className="text-text-main">{task.vehicle_registration || '-'}</dd>
|
<dd className="text-text-main">{getTaskCraneDisplay(task)}</dd>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<dt className="text-xs text-text-muted">Putni nalog</dt>
|
<dt className="text-xs text-text-muted">Putni nalog</dt>
|
||||||
@@ -255,6 +260,26 @@ export default function TaskServiceRecordsModal({
|
|||||||
className="resize-none rounded-lg border border-border-hairline bg-canvas-elevated px-3 py-2 text-text-main disabled:opacity-60"
|
className="resize-none rounded-lg border border-border-hairline bg-canvas-elevated px-3 py-2 text-text-main disabled:opacity-60"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
<div className="grid gap-3 sm:grid-cols-2">
|
||||||
|
<label className="flex flex-col gap-1 text-sm">
|
||||||
|
<span className="text-xs text-text-muted">Vlasnik</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={getTaskCraneOwner(task)}
|
||||||
|
disabled
|
||||||
|
className="rounded-lg border border-border-hairline bg-canvas-deep px-3 py-2 text-text-main disabled:opacity-80"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label className="flex flex-col gap-1 text-sm">
|
||||||
|
<span className="text-xs text-text-muted">Dizalica</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={getTaskCraneDisplay(task)}
|
||||||
|
disabled
|
||||||
|
className="rounded-lg border border-border-hairline bg-canvas-deep px-3 py-2 text-text-main disabled:opacity-80"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
<label className="flex flex-col gap-1 text-sm">
|
<label className="flex flex-col gap-1 text-sm">
|
||||||
<span className="text-xs text-text-muted">Status</span>
|
<span className="text-xs text-text-muted">Status</span>
|
||||||
<select
|
<select
|
||||||
|
|||||||
35
frontend/src/lib/taskCraneDisplay.js
Normal file
35
frontend/src/lib/taskCraneDisplay.js
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
function normalizeText(value) {
|
||||||
|
const text = String(value || '').trim();
|
||||||
|
return text || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTaskCraneKind(task) {
|
||||||
|
const make = normalizeText(task?.vehicle_make);
|
||||||
|
const model = normalizeText(task?.vehicle_model);
|
||||||
|
const kind = [make, model].filter(Boolean).join(' ').trim();
|
||||||
|
return kind || '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTaskCraneSerial(task) {
|
||||||
|
const serial = normalizeText(task?.crane_serial_number);
|
||||||
|
return serial || '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTaskCraneOwner(task) {
|
||||||
|
return normalizeText(task?.vehicle_owner_name) || '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTaskCraneDisplay(task) {
|
||||||
|
const kind = getTaskCraneKind(task);
|
||||||
|
const serial = getTaskCraneSerial(task);
|
||||||
|
if (kind !== '-' && serial !== '-') {
|
||||||
|
return `${kind} (SN ${serial})`;
|
||||||
|
}
|
||||||
|
if (kind !== '-') {
|
||||||
|
return kind;
|
||||||
|
}
|
||||||
|
if (serial !== '-') {
|
||||||
|
return `SN ${serial}`;
|
||||||
|
}
|
||||||
|
return normalizeText(task?.vehicle_registration) || '-';
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user