(Fix): Fixed the excel reporting system, fixed alot of hardcoded dropdown values in activities, auto selection of departments is also implemented
This commit is contained in:
@@ -17,11 +17,13 @@ router.get("/", authenticateToken, async (ctx) => {
|
||||
SELECT cr.*,
|
||||
u.name as contractor_name, u.username as contractor_username,
|
||||
sd.name as sub_department_name,
|
||||
d.name as department_name
|
||||
d.name as department_name,
|
||||
a.unit_of_measurement
|
||||
FROM contractor_rates cr
|
||||
JOIN users u ON cr.contractor_id = u.id
|
||||
LEFT JOIN sub_departments sd ON cr.sub_department_id = sd.id
|
||||
LEFT JOIN departments d ON sd.department_id = d.id
|
||||
LEFT JOIN activities a ON a.sub_department_id = cr.sub_department_id AND a.name = cr.activity
|
||||
WHERE 1=1
|
||||
`;
|
||||
const queryParams: unknown[] = [];
|
||||
@@ -57,10 +59,12 @@ router.get("/contractor/:contractorId/current", authenticateToken, async (ctx) =
|
||||
let query = `
|
||||
SELECT cr.*,
|
||||
u.name as contractor_name, u.username as contractor_username,
|
||||
sd.name as sub_department_name
|
||||
sd.name as sub_department_name,
|
||||
a.unit_of_measurement
|
||||
FROM contractor_rates cr
|
||||
JOIN users u ON cr.contractor_id = u.id
|
||||
LEFT JOIN sub_departments sd ON cr.sub_department_id = sd.id
|
||||
LEFT JOIN activities a ON a.sub_department_id = cr.sub_department_id AND a.name = cr.activity
|
||||
WHERE cr.contractor_id = ?
|
||||
`;
|
||||
const queryParams: unknown[] = [contractorId];
|
||||
@@ -130,10 +134,12 @@ router.post("/", authenticateToken, authorize("Supervisor", "SuperAdmin"), async
|
||||
const newRate = await db.query<ContractorRate[]>(
|
||||
`SELECT cr.*,
|
||||
u.name as contractor_name, u.username as contractor_username,
|
||||
sd.name as sub_department_name
|
||||
sd.name as sub_department_name,
|
||||
a.unit_of_measurement
|
||||
FROM contractor_rates cr
|
||||
JOIN users u ON cr.contractor_id = u.id
|
||||
LEFT JOIN sub_departments sd ON cr.sub_department_id = sd.id
|
||||
LEFT JOIN activities a ON a.sub_department_id = cr.sub_department_id AND a.name = cr.activity
|
||||
WHERE cr.id = ?`,
|
||||
[result.insertId]
|
||||
);
|
||||
@@ -197,10 +203,12 @@ router.put("/:id", authenticateToken, authorize("Supervisor", "SuperAdmin"), asy
|
||||
const updatedRate = await db.query<ContractorRate[]>(
|
||||
`SELECT cr.*,
|
||||
u.name as contractor_name, u.username as contractor_username,
|
||||
sd.name as sub_department_name
|
||||
sd.name as sub_department_name,
|
||||
a.unit_of_measurement
|
||||
FROM contractor_rates cr
|
||||
JOIN users u ON cr.contractor_id = u.id
|
||||
LEFT JOIN sub_departments sd ON cr.sub_department_id = sd.id
|
||||
LEFT JOIN activities a ON a.sub_department_id = cr.sub_department_id AND a.name = cr.activity
|
||||
WHERE cr.id = ?`,
|
||||
[rateId]
|
||||
);
|
||||
|
||||
@@ -34,11 +34,13 @@ router.get("/", authenticateToken, async (ctx) => {
|
||||
sd.name as sub_department_name,
|
||||
d.name as department_name,
|
||||
d.id as department_id,
|
||||
u.name as created_by_name
|
||||
u.name as created_by_name,
|
||||
a.unit_of_measurement
|
||||
FROM standard_rates sr
|
||||
LEFT JOIN sub_departments sd ON sr.sub_department_id = sd.id
|
||||
LEFT JOIN departments d ON sd.department_id = d.id
|
||||
LEFT JOIN users u ON sr.created_by = u.id
|
||||
LEFT JOIN activities a ON a.sub_department_id = sr.sub_department_id AND a.name = sr.activity
|
||||
WHERE 1=1
|
||||
`;
|
||||
const queryParams: unknown[] = [];
|
||||
@@ -209,10 +211,12 @@ router.get("/compare", authenticateToken, authorize("Supervisor", "SuperAdmin"),
|
||||
SELECT sr.*,
|
||||
sd.name as sub_department_name,
|
||||
d.name as department_name,
|
||||
d.id as department_id
|
||||
d.id as department_id,
|
||||
a.unit_of_measurement
|
||||
FROM standard_rates sr
|
||||
LEFT JOIN sub_departments sd ON sr.sub_department_id = sd.id
|
||||
LEFT JOIN departments d ON sd.department_id = d.id
|
||||
LEFT JOIN activities a ON a.sub_department_id = sr.sub_department_id AND a.name = sr.activity
|
||||
WHERE 1=1 ${departmentFilter}
|
||||
`;
|
||||
|
||||
@@ -231,11 +235,13 @@ router.get("/compare", authenticateToken, authorize("Supervisor", "SuperAdmin"),
|
||||
u.name as contractor_name,
|
||||
sd.name as sub_department_name,
|
||||
d.name as department_name,
|
||||
d.id as department_id
|
||||
d.id as department_id,
|
||||
a.unit_of_measurement
|
||||
FROM contractor_rates cr
|
||||
JOIN users u ON cr.contractor_id = u.id
|
||||
LEFT JOIN sub_departments sd ON cr.sub_department_id = sd.id
|
||||
LEFT JOIN departments d ON sd.department_id = d.id
|
||||
LEFT JOIN activities a ON a.sub_department_id = cr.sub_department_id AND a.name = cr.activity
|
||||
WHERE 1=1
|
||||
`;
|
||||
const contractorParams: unknown[] = [];
|
||||
@@ -337,11 +343,13 @@ router.post("/", authenticateToken, authorize("Supervisor", "SuperAdmin"), async
|
||||
`SELECT sr.*,
|
||||
sd.name as sub_department_name,
|
||||
d.name as department_name,
|
||||
u.name as created_by_name
|
||||
u.name as created_by_name,
|
||||
a.unit_of_measurement
|
||||
FROM standard_rates sr
|
||||
LEFT JOIN sub_departments sd ON sr.sub_department_id = sd.id
|
||||
LEFT JOIN departments d ON sd.department_id = d.id
|
||||
LEFT JOIN users u ON sr.created_by = u.id
|
||||
LEFT JOIN activities a ON a.sub_department_id = sr.sub_department_id AND a.name = sr.activity
|
||||
WHERE sr.id = ?`,
|
||||
[result.insertId]
|
||||
);
|
||||
@@ -421,11 +429,13 @@ router.put("/:id", authenticateToken, authorize("Supervisor", "SuperAdmin"), asy
|
||||
`SELECT sr.*,
|
||||
sd.name as sub_department_name,
|
||||
d.name as department_name,
|
||||
u.name as created_by_name
|
||||
u.name as created_by_name,
|
||||
a.unit_of_measurement
|
||||
FROM standard_rates sr
|
||||
LEFT JOIN sub_departments sd ON sr.sub_department_id = sd.id
|
||||
LEFT JOIN departments d ON sd.department_id = d.id
|
||||
LEFT JOIN users u ON sr.created_by = u.id
|
||||
LEFT JOIN activities a ON a.sub_department_id = sr.sub_department_id AND a.name = sr.activity
|
||||
WHERE sr.id = ?`,
|
||||
[rateId]
|
||||
);
|
||||
|
||||
106
package-lock.json
generated
106
package-lock.json
generated
@@ -11,7 +11,8 @@
|
||||
"lucide-react": "^0.555.0",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"recharts": "^3.5.0"
|
||||
"recharts": "^3.5.0",
|
||||
"xlsx": "^0.18.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.1",
|
||||
@@ -1769,6 +1770,15 @@
|
||||
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/adler-32": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz",
|
||||
"integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/ajv": {
|
||||
"version": "6.12.6",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
@@ -1995,6 +2005,19 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"node_modules/cfb": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz",
|
||||
"integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"adler-32": "~1.3.0",
|
||||
"crc-32": "~1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
@@ -2054,6 +2077,15 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/codepage": {
|
||||
"version": "1.15.0",
|
||||
"resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz",
|
||||
"integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
@@ -2090,6 +2122,18 @@
|
||||
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/crc-32": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
|
||||
"integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"crc32": "bin/crc32.njs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
@@ -2648,6 +2692,15 @@
|
||||
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
|
||||
"integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="
|
||||
},
|
||||
"node_modules/frac": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz",
|
||||
"integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/fraction.js": {
|
||||
"version": "5.3.4",
|
||||
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
|
||||
@@ -3721,6 +3774,18 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ssf": {
|
||||
"version": "0.11.2",
|
||||
"resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz",
|
||||
"integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"frac": "~1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-json-comments": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
|
||||
@@ -4106,6 +4171,24 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/wmf": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz",
|
||||
"integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/word": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz",
|
||||
"integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/word-wrap": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
|
||||
@@ -4114,6 +4197,27 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/xlsx": {
|
||||
"version": "0.18.5",
|
||||
"resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz",
|
||||
"integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"adler-32": "~1.3.0",
|
||||
"cfb": "~1.2.1",
|
||||
"codepage": "~1.15.0",
|
||||
"crc-32": "~1.2.1",
|
||||
"ssf": "~0.11.2",
|
||||
"wmf": "~1.0.1",
|
||||
"word": "~0.3.0"
|
||||
},
|
||||
"bin": {
|
||||
"xlsx": "bin/xlsx.njs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/yallist": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
||||
|
||||
@@ -13,7 +13,8 @@
|
||||
"lucide-react": "^0.555.0",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"recharts": "^3.5.0"
|
||||
"recharts": "^3.5.0",
|
||||
"xlsx": "^0.18.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.1",
|
||||
|
||||
@@ -273,12 +273,10 @@ export const ActivitiesPage: React.FC = () => {
|
||||
{/* Sub-Departments List */}
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Sub-Department Name</TableHead>
|
||||
<TableHead>Activities Count</TableHead>
|
||||
<TableHead>Created At</TableHead>
|
||||
<TableHead className="text-right">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{subDepartments.length === 0 ? (
|
||||
@@ -358,13 +356,11 @@ export const ActivitiesPage: React.FC = () => {
|
||||
{/* Activities List */}
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Activity Name</TableHead>
|
||||
<TableHead>Sub-Department</TableHead>
|
||||
<TableHead>Unit of Measurement</TableHead>
|
||||
<TableHead>Created At</TableHead>
|
||||
<TableHead className="text-right">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{activities.length === 0 ? (
|
||||
|
||||
@@ -6,6 +6,7 @@ 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 { useActivities } from '../hooks/useActivities';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
|
||||
export const RatesPage: React.FC = () => {
|
||||
@@ -16,7 +17,6 @@ export const RatesPage: React.FC = () => {
|
||||
const [contractors, setContractors] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
// Form state
|
||||
const [formData, setFormData] = useState({
|
||||
contractorId: '',
|
||||
@@ -27,6 +27,7 @@ export const RatesPage: React.FC = () => {
|
||||
});
|
||||
const [selectedDept, setSelectedDept] = useState('');
|
||||
const { subDepartments } = useSubDepartments(selectedDept);
|
||||
const { activities } = useActivities(formData.subDepartmentId);
|
||||
const [formError, setFormError] = useState('');
|
||||
const [formLoading, setFormLoading] = useState(false);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
@@ -72,7 +73,24 @@ export const RatesPage: React.FC = () => {
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
|
||||
const { name, value } = e.target;
|
||||
|
||||
// Auto-select department when contractor is selected
|
||||
if (name === 'contractorId' && value) {
|
||||
const selectedContractor = contractors.find(c => String(c.id) === value);
|
||||
if (selectedContractor?.department_id) {
|
||||
setSelectedDept(String(selectedContractor.department_id));
|
||||
// Clear sub-department and activity when contractor changes
|
||||
setFormData(prev => ({ ...prev, [name]: value, subDepartmentId: '', activity: '' }));
|
||||
} else {
|
||||
setFormData(prev => ({ ...prev, [name]: value }));
|
||||
}
|
||||
}
|
||||
// Clear activity when sub-department changes
|
||||
else if (name === 'subDepartmentId') {
|
||||
setFormData(prev => ({ ...prev, [name]: value, activity: '' }));
|
||||
} else {
|
||||
setFormData(prev => ({ ...prev, [name]: value }));
|
||||
}
|
||||
setFormError('');
|
||||
};
|
||||
|
||||
@@ -239,7 +257,7 @@ export const RatesPage: React.FC = () => {
|
||||
<TableCell>{rate.sub_department_name || '-'}</TableCell>
|
||||
<TableCell>
|
||||
<span className={`px-2 py-1 rounded text-xs font-medium ${
|
||||
rate.activity === 'Loading' || rate.activity === 'Unloading'
|
||||
rate.unit_of_measurement === 'Per Bag'
|
||||
? 'bg-blue-100 text-blue-700'
|
||||
: 'bg-gray-100 text-gray-700'
|
||||
}`}>
|
||||
@@ -248,7 +266,7 @@ export const RatesPage: React.FC = () => {
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-xs text-gray-500">
|
||||
{rate.activity === 'Loading' || rate.activity === 'Unloading'
|
||||
{rate.unit_of_measurement === 'Per Bag'
|
||||
? 'Per Unit'
|
||||
: 'Flat Rate'}
|
||||
</span>
|
||||
@@ -302,8 +320,8 @@ export const RatesPage: React.FC = () => {
|
||||
<div className="p-4 bg-blue-50 border border-blue-200 rounded-md">
|
||||
<h4 className="font-medium text-blue-800 mb-2">Rate Calculation Info</h4>
|
||||
<ul className="text-sm text-blue-700 space-y-1">
|
||||
<li><strong>Loading/Unloading:</strong> Total = Units × Rate per Unit</li>
|
||||
<li><strong>Standard/Other:</strong> Total = Flat Rate (no unit calculation)</li>
|
||||
<li><strong>Per Bag Activities:</strong> Total = Units × Rate per Unit</li>
|
||||
<li><strong>Fixed Rate Activities:</strong> Total = Flat Rate (no unit calculation)</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -359,18 +377,22 @@ export const RatesPage: React.FC = () => {
|
||||
name="activity"
|
||||
value={formData.activity}
|
||||
onChange={handleInputChange}
|
||||
disabled={!formData.subDepartmentId}
|
||||
options={[
|
||||
{ value: '', label: 'Select Activity (Optional)' },
|
||||
{ value: 'Loading', label: 'Loading (per unit × rate)' },
|
||||
{ value: 'Unloading', label: 'Unloading (per unit × rate)' },
|
||||
{ value: 'Standard', label: 'Standard Work (flat rate)' },
|
||||
{ value: 'Other', label: 'Other (flat rate)' },
|
||||
{ value: '', label: formData.subDepartmentId ? 'Select Activity (Optional)' : 'Select Sub-Department First' },
|
||||
...activities.map(a => ({
|
||||
value: a.name,
|
||||
label: `${a.name} (${a.unit_of_measurement === 'Per Bag' ? 'per unit × rate' : 'flat rate'})`
|
||||
}))
|
||||
]}
|
||||
/>
|
||||
<Input
|
||||
label={formData.activity === 'Loading' || formData.activity === 'Unloading'
|
||||
label={(() => {
|
||||
const selectedActivity = activities.find(a => a.name === formData.activity);
|
||||
return selectedActivity?.unit_of_measurement === 'Per Bag'
|
||||
? "Rate per Unit (₹)"
|
||||
: "Standard Rate (₹)"}
|
||||
: "Rate Amount (₹)";
|
||||
})()}
|
||||
name="rate"
|
||||
type="number"
|
||||
value={formData.rate}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Input, Select } from '../components/ui/Input';
|
||||
import { api } from '../services/api';
|
||||
import { useDepartments } from '../hooks/useDepartments';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { exportWorkReportToXLSX, exportAllocationsToXLSX } from '../utils/excelExport';
|
||||
|
||||
export const ReportingPage: React.FC = () => {
|
||||
const { user } = useAuth();
|
||||
@@ -71,74 +72,35 @@ export const ReportingPage: React.FC = () => {
|
||||
);
|
||||
}, [allocations, searchQuery]);
|
||||
|
||||
// Export to Excel (CSV format)
|
||||
const exportToExcel = () => {
|
||||
// Get selected department name
|
||||
const selectedDeptName = filters.departmentId
|
||||
? departments.find(d => d.id === parseInt(filters.departmentId))?.name || 'All Departments'
|
||||
: user?.role === 'Supervisor'
|
||||
? departments.find(d => d.id === user?.department_id)?.name || 'Department'
|
||||
: 'All Departments';
|
||||
|
||||
// Export to Excel (XLSX format) - Formatted Report
|
||||
const exportFormattedReport = () => {
|
||||
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',
|
||||
];
|
||||
exportWorkReportToXLSX(
|
||||
filteredAllocations,
|
||||
selectedDeptName,
|
||||
{ startDate: filters.startDate, endDate: filters.endDate }
|
||||
);
|
||||
};
|
||||
|
||||
// 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,
|
||||
]);
|
||||
// Export to Excel (XLSX format) - Simple List
|
||||
const exportSimpleList = () => {
|
||||
if (filteredAllocations.length === 0) {
|
||||
alert('No data to export');
|
||||
return;
|
||||
}
|
||||
|
||||
// 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);
|
||||
exportAllocationsToXLSX(filteredAllocations);
|
||||
};
|
||||
|
||||
const handleFilterChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
|
||||
@@ -169,10 +131,16 @@ export const ReportingPage: React.FC = () => {
|
||||
<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}>
|
||||
<div className="flex gap-2">
|
||||
<Button onClick={exportFormattedReport} disabled={filteredAllocations.length === 0}>
|
||||
<Download size={16} className="mr-2" />
|
||||
Export to Excel
|
||||
Export Report (XLSX)
|
||||
</Button>
|
||||
<Button variant="outline" onClick={exportSimpleList} disabled={filteredAllocations.length === 0}>
|
||||
<Download size={16} className="mr-2" />
|
||||
Export List
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ 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 { useActivities } from '../hooks/useActivities';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
|
||||
export const StandardRatesPage: React.FC = () => {
|
||||
@@ -28,6 +29,7 @@ export const StandardRatesPage: React.FC = () => {
|
||||
});
|
||||
const [selectedDept, setSelectedDept] = useState('');
|
||||
const { subDepartments } = useSubDepartments(selectedDept);
|
||||
const { activities } = useActivities(formData.subDepartmentId);
|
||||
const [formError, setFormError] = useState('');
|
||||
const [formLoading, setFormLoading] = useState(false);
|
||||
const [editingId, setEditingId] = useState<number | null>(null);
|
||||
@@ -98,7 +100,12 @@ export const StandardRatesPage: React.FC = () => {
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
|
||||
const { name, value } = e.target;
|
||||
// Clear activity when sub-department changes
|
||||
if (name === 'subDepartmentId') {
|
||||
setFormData(prev => ({ ...prev, [name]: value, activity: '' }));
|
||||
} else {
|
||||
setFormData(prev => ({ ...prev, [name]: value }));
|
||||
}
|
||||
setFormError('');
|
||||
};
|
||||
|
||||
@@ -278,7 +285,7 @@ export const StandardRatesPage: React.FC = () => {
|
||||
<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'
|
||||
rate.unit_of_measurement === 'Per Bag'
|
||||
? 'bg-blue-100 text-blue-700'
|
||||
: 'bg-gray-100 text-gray-700'
|
||||
}`}>
|
||||
@@ -380,17 +387,22 @@ export const StandardRatesPage: React.FC = () => {
|
||||
name="activity"
|
||||
value={formData.activity}
|
||||
onChange={handleInputChange}
|
||||
disabled={!formData.subDepartmentId}
|
||||
options={[
|
||||
{ value: '', label: 'Standard (Default)' },
|
||||
{ value: 'Loading', label: 'Loading (per unit)' },
|
||||
{ value: 'Unloading', label: 'Unloading (per unit)' },
|
||||
{ value: 'Other', label: 'Other' },
|
||||
{ value: '', label: formData.subDepartmentId ? 'Standard (Default)' : 'Select Sub-Department First' },
|
||||
...activities.map(a => ({
|
||||
value: a.name,
|
||||
label: `${a.name} (${a.unit_of_measurement === 'Per Bag' ? 'per unit' : 'flat rate'})`
|
||||
}))
|
||||
]}
|
||||
/>
|
||||
<Input
|
||||
label={formData.activity === 'Loading' || formData.activity === 'Unloading'
|
||||
label={(() => {
|
||||
const selectedActivity = activities.find(a => a.name === formData.activity);
|
||||
return selectedActivity?.unit_of_measurement === 'Per Bag'
|
||||
? "Rate per Unit (₹)"
|
||||
: "Standard Rate (₹)"}
|
||||
: "Standard Rate (₹)";
|
||||
})()}
|
||||
name="rate"
|
||||
type="number"
|
||||
value={formData.rate}
|
||||
@@ -468,7 +480,7 @@ export const StandardRatesPage: React.FC = () => {
|
||||
<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'
|
||||
comp.unit_of_measurement === 'Per Bag'
|
||||
? 'bg-blue-100 text-blue-700'
|
||||
: 'bg-gray-100 text-gray-700'
|
||||
}`}>
|
||||
|
||||
@@ -60,8 +60,12 @@ export const WorkAllocationPage: React.FC = () => {
|
||||
// Get selected rate details
|
||||
const selectedRate = contractorRates.find(r => r.id === parseInt(formData.rateId));
|
||||
|
||||
// Check if rate is per unit (Loading/Unloading)
|
||||
const isPerUnitRate = selectedRate?.activity === 'Loading' || selectedRate?.activity === 'Unloading';
|
||||
// Get selected activity details
|
||||
const selectedActivity = activities.find(a => a.name === formData.activity);
|
||||
|
||||
// Check if rate is per unit based on activity's unit_of_measurement
|
||||
const isPerUnitRate = selectedActivity?.unit_of_measurement === 'Per Bag' ||
|
||||
selectedRate?.unit_of_measurement === 'Per Bag';
|
||||
|
||||
// Calculate total amount
|
||||
const unitCount = parseFloat(formData.units) || 0;
|
||||
@@ -89,7 +93,24 @@ export const WorkAllocationPage: React.FC = () => {
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => {
|
||||
const { name, value } = e.target;
|
||||
|
||||
// Auto-select department when contractor is selected
|
||||
if (name === 'contractorId' && value) {
|
||||
const selectedContractor = contractors.find(c => String(c.id) === value);
|
||||
if (selectedContractor?.department_id) {
|
||||
setSelectedDept(String(selectedContractor.department_id));
|
||||
// Clear sub-department and activity when contractor changes
|
||||
setFormData(prev => ({ ...prev, [name]: value, subDepartmentId: '', activity: '', rateId: '' }));
|
||||
} else {
|
||||
setFormData(prev => ({ ...prev, [name]: value }));
|
||||
}
|
||||
}
|
||||
// Clear activity when sub-department changes
|
||||
else if (name === 'subDepartmentId') {
|
||||
setFormData(prev => ({ ...prev, [name]: value, activity: '' }));
|
||||
} else {
|
||||
setFormData(prev => ({ ...prev, [name]: value }));
|
||||
}
|
||||
setFormError('');
|
||||
};
|
||||
|
||||
@@ -252,7 +273,10 @@ export const WorkAllocationPage: React.FC = () => {
|
||||
disabled={!formData.subDepartmentId}
|
||||
options={[
|
||||
{ value: '', label: formData.subDepartmentId ? 'Select Activity' : 'Select Sub-Department First' },
|
||||
...activities.map(a => ({ value: a.name, label: a.name }))
|
||||
...activities.map(a => ({
|
||||
value: a.name,
|
||||
label: `${a.name} (${a.unit_of_measurement === 'Per Bag' ? 'per unit' : 'flat rate'})`
|
||||
}))
|
||||
]}
|
||||
/>
|
||||
<Input
|
||||
|
||||
306
src/utils/excelExport.ts
Normal file
306
src/utils/excelExport.ts
Normal file
@@ -0,0 +1,306 @@
|
||||
import * as XLSX from 'xlsx';
|
||||
|
||||
interface AllocationData {
|
||||
id: number;
|
||||
employee_name: string;
|
||||
contractor_name: string;
|
||||
department_name: string;
|
||||
sub_department_name: string;
|
||||
activity: string;
|
||||
assigned_date: string;
|
||||
completion_date: string;
|
||||
rate: number;
|
||||
units: number;
|
||||
total_amount: number;
|
||||
status: string;
|
||||
}
|
||||
|
||||
interface WorkReportData {
|
||||
work: string;
|
||||
dates: {
|
||||
[date: string]: {
|
||||
bag: number;
|
||||
rate: number;
|
||||
total: number;
|
||||
};
|
||||
};
|
||||
totalBag: number;
|
||||
totalAmount: number;
|
||||
standardBag?: number;
|
||||
standardRate?: number;
|
||||
standardTotal?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Export work allocation report to XLSX with proper formatting
|
||||
* Format matches the provided image with:
|
||||
* - DATE header row spanning columns for each date
|
||||
* - Bag, Rate, Total sub-headers for each date
|
||||
* - Department section headers with yellow background
|
||||
* - Work activities listed with data
|
||||
* - Sub Total row at bottom
|
||||
* - Total-As per Standard columns
|
||||
*/
|
||||
export const exportWorkReportToXLSX = (
|
||||
allocations: AllocationData[],
|
||||
departmentName: string,
|
||||
_dateRange: { startDate: string; endDate: string }
|
||||
) => {
|
||||
// Group allocations by work (activity + sub_department) and date
|
||||
const workDataMap = new Map<string, WorkReportData>();
|
||||
const allDates = new Set<string>();
|
||||
|
||||
allocations.forEach(allocation => {
|
||||
const workKey = `${allocation.sub_department_name || ''} ${allocation.activity || 'Standard'}`.trim();
|
||||
const date = allocation.assigned_date ? new Date(allocation.assigned_date).getDate().toString() : '';
|
||||
|
||||
if (date) {
|
||||
allDates.add(date);
|
||||
}
|
||||
|
||||
if (!workDataMap.has(workKey)) {
|
||||
workDataMap.set(workKey, {
|
||||
work: workKey,
|
||||
dates: {},
|
||||
totalBag: 0,
|
||||
totalAmount: 0,
|
||||
});
|
||||
}
|
||||
|
||||
const workData = workDataMap.get(workKey)!;
|
||||
|
||||
if (!workData.dates[date]) {
|
||||
workData.dates[date] = { bag: 0, rate: 0, total: 0 };
|
||||
}
|
||||
|
||||
const bag = parseFloat(String(allocation.units)) || 0;
|
||||
const rate = parseFloat(String(allocation.rate)) || 0;
|
||||
const total = parseFloat(String(allocation.total_amount)) || (bag * rate) || rate;
|
||||
|
||||
workData.dates[date].bag += bag;
|
||||
workData.dates[date].rate = rate; // Use latest rate
|
||||
workData.dates[date].total += total;
|
||||
workData.totalBag += bag;
|
||||
workData.totalAmount += total;
|
||||
});
|
||||
|
||||
// Sort dates numerically
|
||||
const sortedDates = Array.from(allDates).sort((a, b) => parseInt(a) - parseInt(b));
|
||||
|
||||
// Create workbook and worksheet
|
||||
const wb = XLSX.utils.book_new();
|
||||
const wsData: (string | number | null)[][] = [];
|
||||
|
||||
// Row 1: DATE header with merged cells for each date
|
||||
const dateHeaderRow: (string | number | null)[] = ['', 'DATE'];
|
||||
sortedDates.forEach(date => {
|
||||
dateHeaderRow.push(date, '', ''); // Each date spans 3 columns (Bag, Rate, Total)
|
||||
});
|
||||
dateHeaderRow.push('', 'Total', '', '', 'Total-As per Standered', '', '');
|
||||
wsData.push(dateHeaderRow);
|
||||
|
||||
// Row 2: WORK and Bag/Rate/Total sub-headers
|
||||
const subHeaderRow: (string | number | null)[] = ['', 'WORK'];
|
||||
sortedDates.forEach(() => {
|
||||
subHeaderRow.push('Bag', 'Rate', 'Total');
|
||||
});
|
||||
subHeaderRow.push('', 'Bag', 'Rate', 'Total', 'Bag', 'Rate', 'Total');
|
||||
wsData.push(subHeaderRow);
|
||||
|
||||
// Row 3: Department header (yellow background)
|
||||
const deptHeaderRow: (string | number | null)[] = ['', `${departmentName.toUpperCase()} Department`];
|
||||
const deptHeaderCols = sortedDates.length * 3 + 7;
|
||||
for (let col = 0; col < deptHeaderCols; col++) {
|
||||
deptHeaderRow.push('');
|
||||
}
|
||||
wsData.push(deptHeaderRow);
|
||||
|
||||
// Data rows for each work item
|
||||
const workDataArray = Array.from(workDataMap.values());
|
||||
|
||||
workDataArray.forEach((workData, index) => {
|
||||
const dataRow: (string | number | null)[] = [index + 1, workData.work];
|
||||
|
||||
sortedDates.forEach(date => {
|
||||
const dateData = workData.dates[date] || { bag: 0, rate: 0, total: 0 };
|
||||
dataRow.push(
|
||||
dateData.bag || '',
|
||||
dateData.rate || '',
|
||||
dateData.total || ''
|
||||
);
|
||||
});
|
||||
|
||||
// Total columns
|
||||
dataRow.push(''); // Empty column
|
||||
dataRow.push(workData.totalBag || '');
|
||||
dataRow.push(''); // Rate for total (could be average)
|
||||
dataRow.push(workData.totalAmount || '');
|
||||
|
||||
// Standard columns (placeholder - would need standard rates data)
|
||||
dataRow.push('');
|
||||
dataRow.push('');
|
||||
dataRow.push('');
|
||||
|
||||
wsData.push(dataRow);
|
||||
});
|
||||
|
||||
// Empty row before Sub Total
|
||||
wsData.push([]);
|
||||
|
||||
// Sub Total row
|
||||
const subTotalRow: (string | number | null)[] = ['', 'Sub Total'];
|
||||
|
||||
// Calculate totals for each date
|
||||
sortedDates.forEach(date => {
|
||||
let dateBagTotal = 0;
|
||||
let dateTotalAmount = 0;
|
||||
workDataArray.forEach(workData => {
|
||||
const dateData = workData.dates[date];
|
||||
if (dateData) {
|
||||
dateBagTotal += dateData.bag;
|
||||
dateTotalAmount += dateData.total;
|
||||
}
|
||||
});
|
||||
subTotalRow.push(dateBagTotal || '', '', dateTotalAmount || '');
|
||||
});
|
||||
|
||||
// Grand totals
|
||||
const grandTotalBag = workDataArray.reduce((sum, w) => sum + w.totalBag, 0);
|
||||
const grandTotalAmount = workDataArray.reduce((sum, w) => sum + w.totalAmount, 0);
|
||||
|
||||
subTotalRow.push('');
|
||||
subTotalRow.push(grandTotalBag || '');
|
||||
subTotalRow.push('');
|
||||
subTotalRow.push(grandTotalAmount || '');
|
||||
subTotalRow.push('');
|
||||
subTotalRow.push('');
|
||||
subTotalRow.push(grandTotalAmount || ''); // Standard total same as actual for now
|
||||
|
||||
wsData.push(subTotalRow);
|
||||
|
||||
// Create worksheet
|
||||
const ws = XLSX.utils.aoa_to_sheet(wsData);
|
||||
|
||||
// Set column widths
|
||||
const colWidths: { wch: number }[] = [
|
||||
{ wch: 4 }, // A - Row number
|
||||
{ wch: 35 }, // B - Work name
|
||||
];
|
||||
|
||||
// Add widths for date columns
|
||||
sortedDates.forEach(() => {
|
||||
colWidths.push({ wch: 8 }); // Bag
|
||||
colWidths.push({ wch: 6 }); // Rate
|
||||
colWidths.push({ wch: 10 }); // Total
|
||||
});
|
||||
|
||||
// Total columns
|
||||
colWidths.push({ wch: 3 }); // Empty
|
||||
colWidths.push({ wch: 10 }); // Total Bag
|
||||
colWidths.push({ wch: 6 }); // Total Rate
|
||||
colWidths.push({ wch: 12 }); // Total Amount
|
||||
colWidths.push({ wch: 10 }); // Standard Bag
|
||||
colWidths.push({ wch: 6 }); // Standard Rate
|
||||
colWidths.push({ wch: 12 }); // Standard Total
|
||||
|
||||
ws['!cols'] = colWidths;
|
||||
|
||||
// Merge cells for DATE headers
|
||||
const merges: XLSX.Range[] = [];
|
||||
|
||||
// Merge DATE header cells for each date (row 1)
|
||||
let colIndex = 2; // Start after row number and WORK columns
|
||||
sortedDates.forEach(() => {
|
||||
merges.push({
|
||||
s: { r: 0, c: colIndex },
|
||||
e: { r: 0, c: colIndex + 2 }
|
||||
});
|
||||
colIndex += 3;
|
||||
});
|
||||
|
||||
// Merge Total header
|
||||
merges.push({
|
||||
s: { r: 0, c: colIndex + 1 },
|
||||
e: { r: 0, c: colIndex + 3 }
|
||||
});
|
||||
|
||||
// Merge "Total-As per Standered" header
|
||||
merges.push({
|
||||
s: { r: 0, c: colIndex + 4 },
|
||||
e: { r: 0, c: colIndex + 6 }
|
||||
});
|
||||
|
||||
// Merge department header row
|
||||
merges.push({
|
||||
s: { r: 2, c: 1 },
|
||||
e: { r: 2, c: colIndex + 6 }
|
||||
});
|
||||
|
||||
ws['!merges'] = merges;
|
||||
|
||||
// Add worksheet to workbook
|
||||
XLSX.utils.book_append_sheet(wb, ws, 'Work Report');
|
||||
|
||||
// Generate filename
|
||||
const filename = `work_report_${departmentName.toLowerCase().replace(/\s+/g, '_')}_${new Date().toISOString().split('T')[0]}.xlsx`;
|
||||
|
||||
// Write and download
|
||||
XLSX.writeFile(wb, filename);
|
||||
};
|
||||
|
||||
/**
|
||||
* Simple export for basic allocation data
|
||||
*/
|
||||
export const exportAllocationsToXLSX = (
|
||||
allocations: AllocationData[],
|
||||
filename?: string
|
||||
) => {
|
||||
if (allocations.length === 0) {
|
||||
alert('No data to export');
|
||||
return;
|
||||
}
|
||||
|
||||
// Transform data for export
|
||||
const exportData = allocations.map((a, index) => ({
|
||||
'S.No': index + 1,
|
||||
'Employee Name': a.employee_name || '',
|
||||
'Contractor': a.contractor_name || '',
|
||||
'Department': a.department_name || '',
|
||||
'Sub-Department': a.sub_department_name || '',
|
||||
'Activity': a.activity || 'Standard',
|
||||
'Assigned Date': a.assigned_date ? new Date(a.assigned_date).toLocaleDateString() : '',
|
||||
'Completion Date': a.completion_date ? new Date(a.completion_date).toLocaleDateString() : '',
|
||||
'Rate': a.rate || 0,
|
||||
'Units': a.units || '',
|
||||
'Total Amount': a.total_amount || a.rate || 0,
|
||||
'Status': a.status || '',
|
||||
}));
|
||||
|
||||
// Create workbook
|
||||
const wb = XLSX.utils.book_new();
|
||||
const ws = XLSX.utils.json_to_sheet(exportData);
|
||||
|
||||
// Set column widths
|
||||
ws['!cols'] = [
|
||||
{ wch: 6 }, // S.No
|
||||
{ wch: 25 }, // Employee Name
|
||||
{ wch: 20 }, // Contractor
|
||||
{ wch: 15 }, // Department
|
||||
{ wch: 20 }, // Sub-Department
|
||||
{ wch: 15 }, // Activity
|
||||
{ wch: 12 }, // Assigned Date
|
||||
{ wch: 14 }, // Completion Date
|
||||
{ wch: 10 }, // Rate
|
||||
{ wch: 8 }, // Units
|
||||
{ wch: 12 }, // Total Amount
|
||||
{ wch: 10 }, // Status
|
||||
];
|
||||
|
||||
XLSX.utils.book_append_sheet(wb, ws, 'Allocations');
|
||||
|
||||
// Generate filename
|
||||
const outputFilename = filename || `allocations_${new Date().toISOString().split('T')[0]}.xlsx`;
|
||||
|
||||
// Write and download
|
||||
XLSX.writeFile(wb, outputFilename);
|
||||
};
|
||||
Reference in New Issue
Block a user