(Feat-Fix): New Activity Creation system, fixed activities being hardcoded and not displaying properly in the dropdown, added dynamic subdepartment activity fetching.
This commit is contained in:
416
src/pages/ActivitiesPage.tsx
Normal file
416
src/pages/ActivitiesPage.tsx
Normal file
@@ -0,0 +1,416 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Plus, RefreshCw, Trash2, Layers, Activity as ActivityIcon } 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 { useDepartments, useSubDepartments } from '../hooks/useDepartments';
|
||||
import { useActivitiesByDepartment } from '../hooks/useActivities';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { api } from '../services/api';
|
||||
import { SubDepartment, Activity } from '../types';
|
||||
|
||||
export const ActivitiesPage: React.FC = () => {
|
||||
const [activeTab, setActiveTab] = useState<'subDepartments' | 'activities'>('subDepartments');
|
||||
const { user } = useAuth();
|
||||
const { departments } = useDepartments();
|
||||
|
||||
// Role-based access
|
||||
const isSupervisor = user?.role === 'Supervisor';
|
||||
const isSuperAdmin = user?.role === 'SuperAdmin';
|
||||
const canAccess = isSupervisor || isSuperAdmin;
|
||||
|
||||
// Department selection - supervisors are locked to their department
|
||||
const [selectedDeptId, setSelectedDeptId] = useState<string>('');
|
||||
|
||||
// Get sub-departments and activities for selected department
|
||||
const { subDepartments, refresh: refreshSubDepts } = useSubDepartments(selectedDeptId);
|
||||
const { activities, refresh: refreshActivities } = useActivitiesByDepartment(selectedDeptId);
|
||||
|
||||
// Form states
|
||||
const [subDeptForm, setSubDeptForm] = useState({ name: '' });
|
||||
const [activityForm, setActivityForm] = useState({
|
||||
subDepartmentId: '',
|
||||
name: '',
|
||||
unitOfMeasurement: 'Per Bag' as 'Per Bag' | 'Fixed Rate-Per Person'
|
||||
});
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [success, setSuccess] = useState('');
|
||||
|
||||
// Auto-select department for supervisors
|
||||
useEffect(() => {
|
||||
if (isSupervisor && user?.department_id) {
|
||||
setSelectedDeptId(String(user.department_id));
|
||||
}
|
||||
}, [isSupervisor, user?.department_id]);
|
||||
|
||||
// Clear messages after 3 seconds
|
||||
useEffect(() => {
|
||||
if (success || error) {
|
||||
const timer = setTimeout(() => {
|
||||
setSuccess('');
|
||||
setError('');
|
||||
}, 3000);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [success, error]);
|
||||
|
||||
const handleCreateSubDepartment = async () => {
|
||||
if (!subDeptForm.name.trim()) {
|
||||
setError('Sub-department name is required');
|
||||
return;
|
||||
}
|
||||
if (!selectedDeptId) {
|
||||
setError('Please select a department first');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
setError('');
|
||||
try {
|
||||
await api.createSubDepartment({
|
||||
department_id: parseInt(selectedDeptId),
|
||||
name: subDeptForm.name.trim()
|
||||
});
|
||||
setSuccess('Sub-department created successfully');
|
||||
setSubDeptForm({ name: '' });
|
||||
refreshSubDepts();
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to create sub-department');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteSubDepartment = async (id: number) => {
|
||||
if (!confirm('Are you sure you want to delete this sub-department? This will also delete all associated activities.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
await api.deleteSubDepartment(id);
|
||||
setSuccess('Sub-department deleted successfully');
|
||||
refreshSubDepts();
|
||||
refreshActivities();
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to delete sub-department');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateActivity = async () => {
|
||||
if (!activityForm.name.trim()) {
|
||||
setError('Activity name is required');
|
||||
return;
|
||||
}
|
||||
if (!activityForm.subDepartmentId) {
|
||||
setError('Please select a sub-department');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
setError('');
|
||||
try {
|
||||
await api.createActivity({
|
||||
sub_department_id: parseInt(activityForm.subDepartmentId),
|
||||
name: activityForm.name.trim(),
|
||||
unit_of_measurement: activityForm.unitOfMeasurement
|
||||
});
|
||||
setSuccess('Activity created successfully');
|
||||
setActivityForm({ subDepartmentId: '', name: '', unitOfMeasurement: 'Per Bag' });
|
||||
refreshActivities();
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to create activity');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteActivity = async (id: number) => {
|
||||
if (!confirm('Are you sure you want to delete this activity?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
await api.deleteActivity(id);
|
||||
setSuccess('Activity deleted successfully');
|
||||
refreshActivities();
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to delete activity');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!canAccess) {
|
||||
return (
|
||||
<div className="p-6">
|
||||
<Card>
|
||||
<CardContent>
|
||||
<p className="text-red-600">You do not have permission to access this page.</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const selectedDeptName = departments.find(d => d.id === parseInt(selectedDeptId))?.name || '';
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<Layers className="h-6 w-6 text-blue-600" />
|
||||
<h2 className="text-xl font-semibold text-gray-800">Manage Activities & Sub-Departments</h2>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => { refreshSubDepts(); refreshActivities(); }}
|
||||
disabled={loading}
|
||||
>
|
||||
<RefreshCw className={`h-4 w-4 mr-2 ${loading ? 'animate-spin' : ''}`} />
|
||||
Refresh
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent>
|
||||
{/* Department Selection */}
|
||||
<div className="mb-6">
|
||||
{isSupervisor ? (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Department</label>
|
||||
<Input value={selectedDeptName || 'Loading...'} disabled />
|
||||
<p className="text-xs text-gray-500 mt-1">As a supervisor, you can only manage your department's activities.</p>
|
||||
</div>
|
||||
) : (
|
||||
<Select
|
||||
label="Select Department"
|
||||
value={selectedDeptId}
|
||||
onChange={(e) => setSelectedDeptId(e.target.value)}
|
||||
options={[
|
||||
{ value: '', label: 'Select a Department' },
|
||||
...departments.map(d => ({ value: String(d.id), label: d.name }))
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Messages */}
|
||||
{error && (
|
||||
<div className="mb-4 p-3 bg-red-100 text-red-700 rounded-md">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
{success && (
|
||||
<div className="mb-4 p-3 bg-green-100 text-green-700 rounded-md">
|
||||
{success}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="border-b border-gray-200 mb-6">
|
||||
<div className="flex space-x-8">
|
||||
<button
|
||||
onClick={() => setActiveTab('subDepartments')}
|
||||
className={`py-3 px-1 border-b-2 font-medium text-sm ${
|
||||
activeTab === 'subDepartments'
|
||||
? 'border-blue-500 text-blue-600'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700'
|
||||
}`}
|
||||
>
|
||||
<Layers className="h-4 w-4 inline mr-2" />
|
||||
Sub-Departments ({subDepartments.length})
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('activities')}
|
||||
className={`py-3 px-1 border-b-2 font-medium text-sm ${
|
||||
activeTab === 'activities'
|
||||
? 'border-blue-500 text-blue-600'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700'
|
||||
}`}
|
||||
>
|
||||
<ActivityIcon className="h-4 w-4 inline mr-2" />
|
||||
Activities ({activities.length})
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!selectedDeptId ? (
|
||||
<p className="text-gray-500 text-center py-8">Please select a department to manage sub-departments and activities.</p>
|
||||
) : (
|
||||
<>
|
||||
{/* Sub-Departments Tab */}
|
||||
{activeTab === 'subDepartments' && (
|
||||
<div className="space-y-6">
|
||||
{/* Create Sub-Department Form */}
|
||||
<div className="bg-gray-50 p-4 rounded-lg">
|
||||
<h3 className="text-md font-semibold text-gray-700 mb-4">Add New Sub-Department</h3>
|
||||
<div className="flex gap-4 items-end">
|
||||
<div className="flex-1">
|
||||
<Input
|
||||
label="Sub-Department Name"
|
||||
value={subDeptForm.name}
|
||||
onChange={(e) => setSubDeptForm({ name: e.target.value })}
|
||||
placeholder="e.g., Loading/Unloading, Destoner, Tank"
|
||||
/>
|
||||
</div>
|
||||
<Button onClick={handleCreateSubDepartment} disabled={loading}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Add Sub-Department
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sub-Departments List */}
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Sub-Department Name</TableHead>
|
||||
<TableHead>Activities Count</TableHead>
|
||||
<TableHead>Created At</TableHead>
|
||||
<TableHead className="text-right">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{subDepartments.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={4} className="text-center text-gray-500 py-8">
|
||||
No sub-departments found. Create one above.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
subDepartments.map((subDept: SubDepartment) => {
|
||||
const activityCount = activities.filter(a => a.sub_department_id === subDept.id).length;
|
||||
return (
|
||||
<TableRow key={subDept.id}>
|
||||
<TableCell className="font-medium">{subDept.name}</TableCell>
|
||||
<TableCell>{activityCount}</TableCell>
|
||||
<TableCell>{new Date(subDept.created_at).toLocaleDateString()}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleDeleteSubDepartment(subDept.id)}
|
||||
className="text-red-600 hover:text-red-800"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Activities Tab */}
|
||||
{activeTab === 'activities' && (
|
||||
<div className="space-y-6">
|
||||
{/* Create Activity Form */}
|
||||
<div className="bg-gray-50 p-4 rounded-lg">
|
||||
<h3 className="text-md font-semibold text-gray-700 mb-4">Add New Activity</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 items-end">
|
||||
<Select
|
||||
label="Sub-Department"
|
||||
value={activityForm.subDepartmentId}
|
||||
onChange={(e) => setActivityForm(prev => ({ ...prev, subDepartmentId: e.target.value }))}
|
||||
options={[
|
||||
{ value: '', label: 'Select Sub-Department' },
|
||||
...subDepartments.map(s => ({ value: String(s.id), label: s.name }))
|
||||
]}
|
||||
/>
|
||||
<Input
|
||||
label="Activity Name"
|
||||
value={activityForm.name}
|
||||
onChange={(e) => setActivityForm(prev => ({ ...prev, name: e.target.value }))}
|
||||
placeholder="e.g., Mufali Aavak Katai"
|
||||
/>
|
||||
<Select
|
||||
label="Unit of Measurement"
|
||||
value={activityForm.unitOfMeasurement}
|
||||
onChange={(e) => setActivityForm(prev => ({
|
||||
...prev,
|
||||
unitOfMeasurement: e.target.value as 'Per Bag' | 'Fixed Rate-Per Person'
|
||||
}))}
|
||||
options={[
|
||||
{ value: 'Per Bag', label: 'Per Bag' },
|
||||
{ value: 'Fixed Rate-Per Person', label: 'Fixed Rate-Per Person' }
|
||||
]}
|
||||
/>
|
||||
<Button onClick={handleCreateActivity} disabled={loading}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Add Activity
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Activities List */}
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Activity Name</TableHead>
|
||||
<TableHead>Sub-Department</TableHead>
|
||||
<TableHead>Unit of Measurement</TableHead>
|
||||
<TableHead>Created At</TableHead>
|
||||
<TableHead className="text-right">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{activities.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={5} className="text-center text-gray-500 py-8">
|
||||
No activities found. Create one above.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
activities.map((activity: Activity) => (
|
||||
<TableRow key={activity.id}>
|
||||
<TableCell className="font-medium">{activity.name}</TableCell>
|
||||
<TableCell>{activity.sub_department_name}</TableCell>
|
||||
<TableCell>
|
||||
<span className={`px-2 py-1 rounded-full text-xs ${
|
||||
activity.unit_of_measurement === 'Per Bag'
|
||||
? 'bg-blue-100 text-blue-800'
|
||||
: 'bg-green-100 text-green-800'
|
||||
}`}>
|
||||
{activity.unit_of_measurement}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell>{new Date(activity.created_at).toLocaleDateString()}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleDeleteActivity(activity.id)}
|
||||
className="text-red-600 hover:text-red-800"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ActivitiesPage;
|
||||
Reference in New Issue
Block a user