(Feat): More changes

This commit is contained in:
2025-11-28 19:04:35 +00:00
parent 25ed1d5c56
commit 8ac2eb1944
42 changed files with 3291 additions and 3407 deletions

View File

@@ -7,9 +7,10 @@ import { UsersPage } from './pages/UsersPage';
import { WorkAllocationPage } from './pages/WorkAllocationPage';
import { AttendancePage } from './pages/AttendancePage';
import { RatesPage } from './pages/RatesPage';
import { EmployeeSwapPage } from './pages/EmployeeSwapPage';
import { LoginPage } from './pages/LoginPage';
type PageType = 'dashboard' | 'users' | 'allocation' | 'attendance' | 'rates';
type PageType = 'dashboard' | 'users' | 'allocation' | 'attendance' | 'rates' | 'swaps';
const AppContent: React.FC = () => {
const [activePage, setActivePage] = useState<PageType>('dashboard');
@@ -27,6 +28,8 @@ const AppContent: React.FC = () => {
return <AttendancePage />;
case 'rates':
return <RatesPage />;
case 'swaps':
return <EmployeeSwapPage />;
default:
return <DashboardPage />;
}

View File

@@ -1,7 +1,9 @@
import React, { useState } from 'react';
import { Bell, LogOut, X, Camera, Shield, User, Mail, Building2, ChevronDown, ChevronUp } from 'lucide-react';
import React, { useState, useEffect } from 'react';
import { Bell, LogOut, X, Camera, Shield, User, Mail, Building2, ChevronDown, ChevronUp, Phone, CreditCard, Landmark, FileText } from 'lucide-react';
import { useAuth } from '../../contexts/AuthContext';
import { useDepartments } from '../../hooks/useDepartments';
import { api } from '../../services/api';
import type { User as UserType } from '../../types';
interface ProfilePopupProps {
isOpen: boolean;
@@ -54,14 +56,25 @@ const ProfilePopup: React.FC<ProfilePopupProps> = ({ isOpen, onClose, onLogout }
const { user } = useAuth();
const { departments } = useDepartments();
const [showPermissions, setShowPermissions] = useState(false);
const [showDetails, setShowDetails] = useState(false);
const [fullUserData, setFullUserData] = useState<UserType | null>(null);
// Fetch full user details when popup opens
useEffect(() => {
if (isOpen && user?.id) {
api.getUser(user.id).then(setFullUserData).catch(console.error);
}
}, [isOpen, user?.id]);
if (!isOpen) return null;
const userDepartment = departments.find(d => d.id === user?.department_id);
const userPermissions = rolePermissions[user?.role || 'Employee'];
const isEmployeeOrContractor = user?.role === 'Employee' || user?.role === 'Contractor';
const isContractor = user?.role === 'Contractor';
return (
<div className="absolute right-4 top-16 w-[380px] bg-gradient-to-b from-slate-50 to-white rounded-2xl shadow-2xl z-50 border border-gray-200 overflow-hidden font-sans text-sm text-gray-800">
<div className="absolute right-4 top-16 w-[400px] bg-gradient-to-b from-slate-50 to-white rounded-2xl shadow-2xl z-50 border border-gray-200 overflow-hidden font-sans text-sm text-gray-800 max-h-[85vh] overflow-y-auto">
{/* Header */}
<div className="bg-gradient-to-r from-teal-600 to-teal-500 px-6 py-4">
<div className="flex justify-between items-start">
@@ -120,6 +133,98 @@ const ProfilePopup: React.FC<ProfilePopupProps> = ({ isOpen, onClose, onLogout }
</div>
)}
{/* Personal & Bank Details Section - for Employee and Contractor */}
{isEmployeeOrContractor && (
<button
onClick={() => setShowDetails(!showDetails)}
className="w-full flex items-center justify-between p-3 bg-teal-50 hover:bg-teal-100 rounded-xl transition-colors"
>
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-teal-100 rounded-full flex items-center justify-center">
<CreditCard size={18} className="text-teal-600" />
</div>
<div className="text-left">
<p className="text-xs text-gray-500 font-medium">Personal & Bank Details</p>
<p className="text-sm font-semibold text-gray-800">View your information</p>
</div>
</div>
{showDetails ? <ChevronUp size={18} className="text-teal-600" /> : <ChevronDown size={18} className="text-teal-600" />}
</button>
)}
{showDetails && isEmployeeOrContractor && fullUserData && (
<div className="bg-teal-50 rounded-xl p-4 border border-teal-200 space-y-4">
{/* Personal Details */}
<div>
<h4 className="font-semibold text-teal-800 mb-2 flex items-center gap-2">
<Phone size={14} /> Personal Details
</h4>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-gray-600">Phone Number</span>
<span className="font-medium text-gray-800">{fullUserData.phone_number || 'Not provided'}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">Aadhar Number</span>
<span className="font-medium text-gray-800">
{fullUserData.aadhar_number
? `XXXX-XXXX-${fullUserData.aadhar_number.slice(-4)}`
: 'Not provided'}
</span>
</div>
</div>
</div>
{/* Bank Details */}
<div>
<h4 className="font-semibold text-teal-800 mb-2 flex items-center gap-2">
<Landmark size={14} /> Bank Details
</h4>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-gray-600">Bank Name</span>
<span className="font-medium text-gray-800">{fullUserData.bank_name || 'Not provided'}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">Account Number</span>
<span className="font-medium text-gray-800">
{fullUserData.bank_account_number
? `XXXX${fullUserData.bank_account_number.slice(-4)}`
: 'Not provided'}
</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">IFSC Code</span>
<span className="font-medium text-gray-800">{fullUserData.bank_ifsc || 'Not provided'}</span>
</div>
</div>
</div>
{/* Contractor-specific Details */}
{isContractor && (
<div>
<h4 className="font-semibold text-teal-800 mb-2 flex items-center gap-2">
<FileText size={14} /> Contractor Details
</h4>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-gray-600">Agreement No.</span>
<span className="font-medium text-gray-800">{fullUserData.contractor_agreement_number || 'Not provided'}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">PF Number</span>
<span className="font-medium text-gray-800">{fullUserData.pf_number || 'Not provided'}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">ESIC Number</span>
<span className="font-medium text-gray-800">{fullUserData.esic_number || 'Not provided'}</span>
</div>
</div>
</div>
)}
</div>
)}
{/* Permissions Section */}
<button
onClick={() => setShowPermissions(!showPermissions)}

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { LayoutDashboard, Users, Briefcase, CalendarCheck, DollarSign, ClipboardList } from 'lucide-react';
import { LayoutDashboard, Users, Briefcase, CalendarCheck, DollarSign, ClipboardList, ArrowRightLeft } from 'lucide-react';
import { useAuth } from '../../contexts/AuthContext';
interface SidebarItemProps {
@@ -30,7 +30,16 @@ interface SidebarProps {
export const Sidebar: React.FC<SidebarProps> = ({ activePage, onNavigate }) => {
const { user } = useAuth();
const canManageRates = user?.role === 'SuperAdmin' || user?.role === 'Supervisor';
const isSuperAdmin = user?.role === 'SuperAdmin';
const isSupervisor = user?.role === 'Supervisor';
const isContractor = user?.role === 'Contractor';
const isEmployee = user?.role === 'Employee';
// Role-based access
const canManageUsers = isSuperAdmin || isSupervisor;
const canManageAllocations = isSuperAdmin || isSupervisor;
const canManageAttendance = isSuperAdmin || isSupervisor;
const canManageRates = isSuperAdmin || isSupervisor;
return (
<div className="w-64 bg-[#1e293b] flex flex-col">
@@ -46,30 +55,45 @@ export const Sidebar: React.FC<SidebarProps> = ({ activePage, onNavigate }) => {
</div>
</div>
<nav className="flex-1 py-4">
{/* Dashboard - visible to all */}
<SidebarItem
icon={LayoutDashboard}
label="Dashboard"
active={activePage === 'dashboard'}
onClick={() => onNavigate('dashboard')}
/>
<SidebarItem
icon={Users}
label="User Management"
active={activePage === 'users'}
onClick={() => onNavigate('users')}
/>
<SidebarItem
icon={Briefcase}
label="Work Allocation"
active={activePage === 'allocation'}
onClick={() => onNavigate('allocation')}
/>
<SidebarItem
icon={CalendarCheck}
label="Attendance"
active={activePage === 'attendance'}
onClick={() => onNavigate('attendance')}
/>
{/* User Management - SuperAdmin and Supervisor only */}
{canManageUsers && (
<SidebarItem
icon={Users}
label="User Management"
active={activePage === 'users'}
onClick={() => onNavigate('users')}
/>
)}
{/* Work Allocation - SuperAdmin and Supervisor only */}
{canManageAllocations && (
<SidebarItem
icon={Briefcase}
label="Work Allocation"
active={activePage === 'allocation'}
onClick={() => onNavigate('allocation')}
/>
)}
{/* Attendance - SuperAdmin and Supervisor only */}
{canManageAttendance && (
<SidebarItem
icon={CalendarCheck}
label="Attendance"
active={activePage === 'attendance'}
onClick={() => onNavigate('attendance')}
/>
)}
{/* Contractor Rates - SuperAdmin and Supervisor only */}
{canManageRates && (
<SidebarItem
icon={DollarSign}
@@ -78,7 +102,30 @@ export const Sidebar: React.FC<SidebarProps> = ({ activePage, onNavigate }) => {
onClick={() => onNavigate('rates')}
/>
)}
{/* Employee Swap - SuperAdmin only */}
{isSuperAdmin && (
<SidebarItem
icon={ArrowRightLeft}
label="Employee Swap"
active={activePage === 'swaps'}
onClick={() => onNavigate('swaps')}
/>
)}
</nav>
{/* Role indicator at bottom */}
<div className="p-4 border-t border-gray-700">
<div className="text-xs text-gray-500 uppercase tracking-wide mb-1">Logged in as</div>
<div className={`text-sm font-medium ${
isSuperAdmin ? 'text-purple-400' :
isSupervisor ? 'text-blue-400' :
isContractor ? 'text-orange-400' :
isEmployee ? 'text-green-400' : 'text-gray-400'
}`}>
{user?.role || 'Unknown'}
</div>
</div>
</div>
);
};

View File

@@ -1,4 +1,5 @@
import React, { InputHTMLAttributes } from 'react';
import React, { InputHTMLAttributes, useState } from 'react';
import { Eye, EyeOff } from 'lucide-react';
interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
label?: string;
@@ -26,6 +27,45 @@ export const Input: React.FC<InputProps> = ({ label, error, required, className
);
};
interface PasswordInputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'type'> {
label?: string;
error?: string;
required?: boolean;
}
export const PasswordInput: React.FC<PasswordInputProps> = ({ label, error, required, className = '', disabled, ...props }) => {
const [showPassword, setShowPassword] = useState(false);
return (
<div className="w-full">
{label && (
<label className="block text-sm font-medium text-gray-700 mb-2">
{label} {required && <span className="text-red-500">*</span>}
</label>
)}
<div className="relative">
<input
type={showPassword ? 'text' : 'password'}
className={`w-full px-4 py-2 pr-10 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent ${
error ? 'border-red-500' : ''
} ${disabled ? 'bg-gray-100 text-gray-600 cursor-not-allowed' : ''} ${className}`}
disabled={disabled}
{...props}
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 transition-colors"
tabIndex={-1}
>
{showPassword ? <EyeOff size={18} /> : <Eye size={18} />}
</button>
</div>
{error && <p className="mt-1 text-sm text-red-600">{error}</p>}
</div>
);
};
interface SelectProps extends InputHTMLAttributes<HTMLSelectElement> {
label?: string;
error?: string;

View File

@@ -12,7 +12,6 @@ interface AuthContextType {
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export const useAuth = () => {
const context = useContext(AuthContext);
if (!context) {

View File

@@ -1,11 +1,13 @@
import React, { useState, useEffect, useMemo } from 'react';
import { AlertTriangle, CheckCircle, Clock, RefreshCw, LogIn, LogOut, Search, ArrowUpDown, ArrowUp, ArrowDown } from 'lucide-react';
import { AlertTriangle, CheckCircle, Clock, RefreshCw, LogIn, LogOut, Search, ArrowUpDown, ArrowUp, ArrowDown, UserX, Edit2, X } from 'lucide-react';
import { Card, CardContent } from '../components/ui/Card';
import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from '../components/ui/Table';
import { Button } from '../components/ui/Button';
import { Select, Input } from '../components/ui/Input';
import { api } from '../services/api';
import { useEmployees } from '../hooks/useEmployees';
import { useAuth } from '../contexts/AuthContext';
import type { AttendanceStatus } from '../types';
export const AttendancePage: React.FC = () => {
const [activeTab, setActiveTab] = useState<'records' | 'checkin'>('records');
@@ -22,6 +24,10 @@ export const AttendancePage: React.FC = () => {
const [searchQuery, setSearchQuery] = useState('');
const [sortField, setSortField] = useState<'date' | 'employee' | 'status'>('date');
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc');
const [editingRecord, setEditingRecord] = useState<number | null>(null);
const [editStatus, setEditStatus] = useState<AttendanceStatus>('CheckedIn');
const [editRemark, setEditRemark] = useState('');
const { user } = useAuth();
// Fetch attendance records
const fetchAttendance = async () => {
@@ -88,6 +94,47 @@ export const AttendancePage: React.FC = () => {
}
};
const handleMarkAbsent = async () => {
if (!selectedEmployee) {
alert('Please select an employee');
return;
}
setCheckInLoading(true);
try {
await api.markAbsent(parseInt(selectedEmployee), workDate, 'Marked absent by supervisor');
await fetchAttendance();
setEmployeeStatus({ status: 'Absent' });
} catch (err: any) {
alert(err.message || 'Failed to mark absent');
} finally {
setCheckInLoading(false);
}
};
const handleUpdateStatus = async (recordId: number) => {
try {
await api.updateAttendanceStatus(recordId, editStatus, editRemark);
await fetchAttendance();
setEditingRecord(null);
setEditRemark('');
} catch (err: any) {
alert(err.message || 'Failed to update status');
}
};
const startEditing = (record: any) => {
setEditingRecord(record.id);
setEditStatus(record.status);
setEditRemark(record.remark || '');
};
const cancelEditing = () => {
setEditingRecord(null);
setEditRemark('');
};
const canEditAttendance = user?.role === 'SuperAdmin' || user?.role === 'Supervisor';
const employeeOptions = [
{ value: '', label: 'Select Employee' },
...employees.filter(e => e.role === 'Employee').map(e => ({
@@ -233,6 +280,8 @@ export const AttendancePage: React.FC = () => {
Status <SortIcon field="status" />
</button>
</TableHead>
<TableHead>Remark</TableHead>
{canEditAttendance && <TableHead>Actions</TableHead>}
</TableHeader>
<TableBody>
{filteredAndSortedAttendance.map((record) => (
@@ -251,15 +300,76 @@ export const AttendancePage: React.FC = () => {
: '-'}
</TableCell>
<TableCell>
<span className={`px-2 py-1 rounded text-xs font-medium ${
record.status === 'CheckedOut' ? 'bg-green-100 text-green-700' :
record.status === 'CheckedIn' ? 'bg-blue-100 text-blue-700' :
'bg-gray-100 text-gray-700'
}`}>
{record.status === 'CheckedOut' ? 'Completed' :
record.status === 'CheckedIn' ? 'Checked In' : record.status}
</span>
{editingRecord === record.id ? (
<select
value={editStatus}
onChange={(e) => setEditStatus(e.target.value as AttendanceStatus)}
className="px-2 py-1 border border-gray-300 rounded text-sm"
>
<option value="CheckedIn">Checked In</option>
<option value="CheckedOut">Checked Out</option>
<option value="Absent">Absent</option>
<option value="HalfDay">Half Day</option>
<option value="Late">Late</option>
</select>
) : (
<span className={`px-2 py-1 rounded text-xs font-medium ${
record.status === 'CheckedOut' ? 'bg-green-100 text-green-700' :
record.status === 'CheckedIn' ? 'bg-blue-100 text-blue-700' :
record.status === 'Absent' ? 'bg-red-100 text-red-700' :
record.status === 'HalfDay' ? 'bg-orange-100 text-orange-700' :
record.status === 'Late' ? 'bg-yellow-100 text-yellow-700' :
'bg-gray-100 text-gray-700'
}`}>
{record.status === 'CheckedOut' ? 'Completed' :
record.status === 'CheckedIn' ? 'Checked In' :
record.status === 'HalfDay' ? 'Half Day' : record.status}
</span>
)}
</TableCell>
<TableCell>
{editingRecord === record.id ? (
<input
type="text"
value={editRemark}
onChange={(e) => setEditRemark(e.target.value)}
placeholder="Add remark..."
className="px-2 py-1 border border-gray-300 rounded text-sm w-32"
/>
) : (
<span className="text-gray-500 text-sm">{record.remark || '-'}</span>
)}
</TableCell>
{canEditAttendance && (
<TableCell>
{editingRecord === record.id ? (
<div className="flex gap-1">
<button
onClick={() => handleUpdateStatus(record.id)}
className="p-1 text-green-600 hover:bg-green-50 rounded"
title="Save"
>
<CheckCircle size={16} />
</button>
<button
onClick={cancelEditing}
className="p-1 text-red-600 hover:bg-red-50 rounded"
title="Cancel"
>
<X size={16} />
</button>
</div>
) : (
<button
onClick={() => startEditing(record)}
className="p-1 text-blue-600 hover:bg-blue-50 rounded"
title="Edit Status"
>
<Edit2 size={16} />
</button>
)}
</TableCell>
)}
</TableRow>
))}
</TableBody>
@@ -332,7 +442,7 @@ export const AttendancePage: React.FC = () => {
<Button
size="lg"
onClick={handleCheckIn}
disabled={checkInLoading || !selectedEmployee || employeeStatus?.status === 'CheckedIn' || employeeStatus?.status === 'CheckedOut'}
disabled={checkInLoading || !selectedEmployee || employeeStatus?.status === 'CheckedIn' || employeeStatus?.status === 'CheckedOut' || employeeStatus?.status === 'Absent'}
>
<LogIn size={16} className="mr-2" />
{checkInLoading ? 'Processing...' : 'Check In'}
@@ -346,6 +456,15 @@ export const AttendancePage: React.FC = () => {
<LogOut size={16} className="mr-2" />
{checkInLoading ? 'Processing...' : 'Check Out'}
</Button>
<Button
size="lg"
variant="danger"
onClick={handleMarkAbsent}
disabled={checkInLoading || !selectedEmployee || employeeStatus?.status === 'CheckedIn' || employeeStatus?.status === 'CheckedOut' || employeeStatus?.status === 'Absent'}
>
<UserX size={16} className="mr-2" />
{checkInLoading ? 'Processing...' : 'Mark Absent'}
</Button>
</div>
</div>
</div>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,594 @@
import React, { useState, useEffect } from 'react';
import {
ArrowRightLeft,
Plus,
CheckCircle,
XCircle,
Clock,
Building2,
User,
AlertCircle,
RefreshCw,
Search,
Filter
} from 'lucide-react';
import { Card, CardContent } from '../components/ui/Card';
import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from '../components/ui/Table';
import { Button } from '../components/ui/Button';
import { Select, Input } from '../components/ui/Input';
import { api } from '../services/api';
import { useEmployees } from '../hooks/useEmployees';
import { useDepartments } from '../hooks/useDepartments';
import type { EmployeeSwap, SwapReason, SwapStatus } from '../types';
export const EmployeeSwapPage: React.FC = () => {
const [activeTab, setActiveTab] = useState<'list' | 'create'>('list');
const [swaps, setSwaps] = useState<EmployeeSwap[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [searchQuery, setSearchQuery] = useState('');
const [statusFilter, setStatusFilter] = useState<SwapStatus | ''>('');
const { employees } = useEmployees();
const { departments } = useDepartments();
// Form state
const [formData, setFormData] = useState({
employeeId: '',
targetDepartmentId: '',
targetContractorId: '',
swapReason: '' as SwapReason | '',
reasonDetails: '',
workCompletionPercentage: 0,
swapDate: new Date().toISOString().split('T')[0],
});
const [submitting, setSubmitting] = useState(false);
const fetchSwaps = async () => {
setLoading(true);
setError('');
try {
const params: { status?: string } = {};
if (statusFilter) params.status = statusFilter;
const data = await api.getEmployeeSwaps(params);
setSwaps(data);
} catch (err: any) {
setError(err.message || 'Failed to fetch swaps');
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchSwaps();
}, [statusFilter]);
const handleCreateSwap = async (e: React.FormEvent) => {
e.preventDefault();
if (!formData.employeeId || !formData.targetDepartmentId || !formData.swapReason) {
alert('Please fill in all required fields');
return;
}
setSubmitting(true);
try {
await api.createEmployeeSwap({
employeeId: parseInt(formData.employeeId),
targetDepartmentId: parseInt(formData.targetDepartmentId),
targetContractorId: formData.targetContractorId ? parseInt(formData.targetContractorId) : undefined,
swapReason: formData.swapReason as SwapReason,
reasonDetails: formData.reasonDetails || undefined,
workCompletionPercentage: formData.workCompletionPercentage,
swapDate: formData.swapDate,
});
// Reset form and switch to list
setFormData({
employeeId: '',
targetDepartmentId: '',
targetContractorId: '',
swapReason: '',
reasonDetails: '',
workCompletionPercentage: 0,
swapDate: new Date().toISOString().split('T')[0],
});
setActiveTab('list');
await fetchSwaps();
} catch (err: any) {
alert(err.message || 'Failed to create swap');
} finally {
setSubmitting(false);
}
};
const handleCompleteSwap = async (id: number) => {
if (!confirm('Complete this swap and return employee to original department?')) return;
try {
await api.completeEmployeeSwap(id);
await fetchSwaps();
} catch (err: any) {
alert(err.message || 'Failed to complete swap');
}
};
const handleCancelSwap = async (id: number) => {
if (!confirm('Cancel this swap and return employee to original department?')) return;
try {
await api.cancelEmployeeSwap(id);
await fetchSwaps();
} catch (err: any) {
alert(err.message || 'Failed to cancel swap');
}
};
// Filter employees (only show employees)
const employeeList = employees.filter(e => e.role === 'Employee');
// Get contractors for selected target department
const targetContractors = employees.filter(
e => e.role === 'Contractor' &&
e.department_id === parseInt(formData.targetDepartmentId)
);
// Get selected employee details
const selectedEmployee = employeeList.find(e => e.id === parseInt(formData.employeeId));
// Filter swaps based on search
const filteredSwaps = swaps.filter(swap => {
if (!searchQuery) return true;
const query = searchQuery.toLowerCase();
return (
swap.employee_name?.toLowerCase().includes(query) ||
swap.original_department_name?.toLowerCase().includes(query) ||
swap.target_department_name?.toLowerCase().includes(query)
);
});
const getStatusBadge = (status: SwapStatus) => {
switch (status) {
case 'Active':
return <span className="px-2 py-1 rounded text-xs font-medium bg-blue-100 text-blue-700">Active</span>;
case 'Completed':
return <span className="px-2 py-1 rounded text-xs font-medium bg-green-100 text-green-700">Completed</span>;
case 'Cancelled':
return <span className="px-2 py-1 rounded text-xs font-medium bg-red-100 text-red-700">Cancelled</span>;
}
};
const getReasonBadge = (reason: SwapReason) => {
const colors: Record<SwapReason, string> = {
'LeftWork': 'bg-orange-100 text-orange-700',
'Sick': 'bg-red-100 text-red-700',
'FinishedEarly': 'bg-green-100 text-green-700',
'Other': 'bg-gray-100 text-gray-700',
};
const labels: Record<SwapReason, string> = {
'LeftWork': 'Left Work',
'Sick': 'Sick',
'FinishedEarly': 'Finished Early',
'Other': 'Other',
};
return <span className={`px-2 py-1 rounded text-xs font-medium ${colors[reason]}`}>{labels[reason]}</span>;
};
return (
<div className="p-6">
<Card>
{/* Header */}
<div className="p-6 border-b border-gray-200">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="p-2 bg-purple-100 rounded-lg">
<ArrowRightLeft className="text-purple-600" size={24} />
</div>
<div>
<h1 className="text-xl font-bold text-gray-800">Employee Work Swap</h1>
<p className="text-sm text-gray-500">Transfer employees between departments temporarily</p>
</div>
</div>
</div>
</div>
{/* Tabs */}
<div className="border-b border-gray-200">
<div className="flex space-x-8 px-6">
<button
onClick={() => setActiveTab('list')}
className={`py-4 px-2 border-b-2 font-medium text-sm ${
activeTab === 'list'
? 'border-purple-500 text-purple-600'
: 'border-transparent text-gray-500 hover:text-gray-700'
}`}
>
Swap History
</button>
<button
onClick={() => setActiveTab('create')}
className={`py-4 px-2 border-b-2 font-medium text-sm flex items-center gap-2 ${
activeTab === 'create'
? 'border-purple-500 text-purple-600'
: 'border-transparent text-gray-500 hover:text-gray-700'
}`}
>
<Plus size={16} />
New Swap
</button>
</div>
</div>
<CardContent>
{activeTab === 'list' && (
<div>
{/* Filters */}
<div className="flex gap-4 mb-6">
<div className="relative flex-1 max-w-md">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400" size={18} />
<input
type="text"
placeholder="Search by employee or department..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-purple-500 focus:border-transparent"
/>
</div>
<div className="flex items-center gap-2">
<Filter size={18} className="text-gray-400" />
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value as SwapStatus | '')}
className="px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-purple-500"
>
<option value="">All Status</option>
<option value="Active">Active</option>
<option value="Completed">Completed</option>
<option value="Cancelled">Cancelled</option>
</select>
</div>
<Button variant="ghost" onClick={fetchSwaps}>
<RefreshCw size={16} className="mr-2" />
Refresh
</Button>
</div>
{/* Stats Cards */}
<div className="grid grid-cols-4 gap-4 mb-6">
<div className="bg-blue-50 rounded-lg p-4">
<div className="flex items-center gap-2 text-blue-600 mb-1">
<Clock size={18} />
<span className="text-sm font-medium">Active</span>
</div>
<div className="text-2xl font-bold text-blue-700">
{swaps.filter(s => s.status === 'Active').length}
</div>
</div>
<div className="bg-green-50 rounded-lg p-4">
<div className="flex items-center gap-2 text-green-600 mb-1">
<CheckCircle size={18} />
<span className="text-sm font-medium">Completed</span>
</div>
<div className="text-2xl font-bold text-green-700">
{swaps.filter(s => s.status === 'Completed').length}
</div>
</div>
<div className="bg-red-50 rounded-lg p-4">
<div className="flex items-center gap-2 text-red-600 mb-1">
<XCircle size={18} />
<span className="text-sm font-medium">Cancelled</span>
</div>
<div className="text-2xl font-bold text-red-700">
{swaps.filter(s => s.status === 'Cancelled').length}
</div>
</div>
<div className="bg-purple-50 rounded-lg p-4">
<div className="flex items-center gap-2 text-purple-600 mb-1">
<ArrowRightLeft size={18} />
<span className="text-sm font-medium">Total Swaps</span>
</div>
<div className="text-2xl font-bold text-purple-700">{swaps.length}</div>
</div>
</div>
{error && (
<div className="mb-4 p-4 bg-red-50 border border-red-200 rounded-lg text-red-700">
{error}
</div>
)}
{loading ? (
<div className="text-center py-8">
<div className="inline-block animate-spin rounded-full h-6 w-6 border-b-2 border-purple-600"></div>
<span className="ml-2 text-gray-600">Loading swaps...</span>
</div>
) : filteredSwaps.length > 0 ? (
<Table>
<TableHeader>
<TableHead>Employee</TableHead>
<TableHead>From To</TableHead>
<TableHead>Reason</TableHead>
<TableHead>Completion %</TableHead>
<TableHead>Swap Date</TableHead>
<TableHead>Status</TableHead>
<TableHead>Actions</TableHead>
</TableHeader>
<TableBody>
{filteredSwaps.map((swap) => (
<TableRow key={swap.id}>
<TableCell>
<div className="flex items-center gap-2">
<div className="w-8 h-8 bg-purple-100 rounded-full flex items-center justify-center">
<User size={16} className="text-purple-600" />
</div>
<div>
<div className="font-medium text-gray-800">{swap.employee_name}</div>
<div className="text-xs text-gray-500">
{swap.original_contractor_name && `Under: ${swap.original_contractor_name}`}
</div>
</div>
</div>
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<span className="text-gray-600">{swap.original_department_name}</span>
<ArrowRightLeft size={14} className="text-gray-400" />
<span className="font-medium text-purple-600">{swap.target_department_name}</span>
</div>
{swap.target_contractor_name && (
<div className="text-xs text-gray-500 mt-1">
New contractor: {swap.target_contractor_name}
</div>
)}
</TableCell>
<TableCell>
<div className="space-y-1">
{getReasonBadge(swap.swap_reason)}
{swap.reason_details && (
<div className="text-xs text-gray-500 max-w-[150px] truncate" title={swap.reason_details}>
{swap.reason_details}
</div>
)}
</div>
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<div className="w-16 bg-gray-200 rounded-full h-2">
<div
className="bg-purple-600 h-2 rounded-full"
style={{ width: `${swap.work_completion_percentage}%` }}
/>
</div>
<span className="text-sm text-gray-600">{swap.work_completion_percentage}%</span>
</div>
</TableCell>
<TableCell>
<div className="text-sm text-gray-600">
{new Date(swap.swap_date).toLocaleDateString()}
</div>
<div className="text-xs text-gray-400">
by {swap.swapped_by_name}
</div>
</TableCell>
<TableCell>{getStatusBadge(swap.status)}</TableCell>
<TableCell>
{swap.status === 'Active' && (
<div className="flex gap-1">
<button
onClick={() => handleCompleteSwap(swap.id)}
className="p-1.5 text-green-600 hover:bg-green-50 rounded"
title="Complete & Return"
>
<CheckCircle size={18} />
</button>
<button
onClick={() => handleCancelSwap(swap.id)}
className="p-1.5 text-red-600 hover:bg-red-50 rounded"
title="Cancel Swap"
>
<XCircle size={18} />
</button>
</div>
)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
) : (
<div className="text-center py-12 text-gray-500">
<ArrowRightLeft size={48} className="mx-auto mb-4 text-gray-300" />
<p>No swap records found</p>
<Button
className="mt-4"
onClick={() => setActiveTab('create')}
>
<Plus size={16} className="mr-2" />
Create First Swap
</Button>
</div>
)}
</div>
)}
{activeTab === 'create' && (
<div className="max-w-3xl mx-auto">
<div className="mb-6">
<h2 className="text-lg font-semibold text-gray-800">Create Employee Swap</h2>
<p className="text-sm text-gray-500">Transfer an employee to a different department temporarily</p>
</div>
<form onSubmit={handleCreateSwap} className="space-y-6">
{/* Employee Selection */}
<div className="bg-gray-50 rounded-lg p-4">
<h3 className="text-sm font-semibold text-gray-700 mb-3 flex items-center gap-2">
<User size={16} />
Select Employee
</h3>
<Select
label="Employee"
value={formData.employeeId}
onChange={(e) => setFormData({ ...formData, employeeId: e.target.value })}
options={[
{ value: '', label: 'Select an employee...' },
...employeeList.map(e => ({
value: String(e.id),
label: `${e.name} - ${e.department_name || 'No Dept'}`
}))
]}
required
/>
{selectedEmployee && (
<div className="mt-3 p-3 bg-white rounded border border-gray-200">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-purple-100 rounded-full flex items-center justify-center">
<User size={20} className="text-purple-600" />
</div>
<div>
<div className="font-medium text-gray-800">{selectedEmployee.name}</div>
<div className="text-sm text-gray-500">
Current: {selectedEmployee.department_name || 'No Department'}
{selectedEmployee.contractor_name && ` • Under: ${selectedEmployee.contractor_name}`}
</div>
</div>
</div>
</div>
)}
</div>
{/* Target Department */}
<div className="bg-gray-50 rounded-lg p-4">
<h3 className="text-sm font-semibold text-gray-700 mb-3 flex items-center gap-2">
<Building2 size={16} />
Target Department
</h3>
<div className="grid grid-cols-2 gap-4">
<Select
label="Department"
value={formData.targetDepartmentId}
onChange={(e) => setFormData({
...formData,
targetDepartmentId: e.target.value,
targetContractorId: '' // Reset contractor when department changes
})}
options={[
{ value: '', label: 'Select department...' },
...departments
.filter(d => d.id !== selectedEmployee?.department_id)
.map(d => ({ value: String(d.id), label: d.name }))
]}
required
/>
<Select
label="Assign to Contractor (Optional)"
value={formData.targetContractorId}
onChange={(e) => setFormData({ ...formData, targetContractorId: e.target.value })}
options={[
{ value: '', label: 'No contractor' },
...targetContractors.map(c => ({ value: String(c.id), label: c.name }))
]}
disabled={!formData.targetDepartmentId}
/>
</div>
</div>
{/* Swap Reason */}
<div className="bg-gray-50 rounded-lg p-4">
<h3 className="text-sm font-semibold text-gray-700 mb-3 flex items-center gap-2">
<AlertCircle size={16} />
Swap Reason
</h3>
<div className="grid grid-cols-2 gap-4">
<Select
label="Reason"
value={formData.swapReason}
onChange={(e) => setFormData({ ...formData, swapReason: e.target.value as SwapReason })}
options={[
{ value: '', label: 'Select reason...' },
{ value: 'LeftWork', label: 'Left Work Early' },
{ value: 'Sick', label: 'Sick / Unwell' },
{ value: 'FinishedEarly', label: 'Finished Work Early' },
{ value: 'Other', label: 'Other Reason' },
]}
required
/>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Work Completion %
</label>
<div className="flex items-center gap-3">
<input
type="range"
min="0"
max="100"
step="5"
value={formData.workCompletionPercentage}
onChange={(e) => setFormData({
...formData,
workCompletionPercentage: parseInt(e.target.value)
})}
className="flex-1"
/>
<span className="text-sm font-medium text-gray-700 w-12">
{formData.workCompletionPercentage}%
</span>
</div>
</div>
</div>
<div className="mt-4">
<Input
label="Additional Details (Optional)"
value={formData.reasonDetails}
onChange={(e) => setFormData({ ...formData, reasonDetails: e.target.value })}
placeholder="Provide more context about the swap..."
/>
</div>
</div>
{/* Swap Date */}
<div className="bg-gray-50 rounded-lg p-4">
<h3 className="text-sm font-semibold text-gray-700 mb-3 flex items-center gap-2">
<Clock size={16} />
Swap Date
</h3>
<Input
label="Date"
type="date"
value={formData.swapDate}
onChange={(e) => setFormData({ ...formData, swapDate: e.target.value })}
required
/>
</div>
{/* Submit */}
<div className="flex justify-end gap-3 pt-4">
<Button
type="button"
variant="outline"
onClick={() => setActiveTab('list')}
>
Cancel
</Button>
<Button
type="submit"
disabled={submitting || !formData.employeeId || !formData.targetDepartmentId || !formData.swapReason}
>
{submitting ? (
<>
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
Creating...
</>
) : (
<>
<ArrowRightLeft size={16} className="mr-2" />
Create Swap
</>
)}
</Button>
</div>
</form>
</div>
)}
</CardContent>
</Card>
</div>
);
};

View File

@@ -1,9 +1,9 @@
import React, { useState, useMemo } from 'react';
import React, { useState } from 'react';
import { RefreshCw, Plus, Trash2, Edit, Save, X, Search, AlertTriangle, UserX } from 'lucide-react';
import { Card, CardHeader, CardContent } from '../components/ui/Card';
import { Card, CardContent } from '../components/ui/Card';
import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from '../components/ui/Table';
import { Button } from '../components/ui/Button';
import { Input, Select } from '../components/ui/Input';
import { Input, Select, PasswordInput } from '../components/ui/Input';
import { useEmployees } from '../hooks/useEmployees';
import { useDepartments } from '../hooks/useDepartments';
import { useAuth } from '../contexts/AuthContext';
@@ -29,6 +29,16 @@ export const UsersPage: React.FC = () => {
departmentId: '',
contractorId: '',
isActive: true,
// New fields
phoneNumber: '',
aadharNumber: '',
bankAccountNumber: '',
bankName: '',
bankIfsc: '',
// Contractor-specific
contractorAgreementNumber: '',
pfNumber: '',
esicNumber: '',
});
const [formError, setFormError] = useState('');
const [formLoading, setFormLoading] = useState(false);
@@ -99,6 +109,16 @@ export const UsersPage: React.FC = () => {
role: formData.role,
departmentId: formData.departmentId ? parseInt(formData.departmentId) : null,
contractorId: formData.contractorId ? parseInt(formData.contractorId) : null,
// New fields
phoneNumber: formData.phoneNumber || null,
aadharNumber: formData.aadharNumber || null,
bankAccountNumber: formData.bankAccountNumber || null,
bankName: formData.bankName || null,
bankIfsc: formData.bankIfsc || null,
// Contractor-specific
contractorAgreementNumber: formData.contractorAgreementNumber || null,
pfNumber: formData.pfNumber || null,
esicNumber: formData.esicNumber || null,
});
// Reset form and switch to list
@@ -111,6 +131,15 @@ export const UsersPage: React.FC = () => {
role: 'Employee',
departmentId: '',
contractorId: '',
isActive: true,
phoneNumber: '',
aadharNumber: '',
bankAccountNumber: '',
bankName: '',
bankIfsc: '',
contractorAgreementNumber: '',
pfNumber: '',
esicNumber: '',
});
setActiveTab('list');
refresh();
@@ -143,6 +172,15 @@ export const UsersPage: React.FC = () => {
departmentId: user.department_id ? String(user.department_id) : '',
contractorId: user.contractor_id ? String(user.contractor_id) : '',
isActive: user.is_active,
// New fields
phoneNumber: user.phone_number || '',
aadharNumber: user.aadhar_number || '',
bankAccountNumber: user.bank_account_number || '',
bankName: user.bank_name || '',
bankIfsc: user.bank_ifsc || '',
contractorAgreementNumber: user.contractor_agreement_number || '',
pfNumber: user.pf_number || '',
esicNumber: user.esic_number || '',
});
setEditingUserId(user.id);
setActiveTab('edit');
@@ -166,6 +204,15 @@ export const UsersPage: React.FC = () => {
departmentId: formData.departmentId ? parseInt(formData.departmentId) : null,
contractorId: formData.contractorId ? parseInt(formData.contractorId) : null,
isActive: formData.isActive,
// New fields
phoneNumber: formData.phoneNumber || null,
aadharNumber: formData.aadharNumber || null,
bankAccountNumber: formData.bankAccountNumber || null,
bankName: formData.bankName || null,
bankIfsc: formData.bankIfsc || null,
contractorAgreementNumber: formData.contractorAgreementNumber || null,
pfNumber: formData.pfNumber || null,
esicNumber: formData.esicNumber || null,
});
resetForm();
@@ -189,6 +236,14 @@ export const UsersPage: React.FC = () => {
departmentId: '',
contractorId: '',
isActive: true,
phoneNumber: '',
aadharNumber: '',
bankAccountNumber: '',
bankName: '',
bankIfsc: '',
contractorAgreementNumber: '',
pfNumber: '',
esicNumber: '',
});
setEditingUserId(null);
setFormError('');
@@ -329,60 +384,89 @@ export const UsersPage: React.FC = () => {
<TableHead>EMAIL</TableHead>
<TableHead>ROLE</TableHead>
<TableHead>DEPARTMENT</TableHead>
<TableHead>REPORTS TO</TableHead>
<TableHead>STATUS</TableHead>
<TableHead>ACTIONS</TableHead>
</TableHeader>
<TableBody>
{filteredEmployees.map((user) => (
<TableRow key={user.id}>
<TableCell>{user.id}</TableCell>
<TableCell className="text-blue-600">{user.username}</TableCell>
<TableCell>{user.name}</TableCell>
<TableCell>{user.email}</TableCell>
<TableCell>
<span className={`px-2 py-1 rounded text-xs font-medium ${
user.role === 'SuperAdmin' ? 'bg-purple-100 text-purple-700' :
user.role === 'Supervisor' ? 'bg-blue-100 text-blue-700' :
user.role === 'Contractor' ? 'bg-orange-100 text-orange-700' :
'bg-gray-100 text-gray-700'
}`}>
{user.role}
</span>
</TableCell>
<TableCell>{user.department_name || '-'}</TableCell>
<TableCell>
<span className={`px-2 py-1 rounded text-xs font-medium ${
user.is_active ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'
}`}>
{user.is_active ? 'Active' : 'Inactive'}
</span>
</TableCell>
<TableCell>
{canManageUsers && (
<div className="flex gap-2">
<Button
variant="ghost"
size="sm"
onClick={() => handleEditUser(user)}
className="text-blue-600 hover:text-blue-800"
title="Edit"
>
<Edit size={14} />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => handleDeleteUser(user.id, user.username)}
className="text-red-600 hover:text-red-800"
title="Delete"
>
<Trash2 size={14} />
</Button>
</div>
)}
</TableCell>
</TableRow>
))}
{filteredEmployees.map((user) => {
// Find supervisor for contractors (supervisor in same department)
const getSupervisorName = () => {
if (user.role !== 'Contractor') return null;
const supervisor = employees.find(
e => e.role === 'Supervisor' && e.department_id === user.department_id
);
return supervisor?.name || null;
};
// Get reports to info based on role
const getReportsTo = () => {
if (user.role === 'Employee') {
return user.contractor_name ? (
<span className="text-orange-600">{user.contractor_name}</span>
) : '-';
}
if (user.role === 'Contractor') {
const supervisorName = getSupervisorName();
return supervisorName ? (
<span className="text-blue-600">{supervisorName}</span>
) : '-';
}
return '-';
};
return (
<TableRow key={user.id}>
<TableCell>{user.id}</TableCell>
<TableCell className="text-blue-600">{user.username}</TableCell>
<TableCell>{user.name}</TableCell>
<TableCell>{user.email}</TableCell>
<TableCell>
<span className={`px-2 py-1 rounded text-xs font-medium ${
user.role === 'SuperAdmin' ? 'bg-purple-100 text-purple-700' :
user.role === 'Supervisor' ? 'bg-blue-100 text-blue-700' :
user.role === 'Contractor' ? 'bg-orange-100 text-orange-700' :
'bg-gray-100 text-gray-700'
}`}>
{user.role}
</span>
</TableCell>
<TableCell>{user.department_name || '-'}</TableCell>
<TableCell>{getReportsTo()}</TableCell>
<TableCell>
<span className={`px-2 py-1 rounded text-xs font-medium ${
user.is_active ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'
}`}>
{user.is_active ? 'Active' : 'Inactive'}
</span>
</TableCell>
<TableCell>
{canManageUsers && (
<div className="flex gap-2">
<Button
variant="ghost"
size="sm"
onClick={() => handleEditUser(user)}
className="text-blue-600 hover:text-blue-800"
title="Edit"
>
<Edit size={14} />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => handleDeleteUser(user.id, user.username)}
className="text-red-600 hover:text-red-800"
title="Delete"
>
<Trash2 size={14} />
</Button>
</div>
)}
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
) : !loading && (
@@ -410,10 +494,9 @@ export const UsersPage: React.FC = () => {
onChange={handleInputChange}
required
/>
<Input
<PasswordInput
label="Password"
name="password"
type="password"
value={formData.password}
onChange={handleInputChange}
required
@@ -425,10 +508,9 @@ export const UsersPage: React.FC = () => {
onChange={handleInputChange}
required
/>
<Input
<PasswordInput
label="Confirm Password"
name="confirmPassword"
type="password"
value={formData.confirmPassword}
onChange={handleInputChange}
required
@@ -476,6 +558,82 @@ export const UsersPage: React.FC = () => {
)}
</div>
{/* Personal & Bank Details - for Employee and Contractor */}
{(formData.role === 'Employee' || formData.role === 'Contractor') && (
<>
<h3 className="text-lg font-semibold text-gray-800 mb-6">Personal Details</h3>
<div className="grid grid-cols-2 gap-6 mb-8">
<Input
label="Phone Number"
name="phoneNumber"
value={formData.phoneNumber}
onChange={handleInputChange}
placeholder="e.g., 9876543210"
/>
<Input
label="Aadhar Card Number"
name="aadharNumber"
value={formData.aadharNumber}
onChange={handleInputChange}
placeholder="12-digit Aadhar number"
maxLength={12}
/>
</div>
<h3 className="text-lg font-semibold text-gray-800 mb-6">Bank Details</h3>
<div className="grid grid-cols-2 gap-6 mb-8">
<Input
label="Bank Account Number"
name="bankAccountNumber"
value={formData.bankAccountNumber}
onChange={handleInputChange}
/>
<Input
label="Bank Name"
name="bankName"
value={formData.bankName}
onChange={handleInputChange}
/>
<Input
label="IFSC Code"
name="bankIfsc"
value={formData.bankIfsc}
onChange={handleInputChange}
placeholder="e.g., SBIN0001234"
/>
</div>
</>
)}
{/* Contractor-specific fields */}
{formData.role === 'Contractor' && (
<>
<h3 className="text-lg font-semibold text-gray-800 mb-6">Contractor Details</h3>
<div className="grid grid-cols-2 gap-6 mb-8">
<Input
label="Contractor Agreement Number"
name="contractorAgreementNumber"
value={formData.contractorAgreementNumber}
onChange={handleInputChange}
/>
<Input
label="PF Number"
name="pfNumber"
value={formData.pfNumber}
onChange={handleInputChange}
placeholder="Provident Fund number"
/>
<Input
label="ESIC Number"
name="esicNumber"
value={formData.esicNumber}
onChange={handleInputChange}
placeholder="ESIC registration number"
/>
</div>
</>
)}
<div className="flex justify-end gap-4">
<Button
variant="outline"
@@ -599,6 +757,82 @@ export const UsersPage: React.FC = () => {
)}
</div>
{/* Personal & Bank Details - for Employee and Contractor */}
{(formData.role === 'Employee' || formData.role === 'Contractor') && (
<>
<h3 className="text-lg font-semibold text-gray-800 mb-6">Personal Details</h3>
<div className="grid grid-cols-2 gap-6 mb-8">
<Input
label="Phone Number"
name="phoneNumber"
value={formData.phoneNumber}
onChange={handleInputChange}
placeholder="e.g., 9876543210"
/>
<Input
label="Aadhar Card Number"
name="aadharNumber"
value={formData.aadharNumber}
onChange={handleInputChange}
placeholder="12-digit Aadhar number"
maxLength={12}
/>
</div>
<h3 className="text-lg font-semibold text-gray-800 mb-6">Bank Details</h3>
<div className="grid grid-cols-2 gap-6 mb-8">
<Input
label="Bank Account Number"
name="bankAccountNumber"
value={formData.bankAccountNumber}
onChange={handleInputChange}
/>
<Input
label="Bank Name"
name="bankName"
value={formData.bankName}
onChange={handleInputChange}
/>
<Input
label="IFSC Code"
name="bankIfsc"
value={formData.bankIfsc}
onChange={handleInputChange}
placeholder="e.g., SBIN0001234"
/>
</div>
</>
)}
{/* Contractor-specific fields */}
{formData.role === 'Contractor' && (
<>
<h3 className="text-lg font-semibold text-gray-800 mb-6">Contractor Details</h3>
<div className="grid grid-cols-2 gap-6 mb-8">
<Input
label="Contractor Agreement Number"
name="contractorAgreementNumber"
value={formData.contractorAgreementNumber}
onChange={handleInputChange}
/>
<Input
label="PF Number"
name="pfNumber"
value={formData.pfNumber}
onChange={handleInputChange}
placeholder="Provident Fund number"
/>
<Input
label="ESIC Number"
name="esicNumber"
value={formData.esicNumber}
onChange={handleInputChange}
placeholder="ESIC registration number"
/>
</div>
</>
)}
<div className="flex justify-end gap-4">
<Button
variant="ghost"
@@ -718,49 +952,78 @@ export const UsersPage: React.FC = () => {
<TableHead>EMAIL</TableHead>
<TableHead>ROLE</TableHead>
<TableHead>DEPARTMENT</TableHead>
<TableHead>REPORTS TO</TableHead>
<TableHead>STATUS</TableHead>
<TableHead>ACTION</TableHead>
</TableHeader>
<TableBody>
{deletableUsers.map((user) => (
<TableRow key={user.id} className="hover:bg-red-50">
<TableCell>{user.id}</TableCell>
<TableCell className="text-blue-600">{user.username}</TableCell>
<TableCell>{user.name}</TableCell>
<TableCell>{user.email}</TableCell>
<TableCell>
<span className={`px-2 py-1 rounded text-xs font-medium ${
user.role === 'Contractor' ? 'bg-orange-100 text-orange-700' :
'bg-gray-100 text-gray-700'
}`}>
{user.role}
</span>
</TableCell>
<TableCell>{user.department_name || '-'}</TableCell>
<TableCell>
<span className={`px-2 py-1 rounded text-xs font-medium ${
user.is_active ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'
}`}>
{user.is_active ? 'Active' : 'Inactive'}
</span>
</TableCell>
<TableCell>
<Button
variant="outline"
size="sm"
onClick={() => {
if (confirm(`Are you sure you want to permanently delete "${user.name}" (${user.username})?\n\nThis action cannot be undone!`)) {
deleteEmployee(user.id);
}
}}
className="text-red-600 border-red-300 hover:bg-red-50 hover:border-red-400"
>
<UserX size={14} className="mr-1" />
Delete
</Button>
</TableCell>
</TableRow>
))}
{deletableUsers.map((user) => {
// Find supervisor for contractors (supervisor in same department)
const getSupervisorName = () => {
if (user.role !== 'Contractor') return null;
const supervisor = employees.find(
e => e.role === 'Supervisor' && e.department_id === user.department_id
);
return supervisor?.name || null;
};
// Get reports to info based on role
const getReportsTo = () => {
if (user.role === 'Employee') {
return user.contractor_name ? (
<span className="text-orange-600">{user.contractor_name}</span>
) : '-';
}
if (user.role === 'Contractor') {
const supervisorName = getSupervisorName();
return supervisorName ? (
<span className="text-blue-600">{supervisorName}</span>
) : '-';
}
return '-';
};
return (
<TableRow key={user.id} className="hover:bg-red-50">
<TableCell>{user.id}</TableCell>
<TableCell className="text-blue-600">{user.username}</TableCell>
<TableCell>{user.name}</TableCell>
<TableCell>{user.email}</TableCell>
<TableCell>
<span className={`px-2 py-1 rounded text-xs font-medium ${
user.role === 'Contractor' ? 'bg-orange-100 text-orange-700' :
'bg-gray-100 text-gray-700'
}`}>
{user.role}
</span>
</TableCell>
<TableCell>{user.department_name || '-'}</TableCell>
<TableCell>{getReportsTo()}</TableCell>
<TableCell>
<span className={`px-2 py-1 rounded text-xs font-medium ${
user.is_active ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'
}`}>
{user.is_active ? 'Active' : 'Inactive'}
</span>
</TableCell>
<TableCell>
<Button
variant="outline"
size="sm"
onClick={() => {
if (confirm(`Are you sure you want to permanently delete "${user.name}" (${user.username})?\n\nThis action cannot be undone!`)) {
deleteEmployee(user.id);
}
}}
className="text-red-600 border-red-300 hover:bg-red-50 hover:border-red-400"
>
<UserX size={14} className="mr-1" />
Delete
</Button>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
) : (

View File

@@ -166,6 +166,57 @@ class ApiService {
return this.request<any[]>(`/attendance/summary/stats${query ? `?${query}` : ''}`);
}
async updateAttendanceStatus(id: number, status: string, remark?: string) {
return this.request<any>(`/attendance/${id}/status`, {
method: 'PUT',
body: JSON.stringify({ status, remark }),
});
}
async markAbsent(employeeId: number, workDate: string, remark?: string) {
return this.request<any>('/attendance/mark-absent', {
method: 'POST',
body: JSON.stringify({ employeeId, workDate, remark }),
});
}
// Employee Swaps
async getEmployeeSwaps(params?: { status?: string; employeeId?: number; startDate?: string; endDate?: string }) {
const query = params ? new URLSearchParams(params as any).toString() : '';
return this.request<any[]>(`/employee-swaps${query ? `?${query}` : ''}`);
}
async getEmployeeSwap(id: number) {
return this.request<any>(`/employee-swaps/${id}`);
}
async createEmployeeSwap(data: {
employeeId: number;
targetDepartmentId: number;
targetContractorId?: number;
swapReason: string;
reasonDetails?: string;
workCompletionPercentage?: number;
swapDate: string;
}) {
return this.request<any>('/employee-swaps', {
method: 'POST',
body: JSON.stringify(data),
});
}
async completeEmployeeSwap(id: number) {
return this.request<any>(`/employee-swaps/${id}/complete`, {
method: 'PUT',
});
}
async cancelEmployeeSwap(id: number) {
return this.request<any>(`/employee-swaps/${id}/cancel`, {
method: 'PUT',
});
}
// Contractor Rates
async getContractorRates(params?: { contractorId?: number; subDepartmentId?: number }) {
const query = params ? new URLSearchParams(params as any).toString() : '';

View File

@@ -10,6 +10,18 @@ export interface User {
created_at: string;
department_name?: string;
contractor_name?: string;
sub_department_id?: number;
sub_department_name?: string;
// Common fields for Employee and Contractor
phone_number?: string;
aadhar_number?: string;
bank_account_number?: string;
bank_name?: string;
bank_ifsc?: string;
// Contractor-specific fields
contractor_agreement_number?: string;
pf_number?: string;
esic_number?: string;
}
export interface Department {
@@ -49,6 +61,8 @@ export interface WorkAllocation {
department_name?: string;
}
export type AttendanceStatus = 'CheckedIn' | 'CheckedOut' | 'Absent' | 'HalfDay' | 'Late';
export interface Attendance {
id: number;
employee_id: number;
@@ -56,7 +70,8 @@ export interface Attendance {
check_in_time: string;
check_out_time?: string;
work_date: string;
status: 'CheckedIn' | 'CheckedOut';
status: AttendanceStatus;
remark?: string;
created_at: string;
updated_at: string;
employee_name?: string;
@@ -66,6 +81,33 @@ export interface Attendance {
contractor_name?: string;
}
export type SwapReason = 'LeftWork' | 'Sick' | 'FinishedEarly' | 'Other';
export type SwapStatus = 'Active' | 'Completed' | 'Cancelled';
export interface EmployeeSwap {
id: number;
employee_id: number;
original_department_id: number;
target_department_id: number;
original_contractor_id?: number;
target_contractor_id?: number;
swap_reason: SwapReason;
reason_details?: string;
work_completion_percentage: number;
swap_date: string;
swapped_by: number;
status: SwapStatus;
created_at: string;
completed_at?: string;
// Joined fields
employee_name?: string;
original_department_name?: string;
target_department_name?: string;
original_contractor_name?: string;
target_contractor_name?: string;
swapped_by_name?: string;
}
export interface ContractorRate {
id: number;
contractor_id: number;