(Feat): Initial Commit

This commit is contained in:
2025-11-27 22:50:08 +00:00
commit 00f9ed128b
79 changed files with 17413 additions and 0 deletions

410
src/pages/RatesPage.tsx Normal file
View 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>
);
};