fix: premjesti kalendar widget u Layout, popravi PDF toast i Servisi danas
- TaskCalendarPortal u Layout.astro (van AnimatedPage transforma) pa fixed pozicija uvijek prati viewport rub bez probijanja sadržaja - Kalendar widgeta panel koristi clipping wrapper (overflow:hidden + animirana width) umjesto translate3d — potpuno skriva sjenu i border kad je zatvoren - Serviser vidi samo svoje taskove, supervisor vidi sve; neprijavljeni korisnik ne vidi widget (return null) - FleetDashboardShell sluša calendar:open-task event i otvara TaskServiceRecordsModal - Servisi danas broji Taskove (scheduled_date=danas, status aktivan/servis) umjesto Servisnih zapisa - downloadGeneratedPdfByUrl hvata grešku i prikazuje Toast (npr. PDF nije dostupan ili je istekao) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -6,7 +6,6 @@ import WorkOrderModal from './WorkOrderModal';
|
|||||||
import TaskCreateModal from './TaskCreateModal';
|
import TaskCreateModal from './TaskCreateModal';
|
||||||
import TaskServiceRecordsModal from './TaskServiceRecordsModal';
|
import TaskServiceRecordsModal from './TaskServiceRecordsModal';
|
||||||
import WorkOrderDetailModal from './WorkOrderDetailModal';
|
import WorkOrderDetailModal from './WorkOrderDetailModal';
|
||||||
import TaskCalendarWidget from './TaskCalendarWidget';
|
|
||||||
import SyncLogPanel from './SyncLogPanel';
|
import SyncLogPanel from './SyncLogPanel';
|
||||||
import ServiceRecordForm from '../fleet/ServiceRecordForm';
|
import ServiceRecordForm from '../fleet/ServiceRecordForm';
|
||||||
import ServicePhotoUpload from '../fleet/ServicePhotoUpload';
|
import ServicePhotoUpload from '../fleet/ServicePhotoUpload';
|
||||||
@@ -145,6 +144,15 @@ export default function FleetDashboardShell({ initialSection = 'dashboard', page
|
|||||||
hydrateServiceContext();
|
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(() => {
|
useEffect(() => {
|
||||||
const nextSection = String(resolvedInitialSection || 'dashboard');
|
const nextSection = String(resolvedInitialSection || 'dashboard');
|
||||||
setActiveSection(nextSection);
|
setActiveSection(nextSection);
|
||||||
@@ -593,12 +601,6 @@ export default function FleetDashboardShell({ initialSection = 'dashboard', page
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<AnimatedPage className="min-h-screen">
|
<AnimatedPage className="min-h-screen">
|
||||||
{hasMounted && (
|
|
||||||
<TaskCalendarWidget
|
|
||||||
tasks={tasks}
|
|
||||||
onTaskClick={(task) => setSelectedTask(task)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<main className="min-w-0">
|
<main className="min-w-0">
|
||||||
<DashboardTopbar
|
<DashboardTopbar
|
||||||
activeSection={activeSection}
|
activeSection={activeSection}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export default function TaskCalendarWidget({ tasks = [], onTaskClick }) {
|
|||||||
const [selectedDay, setSelectedDay] = useState(null);
|
const [selectedDay, setSelectedDay] = useState(null);
|
||||||
|
|
||||||
const panelSpring = useSpring({
|
const panelSpring = useSpring({
|
||||||
transform: open ? 'translate3d(0%,0,0)' : `translate3d(-100%,0,0)`,
|
left: open ? 0 : -PANEL_WIDTH,
|
||||||
config: { tension: 260, friction: 28 },
|
config: { tension: 260, friction: 28 },
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -99,10 +99,18 @@ export default function TaskCalendarWidget({ tasks = [], onTaskClick }) {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Sliding panel */}
|
{/* Clipping wrapper — animates width 0→300, clips shadow+border completely */}
|
||||||
<animated.div
|
<animated.div
|
||||||
style={{ ...panelSpring, width: `${PANEL_WIDTH}px` }}
|
style={{
|
||||||
className="fixed left-0 top-0 z-[46] flex h-screen flex-col overflow-hidden border-r border-border-hairline bg-canvas-elevated shadow-2xl"
|
width: panelSpring.left.to((v) => `${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 */}
|
||||||
|
<div
|
||||||
|
style={{ width: `${PANEL_WIDTH}px` }}
|
||||||
|
className="absolute right-0 top-0 flex h-full flex-col border-r border-border-hairline bg-canvas-elevated shadow-2xl"
|
||||||
>
|
>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<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">
|
||||||
@@ -244,6 +252,7 @@ export default function TaskCalendarWidget({ tasks = [], onTaskClick }) {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</animated.div>
|
</animated.div>
|
||||||
|
|
||||||
{/* Toggle tab button — slides right as panel opens */}
|
{/* Toggle tab button — slides right as panel opens */}
|
||||||
|
|||||||
50
frontend/src/components/layout/TaskCalendarPortal.jsx
Normal file
50
frontend/src/components/layout/TaskCalendarPortal.jsx
Normal file
@@ -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 (
|
||||||
|
<TaskCalendarWidget
|
||||||
|
tasks={tasks}
|
||||||
|
onTaskClick={handleTaskClick}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import Toast from '../components/ui/Toast.jsx';
|
|||||||
import ToastTrigger from '../components/ToastTrigger.jsx';
|
import ToastTrigger from '../components/ToastTrigger.jsx';
|
||||||
import NetworkGuard from '../components/ui/NetworkGuard.jsx';
|
import NetworkGuard from '../components/ui/NetworkGuard.jsx';
|
||||||
import Navbar from '../components/Navbar.jsx';
|
import Navbar from '../components/Navbar.jsx';
|
||||||
|
import TaskCalendarPortal from '../components/layout/TaskCalendarPortal.jsx';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -55,6 +56,7 @@ const isDev = import.meta.env.DEV;
|
|||||||
<Toast client:only="preact" />
|
<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} />}
|
||||||
|
{!isAuthPage && <TaskCalendarPortal client:only="preact" />}
|
||||||
|
|
||||||
<div id="main-content" class="relative z-10 flex flex-col min-h-screen max-w-6xl mx-auto px-6 md:px-8 py-6">
|
<div id="main-content" class="relative z-10 flex flex-col min-h-screen max-w-6xl mx-auto px-6 md:px-8 py-6">
|
||||||
<header class="flex flex-row justify-between items-end pb-6 mb-8 border-b border-border-hairline">
|
<header class="flex flex-row justify-between items-end pb-6 mb-8 border-b border-border-hairline">
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { atom, computed } from 'nanostores';
|
|||||||
import { api } from '../services/apiClient';
|
import { api } from '../services/apiClient';
|
||||||
import { $accessToken } from './authStore';
|
import { $accessToken } from './authStore';
|
||||||
import { showToast } from './toastStore';
|
import { showToast } from './toastStore';
|
||||||
|
import { $tasks } from './taskStore';
|
||||||
|
|
||||||
const DB_NAME = 'erp-offline-db';
|
const DB_NAME = 'erp-offline-db';
|
||||||
const DB_VERSION = 1;
|
const DB_VERSION = 1;
|
||||||
@@ -440,12 +441,15 @@ export const $workOrdersWithVehicle = computed(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const $dashboardStats = computed(
|
export const $dashboardStats = computed(
|
||||||
[$workOrders, $serviceRecords],
|
[$workOrders, $serviceRecords, $tasks],
|
||||||
(workOrders, serviceRecords) => {
|
(workOrders, serviceRecords, tasks) => {
|
||||||
const today = new Date().toISOString().slice(0, 10);
|
const today = new Date().toISOString().slice(0, 10);
|
||||||
const openWorkOrders = workOrders.filter((workOrder) => workOrder.status !== 'closed').length;
|
const openWorkOrders = workOrders.filter((workOrder) => workOrder.status !== 'closed').length;
|
||||||
const closedWorkOrders = 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) => {
|
const warnings = serviceRecords.filter((record) => {
|
||||||
if (record.next_service_due_at == null || record.mileage == null) return false;
|
if (record.next_service_due_at == null || record.mileage == null) return false;
|
||||||
return Number(record.next_service_due_at) - Number(record.mileage) <= 1000;
|
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') {
|
export async function downloadGeneratedPdfByUrl(downloadUrl, filename = 'document.pdf') {
|
||||||
if (!downloadUrl) {
|
if (!downloadUrl) {
|
||||||
|
showToast('Nedostaje URL za preuzimanje PDF-a.', 'error');
|
||||||
throw new Error('Nedostaje URL za preuzimanje PDF-a.');
|
throw new Error('Nedostaje URL za preuzimanje PDF-a.');
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
const blob = await api.get(downloadUrl, { responseType: 'blob' });
|
const blob = await api.get(downloadUrl, { responseType: 'blob' });
|
||||||
saveBlobToFile(blob, filename);
|
saveBlobToFile(blob, filename);
|
||||||
return true;
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
const msg = err?.message || 'Preuzimanje PDF-a nije uspjelo.';
|
||||||
|
showToast(msg, 'error');
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function openWorkOrderInvoicesPdfPage(workOrderId) {
|
export function openWorkOrderInvoicesPdfPage(workOrderId) {
|
||||||
|
|||||||
Reference in New Issue
Block a user