Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 12 additions & 12 deletions src/api/mockApiService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string> = {
'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<string, string> || {}) },
...options,
});

Expand Down Expand Up @@ -183,31 +191,23 @@ 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<Record<string, unknown>>;
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<Project[]> {
const dtos = await callApi(`/api/projects`);
if (!Array.isArray(dtos)) throw new Error('Invalid projects response from server');
const raw = dtos as Array<Record<string, unknown>>;
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<Project[]> {
const dtos = await callApi(`/api/projects`);
if (!Array.isArray(dtos)) throw new Error('Invalid projects response from server');
const raw = dtos as Array<Record<string, unknown>>;
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<Project | undefined> {
Expand Down
42 changes: 28 additions & 14 deletions src/app/customer/dashboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,49 @@

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<Customer | null>(null);
const { user } = useAuth();
const [stats, setStats] = useState<DashboardStats | null>(null);
const [upcomingAppointments, setUpcomingAppointments] = useState<Appointment[]>([]);
const [ongoingProjects, setOngoingProjects] = useState<Project[]>([]);
const [loading, setLoading] = useState(true);

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 {
Expand Down Expand Up @@ -78,7 +92,7 @@ export default function CustomerDashboard() {
<div>
{/* Header */}
<div className="mb-6">
<h1 className="text-2xl font-bold mb-2">Welcome back, {customer?.name}!</h1>
<h1 className="text-2xl font-bold mb-2">Welcome back, {user?.name || user?.email || 'Customer'}!</h1>
<p className="text-gray-600">Here's what's happening with your vehicles and services</p>
</div>

Expand Down
105 changes: 63 additions & 42 deletions src/lib/api/appointmentService.ts
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -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<Appointment[]> => {
getAllAppointments: async (): Promise<AppAppointment[]> => {
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<Appointment[]> => {
const { data } = await axiosInstance.get(`/appointments/customer/${customerId}`);
getCustomerAppointments: async (customerId: string): Promise<AppAppointment[]> => {
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<Appointment, 'id'>): Promise<Appointment> => {
createAppointment: async (appointmentData: Omit<AppAppointment, 'id'>): Promise<AppAppointment> => {
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<Appointment> => {
cancelAppointment: async (appointmentId: string): Promise<AppAppointment> => {
// 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']),
};
},

Expand All @@ -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<string[]> => {
// 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 [];
}
}
}
};
73 changes: 73 additions & 0 deletions src/lib/api/projectService.ts
Original file line number Diff line number Diff line change
@@ -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<string, AppProject['status']> = {
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<AppProject[]> => {
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<AppProject[]> => {
const { data } = await axiosInstance.get('/projects', { params: { customerId } });
return (data || []).map((d: ProjectDTO) => mapDtoToProject(d));
},

getProjectById: async (projectId: string): Promise<AppProject | undefined> => {
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<AppProject> => {
const { data } = await axiosInstance.post('/projects', payload);
return mapDtoToProject(data as ProjectDTO);
},

updateProject: async (projectId: string, payload: Partial<ProjectDTO>): Promise<AppProject> => {
const { data } = await axiosInstance.put(`/projects/${encodeURIComponent(projectId)}`, payload);
return mapDtoToProject(data as ProjectDTO);
},

deleteProject: async (projectId: string): Promise<void> => {
await axiosInstance.delete(`/projects/${encodeURIComponent(projectId)}`);
}
};