diff --git a/src/api/mockApiService.ts b/src/api/mockApiService.ts index 537943b..6274424 100644 --- a/src/api/mockApiService.ts +++ b/src/api/mockApiService.ts @@ -23,9 +23,17 @@ const API_BASE = process.env.NEXT_PUBLIC_API_BASE_URL || (process.env.NODE_ENV = async function callApi(path: string, options?: RequestInit) { const base = API_BASE || ''; + const token = typeof window !== 'undefined' ? localStorage.getItem('auth_token') : null; + + const defaultHeaders: Record = { + 'Content-Type': 'application/json', + }; + if (token) { + defaultHeaders['Authorization'] = `Bearer ${token}`; + } const res = await fetch(`${base}${path}`, { - headers: { 'Content-Type': 'application/json' }, + headers: { ...defaultHeaders, ...(options?.headers as Record || {}) }, ...options, }); @@ -183,9 +191,7 @@ export const projectService = { const dtos = await callApi(`/api/projects`); if (!Array.isArray(dtos)) throw new Error('Invalid projects response from server'); const raw = dtos as Array>; - const mapped = raw.map(mapDtoToProject); - const hasCustomer = raw.some(d => d['customerId'] !== undefined && d['customerId'] !== null && String(d['customerId']).length > 0); - return hasCustomer ? mapped.filter((p: Project) => p.customerId === customerId) : mapped; + return raw.map(mapDtoToProject); }, async getOngoingProjects(customerId: string): Promise { @@ -193,10 +199,7 @@ export const projectService = { if (!Array.isArray(dtos)) throw new Error('Invalid projects response from server'); const raw = dtos as Array>; const mapped = raw.map(mapDtoToProject); - const hasCustomer = raw.some(d => d['customerId'] !== undefined && d['customerId'] !== null && String(d['customerId']).length > 0); - return hasCustomer - ? mapped.filter((p: Project) => p.customerId === customerId && p.status === 'Ongoing') - : mapped.filter((p: Project) => p.status === 'Ongoing'); + return mapped.filter((p: Project) => p.status === 'Ongoing'); }, async getCompletedProjects(customerId: string): Promise { @@ -204,10 +207,7 @@ export const projectService = { if (!Array.isArray(dtos)) throw new Error('Invalid projects response from server'); const raw = dtos as Array>; const mapped = raw.map(mapDtoToProject); - const hasCustomer = raw.some(d => d['customerId'] !== undefined && d['customerId'] !== null && String(d['customerId']).length > 0); - return hasCustomer - ? mapped.filter((p: Project) => p.customerId === customerId && p.status === 'Completed') - : mapped.filter((p: Project) => p.status === 'Completed'); + return mapped.filter((p: Project) => p.status === 'Completed'); }, async getProjectById(projectId: string): Promise { diff --git a/src/app/customer/dashboard/page.tsx b/src/app/customer/dashboard/page.tsx index 8583f1e..2de583a 100644 --- a/src/app/customer/dashboard/page.tsx +++ b/src/app/customer/dashboard/page.tsx @@ -2,11 +2,13 @@ import { useEffect, useState } from 'react'; import { Car, Calendar, Briefcase, Clock, MapPin, DollarSign, User } from 'lucide-react'; -import { dashboardService, customerService, appointmentService, projectService } from '@/api/mockApiService'; -import type { Customer, Appointment, Project, DashboardStats } from '@/types'; +import { useAuth } from '@/app/context/AuthContext'; +import { appointmentService } from '@/lib/api/appointmentService'; +import { projectService } from '@/lib/api/projectService'; +import type { Appointment, Project, DashboardStats } from '@/types'; export default function CustomerDashboard() { - const [customer, setCustomer] = useState(null); + const { user } = useAuth(); const [stats, setStats] = useState(null); const [upcomingAppointments, setUpcomingAppointments] = useState([]); const [ongoingProjects, setOngoingProjects] = useState([]); @@ -14,23 +16,35 @@ export default function CustomerDashboard() { useEffect(() => { loadDashboardData(); - }, []); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [user?.id]); const loadDashboardData = async () => { try { setLoading(true); - const customerData = await customerService.getProfile(); - setCustomer(customerData); + const customerId = user?.id || ''; - const [statsData, appointmentsData, projectsData] = await Promise.all([ - dashboardService.getDashboardStats(customerData.id), - appointmentService.getUpcomingAppointments(customerData.id), - projectService.getOngoingProjects(customerData.id) + // Fetch all relevant data in parallel + const [allAppointments, allProjects] = await Promise.all([ + customerId ? appointmentService.getCustomerAppointments(customerId) : appointmentService.getAllAppointments(), + customerId ? projectService.getCustomerProjects(customerId) : projectService.getAllProjects() ]); - setStats(statsData); - setUpcomingAppointments(appointmentsData); - setOngoingProjects(projectsData); + // Derive dashboard stats and sections + const upcoming = allAppointments.filter(a => String(a.status).toLowerCase() === 'upcoming'); + const ongoing = allProjects.filter(p => String(p.status).toLowerCase() === 'ongoing'); + + setUpcomingAppointments(upcoming); + setOngoingProjects(ongoing); + + const computedStats: DashboardStats = { + totalVehicles: 0, + upcomingAppointments: upcoming.length, + ongoingProjects: ongoing.length, + completedAppointments: allAppointments.filter(a => String(a.status).toLowerCase() === 'completed').length, + completedProjects: allProjects.filter(p => String(p.status).toLowerCase() === 'completed').length, + }; + setStats(computedStats); } catch (error) { console.error('Failed to load dashboard data:', error); } finally { @@ -78,7 +92,7 @@ export default function CustomerDashboard() {
{/* Header */}
-

Welcome back, {customer?.name}!

+

Welcome back, {user?.name || user?.email || 'Customer'}!

Here's what's happening with your vehicles and services

diff --git a/src/lib/api/appointmentService.ts b/src/lib/api/appointmentService.ts index c7b163f..22d372d 100644 --- a/src/lib/api/appointmentService.ts +++ b/src/lib/api/appointmentService.ts @@ -1,5 +1,6 @@ // API configuration import { axiosInstance } from '@/lib/apiClient'; +import type { Appointment as AppAppointment } from '@/types'; const handleResponse = async (response: any) => { if (!response.ok) { @@ -9,76 +10,72 @@ const handleResponse = async (response: any) => { return response.data; }; -export interface Appointment { - id?: string; - customerId?: string; - vehicleNumber: string; - serviceName: string; - date: string; - time: string; - status: string; -} +// Use shared Appointment type from '@/types' for return values export const appointmentService = { // Get all appointments - getAllAppointments: async (): Promise => { + getAllAppointments: async (): Promise => { const { data } = await axiosInstance.get('/appointments'); // map backend DTO to frontend Appointment shape // eslint-disable-next-line @typescript-eslint/no-explicit-any return (data || []).map((d: any) => ({ - id: d.appointmentId || d.id, - customerId: d.customerId, - vehicleNumber: d.vehicleNo || d.vehicleNumber, - serviceName: d.service || d.serviceName, - date: d.date, - time: d.startTime || d.time, - status: d.status + id: String(d.appointmentId || d.id || ''), + customerId: String(d.customerId || ''), + vehicleId: String(d.vehicleId || ''), + vehicleNumber: String(d.vehicleNo || d.vehicleNumber || ''), + serviceName: String(d.service || d.serviceName || ''), + date: String(d.date || ''), + time: String(d.startTime || d.time || ''), + status: (String(d.status || 'Upcoming') as AppAppointment['status']), })); }, // Get appointments for a specific customer - getCustomerAppointments: async (customerId: string): Promise => { - const { data } = await axiosInstance.get(`/appointments/customer/${customerId}`); + getCustomerAppointments: async (customerId: string): Promise => { + const { data } = await axiosInstance.get('/appointments', { params: { customerId } }); // eslint-disable-next-line @typescript-eslint/no-explicit-any return (data || []).map((d: any) => ({ - id: d.appointmentId || d.id, - customerId: d.customerId, - vehicleNumber: d.vehicleNo || d.vehicleNumber, - serviceName: d.service || d.serviceName, - date: d.date, - time: d.startTime || d.time, - status: d.status + id: String(d.appointmentId || d.id || ''), + customerId: String(d.customerId || ''), + vehicleId: String(d.vehicleId || ''), + vehicleNumber: String(d.vehicleNo || d.vehicleNumber || ''), + serviceName: String(d.service || d.serviceName || ''), + date: String(d.date || ''), + time: String(d.startTime || d.time || ''), + status: (String(d.status || 'Upcoming') as AppAppointment['status']) })); }, // Create a new appointment - createAppointment: async (appointmentData: Omit): Promise => { + createAppointment: async (appointmentData: Omit): Promise => { const { data: d } = await axiosInstance.post('/appointments', appointmentData); return { - id: d.appointmentId || d.id, - customerId: d.customerId, - vehicleNumber: d.vehicleNo || d.vehicleNumber, - serviceName: d.service || d.serviceName, - date: d.date, - time: d.startTime || d.time, - status: d.status + id: String(d.appointmentId || d.id || ''), + customerId: String(d.customerId || ''), + vehicleId: String(d.vehicleId || ''), + vehicleNumber: String(d.vehicleNo || d.vehicleNumber || ''), + serviceName: String(d.service || d.serviceName || ''), + date: String(d.date || ''), + time: String(d.startTime || d.time || ''), + status: (String(d.status || 'Upcoming') as AppAppointment['status']) }; }, // Cancel an appointment - cancelAppointment: async (appointmentId: string): Promise => { + cancelAppointment: async (appointmentId: string): Promise => { // Use the generic update endpoint to change status to CANCELLED const payload = { status: 'CANCELLED' }; const { data: d } = await axiosInstance.put(`/appointments/${appointmentId}`, payload); // map to frontend shape return { - id: d.appointmentId || d.id, - customerId: d.customerId, - vehicleNumber: d.vehicleNo || d.vehicleNumber, - serviceName: d.service || d.serviceName, - date: d.date, - time: d.startTime || d.time, - status: d.status, + id: String(d.appointmentId || d.id || ''), + customerId: String(d.customerId || ''), + vehicleId: String(d.vehicleId || ''), + vehicleNumber: String(d.vehicleNo || d.vehicleNumber || ''), + serviceName: String(d.service || d.serviceName || ''), + date: String(d.date || ''), + time: String(d.startTime || d.time || ''), + status: (String(d.status || 'Cancelled') as AppAppointment['status']), }; }, @@ -88,5 +85,29 @@ export const appointmentService = { throw new Error('Invalid appointment id'); } await axiosInstance.delete(`/appointments/${encodeURIComponent(appointmentId)}`); + }, + + // Get globally booked start times for a given date (across all users). + // Tries a dedicated availability endpoint first; falls back to retrieving all appointments. + getBookedStartTimesForDate: async (date: string): Promise => { + // Try availability endpoint + try { + const { data } = await axiosInstance.get(`/appointments/availability`, { params: { date } }); + // Expecting something like: { date: 'YYYY-MM-DD', booked: ['09:00', '09:30', ...] } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const booked = (data?.booked || []) as any[]; + return booked.map((t) => String(t)); + } catch { + // Fallback to pulling all appointments if availability endpoint not present + try { + const all = await appointmentService.getAllAppointments(); + return all + .filter(a => a.date === date && String(a.status).toUpperCase() !== 'CANCELLED') + .map(a => (a.time?.length ? a.time : '')) + .filter(Boolean) as string[]; + } catch { + return []; + } + } } }; \ No newline at end of file diff --git a/src/lib/api/projectService.ts b/src/lib/api/projectService.ts new file mode 100644 index 0000000..8f286dc --- /dev/null +++ b/src/lib/api/projectService.ts @@ -0,0 +1,73 @@ +import { axiosInstance } from '@/lib/apiClient'; +import type { Project as AppProject } from '@/types'; + +export interface ProjectDTO { + projectId?: string; + id?: string; + customerId?: string; + name?: string; + description?: string; + startDate?: string; + endDate?: string; + status?: string; // PLANNED | IN_PROGRESS | COMPLETED | CANCELLED | ON_HOLD +} + +const mapDtoToProject = (d: ProjectDTO): AppProject => { + const statusEnum = String(d.status || '').toUpperCase(); + const statusMap: Record = { + PLANNED: 'Ongoing', + IN_PROGRESS: 'Ongoing', + COMPLETED: 'Completed', + CANCELLED: 'Cancelled', + ON_HOLD: 'Ongoing', + }; + const status = statusMap[statusEnum] || 'Ongoing'; + + return { + id: String(d.projectId || d.id || ''), + customerId: String(d.customerId || ''), + vehicleId: '', + vehicleNumber: '', + vehicleType: '', + taskName: String(d.name || ''), + description: String(d.description || ''), + startDate: String(d.startDate || ''), + estimatedEndDate: d.endDate ? String(d.endDate) : undefined, + completedDate: undefined, + time: '', + status, + }; +}; + +export const projectService = { + // Get projects for current authenticated customer (principal inferred by backend) + getAllProjects: async (): Promise => { + const { data } = await axiosInstance.get('/projects'); + return (data || []).map((d: ProjectDTO) => mapDtoToProject(d)); + }, + + // Get projects for a specific customer id (if needed explicitly) + getCustomerProjects: async (customerId: string): Promise => { + const { data } = await axiosInstance.get('/projects', { params: { customerId } }); + return (data || []).map((d: ProjectDTO) => mapDtoToProject(d)); + }, + + getProjectById: async (projectId: string): Promise => { + const { data } = await axiosInstance.get(`/projects/${encodeURIComponent(projectId)}`); + return data ? mapDtoToProject(data as ProjectDTO) : undefined; + }, + + createProject: async (payload: { name: string; description: string; startDate: string; status?: string; }): Promise => { + const { data } = await axiosInstance.post('/projects', payload); + return mapDtoToProject(data as ProjectDTO); + }, + + updateProject: async (projectId: string, payload: Partial): Promise => { + const { data } = await axiosInstance.put(`/projects/${encodeURIComponent(projectId)}`, payload); + return mapDtoToProject(data as ProjectDTO); + }, + + deleteProject: async (projectId: string): Promise => { + await axiosInstance.delete(`/projects/${encodeURIComponent(projectId)}`); + } +};