From 0d1b538669c37d6711e56def260c8a47be11d0ba Mon Sep 17 00:00:00 2001 From: RandithaK Date: Tue, 11 Nov 2025 12:05:41 +0530 Subject: [PATCH 01/15] chore: commit all changes (automated) --- src/app/auth/login/page.tsx | 30 +++- src/app/auth/register/page.tsx | 40 +++-- src/app/auth/resend-verification/page.tsx | 170 ++++++++++++++++++++++ 3 files changed, 220 insertions(+), 20 deletions(-) create mode 100644 src/app/auth/resend-verification/page.tsx diff --git a/src/app/auth/login/page.tsx b/src/app/auth/login/page.tsx index 6264900..ffc6ba1 100644 --- a/src/app/auth/login/page.tsx +++ b/src/app/auth/login/page.tsx @@ -25,16 +25,19 @@ export default function LoginPage() { const [loading, setLoading] = useState(false); const [error, setError] = useState(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..6080439 --- /dev/null +++ b/src/app/auth/resend-verification/page.tsx @@ -0,0 +1,170 @@ +'use client' + +import React, { useEffect, useState } 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 = () => ; + +export default function ResendVerificationPage() { + 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 ( +
+ {/* Header */} +
+
+
+ +
+ +
+

+ TechTorque Auto +

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

+ Resend Verification Email +

+

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

+
+ + {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 + +

+
+ )} +
+
+
+
+ ); +} From f29671f70d231b721c6fe3f6d715e14dfbd1fca7 Mon Sep 17 00:00:00 2001 From: RandithaK Date: Tue, 11 Nov 2025 12:21:55 +0530 Subject: [PATCH 02/15] style: improve NotificationBell component layout and styling --- src/app/components/NotificationBell.tsx | 32 ++++++++++++------------- 1 file changed, 16 insertions(+), 16 deletions(-) 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 From 83a5ea0e43468f59e4ab97b0088307f973306a08 Mon Sep 17 00:00:00 2001 From: RandithaK Date: Tue, 11 Nov 2025 12:49:25 +0530 Subject: [PATCH 03/15] feat: implement RoleSwitcher component and integrate role management in Dashboard --- src/app/components/RoleSwitcher.tsx | 163 +++++++++++++++++++++++++ src/app/contexts/DashboardContext.tsx | 16 ++- src/app/dashboard/admin/users/page.tsx | 71 +++++++++-- src/app/dashboard/layout.tsx | 6 +- src/app/dashboard/page.tsx | 15 +-- 5 files changed, 250 insertions(+), 21 deletions(-) create mode 100644 src/app/components/RoleSwitcher.tsx 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/users/page.tsx b/src/app/dashboard/admin/users/page.tsx index eea2242..413b507 100644 --- a/src/app/dashboard/admin/users/page.tsx +++ b/src/app/dashboard/admin/users/page.tsx @@ -77,11 +77,13 @@ export default function AdminUsersPage() { setShowCreateModal(false); await loadUsers(); alert(`${createUserType === 'employee' ? 'Employee' : 'Admin'} created successfully!`); - } catch (err: any) { + } catch (err) { console.error('Failed to create user:', err); - const errorMessage = err?.response?.status === 403 + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const error = err as any; + const errorMessage = error?.response?.status === 403 ? 'Permission denied. You do not have the required permissions to create this user type.' - : err?.response?.data?.message || 'Failed to create user. Please try again.'; + : error?.response?.data?.message || 'Failed to create user. Please try again.'; alert(errorMessage); } }; @@ -98,6 +100,12 @@ export default function AdminUsersPage() { const handleSaveRoles = async (userId: string) => { try { + // Validation: Ensure at least one role is selected + if (editingRoles.length === 0) { + alert('Please select at least one role for the user.'); + return; + } + await adminService.updateUser(userId, { roles: editingRoles }); setEditingUserId(null); setEditingRoles([]); @@ -105,18 +113,29 @@ export default function AdminUsersPage() { alert('Roles updated successfully!'); } catch (err) { console.error('Failed to update roles:', err); - alert('Failed to update roles. Please try again.'); + const errorMessage = err instanceof Error ? err.message : 'Failed to update roles. Please try again.'; + alert(errorMessage); } }; const handleToggleRole = (role: string) => { if (editingRoles.includes(role)) { + // Prevent removing the last role + if (editingRoles.length === 1) { + alert('A user must have at least one role.'); + return; + } setEditingRoles(editingRoles.filter(r => r !== role)); } else { setEditingRoles([...editingRoles, role]); } }; + // Check if a role can be modified + const isRoleProtected = (role: string): boolean => { + return role === 'CUSTOMER' || role === 'SUPER_ADMIN'; + }; + // Check if user can be edited (not customer) const canEditUser = (user: UserResponse): boolean => { const isCustomer = user.roles.includes('CUSTOMER'); @@ -263,29 +282,59 @@ export default function AdminUsersPage() { {editingUserId === user.userId ? (
    + {/* Editable roles */} {['EMPLOYEE', 'ADMIN'].map((role) => ( ))} + + {/* Show protected roles (read-only) */} + {user.roles.filter(role => isRoleProtected(role)).map((role) => ( + + {role} 🔒 + + ))}
    +

    + 💡 Tip: Select multiple roles to give users combined access. + {editingRoles.length > 1 && ( + Currently selected: {editingRoles.length} roles + )} +

    ) : (
    {Array.isArray(user.roles) && user.roles.length > 0 ? ( - user.roles.map((role) => ( - - {role} - - )) + user.roles.map((role) => { + const isProtected = isRoleProtected(role); + return ( + + {role} {isProtected && '🔒'} + + ); + }) ) : ( No roles )} diff --git a/src/app/dashboard/layout.tsx b/src/app/dashboard/layout.tsx index 16056a1..5e8fd99 100644 --- a/src/app/dashboard/layout.tsx +++ b/src/app/dashboard/layout.tsx @@ -4,6 +4,7 @@ import { useMemo, useState, type ReactNode } from 'react' import { usePathname } from 'next/navigation' import ThemeToggle from '@/app/components/ThemeToggle' import NotificationBell from '@/app/components/NotificationBell' +import RoleSwitcher from '@/app/components/RoleSwitcher' import { DashboardProvider, useDashboard } from '@/app/contexts/DashboardContext' import { NotificationProvider } from '@/app/contexts/NotificationContext' import { authService } from '@/services/authService' @@ -37,11 +38,11 @@ function DashboardShellWithNotifications({ children }: { children: ReactNode }) } function DashboardShell({ children }: { children: ReactNode }) { - const { profile, loading, roles } = useDashboard() + const { profile, loading, roles, activeRole, setActiveRole } = useDashboard() const pathname = usePathname() const [mobileNavOpen, setMobileNavOpen] = useState(false) - const navSections = useMemo(() => buildNavigation(roles), [roles]) + const navSections = useMemo(() => buildNavigation(activeRole ? [activeRole] : roles), [roles, activeRole]) const handleLogout = () => { authService.logout() window.location.href = '/auth/login' @@ -115,6 +116,7 @@ function DashboardShell({ children }: { children: ReactNode }) {

    Dashboard

    +
    {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

    +
    + +
    + +