From dcae9a69065faab64354491f8142e14e287e00c1 Mon Sep 17 00:00:00 2001 From: RandithaK Date: Sat, 15 Nov 2025 13:52:34 +0530 Subject: [PATCH 01/34] chore: trigger GitOps pipeline (empty commit) From b410798ad5d5bcfc0c15cc341226af56eebbe6ee Mon Sep 17 00:00:00 2001 From: RandithaK Date: Sat, 15 Nov 2025 20:26:18 +0530 Subject: [PATCH 02/34] feat: enhance PayHere payment object with additional fields and logging --- src/services/payhereService.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/services/payhereService.ts b/src/services/payhereService.ts index e40d9fc..27df5f6 100644 --- a/src/services/payhereService.ts +++ b/src/services/payhereService.ts @@ -125,9 +125,14 @@ class PayHereService { sandbox: payment.sandbox, orderId: payment.order_id, amount: payment.amount, - merchantId: payment.merchant_id + merchantId: payment.merchant_id, + currency: payment.currency, + hash: payment.hash, + customerEmail: payment.email }); + console.log('Full PayHere payment object:', payment); + // Setup callbacks window.payhere.onCompleted = function (paymentId: string) { console.log('Payment completed:', paymentId); From d04c054b99257430514c7030cf169dd0e8ccc1ed Mon Sep 17 00:00:00 2001 From: RandithaK Date: Sat, 15 Nov 2025 23:26:50 +0530 Subject: [PATCH 03/34] feat: update AIChatWidget to use shared API client for chat requests and enhance error handling --- README.md | 8 +++++++ src/app/components/chatbot/AIChatWidget.tsx | 24 +++++++-------------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 1a202df..778fb9c 100644 --- a/README.md +++ b/README.md @@ -48,3 +48,11 @@ This repository contains the source code for the TechTorque 2025 customer and em npm run setup-hooks ``` This configures automatic linting on commit and build checking on push. See [GIT_HOOKS.md](GIT_HOOKS.md) for details. + + ### 🔌 Environment + + - The frontend expects an API base to be available at runtime. You can configure this via the + `NEXT_PUBLIC_API_BASE_URL` environment variable. When not set, the runtime defaults to + `http://localhost:8080` (useful in development). + + - The AI chat widget reaches the AI chat proxy at `/api/v1/ai/chat` (or `{{NEXT_PUBLIC_API_BASE_URL}}/api/v1/ai/chat` when the public API base is set). This ensures the frontend talks to the configured API gateway or the local Next.js proxy. diff --git a/src/app/components/chatbot/AIChatWidget.tsx b/src/app/components/chatbot/AIChatWidget.tsx index 9719b94..20d7081 100644 --- a/src/app/components/chatbot/AIChatWidget.tsx +++ b/src/app/components/chatbot/AIChatWidget.tsx @@ -1,6 +1,7 @@ import React, { useState, useRef, useEffect, useCallback } from 'react'; import { Sparkles, Bolt } from 'lucide-react'; import Cookies from 'js-cookie'; +import apiClient from '@/lib/apiClient'; // --- Theme Simulation & Constants --- const theme = { @@ -26,7 +27,8 @@ interface ChatResponse { tool_executed?: string | null; } -const API_ENDPOINT = 'http://localhost:8091/api/v1/ai/chat'; +// Use the shared API client with a baseURL of `${API_BASE_URL}/api/v1`. +// Then call the `ai/chat` endpoint relative to that base. const AIChatWidget: React.FC = () => { // State Management @@ -70,21 +72,8 @@ const AIChatWidget: React.FC = () => { token: currentToken, }; - const response = await fetch(API_ENDPOINT, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${currentToken}`, - }, - body: JSON.stringify(payload), - }); - - if (!response.ok) { - const errorData = await response.json(); - throw new Error(errorData.detail || `HTTP Error ${response.status}`); - } - - const data: ChatResponse = await response.json(); + // Let the shared `apiClient` add auth headers via request interceptor + const { data } = await apiClient.post('/ai/chat', payload); let replyText = data.reply; if (data.tool_executed) { @@ -98,6 +87,9 @@ const AIChatWidget: React.FC = () => { } catch (error: unknown) { console.error("Chat Error:", error); const errorMessage: Message = { + // If axios/our apiClient already stripped the token on 401 it + // will redirect to /auth/login via the interceptor. Otherwise + // check HTTP status if available. text: (error instanceof Error && error.message.includes('401')) ? "🔒 Your session has expired. Please log in again to continue chatting!" : "⚠️ Oops! I'm having trouble connecting to my services right now. Please try again in a moment! 🔄", From 53417f8b7544a1175fe7b9918af2ac17001948a8 Mon Sep 17 00:00:00 2001 From: ChamodiSandunika Date: Thu, 20 Nov 2025 16:00:53 +0530 Subject: [PATCH 04/34] feat: implement toast notifications across various pages and components --- src/app/dashboard/admin/reports/page.tsx | 8 +- .../dashboard/admin/service-types/page.tsx | 16 ++-- src/app/dashboard/admin/users/page.tsx | 20 +++-- .../appointments/[appointmentId]/page.tsx | 6 +- .../dashboard/projects/[projectId]/page.tsx | 19 +++-- src/app/dashboard/time-logs/page.tsx | 8 +- .../dashboard/vehicles/[vehicleId]/page.tsx | 8 +- src/app/dashboard/vehicles/page.tsx | 6 +- src/app/globals.css | 16 ++++ src/components/TimeTracker.tsx | 6 +- src/components/Toast.tsx | 77 +++++++++++++++++++ src/hooks/useToast.ts | 44 +++++++++++ 12 files changed, 204 insertions(+), 30 deletions(-) create mode 100644 src/components/Toast.tsx create mode 100644 src/hooks/useToast.ts diff --git a/src/app/dashboard/admin/reports/page.tsx b/src/app/dashboard/admin/reports/page.tsx index 4aab25f..774330a 100644 --- a/src/app/dashboard/admin/reports/page.tsx +++ b/src/app/dashboard/admin/reports/page.tsx @@ -4,9 +4,12 @@ import { useState } from 'react'; import { adminService } from '@/services/adminService'; import { ReportRequest } from '@/types/admin'; import { useDashboard } from '@/app/contexts/DashboardContext'; +import { useToast } from '@/hooks/useToast'; +import { ToastContainer } from '@/components/Toast'; export default function ReportsPage() { const { roles, loading: rolesLoading } = useDashboard(); + const { toasts, success, error: showError, closeToast } = useToast(); const [generating, setGenerating] = useState(false); const [reportType, setReportType] = useState<'SERVICE_PERFORMANCE' | 'REVENUE' | 'EMPLOYEE_PRODUCTIVITY' | 'CUSTOMER_SATISFACTION' | 'INVENTORY' | 'APPOINTMENT_SUMMARY'>('REVENUE'); const [format, setFormat] = useState<'JSON' | 'PDF' | 'EXCEL' | 'CSV'>('PDF'); @@ -26,10 +29,10 @@ export default function ReportsPage() { const report = await adminService.generateReport(request); // In a real implementation, you would display or download the report console.log('Report generated:', report); - alert('Report generated successfully!'); + success('Report generated successfully!'); } catch (err) { console.error('Failed to generate report:', err); - alert('Failed to generate report'); + showError('Failed to generate report'); } finally { setGenerating(false); } @@ -143,6 +146,7 @@ export default function ReportsPage() { + ); } diff --git a/src/app/dashboard/admin/service-types/page.tsx b/src/app/dashboard/admin/service-types/page.tsx index ca8ab4c..f3e02cd 100644 --- a/src/app/dashboard/admin/service-types/page.tsx +++ b/src/app/dashboard/admin/service-types/page.tsx @@ -4,9 +4,12 @@ import { useState, useEffect } from 'react'; import { adminService } from '@/services/adminService'; import { ServiceTypeResponse, CreateServiceTypeRequest, UpdateServiceTypeRequest } from '@/types/admin'; import { useDashboard } from '@/app/contexts/DashboardContext'; +import { useToast } from '@/hooks/useToast'; +import { ToastContainer } from '@/components/Toast'; export default function ServiceTypesPage() { const { roles, loading: rolesLoading } = useDashboard(); + const { toasts, success, error: showError, closeToast } = useToast(); const [serviceTypes, setServiceTypes] = useState([]); const [loading, setLoading] = useState(true); const [showModal, setShowModal] = useState(false); @@ -80,7 +83,7 @@ export default function ServiceTypesPage() { active: editingService.active, // Keep existing active status }; await adminService.updateServiceType(editingService.id, updateData); - alert('Service type updated successfully!'); + success('Service type updated successfully!'); } else { // Create new service - use CreateServiceTypeRequest format console.log('Creating service:', formData); @@ -92,7 +95,7 @@ export default function ServiceTypesPage() { durationMinutes: formData.durationMinutes, }; await adminService.createServiceType(createData); - alert('Service type created successfully!'); + success('Service type created successfully!'); } // Close modal first @@ -106,7 +109,7 @@ export default function ServiceTypesPage() { } catch (err) { console.error('Failed to save service type:', err); const errorMessage = err instanceof Error ? err.message : 'Failed to save service type. Please try again.'; - alert(errorMessage); + showError(errorMessage); } }; @@ -117,12 +120,12 @@ export default function ServiceTypesPage() { try { await adminService.removeServiceType(service.id); - alert('Service type deleted successfully!'); + success('Service type deleted successfully!'); await loadServiceTypes(); } catch (err) { console.error('Failed to delete service type:', err); const errorMessage = err instanceof Error ? err.message : 'Failed to delete service type. Please try again.'; - alert(errorMessage); + showError(errorMessage); } }; @@ -144,7 +147,7 @@ export default function ServiceTypesPage() { await loadServiceTypes(); } catch (err) { console.error('Failed to toggle service status:', err); - alert('Failed to update service status. Please try again.'); + showError('Failed to update service status. Please try again.'); } }; @@ -395,6 +398,7 @@ export default function ServiceTypesPage() { )} + ); } diff --git a/src/app/dashboard/admin/users/page.tsx b/src/app/dashboard/admin/users/page.tsx index c303696..2ef1da9 100644 --- a/src/app/dashboard/admin/users/page.tsx +++ b/src/app/dashboard/admin/users/page.tsx @@ -6,9 +6,12 @@ import { authService } from '@/services/authService'; import { UserResponse } from '@/types/admin'; import { CreateEmployeeRequest, CreateAdminRequest } from '@/types/api'; import { useDashboard } from '@/app/contexts/DashboardContext'; +import { useToast } from '@/hooks/useToast'; +import { ToastContainer } from '@/components/Toast'; export default function AdminUsersPage() { const { roles: currentUserRoles } = useDashboard(); + const { toasts, success, error: showError, closeToast } = useToast(); const [users, setUsers] = useState([]); const [loading, setLoading] = useState(true); const [roleFilter, setRoleFilter] = useState('ALL'); @@ -76,7 +79,7 @@ export default function AdminUsersPage() { setShowCreateModal(false); await loadUsers(); - alert(`${createUserType === 'employee' ? 'Employee' : 'Admin'} created successfully!`); + success(`${createUserType === 'employee' ? 'Employee' : 'Admin'} created successfully!`); } catch (err) { console.error('Failed to create user:', err); // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -84,7 +87,7 @@ export default function AdminUsersPage() { const errorMessage = error?.response?.status === 403 ? 'Permission denied. You do not have the required permissions to create this user type.' : error?.response?.data?.message || 'Failed to create user. Please try again.'; - alert(errorMessage); + showError(errorMessage); } }; @@ -102,7 +105,7 @@ export default function AdminUsersPage() { try { // Validation: Ensure at least one role is selected if (editingRoles.length === 0) { - alert('Please select at least one role for the user.'); + showError('Please select at least one role for the user.'); return; } @@ -110,11 +113,11 @@ export default function AdminUsersPage() { setEditingUserId(null); setEditingRoles([]); await loadUsers(); - alert('Roles updated successfully!'); + success('Roles updated successfully!'); } catch (err) { console.error('Failed to update roles:', err); const errorMessage = err instanceof Error ? err.message : 'Failed to update roles. Please try again.'; - alert(errorMessage); + showError(errorMessage); } }; @@ -122,7 +125,7 @@ export default function AdminUsersPage() { if (editingRoles.includes(role)) { // Prevent removing the last role if (editingRoles.length === 1) { - alert('A user must have at least one role.'); + showError('A user must have at least one role.'); return; } setEditingRoles(editingRoles.filter(r => r !== role)); @@ -144,11 +147,11 @@ export default function AdminUsersPage() { try { await adminService.updateUser(user.userId, { enabled: !user.enabled }); await loadUsers(); - alert(`User ${action}d successfully!`); + success(`User ${action}d successfully!`); } catch (err) { console.error(`Failed to ${action} user:`, err); const errorMessage = err instanceof Error ? err.message : `Failed to ${action} user. Please try again.`; - alert(errorMessage); + showError(errorMessage); } }; @@ -543,6 +546,7 @@ export default function AdminUsersPage() { )} + ); } diff --git a/src/app/dashboard/appointments/[appointmentId]/page.tsx b/src/app/dashboard/appointments/[appointmentId]/page.tsx index af74cac..dd7d8f3 100644 --- a/src/app/dashboard/appointments/[appointmentId]/page.tsx +++ b/src/app/dashboard/appointments/[appointmentId]/page.tsx @@ -9,6 +9,8 @@ import type { UserResponse } from '@/types/admin' import type { CreateInvoiceDto, InvoiceItemDto } from '@/types/payment' import { useDashboard } from '@/app/contexts/DashboardContext' import TimeTracker from '@/components/TimeTracker' +import { useToast } from '@/hooks/useToast' +import { ToastContainer } from '@/components/Toast' interface StatusOption { value: AppointmentStatus @@ -28,6 +30,7 @@ const STATUS_OPTIONS: StatusOption[] = [ export default function AppointmentDetailPage() { const router = useRouter() const params = useParams<{ appointmentId: string }>() + const { toasts, success, error: showError, closeToast } = useToast() const appointmentId = params.appointmentId const { roles, profile } = useDashboard() @@ -283,7 +286,7 @@ export default function AppointmentDetailPage() { const createdInvoice = await paymentService.createInvoice(invoiceData) - alert('Invoice generated successfully!') + success('Invoice generated successfully!') setShowInvoiceForm(false) setError(null) @@ -823,6 +826,7 @@ export default function AppointmentDetailPage() { {loading && (
Refreshing appointment data...
)} + ) } diff --git a/src/app/dashboard/projects/[projectId]/page.tsx b/src/app/dashboard/projects/[projectId]/page.tsx index 22f6e27..45bf9c0 100644 --- a/src/app/dashboard/projects/[projectId]/page.tsx +++ b/src/app/dashboard/projects/[projectId]/page.tsx @@ -5,12 +5,15 @@ import { useParams, useRouter } from 'next/navigation'; import { projectService } from '@/services/projectService'; import { ProjectResponseDto, ProjectStatus } from '@/types/project'; import { useDashboard } from '@/app/contexts/DashboardContext'; +import { useToast } from '@/hooks/useToast'; +import { ToastContainer } from '@/components/Toast'; export default function ProjectDetailPage() { const params = useParams(); const router = useRouter(); const { profile, roles } = useDashboard(); const projectId = params.projectId as string; + const { toasts, success, error: showError, closeToast } = useToast(); const [project, setProject] = useState(null); const [loading, setLoading] = useState(true); @@ -42,10 +45,11 @@ export default function ProjectDetailPage() { setActionLoading(true); await projectService.approveQuote(projectId); await loadProject(); + success('Quote approved successfully!'); } catch (err) { console.error('Failed to approve quote:', err); - alert(err instanceof Error ? err.message : 'Failed to approve quote'); - } finally { + showError(err instanceof Error ? err.message : 'Failed to approve quote'); + } finally{ setActionLoading(false); } }; @@ -59,7 +63,7 @@ export default function ProjectDetailPage() { await loadProject(); } catch (err) { console.error('Failed to reject quote:', err); - alert(err instanceof Error ? err.message : 'Failed to reject quote'); + showError(err instanceof Error ? err.message : 'Failed to reject quote'); } finally { setActionLoading(false); } @@ -72,10 +76,10 @@ export default function ProjectDetailPage() { setActionLoading(true); await projectService.adminApproveProject(projectId); await loadProject(); - alert('Project approved successfully! Customer has been notified.'); + success('Project approved successfully! Customer has been notified.'); } catch (err) { console.error('Failed to approve project:', err); - alert(err instanceof Error ? err.message : 'Failed to approve project'); + showError(err instanceof Error ? err.message : 'Failed to approve project'); } finally { setActionLoading(false); } @@ -93,10 +97,10 @@ export default function ProjectDetailPage() { setActionLoading(true); await projectService.adminRejectProject(projectId, reason || undefined); await loadProject(); - alert('Project rejected. Customer has been notified.'); + success('Project rejected. Customer has been notified.'); } catch (err) { console.error('Failed to reject project:', err); - alert(err instanceof Error ? err.message : 'Failed to reject project'); + showError(err instanceof Error ? err.message : 'Failed to reject project'); } finally { setActionLoading(false); } @@ -375,6 +379,7 @@ export default function ProjectDetailPage() { + ); } diff --git a/src/app/dashboard/time-logs/page.tsx b/src/app/dashboard/time-logs/page.tsx index 8f29ec2..e02ec6a 100644 --- a/src/app/dashboard/time-logs/page.tsx +++ b/src/app/dashboard/time-logs/page.tsx @@ -4,9 +4,12 @@ import { useState, useEffect } from 'react'; import { timeLoggingService } from '@/services/timeLoggingService'; import { TimeLogResponse, TimeLogRequest } from '@/types/timeLogging'; import { useDashboard } from '@/app/contexts/DashboardContext'; +import { useToast } from '@/hooks/useToast'; +import { ToastContainer } from '@/components/Toast'; export default function TimeLogsPage() { const { roles, loading: rolesLoading, profile } = useDashboard(); + const { toasts, success, error: showError, closeToast } = useToast(); const [allTimeLogs, setAllTimeLogs] = useState([]); const [loading, setLoading] = useState(true); const [showForm, setShowForm] = useState(false); @@ -68,7 +71,7 @@ export default function TimeLogsPage() { await loadTimeLogs(); } catch (err) { console.error('Failed to log time:', err); - alert(err instanceof Error ? err.message : 'Failed to log time'); + showError(err instanceof Error ? err.message : 'Failed to log time'); } finally { setSubmitting(false); } @@ -82,7 +85,7 @@ export default function TimeLogsPage() { await loadTimeLogs(); } catch (err) { console.error('Failed to delete time log:', err); - alert(err instanceof Error ? err.message : 'Failed to delete time log'); + showError(err instanceof Error ? err.message : 'Failed to delete time log'); } }; @@ -386,6 +389,7 @@ export default function TimeLogsPage() { )} + ); } diff --git a/src/app/dashboard/vehicles/[vehicleId]/page.tsx b/src/app/dashboard/vehicles/[vehicleId]/page.tsx index 854f162..f5d1a79 100644 --- a/src/app/dashboard/vehicles/[vehicleId]/page.tsx +++ b/src/app/dashboard/vehicles/[vehicleId]/page.tsx @@ -4,11 +4,14 @@ import Image from 'next/image' import { useRouter, useParams } from 'next/navigation'; import { vehicleService } from '@/services/vehicleService'; import type { Vehicle, ServiceHistory } from '@/types/vehicle'; +import { useToast } from '@/hooks/useToast'; +import { ToastContainer } from '@/components/Toast'; export default function VehicleDetailsPage() { const router = useRouter(); const params = useParams(); const vehicleId = params.vehicleId as string; + const { toasts, success, error: showError, closeToast } = useToast(); const [vehicle, setVehicle] = useState(null); const [serviceHistory, setServiceHistory] = useState([]); @@ -52,10 +55,10 @@ export default function VehicleDetailsPage() { try { const fileArray = Array.from(files); await vehicleService.uploadVehiclePhotos(vehicleId, fileArray); - alert('Photos uploaded successfully!'); + success('Photos uploaded successfully!'); } catch (err: unknown) { const errorMessage = (err as { response?: { data?: { message?: string } } })?.response?.data?.message || 'Failed to upload photos'; - alert(errorMessage); + showError(errorMessage); } finally { setUploadingPhotos(false); } @@ -87,6 +90,7 @@ export default function VehicleDetailsPage() { return (
+
+
+ ); +}; + +interface ToastContainerProps { + toasts: ToastMessage[]; + onClose: (id: string) => void; +} + +export const ToastContainer = ({ toasts, onClose }: ToastContainerProps) => { + return ( +
+
+ {toasts.map((toast) => ( + + ))} +
+
+ ); +}; + +export default Toast; diff --git a/src/hooks/useToast.ts b/src/hooks/useToast.ts new file mode 100644 index 0000000..4e7b12f --- /dev/null +++ b/src/hooks/useToast.ts @@ -0,0 +1,44 @@ +'use client'; + +import { useState, useCallback } from 'react'; +import type { ToastMessage, ToastType } from '../components/Toast'; + +export const useToast = () => { + const [toasts, setToasts] = useState([]); + + const showToast = useCallback((message: string, type: ToastType = 'info', duration?: number) => { + const id = Math.random().toString(36).substring(7); + const newToast: ToastMessage = { id, message, type, duration }; + setToasts((prev) => [...prev, newToast]); + }, []); + + const success = useCallback((message: string, duration?: number) => { + showToast(message, 'success', duration); + }, [showToast]); + + const error = useCallback((message: string, duration?: number) => { + showToast(message, 'error', duration); + }, [showToast]); + + const info = useCallback((message: string, duration?: number) => { + showToast(message, 'info', duration); + }, [showToast]); + + const warning = useCallback((message: string, duration?: number) => { + showToast(message, 'warning', duration); + }, [showToast]); + + const closeToast = useCallback((id: string) => { + setToasts((prev) => prev.filter((toast) => toast.id !== id)); + }, []); + + return { + toasts, + showToast, + success, + error, + info, + warning, + closeToast, + }; +}; From a4be154eeda9e730fff04454ad76039688081c20 Mon Sep 17 00:00:00 2001 From: Pramudi Samarawickrama Date: Thu, 20 Nov 2025 17:20:47 +0530 Subject: [PATCH 05/34] feat: enhance light and dark theme color variables for improved UI consistency --- src/app/globals.css | 564 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 549 insertions(+), 15 deletions(-) diff --git a/src/app/globals.css b/src/app/globals.css index 37d2d45..3ceb91b 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -2,28 +2,68 @@ /* CSS Variables for theme colors - Automotive Theme */ :root { - /* Light theme colors - Automotive inspired */ + /* Light theme colors - Automotive inspired with improved consistency */ --bg-primary: #ffffff; --bg-secondary: #f8fafc; --bg-tertiary: #f1f5f9; + --bg-hover: #e2e8f0; --bg-accent: linear-gradient(135deg, #1e293b 0%, #334155 100%); --bg-secondary-translucent: rgba(248, 250, 252, 0.85); + + /* Text colors with clear hierarchy */ --text-primary: #0f172a; - --text-secondary: #334155; + --text-secondary: #475569; --text-muted: #64748b; + + /* Border colors */ --border-color: #e2e8f0; + --border-hover: #cbd5e1; + + /* Primary accent colors */ --accent-primary: #0ea5e9; --accent-hover: #0284c7; --accent-secondary: #f59e0b; --accent-secondary-hover: #d97706; + + /* Status colors - Light mode optimized */ --success: #10b981; --success-hover: #059669; + --success-bg: #d1fae5; + --success-border: #6ee7b7; + --success-text: #065f46; + --warning: #f59e0b; --warning-hover: #d97706; + --warning-bg: #fef3c7; + --warning-border: #fcd34d; + --warning-text: #92400e; + --danger: #ef4444; --danger-hover: #dc2626; - --shadow-color: rgba(15, 23, 42, 0.05); - --shadow-accent: rgba(14, 165, 233, 0.2); + --danger-bg: #fee2e2; + --danger-border: #fca5a5; + --danger-text: #991b1b; + + --info: #3b82f6; + --info-hover: #2563eb; + --info-bg: #dbeafe; + --info-border: #93c5fd; + --info-text: #1e3a8a; + + /* Neutral shades for consistent UI elements */ + --gray-50: #f9fafb; + --gray-100: #f3f4f6; + --gray-200: #e5e7eb; + --gray-300: #d1d5db; + --gray-400: #9ca3af; + --gray-500: #6b7280; + --gray-600: #4b5563; + --gray-700: #374151; + --gray-800: #1f2937; + + /* Shadows */ + --shadow-color: rgba(15, 23, 42, 0.08); + --shadow-accent: rgba(14, 165, 233, 0.25); } /* Dark theme colors - Automotive Night Mode */ @@ -31,22 +71,62 @@ html.dark { --bg-primary: #0f172a; --bg-secondary: #1e293b; --bg-tertiary: #111c32; + --bg-hover: #334155; --bg-accent: linear-gradient(135deg, #0f172a 0%, #1e293b 100%); --bg-secondary-translucent: rgba(30, 41, 59, 0.85); + + /* Text colors with clear hierarchy */ --text-primary: #f8fafc; --text-secondary: #cbd5e1; --text-muted: #94a3b8; + + /* Border colors */ --border-color: #334155; + --border-hover: #475569; + + /* Primary accent colors */ --accent-primary: #38bdf8; --accent-hover: #0ea5e9; --accent-secondary: #fbbf24; --accent-secondary-hover: #f59e0b; + + /* Status colors - Dark mode optimized */ --success: #22d3ee; --success-hover: #06b6d4; + --success-bg: rgba(34, 211, 238, 0.1); + --success-border: rgba(34, 211, 238, 0.3); + --success-text: #5eead4; + --warning: #fbbf24; --warning-hover: #f59e0b; + --warning-bg: rgba(251, 191, 36, 0.1); + --warning-border: rgba(251, 191, 36, 0.3); + --warning-text: #fcd34d; + --danger: #f87171; --danger-hover: #ef4444; + --danger-bg: rgba(248, 113, 113, 0.1); + --danger-border: rgba(248, 113, 113, 0.3); + --danger-text: #fca5a5; + + --info: #60a5fa; + --info-hover: #3b82f6; + --info-bg: rgba(96, 165, 250, 0.1); + --info-border: rgba(96, 165, 250, 0.3); + --info-text: #93c5fd; + + /* Neutral shades for consistent UI elements */ + --gray-50: #1e293b; + --gray-100: #334155; + --gray-200: #475569; + --gray-300: #64748b; + --gray-400: #94a3b8; + --gray-500: #cbd5e1; + --gray-600: #e2e8f0; + --gray-700: #f1f5f9; + --gray-800: #f8fafc; + + /* Shadows */ --shadow-color: rgba(0, 0, 0, 0.6); --shadow-accent: rgba(56, 189, 248, 0.2); } @@ -68,11 +148,391 @@ body { .theme-bg-primary { background-color: var(--bg-primary); } .theme-bg-secondary { background-color: var(--bg-secondary); } .theme-bg-tertiary { background-color: var(--bg-tertiary); } +.theme-bg-hover { background-color: var(--bg-hover); } .theme-text-primary { color: var(--text-primary); } .theme-text-secondary { color: var(--text-secondary); } .theme-text-muted { color: var(--text-muted); } .theme-border { border-color: var(--border-color); } +/* Gray utility classes for consistency */ +.theme-bg-gray-50 { background-color: var(--gray-50); } +.theme-bg-gray-100 { background-color: var(--gray-100); } +.theme-bg-gray-200 { background-color: var(--gray-200); } +.theme-text-gray-400 { color: var(--gray-400); } +.theme-text-gray-500 { color: var(--gray-500); } +.theme-text-gray-600 { color: var(--gray-600); } +.theme-border-gray-200 { border-color: var(--gray-200); } +.theme-border-gray-300 { border-color: var(--gray-300); } +.theme-border-gray-700 { border-color: var(--gray-700); } + +/* Alert/Message styles - Status colors */ +.theme-alert-success { + background-color: var(--success-bg); + border: 1px solid var(--success-border); + color: var(--success-text); + padding: 1rem; + border-radius: 0.5rem; +} + +.theme-alert-warning { + background-color: var(--warning-bg); + border: 1px solid var(--warning-border); + color: var(--warning-text); + padding: 1rem; + border-radius: 0.5rem; +} + +.theme-alert-danger { + background-color: var(--danger-bg); + border: 1px solid var(--danger-border); + color: var(--danger-text); + padding: 1rem; + border-radius: 0.5rem; +} + +.theme-alert-info { + background-color: var(--info-bg); + border: 1px solid var(--info-border); + color: var(--info-text); + padding: 1rem; + border-radius: 0.5rem; +} + +/* Badge/Status pill styles */ +.theme-badge { + display: inline-flex; + align-items: center; + padding: 0.25rem 0.75rem; + border-radius: 9999px; + font-size: 0.75rem; + font-weight: 600; +} + +.theme-badge-success { + background-color: var(--success-bg); + color: var(--success-text); +} + +.theme-badge-warning { + background-color: var(--warning-bg); + color: var(--warning-text); +} + +.theme-badge-danger { + background-color: var(--danger-bg); + color: var(--danger-text); +} + +.theme-badge-info { + background-color: var(--info-bg); + color: var(--info-text); +} + +/* Status text colors */ +.theme-text-success { color: var(--success-text); } +.theme-text-warning { color: var(--warning-text); } +.theme-text-danger { color: var(--danger-text); } +.theme-text-info { color: var(--info-text); } + +/* STANDARDIZED PAGE LAYOUT CLASSES */ +.dashboard-page-container { + min-height: 100vh; + background-color: var(--bg-primary); + padding: 2rem 1rem; +} + +@media (min-width: 768px) { + .dashboard-page-container { + padding: 2rem; + } +} + +.dashboard-page-header { + margin-bottom: 2rem; +} + +.dashboard-page-title { + font-size: 1.875rem; + font-weight: 700; + color: var(--text-primary); + margin-bottom: 0.5rem; +} + +.dashboard-page-subtitle { + color: var(--text-muted); + font-size: 0.875rem; +} + +.dashboard-content-card { + background-color: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: 1rem; + padding: 1.5rem; + box-shadow: 0 1px 3px var(--shadow-color); +} + +/* STANDARDIZED FILTER/TAB BUTTON STYLES */ +.filter-tabs-container { + display: flex; + gap: 0.5rem; + flex-wrap: wrap; + margin-bottom: 1.5rem; +} + +.filter-tab { + padding: 0.625rem 1.25rem; + border-radius: 0.5rem; + font-size: 0.875rem; + font-weight: 500; + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); + cursor: pointer; + border: 1px solid transparent; + white-space: nowrap; +} + +.filter-tab-active { + background: linear-gradient(135deg, #0CA0E4 0%, #0CA0E4 100%); + color: white; + border-color: #0CA0E4; + font-weight: 600; + box-shadow: 0 4px 6px -1px rgba(59, 130, 246, 0.3), 0 2px 4px -1px rgba(59, 130, 246, 0.2); +} + +.filter-tab-inactive { + background-color: var(--bg-secondary); + color: var(--text-secondary); + border-color: var(--border-color); +} + +.filter-tab-inactive:hover { + background-color: #0ca0e483; + color: #ffffff; + border-color: #0ca0e4b7; +} + +:root.dark .filter-tab-inactive:hover { + background-color: rgba(59, 130, 246, 0.1); + color: #0CA0E4; + border-color: #0CA0E4; +} + +/* Legacy support - keep for backward compatibility */ +.theme-filter-active { + background: linear-gradient(135deg, #0CA0E4 0%, #0CA0E4 100%); + color: white; + border: 1px solid #0CA0E4; + font-weight: 600; + box-shadow: 0 4px 6px -1px rgba(59, 130, 246, 0.3), 0 2px 4px -1px rgba(59, 130, 246, 0.2); +} + +.theme-filter-inactive { + background-color: var(--bg-secondary); + color: var(--text-secondary); + border: 1px solid var(--border-color); +} + +.theme-filter-inactive:hover { + background-color: #0CA0E4; + color: #0CA0E4; + border-color: #0CA0E4; +} + +:root.dark .theme-filter-inactive:hover { + background-color:#0CA0E4; + color: #0CA0E4; + border-color: #60a5fa; +} + +/* Action button variants */ +.theme-button-action { + background: var(--accent-primary); + color: white; + border: none; + padding: 0.75rem 1.5rem; + border-radius: 0.5rem; + font-weight: 600; + font-size: 0.875rem; + transition: all 0.2s ease; + cursor: pointer; +} + +.theme-button-action:hover { + background: var(--accent-hover); + transform: translateY(-1px); + box-shadow: 0 4px 12px var(--shadow-accent); +} + +.theme-button-action:disabled { + opacity: 0.5; + cursor: not-allowed; + transform: none; +} + +/* STANDARDIZED BUTTON STYLES */ +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0.75rem 1.5rem; + border-radius: 0.5rem; + font-weight: 600; + font-size: 0.875rem; + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); + cursor: pointer; + border: 1px solid transparent; + white-space: nowrap; +} + +.btn-primary { + background: var(--accent-primary); + color: white; + border-color: var(--accent-primary); +} + +.btn-primary:hover { + background: var(--accent-hover); + border-color: var(--accent-hover); + transform: translateY(-1px); + box-shadow: 0 4px 12px var(--shadow-accent); +} + +.btn-secondary { + background: var(--bg-secondary); + color: var(--text-primary); + border-color: var(--border-color); +} + +.btn-secondary:hover { + background: var(--bg-tertiary); + border-color: var(--border-hover); +} + +.btn-danger { + background: var(--danger); + color: white; + border-color: var(--danger); +} + +.btn-danger:hover { + background: var(--danger-hover); + border-color: var(--danger-hover); +} + +.btn-success { + background: var(--success); + color: white; + border-color: var(--success); +} + +.btn-success:hover { + background: var(--success-hover); + border-color: var(--success-hover); +} + +/* STANDARDIZED EMPTY STATE STYLES */ +.empty-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 4rem 2rem; + text-align: center; + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: 1rem; +} + +.empty-state-icon { + width: 4rem; + height: 4rem; + color: var(--text-muted); + margin-bottom: 1rem; +} + +.empty-state-title { + font-size: 1.25rem; + font-weight: 600; + color: var(--text-primary); + margin-bottom: 0.5rem; +} + +.empty-state-description { + color: var(--text-muted); + margin-bottom: 1.5rem; +} + +/* STANDARDIZED STAT CARD STYLES */ +.stat-card { + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: 0.75rem; + padding: 1.25rem; + transition: all 0.2s ease; +} + +.stat-card:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px var(--shadow-color); +} + +.stat-card-label { + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--text-muted); + font-weight: 600; + margin-bottom: 0.5rem; +} + +.stat-card-value { + font-size: 1.5rem; + font-weight: 700; + color: var(--text-primary); +} + +/* STANDARDIZED TABLE STYLES */ +.data-table { + width: 100%; + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: 0.75rem; + overflow: hidden; +} + +.data-table-header { + background: var(--bg-tertiary); + border-bottom: 1px solid var(--border-color); +} + +.data-table-header th { + padding: 0.75rem 1.5rem; + text-align: left; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--text-muted); +} + +.data-table-body tr { + border-bottom: 1px solid var(--border-color); + transition: background-color 0.15s ease; +} + +.data-table-body tr:last-child { + border-bottom: none; +} + +.data-table-body tr:hover { + background: var(--bg-hover); +} + +.data-table-body td { + padding: 1rem 1.5rem; + color: var(--text-secondary); + font-size: 0.875rem; +} + /* Enhanced form styles */ .theme-input { background-color: var(--bg-primary); @@ -94,6 +554,65 @@ body { color: var(--text-muted); } +/* Form input variants for better visibility */ +.form-input, +.form-select, +.form-textarea { + width: 100%; + padding: 0.625rem 0.75rem; + border: 1px solid var(--border-color); + border-radius: 0.5rem; + background-color: var(--bg-primary); + color: var(--text-primary); + transition: all 0.2s ease; + font-size: 0.875rem; +} + +.form-input:focus, +.form-select:focus, +.form-textarea:focus { + outline: none; + border-color: var(--accent-primary); + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); +} + +.form-input::placeholder, +.form-textarea::placeholder { + color: var(--text-muted); +} + +.form-input:disabled, +.form-select:disabled, +.form-textarea:disabled { + opacity: 0.6; + cursor: not-allowed; + background-color: var(--bg-tertiary); +} + +/* Modal/Dialog styles */ +.modal-overlay { + position: fixed; + inset: 0; + background-color: rgba(0, 0, 0, 0.5); + backdrop-filter: blur(4px); + z-index: 50; + display: flex; + align-items: center; + justify-content: center; + padding: 1rem; +} + +.modal-content { + background-color: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: 1rem; + box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.3), 0 10px 10px -5px rgba(0, 0, 0, 0.2); + max-width: 42rem; + width: 100%; + max-height: 90vh; + overflow-y: auto; +} + /* Enhanced button styles */ .theme-button-primary { background: linear-gradient(135deg, var(--accent-primary), var(--accent-hover)); @@ -247,11 +766,11 @@ html.dark .bg-grid-pattern { .automotive-card { - background: var(--bg-primary); + background: var(--bg-secondary); border: 1px solid var(--border-color); - border-radius: 20px; - box-shadow: 0 4px 15px var(--shadow-color); - transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); + border-radius: 16px; + box-shadow: 0 2px 8px var(--shadow-color); + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); position: relative; overflow: hidden; } @@ -262,7 +781,7 @@ html.dark .bg-grid-pattern { top: 0; left: 0; right: 0; - height: 4px; + height: 3px; background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary), @@ -277,11 +796,24 @@ html.dark .bg-grid-pattern { } .automotive-card:hover { - transform: translateY(-5px); - box-shadow: - 0 20px 40px var(--shadow-color), - 0 0 30px var(--shadow-accent); - border-color: var(--accent-primary); + transform: translateY(-2px); + box-shadow: 0 8px 20px var(--shadow-color); + border-color: var(--border-hover); +} + +/* Card without hover effects for empty states */ +.automotive-card-static { + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: 16px; + box-shadow: 0 2px 8px var(--shadow-color); + position: relative; + overflow: hidden; +} + +/* Automotive accent gradient */ +.automotive-accent { + background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary)); } /* New Tech Stack Item style */ @@ -311,9 +843,11 @@ html.dark .bg-grid-pattern { top: 0; z-index: 50; width: 100%; - background: var(--bg-secondary-translucent); + background: var(--bg-secondary); + border-bottom: 1px solid var(--border-color); backdrop-filter: blur(10px); transition: all 0.3s ease-in-out; + box-shadow: 0 1px 3px var(--shadow-color); } /* How It Works - Timeline */ From 2a97e983d0801f683c5b75ffb289567fa480548b Mon Sep 17 00:00:00 2001 From: Pramudi Samarawickrama Date: Thu, 20 Nov 2025 17:21:53 +0530 Subject: [PATCH 06/34] style: update error message styling for consistency across components --- src/app/admin/page.tsx | 6 +++--- src/app/auth/forgot-password/page.tsx | 2 +- src/app/auth/login/page.tsx | 2 +- src/app/auth/resend-verification/page.tsx | 2 +- src/app/auth/reset-password/ResetPasswordForm.tsx | 2 +- src/app/components/NotificationBell.tsx | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index 99eaafe..27a685d 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -108,13 +108,13 @@ export default function AdminDashboard() {
{error && ( -
-

{error}

+
+

{error}

)} {success && ( -
+

{success}

diff --git a/src/app/auth/forgot-password/page.tsx b/src/app/auth/forgot-password/page.tsx index 139d021..bd72570 100644 --- a/src/app/auth/forgot-password/page.tsx +++ b/src/app/auth/forgot-password/page.tsx @@ -74,7 +74,7 @@ export default function ForgotPasswordPage() {
{error && (
-

{error}

+

{error}

)}
diff --git a/src/app/auth/login/page.tsx b/src/app/auth/login/page.tsx index ffc6ba1..f16b2f4 100644 --- a/src/app/auth/login/page.tsx +++ b/src/app/auth/login/page.tsx @@ -160,7 +160,7 @@ export default function LoginPage() {
{error && (
-

{error}

+

{error}

{unverifiedEmail && ( -

{error}

+

{error}

)} diff --git a/src/app/auth/reset-password/ResetPasswordForm.tsx b/src/app/auth/reset-password/ResetPasswordForm.tsx index e8e7a1f..ed32f49 100644 --- a/src/app/auth/reset-password/ResetPasswordForm.tsx +++ b/src/app/auth/reset-password/ResetPasswordForm.tsx @@ -143,7 +143,7 @@ export default function ResetPasswordForm() { {error && (
-

{error}

+

{error}

)} diff --git a/src/app/components/NotificationBell.tsx b/src/app/components/NotificationBell.tsx index 39a3407..d80f410 100644 --- a/src/app/components/NotificationBell.tsx +++ b/src/app/components/NotificationBell.tsx @@ -102,7 +102,7 @@ export default function NotificationBell() { {loading ? (
Loading notifications...
) : error ? ( -
{error}
+
{error}
) : notifications.length === 0 ? (
No notifications yet.
) : ( From 06eb1d2e874d1f4a1211d80eadf5f5509be91342 Mon Sep 17 00:00:00 2001 From: Pramudi Samarawickrama Date: Thu, 20 Nov 2025 17:25:13 +0530 Subject: [PATCH 07/34] style: unify error message styling across dashboards and payment gateway components --- src/app/components/PaymentGateway.tsx | 12 ++++++------ src/app/components/VehicleCard.tsx | 2 +- src/app/components/dashboards/AdminDashboard.tsx | 2 +- src/app/components/dashboards/CustomerDashboard.tsx | 2 +- src/app/components/dashboards/EmployeeDashboard.tsx | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/app/components/PaymentGateway.tsx b/src/app/components/PaymentGateway.tsx index 40649ca..0343e1d 100644 --- a/src/app/components/PaymentGateway.tsx +++ b/src/app/components/PaymentGateway.tsx @@ -123,14 +123,14 @@ export default function PaymentGateway({
{error && ( -
-

{error}

+
+

{error}

)} {success && ( -
-

Payment completed successfully!

+
+

Payment completed successfully!

)} @@ -144,8 +144,8 @@ export default function PaymentGateway({ {loading ? 'Processing...' : success ? 'Payment Completed' : 'Pay with PayHere'} -
-

+

+

Secure payment powered by PayHere. Your payment information is encrypted and secure.

diff --git a/src/app/components/VehicleCard.tsx b/src/app/components/VehicleCard.tsx index fcdac0f..d72a5fb 100644 --- a/src/app/components/VehicleCard.tsx +++ b/src/app/components/VehicleCard.tsx @@ -10,7 +10,7 @@ interface VehicleCardProps { export default function VehicleCard({ vehicle, onDelete, onEdit }: VehicleCardProps) { return ( -
+

diff --git a/src/app/components/dashboards/AdminDashboard.tsx b/src/app/components/dashboards/AdminDashboard.tsx index 818b715..c6bf952 100644 --- a/src/app/components/dashboards/AdminDashboard.tsx +++ b/src/app/components/dashboards/AdminDashboard.tsx @@ -57,7 +57,7 @@ const AdminDashboard: React.FC = ({ profile }) => {

{error && ( -
+
{error}
)} diff --git a/src/app/components/dashboards/CustomerDashboard.tsx b/src/app/components/dashboards/CustomerDashboard.tsx index 00b8bd7..3b2da0a 100644 --- a/src/app/components/dashboards/CustomerDashboard.tsx +++ b/src/app/components/dashboards/CustomerDashboard.tsx @@ -75,7 +75,7 @@ const CustomerDashboard: React.FC = ({ profile }) => {
{error && ( -
+
{error}
)} diff --git a/src/app/components/dashboards/EmployeeDashboard.tsx b/src/app/components/dashboards/EmployeeDashboard.tsx index 4013d55..9e56ece 100644 --- a/src/app/components/dashboards/EmployeeDashboard.tsx +++ b/src/app/components/dashboards/EmployeeDashboard.tsx @@ -94,7 +94,7 @@ const EmployeeDashboard: React.FC = ({ profile }) => {
{error && ( -
+
{error}
)} From 60270fc598f0ca657d761e3ccb104c67e3d28d57 Mon Sep 17 00:00:00 2001 From: Pramudi Samarawickrama Date: Thu, 20 Nov 2025 17:27:21 +0530 Subject: [PATCH 08/34] style: Refactor dashboard components for improved styling and consistency --- src/app/dashboard/admin/audit-logs/page.tsx | 10 +-- src/app/dashboard/admin/page.tsx | 74 +++++++++---------- src/app/dashboard/admin/reports/page.tsx | 14 ++-- .../dashboard/admin/service-types/page.tsx | 20 ++--- src/app/dashboard/admin/users/page.tsx | 68 ++++++++--------- .../appointments/[appointmentId]/page.tsx | 10 +-- .../appointments/availability/page.tsx | 2 +- src/app/dashboard/appointments/book/page.tsx | 6 +- src/app/dashboard/appointments/page.tsx | 16 ++-- src/app/dashboard/invoices/page.tsx | 36 ++++----- src/app/dashboard/layout.tsx | 12 +-- src/app/dashboard/notifications/page.tsx | 22 +++--- 12 files changed, 145 insertions(+), 145 deletions(-) diff --git a/src/app/dashboard/admin/audit-logs/page.tsx b/src/app/dashboard/admin/audit-logs/page.tsx index 699f8f9..c6b4b88 100644 --- a/src/app/dashboard/admin/audit-logs/page.tsx +++ b/src/app/dashboard/admin/audit-logs/page.tsx @@ -52,7 +52,7 @@ export default function AuditLogsPage() {
{[...Array(10)].map((_, i) => ( -
+
))}
@@ -94,10 +94,10 @@ export default function AuditLogsPage() {
)} -
+
- + @@ -110,7 +110,7 @@ export default function AuditLogsPage() { {logs.length === 0 ? ( ) : ( logs.map((log) => ( - + diff --git a/src/app/dashboard/admin/page.tsx b/src/app/dashboard/admin/page.tsx index 83f1ade..7727a8c 100644 --- a/src/app/dashboard/admin/page.tsx +++ b/src/app/dashboard/admin/page.tsx @@ -35,10 +35,10 @@ export default function AdminPage() { return (
-
+
{[...Array(8)].map((_, i) => ( -
+
))}
@@ -79,11 +79,11 @@ export default function AdminPage() {
- +
@@ -96,11 +96,11 @@ export default function AdminPage() {
- +
@@ -113,11 +113,11 @@ export default function AdminPage() {
- +
@@ -130,11 +130,11 @@ export default function AdminPage() {
- +
@@ -152,17 +152,17 @@ export default function AdminPage() {

User Statistics

-
-

Total customers

-

{stats.totalCustomers ?? 0}

+
+

Total customers

+

{stats.totalCustomers ?? 0}

-
-

Total employees

-

{stats.totalEmployees ?? 0}

+
+

Total employees

+

{stats.totalEmployees ?? 0}

-
-

Total vehicles

-

{stats.totalVehicles ?? 0}

+
+

Total vehicles

+

{stats.totalVehicles ?? 0}

@@ -170,21 +170,21 @@ export default function AdminPage() {

Service Statistics

-
-

Active appointments

-

{stats.activeAppointments ?? 0}

+
+

Active appointments

+

{stats.activeAppointments ?? 0}

-
-

Completed (month)

-

{stats.completedServicesThisMonth ?? 0}

+
+

Completed (month)

+

{stats.completedServicesThisMonth ?? 0}

-
-

Active projects

-

{stats.activeProjects ?? 0}

+
+

Active projects

+

{stats.activeProjects ?? 0}

-
-

Pending invoices

-

{stats.pendingInvoices ?? 0}

+
+

Pending invoices

+

{stats.pendingInvoices ?? 0}

@@ -192,13 +192,13 @@ export default function AdminPage() {

Financial Overview

-
-

Revenue this month

-

LKR {(stats.revenueThisMonth ?? 0).toLocaleString()}

+
+

Revenue this month

+

LKR {(stats.revenueThisMonth ?? 0).toLocaleString()}

-
-

Pending invoices count

-

{stats.pendingInvoices ?? 0}

+
+

Pending invoices count

+

{stats.pendingInvoices ?? 0}

diff --git a/src/app/dashboard/admin/reports/page.tsx b/src/app/dashboard/admin/reports/page.tsx index 4aab25f..e80403b 100644 --- a/src/app/dashboard/admin/reports/page.tsx +++ b/src/app/dashboard/admin/reports/page.tsx @@ -43,8 +43,8 @@ export default function ReportsPage() { return (
-
-
+
+
); @@ -79,7 +79,7 @@ export default function ReportsPage() {

Generate business analytics and reports

-
+

Generate Report

@@ -88,7 +88,7 @@ export default function ReportsPage() { setFormat(e.target.value as typeof format)} - className="w-full px-4 py-3 rounded-lg border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 theme-text-primary focus:ring-2 focus:ring-blue-500" + className="form-select" > @@ -120,7 +120,7 @@ export default function ReportsPage() { type="date" value={dateRange.fromDate} onChange={(e) => setDateRange({ ...dateRange, fromDate: e.target.value })} - className="w-full px-4 py-3 rounded-lg border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 theme-text-primary focus:ring-2 focus:ring-blue-500" + className="form-input" />
@@ -129,7 +129,7 @@ export default function ReportsPage() { type="date" value={dateRange.toDate} onChange={(e) => setDateRange({ ...dateRange, toDate: e.target.value })} - className="w-full px-4 py-3 rounded-lg border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 theme-text-primary focus:ring-2 focus:ring-blue-500" + className="form-input" />
diff --git a/src/app/dashboard/admin/service-types/page.tsx b/src/app/dashboard/admin/service-types/page.tsx index ca8ab4c..8ccbc84 100644 --- a/src/app/dashboard/admin/service-types/page.tsx +++ b/src/app/dashboard/admin/service-types/page.tsx @@ -195,7 +195,7 @@ export default function ServiceTypesPage() {
@@ -280,7 +280,7 @@ export default function ServiceTypesPage() { {/* Create/Edit Modal */} {showModal && (
-
+

@@ -304,7 +304,7 @@ export default function ServiceTypesPage() { required value={formData.name} onChange={(e) => setFormData({ ...formData, name: e.target.value })} - className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 theme-text-primary" + className="form-input" placeholder="e.g., Oil Change" />

@@ -317,7 +317,7 @@ export default function ServiceTypesPage() { required value={formData.category} onChange={(e) => setFormData({ ...formData, category: e.target.value })} - className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 theme-text-primary" + className="form-select" > @@ -334,7 +334,7 @@ export default function ServiceTypesPage() {
Timestamp User
- +

@@ -123,7 +123,7 @@ export default function AuditLogsPage() {

{formatDateTime(log.timestamp)}