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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion client/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { AnalyticsPage } from './pages/AnalyticsPage';
import { ClientTrackerPage } from './pages/ClientTrackerPage';
import { ClientDetailsPage } from './pages/ClientDetailsPage';
import { AdminPerformancePage } from './pages/AdminPerformancePage';
import { AdminUserManagementPage } from './pages/AdminUserManagementPage';
import { MeetingTrackerPage } from './pages/MeetingTrackerPage';
import { UniversalCalendarPage } from './pages/UniversalCalendarPage';
import { MyDashboardPage } from './pages/MyDashboardPage';
Expand Down Expand Up @@ -215,7 +216,7 @@ const AppRoutes = () => {
</OperationalRoute>
} />

{/* Analytics (Super Admin Only) */}
{/* Analytics & User Management (Super Admin Only) */}
<Route path="/reports" element={
<SuperAdminRoute>
<AnalyticsPage title="Reports" />
Expand All @@ -226,6 +227,11 @@ const AppRoutes = () => {
<AdminPerformancePage />
</SuperAdminRoute>
} />
<Route path="/admin/users" element={
<SuperAdminRoute>
<AdminUserManagementPage />
</SuperAdminRoute>
} />

{/* System Routes */}
<Route path="/unauthorized" element={<UnauthorizedPage />} />
Expand Down
166 changes: 166 additions & 0 deletions client/components/admin/CreateUserModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@

import React, { useState } from 'react';
import { X, Save, User, Mail, Lock, Shield } from 'lucide-react';
import { authApi } from '../../services/api';
import { useToast } from '../../context/ToastContext';
import { CustomSelect } from '../ui/CustomSelect';

interface CreateUserModalProps {
isOpen: boolean;
onClose: () => void;
onSuccess: () => void;
}

const ROLE_OPTIONS = [
{ label: 'Employee', value: 'ROLE_EMPLOYEE' },
{ label: 'Admin', value: 'ROLE_ADMIN' },
{ label: 'Super Admin', value: 'ROLE_SUPER_ADMIN' },
];

export const CreateUserModal: React.FC<CreateUserModalProps> = ({ isOpen, onClose, onSuccess }) => {
const { showToast } = useToast();
const [formData, setFormData] = useState({
name: '',
email: '',
password: '',
role: 'ROLE_EMPLOYEE'
});
const [isLoading, setIsLoading] = useState(false);

if (!isOpen) return null;

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsLoading(true);

try {
await authApi.registerUser(formData);
showToast(`User ${formData.name} created successfully`, 'success');
// Reset form
setFormData({
name: '',
email: '',
password: '',
role: 'ROLE_EMPLOYEE'
});
onSuccess();
onClose();
} catch (err: any) {
showToast(err.message || 'Failed to create user', 'error');
} finally {
setIsLoading(false);
}
};

return (
<div className="fixed inset-0 z-[70] flex items-center justify-center bg-black/60 backdrop-blur-sm p-4 animate-in fade-in duration-200" onClick={onClose}>
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-lg overflow-hidden flex flex-col transform transition-all scale-100" onClick={(e) => e.stopPropagation()}>

{/* Header */}
<div className="flex items-center justify-between p-5 border-b border-gray-100 bg-gray-50/50">
<h2 className="text-xl font-bold text-gray-900 flex items-center gap-2">
<User className="h-5 w-5 text-brand-600" /> Create New User
</h2>
<button onClick={onClose} className="text-gray-400 hover:text-gray-600 p-2 rounded-full hover:bg-gray-200 transition-colors">
<X className="h-5 w-5" />
</button>
</div>

{/* Content */}
<div className="p-6">
<form onSubmit={handleSubmit} className="space-y-5">

{/* Name */}
<div>
<label className="block text-xs font-bold text-gray-500 uppercase tracking-wide mb-1.5">Full Name <span className="text-red-500">*</span></label>
<div className="relative">
<User className="absolute left-3 top-3 h-4 w-4 text-gray-400" />
<input
type="text"
required
className="w-full pl-10 pr-4 py-2.5 bg-white border border-gray-300 rounded-xl text-sm focus:ring-2 focus:ring-brand-500 focus:outline-none transition-all"
placeholder="John Doe"
value={formData.name}
onChange={e => setFormData({...formData, name: e.target.value})}
/>
</div>
</div>

{/* Email */}
<div>
<label className="block text-xs font-bold text-gray-500 uppercase tracking-wide mb-1.5">Email Address <span className="text-red-500">*</span></label>
<div className="relative">
<Mail className="absolute left-3 top-3 h-4 w-4 text-gray-400" />
<input
type="email"
required
className="w-full pl-10 pr-4 py-2.5 bg-white border border-gray-300 rounded-xl text-sm focus:ring-2 focus:ring-brand-500 focus:outline-none transition-all"
placeholder="john@incial.com"
value={formData.email}
onChange={e => setFormData({...formData, email: e.target.value})}
/>
</div>
</div>

{/* Password */}
<div>
<label className="block text-xs font-bold text-gray-500 uppercase tracking-wide mb-1.5">Password <span className="text-red-500">*</span></label>
<div className="relative">
<Lock className="absolute left-3 top-3 h-4 w-4 text-gray-400" />
<input
type="password"
required
className="w-full pl-10 pr-4 py-2.5 bg-white border border-gray-300 rounded-xl text-sm focus:ring-2 focus:ring-brand-500 focus:outline-none transition-all"
placeholder="Min 6 characters"
value={formData.password}
onChange={e => setFormData({...formData, password: e.target.value})}
minLength={6}
/>
</div>
</div>

{/* Role */}
<div>
<CustomSelect
label="System Role"
value={formData.role}
onChange={(val) => setFormData({...formData, role: val})}
options={ROLE_OPTIONS}
required
/>
<p className="text-xs text-gray-400 mt-1.5 flex items-center gap-1">
<Shield className="h-3 w-3" />
Assign appropriate permissions.
</p>
</div>

{/* Footer */}
<div className="flex justify-end gap-3 pt-4 border-t border-gray-50 mt-2">
<button
type="button"
onClick={onClose}
className="px-5 py-2.5 text-gray-600 bg-gray-100 hover:bg-gray-200 rounded-xl font-medium transition-colors"
>
Cancel
</button>
<button
type="submit"
disabled={isLoading}
className="px-5 py-2.5 text-white bg-brand-600 hover:bg-brand-700 rounded-xl font-medium shadow-lg shadow-brand-500/20 flex items-center gap-2 transition-colors disabled:opacity-70"
>
{isLoading ? (
<span className="flex items-center gap-2">Creating...</span>
) : (
<>
<Save className="h-4 w-4" /> Create User
</>
)}
</button>
</div>

</form>
</div>
</div>
</div>
);
};
135 changes: 135 additions & 0 deletions client/components/admin/UserProfileModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@

