diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100755 index 0000000..01d4da2 --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,19 @@ +#!/bin/bash + +# Git pre-commit hook to run ESLint +# This hook runs on every commit to ensure code quality + +echo "🔍 Running ESLint check..." + +# Run lint check +npm run lint + +# Check the exit status +if [ $? -ne 0 ]; then + echo "❌ ESLint failed. Please fix the linting errors before committing." + echo "💡 Run 'npm run lint' to see the issues" + exit 1 +fi + +echo "✅ ESLint passed! Commit is proceeding..." +exit 0 \ No newline at end of file diff --git a/.githooks/pre-push b/.githooks/pre-push new file mode 100755 index 0000000..a04cd31 --- /dev/null +++ b/.githooks/pre-push @@ -0,0 +1,19 @@ +#!/bin/bash + +# Git pre-push hook to run build check +# This hook runs before pushing to ensure the project builds successfully + +echo "🏗️ Running build check before push..." + +# Run build check +npm run build + +# Check the exit status +if [ $? -ne 0 ]; then + echo "❌ Build failed. Please fix the build errors before pushing." + echo "💡 Run 'npm run build' to see the issues" + exit 1 +fi + +echo "✅ Build passed! Push is proceeding..." +exit 0 \ No newline at end of file diff --git a/GIT_HOOKS.md b/GIT_HOOKS.md new file mode 100644 index 0000000..1fdc073 --- /dev/null +++ b/GIT_HOOKS.md @@ -0,0 +1,61 @@ +# Git Hooks + +This project uses git hooks to maintain code quality and ensure builds pass before commits and pushes. + +## Hooks Installed + +- **pre-commit**: Runs `npm run lint` before each commit + - Prevents commits if ESLint errors are found + - Ensures code follows the project's linting standards + +- **pre-push**: Runs `npm run build` before each push + - Prevents pushes if the build fails + - Ensures the project compiles successfully before sharing changes + +## Setup + +Run the setup script to configure git hooks: + +```bash +./setup-hooks.sh +``` + +Or manually configure: + +```bash +chmod +x .githooks/pre-commit .githooks/pre-push +git config core.hooksPath .githooks +``` + +## Bypassing Hooks + +Sometimes you may need to bypass hooks (use sparingly): + +```bash +# Skip pre-commit hook +git commit --no-verify -m "your message" + +# Skip pre-push hook +git push --no-verify +``` + +## Hook Details + +### Pre-commit Hook +- Runs automatically on `git commit` +- Executes `npm run lint` +- Exits with error code if linting fails +- Shows helpful error messages + +### Pre-push Hook +- Runs automatically on `git push` +- Executes `npm run build` +- Exits with error code if build fails +- Prevents pushing broken code to remote repository + +## Benefits + +✅ **Code Quality**: Ensures all committed code passes linting standards +✅ **Build Safety**: Prevents pushing code that doesn't compile +✅ **Team Consistency**: All developers follow the same quality checks +✅ **CI/CD Friendly**: Reduces failed builds in CI/CD pipelines \ No newline at end of file diff --git a/README.md b/README.md index a8db4a8..1a202df 100644 --- a/README.md +++ b/README.md @@ -42,3 +42,9 @@ This repository contains the source code for the TechTorque 2025 customer and em 3. **Access Application:** Open [http://localhost:3000](http://localhost:3000) in your browser. + +4. **Setup Git Hooks (Recommended):** + ```bash + npm run setup-hooks + ``` + This configures automatic linting on commit and build checking on push. See [GIT_HOOKS.md](GIT_HOOKS.md) for details. diff --git a/package-lock.json b/package-lock.json index 004eb4e..b09b2b8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1310,6 +1310,7 @@ "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -1377,6 +1378,7 @@ "integrity": "sha512-6m1I5RmHBGTnUGS113G04DMu3CpSdxCAU/UvtjNWL4Nuf3MW9tQhiJqRlHzChIkhy6kZSAQmc+I1bcGjE3yNKg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.3", "@typescript-eslint/types": "8.46.3", @@ -1894,6 +1896,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2766,6 +2769,7 @@ "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -2939,6 +2943,7 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -5162,6 +5167,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -5171,6 +5177,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.26.0" }, @@ -5901,6 +5908,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -6050,6 +6058,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/package.json b/package.json index a3227be..f80d5b9 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,8 @@ "dev": "next dev --turbopack", "build": "next build --turbopack", "start": "next start", - "lint": "eslint" + "lint": "eslint", + "setup-hooks": "chmod +x .githooks/pre-commit .githooks/pre-push && git config core.hooksPath .githooks && echo '✅ Git hooks configured successfully!'" }, "dependencies": { "@stomp/stompjs": "^7.2.1", diff --git a/public/runtime-config.js b/public/runtime-config.js new file mode 100644 index 0000000..8a09c7d --- /dev/null +++ b/public/runtime-config.js @@ -0,0 +1,13 @@ +// runtime-config.js +// This file is intentionally simple and is loaded before the app hydrates. +// In production you may overwrite this file at container startup with real +// environment values. Example generation (Linux entrypoint): +// cat > /usr/share/app/public/runtime-config.js <(null); const [warning, setWarning] = useState(null); + const [unverifiedEmail, setUnverifiedEmail] = useState(null); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setError(null); setWarning(null); + setUnverifiedEmail(null); setLoading(true); const form = e.currentTarget; const formData = new FormData(form); + const email = (formData.get('username') as string) || ''; const payload: LoginRequest = { - username: (formData.get('username') as string) || '', + username: email, password: (formData.get('password') as string) || '', }; try { @@ -51,7 +54,14 @@ export default function LoginPage() { } } catch (err: unknown) { const errorMessage = err instanceof Error ? err.message : 'Login failed'; - setError(errorMessage); + + // Check if error is about email verification + if (errorMessage.includes('verify your email') || errorMessage.includes('email address')) { + setUnverifiedEmail(email); + setError(errorMessage); + } else { + setError(errorMessage); + } } finally { setLoading(false); } @@ -149,13 +159,21 @@ export default function LoginPage() { {error && ( -
- {error} +
+

