diff --git a/package-lock.json b/package-lock.json index e408a11..0357b29 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "eda-frontend", "version": "0.1.0", "dependencies": { + "lucide-react": "^0.552.0", "next": "15.5.3", "react": "19.1.0", "react-dom": "19.1.0", @@ -4330,6 +4331,15 @@ "dev": true, "license": "ISC" }, + "node_modules/lucide-react": { + "version": "0.552.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.552.0.tgz", + "integrity": "sha512-g9WCjmfwqbexSnZE+2cl21PCfXOcqnGeWeMTNAOGEfpPbm/ZF4YIq77Z8qWrxbu660EKuLB4nSLggoKnCb+isw==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", diff --git a/package.json b/package.json index a82582d..887e83c 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "lint": "eslint" }, "dependencies": { + "lucide-react": "^0.552.0", "next": "15.5.3", "react": "19.1.0", "react-dom": "19.1.0", diff --git a/src/app/customer/progress/page.tsx b/src/app/customer/progress/page.tsx new file mode 100644 index 0000000..4546e8c --- /dev/null +++ b/src/app/customer/progress/page.tsx @@ -0,0 +1,251 @@ +'use client'; + +import { useState, useEffect, useCallback } from 'react'; +import { ClipboardList, Wifi, WifiOff, Bell, TrendingUp } from 'lucide-react'; +import { AppointmentProgress } from '@/types/progress.types'; +import { + getCustomerAppointments, + API_BASE_URL, + type Appointment, +} from '@/lib/api'; +import customerWebSocketService from '@/lib/customer-websocket'; +import CustomerProgressCard from '@/components/progress/CustomerProgressCard'; +import Timeline from '@/components/progress/Timeline'; + +export default function CustomerProgressPage() { + const [appointments, setAppointments] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [selectedAppointment, setSelectedAppointment] = useState(null); + const [showTimeline, setShowTimeline] = useState(false); + const [wsConnected, setWsConnected] = useState(false); + const [notification, setNotification] = useState(null); + + // TODO: Replace with actual customer ID from auth context + const CUSTOMER_ID = '1'; + + const transformAppointment = (item: Appointment): AppointmentProgress => ({ + appointmentId: item.id, + serviceName: item.serviceName || 'Service', + serviceDescription: undefined, + location: item.location || 'Location TBD', + customerName: item.customerName || 'Unknown Customer', + customerPhone: undefined, + status: (item.status?.toLowerCase() || 'not started') as AppointmentProgress['status'], + progressPercentage: item.progressPercentage || 0, + currentStage: item.currentStage || 'Not Started', + estimatedHours: item.estimatedHours, + scheduledDate: item.scheduledDate, + latestRemarks: item.notes, + vehicleInfo: item.vehicleId ? `Vehicle #${item.vehicleId}` : undefined, + }); + + const fetchAppointments = useCallback(async () => { + try { + setError(null); + const data = await getCustomerAppointments(Number(CUSTOMER_ID)); + const transformedData = data.map(transformAppointment); + setAppointments(transformedData); + } catch (err) { + const errorMessage = err instanceof Error ? err.message : 'Failed to fetch appointments'; + console.error('Error fetching customer appointments:', err); + setError(errorMessage); + } finally { + setLoading(false); + } + }, [CUSTOMER_ID]); + + const showNotification = (message: string) => { + setNotification(message); + setTimeout(() => setNotification(null), 5000); + }; + + // WebSocket setup for real-time updates + useEffect(() => { + customerWebSocketService.connect(CUSTOMER_ID); + + const unsubscribeConnect = customerWebSocketService.onConnect(() => { + setWsConnected(true); + showNotification('Connected to real-time updates'); + }); + + const unsubscribeDisconnect = customerWebSocketService.onDisconnect(() => { + setWsConnected(false); + showNotification('Real-time updates disconnected'); + }); + + const unsubscribeMessage = customerWebSocketService.onMessage((notif) => { + // Update the appointment in the list with new progress data + setAppointments((prev) => + prev.map((a) => + a.appointmentId === notif.appointmentId + ? { + ...a, + progressPercentage: notif.data.percentage, + currentStage: notif.data.stage, + latestRemarks: notif.data.remarks, + } + : a + ) + ); + + // Show notification to user + showNotification(notif.message); + }); + + return () => { + unsubscribeConnect(); + unsubscribeDisconnect(); + unsubscribeMessage(); + customerWebSocketService.disconnect(); + }; + }, [CUSTOMER_ID]); + + // Fetch appointments on mount and periodically + useEffect(() => { + fetchAppointments(); + const interval = setInterval(fetchAppointments, 60000); // Refresh every minute + return () => clearInterval(interval); + }, [fetchAppointments]); + + return ( +
+ {/* Header */} +
+
+

+ + My Service Progress +

+

+ Track real-time progress on your service appointments +

+
+
+ {wsConnected ? ( +
+ + Live Updates +
+ ) : ( +
+ + Offline +
+ )} +
+
+ + {/* Toast Notification */} + {notification && ( +
+
+ +

{notification}

+
+
+ )} + + {/* Loading State */} + {loading && ( +
+
+
+

Loading your appointments...

+
+
+ )} + + {/* Error State */} + {error && !loading && ( +
+

{error}

+

+ Please ensure the backend API is running at{' '} + {API_BASE_URL} +

+ +
+ )} + + {/* Empty State */} + {!loading && !error && appointments.length === 0 && ( +
+ +

No Appointments

+

+ You don't have any service appointments at the moment. +

+

+ Book a service to see progress updates here. +

+
+ )} + + {/* Appointments Grid */} + {!loading && appointments.length > 0 && ( +
+ {appointments.map((appointment) => ( + { + setSelectedAppointment(apt); + setShowTimeline(true); + }} + /> + ))} +
+ )} + + {/* Stats Summary - Optional Enhancement */} + {!loading && appointments.length > 0 && ( +
+
+

Total Services

+

{appointments.length}

+
+
+

In Progress

+

+ {appointments.filter((a) => a.status.toLowerCase() === 'in progress').length} +

+
+
+

Completed

+

+ {appointments.filter((a) => a.status.toLowerCase() === 'completed').length} +

+
+
+

Average Progress

+

+ {appointments.length > 0 + ? Math.round( + appointments.reduce((sum, a) => sum + a.progressPercentage, 0) / + appointments.length + ) + : 0} + % +

+
+
+ )} + + {/* Timeline Modal */} + { + setShowTimeline(false); + setSelectedAppointment(null); + }} + appointment={selectedAppointment} + /> +
+ ); +} diff --git a/src/app/employee/progress/page.tsx b/src/app/employee/progress/page.tsx index eafe6b9..ca881ae 100644 --- a/src/app/employee/progress/page.tsx +++ b/src/app/employee/progress/page.tsx @@ -1,387 +1,46 @@ -'use client'; +'use client'; -import { useState, useEffect } from 'react'; -import { - Clock, - Play, - Pause, - CheckCircle, - ClipboardList -} from 'lucide-react'; -import { AppointmentProgress, ProgressUpdate, TimeLog } from '@/types/progress.types'; +import { useState, useEffect, useCallback } from 'react'; +import { ClipboardList, Wifi, WifiOff, Bell } from 'lucide-react'; +import { AppointmentProgress } from '@/types/progress.types'; import { getEmployeeAppointments, - startTimer, - pauseTimer, - logTime, updateProgress, - type Appointment + API_BASE_URL, + type Appointment, } from '@/lib/api'; +import websocketService from '@/lib/websocket'; +import ProgressCard from '@/components/progress/ProgressCard'; +import StatusModal from '@/components/progress/StatusModal'; +import Timeline from '@/components/progress/Timeline'; -// Modal Components -function UpdateStatusModal({ - isOpen, - onClose, - appointment, - onUpdate -}: { - isOpen: boolean; - onClose: () => void; - appointment: AppointmentProgress | null; - onUpdate: (data: ProgressUpdate) => Promise; -}) { - const [stage, setStage] = useState(''); - const [percentage, setPercentage] = useState(0); - const [remarks, setRemarks] = useState(''); - const [loading, setLoading] = useState(false); - - useEffect(() => { - if (appointment) { - setStage(appointment.status); - setPercentage(appointment.progressPercentage); - setRemarks(appointment.latestRemarks || ''); - } - }, [appointment]); - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - if (percentage < 0 || percentage > 100) { - alert('Percentage must be between 0 and 100'); - return; - } - setLoading(true); - try { - await onUpdate({ stage, percentage, remarks }); - onClose(); - } catch (error) { - console.error('Failed to update status:', error); - } finally { - setLoading(false); - } - }; - - if (!isOpen || !appointment) return null; - - return ( -
-
-

Update Progress Status

-

{appointment.serviceName}

- -
-
- - -
- -
- - setPercentage(Number(e.target.value))} - className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" - required - /> -
- -
- -