(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,43 +1,62 @@
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';
import React, { useEffect, useState } from "react";
import {
Activity as ActivityIcon,
Layers,
Plus,
RefreshCw,
Trash2,
} from "lucide-react";
import { Card, CardContent, CardHeader } 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 { useDepartments, useSubDepartments } from "../hooks/useDepartments.ts";
import { useActivitiesByDepartment } from "../hooks/useActivities.ts";
import { useAuth } from "../contexts/AuthContext.tsx";
import { api } from "../services/api.ts";
import { Activity, SubDepartment } from "../types.ts";
export const ActivitiesPage: React.FC = () => {
const [activeTab, setActiveTab] = useState<'subDepartments' | 'activities'>('subDepartments');
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 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>('');
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);
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 [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('');
const [error, setError] = useState("");
const [success, setSuccess] = useState("");
// Auto-select department for supervisors
useEffect(() => {
@@ -50,8 +69,8 @@ export const ActivitiesPage: React.FC = () => {
useEffect(() => {
if (success || error) {
const timer = setTimeout(() => {
setSuccess('');
setError('');
setSuccess("");
setError("");
}, 3000);
return () => clearTimeout(timer);
}
@@ -59,44 +78,52 @@ export const ActivitiesPage: React.FC = () => {
const handleCreateSubDepartment = async () => {
if (!subDeptForm.name.trim()) {
setError('Sub-department name is required');
setError("Sub-department name is required");
return;
}
if (!selectedDeptId) {
setError('Please select a department first');
setError("Please select a department first");
return;
}
setLoading(true);
setError('');
setError("");
try {
await api.createSubDepartment({
department_id: parseInt(selectedDeptId),
name: subDeptForm.name.trim()
name: subDeptForm.name.trim(),
});
setSuccess('Sub-department created successfully');
setSubDeptForm({ name: '' });
setSuccess("Sub-department created successfully");
setSubDeptForm({ name: "" });
refreshSubDepts();
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to create sub-department');
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.')) {
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');
setSuccess("Sub-department deleted successfully");
refreshSubDepts();
refreshActivities();
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to delete sub-department');
setError(
err instanceof Error ? err.message : "Failed to delete sub-department",
);
} finally {
setLoading(false);
}
@@ -104,44 +131,52 @@ export const ActivitiesPage: React.FC = () => {
const handleCreateActivity = async () => {
if (!activityForm.name.trim()) {
setError('Activity name is required');
setError("Activity name is required");
return;
}
if (!activityForm.subDepartmentId) {
setError('Please select a sub-department');
setError("Please select a sub-department");
return;
}
setLoading(true);
setError('');
setError("");
try {
await api.createActivity({
sub_department_id: parseInt(activityForm.subDepartmentId),
name: activityForm.name.trim(),
unit_of_measurement: activityForm.unitOfMeasurement
unit_of_measurement: activityForm.unitOfMeasurement,
});
setSuccess("Activity created successfully");
setActivityForm({
subDepartmentId: "",
name: "",
unitOfMeasurement: "Per Bag",
});
setSuccess('Activity created successfully');
setActivityForm({ subDepartmentId: '', name: '', unitOfMeasurement: 'Per Bag' });
refreshActivities();
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to create activity');
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?')) {
if (!confirm("Are you sure you want to delete this activity?")) {
return;
}
setLoading(true);
try {
await api.deleteActivity(id);
setSuccess('Activity deleted successfully');
setSuccess("Activity deleted successfully");
refreshActivities();
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to delete activity');
setError(
err instanceof Error ? err.message : "Failed to delete activity",
);
} finally {
setLoading(false);
}
@@ -152,14 +187,17 @@ export const ActivitiesPage: React.FC = () => {
<div className="p-6">
<Card>
<CardContent>
<p className="text-red-600">You do not have permission to access this page.</p>
<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 || '';
const selectedDeptName =
departments.find((d) => d.id === parseInt(selectedDeptId))?.name || "";
return (
<div className="p-6 space-y-6">
@@ -168,15 +206,22 @@ export const ActivitiesPage: React.FC = () => {
<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>
<h2 className="text-xl font-semibold text-gray-800">
Manage Activities & Sub-Departments
</h2>
</div>
<Button
variant="outline"
size="sm"
onClick={() => { refreshSubDepts(); refreshActivities(); }}
onClick={() => {
refreshSubDepts();
refreshActivities();
}}
disabled={loading}
>
<RefreshCw className={`h-4 w-4 mr-2 ${loading ? 'animate-spin' : ''}`} />
<RefreshCw
className={`h-4 w-4 mr-2 ${loading ? "animate-spin" : ""}`}
/>
Refresh
</Button>
</div>
@@ -185,23 +230,33 @@ export const ActivitiesPage: React.FC = () => {
<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 }))
]}
/>
)}
{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 */}
@@ -220,22 +275,22 @@ export const ActivitiesPage: React.FC = () => {
<div className="border-b border-gray-200 mb-6">
<div className="flex space-x-8">
<button
onClick={() => setActiveTab('subDepartments')}
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'
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')}
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'
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" />
@@ -244,165 +299,226 @@ export const ActivitiesPage: React.FC = () => {
</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"
/>
{!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>
<Button onClick={handleCreateSubDepartment} disabled={loading}>
<Plus className="h-4 w-4 mr-2" />
Add Sub-Department
</Button>
</div>
</div>
{/* Sub-Departments List */}
<Table>
<TableHeader>
<TableHead>Sub-Department Name</TableHead>
<TableHead>Activities Count</TableHead>
<TableHead>Created At</TableHead>
<TableHead className="text-right">Actions</TableHead>
</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>
{/* Sub-Departments List */}
<Table>
<TableHeader>
<TableHead>Sub-Department Name</TableHead>
<TableHead>Activities Count</TableHead>
<TableHead>Created At</TableHead>
<TableHead className="text-right">Actions</TableHead>
</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>
);
})
)}
</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>
)
: (
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 List */}
<Table>
<TableHeader>
<TableHead>Activity Name</TableHead>
<TableHead>Sub-Department</TableHead>
<TableHead>Unit of Measurement</TableHead>
<TableHead>Created At</TableHead>
<TableHead className="text-right">Actions</TableHead>
</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"
{/* 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>
<TableHead>Activity Name</TableHead>
<TableHead>Sub-Department</TableHead>
<TableHead>Unit of Measurement</TableHead>
<TableHead>Created At</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableHeader>
<TableBody>
{activities.length === 0
? (
<TableRow>
<TableCell
colSpan={5}
className="text-center text-gray-500 py-8"
>
<Trash2 className="h-4 w-4" />
</Button>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
)}
</>
)}
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>