{error}

+ {unverifiedEmail && ( + + Resend Verification Email → + + )}
)} {warning && ( -
- {warning} +
+

{warning}

)}
diff --git a/src/app/auth/register/page.tsx b/src/app/auth/register/page.tsx index 05d4d20..1dafed5 100644 --- a/src/app/auth/register/page.tsx +++ b/src/app/auth/register/page.tsx @@ -2,7 +2,6 @@ import React from "react"; import Link from "next/link"; -import { useRouter } from "next/navigation"; import { useState } from "react"; import authService from "../../../services/authService"; import ThemeToggle from "../../components/ThemeToggle"; @@ -70,8 +69,6 @@ const EyeOffIcon = () => ( // --- Register Page Component --- export default function RegisterPage() { - const router = useRouter(); - const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [success, setSuccess] = useState(false); @@ -118,10 +115,7 @@ export default function RegisterPage() { try { await authService.register(payload); setSuccess(true); - // Redirect to login after 3 seconds - setTimeout(() => { - router.push("/auth/login"); - }, 3000); + // Don't automatically redirect - let user see the success message } catch (err: unknown) { const errorMessage = err instanceof Error ? err.message : "Registration failed"; @@ -186,13 +180,31 @@ export default function RegisterPage() {

Welcome to TechTorque Auto, {formData.fullName}!

-

- A verification email has been sent to {formData.email}. Please - verify your email to log in. -

-

- Redirecting to login... -

+
+

+ 📧 Verify Your Email +

+

+ A verification email has been sent to {formData.email}. +
+ You must verify your email before you can log in. +

+

+ Didn't receive the email?{' '} + + Resend verification email + +

+
+ + Go to Login +
) : ( <> diff --git a/src/app/auth/resend-verification/page.tsx b/src/app/auth/resend-verification/page.tsx new file mode 100644 index 0000000..683ced8 --- /dev/null +++ b/src/app/auth/resend-verification/page.tsx @@ -0,0 +1,180 @@ +'use client' + +import React, { useEffect, useState, Suspense } from 'react'; +import Link from 'next/link'; +import { useSearchParams } from 'next/navigation'; +import authService from '../../../services/authService'; +import ThemeToggle from '../../components/ThemeToggle'; + +// Icon Components +const Icon = ({ d, size = 10 }: { d: string; size?: number }) => ( + + + +); +const BoltIcon = ({size = 10}) => ; +const MailIcon = () => ; + +function ResendVerificationContent() { + const searchParams = useSearchParams(); + const emailParam = searchParams.get('email'); + + const [loading, setLoading] = useState(false); + const [success, setSuccess] = useState(false); + const [error, setError] = useState(null); + const [email, setEmail] = useState(emailParam || ''); + + useEffect(() => { + if (emailParam) { + setEmail(emailParam); + } + }, [emailParam]); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(null); + setSuccess(false); + setLoading(true); + + try { + await authService.resendVerificationEmail(email); + setSuccess(true); + } catch (err: unknown) { + const errorMessage = err instanceof Error ? err.message : 'Failed to resend verification email'; + setError(errorMessage); + } finally { + setLoading(false); + } + }; + + return ( + <> + {success ? ( +
+
+ + + +
+

+ Email Sent! +

+

+ We've sent a new verification link to {email}. + Please check your inbox and click the link to verify your account. +

+
+ + Back to Login + + +
+
+ ) : ( +
+
+ + setEmail(e.target.value)} + className="theme-input w-full" + placeholder="you@example.com" + /> +
+ +
+ +
+
+ )} + + {error && ( +
+

{error}

+
+ )} + + {!success && ( +
+

+ Remember your password?{' '} + + Sign In + +

+
+ )} + + ); +} + +export default function ResendVerificationPage() { + return ( +
+ {/* Header */} +
+
+
+ +
+ +
+

+ TechTorque Auto +

+ +
+ + Back to Login + + +
+
+
+
+ + {/* Main Content */} +
+
+
+
+
+

+ Resend Verification Email +

+

+ Enter your email address to receive a new verification link. +

+
+ + Loading...
}> + + +
+
+ + + ); +} diff --git a/src/app/components/AddVehicleForm.tsx b/src/app/components/AddVehicleForm.tsx index f124ca8..c5a9c0d 100644 --- a/src/app/components/AddVehicleForm.tsx +++ b/src/app/components/AddVehicleForm.tsx @@ -30,7 +30,17 @@ export default function AddVehicleForm({ onSuccess, onCancel }: AddVehicleFormPr await vehicleService.registerVehicle(formData); onSuccess(); } catch (err: unknown) { - const errorMessage = (err as { response?: { data?: { message?: string } } })?.response?.data?.message || 'Failed to add vehicle'; + let errorMessage = 'Failed to add vehicle'; + + const errorResponse = (err as { response?: { status?: number; data?: { message?: string } } }); + + // Handle 409 Conflict - Duplicate VIN + if (errorResponse?.response?.status === 409) { + errorMessage = 'This VIN is already registered in the system. Please check the VIN and try again.'; + } else { + errorMessage = errorResponse?.response?.data?.message || errorMessage; + } + setError(errorMessage); } finally { setLoading(false); diff --git a/src/app/components/NotificationBell.tsx b/src/app/components/NotificationBell.tsx index 8abe3e7..39a3407 100644 --- a/src/app/components/NotificationBell.tsx +++ b/src/app/components/NotificationBell.tsx @@ -75,7 +75,7 @@ export default function NotificationBell() { /> {unreadCount > 0 && ( - + {unreadCount} )} @@ -85,14 +85,14 @@ export default function NotificationBell() { {open && ( -
-
- Notifications +
+
+ Notifications {notifications.length > 0 && ( @@ -100,23 +100,23 @@ export default function NotificationBell() {
{loading ? ( -
Loading notifications...
+
Loading notifications...
) : error ? ( -
{error}
+
{error}
) : notifications.length === 0 ? ( -
No notifications yet.
+
No notifications yet.
) : ( -
    +
      {notifications.map((notification) => (
    • -
      -
      -

      {notification.message}

      +
      +
      +

      {notification.message}

      {notification.details && ( -

      {notification.details}

      +

      {notification.details}

      )}

      {new Date(notification.createdAt).toLocaleString()} @@ -126,7 +126,7 @@ export default function NotificationBell() { @@ -137,8 +137,8 @@ export default function NotificationBell() {

    )} View all notifications diff --git a/src/app/components/RoleSwitcher.tsx b/src/app/components/RoleSwitcher.tsx new file mode 100644 index 0000000..8d8f319 --- /dev/null +++ b/src/app/components/RoleSwitcher.tsx @@ -0,0 +1,163 @@ +'use client'; + +import { useState, useRef, useEffect } from 'react'; +import { useRouter } from 'next/navigation'; + +interface RoleSwitcherProps { + roles: string[]; + currentRole: string; + onRoleChange: (role: string) => void; +} + +const roleConfig = { + SUPER_ADMIN: { + label: 'Super Admin', + icon: '👑', + color: 'text-purple-600 dark:text-purple-400', + bgColor: 'bg-purple-100 dark:bg-purple-900/30', + dashboardPath: '/dashboard', + }, + ADMIN: { + label: 'Admin', + icon: '⚙️', + color: 'text-blue-600 dark:text-blue-400', + bgColor: 'bg-blue-100 dark:bg-blue-900/30', + dashboardPath: '/dashboard', + }, + EMPLOYEE: { + label: 'Employee', + icon: '👷', + color: 'text-green-600 dark:text-green-400', + bgColor: 'bg-green-100 dark:bg-green-900/30', + dashboardPath: '/dashboard', + }, + CUSTOMER: { + label: 'Customer', + icon: '👤', + color: 'text-gray-600 dark:text-gray-400', + bgColor: 'bg-gray-100 dark:bg-gray-800', + dashboardPath: '/dashboard', + }, +}; + +export default function RoleSwitcher({ roles, currentRole, onRoleChange }: RoleSwitcherProps) { + const [isOpen, setIsOpen] = useState(false); + const dropdownRef = useRef(null); + const router = useRouter(); + + // Close dropdown when clicking outside + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + setIsOpen(false); + } + }; + + if (isOpen) { + document.addEventListener('mousedown', handleClickOutside); + } + + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [isOpen]); + + // Only show switcher if user has multiple roles + if (!roles || roles.length <= 1) { + return null; + } + + // Sort roles by priority + const sortedRoles = [...roles].sort((a, b) => { + const priority = { SUPER_ADMIN: 0, ADMIN: 1, EMPLOYEE: 2, CUSTOMER: 3 }; + return (priority[a as keyof typeof priority] ?? 99) - (priority[b as keyof typeof priority] ?? 99); + }); + + const currentConfig = roleConfig[currentRole as keyof typeof roleConfig] || { + label: currentRole, + icon: '🔹', + color: 'text-gray-600', + bgColor: 'bg-gray-100', + dashboardPath: '/dashboard', + }; + + const handleRoleSwitch = (role: string) => { + onRoleChange(role); + setIsOpen(false); + // Optionally refresh the page to update the dashboard view + router.refresh(); + }; + + return ( +
    + {/* Current Role Button */} + + + {/* Dropdown Menu */} + {isOpen && ( +
    +
    +
    + Switch View +
    + {sortedRoles.map((role) => { + const config = roleConfig[role as keyof typeof roleConfig] || { + label: role, + icon: '🔹', + color: 'text-gray-600', + bgColor: 'bg-gray-100', + }; + const isActive = role === currentRole; + + return ( + + ); + })} +
    +
    +

    + 💡 Your current view affects the dashboard and available menu items +

    +
    +
    + )} +
    + ); +} diff --git a/src/app/contexts/DashboardContext.tsx b/src/app/contexts/DashboardContext.tsx index 4a4c2a0..0e69972 100644 --- a/src/app/contexts/DashboardContext.tsx +++ b/src/app/contexts/DashboardContext.tsx @@ -7,6 +7,8 @@ interface DashboardContextState { profile: UserDto | null loading: boolean roles: string[] + activeRole: string + setActiveRole: (role: string) => void refreshProfile: () => Promise } @@ -15,6 +17,7 @@ const DashboardContext = createContext(undefi export function DashboardProvider({ children }: { children: ReactNode }) { const [profile, setProfile] = useState(null) const [loading, setLoading] = useState(true) + const [activeRole, setActiveRole] = useState('') const loadProfile = async () => { try { @@ -22,6 +25,15 @@ export function DashboardProvider({ children }: { children: ReactNode }) { const response = await userService.getCurrentProfile() const payload = (response.data?.data as UserDto | undefined) ?? (response.data as UserDto | null) setProfile(payload ?? null) + + // Set initial active role to the highest privilege role + if (payload?.roles && payload.roles.length > 0) { + const sortedRoles = [...payload.roles].sort((a, b) => { + const priority = { SUPER_ADMIN: 0, ADMIN: 1, EMPLOYEE: 2, CUSTOMER: 3 }; + return (priority[a as keyof typeof priority] ?? 99) - (priority[b as keyof typeof priority] ?? 99); + }); + setActiveRole(sortedRoles[0]); + } } catch (error) { console.error('Failed to load dashboard profile', error) setProfile(null) @@ -38,8 +50,10 @@ export function DashboardProvider({ children }: { children: ReactNode }) { profile, loading, roles: profile?.roles ?? [], + activeRole, + setActiveRole, refreshProfile: loadProfile, - }), [profile, loading]) + }), [profile, loading, activeRole]) return ( diff --git a/src/app/dashboard/admin/service-types/page.tsx b/src/app/dashboard/admin/service-types/page.tsx index fc5953c..ca8ab4c 100644 --- a/src/app/dashboard/admin/service-types/page.tsx +++ b/src/app/dashboard/admin/service-types/page.tsx @@ -2,13 +2,22 @@ import { useState, useEffect } from 'react'; import { adminService } from '@/services/adminService'; -import { ServiceTypeResponse } from '@/types/admin'; +import { ServiceTypeResponse, CreateServiceTypeRequest, UpdateServiceTypeRequest } from '@/types/admin'; import { useDashboard } from '@/app/contexts/DashboardContext'; export default function ServiceTypesPage() { const { roles, loading: rolesLoading } = useDashboard(); const [serviceTypes, setServiceTypes] = useState([]); const [loading, setLoading] = useState(true); + const [showModal, setShowModal] = useState(false); + const [editingService, setEditingService] = useState(null); + const [formData, setFormData] = useState({ + name: '', + category: 'MAINTENANCE', + description: '', + price: '', + durationMinutes: 30, + }); useEffect(() => { loadServiceTypes(); @@ -18,16 +27,127 @@ export default function ServiceTypesPage() { try { setLoading(true); const data = await adminService.getAllServiceTypes(); - // Ensure data is an array - setServiceTypes(Array.isArray(data) ? data : []); + console.log('Loaded service types:', data); // Debug log + // Ensure we always get an array and include both active and inactive services + const services = Array.isArray(data) ? data : []; + setServiceTypes(services); + console.log('Set service types count:', services.length); // Debug log } catch (err) { console.error('Failed to load service types:', err); - setServiceTypes([]); // Set empty array on error + setServiceTypes([]); } finally { setLoading(false); } }; + const handleCreate = () => { + setEditingService(null); + setFormData({ + name: '', + category: 'MAINTENANCE', + description: '', + price: '', + durationMinutes: 30, + }); + setShowModal(true); + }; + + const handleEdit = (service: ServiceTypeResponse) => { + setEditingService(service); + setFormData({ + name: service.name, + category: service.category, + description: service.description || '', + price: service.basePriceLKR.toString(), + durationMinutes: service.estimatedDurationMinutes, + }); + setShowModal(true); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + try { + if (editingService) { + // Update existing service - use UpdateServiceTypeRequest format + console.log('Updating service:', editingService.id); + const updateData: UpdateServiceTypeRequest = { + name: formData.name, + category: formData.category, + description: formData.description, + price: parseFloat(formData.price) || 0, + durationMinutes: formData.durationMinutes, + active: editingService.active, // Keep existing active status + }; + await adminService.updateServiceType(editingService.id, updateData); + alert('Service type updated successfully!'); + } else { + // Create new service - use CreateServiceTypeRequest format + console.log('Creating service:', formData); + const createData: CreateServiceTypeRequest = { + name: formData.name, + category: formData.category, + description: formData.description, + price: parseFloat(formData.price) || 0, + durationMinutes: formData.durationMinutes, + }; + await adminService.createServiceType(createData); + alert('Service type created successfully!'); + } + + // Close modal first + setShowModal(false); + setEditingService(null); + + // Then reload the list + console.log('Reloading service types...'); + await loadServiceTypes(); + console.log('Service types reloaded'); + } 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); + } + }; + + const handleDelete = async (service: ServiceTypeResponse) => { + if (!confirm(`Are you sure you want to delete "${service.name}"? This action cannot be undone.`)) { + return; + } + + try { + await adminService.removeServiceType(service.id); + alert('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); + } + }; + + const handleToggleActive = async (service: ServiceTypeResponse) => { + try { + const newStatus = !service.active; + console.log(`Toggling service ${service.id} from ${service.active} to ${newStatus}`); + + await adminService.updateServiceType(service.id, { + name: service.name, + category: service.category, + description: service.description || '', + price: service.basePriceLKR, + durationMinutes: service.estimatedDurationMinutes, + active: newStatus, + }); + + console.log('Toggle successful, reloading list...'); + await loadServiceTypes(); + } catch (err) { + console.error('Failed to toggle service status:', err); + alert('Failed to update service status. Please try again.'); + } + }; + // Check if user has permission to access this page const hasRole = (role: string) => roles?.includes(role); const hasAccess = hasRole('ADMIN') || hasRole('SUPER_ADMIN'); @@ -68,19 +188,35 @@ export default function ServiceTypesPage() { return (
    -
    -

    Service Types

    -

    Manage available service types and pricing

    +
    +
    +

    Service Types

    +

    Manage available service types and pricing

    +
    +
    {serviceTypes.length === 0 ? (
    -

    No service types found

    +

    No service types found. Create one to get started!

    ) : ( serviceTypes.map((serviceType) => ( -
    +

    {serviceType.name}

    {serviceType.active ? ( @@ -88,13 +224,13 @@ export default function ServiceTypesPage() { Active ) : ( - + Inactive )}

    {serviceType.description}

    -
    +
    Category: {serviceType.category} @@ -110,10 +246,155 @@ export default function ServiceTypesPage() { {serviceType.estimatedDurationMinutes ?? 0} min
    + + {/* Action Buttons */} +
    + + + +
    )) )}
    + + {/* Create/Edit Modal */} + {showModal && ( +
    +
    +
    +
    +

    + {editingService ? 'Edit Service Type' : 'Create Service Type'} +

    + +
    + +
    +
    + + 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" + placeholder="e.g., Oil Change" + /> +
    + +
    + + +

    Choose the service category

    +
    + +
    + +