529 lines
19 KiB
TypeScript
529 lines
19 KiB
TypeScript
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.ts";
|
|
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 { 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>
|
|
<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>
|
|
</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>
|
|
<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"
|
|
>
|
|
<Trash2 className="h-4 w-4" />
|
|
</Button>
|
|
</TableCell>
|
|
</TableRow>
|
|
))
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default ActivitiesPage;
|