(Feat-Fix): New Reporting system, more seeded data, fixed subdepartments and activity inversion, login page changes, etc etc

This commit is contained in:
2025-12-18 08:15:31 +00:00
parent 916ee19677
commit ac29bb2882
24 changed files with 3306 additions and 207 deletions

View File

@@ -9,8 +9,11 @@ import { AttendancePage } from './pages/AttendancePage';
import { RatesPage } from './pages/RatesPage';
import { EmployeeSwapPage } from './pages/EmployeeSwapPage';
import { LoginPage } from './pages/LoginPage';
import { ReportingPage } from './pages/ReportingPage';
import { StandardRatesPage } from './pages/StandardRatesPage';
import { AllRatesPage } from './pages/AllRatesPage';
type PageType = 'dashboard' | 'users' | 'allocation' | 'attendance' | 'rates' | 'swaps';
type PageType = 'dashboard' | 'users' | 'allocation' | 'attendance' | 'rates' | 'swaps' | 'reports' | 'standard-rates' | 'all-rates';
const AppContent: React.FC = () => {
const [activePage, setActivePage] = useState<PageType>('dashboard');
@@ -30,6 +33,12 @@ const AppContent: React.FC = () => {
return <RatesPage />;
case 'swaps':
return <EmployeeSwapPage />;
case 'reports':
return <ReportingPage />;
case 'standard-rates':
return <StandardRatesPage />;
case 'all-rates':
return <AllRatesPage />;
default:
return <DashboardPage />;
}

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { LayoutDashboard, Users, Briefcase, CalendarCheck, DollarSign, ClipboardList, ArrowRightLeft } from 'lucide-react';
import { LayoutDashboard, Users, Briefcase, CalendarCheck, DollarSign, ClipboardList, ArrowRightLeft, FileSpreadsheet, Scale, Eye } from 'lucide-react';
import { useAuth } from '../../contexts/AuthContext';
interface SidebarItemProps {
@@ -112,6 +112,36 @@ export const Sidebar: React.FC<SidebarProps> = ({ activePage, onNavigate }) => {
onClick={() => onNavigate('swaps')}
/>
)}
{/* Reports - SuperAdmin and Supervisor */}
{canManageRates && (
<SidebarItem
icon={FileSpreadsheet}
label="Reports"
active={activePage === 'reports'}
onClick={() => onNavigate('reports')}
/>
)}
{/* Standard Rates - SuperAdmin and Supervisor */}
{canManageRates && (
<SidebarItem
icon={Scale}
label="Standard Rates"
active={activePage === 'standard-rates'}
onClick={() => onNavigate('standard-rates')}
/>
)}
{/* All Rates View - SuperAdmin only */}
{isSuperAdmin && (
<SidebarItem
icon={Eye}
label="All Rates"
active={activePage === 'all-rates'}
onClick={() => onNavigate('all-rates')}
/>
)}
</nav>
{/* Role indicator at bottom */}

View File

@@ -65,4 +65,4 @@ export const useSubDepartments = (departmentId?: string) => {
error,
refresh: fetchSubDepartments,
};
};
};

302
src/pages/AllRatesPage.tsx Normal file
View File

