Files
EmployeeManagementSystem/src/pages/RatesPage.tsx

512 lines
18 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.ts";
export const RatesPage: React.FC = () => {
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("");
// Form state
const [formData, setFormData] = useState({
contractorId: "",
subDepartmentId: "",
activity: "",
rate: "",
effectiveDate: new Date().toISOString().split("T")[0],
});
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("");
// Edit mode
const [editingId, setEditingId] = useState<number | null>(null);
// Fetch rates
const fetchRates = async () => {
setLoading(true);
setError("");
try {
const data = await api.getContractorRates();
setRates(data);
} catch (err: any) {
setError(err.message || "Failed to fetch rates");
} finally {
setLoading(false);
}
};
// Fetch contractors
const fetchContractors = async () => {
try {
const data = await api.getUsers({ role: "Contractor" });
setContractors(data);
} catch (err) {
console.error("Failed to fetch contractors:", err);
}
};
useEffect(() => {
fetchRates();
fetchContractors();
}, []);
// Auto-select department for supervisors
useEffect(() => {
if (user?.role === "Supervisor" && user?.department_id) {
setSelectedDept(String(user.department_id));
}
}, [user]);
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("");
};
const resetForm = () => {
setFormData({
contractorId: "",
subDepartmentId: "",
activity: "",
rate: "",
effectiveDate: new Date().toISOString().split("T")[0],
});
setEditingId(null);
setFormError("");
};
const handleSubmit = async () => {
if (!formData.contractorId || !formData.rate || !formData.effectiveDate) {
setFormError("Contractor, rate, and effective date are required");
return;
}
setFormLoading(true);
setFormError("");
try {
if (editingId) {
await api.updateContractorRate(editingId, {
rate: parseFloat(formData.rate),
activity: formData.activity || undefined,
effectiveDate: formData.effectiveDate,
});
} else {
await api.setContractorRate({
contractorId: parseInt(formData.contractorId),
subDepartmentId: formData.subDepartmentId
? parseInt(formData.subDepartmentId)
: undefined,
activity: formData.activity || undefined,
rate: parseFloat(formData.rate),
effectiveDate: formData.effectiveDate,
});
}
resetForm();
setActiveTab("list");
fetchRates();
} catch (err: any) {
setFormError(err.message || "Failed to save rate");
} finally {
setFormLoading(false);
}
};
const handleEdit = (rate: any) => {
setFormData({
contractorId: String(rate.contractor_id),
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],
});
setEditingId(rate.id);
setActiveTab("add");
};
const handleDelete = async (id: number) => {
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");
}
};
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) =>
rate.contractor_name?.toLowerCase().includes(query) ||
rate.sub_department_name?.toLowerCase().includes(query) ||
rate.activity?.toLowerCase().includes(query)
);
}, [rates, searchQuery]);
return (
<div className="p-6">
<Card>
<div className="border-b border-gray-200">
<div className="flex space-x-8 px-6">
<button
onClick={() => {
setActiveTab("list");
resetForm();
}}
className={`py-4 px-2 border-b-2 font-medium text-sm ${
activeTab === "list"
? "border-blue-500 text-blue-600"
: "border-transparent text-gray-500 hover:text-gray-700"
}`}
>
Rate List
</button>
{canManageRates && (
<button
onClick={() => setActiveTab("add")}
className={`py-4 px-2 border-b-2 font-medium text-sm ${
activeTab === "add"
? "border-blue-500 text-blue-600"
: "border-transparent text-gray-500 hover:text-gray-700"
}`}
>
{editingId ? "Edit Rate" : "Add Rate"}
</button>
)}
</div>
</div>
<CardContent>
{activeTab === "list" && (
<div>
<div className="flex gap-4 mb-4">
<div className="relative min-w-[300px] flex-1">
<Search
className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"
size={18}
/>
<input
type="text"
placeholder="Search by contractor, sub-department, activity..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
<Button variant="ghost" onClick={fetchRates}>
<RefreshCw size={16} className="mr-2" />
Refresh
</Button>
</div>
<div className="mb-4 text-sm text-gray-600">
Total Rates: {filteredRates.length}
</div>
{error && (
<div className="text-center py-8 text-red-600">
Error: {error}
</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>
</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 && (
<div className="max-w-2xl space-y-6">
<h3 className="text-lg font-semibold text-gray-800">
{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>
<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>
</ul>
</div>
{formError && (
<div className="p-3 bg-red-100 text-red-700 rounded-md">
{formError}
</div>
)}
<div className="grid grid-cols-2 gap-6">
<Select
label="Contractor"
name="contractorId"
value={formData.contractorId}
onChange={handleInputChange}
required
disabled={!!editingId}
options={[
{ 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,
})),
]}
/>
)}
<Select
label="Sub-Department"
name="subDepartmentId"
value={formData.subDepartmentId}
onChange={handleInputChange}
disabled={!!editingId}
options={[
{ value: "", label: "Select Sub-Department (Optional)" },
...subDepartments.map((s) => ({
value: String(s.id),
label: s.name,
})),
]}
/>
<Select
label="Activity Type"
name="activity"
value={formData.activity}
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"
})`,
})),
]}
/>
<Input
label={(() => {
const selectedActivity = activities.find((a) =>
a.name === formData.activity
);
return selectedActivity?.unit_of_measurement === "Per Bag"
? "Rate per Unit (₹)"
: "Rate Amount (₹)";
})()}
name="rate"
type="number"
value={formData.rate}
onChange={handleInputChange}
placeholder="Enter rate amount"
required
/>
<Input
label="Effective Date"
name="effectiveDate"
type="date"
value={formData.effectiveDate}
onChange={handleInputChange}
required
/>
</div>
<div className="flex justify-end gap-4">
<Button
variant="primary"
onClick={() => {
setActiveTab("list");
resetForm();
}}
>
Cancel
</Button>
<Button onClick={handleSubmit} disabled={formLoading}>
{formLoading ? "Saving..." : (
<>
<DollarSign size={16} className="mr-2" />
{editingId ? "Update Rate" : "Add Rate"}
</>
)}
</Button>
</div>
</div>
)}
</CardContent>
</Card>
</div>
);
};