(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:
2025-12-18 10:11:22 +00:00
parent 2a38cf372a
commit 01400ad4e1
10 changed files with 568 additions and 117 deletions

View File

@@ -17,11 +17,13 @@ router.get("/", authenticateToken, async (ctx) => {
SELECT cr.*, SELECT cr.*,
u.name as contractor_name, u.username as contractor_username, u.name as contractor_name, u.username as contractor_username,
sd.name as sub_department_name, sd.name as sub_department_name,
d.name as department_name d.name as department_name,
a.unit_of_measurement
FROM contractor_rates cr FROM contractor_rates cr
JOIN users u ON cr.contractor_id = u.id JOIN users u ON cr.contractor_id = u.id
LEFT JOIN sub_departments sd ON cr.sub_department_id = sd.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 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 WHERE 1=1
`; `;
const queryParams: unknown[] = []; const queryParams: unknown[] = [];
@@ -57,10 +59,12 @@ router.get("/contractor/:contractorId/current", authenticateToken, async (ctx) =
let query = ` let query = `
SELECT cr.*, SELECT cr.*,
u.name as contractor_name, u.username as contractor_username, 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 FROM contractor_rates cr
JOIN users u ON cr.contractor_id = u.id JOIN users u ON cr.contractor_id = u.id
LEFT JOIN sub_departments sd ON cr.sub_department_id = sd.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 = ? WHERE cr.contractor_id = ?
`; `;
const queryParams: unknown[] = [contractorId]; const queryParams: unknown[] = [contractorId];
@@ -130,10 +134,12 @@ router.post("/", authenticateToken, authorize("Supervisor", "SuperAdmin"), async
const newRate = await db.query<ContractorRate[]>( const newRate = await db.query<ContractorRate[]>(
`SELECT cr.*, `SELECT cr.*,
u.name as contractor_name, u.username as contractor_username, 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 FROM contractor_rates cr
JOIN users u ON cr.contractor_id = u.id JOIN users u ON cr.contractor_id = u.id
LEFT JOIN sub_departments sd ON cr.sub_department_id = sd.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 = ?`, WHERE cr.id = ?`,
[result.insertId] [result.insertId]
); );
@@ -197,10 +203,12 @@ router.put("/:id", authenticateToken, authorize("Supervisor", "SuperAdmin"), asy
const updatedRate = await db.query<ContractorRate[]>( const updatedRate = await db.query<ContractorRate[]>(
`SELECT cr.*, `SELECT cr.*,
u.name as contractor_name, u.username as contractor_username, 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 FROM contractor_rates cr
JOIN users u ON cr.contractor_id = u.id JOIN users u ON cr.contractor_id = u.id
LEFT JOIN sub_departments sd ON cr.sub_department_id = sd.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 = ?`, WHERE cr.id = ?`,
[rateId] [rateId]
); );

View File

@@ -34,11 +34,13 @@ router.get("/", authenticateToken, async (ctx) => {
sd.name as sub_department_name, sd.name as sub_department_name,
d.name as department_name, d.name as department_name,
d.id as department_id, 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 FROM standard_rates sr
LEFT JOIN sub_departments sd ON sr.sub_department_id = sd.id LEFT JOIN sub_departments sd ON sr.sub_department_id = sd.id
LEFT JOIN departments d ON sd.department_id = d.id LEFT JOIN departments d ON sd.department_id = d.id
LEFT JOIN users u ON sr.created_by = u.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 WHERE 1=1
`; `;
const queryParams: unknown[] = []; const queryParams: unknown[] = [];
@@ -209,10 +211,12 @@ router.get("/compare", authenticateToken, authorize("Supervisor", "SuperAdmin"),
SELECT sr.*, SELECT sr.*,
sd.name as sub_department_name, sd.name as sub_department_name,
d.name as 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 FROM standard_rates sr
LEFT JOIN sub_departments sd ON sr.sub_department_id = sd.id LEFT JOIN sub_departments sd ON sr.sub_department_id = sd.id
LEFT JOIN departments d ON sd.department_id = d.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} WHERE 1=1 ${departmentFilter}
`; `;
@@ -231,11 +235,13 @@ router.get("/compare", authenticateToken, authorize("Supervisor", "SuperAdmin"),
u.name as contractor_name, u.name as contractor_name,
sd.name as sub_department_name, sd.name as sub_department_name,
d.name as 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 FROM contractor_rates cr
JOIN users u ON cr.contractor_id = u.id JOIN users u ON cr.contractor_id = u.id
LEFT JOIN sub_departments sd ON cr.sub_department_id = sd.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 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 WHERE 1=1
`; `;
const contractorParams: unknown[] = []; const contractorParams: unknown[] = [];
@@ -337,11 +343,13 @@ router.post("/", authenticateToken, authorize("Supervisor", "SuperAdmin"), async
`SELECT sr.*, `SELECT sr.*,
sd.name as sub_department_name, sd.name as sub_department_name,
d.name as 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 FROM standard_rates sr
LEFT JOIN sub_departments sd ON sr.sub_department_id = sd.id LEFT JOIN sub_departments sd ON sr.sub_department_id = sd.id
LEFT JOIN departments d ON sd.department_id = d.id LEFT JOIN departments d ON sd.department_id = d.id
LEFT JOIN users u ON sr.created_by = u.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 = ?`, WHERE sr.id = ?`,
[result.insertId] [result.insertId]
); );
@@ -421,11 +429,13 @@ router.put("/:id", authenticateToken, authorize("Supervisor", "SuperAdmin"), asy
`SELECT sr.*, `SELECT sr.*,
sd.name as sub_department_name, sd.name as sub_department_name,
d.name as 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 FROM standard_rates sr
LEFT JOIN sub_departments sd ON sr.sub_department_id = sd.id LEFT JOIN sub_departments sd ON sr.sub_department_id = sd.id
LEFT JOIN departments d ON sd.department_id = d.id LEFT JOIN departments d ON sd.department_id = d.id
LEFT JOIN users u ON sr.created_by = u.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 = ?`, WHERE sr.id = ?`,
[rateId] [rateId]
); );

106
package-lock.json generated
View File

@@ -11,7 +11,8 @@
"lucide-react": "^0.555.0", "lucide-react": "^0.555.0",
"react": "^19.2.0", "react": "^19.2.0",
"react-dom": "^19.2.0", "react-dom": "^19.2.0",
"recharts": "^3.5.0" "recharts": "^3.5.0",
"xlsx": "^0.18.5"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.39.1", "@eslint/js": "^9.39.1",
@@ -1769,6 +1770,15 @@
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" "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": { "node_modules/ajv": {
"version": "6.12.6", "version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "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": { "node_modules/chalk": {
"version": "4.1.2", "version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@@ -2054,6 +2077,15 @@
"node": ">=6" "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": { "node_modules/color-convert": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -2090,6 +2122,18 @@
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
"dev": true "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": { "node_modules/cross-spawn": {
"version": "7.0.6", "version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "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", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
"integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==" "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": { "node_modules/fraction.js": {
"version": "5.3.4", "version": "5.3.4",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
@@ -3721,6 +3774,18 @@
"node": ">=0.10.0" "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": { "node_modules/strip-json-comments": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
@@ -4106,6 +4171,24 @@
"node": ">= 8" "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": { "node_modules/word-wrap": {
"version": "1.2.5", "version": "1.2.5",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
@@ -4114,6 +4197,27 @@
"node": ">=0.10.0" "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": { "node_modules/yallist": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",

View File

@@ -13,7 +13,8 @@
"lucide-react": "^0.555.0", "lucide-react": "^0.555.0",
"react": "^19.2.0", "react": "^19.2.0",
"react-dom": "^19.2.0", "react-dom": "^19.2.0",
"recharts": "^3.5.0" "recharts": "^3.5.0",
"xlsx": "^0.18.5"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.39.1", "@eslint/js": "^9.39.1",

View File

@@ -273,12 +273,10 @@ export const ActivitiesPage: React.FC = () => {
{/* Sub-Departments List */} {/* Sub-Departments List */}
<Table> <Table>
<TableHeader> <TableHeader>
<TableRow> <TableHead>Sub-Department Name</TableHead>
<TableHead>Sub-Department Name</TableHead> <TableHead>Activities Count</TableHead>
<TableHead>Activities Count</TableHead> <TableHead>Created At</TableHead>
<TableHead>Created At</TableHead> <TableHead className="text-right">Actions</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{subDepartments.length === 0 ? ( {subDepartments.length === 0 ? (
@@ -358,13 +356,11 @@ export const ActivitiesPage: React.FC = () => {
{/* Activities List */} {/* Activities List */}
<Table> <Table>
<TableHeader> <TableHeader>
<TableRow> <TableHead>Activity Name</TableHead>
<TableHead>Activity Name</TableHead> <TableHead>Sub-Department</TableHead>
<TableHead>Sub-Department</TableHead> <TableHead>Unit of Measurement</TableHead>
<TableHead>Unit of Measurement</TableHead> <TableHead>Created At</TableHead>
<TableHead>Created At</TableHead> <TableHead className="text-right">Actions</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{activities.length === 0 ? ( {activities.length === 0 ? (

View File

@@ -6,6 +6,7 @@ import { Button } from '../components/ui/Button';
import { Input, Select } from '../components/ui/Input'; import { Input, Select } from '../components/ui/Input';
import { api } from '../services/api'; import { api } from '../services/api';
import { useDepartments, useSubDepartments } from '../hooks/useDepartments'; import { useDepartments, useSubDepartments } from '../hooks/useDepartments';
import { useActivities } from '../hooks/useActivities';
import { useAuth } from '../contexts/AuthContext'; import { useAuth } from '../contexts/AuthContext';
export const RatesPage: React.FC = () => { export const RatesPage: React.FC = () => {
@@ -16,7 +17,6 @@ export const RatesPage: React.FC = () => {
const [contractors, setContractors] = useState<any[]>([]); const [contractors, setContractors] = useState<any[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState(''); const [error, setError] = useState('');
// Form state // Form state
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
contractorId: '', contractorId: '',
@@ -27,6 +27,7 @@ export const RatesPage: React.FC = () => {
}); });
const [selectedDept, setSelectedDept] = useState(''); const [selectedDept, setSelectedDept] = useState('');
const { subDepartments } = useSubDepartments(selectedDept); const { subDepartments } = useSubDepartments(selectedDept);
const { activities } = useActivities(formData.subDepartmentId);
const [formError, setFormError] = useState(''); const [formError, setFormError] = useState('');
const [formLoading, setFormLoading] = useState(false); const [formLoading, setFormLoading] = useState(false);
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
@@ -72,7 +73,24 @@ export const RatesPage: React.FC = () => {
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => { const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
const { name, value } = e.target; const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
// 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(''); setFormError('');
}; };
@@ -239,7 +257,7 @@ export const RatesPage: React.FC = () => {
<TableCell>{rate.sub_department_name || '-'}</TableCell> <TableCell>{rate.sub_department_name || '-'}</TableCell>
<TableCell> <TableCell>
<span className={`px-2 py-1 rounded text-xs font-medium ${ <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-blue-100 text-blue-700'
: 'bg-gray-100 text-gray-700' : 'bg-gray-100 text-gray-700'
}`}> }`}>
@@ -248,7 +266,7 @@ export const RatesPage: React.FC = () => {
</TableCell> </TableCell>
<TableCell> <TableCell>
<span className="text-xs text-gray-500"> <span className="text-xs text-gray-500">
{rate.activity === 'Loading' || rate.activity === 'Unloading' {rate.unit_of_measurement === 'Per Bag'
? 'Per Unit' ? 'Per Unit'
: 'Flat Rate'} : 'Flat Rate'}
</span> </span>
@@ -302,8 +320,8 @@ export const RatesPage: React.FC = () => {
<div className="p-4 bg-blue-50 border border-blue-200 rounded-md"> <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> <h4 className="font-medium text-blue-800 mb-2">Rate Calculation Info</h4>
<ul className="text-sm text-blue-700 space-y-1"> <ul className="text-sm text-blue-700 space-y-1">
<li><strong>Loading/Unloading:</strong> Total = Units × Rate per Unit</li> <li><strong>Per Bag Activities:</strong> Total = Units × Rate per Unit</li>
<li><strong>Standard/Other:</strong> Total = Flat Rate (no unit calculation)</li> <li><strong>Fixed Rate Activities:</strong> Total = Flat Rate (no unit calculation)</li>
</ul> </ul>
</div> </div>
@@ -359,18 +377,22 @@ export const RatesPage: React.FC = () => {
name="activity" name="activity"
value={formData.activity} value={formData.activity}
onChange={handleInputChange} onChange={handleInputChange}
disabled={!formData.subDepartmentId}
options={[ options={[
{ value: '', label: 'Select Activity (Optional)' }, { value: '', label: formData.subDepartmentId ? 'Select Activity (Optional)' : 'Select Sub-Department First' },
{ value: 'Loading', label: 'Loading (per unit × rate)' }, ...activities.map(a => ({
{ value: 'Unloading', label: 'Unloading (per unit × rate)' }, value: a.name,
{ value: 'Standard', label: 'Standard Work (flat rate)' }, label: `${a.name} (${a.unit_of_measurement === 'Per Bag' ? 'per unit × rate' : 'flat rate'})`
{ value: 'Other', label: 'Other (flat rate)' }, }))
]} ]}
/> />
<Input <Input
label={formData.activity === 'Loading' || formData.activity === 'Unloading' label={(() => {
? "Rate per Unit (₹)" const selectedActivity = activities.find(a => a.name === formData.activity);
: "Standard Rate (₹)"} return selectedActivity?.unit_of_measurement === 'Per Bag'
? "Rate per Unit (₹)"
: "Rate Amount (₹)";
})()}
name="rate" name="rate"
type="number" type="number"
value={formData.rate} value={formData.rate}

View File

@@ -7,6 +7,7 @@ import { Input, Select } from '../components/ui/Input';
import { api } from '../services/api'; import { api } from '../services/api';
import { useDepartments } from '../hooks/useDepartments'; import { useDepartments } from '../hooks/useDepartments';
import { useAuth } from '../contexts/AuthContext'; import { useAuth } from '../contexts/AuthContext';
import { exportWorkReportToXLSX, exportAllocationsToXLSX } from '../utils/excelExport';
export const ReportingPage: React.FC = () => { export const ReportingPage: React.FC = () => {
const { user } = useAuth(); const { user } = useAuth();
@@ -71,74 +72,35 @@ export const ReportingPage: React.FC = () => {
); );
}, [allocations, searchQuery]); }, [allocations, searchQuery]);
// Export to Excel (CSV format) // Get selected department name
const exportToExcel = () => { 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) { if (filteredAllocations.length === 0) {
alert('No data to export'); alert('No data to export');
return; return;
} }
// Define headers exportWorkReportToXLSX(
const headers = [ filteredAllocations,
'ID', selectedDeptName,
'Employee Name', { startDate: filters.startDate, endDate: filters.endDate }
'Employee Phone', );
'Contractor', };
'Department',
'Sub-Department',
'Activity',
'Assigned Date',
'Completion Date',
'Rate (₹)',
'Units',
'Total Amount (₹)',
'Status',
];
// Map data to rows // Export to Excel (XLSX format) - Simple List
const rows = filteredAllocations.map(a => [ const exportSimpleList = () => {
a.id, if (filteredAllocations.length === 0) {
a.employee_name || '', alert('No data to export');
a.employee_phone || '', return;
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 exportAllocationsToXLSX(filteredAllocations);
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 handleFilterChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
@@ -169,10 +131,16 @@ export const ReportingPage: React.FC = () => {
<FileSpreadsheet className="text-green-600" size={24} /> <FileSpreadsheet className="text-green-600" size={24} />
<h2 className="text-xl font-semibold text-gray-800">Work Allocation Reports</h2> <h2 className="text-xl font-semibold text-gray-800">Work Allocation Reports</h2>
</div> </div>
<Button onClick={exportToExcel} disabled={filteredAllocations.length === 0}> <div className="flex gap-2">
<Download size={16} className="mr-2" /> <Button onClick={exportFormattedReport} disabled={filteredAllocations.length === 0}>
Export to Excel <Download size={16} className="mr-2" />
</Button> 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>
</div> </div>

View File

@@ -6,6 +6,7 @@ import { Button } from '../components/ui/Button';
import { Input, Select } from '../components/ui/Input'; import { Input, Select } from '../components/ui/Input';
import { api } from '../services/api'; import { api } from '../services/api';
import { useDepartments, useSubDepartments } from '../hooks/useDepartments'; import { useDepartments, useSubDepartments } from '../hooks/useDepartments';
import { useActivities } from '../hooks/useActivities';
import { useAuth } from '../contexts/AuthContext'; import { useAuth } from '../contexts/AuthContext';
export const StandardRatesPage: React.FC = () => { export const StandardRatesPage: React.FC = () => {
@@ -28,6 +29,7 @@ export const StandardRatesPage: React.FC = () => {
}); });
const [selectedDept, setSelectedDept] = useState(''); const [selectedDept, setSelectedDept] = useState('');
const { subDepartments } = useSubDepartments(selectedDept); const { subDepartments } = useSubDepartments(selectedDept);
const { activities } = useActivities(formData.subDepartmentId);
const [formError, setFormError] = useState(''); const [formError, setFormError] = useState('');
const [formLoading, setFormLoading] = useState(false); const [formLoading, setFormLoading] = useState(false);
const [editingId, setEditingId] = useState<number | null>(null); const [editingId, setEditingId] = useState<number | null>(null);
@@ -98,7 +100,12 @@ export const StandardRatesPage: React.FC = () => {
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => { const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
const { name, value } = e.target; const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value })); // Clear activity when sub-department changes
if (name === 'subDepartmentId') {
setFormData(prev => ({ ...prev, [name]: value, activity: '' }));
} else {
setFormData(prev => ({ ...prev, [name]: value }));
}
setFormError(''); setFormError('');
}; };
@@ -278,7 +285,7 @@ export const StandardRatesPage: React.FC = () => {
<TableCell className="font-medium">{rate.sub_department_name || 'All'}</TableCell> <TableCell className="font-medium">{rate.sub_department_name || 'All'}</TableCell>
<TableCell> <TableCell>
<span className={`px-2 py-1 rounded text-xs font-medium ${ <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-blue-100 text-blue-700'
: 'bg-gray-100 text-gray-700' : 'bg-gray-100 text-gray-700'
}`}> }`}>
@@ -380,17 +387,22 @@ export const StandardRatesPage: React.FC = () => {
name="activity" name="activity"
value={formData.activity} value={formData.activity}
onChange={handleInputChange} onChange={handleInputChange}
disabled={!formData.subDepartmentId}
options={[ options={[
{ value: '', label: 'Standard (Default)' }, { value: '', label: formData.subDepartmentId ? 'Standard (Default)' : 'Select Sub-Department First' },
{ value: 'Loading', label: 'Loading (per unit)' }, ...activities.map(a => ({
{ value: 'Unloading', label: 'Unloading (per unit)' }, value: a.name,
{ value: 'Other', label: 'Other' }, label: `${a.name} (${a.unit_of_measurement === 'Per Bag' ? 'per unit' : 'flat rate'})`
}))
]} ]}
/> />
<Input <Input
label={formData.activity === 'Loading' || formData.activity === 'Unloading' label={(() => {
? "Rate per Unit (₹)" const selectedActivity = activities.find(a => a.name === formData.activity);
: "Standard Rate (₹)"} return selectedActivity?.unit_of_measurement === 'Per Bag'
? "Rate per Unit (₹)"
: "Standard Rate (₹)";
})()}
name="rate" name="rate"
type="number" type="number"
value={formData.rate} value={formData.rate}
@@ -468,7 +480,7 @@ export const StandardRatesPage: React.FC = () => {
<TableCell className="font-medium">{comp.sub_department_name || 'All'}</TableCell> <TableCell className="font-medium">{comp.sub_department_name || 'All'}</TableCell>
<TableCell> <TableCell>
<span className={`px-2 py-1 rounded text-xs font-medium ${ <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-blue-100 text-blue-700'
: 'bg-gray-100 text-gray-700' : 'bg-gray-100 text-gray-700'
}`}> }`}>

View File

@@ -60,8 +60,12 @@ export const WorkAllocationPage: React.FC = () => {
// Get selected rate details // Get selected rate details
const selectedRate = contractorRates.find(r => r.id === parseInt(formData.rateId)); const selectedRate = contractorRates.find(r => r.id === parseInt(formData.rateId));
// Check if rate is per unit (Loading/Unloading) // Get selected activity details
const isPerUnitRate = selectedRate?.activity === 'Loading' || selectedRate?.activity === 'Unloading'; 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 // Calculate total amount
const unitCount = parseFloat(formData.units) || 0; const unitCount = parseFloat(formData.units) || 0;
@@ -89,7 +93,24 @@ export const WorkAllocationPage: React.FC = () => {
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => { const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => {
const { name, value } = e.target; const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
// 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(''); setFormError('');
}; };
@@ -252,7 +273,10 @@ export const WorkAllocationPage: React.FC = () => {
disabled={!formData.subDepartmentId} disabled={!formData.subDepartmentId}
options={[ options={[
{ value: '', label: formData.subDepartmentId ? 'Select Activity' : 'Select Sub-Department First' }, { 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 <Input

306
src/utils/excelExport.ts Normal file
View 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);
};