diff --git a/frontend/src/components/dashboard/FleetDashboardShell.jsx b/frontend/src/components/dashboard/FleetDashboardShell.jsx index a2df441..44e638f 100644 --- a/frontend/src/components/dashboard/FleetDashboardShell.jsx +++ b/frontend/src/components/dashboard/FleetDashboardShell.jsx @@ -6,7 +6,6 @@ import WorkOrderModal from './WorkOrderModal'; import TaskCreateModal from './TaskCreateModal'; import TaskServiceRecordsModal from './TaskServiceRecordsModal'; import WorkOrderDetailModal from './WorkOrderDetailModal'; -import TaskCalendarWidget from './TaskCalendarWidget'; import SyncLogPanel from './SyncLogPanel'; import ServiceRecordForm from '../fleet/ServiceRecordForm'; import ServicePhotoUpload from '../fleet/ServicePhotoUpload'; @@ -145,6 +144,15 @@ export default function FleetDashboardShell({ initialSection = 'dashboard', page hydrateServiceContext(); }, []); + useEffect(() => { + function onCalendarOpenTask(event) { + const task = event?.detail?.task; + if (task) setSelectedTask(task); + } + window.addEventListener('calendar:open-task', onCalendarOpenTask); + return () => window.removeEventListener('calendar:open-task', onCalendarOpenTask); + }, []); + useEffect(() => { const nextSection = String(resolvedInitialSection || 'dashboard'); setActiveSection(nextSection); @@ -593,12 +601,6 @@ export default function FleetDashboardShell({ initialSection = 'dashboard', page return ( - {hasMounted && ( - setSelectedTask(task)} - /> - )}
)} - {/* Sliding panel */} + {/* Clipping wrapper — animates width 0→300, clips shadow+border completely */} `${Math.max(0, v + PANEL_WIDTH)}px`), + pointerEvents: open ? 'auto' : 'none', + }} + className="fixed left-0 top-0 z-[46] h-screen overflow-hidden" > + {/* Inner panel — always 300px, anchored to right edge of wrapper */} +
{/* Header */}

📅 Kalendar zadataka

@@ -244,6 +252,7 @@ export default function TaskCalendarWidget({ tasks = [], onTaskClick }) { )}
+
{/* Toggle tab button — slides right as panel opens */} diff --git a/frontend/src/components/layout/TaskCalendarPortal.jsx b/frontend/src/components/layout/TaskCalendarPortal.jsx new file mode 100644 index 0000000..930b7ce --- /dev/null +++ b/frontend/src/components/layout/TaskCalendarPortal.jsx @@ -0,0 +1,50 @@ +import { useMemo } from 'preact/hooks'; +import { useStore } from '@nanostores/preact'; +import { $tasks } from '../../stores/taskStore'; +import { $user } from '../../stores/authStore'; +import TaskCalendarWidget from '../dashboard/TaskCalendarWidget'; + +/** + * Mountan u Layout.astro (van AnimatedPage transforma) pa fixed pozicija + * uvijek ostaje uz viewport rub. + * + * Filtriranje taskova po ulozi: + * - Serviser → samo taskovi dodijeljeni njemu + * - Supervisor (nije serviser) → svi taskovi + * + * Kad korisnik klikne task u kalendaru, šaljemo custom event koji + * FleetDashboardShell sluša i otvara TaskServiceRecordsModal. + */ +export default function TaskCalendarPortal() { + const allTasks = useStore($tasks); + const user = useStore($user); + + const isServiser = Boolean(user?.is_serviser); + const isTeamMember = Boolean(user?.is_team_member); + const isSupervisorNotServiser = isTeamMember && !isServiser; + + const tasks = useMemo(() => { + if (!user?.id) return []; + // Supervisor vidi sve; serviser vidi samo svoje + if (isSupervisorNotServiser) return allTasks; + return allTasks.filter( + (t) => String(t.assigned_to || '') === String(user.id) + ); + }, [allTasks, user, isSupervisorNotServiser]); + + // Ne renderiramo ništa ako korisnik nije prijavljen + if (!user?.id) return null; + + function handleTaskClick(task) { + window.dispatchEvent( + new CustomEvent('calendar:open-task', { detail: { task } }) + ); + } + + return ( + + ); +} diff --git a/frontend/src/layouts/Layout.astro b/frontend/src/layouts/Layout.astro index ec4acdc..e681f25 100644 --- a/frontend/src/layouts/Layout.astro +++ b/frontend/src/layouts/Layout.astro @@ -8,6 +8,7 @@ import Toast from '../components/ui/Toast.jsx'; import ToastTrigger from '../components/ToastTrigger.jsx'; import NetworkGuard from '../components/ui/NetworkGuard.jsx'; import Navbar from '../components/Navbar.jsx'; +import TaskCalendarPortal from '../components/layout/TaskCalendarPortal.jsx'; interface Props { title: string; @@ -55,6 +56,7 @@ const isDev = import.meta.env.DEV; {!isAuthPage && isDev && } {!isAuthPage && } + {!isAuthPage && }
diff --git a/frontend/src/stores/fleetDashboardStore.js b/frontend/src/stores/fleetDashboardStore.js index 5a8531e..960d643 100644 --- a/frontend/src/stores/fleetDashboardStore.js +++ b/frontend/src/stores/fleetDashboardStore.js @@ -2,6 +2,7 @@ import { atom, computed } from 'nanostores'; import { api } from '../services/apiClient'; import { $accessToken } from './authStore'; import { showToast } from './toastStore'; +import { $tasks } from './taskStore'; const DB_NAME = 'erp-offline-db'; const DB_VERSION = 1; @@ -440,12 +441,15 @@ export const $workOrdersWithVehicle = computed( ); export const $dashboardStats = computed( - [$workOrders, $serviceRecords], - (workOrders, serviceRecords) => { + [$workOrders, $serviceRecords, $tasks], + (workOrders, serviceRecords, tasks) => { const today = new Date().toISOString().slice(0, 10); const openWorkOrders = workOrders.filter((workOrder) => workOrder.status !== 'closed').length; const closedWorkOrders = workOrders.filter((workOrder) => workOrder.status === 'closed').length; - const servicesToday = serviceRecords.filter((record) => record.service_date === today).length; + // Broji Taskove čiji je scheduled_date danas i status aktivan ili u tijeku + const servicesToday = tasks.filter( + (t) => t.scheduled_date === today && (t.status === 'aktivan' || t.status === 'servis') + ).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; @@ -809,11 +813,18 @@ export async function downloadWorkOrderServiceRecordsPdf(workOrderId) { export async function downloadGeneratedPdfByUrl(downloadUrl, filename = 'document.pdf') { if (!downloadUrl) { + showToast('Nedostaje URL za preuzimanje PDF-a.', 'error'); throw new Error('Nedostaje URL za preuzimanje PDF-a.'); } - const blob = await api.get(downloadUrl, { responseType: 'blob' }); - saveBlobToFile(blob, filename); - return true; + try { + const blob = await api.get(downloadUrl, { responseType: 'blob' }); + saveBlobToFile(blob, filename); + return true; + } catch (err) { + const msg = err?.message || 'Preuzimanje PDF-a nije uspjelo.'; + showToast(msg, 'error'); + throw err; + } } export function openWorkOrderInvoicesPdfPage(workOrderId) {