(Feat-Fix): Lots of fixes done, reporting system fixed, stricter types

This commit is contained in:
2025-12-19 18:48:05 +00:00
parent 01400ad4e1
commit 865e0bf00e
61 changed files with 10072 additions and 6645 deletions

View File

@@ -1,36 +1,43 @@
import React, { useState, useEffect, useMemo } from 'react';
import { Plus, RefreshCw, Trash2, Edit, DollarSign, Search } from 'lucide-react';
import { Card, CardHeader, CardContent } from '../components/ui/Card';
import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from '../components/ui/Table';
import { Button } from '../components/ui/Button';
import { Input, Select } from '../components/ui/Input';
import { api } from '../services/api';
import { useDepartments, useSubDepartments } from '../hooks/useDepartments';
import { useActivities } from '../hooks/useActivities';
import { useAuth } from '../contexts/AuthContext';
import React, { useEffect, useMemo, useState } from "react";
import { DollarSign, Edit, RefreshCw, Search, Trash2 } from "lucide-react";
import { Card, CardContent } from "../components/ui/Card.tsx";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "../components/ui/Table.tsx";
import { Button } from "../components/ui/Button.tsx";
import { Input, Select } from "../components/ui/Input.tsx";
import { api } from "../services/api.ts";
import { useDepartments, useSubDepartments } from "../hooks/useDepartments.ts";
import { useActivities } from "../hooks/useActivities.ts";
import { useAuth } from "../contexts/AuthContext.tsx";
export const RatesPage: React.FC = () => {
const [activeTab, setActiveTab] = useState<'list' | 'add'>('list');
const [activeTab, setActiveTab] = useState<"list" | "add">("list");
const { user } = useAuth();
const { departments } = useDepartments();
const [rates, setRates] = useState<any[]>([]);
const [contractors, setContractors] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [error, setError] = useState("");
// Form state
const [formData, setFormData] = useState({
contractorId: '',
subDepartmentId: '',
activity: '',
rate: '',
effectiveDate: new Date().toISOString().split('T')[0],
contractorId: "",
subDepartmentId: "",
activity: "",
rate: "",
effectiveDate: new Date().toISOString().split("T")[0],
});
const [selectedDept, setSelectedDept] = useState('');
const [selectedDept, setSelectedDept] = useState("");
const { subDepartments } = useSubDepartments(selectedDept);
const { activities } = useActivities(formData.subDepartmentId);
const [formError, setFormError] = useState('');
const [formError, setFormError] = useState("");
const [formLoading, setFormLoading] = useState(false);
const [searchQuery, setSearchQuery] = useState('');
const [searchQuery, setSearchQuery] = useState("");
// Edit mode
const [editingId, setEditingId] = useState<number | null>(null);
@@ -38,12 +45,12 @@ export const RatesPage: React.FC = () => {
// Fetch rates
const fetchRates = async () => {
setLoading(true);
setError('');
setError("");
try {
const data = await api.getContractorRates();
setRates(data);
} catch (err: any) {
setError(err.message || 'Failed to fetch rates');
setError(err.message || "Failed to fetch rates");
} finally {
setLoading(false);
}
@@ -52,10 +59,10 @@ export const RatesPage: React.FC = () => {
// Fetch contractors
const fetchContractors = async () => {
try {
const data = await api.getUsers({ role: 'Contractor' });
const data = await api.getUsers({ role: "Contractor" });
setContractors(data);
} catch (err) {
console.error('Failed to fetch contractors:', err);
console.error("Failed to fetch contractors:", err);
}
};
@@ -66,54 +73,62 @@ export const RatesPage: React.FC = () => {
// Auto-select department for supervisors
useEffect(() => {
if (user?.role === 'Supervisor' && user?.department_id) {
if (user?.role === "Supervisor" && user?.department_id) {
setSelectedDept(String(user.department_id));
}
}, [user]);
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
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 (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: '' }));
setFormData((prev) => ({
...prev,
[name]: value,
subDepartmentId: "",
activity: "",
}));
} else {
setFormData(prev => ({ ...prev, [name]: value }));
setFormData((prev) => ({ ...prev, [name]: value }));
}
}
// Clear activity when sub-department changes
else if (name === 'subDepartmentId') {
setFormData(prev => ({ ...prev, [name]: value, activity: '' }));
} // Clear activity when sub-department changes
else if (name === "subDepartmentId") {
setFormData((prev) => ({ ...prev, [name]: value, activity: "" }));
} else {
setFormData(prev => ({ ...prev, [name]: value }));
setFormData((prev) => ({ ...prev, [name]: value }));
}
setFormError('');
setFormError("");
};
const resetForm = () => {
setFormData({
contractorId: '',
subDepartmentId: '',
activity: '',
rate: '',
effectiveDate: new Date().toISOString().split('T')[0],
contractorId: "",
subDepartmentId: "",
activity: "",
rate: "",
effectiveDate: new Date().toISOString().split("T")[0],
});
setEditingId(null);
setFormError('');
setFormError("");
};
const handleSubmit = async () => {
if (!formData.contractorId || !formData.rate || !formData.effectiveDate) {
setFormError('Contractor, rate, and effective date are required');
setFormError("Contractor, rate, and effective date are required");
return;
}
setFormLoading(true);
setFormError('');
setFormError("");
try {
if (editingId) {
@@ -125,7 +140,9 @@ export const RatesPage: React.FC = () => {
} else {
await api.setContractorRate({
contractorId: parseInt(formData.contractorId),
subDepartmentId: formData.subDepartmentId ? parseInt(formData.subDepartmentId) : undefined,
subDepartmentId: formData.subDepartmentId
? parseInt(formData.subDepartmentId)
: undefined,
activity: formData.activity || undefined,
rate: parseFloat(formData.rate),
effectiveDate: formData.effectiveDate,
@@ -133,10 +150,10 @@ export const RatesPage: React.FC = () => {
}
resetForm();
setActiveTab('list');
setActiveTab("list");
fetchRates();
} catch (err: any) {
setFormError(err.message || 'Failed to save rate');
setFormError(err.message || "Failed to save rate");
} finally {
setFormLoading(false);
}
@@ -145,32 +162,36 @@ export const RatesPage: React.FC = () => {
const handleEdit = (rate: any) => {
setFormData({
contractorId: String(rate.contractor_id),
subDepartmentId: rate.sub_department_id ? String(rate.sub_department_id) : '',
activity: rate.activity || '',
subDepartmentId: rate.sub_department_id
? String(rate.sub_department_id)
: "",
activity: rate.activity || "",
rate: String(rate.rate),
effectiveDate: rate.effective_date?.split('T')[0] || new Date().toISOString().split('T')[0],
effectiveDate: rate.effective_date?.split("T")[0] ||
new Date().toISOString().split("T")[0],
});
setEditingId(rate.id);
setActiveTab('add');
setActiveTab("add");
};
const handleDelete = async (id: number) => {
if (!confirm('Are you sure you want to delete this rate?')) return;
if (!confirm("Are you sure you want to delete this rate?")) return;
try {
await api.deleteContractorRate(id);
fetchRates();
} catch (err: any) {
alert(err.message || 'Failed to delete rate');
alert(err.message || "Failed to delete rate");
}
};
const canManageRates = user?.role === 'SuperAdmin' || user?.role === 'Supervisor';
const canManageRates = user?.role === "SuperAdmin" ||
user?.role === "Supervisor";
// Filter rates based on search
const filteredRates = useMemo(() => {
if (!searchQuery) return rates;
const query = searchQuery.toLowerCase();
return rates.filter(rate =>
return rates.filter((rate) =>
rate.contractor_name?.toLowerCase().includes(query) ||
rate.sub_department_name?.toLowerCase().includes(query) ||
rate.activity?.toLowerCase().includes(query)
@@ -183,36 +204,42 @@ export const RatesPage: React.FC = () => {
<div className="border-b border-gray-200">
<div className="flex space-x-8 px-6">
<button
onClick={() => { setActiveTab('list'); resetForm(); }}
onClick={() => {
setActiveTab("list");
resetForm();
}}
className={`py-4 px-2 border-b-2 font-medium text-sm ${
activeTab === 'list'
? 'border-blue-500 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700'
activeTab === "list"
? "border-blue-500 text-blue-600"
: "border-transparent text-gray-500 hover:text-gray-700"
}`}
>
Rate List
</button>
{canManageRates && (
<button
onClick={() => setActiveTab('add')}
onClick={() => setActiveTab("add")}
className={`py-4 px-2 border-b-2 font-medium text-sm ${
activeTab === 'add'
? 'border-blue-500 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700'
activeTab === "add"
? "border-blue-500 text-blue-600"
: "border-transparent text-gray-500 hover:text-gray-700"
}`}
>
{editingId ? 'Edit Rate' : 'Add Rate'}
{editingId ? "Edit Rate" : "Add Rate"}
</button>
)}
</div>
</div>
<CardContent>
{activeTab === 'list' && (
{activeTab === "list" && (
<div>
<div className="flex gap-4 mb-4">
<div className="relative min-w-[300px] flex-1">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400" size={18} />
<Search
className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"
size={18}
/>
<input
type="text"
placeholder="Search by contractor, sub-department, activity..."
@@ -226,7 +253,7 @@ export const RatesPage: React.FC = () => {
Refresh
</Button>
</div>
<div className="mb-4 text-sm text-gray-600">
Total Rates: {filteredRates.length}
</div>
@@ -237,91 +264,113 @@ export const RatesPage: React.FC = () => {
</div>
)}
{loading ? (
<div className="text-center py-8">Loading rates...</div>
) : filteredRates.length > 0 ? (
<Table>
<TableHeader>
<TableHead>Contractor</TableHead>
<TableHead>Sub-Department</TableHead>
<TableHead>Activity</TableHead>
<TableHead>Rate Type</TableHead>
<TableHead>Rate ()</TableHead>
<TableHead>Effective Date</TableHead>
{canManageRates && <TableHead>Actions</TableHead>}
</TableHeader>
<TableBody>
{filteredRates.map((rate) => (
<TableRow key={rate.id}>
<TableCell className="font-medium">{rate.contractor_name}</TableCell>
<TableCell>{rate.sub_department_name || '-'}</TableCell>
<TableCell>
<span className={`px-2 py-1 rounded text-xs font-medium ${
rate.unit_of_measurement === 'Per Bag'
? 'bg-blue-100 text-blue-700'
: 'bg-gray-100 text-gray-700'
}`}>
{rate.activity || 'Standard'}
</span>
</TableCell>
<TableCell>
<span className="text-xs text-gray-500">
{rate.unit_of_measurement === 'Per Bag'
? 'Per Unit'
: 'Flat Rate'}
</span>
</TableCell>
<TableCell>
<span className="text-green-600 font-semibold">{rate.rate}</span>
</TableCell>
<TableCell>{new Date(rate.effective_date).toLocaleDateString()}</TableCell>
{canManageRates && (
<TableCell>
<div className="flex gap-2">
<Button
variant="ghost"
size="sm"
onClick={() => handleEdit(rate)}
className="text-blue-600"
title="Edit"
>
<Edit size={14} />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => handleDelete(rate.id)}
className="text-red-600"
title="Delete"
>
<Trash2 size={14} />
</Button>
</div>
{loading
? <div className="text-center py-8">Loading rates...</div>
: filteredRates.length > 0
? (
<Table>
<TableHeader>
<TableHead>Contractor</TableHead>
<TableHead>Sub-Department</TableHead>
<TableHead>Activity</TableHead>
<TableHead>Rate Type</TableHead>
<TableHead>Rate ()</TableHead>
<TableHead>Effective Date</TableHead>
{canManageRates && <TableHead>Actions</TableHead>}
</TableHeader>
<TableBody>
{filteredRates.map((rate) => (
<TableRow key={rate.id}>
<TableCell className="font-medium">
{rate.contractor_name}
</TableCell>
)}
</TableRow>
))}
</TableBody>
</Table>
) : (
<div className="text-center py-8 text-gray-500">
{searchQuery ? 'No matching rates found' : 'No rates configured yet. Add one to get started!'}
</div>
)}
<TableCell>
{rate.sub_department_name || "-"}
</TableCell>
<TableCell>
<span
className={`px-2 py-1 rounded text-xs font-medium ${
rate.unit_of_measurement === "Per Bag"
? "bg-blue-100 text-blue-700"
: "bg-gray-100 text-gray-700"
}`}
>
{rate.activity || "Standard"}
</span>
</TableCell>
<TableCell>
<span className="text-xs text-gray-500">
{rate.unit_of_measurement === "Per Bag"
? "Per Unit"
: "Flat Rate"}
</span>
</TableCell>
<TableCell>
<span className="text-green-600 font-semibold">
{rate.rate}
</span>
</TableCell>
<TableCell>
{new Date(rate.effective_date).toLocaleDateString()}
</TableCell>
{canManageRates && (
<TableCell>
<div className="flex gap-2">
<Button
variant="ghost"
size="sm"
onClick={() => handleEdit(rate)}
className="text-blue-600"
title="Edit"
>
<Edit size={14} />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => handleDelete(rate.id)}
className="text-red-600"
title="Delete"
>
<Trash2 size={14} />
</Button>
</div>
</TableCell>
)}
</TableRow>
))}
</TableBody>
</Table>
)
: (
<div className="text-center py-8 text-gray-500">
{searchQuery
? "No matching rates found"
: "No rates configured yet. Add one to get started!"}
</div>
)}
</div>
)}
{activeTab === 'add' && canManageRates && (
{activeTab === "add" && canManageRates && (
<div className="max-w-2xl space-y-6">
<h3 className="text-lg font-semibold text-gray-800">
{editingId ? 'Edit Rate' : 'Add New Rate'}
{editingId ? "Edit Rate" : "Add New Rate"}
</h3>
<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">
<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>
<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>
@@ -340,27 +389,37 @@ export const RatesPage: React.FC = () => {
required
disabled={!!editingId}
options={[
{ value: '', label: 'Select Contractor' },
...contractors.map(c => ({ value: String(c.id), label: c.name }))
{ value: "", label: "Select Contractor" },
...contractors.map((c) => ({
value: String(c.id),
label: c.name,
})),
]}
/>
{user?.role === 'Supervisor' ? (
<Input
label="Department"
value={departments.find(d => d.id === user?.department_id)?.name || 'Loading...'}
disabled
/>
) : (
<Select
label="Department"
value={selectedDept}
onChange={(e) => setSelectedDept(e.target.value)}
options={[
{ value: '', label: 'Select Department' },
...departments.map(d => ({ value: String(d.id), label: d.name }))
]}
/>
)}
{user?.role === "Supervisor"
? (
<Input
label="Department"
value={departments.find((d) =>
d.id === user?.department_id
)?.name || "Loading..."}
disabled
/>
)
: (
<Select
label="Department"
value={selectedDept}
onChange={(e) => setSelectedDept(e.target.value)}
options={[
{ value: "", label: "Select Department" },
...departments.map((d) => ({
value: String(d.id),
label: d.name,
})),
]}
/>
)}
<Select
label="Sub-Department"
name="subDepartmentId"
@@ -368,8 +427,11 @@ export const RatesPage: React.FC = () => {
onChange={handleInputChange}
disabled={!!editingId}
options={[
{ value: '', label: 'Select Sub-Department (Optional)' },
...subDepartments.map(s => ({ value: String(s.id), label: s.name }))
{ value: "", label: "Select Sub-Department (Optional)" },
...subDepartments.map((s) => ({
value: String(s.id),
label: s.name,
})),
]}
/>
<Select
@@ -379,18 +441,29 @@ export const RatesPage: React.FC = () => {
onChange={handleInputChange}
disabled={!formData.subDepartmentId}
options={[
{ 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'})`
}))
{
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={(() => {
const selectedActivity = activities.find(a => a.name === formData.activity);
return selectedActivity?.unit_of_measurement === 'Per Bag'
? "Rate per Unit (₹)"
const selectedActivity = activities.find((a) =>
a.name === formData.activity
);
return selectedActivity?.unit_of_measurement === "Per Bag"
? "Rate per Unit (₹)"
: "Rate Amount (₹)";
})()}
name="rate"
@@ -411,14 +484,20 @@ export const RatesPage: React.FC = () => {
</div>
<div className="flex justify-end gap-4">
<Button variant="outline" onClick={() => { setActiveTab('list'); resetForm(); }}>
<Button
variant="outline"
onClick={() => {
setActiveTab("list");
resetForm();
}}
>
Cancel
</Button>
<Button onClick={handleSubmit} disabled={formLoading}>
{formLoading ? 'Saving...' : (
{formLoading ? "Saving..." : (
<>
<DollarSign size={16} className="mr-2" />
{editingId ? 'Update Rate' : 'Add Rate'}
{editingId ? "Update Rate" : "Add Rate"}
</>
)}
</Button>