Files
EmployeeManagementSystem/src/pages/ContractorPaymentPage.tsx

577 lines
24 KiB
TypeScript

import React, { useMemo, useState } from "react";
import { Download, FileSpreadsheet, Printer, RefreshCw } from "lucide-react";
import { Card, CardContent } from "../components/ui/Card.tsx";
import { Button } from "../components/ui/Button.tsx";
import { api } from "../services/api.ts";
import { useDepartments } from "../hooks/useDepartments.ts";
import * as XLSX from "xlsx";
interface ContractorPaymentData {
contractor_id: number;
contractor_name: string;
as_per_contractor: number;
dana: number;
tukdi: number;
groundnut: number;
commission_salary: number;
total: number;
tds_base_amount: number;
payable_before_deduction: number;
security_deduction: number;
advance: number;
final_payable: number;
excess_short: number;
}
interface SubDepartmentTotals {
sub_department_id: number;
sub_department_name: string;
contractors: ContractorPaymentData[];
subTotal: ContractorPaymentData;
}
export const ContractorPaymentPage: React.FC = () => {
const { departments } = useDepartments();
const [selectedDepartment, setSelectedDepartment] = useState<string>("");
const [startDate, setStartDate] = useState(() => {
const date = new Date();
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-01`;
});
const [endDate, setEndDate] = useState(() => {
const date = new Date();
return date.toISOString().split("T")[0];
});
const [loading, setLoading] = useState(false);
const [reportData, setReportData] = useState<SubDepartmentTotals[]>([]);
const [companyName, setCompanyName] = useState("DUSAD AGROFOOD PVT LTD");
// Fetch report data
const fetchReport = async () => {
if (!selectedDepartment) {
alert("Please select a department");
return;
}
setLoading(true);
try {
// Fetch completed allocations for the date range
const params: Record<string, any> = {
startDate,
endDate,
departmentId: parseInt(selectedDepartment),
};
const data = await api.getCompletedAllocationsReport(params);
// Process data to group by sub-department and contractor
const processedData = processReportData(data.allocations);
setReportData(processedData);
} catch (err: any) {
console.error("Failed to fetch report:", err);
alert(err.message || "Failed to fetch report");
} finally {
setLoading(false);
}
};
// Process raw allocation data into the payment report format
const processReportData = (allocations: any[]): SubDepartmentTotals[] => {
const subDeptMap = new Map<number, Map<number, ContractorPaymentData>>();
const subDeptNames = new Map<number, string>();
allocations.forEach((alloc) => {
const subDeptId = alloc.sub_department_id || 0;
const contractorId = alloc.contractor_id || 0;
const subDeptName = alloc.sub_department_name || "Other";
subDeptNames.set(subDeptId, subDeptName);
if (!subDeptMap.has(subDeptId)) {
subDeptMap.set(subDeptId, new Map());
}
const contractorMap = subDeptMap.get(subDeptId)!;
if (!contractorMap.has(contractorId)) {
contractorMap.set(contractorId, {
contractor_id: contractorId,
contractor_name: alloc.contractor_name || "Unknown",
as_per_contractor: 0,
dana: 0,
tukdi: 0,
groundnut: 0,
commission_salary: 0,
total: 0,
tds_base_amount: 0,
payable_before_deduction: 0,
security_deduction: 0,
advance: 0,
final_payable: 0,
excess_short: 0,
});
}
const contractor = contractorMap.get(contractorId)!;
const amount = parseFloat(alloc.total_amount) || parseFloat(alloc.rate) || 0;
const activity = (alloc.activity || "").toLowerCase();
// Categorize by activity type
if (activity.includes("dana")) {
contractor.dana += amount;
} else if (activity.includes("tukdi")) {
contractor.tukdi += amount;
} else if (activity.includes("groundnut")) {
contractor.groundnut += amount;
} else if (activity.includes("commission") || activity.includes("salary")) {
contractor.commission_salary += amount;
} else {
contractor.as_per_contractor += amount;
}
});
// Calculate totals for each contractor
const result: SubDepartmentTotals[] = [];
subDeptMap.forEach((contractorMap, subDeptId) => {
const contractors: ContractorPaymentData[] = [];
const subTotal: ContractorPaymentData = {
contractor_id: 0,
contractor_name: "Sub Total",
as_per_contractor: 0,
dana: 0,
tukdi: 0,
groundnut: 0,
commission_salary: 0,
total: 0,
tds_base_amount: 0,
payable_before_deduction: 0,
security_deduction: 0,
advance: 0,
final_payable: 0,
excess_short: 0,
};
contractorMap.forEach((contractor) => {
// Calculate derived values
contractor.total = contractor.as_per_contractor + contractor.dana +
contractor.tukdi + contractor.groundnut + contractor.commission_salary;
contractor.tds_base_amount = Math.round(contractor.total * 0.01); // 1% TDS
contractor.payable_before_deduction = contractor.total - contractor.tds_base_amount;
contractor.security_deduction = Math.round(contractor.total * 0.0035); // 0.35% security
contractor.final_payable = contractor.payable_before_deduction -
contractor.security_deduction - contractor.advance;
contractor.excess_short = contractor.as_per_contractor - contractor.final_payable;
// Add to subtotal
subTotal.as_per_contractor += contractor.as_per_contractor;
subTotal.dana += contractor.dana;
subTotal.tukdi += contractor.tukdi;
subTotal.groundnut += contractor.groundnut;
subTotal.commission_salary += contractor.commission_salary;
subTotal.total += contractor.total;
subTotal.tds_base_amount += contractor.tds_base_amount;
subTotal.payable_before_deduction += contractor.payable_before_deduction;
subTotal.security_deduction += contractor.security_deduction;
subTotal.advance += contractor.advance;
subTotal.final_payable += contractor.final_payable;
subTotal.excess_short += contractor.excess_short;
contractors.push(contractor);
});
result.push({
sub_department_id: subDeptId,
sub_department_name: subDeptNames.get(subDeptId) || "Other",
contractors,
subTotal,
});
});
return result;
};
// Format date for display
const formatDateRange = () => {
const start = new Date(startDate);
const end = new Date(endDate);
return `${start.toLocaleDateString("en-GB", { day: "2-digit", month: "2-digit", year: "numeric" }).replace(/\//g, ".")} TO ${end.toLocaleDateString("en-GB", { day: "2-digit", month: "2-digit", year: "numeric" }).replace(/\//g, ".")}`;
};
// Export to Excel
const exportToExcel = () => {
if (reportData.length === 0) {
alert("No data to export");
return;
}
const wb = XLSX.utils.book_new();
const wsData: any[][] = [];
// Header rows
wsData.push([companyName]);
wsData.push([formatDateRange()]);
wsData.push(["ALL CONTRACTOR PAYMENT SHEET"]);
wsData.push([]);
// Column headers
wsData.push([
"S.NO", "NAME CONTRACTOR", "As per Contractor", "", "", "", "", "",
"TDS @1%", "Payable", "SECURITY", "ADVANCE", "Final Payable", "Excess/Short"
]);
wsData.push([
"", "", "ALL", "DANA", "TUKDI", "GROUNDNUT", "Commission & Salary", "Total",
"(BASE AMOUNT)", "(Before deduction)", "DEDUCTION 0.35%", "", "", ""
]);
let serialNo = 1;
reportData.forEach((subDept) => {
// Sub-department header
wsData.push([subDept.sub_department_name.toUpperCase()]);
subDept.contractors.forEach((contractor) => {
wsData.push([
serialNo++,
contractor.contractor_name,
contractor.as_per_contractor || "",
contractor.dana || "",
contractor.tukdi || "",
contractor.groundnut || "",
contractor.commission_salary || "",
contractor.total || "",
contractor.tds_base_amount || "",
contractor.payable_before_deduction || "",
contractor.security_deduction || "",
contractor.advance || "",
contractor.final_payable || "",
contractor.excess_short || "",
]);
});
// Sub Total row
wsData.push([
"",
"Sub Total",
subDept.subTotal.as_per_contractor,
subDept.subTotal.dana,
subDept.subTotal.tukdi,
subDept.subTotal.groundnut,
subDept.subTotal.commission_salary,
subDept.subTotal.total,
subDept.subTotal.tds_base_amount,
subDept.subTotal.payable_before_deduction,
subDept.subTotal.security_deduction,
subDept.subTotal.advance,
subDept.subTotal.final_payable,
subDept.subTotal.excess_short,
]);
});
// Footer
wsData.push([]);
wsData.push(["CONTRACTOR", "", "", "", "", "", "CHECKER", "", "", "", "", "", "AUTHORISED AUTHORITY"]);
const ws = XLSX.utils.aoa_to_sheet(wsData);
// Set column widths
ws["!cols"] = [
{ wch: 5 }, // S.NO
{ wch: 30 }, // NAME CONTRACTOR
{ wch: 12 }, // ALL
{ wch: 10 }, // DANA
{ wch: 10 }, // TUKDI
{ wch: 12 }, // GROUNDNUT
{ wch: 14 }, // Commission
{ wch: 12 }, // Total
{ wch: 12 }, // TDS
{ wch: 12 }, // Payable
{ wch: 12 }, // Security
{ wch: 10 }, // Advance
{ wch: 14 }, // Final Payable
{ wch: 12 }, // Excess/Short
];
XLSX.utils.book_append_sheet(wb, ws, "Payment Report");
const filename = `contractor_payment_${startDate}_to_${endDate}.xlsx`;
XLSX.writeFile(wb, filename);
};
// Print report
const printReport = () => {
window.print();
};
// Calculate grand totals
const grandTotal = useMemo(() => {
const total: ContractorPaymentData = {
contractor_id: 0,
contractor_name: "Grand Total",
as_per_contractor: 0,
dana: 0,
tukdi: 0,
groundnut: 0,
commission_salary: 0,
total: 0,
tds_base_amount: 0,
payable_before_deduction: 0,
security_deduction: 0,
advance: 0,
final_payable: 0,
excess_short: 0,
};
reportData.forEach((subDept) => {
total.as_per_contractor += subDept.subTotal.as_per_contractor;
total.dana += subDept.subTotal.dana;
total.tukdi += subDept.subTotal.tukdi;
total.groundnut += subDept.subTotal.groundnut;
total.commission_salary += subDept.subTotal.commission_salary;
total.total += subDept.subTotal.total;
total.tds_base_amount += subDept.subTotal.tds_base_amount;
total.payable_before_deduction += subDept.subTotal.payable_before_deduction;
total.security_deduction += subDept.subTotal.security_deduction;
total.advance += subDept.subTotal.advance;
total.final_payable += subDept.subTotal.final_payable;
total.excess_short += subDept.subTotal.excess_short;
});
return total;
}, [reportData]);
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">
Contractor Payment Report
</h2>
</div>
<div className="flex gap-2">
<Button variant="secondary" onClick={printReport} disabled={reportData.length === 0}>
<Printer size={16} className="mr-2" />
Print
</Button>
<Button onClick={exportToExcel} disabled={reportData.length === 0}>
<Download size={16} className="mr-2" />
Export to Excel
</Button>
</div>
</div>
</div>
<CardContent>
{/* Filters */}
<div className="mb-6 p-4 bg-gray-50 rounded-lg border border-gray-200">
<div className="grid grid-cols-4 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Company Name</label>
<input
type="text"
value={companyName}
onChange={(e) => setCompanyName(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Department</label>
<select
value={selectedDepartment}
onChange={(e) => setSelectedDepartment(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500"
>
<option value="">Select Department</option>
{departments.map((dept) => (
<option key={dept.id} value={dept.id}>{dept.name}</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Start Date</label>
<input
type="date"
value={startDate}
onChange={(e) => setStartDate(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">End Date</label>
<input
type="date"
value={endDate}
onChange={(e) => setEndDate(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500"
/>
</div>
</div>
<div className="mt-4">
<Button onClick={fetchReport} disabled={loading}>
<RefreshCw size={16} className={`mr-2 ${loading ? "animate-spin" : ""}`} />
{loading ? "Generating..." : "Generate Report"}
</Button>
</div>
</div>
{/* Report Table */}
{reportData.length > 0 && (
<div className="overflow-x-auto print:overflow-visible" id="payment-report">
<div className="min-w-[1200px]">
{/* Report Header */}
<div className="text-center mb-4 bg-yellow-300 py-2">
<h1 className="text-lg font-bold text-black">{companyName}</h1>
<p className="text-sm font-semibold">{formatDateRange()}</p>
<p className="text-base font-bold">ALL CONTRACTOR PAYMENT SHEET</p>
</div>
{/* Table */}
<table className="w-full border-collapse text-sm">
<thead>
<tr className="bg-yellow-300">
<th rowSpan={2} className="border border-black px-2 py-1 text-left">S.NO</th>
<th rowSpan={2} className="border border-black px-2 py-1 text-left">NAME CONTRACTOR</th>
<th colSpan={6} className="border border-black px-2 py-1 text-center">As per DAFPL</th>
<th rowSpan={2} className="border border-black px-2 py-1 text-center">TDS @1%<br/>(BASE AMOUNT)</th>
<th rowSpan={2} className="border border-black px-2 py-1 text-center">Payable<br/>(Before deduction)</th>
<th rowSpan={2} className="border border-black px-2 py-1 text-center">SECURITY<br/>DEDUCTION 0.35%</th>
<th rowSpan={2} className="border border-black px-2 py-1 text-center">ADVANCE</th>
<th rowSpan={2} className="border border-black px-2 py-1 text-center">Final Payable</th>
<th rowSpan={2} className="border border-black px-2 py-1 text-center">Excess/Short</th>
</tr>
<tr className="bg-yellow-300">
<th className="border border-black px-2 py-1 text-center">ALL</th>
<th className="border border-black px-2 py-1 text-center">DANA</th>
<th className="border border-black px-2 py-1 text-center">TUKDI</th>
<th className="border border-black px-2 py-1 text-center">GROUNDNUT</th>
<th className="border border-black px-2 py-1 text-center">Commission<br/>& Salary</th>
<th className="border border-black px-2 py-1 text-center">Total</th>
</tr>
</thead>
<tbody>
{reportData.map((subDept, subDeptIdx) => (
<React.Fragment key={subDept.sub_department_id}>
{/* Sub-department header */}
<tr className="bg-yellow-300">
<td colSpan={14} className="border border-black px-2 py-1 font-bold">
{subDept.sub_department_name.toUpperCase()}
</td>
</tr>
{/* Contractors */}
{subDept.contractors.map((contractor, idx) => {
const rowNum = reportData
.slice(0, subDeptIdx)
.reduce((acc, sd) => acc + sd.contractors.length, 0) + idx + 1;
return (
<tr key={contractor.contractor_id} className="hover:bg-gray-50">
<td className="border border-black px-2 py-1">{rowNum}</td>
<td className="border border-black px-2 py-1 font-medium">{contractor.contractor_name}</td>
<td className="border border-black px-2 py-1 text-right">{contractor.as_per_contractor || ""}</td>
<td className="border border-black px-2 py-1 text-right">{contractor.dana || ""}</td>
<td className="border border-black px-2 py-1 text-right">{contractor.tukdi || ""}</td>
<td className="border border-black px-2 py-1 text-right">{contractor.groundnut || ""}</td>
<td className="border border-black px-2 py-1 text-right">{contractor.commission_salary || ""}</td>
<td className="border border-black px-2 py-1 text-right">{contractor.total || ""}</td>
<td className="border border-black px-2 py-1 text-right">{contractor.tds_base_amount || ""}</td>
<td className="border border-black px-2 py-1 text-right">{contractor.payable_before_deduction || ""}</td>
<td className="border border-black px-2 py-1 text-right">{contractor.security_deduction || ""}</td>
<td className="border border-black px-2 py-1 text-right">{contractor.advance || ""}</td>
<td className="border border-black px-2 py-1 text-right">{contractor.final_payable || ""}</td>
<td className="border border-black px-2 py-1 text-right">{contractor.excess_short || ""}</td>
</tr>
);
})}
{/* Sub Total */}
<tr className="bg-yellow-300 font-bold">
<td className="border border-black px-2 py-1"></td>
<td className="border border-black px-2 py-1">Sub Total</td>
<td className="border border-black px-2 py-1 text-right">{subDept.subTotal.as_per_contractor}</td>
<td className="border border-black px-2 py-1 text-right">{subDept.subTotal.dana}</td>
<td className="border border-black px-2 py-1 text-right">{subDept.subTotal.tukdi}</td>
<td className="border border-black px-2 py-1 text-right">{subDept.subTotal.groundnut}</td>
<td className="border border-black px-2 py-1 text-right">{subDept.subTotal.commission_salary}</td>
<td className="border border-black px-2 py-1 text-right">{subDept.subTotal.total}</td>
<td className="border border-black px-2 py-1 text-right">{subDept.subTotal.tds_base_amount}</td>
<td className="border border-black px-2 py-1 text-right">{subDept.subTotal.payable_before_deduction}</td>
<td className="border border-black px-2 py-1 text-right">{subDept.subTotal.security_deduction}</td>
<td className="border border-black px-2 py-1 text-right">{subDept.subTotal.advance}</td>
<td className="border border-black px-2 py-1 text-right">{subDept.subTotal.final_payable}</td>
<td className="border border-black px-2 py-1 text-right">{subDept.subTotal.excess_short}</td>
</tr>
</React.Fragment>
))}
{/* Grand Total */}
<tr className="bg-yellow-400 font-bold text-base">
<td className="border border-black px-2 py-2"></td>
<td className="border border-black px-2 py-2">GRAND TOTAL</td>
<td className="border border-black px-2 py-2 text-right">{grandTotal.as_per_contractor}</td>
<td className="border border-black px-2 py-2 text-right">{grandTotal.dana}</td>
<td className="border border-black px-2 py-2 text-right">{grandTotal.tukdi}</td>
<td className="border border-black px-2 py-2 text-right">{grandTotal.groundnut}</td>
<td className="border border-black px-2 py-2 text-right">{grandTotal.commission_salary}</td>
<td className="border border-black px-2 py-2 text-right">{grandTotal.total}</td>
<td className="border border-black px-2 py-2 text-right">{grandTotal.tds_base_amount}</td>
<td className="border border-black px-2 py-2 text-right">{grandTotal.payable_before_deduction}</td>
<td className="border border-black px-2 py-2 text-right">{grandTotal.security_deduction}</td>
<td className="border border-black px-2 py-2 text-right">{grandTotal.advance}</td>
<td className="border border-black px-2 py-2 text-right">{grandTotal.final_payable}</td>
<td className="border border-black px-2 py-2 text-right">{grandTotal.excess_short}</td>
</tr>
</tbody>
</table>
{/* Footer */}
<div className="mt-8 flex justify-between text-sm font-medium border-t border-black pt-4">
<div className="text-center">
<div className="border-t border-black w-40 pt-2">CONTRACTOR</div>
</div>
<div className="text-center">
<div className="border-t border-black w-40 pt-2">CHECKER</div>
</div>
<div className="text-center">
<div className="border-t border-black w-48 pt-2">AUTHORISED AUTHORITY</div>
</div>
</div>
</div>
</div>
)}
{reportData.length === 0 && !loading && (
<div className="text-center py-12 text-gray-500">
<FileSpreadsheet size={48} className="mx-auto mb-4 text-gray-300" />
<p>Select a department and date range, then click "Generate Report"</p>
</div>
)}
</CardContent>
</Card>
{/* Print Styles */}
<style>{`
@media print {
body * {
visibility: hidden;
}
#payment-report, #payment-report * {
visibility: visible;
}
#payment-report {
position: absolute;
left: 0;
top: 0;
width: 100%;
}
.no-print {
display: none !important;
}
}
`}</style>
</div>
);
};