(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:
@@ -1,6 +1,6 @@
|
|||||||
import { Router } from "@oak/oak";
|
import { Router } from "@oak/oak";
|
||||||
import { db } from "../config/database.ts";
|
import { db } from "../config/database.ts";
|
||||||
import { authenticateToken } from "../middleware/auth.ts";
|
import { authenticateToken, getCurrentUser } from "../middleware/auth.ts";
|
||||||
|
|
||||||
const router = new Router();
|
const router = new Router();
|
||||||
|
|
||||||
@@ -86,9 +86,10 @@ router.get("/:id", authenticateToken, async (ctx) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create activity (SuperAdmin only)
|
// Create activity (SuperAdmin or Supervisor for their own department)
|
||||||
router.post("/", authenticateToken, async (ctx) => {
|
router.post("/", authenticateToken, async (ctx) => {
|
||||||
try {
|
try {
|
||||||
|
const user = getCurrentUser(ctx);
|
||||||
const body = await ctx.request.body.json();
|
const body = await ctx.request.body.json();
|
||||||
const { sub_department_id, name, unit_of_measurement } = body;
|
const { sub_department_id, name, unit_of_measurement } = body;
|
||||||
|
|
||||||
@@ -98,6 +99,33 @@ router.post("/", authenticateToken, async (ctx) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get the sub-department to check department ownership
|
||||||
|
const subDepts = await db.query<{ department_id: number }[]>(
|
||||||
|
"SELECT department_id FROM sub_departments WHERE id = ?",
|
||||||
|
[sub_department_id]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (subDepts.length === 0) {
|
||||||
|
ctx.response.status = 404;
|
||||||
|
ctx.response.body = { error: "Sub-department not found" };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const subDeptDepartmentId = subDepts[0].department_id;
|
||||||
|
|
||||||
|
// Check authorization
|
||||||
|
if (user.role === 'Supervisor' && user.departmentId !== subDeptDepartmentId) {
|
||||||
|
ctx.response.status = 403;
|
||||||
|
ctx.response.body = { error: "You can only create activities for your own department" };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.role !== 'SuperAdmin' && user.role !== 'Supervisor') {
|
||||||
|
ctx.response.status = 403;
|
||||||
|
ctx.response.body = { error: "Unauthorized" };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const result = await db.execute(
|
const result = await db.execute(
|
||||||
"INSERT INTO activities (sub_department_id, name, unit_of_measurement) VALUES (?, ?, ?)",
|
"INSERT INTO activities (sub_department_id, name, unit_of_measurement) VALUES (?, ?, ?)",
|
||||||
[sub_department_id, name, unit_of_measurement || "Per Bag"]
|
[sub_department_id, name, unit_of_measurement || "Per Bag"]
|
||||||
@@ -109,6 +137,12 @@ router.post("/", authenticateToken, async (ctx) => {
|
|||||||
message: "Activity created successfully"
|
message: "Activity created successfully"
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
const err = error as { code?: string };
|
||||||
|
if (err.code === "ER_DUP_ENTRY") {
|
||||||
|
ctx.response.status = 400;
|
||||||
|
ctx.response.body = { error: "Activity already exists in this sub-department" };
|
||||||
|
return;
|
||||||
|
}
|
||||||
console.error("Create activity error:", error);
|
console.error("Create activity error:", error);
|
||||||
ctx.response.status = 500;
|
ctx.response.status = 500;
|
||||||
ctx.response.body = { error: "Internal server error" };
|
ctx.response.body = { error: "Internal server error" };
|
||||||
@@ -135,11 +169,42 @@ router.put("/:id", authenticateToken, async (ctx) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Delete activity
|
// Delete activity (SuperAdmin or Supervisor for their own department)
|
||||||
router.delete("/:id", authenticateToken, async (ctx) => {
|
router.delete("/:id", authenticateToken, async (ctx) => {
|
||||||
try {
|
try {
|
||||||
|
const user = getCurrentUser(ctx);
|
||||||
const activityId = ctx.params.id;
|
const activityId = ctx.params.id;
|
||||||
|
|
||||||
|
// Get the activity and its sub-department to check department ownership
|
||||||
|
const activities = await db.query<Activity[]>(
|
||||||
|
`SELECT a.*, sd.department_id
|
||||||
|
FROM activities a
|
||||||
|
JOIN sub_departments sd ON a.sub_department_id = sd.id
|
||||||
|
WHERE a.id = ?`,
|
||||||
|
[activityId]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (activities.length === 0) {
|
||||||
|
ctx.response.status = 404;
|
||||||
|
ctx.response.body = { error: "Activity not found" };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const activity = activities[0] as Activity & { department_id: number };
|
||||||
|
|
||||||
|
// Check authorization
|
||||||
|
if (user.role === 'Supervisor' && user.departmentId !== activity.department_id) {
|
||||||
|
ctx.response.status = 403;
|
||||||
|
ctx.response.body = { error: "You can only delete activities from your own department" };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.role !== 'SuperAdmin' && user.role !== 'Supervisor') {
|
||||||
|
ctx.response.status = 403;
|
||||||
|
ctx.response.body = { error: "Unauthorized" };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await db.execute("DELETE FROM activities WHERE id = ?", [activityId]);
|
await db.execute("DELETE FROM activities WHERE id = ?", [activityId]);
|
||||||
|
|
||||||
ctx.response.body = { message: "Activity deleted successfully" };
|
ctx.response.body = { message: "Activity deleted successfully" };
|
||||||
|
|||||||
@@ -101,35 +101,140 @@ router.post("/", authenticateToken, authorize("SuperAdmin"), async (ctx) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create sub-department (SuperAdmin only)
|
// Create sub-department (SuperAdmin or Supervisor for their own department)
|
||||||
router.post("/:id/sub-departments", authenticateToken, authorize("SuperAdmin"), async (ctx) => {
|
router.post("/sub-departments", authenticateToken, async (ctx) => {
|
||||||
try {
|
try {
|
||||||
const deptId = ctx.params.id;
|
const user = getCurrentUser(ctx);
|
||||||
const body = await ctx.request.body.json() as { name: string; primaryActivity: string };
|
const body = await ctx.request.body.json() as { department_id: number; name: string };
|
||||||
const { name, primaryActivity } = body;
|
const { department_id, name } = body;
|
||||||
|
|
||||||
if (!name || !primaryActivity) {
|
if (!name || !department_id) {
|
||||||
ctx.response.status = 400;
|
ctx.response.status = 400;
|
||||||
ctx.response.body = { error: "Name and primary activity required" };
|
ctx.response.body = { error: "Department ID and name are required" };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check authorization
|
||||||
|
if (user.role === 'Supervisor' && user.departmentId !== department_id) {
|
||||||
|
ctx.response.status = 403;
|
||||||
|
ctx.response.body = { error: "You can only create sub-departments for your own department" };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.role !== 'SuperAdmin' && user.role !== 'Supervisor') {
|
||||||
|
ctx.response.status = 403;
|
||||||
|
ctx.response.body = { error: "Unauthorized" };
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const sanitizedName = sanitizeInput(name);
|
const sanitizedName = sanitizeInput(name);
|
||||||
const sanitizedActivity = sanitizeInput(primaryActivity);
|
|
||||||
|
|
||||||
const result = await db.execute(
|
const result = await db.execute(
|
||||||
"INSERT INTO sub_departments (department_id, name, primary_activity) VALUES (?, ?, ?)",
|
"INSERT INTO sub_departments (department_id, name) VALUES (?, ?)",
|
||||||
[deptId, sanitizedName, sanitizedActivity]
|
[department_id, sanitizedName]
|
||||||
);
|
);
|
||||||
|
|
||||||
const newSubDepartment = await db.query<SubDepartment[]>(
|
const newSubDepartment = await db.query<SubDepartment[]>(
|
||||||
"SELECT * FROM sub_departments WHERE id = ?",
|
"SELECT * FROM sub_departments WHERE id = ?",
|
||||||
[result.insertId]
|
[result.lastInsertId]
|
||||||
);
|
);
|
||||||
|
|
||||||
ctx.response.status = 201;
|
ctx.response.status = 201;
|
||||||
ctx.response.body = newSubDepartment[0];
|
ctx.response.body = newSubDepartment[0];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
const err = error as { code?: string };
|
||||||
|
if (err.code === "ER_DUP_ENTRY") {
|
||||||
|
ctx.response.status = 400;
|
||||||
|
ctx.response.body = { error: "Sub-department already exists in this department" };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.error("Create sub-department error:", error);
|
||||||
|
ctx.response.status = 500;
|
||||||
|
ctx.response.body = { error: "Internal server error" };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete sub-department (SuperAdmin or Supervisor for their own department)
|
||||||
|
router.delete("/sub-departments/:id", authenticateToken, async (ctx) => {
|
||||||
|
try {
|
||||||
|
const user = getCurrentUser(ctx);
|
||||||
|
const subDeptId = ctx.params.id;
|
||||||
|
|
||||||
|
// Get the sub-department to check department ownership
|
||||||
|
const subDepts = await db.query<SubDepartment[]>(
|
||||||
|
"SELECT * FROM sub_departments WHERE id = ?",
|
||||||
|
[subDeptId]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (subDepts.length === 0) {
|
||||||
|
ctx.response.status = 404;
|
||||||
|
ctx.response.body = { error: "Sub-department not found" };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const subDept = subDepts[0];
|
||||||
|
|
||||||
|
// Check authorization
|
||||||
|
if (user.role === 'Supervisor' && user.departmentId !== subDept.department_id) {
|
||||||
|
ctx.response.status = 403;
|
||||||
|
ctx.response.body = { error: "You can only delete sub-departments from your own department" };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.role !== 'SuperAdmin' && user.role !== 'Supervisor') {
|
||||||
|
ctx.response.status = 403;
|
||||||
|
ctx.response.body = { error: "Unauthorized" };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete associated activities first (cascade should handle this, but being explicit)
|
||||||
|
await db.execute("DELETE FROM activities WHERE sub_department_id = ?", [subDeptId]);
|
||||||
|
|
||||||
|
// Delete the sub-department
|
||||||
|
await db.execute("DELETE FROM sub_departments WHERE id = ?", [subDeptId]);
|
||||||
|
|
||||||
|
ctx.response.body = { message: "Sub-department deleted successfully" };
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Delete sub-department error:", error);
|
||||||
|
ctx.response.status = 500;
|
||||||
|
ctx.response.body = { error: "Internal server error" };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Legacy route for creating sub-department under specific department (SuperAdmin only)
|
||||||
|
router.post("/:id/sub-departments", authenticateToken, authorize("SuperAdmin"), async (ctx) => {
|
||||||
|
try {
|
||||||
|
const deptId = ctx.params.id;
|
||||||
|
const body = await ctx.request.body.json() as { name: string };
|
||||||
|
const { name } = body;
|
||||||
|
|
||||||
|
if (!name) {
|
||||||
|
ctx.response.status = 400;
|
||||||
|
ctx.response.body = { error: "Name is required" };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sanitizedName = sanitizeInput(name);
|
||||||
|
|
||||||
|
const result = await db.execute(
|
||||||
|
"INSERT INTO sub_departments (department_id, name) VALUES (?, ?)",
|
||||||
|
[deptId, sanitizedName]
|
||||||
|
);
|
||||||
|
|
||||||
|
const newSubDepartment = await db.query<SubDepartment[]>(
|
||||||
|
"SELECT * FROM sub_departments WHERE id = ?",
|
||||||
|
[result.lastInsertId]
|
||||||
|
);
|
||||||
|
|
||||||
|
ctx.response.status = 201;
|
||||||
|
ctx.response.body = newSubDepartment[0];
|
||||||
|
} catch (error) {
|
||||||
|
const err = error as { code?: string };
|
||||||
|
if (err.code === "ER_DUP_ENTRY") {
|
||||||
|
ctx.response.status = 400;
|
||||||
|
ctx.response.body = { error: "Sub-department already exists in this department" };
|
||||||
|
return;
|
||||||
|
}
|
||||||
console.error("Create sub-department error:", error);
|
console.error("Create sub-department error:", error);
|
||||||
ctx.response.status = 500;
|
ctx.response.status = 500;
|
||||||
ctx.response.body = { error: "Internal server error" };
|
ctx.response.body = { error: "Internal server error" };
|
||||||
|
|||||||
@@ -12,8 +12,9 @@ import { LoginPage } from './pages/LoginPage';
|
|||||||
import { ReportingPage } from './pages/ReportingPage';
|
import { ReportingPage } from './pages/ReportingPage';
|
||||||
import { StandardRatesPage } from './pages/StandardRatesPage';
|
import { StandardRatesPage } from './pages/StandardRatesPage';
|
||||||
import { AllRatesPage } from './pages/AllRatesPage';
|
import { AllRatesPage } from './pages/AllRatesPage';
|
||||||
|
import { ActivitiesPage } from './pages/ActivitiesPage';
|
||||||
|
|
||||||
type PageType = 'dashboard' | 'users' | 'allocation' | 'attendance' | 'rates' | 'swaps' | 'reports' | 'standard-rates' | 'all-rates';
|
type PageType = 'dashboard' | 'users' | 'allocation' | 'attendance' | 'rates' | 'swaps' | 'reports' | 'standard-rates' | 'all-rates' | 'activities';
|
||||||
|
|
||||||
const AppContent: React.FC = () => {
|
const AppContent: React.FC = () => {
|
||||||
const [activePage, setActivePage] = useState<PageType>('dashboard');
|
const [activePage, setActivePage] = useState<PageType>('dashboard');
|
||||||
@@ -39,6 +40,8 @@ const AppContent: React.FC = () => {
|
|||||||
return <StandardRatesPage />;
|
return <StandardRatesPage />;
|
||||||
case 'all-rates':
|
case 'all-rates':
|
||||||
return <AllRatesPage />;
|
return <AllRatesPage />;
|
||||||
|
case 'activities':
|
||||||
|
return <ActivitiesPage />;
|
||||||
default:
|
default:
|
||||||
return <DashboardPage />;
|
return <DashboardPage />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { LayoutDashboard, Users, Briefcase, CalendarCheck, DollarSign, ClipboardList, ArrowRightLeft, FileSpreadsheet, Scale, Eye } from 'lucide-react';
|
import { LayoutDashboard, Users, Briefcase, CalendarCheck, DollarSign, ClipboardList, ArrowRightLeft, FileSpreadsheet, Scale, Eye, Layers } from 'lucide-react';
|
||||||
import { useAuth } from '../../contexts/AuthContext';
|
import { useAuth } from '../../contexts/AuthContext';
|
||||||
|
|
||||||
interface SidebarItemProps {
|
interface SidebarItemProps {
|
||||||
@@ -142,6 +142,16 @@ export const Sidebar: React.FC<SidebarProps> = ({ activePage, onNavigate }) => {
|
|||||||
onClick={() => onNavigate('all-rates')}
|
onClick={() => onNavigate('all-rates')}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Activities Management - SuperAdmin and Supervisor */}
|
||||||
|
{(isSuperAdmin || isSupervisor) && (
|
||||||
|
<SidebarItem
|
||||||
|
icon={Layers}
|
||||||
|
label="Activities"
|
||||||
|
active={activePage === 'activities'}
|
||||||
|
onClick={() => onNavigate('activities')}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{/* Role indicator at bottom */}
|
{/* Role indicator at bottom */}
|
||||||
|
|||||||
64
src/hooks/useActivities.ts
Normal file
64
src/hooks/useActivities.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
|
import { api } from '../services/api';
|
||||||
|
import { Activity } from '../types';
|
||||||
|
|
||||||
|
export const useActivities = (subDepartmentId?: string | number) => {
|
||||||
|
const [activities, setActivities] = useState<Activity[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const fetchActivities = useCallback(async () => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const params: { subDepartmentId?: number; departmentId?: number } = {};
|
||||||
|
if (subDepartmentId) {
|
||||||
|
params.subDepartmentId = Number(subDepartmentId);
|
||||||
|
}
|
||||||
|
const data = await api.getActivities(params);
|
||||||
|
setActivities(data);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : 'Failed to fetch activities');
|
||||||
|
setActivities([]);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [subDepartmentId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchActivities();
|
||||||
|
}, [fetchActivities]);
|
||||||
|
|
||||||
|
return { activities, loading, error, refresh: fetchActivities };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useActivitiesByDepartment = (departmentId?: string | number) => {
|
||||||
|
const [activities, setActivities] = useState<Activity[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const fetchActivities = useCallback(async () => {
|
||||||
|
if (!departmentId) {
|
||||||
|
setActivities([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const data = await api.getActivities({ departmentId: Number(departmentId) });
|
||||||
|
setActivities(data);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : 'Failed to fetch activities');
|
||||||
|
setActivities([]);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [departmentId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchActivities();
|
||||||
|
}, [fetchActivities]);
|
||||||
|
|
||||||
|
return { activities, loading, error, refresh: fetchActivities };
|
||||||
|
};
|
||||||
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;
|
||||||
@@ -7,6 +7,7 @@ import { Input, Select, TextArea } from '../components/ui/Input';
|
|||||||
import { useWorkAllocations } from '../hooks/useWorkAllocations';
|
import { useWorkAllocations } from '../hooks/useWorkAllocations';
|
||||||
import { useDepartments, useSubDepartments } from '../hooks/useDepartments';
|
import { useDepartments, useSubDepartments } from '../hooks/useDepartments';
|
||||||
import { useEmployees } from '../hooks/useEmployees';
|
import { useEmployees } from '../hooks/useEmployees';
|
||||||
|
import { useActivities } from '../hooks/useActivities';
|
||||||
import { useAuth } from '../contexts/AuthContext';
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
import { api } from '../services/api';
|
import { api } from '../services/api';
|
||||||
|
|
||||||
@@ -40,6 +41,7 @@ export const WorkAllocationPage: React.FC = () => {
|
|||||||
});
|
});
|
||||||
const [selectedDept, setSelectedDept] = useState('');
|
const [selectedDept, setSelectedDept] = useState('');
|
||||||
const { subDepartments } = useSubDepartments(selectedDept);
|
const { subDepartments } = useSubDepartments(selectedDept);
|
||||||
|
const { activities } = useActivities(formData.subDepartmentId);
|
||||||
const [formError, setFormError] = useState('');
|
const [formError, setFormError] = useState('');
|
||||||
const [formLoading, setFormLoading] = useState(false);
|
const [formLoading, setFormLoading] = useState(false);
|
||||||
const [contractorRates, setContractorRates] = useState<any[]>([]);
|
const [contractorRates, setContractorRates] = useState<any[]>([]);
|
||||||
@@ -247,12 +249,10 @@ export const WorkAllocationPage: React.FC = () => {
|
|||||||
name="activity"
|
name="activity"
|
||||||
value={formData.activity}
|
value={formData.activity}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
|
disabled={!formData.subDepartmentId}
|
||||||
options={[
|
options={[
|
||||||
{ value: '', label: 'Select Activity' },
|
{ value: '', label: formData.subDepartmentId ? 'Select Activity' : 'Select Sub-Department First' },
|
||||||
{ value: 'Loading', label: 'Loading' },
|
...activities.map(a => ({ value: a.name, label: a.name }))
|
||||||
{ value: 'Unloading', label: 'Unloading' },
|
|
||||||
{ value: 'Standard', label: 'Standard Work' },
|
|
||||||
{ value: 'Other', label: 'Other' },
|
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
|
|||||||
@@ -111,6 +111,20 @@ class ApiService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sub-Departments
|
||||||
|
async createSubDepartment(data: { department_id: number; name: string }) {
|
||||||
|
return this.request<any>('/departments/sub-departments', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteSubDepartment(id: number) {
|
||||||
|
return this.request<{ message: string }>(`/departments/sub-departments/${id}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Work Allocations
|
// Work Allocations
|
||||||
async getWorkAllocations(params?: { employeeId?: number; status?: string; departmentId?: number }) {
|
async getWorkAllocations(params?: { employeeId?: number; status?: string; departmentId?: number }) {
|
||||||
const query = new URLSearchParams(params as any).toString();
|
const query = new URLSearchParams(params as any).toString();
|
||||||
|
|||||||
Reference in New Issue
Block a user