(Feat): More changes
This commit is contained in:
@@ -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 />;
|
||||
}
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -12,7 +12,6 @@ interface AuthContextType {
|
||||
}
|
||||
|
||||
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||
|
||||
export const useAuth = () => {
|
||||
const context = useContext(AuthContext);
|
||||
if (!context) {
|
||||
|
||||
@@ -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
594
src/pages/EmployeeSwapPage.tsx
Normal file
594
src/pages/EmployeeSwapPage.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
) : (
|
||||
|
||||
@@ -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() : '';
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user