import React from 'react';
import { X, User, Mail, Calendar, Shield, Trash2, MapPin } from 'lucide-react';
import { User as UserType } from '../../types';
import { useAuth } from '../../context/AuthContext';
import { CustomSelect } from '../ui/CustomSelect';

interface UserProfileModalProps {
isOpen: boolean;
onClose: () => void;
user: UserType | null;
onDeleteRequest: (user: UserType) => void;
onRoleUpdate: (userId: number, newRole: string) => void;
}

const ROLE_OPTIONS = [
{ label: 'Super Admin', value: 'ROLE_SUPER_ADMIN' },
{ label: 'Admin', value: 'ROLE_ADMIN' },
{ label: 'Employee', value: 'ROLE_EMPLOYEE' },
{ label: 'Client', value: 'ROLE_CLIENT' },
];

export const UserProfileModal: React.FC<UserProfileModalProps> = ({ isOpen, onClose, user, onDeleteRequest, onRoleUpdate }) => {
const { user: currentUser } = useAuth();

if (!isOpen || !user) return null;

const isSelf = currentUser?.id === user.id;

const getRoleBadge = (role: string) => {
let styles = 'bg-gray-100 text-gray-600 border-gray-200';
if (role === 'ROLE_SUPER_ADMIN') styles = 'bg-yellow-50 text-yellow-700 border-yellow-200';
if (role === 'ROLE_ADMIN') styles = 'bg-purple-50 text-purple-700 border-purple-200';
if (role === 'ROLE_EMPLOYEE') styles = 'bg-blue-50 text-blue-700 border-blue-200';

const label = role.replace('ROLE_', '').replace('_', ' ');
return <span className={`inline-flex items-center gap-1.5 px-3 py-1 rounded-lg text-xs font-bold uppercase tracking-wide border ${styles}`}>{label}</span>;
};

return (
<div className="fixed inset-0 z-[70] flex items-center justify-center bg-black/60 backdrop-blur-sm p-4 animate-in fade-in duration-200" onClick={onClose}>
<div className="bg-white rounded-[2rem] shadow-2xl w-full max-w-md overflow-hidden flex flex-col transform transition-all scale-100 relative border border-gray-100" onClick={(e) => e.stopPropagation()}>

{/* Close Button - Z-Index 50 to stay above banner */}
<button
onClick={onClose}
className="absolute top-4 right-4 z-50 p-2 bg-black/20 hover:bg-black/30 text-white rounded-full transition-colors backdrop-blur-md"
>
<X className="h-5 w-5" />
</button>

{/* Cover Background */}
<div className="h-48 relative bg-gradient-to-r from-brand-600 to-indigo-600 shrink-0">
<img
src="/banner.png"
alt="Profile Banner"
className="w-full h-full object-cover absolute inset-0 z-10"
onError={(e) => e.currentTarget.style.display = 'none'}
/>
{/* Avatar - Absolute to banner but hanging off bottom */}
<div className="absolute -bottom-12 left-8 z-30">
<div className="h-24 w-24 rounded-3xl bg-white p-1.5 shadow-xl border border-gray-100">
{user.avatarUrl ? (
<img src={user.avatarUrl} alt={user.name} className="h-full w-full rounded-2xl object-cover bg-gray-100" />
) : (
<div className="h-full w-full rounded-2xl bg-gradient-to-br from-gray-100 to-gray-200 flex items-center justify-center text-3xl font-bold text-gray-400 uppercase tracking-tighter">
{user.name.charAt(0)}
</div>
)}
</div>
</div>
</div>

{/* User Info - Added top padding to account for avatar overlap */}
<div className="pt-16 pb-8 px-8">
<div className="mb-6">
<div className="flex flex-col gap-4">
<div>
<h2 className="text-2xl font-bold text-gray-900">{user.name}</h2>
<div className="mt-3">
{isSelf ? (
<div title="You cannot change your own role.">
{getRoleBadge(user.role)}
</div>
) : (
<div className="w-full">
<CustomSelect
value={user.role}
onChange={(val) => onRoleUpdate(user.id, val)}
options={ROLE_OPTIONS}
placeholder="Select Role"
className="w-full"
/>
</div>
)}
</div>
</div>
</div>

<div className="mt-6 space-y-3">
<div className="flex items-center gap-3 text-sm text-gray-600 p-3.5 bg-gray-50 rounded-2xl border border-gray-100">
<Mail className="h-5 w-5 text-gray-400" />
<span className="font-medium">{user.email}</span>
</div>

<div className="flex items-center gap-3 text-sm text-gray-600 p-3.5 bg-gray-50 rounded-2xl border border-gray-100">
<Calendar className="h-5 w-5 text-gray-400" />
<span>Joined {user.createdAt ? new Date(user.createdAt).toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' }) : 'Unknown'}</span>
</div>

<div className="flex items-center gap-3 text-sm text-gray-600 p-3.5 bg-gray-50 rounded-2xl border border-gray-100">
<Shield className="h-5 w-5 text-gray-400" />
<span>System ID: <span className="font-mono text-xs font-bold bg-gray-200 px-2 py-0.5 rounded text-gray-600">{user.id}</span></span>
</div>
</div>
</div>

{/* Actions */}
<div className="pt-6 border-t border-gray-100 flex justify-end">
{isSelf ? (
<div className="text-xs text-gray-400 italic">You cannot delete your own account.</div>
) : (
<button
onClick={() => onDeleteRequest(user)}
className="flex items-center gap-2 px-5 py-2.5 bg-red-50 text-red-600 hover:bg-red-100 rounded-xl font-bold text-sm transition-colors border border-red-100"
>
<Trash2 className="h-4 w-4" /> Delete User
</button>
)}
</div>
</div>
</div>
</div>
);
};
5 changes: 2 additions & 3 deletions client/components/layout/Sidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@

import React from 'react';
import { Users, Briefcase, Settings, PieChart, ChevronRight, CheckSquare, ListTodo, BarChart2, Calendar, LayoutDashboard, Home, Command } from 'lucide-react';
import { Users, Briefcase, Settings, PieChart, ChevronRight, CheckSquare, ListTodo, BarChart2, Calendar, LayoutDashboard, Home, Command, Shield } from 'lucide-react';
import { Link, useLocation } from 'react-router-dom';
import { useAuth } from '../../context/AuthContext';
import { useLayout } from '../../context/LayoutContext';
Expand Down Expand Up @@ -65,10 +65,8 @@ export const Sidebar: React.FC = () => {
<div className={`h-[88px] flex items-center ${isSidebarCollapsed ? 'justify-center' : 'px-8'} transition-all duration-300`}>
<div className="flex items-center gap-3.5 overflow-hidden whitespace-nowrap group cursor-pointer">
<div className={`relative flex items-center justify-center rounded-xl bg-gradient-to-br from-brand-500 to-brand-700 shadow-lg shadow-brand-500/30 transition-all duration-500 ${isSidebarCollapsed ? 'h-10 w-10' : 'h-9 w-9'}`}>
<div className={`relative flex items-center justify-center rounded-xl bg-gradient-to-br from-brand-500 to-brand-700 shadow-lg shadow-brand-500/30 transition-all duration-500 ${isSidebarCollapsed ? 'h-10 w-10' : 'h-9 w-9'}`}>
<img src="/logo.png" alt="Incial" className="h-9 w-9 rounded-xl bg-white shadow-lg object-contain p-1 flex-shrink-0" />
</div>
</div>

{!isSidebarCollapsed && (
<div className="flex flex-col opacity-100 transition-opacity duration-300">
Expand Down Expand Up @@ -131,6 +129,7 @@ export const Sidebar: React.FC = () => {
<div className="space-y-0.5">
<NavItem collapsed={isSidebarCollapsed} icon={PieChart} label="Reports" to="/reports" active={currentPath === '/reports'} />
<NavItem collapsed={isSidebarCollapsed} icon={BarChart2} label="Performance" to="/admin/performance" active={currentPath === '/admin/performance'} />
<NavItem collapsed={isSidebarCollapsed} icon={Shield} label="Users" to="/admin/users" active={currentPath === '/admin/users'} />
</div>
</div>
)}
Expand Down
2 changes: 1 addition & 1 deletion client/components/ui/DeleteConfirmationModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export const DeleteConfirmationModal: React.FC<DeleteConfirmationModalProps> = (
if (!isOpen) return null;

return (
<div className="fixed inset-0 z-[60] flex items-center justify-center bg-black/60 backdrop-blur-sm p-4 animate-in fade-in duration-200" onClick={onClose}>
<div className="fixed inset-0 z-[100] flex items-center justify-center bg-black/60 backdrop-blur-sm p-4 animate-in fade-in duration-200" onClick={onClose}>
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-md overflow-hidden transform transition-all scale-100 p-6" onClick={(e) => e.stopPropagation()}>

<div className="flex items-start gap-4">
Expand Down
Loading
Loading