Refactor fleet module and add cleanup script
- Update Celery configuration in celery.py - Modify base settings for improved performance - Enhance task management in fleet tasks.py - Revise fleet views.py for better data handling - Add cleanup_tmp_archives.sh script for temporary file management - Adjust docker-compose.prod.yml for consistency - Optimize fleetDashboardStore.js with reduced code complexity
This commit is contained in:
@@ -28,9 +28,6 @@ const DASHBOARD_FETCH_TTL_MS = 30_000; // 30 sekundi
|
||||
let dbPromise = null;
|
||||
let syncListenerStarted = false;
|
||||
let isSyncInProgress = false;
|
||||
const archiveNotificationPollers = new Map();
|
||||
const ARCHIVE_NOTIFICATION_POLL_INTERVAL_MS = 10_000;
|
||||
const ARCHIVE_NOTIFICATION_POLL_TIMEOUT_MS = 15 * 60 * 1000;
|
||||
|
||||
function isBrowser() {
|
||||
return typeof window !== 'undefined';
|
||||
@@ -467,19 +464,16 @@ export const $dashboardStats = computed(
|
||||
const servicesToday = tasks.filter(
|
||||
(t) => t.scheduled_date === today && (t.status === 'aktivan' || t.status === 'servis')
|
||||
).length;
|
||||
const totalWorkOrders = workOrders.length;
|
||||
// Aktivni taskovi bez dodijeljenog putnog naloga
|
||||
const tasksWithoutWorkOrder = tasks.filter(
|
||||
(t) => (t.status === 'aktivan' || t.status === 'servis' || t.status === 'spreman_za_zavrsetak')
|
||||
&& !t.work_order
|
||||
).length;
|
||||
const warnings = serviceRecords.filter((record) => {
|
||||
if (record.next_service_due_at == null || record.mileage == null) return false;
|
||||
return Number(record.next_service_due_at) - Number(record.mileage) <= 1000;
|
||||
}).length;
|
||||
|
||||
return {
|
||||
openWorkOrders,
|
||||
closedWorkOrders,
|
||||
totalWorkOrders,
|
||||
servicesToday,
|
||||
tasksWithoutWorkOrder,
|
||||
warnings,
|
||||
};
|
||||
}
|
||||
);
|
||||
@@ -737,12 +731,10 @@ export async function fetchTasksByWorkOrder(workOrderId) {
|
||||
|
||||
export async function fetchWorkOrderTaskServiceContext(workOrderId) {
|
||||
if (!workOrderId) {
|
||||
return { tasks: [], additional_costs_table: null, travel_expenses_table: null };
|
||||
return { tasks: [], additional_costs_table: null };
|
||||
}
|
||||
const payload = await api.get(`fleet/work-orders/${encodeURIComponent(workOrderId)}/task-service-context/`);
|
||||
return payload && typeof payload === 'object'
|
||||
? payload
|
||||
: { tasks: [], additional_costs_table: null, travel_expenses_table: null };
|
||||
return payload && typeof payload === 'object' ? payload : { tasks: [], additional_costs_table: null };
|
||||
}
|
||||
|
||||
export async function fetchWorkOrderAdditionalCostsTable(workOrderId) {
|
||||
@@ -767,42 +759,6 @@ export async function updateWorkOrderAdditionalCostsTable(workOrderId, data) {
|
||||
return payload;
|
||||
}
|
||||
|
||||
export async function fetchWorkOrderTravelExpensesTable(workOrderId) {
|
||||
if (!workOrderId) {
|
||||
return {
|
||||
work_order: null,
|
||||
broj_sati: '0.00',
|
||||
kolicina_dnevnica: '0.00',
|
||||
iznos_dnevnica: '30.00',
|
||||
daily_rate_country: 'HR',
|
||||
total_for_payout: '0.00',
|
||||
};
|
||||
}
|
||||
const payload = await api.get(`fleet/work-orders/${encodeURIComponent(workOrderId)}/travel-expenses-table/`);
|
||||
return payload && typeof payload === 'object'
|
||||
? payload
|
||||
: {
|
||||
work_order: null,
|
||||
broj_sati: '0.00',
|
||||
kolicina_dnevnica: '0.00',
|
||||
iznos_dnevnica: '30.00',
|
||||
daily_rate_country: 'HR',
|
||||
total_for_payout: '0.00',
|
||||
};
|
||||
}
|
||||
|
||||
export async function updateWorkOrderTravelExpensesTable(workOrderId, data) {
|
||||
if (!workOrderId) {
|
||||
throw new Error('Work order ID je obavezan.');
|
||||
}
|
||||
const payload = await api.put(
|
||||
`fleet/work-orders/${encodeURIComponent(workOrderId)}/travel-expenses-table/`,
|
||||
data ?? {},
|
||||
);
|
||||
showToast('Obračun putnih troškova je uspješno spremljen.', 'success');
|
||||
return payload;
|
||||
}
|
||||
|
||||
export async function updateTaskWorkHoursTable(taskId, data) {
|
||||
if (!taskId) {
|
||||
throw new Error('Task ID je obavezan.');
|
||||
@@ -812,17 +768,6 @@ export async function updateTaskWorkHoursTable(taskId, data) {
|
||||
return payload;
|
||||
}
|
||||
|
||||
export async function updateTaskServiceReportNote(taskId, note) {
|
||||
if (!taskId) {
|
||||
throw new Error('Task ID je obavezan.');
|
||||
}
|
||||
const payload = await api.patch(`tasks/tasks/${encodeURIComponent(taskId)}/`, {
|
||||
service_report_note: String(note ?? ''),
|
||||
});
|
||||
showToast('Napomena servisnog izvještaja je uspješno spremljena.', 'success');
|
||||
return payload;
|
||||
}
|
||||
|
||||
export async function createWorkOrderInvoice(workOrderId, data = {}, signal = null) {
|
||||
if (!workOrderId) {
|
||||
throw new Error('Work order ID je obavezan.');
|
||||
@@ -872,70 +817,6 @@ function _schedulePdfNotificationPolling() {
|
||||
});
|
||||
}
|
||||
|
||||
function _findCompletedArchiveNotification(notifications, generatedArchiveId) {
|
||||
if (!generatedArchiveId || !Array.isArray(notifications)) {
|
||||
return null;
|
||||
}
|
||||
return notifications.find((notification) => {
|
||||
const metadata = notification?.metadata || {};
|
||||
return metadata.entity_type === 'fleet_archive'
|
||||
&& String(metadata.generated_archive_id || '') === String(generatedArchiveId)
|
||||
&& ['completed', 'failed'].includes(String(metadata.stage || ''));
|
||||
}) || null;
|
||||
}
|
||||
|
||||
function _clearArchiveNotificationPoller(generatedArchiveId) {
|
||||
const key = String(generatedArchiveId || '');
|
||||
const handles = archiveNotificationPollers.get(key);
|
||||
if (!handles) {
|
||||
return;
|
||||
}
|
||||
window.clearInterval(handles.intervalId);
|
||||
window.clearTimeout(handles.timeoutId);
|
||||
archiveNotificationPollers.delete(key);
|
||||
}
|
||||
|
||||
function _scheduleArchiveNotificationPolling(generatedArchiveId = null) {
|
||||
if (!isBrowser()) return;
|
||||
if (!generatedArchiveId) {
|
||||
[5000, 20000, 60000].forEach((delay) => {
|
||||
setTimeout(() => _refreshNotificationsAsync(), delay);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const key = String(generatedArchiveId);
|
||||
if (archiveNotificationPollers.has(key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pollOnce = async () => {
|
||||
try {
|
||||
const notificationModule = await import('./notificationStore.js');
|
||||
await notificationModule.fetchNotifications();
|
||||
const resolvedNotification = _findCompletedArchiveNotification(
|
||||
notificationModule.$notifications.get(),
|
||||
key
|
||||
);
|
||||
if (resolvedNotification) {
|
||||
_clearArchiveNotificationPoller(key);
|
||||
}
|
||||
} catch (_) {
|
||||
// silent — korisnik će i dalje vidjeti toast ili ručno osvježiti notifikacije
|
||||
}
|
||||
};
|
||||
|
||||
const intervalId = window.setInterval(() => {
|
||||
void pollOnce();
|
||||
}, ARCHIVE_NOTIFICATION_POLL_INTERVAL_MS);
|
||||
const timeoutId = window.setTimeout(() => {
|
||||
_clearArchiveNotificationPoller(key);
|
||||
}, ARCHIVE_NOTIFICATION_POLL_TIMEOUT_MS);
|
||||
|
||||
archiveNotificationPollers.set(key, { intervalId, timeoutId });
|
||||
void pollOnce();
|
||||
}
|
||||
|
||||
export async function downloadWorkOrderPdf(workOrderId) {
|
||||
if (!workOrderId) {
|
||||
throw new Error('Work order ID je obavezan.');
|
||||
@@ -970,30 +851,10 @@ export async function downloadWorkOrderInvoicesPdf(workOrderId) {
|
||||
return payload;
|
||||
}
|
||||
|
||||
export async function downloadWorkOrderServiceRecordsPdf(workOrderId, taskId = null, taskOptions = {}) {
|
||||
export async function downloadWorkOrderServiceRecordsPdf(workOrderId) {
|
||||
if (!workOrderId) {
|
||||
throw new Error('Work order ID je obavezan.');
|
||||
}
|
||||
if (taskId) {
|
||||
try {
|
||||
const blob = await api.get(
|
||||
`fleet/work-orders/${encodeURIComponent(workOrderId)}/service-records-pdf/?task_id=${encodeURIComponent(taskId)}`,
|
||||
{ responseType: 'blob' },
|
||||
);
|
||||
const workOrderDisplayCode = String(taskOptions?.workOrderDisplayCode || '').trim().toUpperCase() || String(workOrderId);
|
||||
const taskTitle = String(taskOptions?.taskTitle || '').trim()
|
||||
.replace(/\s+/g, '_')
|
||||
.replace(/[^A-Za-z0-9_-]+/g, '')
|
||||
.replace(/^[_\-.]+|[_\-.]+$/g, '') || `task-${taskId}`;
|
||||
saveBlobToFile(blob, `${workOrderDisplayCode}.SN-${taskTitle}.pdf`);
|
||||
showToast('PDF servisnih zapisa za odabrani task je preuzet.', 'success');
|
||||
return;
|
||||
} catch (err) {
|
||||
const msg = err?.message || 'Preuzimanje PDF-a servisnih zapisa za task nije uspjelo.';
|
||||
showToast(msg, 'error');
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
showToast('Kreiran je zahtjev za PDF servisnih zapisa putnog naloga.', 'info');
|
||||
const payload = await api.post(`fleet/work-orders/${encodeURIComponent(workOrderId)}/service-records-pdf-request/`, {});
|
||||
if (payload?.status === 'ready' && payload?.download_url) {
|
||||
@@ -1022,28 +883,14 @@ export async function downloadWorkOrderDocx(workOrderId) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function downloadWorkOrderServiceRecordsDocx(workOrderId, taskId = null, taskOptions = {}) {
|
||||
export async function downloadWorkOrderServiceRecordsDocx(workOrderId) {
|
||||
if (!workOrderId) {
|
||||
throw new Error('Work order ID je obavezan.');
|
||||
}
|
||||
const query = taskId ? `?task_id=${encodeURIComponent(taskId)}` : '';
|
||||
try {
|
||||
const blob = await api.get(
|
||||
`fleet/work-orders/${encodeURIComponent(workOrderId)}/service-records-docx/${query}`,
|
||||
{ responseType: 'blob' },
|
||||
);
|
||||
if (taskId) {
|
||||
const workOrderDisplayCode = String(taskOptions?.workOrderDisplayCode || '').trim().toUpperCase() || String(workOrderId);
|
||||
const taskTitle = String(taskOptions?.taskTitle || '').trim()
|
||||
.replace(/\s+/g, '_')
|
||||
.replace(/[^A-Za-z0-9_-]+/g, '')
|
||||
.replace(/^[_\-.]+|[_\-.]+$/g, '') || `task-${taskId}`;
|
||||
saveBlobToFile(blob, `${workOrderDisplayCode}.SN-${taskTitle}.docx`);
|
||||
showToast('DOCX servisnih zapisa za odabrani task je preuzet.', 'success');
|
||||
} else {
|
||||
saveBlobToFile(blob, `${workOrderId}.work-order-service-records.docx`);
|
||||
showToast('DOCX servisnih zapisa je preuzet.', 'success');
|
||||
}
|
||||
const blob = await api.get(`fleet/work-orders/${encodeURIComponent(workOrderId)}/service-records-docx/`, { responseType: 'blob' });
|
||||
saveBlobToFile(blob, `${workOrderId}.work-order-service-records.docx`);
|
||||
showToast('DOCX servisnih zapisa je preuzet.', 'success');
|
||||
} catch (err) {
|
||||
const msg = err?.message || 'Preuzimanje DOCX servisnih zapisa nije uspjelo.';
|
||||
showToast(msg, 'error');
|
||||
@@ -1125,40 +972,6 @@ export async function downloadMonthlyCostsReport(year, month) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function downloadMonthlyServiceTasksArchive(year, month) {
|
||||
try {
|
||||
const payload = await api.post('fleet/reports/monthly-service-tasks-archive-request/', { year, month });
|
||||
await _refreshNotificationsAsync();
|
||||
if (payload?.status === 'ready' && payload?.download_url) {
|
||||
showToast('ZIP arhiva servisnih taskova je spremna. Preuzmite je kroz notifikaciju.', 'success');
|
||||
return payload;
|
||||
}
|
||||
showToast('ZIP arhiva servisnih taskova se generira u pozadini. Preuzimanje je dostupno kroz notifikaciju.', 'info');
|
||||
_scheduleArchiveNotificationPolling(payload?.generated_archive_id || null);
|
||||
return payload;
|
||||
} catch (err) {
|
||||
showToast(err?.message || 'Pokretanje generiranja ZIP arhive servisnih taskova nije uspjelo.', 'error');
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
export async function downloadMonthlyWorkOrdersArchive(year, month) {
|
||||
try {
|
||||
const payload = await api.post('fleet/reports/monthly-work-orders-archive-request/', { year, month });
|
||||
await _refreshNotificationsAsync();
|
||||
if (payload?.status === 'ready' && payload?.download_url) {
|
||||
showToast('ZIP arhiva putnih naloga je spremna. Preuzmite je kroz notifikaciju.', 'success');
|
||||
return payload;
|
||||
}
|
||||
showToast('ZIP arhiva putnih naloga se generira u pozadini. Preuzimanje je dostupno kroz notifikaciju.', 'info');
|
||||
_scheduleArchiveNotificationPolling(payload?.generated_archive_id || null);
|
||||
return payload;
|
||||
} catch (err) {
|
||||
showToast(err?.message || 'Pokretanje generiranja ZIP arhive putnih naloga nije uspjelo.', 'error');
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
export async function downloadGeneratedPdfByUrl(downloadUrl, filename = 'document.pdf') {
|
||||
if (!downloadUrl) {
|
||||
showToast('Nedostaje URL za preuzimanje PDF-a.', 'error');
|
||||
@@ -1175,22 +988,6 @@ export async function downloadGeneratedPdfByUrl(downloadUrl, filename = 'documen
|
||||
}
|
||||
}
|
||||
|
||||
export async function downloadGeneratedArchiveByUrl(downloadUrl, filename = 'archive.zip') {
|
||||
if (!downloadUrl) {
|
||||
showToast('Nedostaje URL za preuzimanje ZIP arhive.', 'error');
|
||||
throw new Error('Nedostaje URL za preuzimanje ZIP arhive.');
|
||||
}
|
||||
try {
|
||||
const blob = await api.get(downloadUrl, { responseType: 'blob' });
|
||||
saveBlobToFile(blob, filename);
|
||||
return true;
|
||||
} catch (err) {
|
||||
const msg = err?.message || 'Preuzimanje ZIP arhive nije uspjelo.';
|
||||
showToast(msg, 'error');
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
export function openWorkOrderInvoicesPdfPage(workOrderId) {
|
||||
if (!isBrowser() || !workOrderId) {
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user