@@ -0,0 +1,302 @@
import React, { useState, useEffect, useMemo } from 'react';
import { RefreshCw, Search, Filter, Eye, Calendar } 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 { Input, Select } from '../components/ui/Input';
import { api } from '../services/api';
import { useDepartments } from '../hooks/useDepartments';
import { useAuth } from '../contexts/AuthContext';
export const AllRatesPage: React.FC = () => {
const { user } = useAuth();
const { departments } = useDepartments();
const [allRates, setAllRates] = useState<any[]>([]);
const [summary, setSummary] = useState<{ totalContractorRates: number; totalStandardRates: number; totalRates: number } | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [searchQuery, setSearchQuery] = useState('');
// Filters
const [filters, setFilters] = useState({
departmentId: '',
startDate: '',
endDate: '',
rateType: '', // 'contractor' | 'standard' | ''
});
const isSuperAdmin = user?.role === 'SuperAdmin';
// Fetch all rates
const fetchAllRates = async () => {
setLoading(true);
setError('');
try {
const params: any = {};
if (filters.departmentId) params.departmentId = parseInt(filters.departmentId);
if (filters.startDate) params.startDate = filters.startDate;
if (filters.endDate) params.endDate = filters.endDate;
const data = await api.getAllRates(params);
setAllRates(data.allRates);
setSummary(data.summary);
} catch (err: any) {
setError(err.message || 'Failed to fetch rates');
} finally {
setLoading(false);
}
};
useEffect(() => {
if (isSuperAdmin) {
fetchAllRates();
}
}, [isSuperAdmin]);
const handleFilterChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
const { name, value } = e.target;
setFilters(prev => ({ ...prev, [name]: value }));
};
const applyFilters = () => {
fetchAllRates();
};
const clearFilters = () => {
setFilters({
departmentId: '',
startDate: '',
endDate: '',
rateType: '',
});
setTimeout(fetchAllRates, 0);
};
// Filter rates based on search and rate type
const filteredRates = useMemo(() => {
let rates = allRates;
// Filter by rate type
if (filters.rateType) {
rates = rates.filter(r => r.rate_type === filters.rateType);
}
// Filter by search query
if (searchQuery) {
const query = searchQuery.toLowerCase();
rates = rates.filter(r =>
r.contractor_name?.toLowerCase().includes(query) ||
r.sub_department_name?.toLowerCase().includes(query) ||
r.department_name?.toLowerCase().includes(query) ||
r.activity?.toLowerCase().includes(query) ||
r.created_by_name?.toLowerCase().includes(query)
);
}
return rates;
}, [allRates, searchQuery, filters.rateType]);
// Access check
if (!isSuperAdmin) {
return (
<div className="p-6">
<Card>
<CardContent>
<div className="text-center py-12">
<Eye size={48} className="mx-auto text-gray-400 mb-4" />
<h2 className="text-xl font-semibold text-gray-700 mb-2">Access Restricted</h2>
<p className="text-gray-500">
This page is only accessible to Super Admin accounts.
</p>
</div>
</CardContent>
</Card>
</div>
);
}
return (
<div className="p-6">
<Card>
<div className="border-b border-gray-200 px-6 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Eye className="text-purple-600" size={24} />
<div>
<h2 className="text-xl font-semibold text-gray-800">All Rates Overview</h2>
<p className="text-sm text-gray-500">View all contractor and standard rates across all departments</p>
</div>
</div>
</div>
</div>
<CardContent>
{/* Filters */}
<div className="mb-6 p-4 bg-gray-50 rounded-lg border border-gray-200">
<div className="flex items-center gap-2 mb-4">
<Filter size={18} className="text-gray-500" />
<h3 className="font-medium text-gray-700">Filters</h3>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<Select
label="Department"
name="departmentId"
value={filters.departmentId}
onChange={handleFilterChange}
options={[
{ value: '', label: 'All Departments' },
...departments.map(d => ({ value: String(d.id), label: d.name }))
]}
/>
<Select
label="Rate Type"
name="rateType"
value={filters.rateType}
onChange={handleFilterChange}
options={[
{ value: '', label: 'All Types' },
{ value: 'contractor', label: 'Contractor Rates' },
{ value: 'standard', label: 'Standard Rates' },
]}
/>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Start Date</label>
<Input
type="date"
name="startDate"
value={filters.startDate}
onChange={handleFilterChange}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">End Date</label>
<Input
type="date"
name="endDate"
value={filters.endDate}
onChange={handleFilterChange}
/>
</div>
</div>
<div className="flex gap-2 mt-4">
<Button onClick={applyFilters} size="sm">
Apply Filters
</Button>
<Button variant="outline" onClick={clearFilters} size="sm">
Clear
</Button>
</div>
</div>
{/* Summary Cards */}
{summary && (
<div className="grid grid-cols-3 gap-4 mb-6">
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<div className="text-sm text-blue-600 font-medium">Total Rates</div>
<div className="text-2xl font-bold text-blue-800">{summary.totalRates}</div>
</div>
<div className="bg-orange-50 border border-orange-200 rounded-lg p-4">
<div className="text-sm text-orange-600 font-medium">Contractor Rates</div>
<div className="text-2xl font-bold text-orange-800">{summary.totalContractorRates}</div>
</div>
<div className="bg-green-50 border border-green-200 rounded-lg p-4">
<div className="text-sm text-green-600 font-medium">Standard Rates</div>
<div className="text-2xl font-bold text-green-800">{summary.totalStandardRates}</div>
</div>
</div>
)}
{/* Search and Refresh */}
<div className="flex gap-4 mb-4">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400" size={18} />
<input
type="text"
placeholder="Search by contractor, department, activity..."
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-blue-500 focus:border-transparent"
/>
</div>
<Button variant="ghost" onClick={fetchAllRates}>
<RefreshCw size={16} className="mr-2" />
Refresh
</Button>
</div>
{/* Error */}
{error && (
<div className="text-center py-4 text-red-600 bg-red-50 rounded-md mb-4">
Error: {error}
</div>
)}
{/* Table */}
{loading ? (
<div className="text-center py-8">Loading all rates...</div>
) : filteredRates.length > 0 ? (
<div className="overflow-x-auto">
<Table>
<TableHeader>
<TableHead>Type</TableHead>
<TableHead>Contractor</TableHead>
<TableHead>Department</TableHead>
<TableHead>Sub-Department</TableHead>
<TableHead>Activity</TableHead>
<TableHead>Rate ()</TableHead>
<TableHead>Effective Date</TableHead>
<TableHead>Created By</TableHead>
</TableHeader>
<TableBody>
{filteredRates.map((rate, idx) => (
<TableRow key={`${rate.rate_type}-${rate.id}-${idx}`}>
<TableCell>
<span className={`px-2 py-1 rounded text-xs font-medium ${
rate.rate_type === 'contractor'
? 'bg-orange-100 text-orange-700'
: 'bg-green-100 text-green-700'
}`}>
{rate.rate_type === 'contractor' ? 'Contractor' : 'Standard'}
</span>
</TableCell>
<TableCell className="font-medium">
{rate.contractor_name || '-'}
</TableCell>
<TableCell>{rate.department_name || '-'}</TableCell>
<TableCell>{rate.sub_department_name || '-'}</TableCell>
<TableCell>
<span className={`px-2 py-1 rounded text-xs font-medium ${
rate.activity === 'Loading' || rate.activity === 'Unloading'
? 'bg-purple-100 text-purple-700'
: 'bg-gray-100 text-gray-700'
}`}>
{rate.activity || 'Standard'}
</span>
</TableCell>
<TableCell>
<span className="text-green-600 font-semibold">{rate.rate}</span>
</TableCell>
<TableCell>
<div className="flex items-center gap-1">
<Calendar size={14} className="text-gray-400" />
{new Date(rate.effective_date).toLocaleDateString()}
</div>
</TableCell>
<TableCell className="text-gray-500">
{rate.created_by_name || '-'}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
) : (
<div className="text-center py-8 text-gray-500">
No rates found. Adjust your filters or check back later.
</div>
)}
</CardContent>
</Card>
</div>
);
};

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect, useMemo } from 'react';
import { Users, Briefcase, Clock, Building2, Search, Calendar, ChevronDown, ChevronRight, ExternalLink } from 'lucide-react';
import { Users, Briefcase, Clock, Building2, Search, Calendar, ChevronDown, ChevronRight, ExternalLink, RefreshCw } from 'lucide-react';
import { PieChart, Pie, Cell, ResponsiveContainer, BarChart, Bar, XAxis, YAxis, Tooltip } from 'recharts';
import { Card, CardHeader, CardContent } from '../components/ui/Card';
import { useEmployees } from '../hooks/useEmployees';
@@ -43,15 +43,25 @@ interface HierarchyNode {
}
export const DashboardPage: React.FC = () => {
const { employees, loading: employeesLoading } = useEmployees();
const { employees, loading: employeesLoading, refresh: refreshEmployees } = useEmployees();
const { departments, loading: deptLoading } = useDepartments();
const { allocations, loading: allocLoading } = useWorkAllocations();
const { allocations, loading: allocLoading, refresh: refreshAllocations } = useWorkAllocations();
const { user } = useAuth();
const [attendance, setAttendance] = useState<AttendanceRecord[]>([]);
const [searchQuery, setSearchQuery] = useState('');
const [expandedNodes, setExpandedNodes] = useState<Set<string>>(new Set());
const [contractorRates, setContractorRates] = useState<Record<number, number>>({});
// Refresh all data function
const refreshAllData = () => {
refreshEmployees();
refreshAllocations();
const today = new Date().toISOString().split('T')[0];
api.getAttendance({ startDate: today, endDate: today })
.then(setAttendance)
.catch(console.error);
};
const isSuperAdmin = user?.role === 'SuperAdmin';
const isSupervisor = user?.role === 'Supervisor';
const isContractor = user?.role === 'Contractor';
@@ -161,41 +171,78 @@ export const DashboardPage: React.FC = () => {
e => e.role === 'Contractor' && e.department_id === supervisor.department_id
);
// Get employees without a contractor but in this department (e.g., swapped employees)
const unassignedEmployees = employees.filter(
e => e.role === 'Employee' &&
e.department_id === supervisor.department_id &&
!e.contractor_id
);
const contractorNodes = deptContractors.map(contractor => {
const contractorEmployees = employees.filter(
e => e.role === 'Employee' && e.contractor_id === contractor.id
);
return {
id: contractor.id,
name: contractor.name,
role: 'Contractor',
department: contractor.department_name || '',
children: contractorEmployees.map(emp => {
const empAttendance = attendance.find(a => a.employee_id === emp.id);
const empAllocation = allocations.find(a => a.employee_id === emp.id && a.status !== 'Completed');
return {
id: emp.id,
name: emp.name,
role: 'Employee',
department: emp.department_name || '',
subDepartment: empAllocation?.sub_department_name || '-',
activity: empAllocation?.description || empAllocation?.activity || '-',
status: empAttendance ? (empAttendance.status === 'CheckedIn' || empAttendance.status === 'CheckedOut' ? 'Present' : 'Absent') : undefined,
inTime: empAttendance?.check_in_time?.substring(0, 5),
outTime: empAttendance?.check_out_time?.substring(0, 5),
remark: empAttendance?.remark,
children: [],
};
}),
};
});
// Add unassigned employees node if there are any
if (unassignedEmployees.length > 0) {
contractorNodes.push({
id: -supervisor.department_id!, // Negative ID to avoid conflicts
name: 'Unassigned (Swapped)',
role: 'Contractor',
department: supervisor.department_name || '',
children: unassignedEmployees.map(emp => {
const empAttendance = attendance.find(a => a.employee_id === emp.id);
const empAllocation = allocations.find(a => a.employee_id === emp.id && a.status !== 'Completed');
return {
id: emp.id,
name: emp.name,
role: 'Employee',
department: emp.department_name || '',
subDepartment: empAllocation?.sub_department_name || '-',
activity: empAllocation?.description || empAllocation?.activity || 'Swapped',
status: empAttendance ? (empAttendance.status === 'CheckedIn' || empAttendance.status === 'CheckedOut' ? 'Present' : 'Absent') : undefined,
inTime: empAttendance?.check_in_time?.substring(0, 5),
outTime: empAttendance?.check_out_time?.substring(0, 5),
remark: empAttendance?.remark,
children: [],
};
}),
});
}
const supervisorNode: HierarchyNode = {
id: supervisor.id,
name: supervisor.name,
role: 'Supervisor',
department: supervisor.department_name || '',
children: deptContractors.map(contractor => {
const contractorEmployees = employees.filter(
e => e.role === 'Employee' && e.contractor_id === contractor.id
);
return {
id: contractor.id,
name: contractor.name,
role: 'Contractor',
department: contractor.department_name || '',
children: contractorEmployees.map(emp => {
const empAttendance = attendance.find(a => a.employee_id === emp.id);
const empAllocation = allocations.find(a => a.employee_id === emp.id);
return {
id: emp.id,
name: emp.name,
role: 'Employee',
department: emp.department_name || '',
subDepartment: emp.sub_department_name,
activity: empAllocation?.description || 'Loading',
status: empAttendance ? (empAttendance.status === 'CheckedIn' || empAttendance.status === 'CheckedOut' ? 'Present' : 'Absent') : undefined,
inTime: empAttendance?.check_in_time?.substring(0, 5),
outTime: empAttendance?.check_out_time?.substring(0, 5),
remark: empAttendance?.remark,
children: [],
};
}),
};
}),
children: contractorNodes,
};
return supervisorNode;
@@ -211,27 +258,34 @@ export const DashboardPage: React.FC = () => {
e => e.role === 'Contractor' && e.department_id === user.department_id
);
return deptContractors.map(contractor => {
// Get employees without a contractor but in this department (e.g., swapped employees)
const unassignedEmployees = employees.filter(
e => e.role === 'Employee' &&
e.department_id === user.department_id &&
!e.contractor_id
);
const contractorNodes: HierarchyNode[] = deptContractors.map(contractor => {
const contractorEmployees = employees.filter(
e => e.role === 'Employee' && e.contractor_id === contractor.id
);
const contractorNode: HierarchyNode = {
return {
id: contractor.id,
name: contractor.name,
role: 'Contractor',
department: contractor.department_name || '',
children: contractorEmployees.map(emp => {
const empAttendance = attendance.find(a => a.employee_id === emp.id);
const empAllocation = allocations.find(a => a.employee_id === emp.id);
const empAllocation = allocations.find(a => a.employee_id === emp.id && a.status !== 'Completed');
return {
id: emp.id,
name: emp.name,
role: 'Employee',
department: emp.department_name || '',
subDepartment: emp.sub_department_name,
activity: empAllocation?.description || '-',
subDepartment: empAllocation?.sub_department_name || '-',
activity: empAllocation?.description || empAllocation?.activity || '-',
status: empAttendance ? (empAttendance.status === 'CheckedIn' || empAttendance.status === 'CheckedOut' ? 'Present' : 'Absent') : undefined,
inTime: empAttendance?.check_in_time?.substring(0, 5),
outTime: empAttendance?.check_out_time?.substring(0, 5),
@@ -240,10 +294,38 @@ export const DashboardPage: React.FC = () => {
};
}),
};
return contractorNode;
});
}, [isSupervisor, user, employees, attendance, allocations]);
// Add unassigned employees node if there are any
if (unassignedEmployees.length > 0) {
contractorNodes.push({
id: -user.department_id, // Negative ID to avoid conflicts
name: 'Unassigned (Swapped)',
role: 'Contractor',
department: filteredDepartments[0]?.name || '',
children: unassignedEmployees.map(emp => {
const empAttendance = attendance.find(a => a.employee_id === emp.id);
const empAllocation = allocations.find(a => a.employee_id === emp.id && a.status !== 'Completed');
return {
id: emp.id,
name: emp.name,
role: 'Employee',
department: emp.department_name || '',
subDepartment: empAllocation?.sub_department_name || '-',
activity: empAllocation?.description || empAllocation?.activity || 'Swapped',
status: empAttendance ? (empAttendance.status === 'CheckedIn' || empAttendance.status === 'CheckedOut' ? 'Present' : 'Absent') : undefined,
inTime: empAttendance?.check_in_time?.substring(0, 5),
outTime: empAttendance?.check_out_time?.substring(0, 5),
remark: empAttendance?.remark,
children: [],
};
}),
});
}
return contractorNodes;
}, [isSupervisor, user, employees, attendance, allocations, filteredDepartments]);
// Department presence data for bar chart
const departmentPresenceData = useMemo(() => {
@@ -405,10 +487,19 @@ export const DashboardPage: React.FC = () => {
{/* Daily Attendance Report Header */}
<div className="flex items-center justify-between">
<h1 className="text-2xl font-bold text-gray-800">Daily Attendance Report</h1>
<button className="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors">
<Calendar size={18} />
Date Range
</button>
<div className="flex items-center gap-2">
<button
onClick={refreshAllData}
className="flex items-center gap-2 px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-colors"
>
<RefreshCw size={18} />
Refresh
</button>
<button className="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors">
<Calendar size={18} />
Date Range
</button>
</div>
</div>
{/* Search Bar */}
@@ -647,10 +738,19 @@ export const DashboardPage: React.FC = () => {
<h1 className="text-2xl font-bold text-gray-800">{departmentName} Dashboard</h1>
<p className="text-gray-500 mt-1">Daily Attendance & Work Overview</p>
</div>
<button className="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors">
<Calendar size={18} />
Date Range
</button>
<div className="flex items-center gap-2">
<button
onClick={refreshAllData}
className="flex items-center gap-2 px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-colors"
>
<RefreshCw size={18} />
Refresh
</button>
<button className="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors">
<Calendar size={18} />
Date Range
</button>
</div>
</div>
{/* Search Bar */}

View File

@@ -202,16 +202,11 @@ export const LoginPage: React.FC = () => {
<div className="w-full border-t border-white/10" />
</div>
<div className="relative flex justify-center text-xs">
<span className="px-4 bg-transparent text-blue-200/50">Work Allocation System</span>
<span className="px-4 bg-transparent text-blue-200/50"></span>
</div>
</div>
{/* Footer Info */}
<div className="text-center">
<p className="text-blue-200/40 text-xs">
Secure login powered by JWT authentication
</p>
</div>
</div>
{/* Version badge */}

346
src/pages/ReportingPage.tsx Normal file
View File

@@ -0,0 +1,346 @@
import React, { useState, useEffect, useMemo } from 'react';
import { Download, RefreshCw, Search, FileSpreadsheet, 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 { Input, Select } from '../components/ui/Input';
import { api } from '../services/api';
import { useDepartments } from '../hooks/useDepartments';
import { useAuth } from '../contexts/AuthContext';
export const ReportingPage: React.FC = () => {
const { user } = useAuth();
const { departments } = useDepartments();
const [allocations, setAllocations] = useState<any[]>([]);
const [summary, setSummary] = useState<{ totalAllocations: number; totalAmount: string; totalUnits: string } | null>(null);
const [contractors, setContractors] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [searchQuery, setSearchQuery] = useState('');
// Filters
const [filters, setFilters] = useState({
startDate: '',
endDate: '',
departmentId: '',
contractorId: '',
});
const isSuperAdmin = user?.role === 'SuperAdmin';
// Fetch contractors
useEffect(() => {
api.getUsers({ role: 'Contractor' }).then(setContractors).catch(console.error);
}, []);
// Fetch report data
const fetchReport = async () => {
setLoading(true);
setError('');
try {
const params: any = {};
if (filters.startDate) params.startDate = filters.startDate;
if (filters.endDate) params.endDate = filters.endDate;
if (filters.departmentId && isSuperAdmin) params.departmentId = parseInt(filters.departmentId);
if (filters.contractorId) params.contractorId = parseInt(filters.contractorId);
const data = await api.getCompletedAllocationsReport(params);
setAllocations(data.allocations);
setSummary(data.summary);
} catch (err: any) {
setError(err.message || 'Failed to fetch report');
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchReport();
}, []);
// Filter allocations based on search
const filteredAllocations = useMemo(() => {
if (!searchQuery) return allocations;
const query = searchQuery.toLowerCase();
return allocations.filter(a =>
a.employee_name?.toLowerCase().includes(query) ||
a.contractor_name?.toLowerCase().includes(query) ||
a.sub_department_name?.toLowerCase().includes(query) ||
a.activity?.toLowerCase().includes(query) ||
a.department_name?.toLowerCase().includes(query)
);
}, [allocations, searchQuery]);
// Export to Excel (CSV format)
const exportToExcel = () => {
if (filteredAllocations.length === 0) {
alert('No data to export');
return;
}
// Define headers
const headers = [
'ID',
'Employee Name',
'Employee Phone',
'Contractor',
'Department',
'Sub-Department',
'Activity',
'Assigned Date',
'Completion Date',
'Rate (₹)',
'Units',
'Total Amount (₹)',
'Status',
];
// Map data to rows
const rows = filteredAllocations.map(a => [
a.id,
a.employee_name || '',
a.employee_phone || '',
a.contractor_name || '',
a.department_name || '',
a.sub_department_name || '',
a.activity || 'Standard',
a.assigned_date ? new Date(a.assigned_date).toLocaleDateString() : '',
a.completion_date ? new Date(a.completion_date).toLocaleDateString() : '',
a.rate || 0,
a.units || '',
a.total_amount || a.rate || 0,
a.status,
]);
// Create CSV content
const csvContent = [
headers.join(','),
...rows.map(row => row.map(cell => `"${String(cell).replace(/"/g, '""')}"`).join(','))
].join('\n');
// Add summary at the end
const summaryRows = [
'',
'SUMMARY',
`Total Allocations,${summary?.totalAllocations || 0}`,
`Total Amount (₹),${summary?.totalAmount || 0}`,
`Total Units,${summary?.totalUnits || 0}`,
];
const fullContent = csvContent + '\n' + summaryRows.join('\n');
// Create and download file
const blob = new Blob([fullContent], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
const url = URL.createObjectURL(blob);
link.setAttribute('href', url);
link.setAttribute('download', `completed_work_allocations_${new Date().toISOString().split('T')[0]}.csv`);
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
const handleFilterChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
const { name, value } = e.target;
setFilters(prev => ({ ...prev, [name]: value }));
};
const applyFilters = () => {
fetchReport();
};
const clearFilters = () => {
setFilters({
startDate: '',
endDate: '',
departmentId: '',
contractorId: '',
});
setTimeout(fetchReport, 0);
};
return (
<div className="p-6">
<Card>
<div className="border-b border-gray-200 px-6 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<FileSpreadsheet className="text-green-600" size={24} />
<h2 className="text-xl font-semibold text-gray-800">Work Allocation Reports</h2>
</div>
<Button onClick={exportToExcel} disabled={filteredAllocations.length === 0}>
<Download size={16} className="mr-2" />
Export to Excel
</Button>
</div>
</div>
<CardContent>
{/* Filters */}
<div className="mb-6 p-4 bg-gray-50 rounded-lg border border-gray-200">
<div className="flex items-center gap-2 mb-4">
<Filter size={18} className="text-gray-500" />
<h3 className="font-medium text-gray-700">Filters</h3>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Start Date</label>
<Input
type="date"
name="startDate"
value={filters.startDate}
onChange={handleFilterChange}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">End Date</label>
<Input
type="date"
name="endDate"
value={filters.endDate}
onChange={handleFilterChange}
/>
</div>
{isSuperAdmin && (
<Select
label="Department"
name="departmentId"
value={filters.departmentId}
onChange={handleFilterChange}
options={[
{ value: '', label: 'All Departments' },
...departments.map(d => ({ value: String(d.id), label: d.name }))
]}
/>
)}
<Select
label="Contractor"
name="contractorId"
value={filters.contractorId}
onChange={handleFilterChange}
options={[
{ value: '', label: 'All Contractors' },
...contractors.map(c => ({ value: String(c.id), label: c.name }))
]}
/>
</div>
<div className="flex gap-2 mt-4">
<Button onClick={applyFilters} size="sm">
Apply Filters
</Button>
<Button variant="outline" onClick={clearFilters} size="sm">
Clear
</Button>
</div>
</div>
{/* Summary Cards */}
{summary && (
<div className="grid grid-cols-3 gap-4 mb-6">
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<div className="text-sm text-blue-600 font-medium">Total Completed</div>
<div className="text-2xl font-bold text-blue-800">{summary.totalAllocations}</div>
</div>
<div className="bg-green-50 border border-green-200 rounded-lg p-4">
<div className="text-sm text-green-600 font-medium">Total Amount</div>
<div className="text-2xl font-bold text-green-800">{parseFloat(summary.totalAmount).toLocaleString()}</div>
</div>
<div className="bg-purple-50 border border-purple-200 rounded-lg p-4">
<div className="text-sm text-purple-600 font-medium">Total Units</div>
<div className="text-2xl font-bold text-purple-800">{parseFloat(summary.totalUnits).toLocaleString()}</div>
</div>
</div>
)}
{/* Search and Refresh */}
<div className="flex gap-4 mb-4">
<div className="relative flex-1">
<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, contractor, 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-blue-500 focus:border-transparent"
/>
</div>
<Button variant="ghost" onClick={fetchReport}>
<RefreshCw size={16} className="mr-2" />
Refresh
</Button>
</div>
{/* Error */}
{error && (
<div className="text-center py-4 text-red-600 bg-red-50 rounded-md mb-4">
Error: {error}
</div>
)}
{/* Table */}
{loading ? (
<div className="text-center py-8">Loading report data...</div>
) : filteredAllocations.length > 0 ? (
<div className="overflow-x-auto">
<Table>
<TableHeader>
<TableHead>ID</TableHead>
<TableHead>Employee</TableHead>
<TableHead>Contractor</TableHead>
<TableHead>Department</TableHead>
<TableHead>Sub-Department</TableHead>
<TableHead>Activity</TableHead>
<TableHead>Assigned</TableHead>
<TableHead>Completed</TableHead>
<TableHead>Rate ()</TableHead>
<TableHead>Units</TableHead>
<TableHead>Total ()</TableHead>
</TableHeader>
<TableBody>
{filteredAllocations.map((allocation) => {
const rate = parseFloat(allocation.rate) || 0;
const units = parseFloat(allocation.units) || 0;
const total = parseFloat(allocation.total_amount) || rate;
return (
<TableRow key={allocation.id}>
<TableCell>{allocation.id}</TableCell>
<TableCell className="font-medium">{allocation.employee_name || '-'}</TableCell>
<TableCell>{allocation.contractor_name || '-'}</TableCell>
<TableCell>{allocation.department_name || '-'}</TableCell>
<TableCell>{allocation.sub_department_name || '-'}</TableCell>
<TableCell>
<span className={`px-2 py-1 rounded text-xs font-medium ${
allocation.activity === 'Loading' || allocation.activity === 'Unloading'
? 'bg-purple-100 text-purple-700'
: 'bg-gray-100 text-gray-700'
}`}>
{allocation.activity || 'Standard'}
</span>
</TableCell>
<TableCell>{new Date(allocation.assigned_date).toLocaleDateString()}</TableCell>
<TableCell>
{allocation.completion_date
? new Date(allocation.completion_date).toLocaleDateString()
: '-'}
</TableCell>
<TableCell>{rate.toFixed(2)}</TableCell>
<TableCell>{units > 0 ? units : '-'}</TableCell>
<TableCell className="font-semibold text-green-600">{total.toFixed(2)}</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</div>
) : (
<div className="text-center py-8 text-gray-500">
No completed work allocations found. Adjust your filters or check back later.
</div>
)}
</CardContent>
</Card>
</div>
);
};

View File

@@ -0,0 +1,523 @@
import React, { useState, useEffect, useMemo } from 'react';
import { RefreshCw, Trash2, Edit, DollarSign, Search, Scale, ArrowUpDown } 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 { Input, Select } from '../components/ui/Input';
import { api } from '../services/api';
import { useDepartments, useSubDepartments } from '../hooks/useDepartments';
import { useAuth } from '../contexts/AuthContext';
export const StandardRatesPage: React.FC = () => {
const [activeTab, setActiveTab] = useState<'list' | 'add' | 'compare'>('list');
const { user } = useAuth();
const { departments } = useDepartments();
const [standardRates, setStandardRates] = useState<any[]>([]);
const [contractors, setContractors] = useState<any[]>([]);
const [comparisons, setComparisons] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [searchQuery, setSearchQuery] = useState('');
// Form state
const [formData, setFormData] = useState({
subDepartmentId: '',
activity: '',
rate: '',
effectiveDate: new Date().toISOString().split('T')[0],
});
const [selectedDept, setSelectedDept] = useState('');
const { subDepartments } = useSubDepartments(selectedDept);
const [formError, setFormError] = useState('');
const [formLoading, setFormLoading] = useState(false);
const [editingId, setEditingId] = useState<number | null>(null);
// Compare filters
const [compareContractorId, setCompareContractorId] = useState('');
const isSupervisor = user?.role === 'Supervisor';
const canManageRates = user?.role === 'SuperAdmin' || user?.role === 'Supervisor';
// Fetch standard rates
const fetchStandardRates = async () => {
setLoading(true);
setError('');
try {
const data = await api.getStandardRates();
setStandardRates(data);
} catch (err: any) {
setError(err.message || 'Failed to fetch standard rates');
} finally {
setLoading(false);
}
};
// Fetch contractors
const fetchContractors = async () => {
try {
const data = await api.getUsers({ role: 'Contractor' });
setContractors(data);
} catch (err) {
console.error('Failed to fetch contractors:', err);
}
};
// Fetch comparison data
const fetchComparison = async () => {
if (!compareContractorId) {
setComparisons([]);
return;
}
setLoading(true);
try {
const data = await api.compareRates({ contractorId: parseInt(compareContractorId) });
setComparisons(data.comparisons);
} catch (err: any) {
setError(err.message || 'Failed to fetch comparison');
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchStandardRates();
fetchContractors();
}, []);
useEffect(() => {
if (isSupervisor && user?.department_id) {
setSelectedDept(String(user.department_id));
}
}, [isSupervisor, user?.department_id]);
useEffect(() => {
if (activeTab === 'compare' && compareContractorId) {
fetchComparison();
}
}, [activeTab, compareContractorId]);
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
setFormError('');
};
const resetForm = () => {
setFormData({
subDepartmentId: '',
activity: '',
rate: '',
effectiveDate: new Date().toISOString().split('T')[0],
});
setEditingId(null);
setFormError('');
};
const handleSubmit = async () => {
if (!formData.rate || !formData.effectiveDate) {
setFormError('Rate and effective date are required');
return;
}
setFormLoading(true);
setFormError('');
try {
if (editingId) {
await api.updateStandardRate(editingId, {
rate: parseFloat(formData.rate),
activity: formData.activity || undefined,
effectiveDate: formData.effectiveDate,
});
} else {
await api.createStandardRate({
subDepartmentId: formData.subDepartmentId ? parseInt(formData.subDepartmentId) : undefined,
activity: formData.activity || undefined,
rate: parseFloat(formData.rate),
effectiveDate: formData.effectiveDate,
});
}
resetForm();
setActiveTab('list');
fetchStandardRates();
} catch (err: any) {
setFormError(err.message || 'Failed to save rate');
} finally {
setFormLoading(false);
}
};
const handleEdit = (rate: any) => {
setFormData({
subDepartmentId: rate.sub_department_id ? String(rate.sub_department_id) : '',
activity: rate.activity || '',
rate: String(rate.rate),
effectiveDate: rate.effective_date?.split('T')[0] || new Date().toISOString().split('T')[0],
});
if (rate.department_id) {
setSelectedDept(String(rate.department_id));
}
setEditingId(rate.id);
setActiveTab('add');
};
const handleDelete = async (id: number) => {
if (!confirm('Are you sure you want to delete this standard rate?')) return;
try {
await api.deleteStandardRate(id);
fetchStandardRates();
} catch (err: any) {
alert(err.message || 'Failed to delete rate');
}
};
// Filter rates based on search
const filteredRates = useMemo(() => {
if (!searchQuery) return standardRates;
const query = searchQuery.toLowerCase();
return standardRates.filter(rate =>
rate.sub_department_name?.toLowerCase().includes(query) ||
rate.department_name?.toLowerCase().includes(query) ||
rate.activity?.toLowerCase().includes(query) ||
rate.created_by_name?.toLowerCase().includes(query)
);
}, [standardRates, searchQuery]);
return (
<div className="p-6">
<Card>
<div className="border-b border-gray-200">
<div className="flex space-x-8 px-6">
<button
onClick={() => { setActiveTab('list'); resetForm(); }}
className={`py-4 px-2 border-b-2 font-medium text-sm ${
activeTab === 'list'
? 'border-blue-500 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700'
}`}
>
Standard Rates
</button>
{canManageRates && (
<button
onClick={() => setActiveTab('add')}
className={`py-4 px-2 border-b-2 font-medium text-sm ${
activeTab === 'add'
? 'border-blue-500 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700'
}`}
>
{editingId ? 'Edit Rate' : 'Add Standard Rate'}
</button>
)}
<button
onClick={() => setActiveTab('compare')}
className={`py-4 px-2 border-b-2 font-medium text-sm ${
activeTab === 'compare'
? 'border-blue-500 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700'
}`}
>
<Scale size={16} className="inline mr-1" />
Compare Rates
</button>
</div>
</div>
<CardContent>
{activeTab === 'list' && (
<div>
<div className="flex gap-4 mb-4">
<div className="relative min-w-[300px] flex-1">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400" size={18} />
<input
type="text"
placeholder="Search by sub-department, activity..."
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-blue-500 focus:border-transparent"
/>
</div>
<Button variant="ghost" onClick={fetchStandardRates}>
<RefreshCw size={16} className="mr-2" />
Refresh
</Button>
</div>
<div className="mb-4 p-3 bg-blue-50 border border-blue-200 rounded-md">
<p className="text-sm text-blue-700">
<strong>Standard Rates</strong> are default rates set by supervisors for sub-departments and activities.
These are used as benchmarks to compare against contractor-specific rates.
</p>
</div>
{error && (
<div className="text-center py-4 text-red-600 bg-red-50 rounded-md mb-4">
Error: {error}
</div>
)}
{loading ? (
<div className="text-center py-8">Loading standard rates...</div>
) : filteredRates.length > 0 ? (
<Table>
<TableHeader>
<TableHead>Department</TableHead>
<TableHead>Sub-Department</TableHead>
<TableHead>Activity</TableHead>
<TableHead>Rate ()</TableHead>
<TableHead>Effective Date</TableHead>
<TableHead>Created By</TableHead>
{canManageRates && <TableHead>Actions</TableHead>}
</TableHeader>
<TableBody>
{filteredRates.map((rate) => (
<TableRow key={rate.id}>
<TableCell>{rate.department_name || '-'}</TableCell>
<TableCell className="font-medium">{rate.sub_department_name || 'All'}</TableCell>
<TableCell>
<span className={`px-2 py-1 rounded text-xs font-medium ${
rate.activity === 'Loading' || rate.activity === 'Unloading'
? 'bg-blue-100 text-blue-700'
: 'bg-gray-100 text-gray-700'
}`}>
{rate.activity || 'Standard'}
</span>
</TableCell>
<TableCell>
<span className="text-green-600 font-semibold">{rate.rate}</span>
</TableCell>
<TableCell>{new Date(rate.effective_date).toLocaleDateString()}</TableCell>
<TableCell className="text-gray-500">{rate.created_by_name || '-'}</TableCell>
{canManageRates && (
<TableCell>
<div className="flex gap-2">
<Button
variant="ghost"
size="sm"
onClick={() => handleEdit(rate)}
className="text-blue-600"
title="Edit"
>
<Edit size={14} />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => handleDelete(rate.id)}
className="text-red-600"
title="Delete"
>
<Trash2 size={14} />
</Button>
</div>
</TableCell>
)}
</TableRow>
))}
</TableBody>
</Table>
) : (
<div className="text-center py-8 text-gray-500">
No standard rates configured yet. Add one to get started!
</div>
)}
</div>
)}
{activeTab === 'add' && canManageRates && (
<div className="max-w-2xl space-y-6">
<h3 className="text-lg font-semibold text-gray-800">
{editingId ? 'Edit Standard Rate' : 'Add New Standard Rate'}
</h3>
<div className="p-4 bg-yellow-50 border border-yellow-200 rounded-md">
<h4 className="font-medium text-yellow-800 mb-2">About Standard Rates</h4>
<p className="text-sm text-yellow-700">
Standard rates serve as default benchmarks for sub-departments and activities.
Contractor rates can be compared against these to identify deviations.
</p>
</div>
{formError && (
<div className="p-3 bg-red-100 text-red-700 rounded-md">
{formError}
</div>
)}
<div className="grid grid-cols-2 gap-6">
{isSupervisor ? (
<Input
label="Department"
value={departments.find(d => d.id === user?.department_id)?.name || 'Loading...'}
disabled
/>
) : (
<Select
label="Department"
value={selectedDept}
onChange={(e) => setSelectedDept(e.target.value)}
options={[
{ value: '', label: 'Select Department' },
...departments.map(d => ({ value: String(d.id), label: d.name }))
]}
/>
)}
<Select
label="Sub-Department"
name="subDepartmentId"
value={formData.subDepartmentId}
onChange={handleInputChange}
disabled={!!editingId}
options={[
{ value: '', label: 'All Sub-Departments' },
...subDepartments.map(s => ({ value: String(s.id), label: s.name }))
]}
/>
<Select
label="Activity Type"
name="activity"
value={formData.activity}
onChange={handleInputChange}
options={[
{ value: '', label: 'Standard (Default)' },
{ value: 'Loading', label: 'Loading (per unit)' },
{ value: 'Unloading', label: 'Unloading (per unit)' },
{ value: 'Other', label: 'Other' },
]}
/>
<Input
label={formData.activity === 'Loading' || formData.activity === 'Unloading'
? "Rate per Unit (₹)"
: "Standard Rate (₹)"}
name="rate"
type="number"
value={formData.rate}
onChange={handleInputChange}
placeholder="Enter rate amount"
required
/>
<Input
label="Effective Date"
name="effectiveDate"
type="date"
value={formData.effectiveDate}
onChange={handleInputChange}
required
/>
</div>
<div className="flex justify-end gap-4">
<Button variant="outline" onClick={() => { setActiveTab('list'); resetForm(); }}>
Cancel
</Button>
<Button onClick={handleSubmit} disabled={formLoading}>
{formLoading ? 'Saving...' : (
<>
<DollarSign size={16} className="mr-2" />
{editingId ? 'Update Rate' : 'Add Standard Rate'}
</>
)}
</Button>
</div>
</div>
)}
{activeTab === 'compare' && (
<div>
<div className="mb-6">
<h3 className="text-lg font-semibold text-gray-800 mb-4">
<ArrowUpDown size={20} className="inline mr-2" />
Compare Contractor Rates vs Standard Rates
</h3>
<div className="flex gap-4 items-end">
<div className="w-64">
<Select
label="Select Contractor"
value={compareContractorId}
onChange={(e) => setCompareContractorId(e.target.value)}
options={[
{ value: '', label: 'Select Contractor' },
...contractors.map(c => ({ value: String(c.id), label: c.name }))
]}
/>
</div>
<Button onClick={fetchComparison} disabled={!compareContractorId}>
Compare
</Button>
</div>
</div>
{loading ? (
<div className="text-center py-8">Loading comparison...</div>
) : comparisons.length > 0 ? (
<Table>
<TableHeader>
<TableHead>Sub-Department</TableHead>
<TableHead>Activity</TableHead>
<TableHead>Contractor Rate ()</TableHead>
<TableHead>Standard Rate ()</TableHead>
<TableHead>Difference ()</TableHead>
<TableHead>Status</TableHead>
</TableHeader>
<TableBody>
{comparisons.map((comp, idx) => (
<TableRow key={idx}>
<TableCell className="font-medium">{comp.sub_department_name || 'All'}</TableCell>
<TableCell>
<span className={`px-2 py-1 rounded text-xs font-medium ${
comp.activity === 'Loading' || comp.activity === 'Unloading'
? 'bg-blue-100 text-blue-700'
: 'bg-gray-100 text-gray-700'
}`}>
{comp.activity || 'Standard'}
</span>
</TableCell>
<TableCell className="font-semibold">{comp.rate}</TableCell>
<TableCell className="text-gray-600">{comp.standard_rate}</TableCell>
<TableCell>
<span className={`font-semibold ${
comp.difference > 0 ? 'text-red-600' :
comp.difference < 0 ? 'text-green-600' :
'text-gray-600'
}`}>
{comp.difference > 0 ? '+' : ''}{comp.difference.toFixed(2)}
</span>
</TableCell>
<TableCell>
{comp.is_above_standard ? (
<span className="px-2 py-1 rounded text-xs font-medium bg-red-100 text-red-700">
Above Standard ({comp.percentage_difference}%)
</span>
) : comp.is_below_standard ? (
<span className="px-2 py-1 rounded text-xs font-medium bg-green-100 text-green-700">
Below Standard ({comp.percentage_difference}%)
</span>
) : (
<span className="px-2 py-1 rounded text-xs font-medium bg-gray-100 text-gray-700">
At Standard
</span>
)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
) : compareContractorId ? (
<div className="text-center py-8 text-gray-500">
No rates found for this contractor to compare.
</div>
) : (
<div className="text-center py-8 text-gray-500">
Select a contractor to compare their rates against standard rates.
</div>
)}
</div>
)}
</CardContent>
</Card>
</div>
);
};

View File

@@ -253,6 +253,108 @@ class ApiService {
method: 'DELETE',
});
}
// Reports
async getCompletedAllocationsReport(params?: {
startDate?: string;
endDate?: string;
departmentId?: number;
contractorId?: number;
employeeId?: number;
}) {
const query = params ? new URLSearchParams(params as any).toString() : '';
return this.request<{
allocations: any[];
summary: { totalAllocations: number; totalAmount: string; totalUnits: string }
}>(`/reports/completed-allocations${query ? `?${query}` : ''}`);
}
async getReportSummary(params?: { startDate?: string; endDate?: string }) {
const query = params ? new URLSearchParams(params as any).toString() : '';
return this.request<{
byContractor: any[];
bySubDepartment: any[];
byActivity: any[];
}>(`/reports/summary${query ? `?${query}` : ''}`);
}
// Standard Rates
async getStandardRates(params?: { departmentId?: number; subDepartmentId?: number; activity?: string }) {
const query = params ? new URLSearchParams(params as any).toString() : '';
return this.request<any[]>(`/standard-rates${query ? `?${query}` : ''}`);
}
async getAllRates(params?: { departmentId?: number; startDate?: string; endDate?: string }) {
const query = params ? new URLSearchParams(params as any).toString() : '';
return this.request<{
allRates: any[];
summary: { totalContractorRates: number; totalStandardRates: number; totalRates: number };
}>(`/standard-rates/all-rates${query ? `?${query}` : ''}`);
}
async compareRates(params?: { contractorId?: number; subDepartmentId?: number }) {
const query = params ? new URLSearchParams(params as any).toString() : '';
return this.request<{
standardRates: any[];
contractorRates: any[];
comparisons: any[];
}>(`/standard-rates/compare${query ? `?${query}` : ''}`);
}
async createStandardRate(data: {
subDepartmentId?: number;
activity?: string;
rate: number;
effectiveDate: string
}) {
return this.request<any>('/standard-rates', {
method: 'POST',
body: JSON.stringify(data),
});
}
async updateStandardRate(id: number, data: { rate?: number; activity?: string; effectiveDate?: string }) {
return this.request<any>(`/standard-rates/${id}`, {
method: 'PUT',
body: JSON.stringify(data),
});
}
async deleteStandardRate(id: number) {
return this.request<{ message: string }>(`/standard-rates/${id}`, {
method: 'DELETE',
});
}
// Activities
async getActivities(params?: { departmentId?: number; subDepartmentId?: number }) {
const query = params ? new URLSearchParams(params as any).toString() : '';
return this.request<any[]>(`/activities${query ? `?${query}` : ''}`);
}
async getActivity(id: number) {
return this.request<any>(`/activities/${id}`);
}
async createActivity(data: { sub_department_id: number; name: string; unit_of_measurement?: string }) {
return this.request<any>('/activities', {
method: 'POST',
body: JSON.stringify(data),
});
}
async updateActivity(id: number, data: { name?: string; unit_of_measurement?: string }) {
return this.request<any>(`/activities/${id}`, {
method: 'PUT',
body: JSON.stringify(data),
});
}
async deleteActivity(id: number) {
return this.request<{ message: string }>(`/activities/${id}`, {
method: 'DELETE',
});
}
}
export const api = new ApiService(API_BASE_URL);

View File

@@ -12,6 +12,7 @@ export interface User {
contractor_name?: string;
sub_department_id?: number;
sub_department_name?: string;
primary_activity?: string;
// Common fields for Employee and Contractor
phone_number?: string;
aadhar_number?: string;
@@ -35,9 +36,20 @@ export interface SubDepartment {
id: number;
department_id: number;
name: string;
primary_activity: string;
created_at: string;
updated_at: string;
department_name?: string;
}
export interface Activity {
id: number;
sub_department_id: number;
name: string;
unit_of_measurement: 'Per Bag' | 'Fixed Rate-Per Person';
created_at: string;
sub_department_name?: string;
department_id?: number;
department_name?: string;
}
export interface WorkAllocation {
@@ -111,10 +123,43 @@ export interface EmployeeSwap {
export interface ContractorRate {
id: number;
contractor_id: number;
sub_department_id?: number;
activity?: string;
rate: number;
effective_date: string;
created_at: string;
updated_at: string;
contractor_name?: string;
contractor_username?: string;
sub_department_name?: string;
department_name?: string;
}
export interface StandardRate {
id: number;
sub_department_id?: number;
activity?: string;
rate: number;
effective_date: string;
created_by: number;
created_at: string;
sub_department_name?: string;
department_name?: string;
department_id?: number;
created_by_name?: string;
}
export interface RateComparison {
id: number;
contractor_id: number;
contractor_name: string;
sub_department_id?: number;
sub_department_name?: string;
activity?: string;
rate: number;
standard_rate: number;
difference: number;
percentage_difference: string | null;
is_above_standard: boolean;
is_below_standard: boolean;
}