diff --git a/backend/modules/fleet/admin.py b/backend/modules/fleet/admin.py
index 254ffe1..849d950 100644
--- a/backend/modules/fleet/admin.py
+++ b/backend/modules/fleet/admin.py
@@ -1,3 +1,4 @@
+from pathlib import Path
from django.contrib import admin
from django.contrib.admin import ModelAdmin
from django.utils.html import format_html
@@ -61,8 +62,27 @@ class ServiceRecordInline(admin.TabularInline):
class WorkOrderInvoiceInline(admin.TabularInline):
model = WorkOrderInvoice
extra = 0
- fields = ('naziv_racuna', 'lokacija', 'datum', 'opis', 'image', 'created_by', 'created_at', 'is_active')
- readonly_fields = ('created_at',)
+ fields = ('thumbnail_preview', 'naziv_racuna', 'lokacija', 'datum', 'opis', 'image', 'created_by', 'created_at', 'is_active')
+ readonly_fields = ('thumbnail_preview', 'created_at')
+
+ @admin.display(description="Pregled")
+ def thumbnail_preview(self, obj):
+ if not obj.pk or not obj.image:
+ return "—"
+ suffix = Path(obj.image.name or '').suffix.lower()
+ if suffix == '.pdf':
+ return format_html(
+ '📄 PDF',
+ obj.pk,
+ )
+ return format_html(
+ ''
+ ''
+ '',
+ obj.pk,
+ obj.pk,
+ )
class WorkOrderPhotoInline(admin.TabularInline):
@@ -193,10 +213,30 @@ class WorkOrderAdmin(admin.ModelAdmin):
@admin.register(WorkOrderInvoice)
class WorkOrderInvoiceAdmin(admin.ModelAdmin):
- list_display = ('id', 'work_order', 'naziv_racuna', 'lokacija', 'datum', 'created_by', 'is_active')
+ list_display = ('id', 'image_preview', 'work_order', 'naziv_racuna', 'lokacija', 'datum', 'created_by', 'is_active')
list_filter = ('datum', 'is_active', 'work_order')
search_fields = ('naziv_racuna', 'lokacija', 'opis', 'work_order__id')
ordering = ('-datum', '-created_at')
+ readonly_fields = ('image_preview', 'created_at')
+
+ @admin.display(description="Slika")
+ def image_preview(self, obj):
+ if not obj.pk or not obj.image:
+ return "—"
+ suffix = Path(obj.image.name or '').suffix.lower()
+ if suffix == '.pdf':
+ return format_html(
+ '📄 PDF',
+ obj.pk,
+ )
+ return format_html(
+ ''
+ '
'
+ '',
+ obj.pk,
+ obj.pk,
+ )
@admin.register(WorkOrderAdditionalCostsTable)
diff --git a/backend/modules/fleet/serializers.py b/backend/modules/fleet/serializers.py
index 96a7aac..74cb1cc 100644
--- a/backend/modules/fleet/serializers.py
+++ b/backend/modules/fleet/serializers.py
@@ -373,9 +373,13 @@ class WorkOrderInvoiceSerializer(serializers.ModelSerializer):
return None
if request is None:
return None
- return request.build_absolute_uri(
+ base = request.build_absolute_uri(
reverse('work-order-invoice-image', kwargs={'pk': obj.pk})
)
+ suffix = Path(obj.image.name or '').suffix.lower()
+ if suffix == '.pdf':
+ return base
+ return f"{base}?w=1280&q=80&fmt=jpeg"
def get_image_content_type(self, obj):
if not obj.image:
diff --git a/backend/modules/fleet/tasks.py b/backend/modules/fleet/tasks.py
index 88f0cba..32ec4a1 100644
--- a/backend/modules/fleet/tasks.py
+++ b/backend/modules/fleet/tasks.py
@@ -645,10 +645,19 @@ def _build_work_order_invoices_pdf(work_order):
y = content_top
max_height = y - content_bottom
+ # Kompresiraj u JPEG u memoriji radi manje veličine PDF-a
+ compress_w = min(image.width, 1280)
+ if image.width > compress_w:
+ ratio_c = compress_w / float(image.width)
+ image = image.resize((compress_w, max(1, int(image.height * ratio_c))), Image.LANCZOS)
+ buf = BytesIO()
+ image.save(buf, format='JPEG', quality=75, optimize=True)
+ buf.seek(0)
+
ratio = min(max_width / float(image.width), max_height / float(image.height), 1.0)
draw_width = max(1, int(image.width * ratio))
draw_height = max(1, int(image.height * ratio))
- image_reader = ImageReader(image)
+ image_reader = ImageReader(buf)
pdf.drawImage(
image_reader,
margin,
diff --git a/backend/modules/fleet/views.py b/backend/modules/fleet/views.py
index 4321d6a..0501caf 100644
--- a/backend/modules/fleet/views.py
+++ b/backend/modules/fleet/views.py
@@ -208,6 +208,32 @@ def _docx_filename(work_order, doc_type):
return f"{display_code}.work-order.docx"
+def _compress_image_for_pdf(image_field, max_width=1280, quality=75):
+ """
+ Otvori image_field (Django FileField), kompresiraj na max_width JPEG u memoriji,
+ vrati ImageReader spreman za reportlab. Vraća None ako slika nije dostupna.
+ """
+ try:
+ image_field.open('rb')
+ with Image.open(image_field) as src:
+ img = src.convert('RGB')
+ if img.width > max_width:
+ ratio = max_width / float(img.width)
+ new_h = max(1, int(img.height * ratio))
+ img = img.resize((max_width, new_h), Image.LANCZOS)
+ buf = BytesIO()
+ img.save(buf, format='JPEG', quality=quality, optimize=True)
+ buf.seek(0)
+ return ImageReader(buf)
+ except Exception:
+ return None
+ finally:
+ try:
+ image_field.close()
+ except Exception:
+ pass
+
+
def _get_cached_pdf(work_order, pdf_type):
now = timezone.now()
expected_filename = _pdf_filename(work_order, pdf_type)
@@ -1166,7 +1192,9 @@ def _build_work_order_service_records_pdf(work_order):
prepared = []
for photo in row_photos:
try:
- image_reader = ImageReader(photo.image)
+ image_reader = _compress_image_for_pdf(photo.image)
+ if image_reader is None:
+ continue
source_w, source_h = image_reader.getSize()
if not source_w or not source_h:
continue
@@ -1390,7 +1418,9 @@ def _build_service_record_pdf(service_record):
prepared = []
for photo in row_photos:
try:
- image_reader = ImageReader(photo.image)
+ image_reader = _compress_image_for_pdf(photo.image)
+ if image_reader is None:
+ continue
source_w, source_h = image_reader.getSize()
if not source_w or not source_h:
continue
diff --git a/frontend/src/components/dashboard/WorkOrderInvoicesPdfPage.jsx b/frontend/src/components/dashboard/WorkOrderInvoicesPdfPage.jsx
index fda1ab2..9bec321 100644
--- a/frontend/src/components/dashboard/WorkOrderInvoicesPdfPage.jsx
+++ b/frontend/src/components/dashboard/WorkOrderInvoicesPdfPage.jsx
@@ -19,7 +19,7 @@ import { $accessToken, $authReady, hydrateAuthFromStorage } from '../../stores/a
import { fetchNotifications, connectNotifications } from '../../stores/notificationStore';
import { formatWorkOrderDisplayCode } from '../../lib/displayIds';
import { showToast } from '../../stores/toastStore';
-import { resolveMediaUrl, useAuthenticatedMediaSources } from './WorkOrderImageCarousel';
+import { useAuthenticatedMediaSources } from './WorkOrderImageCarousel';
import TaskWorkHoursTableModal from './TaskWorkHoursTableModal';
import WorkOrderAdditionalCostsModal from './WorkOrderAdditionalCostsModal';
import WorkOrderServiceNotesModal from './WorkOrderServiceNotesModal';
@@ -32,9 +32,11 @@ function readWorkOrderIdFromQuery() {
return params.get('workOrderId') || '';
}
-function InvoiceImagePreview({ imageUrl, alt, contentType }) {
+function InvoiceImagePreview({ imageUrl, rawUrl, alt, contentType, onClick }) {
const [broken, setBroken] = useState(false);
const isPdf = String(contentType || '').toLowerCase() === 'application/pdf';
+ const isProtected = typeof rawUrl === 'string' && /\/api\/fleet\//i.test(rawUrl);
+ const isLoading = isProtected && !imageUrl && !broken;
if (isPdf && imageUrl) {
return (
@@ -49,6 +51,14 @@ function InvoiceImagePreview({ imageUrl, alt, contentType }) {
);
}
+ if (isLoading) {
+ return (
+
Slika nije dostupna.
+ )} +