(Feat): Initial Commit
This commit is contained in:
410
src/pages/RatesPage.tsx
Normal file
410
src/pages/RatesPage.tsx
Normal file
@@ -0,0 +1,410 @@
|
||||
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 { useAuth } from '../contexts/AuthContext';
|
||||
|
||||
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 [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;
|
||||
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.activity === 'Loading' || rate.activity === 'Unloading'
|
||||
? '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.activity === 'Loading' || rate.activity === 'Unloading'
|
||||
? '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>Loading/Unloading:</strong> Total = Units × Rate per Unit</li>
|
||||
<li><strong>Standard/Other:</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}
|
||||
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)' },
|
||||
]}
|
||||
/>
|
||||
<Input
|
||||
label={formData.activity === 'Loading' || formData.activity === 'Unloading'
|
||||
? "Rate per Unit (₹)"
|
||||
: "Standard Rate (₹)"}
|
||||
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="outline" 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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user