(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,6 +1,5 @@
# Quick Start Guide # Quick Start Guide
## Start the Application ## Start the Application
### Option 1: Use the Start Script (Recommended) ### Option 1: Use the Start Script (Recommended)

View File

@@ -1,16 +1,26 @@
# React + Vite # React + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. This template provides a minimal setup to get React working in Vite with HMR and
some ESLint rules.
Currently, two official plugins are available: Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react)
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in
[rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc)
uses [SWC](https://swc.rs/) for Fast Refresh
## React Compiler ## React Compiler
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). The React Compiler is not enabled on this template because of its impact on dev
& build performances. To add it, see
[this documentation](https://react.dev/learn/react-compiler/installation).
## Expanding the ESLint configuration ## Expanding the ESLint configuration
If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project. If you are developing a production application, we recommend using TypeScript
with type-aware lint rules enabled. Check out the
[TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts)
for information on how to integrate TypeScript and
[`typescript-eslint`](https://typescript-eslint.io) in your project.

View File

@@ -3,7 +3,7 @@
## GROUNDNUT Department ## GROUNDNUT Department
| # | Activity Name | Sub-Department | Unit of Measurement | | # | Activity Name | Sub-Department | Unit of Measurement |
|---|---------------|----------------|---------------------| | -- | ---------------------------------------------------------------------------------------------- | ---------------------------------------- | --------------------- |
| 1 | Mufali Aavak Katai (Groundnut Arrival Cutting) | Loading/Unloading | Per Bag | | 1 | Mufali Aavak Katai (Groundnut Arrival Cutting) | Loading/Unloading | Per Bag |
| 2 | Mufali Aavak Dhaang (Groundnut Arrival Stacking) | Loading/Unloading | Per Bag | | 2 | Mufali Aavak Dhaang (Groundnut Arrival Stacking) | Loading/Unloading | Per Bag |
| 3 | Dhaang Se Katai (Cutting from Stack) | Loading/Unloading | Per Bag | | 3 | Dhaang Se Katai (Cutting from Stack) | Loading/Unloading | Per Bag |
@@ -26,7 +26,7 @@
## DANA Department ## DANA Department
| # | Activity Name | Sub-Department | Unit of Measurement | | # | Activity Name | Sub-Department | Unit of Measurement |
|---|---------------|----------------|---------------------| | -- | ----------------------------------------------------------------------------------------------------------------------------------- | ----------------- | --------------------- |
| 1 | Tulai Silai Loading (Weighing Stitching Loading) | Loading/Unloading | Per Bag | | 1 | Tulai Silai Loading (Weighing Stitching Loading) | Loading/Unloading | Per Bag |
| 2 | Dhaang se Loading (Loading from Stack) | Loading/Unloading | Per Bag | | 2 | Dhaang se Loading (Loading from Stack) | Loading/Unloading | Per Bag |
| 3 | Silai Dhaang (Stitching Stack) | Loading/Unloading | Per Bag | | 3 | Silai Dhaang (Stitching Stack) | Loading/Unloading | Per Bag |
@@ -55,7 +55,7 @@
## TUKDI Department ## TUKDI Department
| # | Activity Name | Sub-Department | Unit of Measurement | | # | Activity Name | Sub-Department | Unit of Measurement |
|---|---------------|----------------|---------------------| | -- | ----------------------------------------------------------------------------------------------------------------- | ----------------- | --------------------- |
| 1 | Dana Loaning/Unloading (Grain Loading/Unloading) | Loading/Unloading | Per Bag | | 1 | Dana Loaning/Unloading (Grain Loading/Unloading) | Loading/Unloading | Per Bag |
| 2 | Loading/Unloading 40 Kg | Loading/Unloading | Per Bag | | 2 | Loading/Unloading 40 Kg | Loading/Unloading | Per Bag |
| 3 | Grading Chalne se Maal Bharai Tulai Silai Dhaang (Grading Running Material Filling Weighing Stitching Stack) | Loading/Unloading | Per Bag | | 3 | Grading Chalne se Maal Bharai Tulai Silai Dhaang (Grading Running Material Filling Weighing Stitching Stack) | Loading/Unloading | Per Bag |

View File

@@ -1,6 +1,7 @@
# Work Allocation Backend - Deno TypeScript # Work Allocation Backend - Deno TypeScript
A secure, type-safe backend for the Work Allocation System built with Deno and TypeScript. A secure, type-safe backend for the Work Allocation System built with Deno and
TypeScript.
## Features ## Features
@@ -90,7 +91,8 @@ deno task seed
- `GET /api/departments/:id` - Get department - `GET /api/departments/:id` - Get department
- `GET /api/departments/:id/sub-departments` - Get sub-departments - `GET /api/departments/:id/sub-departments` - Get sub-departments
- `POST /api/departments` - Create department (SuperAdmin) - `POST /api/departments` - Create department (SuperAdmin)
- `POST /api/departments/:id/sub-departments` - Create sub-department (SuperAdmin) - `POST /api/departments/:id/sub-departments` - Create sub-department
(SuperAdmin)
### Work Allocations ### Work Allocations
@@ -123,7 +125,7 @@ deno task seed
## Environment Variables ## Environment Variables
| Variable | Description | Default | | Variable | Description | Default |
|----------|-------------|---------| | ------------------------- | ----------------------- | ----------------------- |
| `PORT` | Server port | 3000 | | `PORT` | Server port | 3000 |
| `DB_HOST` | Database host | localhost | | `DB_HOST` | Database host | localhost |
| `DB_USER` | Database user | root | | `DB_USER` | Database user | root |
@@ -197,7 +199,7 @@ backend-deno/
## Differences from Node.js Backend ## Differences from Node.js Backend
| Feature | Node.js | Deno | | Feature | Node.js | Deno |
|---------|---------|------| | --------------- | -------------------- | -------------------- |
| Runtime | Node.js | Deno | | Runtime | Node.js | Deno |
| Package Manager | npm | Built-in (JSR/npm) | | Package Manager | npm | Built-in (JSR/npm) |
| TypeScript | Requires compilation | Native support | | TypeScript | Requires compilation | Native support |

View File

@@ -1,4 +1,4 @@
import { createPool, Pool } from "mysql2/promise"; import { createPool, Pool, PoolConnection } from "mysql2/promise";
import { load } from "@std/dotenv"; import { load } from "@std/dotenv";
// Load environment variables // Load environment variables
@@ -40,7 +40,10 @@ class Database {
console.log("✅ Database connected successfully"); console.log("✅ Database connected successfully");
connection.release(); connection.release();
} catch (error) { } catch (error) {
console.error("❌ Database connection failed:", (error as Error).message); console.error(
"❌ Database connection failed:",
(error as Error).message,
);
throw error; throw error;
} }
} }
@@ -60,12 +63,39 @@ class Database {
return rows as T; return rows as T;
} }
async execute(sql: string, params?: unknown[]): Promise<{ insertId: number; affectedRows: number }> { async execute(
sql: string,
params?: unknown[],
): Promise<{ insertId: number; affectedRows: number }> {
const pool = await this.getPool(); const pool = await this.getPool();
const [result] = await pool.execute(sql, params); const [result] = await pool.execute(sql, params);
return result as { insertId: number; affectedRows: number }; return result as { insertId: number; affectedRows: number };
} }
// Get a connection for transaction support
async getConnection(): Promise<PoolConnection> {
const pool = await this.getPool();
return await pool.getConnection();
}
// Execute within a transaction
async transaction<T>(
callback: (connection: PoolConnection) => Promise<T>,
): Promise<T> {
const connection = await this.getConnection();
try {
await connection.beginTransaction();
const result = await callback(connection);
await connection.commit();
return result;
} catch (error) {
await connection.rollback();
throw error;
} finally {
connection.release();
}
}
async close(): Promise<void> { async close(): Promise<void> {
if (this.pool) { if (this.pool) {
await this.pool.end(); await this.pool.end();

View File

@@ -14,13 +14,18 @@ export const config = {
DB_PORT: parseInt(Deno.env.get("DB_PORT") || "3306"), DB_PORT: parseInt(Deno.env.get("DB_PORT") || "3306"),
// JWT - Security: Use strong secret in production // JWT - Security: Use strong secret in production
JWT_SECRET: Deno.env.get("JWT_SECRET") || "work_alloc_jwt_secret_key_change_in_production_2024", JWT_SECRET: Deno.env.get("JWT_SECRET") ||
"work_alloc_jwt_secret_key_change_in_production_2024",
JWT_EXPIRES_IN: Deno.env.get("JWT_EXPIRES_IN") || "7d", JWT_EXPIRES_IN: Deno.env.get("JWT_EXPIRES_IN") || "7d",
// Security settings // Security settings
BCRYPT_ROUNDS: parseInt(Deno.env.get("BCRYPT_ROUNDS") || "12"), BCRYPT_ROUNDS: parseInt(Deno.env.get("BCRYPT_ROUNDS") || "12"),
RATE_LIMIT_WINDOW_MS: parseInt(Deno.env.get("RATE_LIMIT_WINDOW_MS") || "900000"), // 15 minutes RATE_LIMIT_WINDOW_MS: parseInt(
RATE_LIMIT_MAX_REQUESTS: parseInt(Deno.env.get("RATE_LIMIT_MAX_REQUESTS") || "100"), Deno.env.get("RATE_LIMIT_WINDOW_MS") || "900000",
), // 15 minutes
RATE_LIMIT_MAX_REQUESTS: parseInt(
Deno.env.get("RATE_LIMIT_MAX_REQUESTS") || "100",
),
// CORS // CORS
CORS_ORIGIN: Deno.env.get("CORS_ORIGIN") || "http://localhost:5173", CORS_ORIGIN: Deno.env.get("CORS_ORIGIN") || "http://localhost:5173",
@@ -34,7 +39,7 @@ export const config = {
isProduction(): boolean { isProduction(): boolean {
return this.NODE_ENV === "production"; return this.NODE_ENV === "production";
} },
}; };
export default config; export default config;

View File

@@ -1,7 +1,12 @@
import { Application, Router } from "@oak/oak"; import { Application, Router } from "@oak/oak";
import { config } from "./config/env.ts"; import { config } from "./config/env.ts";
import { db } from "./config/database.ts"; import { db } from "./config/database.ts";
import { cors, securityHeaders, requestLogger, rateLimit } from "./middleware/security.ts"; import {
cors,
rateLimit,
requestLogger,
securityHeaders,
} from "./middleware/security.ts";
// Import routes // Import routes
import authRoutes from "./routes/auth.ts"; import authRoutes from "./routes/auth.ts";
@@ -61,14 +66,46 @@ router.get("/health", (ctx) => {
// Mount API routes // Mount API routes
router.use("/api/auth", authRoutes.routes(), authRoutes.allowedMethods()); router.use("/api/auth", authRoutes.routes(), authRoutes.allowedMethods());
router.use("/api/users", userRoutes.routes(), userRoutes.allowedMethods()); router.use("/api/users", userRoutes.routes(), userRoutes.allowedMethods());
router.use("/api/departments", departmentRoutes.routes(), departmentRoutes.allowedMethods()); router.use(
router.use("/api/work-allocations", workAllocationRoutes.routes(), workAllocationRoutes.allowedMethods()); "/api/departments",
router.use("/api/attendance", attendanceRoutes.routes(), attendanceRoutes.allowedMethods()); departmentRoutes.routes(),
router.use("/api/contractor-rates", contractorRateRoutes.routes(), contractorRateRoutes.allowedMethods()); departmentRoutes.allowedMethods(),
router.use("/api/employee-swaps", employeeSwapRoutes.routes(), employeeSwapRoutes.allowedMethods()); );
router.use("/api/reports", reportRoutes.routes(), reportRoutes.allowedMethods()); router.use(
router.use("/api/standard-rates", standardRateRoutes.routes(), standardRateRoutes.allowedMethods()); "/api/work-allocations",
router.use("/api/activities", activityRoutes.routes(), activityRoutes.allowedMethods()); workAllocationRoutes.routes(),
workAllocationRoutes.allowedMethods(),
);
router.use(
"/api/attendance",
attendanceRoutes.routes(),
attendanceRoutes.allowedMethods(),
);
router.use(
"/api/contractor-rates",
contractorRateRoutes.routes(),
contractorRateRoutes.allowedMethods(),
);
router.use(
"/api/employee-swaps",
employeeSwapRoutes.routes(),
employeeSwapRoutes.allowedMethods(),
);
router.use(
"/api/reports",
reportRoutes.routes(),
reportRoutes.allowedMethods(),
);
router.use(
"/api/standard-rates",
standardRateRoutes.routes(),
standardRateRoutes.allowedMethods(),
);
router.use(
"/api/activities",
activityRoutes.routes(),
activityRoutes.allowedMethods(),
);
// Apply routes // Apply routes
app.use(router.routes()); app.use(router.routes());

View File

@@ -1,5 +1,5 @@
import { Context, Next } from "@oak/oak"; import { Context, Next } from "@oak/oak";
import { verify, create, getNumericDate } from "djwt"; import { create, getNumericDate, verify } from "djwt";
import { config } from "../config/env.ts"; import { config } from "../config/env.ts";
import type { JWTPayload, UserRole } from "../types/index.ts"; import type { JWTPayload, UserRole } from "../types/index.ts";
@@ -12,11 +12,13 @@ const cryptoKey = await crypto.subtle.importKey(
keyData, keyData,
{ name: "HMAC", hash: "SHA-256" }, { name: "HMAC", hash: "SHA-256" },
false, false,
["sign", "verify"] ["sign", "verify"],
); );
// Generate JWT token // Generate JWT token
export async function generateToken(payload: Omit<JWTPayload, "exp" | "iat">): Promise<string> { export async function generateToken(
payload: Omit<JWTPayload, "exp" | "iat">,
): Promise<string> {
const expiresIn = config.JWT_EXPIRES_IN; const expiresIn = config.JWT_EXPIRES_IN;
let expSeconds = 7 * 24 * 60 * 60; // Default 7 days let expSeconds = 7 * 24 * 60 * 60; // Default 7 days
@@ -35,7 +37,7 @@ export async function generateToken(payload: Omit<JWTPayload, "exp" | "iat">): P
exp: getNumericDate(expSeconds), exp: getNumericDate(expSeconds),
iat: getNumericDate(0), iat: getNumericDate(0),
}, },
cryptoKey cryptoKey,
); );
return token; return token;
@@ -52,7 +54,10 @@ export async function verifyToken(token: string): Promise<JWTPayload | null> {
} }
// Authentication middleware // Authentication middleware
export async function authenticateToken(ctx: Context, next: Next): Promise<void> { export async function authenticateToken(
ctx: Context,
next: Next,
): Promise<void> {
const authHeader = ctx.request.headers.get("Authorization"); const authHeader = ctx.request.headers.get("Authorization");
const token = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : null; const token = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : null;

View File

@@ -22,7 +22,7 @@ export async function rateLimit(ctx: Context, next: Next): Promise<void> {
ctx.response.status = 429; ctx.response.status = 429;
ctx.response.body = { ctx.response.body = {
error: "Too many requests", error: "Too many requests",
retryAfter: Math.ceil((record.resetTime - now) / 1000) retryAfter: Math.ceil((record.resetTime - now) / 1000),
}; };
return; return;
} }
@@ -45,19 +45,22 @@ export async function securityHeaders(ctx: Context, next: Next): Promise<void> {
ctx.response.headers.set("X-XSS-Protection", "1; mode=block"); ctx.response.headers.set("X-XSS-Protection", "1; mode=block");
// Referrer policy // Referrer policy
ctx.response.headers.set("Referrer-Policy", "strict-origin-when-cross-origin"); ctx.response.headers.set(
"Referrer-Policy",
"strict-origin-when-cross-origin",
);
// Content Security Policy // Content Security Policy
ctx.response.headers.set( ctx.response.headers.set(
"Content-Security-Policy", "Content-Security-Policy",
"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline';" "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline';",
); );
// Strict Transport Security (only in production with HTTPS) // Strict Transport Security (only in production with HTTPS)
if (config.isProduction()) { if (config.isProduction()) {
ctx.response.headers.set( ctx.response.headers.set(
"Strict-Transport-Security", "Strict-Transport-Security",
"max-age=31536000; includeSubDomains" "max-age=31536000; includeSubDomains",
); );
} }
} }
@@ -65,18 +68,26 @@ export async function securityHeaders(ctx: Context, next: Next): Promise<void> {
// CORS middleware // CORS middleware
export async function cors(ctx: Context, next: Next): Promise<void> { export async function cors(ctx: Context, next: Next): Promise<void> {
const origin = ctx.request.headers.get("Origin"); const origin = ctx.request.headers.get("Origin");
const allowedOrigins = config.CORS_ORIGIN.split(",").map(o => o.trim()); const allowedOrigins = config.CORS_ORIGIN.split(",").map((o) => o.trim());
// Check if origin is allowed // Check if origin is allowed
if (origin && (allowedOrigins.includes(origin) || allowedOrigins.includes("*"))) { if (
origin && (allowedOrigins.includes(origin) || allowedOrigins.includes("*"))
) {
ctx.response.headers.set("Access-Control-Allow-Origin", origin); ctx.response.headers.set("Access-Control-Allow-Origin", origin);
} else if (config.isDevelopment()) { } else if (config.isDevelopment()) {
// Allow all origins in development // Allow all origins in development
ctx.response.headers.set("Access-Control-Allow-Origin", origin || "*"); ctx.response.headers.set("Access-Control-Allow-Origin", origin || "*");
} }
ctx.response.headers.set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS"); ctx.response.headers.set(
ctx.response.headers.set("Access-Control-Allow-Headers", "Content-Type, Authorization"); "Access-Control-Allow-Methods",
"GET, POST, PUT, DELETE, OPTIONS",
);
ctx.response.headers.set(
"Access-Control-Allow-Headers",
"Content-Type, Authorization",
);
ctx.response.headers.set("Access-Control-Allow-Credentials", "true"); ctx.response.headers.set("Access-Control-Allow-Credentials", "true");
ctx.response.headers.set("Access-Control-Max-Age", "86400"); ctx.response.headers.set("Access-Control-Max-Age", "86400");
@@ -105,7 +116,9 @@ export async function requestLogger(ctx: Context, next: Next): Promise<void> {
if (status >= 500) statusColor = "\x1b[31m"; // Red for 5xx if (status >= 500) statusColor = "\x1b[31m"; // Red for 5xx
console.log( console.log(
`${new Date().toISOString()} - ${method} ${url.pathname} ${statusColor}${status}\x1b[0m ${ms}ms` `${
new Date().toISOString()
} - ${method} ${url.pathname} ${statusColor}${status}\x1b[0m ${ms}ms`,
); );
} }
@@ -125,18 +138,32 @@ export function isValidEmail(email: string): boolean {
} }
// Validate password strength // Validate password strength
export function isStrongPassword(password: string): { valid: boolean; message?: string } { export function isStrongPassword(
password: string,
): { valid: boolean; message?: string } {
if (password.length < 8) { if (password.length < 8) {
return { valid: false, message: "Password must be at least 8 characters long" }; return {
valid: false,
message: "Password must be at least 8 characters long",
};
} }
if (!/[A-Z]/.test(password)) { if (!/[A-Z]/.test(password)) {
return { valid: false, message: "Password must contain at least one uppercase letter" }; return {
valid: false,
message: "Password must contain at least one uppercase letter",
};
} }
if (!/[a-z]/.test(password)) { if (!/[a-z]/.test(password)) {
return { valid: false, message: "Password must contain at least one lowercase letter" }; return {
valid: false,
message: "Password must contain at least one lowercase letter",
};
} }
if (!/[0-9]/.test(password)) { if (!/[0-9]/.test(password)) {
return { valid: false, message: "Password must contain at least one number" }; return {
valid: false,
message: "Password must contain at least one number",
};
} }
return { valid: true }; return { valid: true };
} }

View File

@@ -69,7 +69,7 @@ router.get("/:id", authenticateToken, async (ctx) => {
JOIN sub_departments sd ON a.sub_department_id = sd.id JOIN sub_departments sd ON a.sub_department_id = sd.id
JOIN departments d ON sd.department_id = d.id JOIN departments d ON sd.department_id = d.id
WHERE a.id = ?`, WHERE a.id = ?`,
[activityId] [activityId],
); );
if (activities.length === 0) { if (activities.length === 0) {
@@ -102,7 +102,7 @@ router.post("/", authenticateToken, async (ctx) => {
// Get the sub-department to check department ownership // Get the sub-department to check department ownership
const subDepts = await db.query<{ department_id: number }[]>( const subDepts = await db.query<{ department_id: number }[]>(
"SELECT department_id FROM sub_departments WHERE id = ?", "SELECT department_id FROM sub_departments WHERE id = ?",
[sub_department_id] [sub_department_id],
); );
if (subDepts.length === 0) { if (subDepts.length === 0) {
@@ -114,13 +114,17 @@ router.post("/", authenticateToken, async (ctx) => {
const subDeptDepartmentId = subDepts[0].department_id; const subDeptDepartmentId = subDepts[0].department_id;
// Check authorization // Check authorization
if (user.role === 'Supervisor' && user.departmentId !== subDeptDepartmentId) { if (
user.role === "Supervisor" && user.departmentId !== subDeptDepartmentId
) {
ctx.response.status = 403; ctx.response.status = 403;
ctx.response.body = { error: "You can only create activities for your own department" }; ctx.response.body = {
error: "You can only create activities for your own department",
};
return; return;
} }
if (user.role !== 'SuperAdmin' && user.role !== 'Supervisor') { if (user.role !== "SuperAdmin" && user.role !== "Supervisor") {
ctx.response.status = 403; ctx.response.status = 403;
ctx.response.body = { error: "Unauthorized" }; ctx.response.body = { error: "Unauthorized" };
return; return;
@@ -128,19 +132,21 @@ router.post("/", authenticateToken, async (ctx) => {
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"],
); );
ctx.response.status = 201; ctx.response.status = 201;
ctx.response.body = { ctx.response.body = {
id: result.lastInsertId, id: result.insertId,
message: "Activity created successfully" message: "Activity created successfully",
}; };
} catch (error) { } catch (error) {
const err = error as { code?: string }; const err = error as { code?: string };
if (err.code === "ER_DUP_ENTRY") { if (err.code === "ER_DUP_ENTRY") {
ctx.response.status = 400; ctx.response.status = 400;
ctx.response.body = { error: "Activity already exists in this sub-department" }; ctx.response.body = {
error: "Activity already exists in this sub-department",
};
return; return;
} }
console.error("Create activity error:", error); console.error("Create activity error:", error);
@@ -158,7 +164,7 @@ router.put("/:id", authenticateToken, async (ctx) => {
await db.execute( await db.execute(
"UPDATE activities SET name = ?, unit_of_measurement = ? WHERE id = ?", "UPDATE activities SET name = ?, unit_of_measurement = ? WHERE id = ?",
[name, unit_of_measurement, activityId] [name, unit_of_measurement, activityId],
); );
ctx.response.body = { message: "Activity updated successfully" }; ctx.response.body = { message: "Activity updated successfully" };
@@ -181,7 +187,7 @@ router.delete("/:id", authenticateToken, async (ctx) => {
FROM activities a FROM activities a
JOIN sub_departments sd ON a.sub_department_id = sd.id JOIN sub_departments sd ON a.sub_department_id = sd.id
WHERE a.id = ?`, WHERE a.id = ?`,
[activityId] [activityId],
); );
if (activities.length === 0) { if (activities.length === 0) {
@@ -193,13 +199,17 @@ router.delete("/:id", authenticateToken, async (ctx) => {
const activity = activities[0] as Activity & { department_id: number }; const activity = activities[0] as Activity & { department_id: number };
// Check authorization // Check authorization
if (user.role === 'Supervisor' && user.departmentId !== activity.department_id) { if (
user.role === "Supervisor" && user.departmentId !== activity.department_id
) {
ctx.response.status = 403; ctx.response.status = 403;
ctx.response.body = { error: "You can only delete activities from your own department" }; ctx.response.body = {
error: "You can only delete activities from your own department",
};
return; return;
} }
if (user.role !== 'SuperAdmin' && user.role !== 'Supervisor') { if (user.role !== "SuperAdmin" && user.role !== "Supervisor") {
ctx.response.status = 403; ctx.response.status = 403;
ctx.response.body = { error: "Unauthorized" }; ctx.response.body = { error: "Unauthorized" };
return; return;

View File

@@ -1,19 +1,35 @@
import { Router } from "@oak/oak"; import { Router, type RouterContext, type State } from "@oak/oak";
import { db } from "../config/database.ts"; import { db } from "../config/database.ts";
import { authenticateToken, authorize, getCurrentUser } from "../middleware/auth.ts"; import {
import type { Attendance, CheckInOutRequest, User, UpdateAttendanceStatusRequest, AttendanceStatus } from "../types/index.ts"; authenticateToken,
authorize,
getCurrentUser,
} from "../middleware/auth.ts";
import type {
Attendance,
AttendanceStatus,
CheckInOutRequest,
JWTPayload,
UpdateAttendanceStatusRequest,
User,
} from "../types/index.ts";
const router = new Router(); const router = new Router();
// Get all attendance records // Get all attendance records
router.get("/", authenticateToken, async (ctx) => { router.get(
"/",
authenticateToken,
async (
ctx: RouterContext<"/", Record<string | number, string | undefined>, State>,
) => {
try { try {
const currentUser = getCurrentUser(ctx); const currentUser: JWTPayload = getCurrentUser(ctx);
const params = ctx.request.url.searchParams; const params: URLSearchParams = ctx.request.url.searchParams;
const employeeId = params.get("employeeId"); const employeeId: string | null = params.get("employeeId");
const startDate = params.get("startDate"); const startDate: string | null = params.get("startDate");
const endDate = params.get("endDate"); const endDate: string | null = params.get("endDate");
const status = params.get("status"); const status: string | null = params.get("status");
let query = ` let query = `
SELECT a.*, SELECT a.*,
@@ -68,7 +84,8 @@ router.get("/", authenticateToken, async (ctx) => {
ctx.response.status = 500; ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" }; ctx.response.body = { error: "Internal server error" };
} }
}); },
);
// Get attendance by ID // Get attendance by ID
router.get("/:id", authenticateToken, async (ctx) => { router.get("/:id", authenticateToken, async (ctx) => {
@@ -87,7 +104,7 @@ router.get("/:id", authenticateToken, async (ctx) => {
LEFT JOIN departments d ON e.department_id = d.id LEFT JOIN departments d ON e.department_id = d.id
LEFT JOIN users c ON e.contractor_id = c.id LEFT JOIN users c ON e.contractor_id = c.id
WHERE a.id = ?`, WHERE a.id = ?`,
[attendanceId] [attendanceId],
); );
if (records.length === 0) { if (records.length === 0) {
@@ -105,7 +122,11 @@ router.get("/:id", authenticateToken, async (ctx) => {
}); });
// Check in employee (Supervisor or SuperAdmin) // Check in employee (Supervisor or SuperAdmin)
router.post("/check-in", authenticateToken, authorize("Supervisor", "SuperAdmin"), async (ctx) => { router.post(
"/check-in",
authenticateToken,
authorize("Supervisor", "SuperAdmin"),
async (ctx) => {
try { try {
const currentUser = getCurrentUser(ctx); const currentUser = getCurrentUser(ctx);
const body = await ctx.request.body.json() as CheckInOutRequest; const body = await ctx.request.body.json() as CheckInOutRequest;
@@ -130,14 +151,16 @@ router.post("/check-in", authenticateToken, authorize("Supervisor", "SuperAdmin"
if (employees.length === 0) { if (employees.length === 0) {
ctx.response.status = 403; ctx.response.status = 403;
ctx.response.body = { error: "Employee not found or not in your department" }; ctx.response.body = {
error: "Employee not found or not in your department",
};
return; return;
} }
// Check if already checked in today // Check if already checked in today
const existing = await db.query<Attendance[]>( const existing = await db.query<Attendance[]>(
"SELECT * FROM attendance WHERE employee_id = ? AND work_date = ? AND status = ?", "SELECT * FROM attendance WHERE employee_id = ? AND work_date = ? AND status = ?",
[employeeId, workDate, "CheckedIn"] [employeeId, workDate, "CheckedIn"],
); );
if (existing.length > 0) { if (existing.length > 0) {
@@ -146,11 +169,14 @@ router.post("/check-in", authenticateToken, authorize("Supervisor", "SuperAdmin"
return; return;
} }
const checkInTime = new Date().toISOString().slice(0, 19).replace("T", " "); const checkInTime = new Date().toISOString().slice(0, 19).replace(
"T",
" ",
);
const result = await db.execute( const result = await db.execute(
"INSERT INTO attendance (employee_id, supervisor_id, check_in_time, work_date, status) VALUES (?, ?, ?, ?, ?)", "INSERT INTO attendance (employee_id, supervisor_id, check_in_time, work_date, status) VALUES (?, ?, ?, ?, ?)",
[employeeId, currentUser.id, checkInTime, workDate, "CheckedIn"] [employeeId, currentUser.id, checkInTime, workDate, "CheckedIn"],
); );
const newRecord = await db.query<Attendance[]>( const newRecord = await db.query<Attendance[]>(
@@ -165,7 +191,7 @@ router.post("/check-in", authenticateToken, authorize("Supervisor", "SuperAdmin"
LEFT JOIN departments d ON e.department_id = d.id LEFT JOIN departments d ON e.department_id = d.id
LEFT JOIN users c ON e.contractor_id = c.id LEFT JOIN users c ON e.contractor_id = c.id
WHERE a.id = ?`, WHERE a.id = ?`,
[result.insertId] [result.insertId],
); );
ctx.response.status = 201; ctx.response.status = 201;
@@ -175,10 +201,15 @@ router.post("/check-in", authenticateToken, authorize("Supervisor", "SuperAdmin"
ctx.response.status = 500; ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" }; ctx.response.body = { error: "Internal server error" };
} }
}); },
);
// Check out employee (Supervisor or SuperAdmin) // Check out employee (Supervisor or SuperAdmin)
router.post("/check-out", authenticateToken, authorize("Supervisor", "SuperAdmin"), async (ctx) => { router.post(
"/check-out",
authenticateToken,
authorize("Supervisor", "SuperAdmin"),
async (ctx) => {
try { try {
const currentUser = getCurrentUser(ctx); const currentUser = getCurrentUser(ctx);
const body = await ctx.request.body.json() as CheckInOutRequest; const body = await ctx.request.body.json() as CheckInOutRequest;
@@ -191,7 +222,8 @@ router.post("/check-out", authenticateToken, authorize("Supervisor", "SuperAdmin
} }
// Find the check-in record // Find the check-in record
let query = "SELECT * FROM attendance WHERE employee_id = ? AND work_date = ? AND status = ?"; let query =
"SELECT * FROM attendance WHERE employee_id = ? AND work_date = ? AND status = ?";
const params: unknown[] = [employeeId, workDate, "CheckedIn"]; const params: unknown[] = [employeeId, workDate, "CheckedIn"];
if (currentUser.role === "Supervisor") { if (currentUser.role === "Supervisor") {
@@ -207,11 +239,14 @@ router.post("/check-out", authenticateToken, authorize("Supervisor", "SuperAdmin
return; return;
} }
const checkOutTime = new Date().toISOString().slice(0, 19).replace("T", " "); const checkOutTime = new Date().toISOString().slice(0, 19).replace(
"T",
" ",
);
await db.execute( await db.execute(
"UPDATE attendance SET check_out_time = ?, status = ? WHERE id = ?", "UPDATE attendance SET check_out_time = ?, status = ? WHERE id = ?",
[checkOutTime, "CheckedOut", records[0].id] [checkOutTime, "CheckedOut", records[0].id],
); );
const updatedRecord = await db.query<Attendance[]>( const updatedRecord = await db.query<Attendance[]>(
@@ -226,7 +261,7 @@ router.post("/check-out", authenticateToken, authorize("Supervisor", "SuperAdmin
LEFT JOIN departments d ON e.department_id = d.id LEFT JOIN departments d ON e.department_id = d.id
LEFT JOIN users c ON e.contractor_id = c.id LEFT JOIN users c ON e.contractor_id = c.id
WHERE a.id = ?`, WHERE a.id = ?`,
[records[0].id] [records[0].id],
); );
ctx.response.body = updatedRecord[0]; ctx.response.body = updatedRecord[0];
@@ -235,27 +270,42 @@ router.post("/check-out", authenticateToken, authorize("Supervisor", "SuperAdmin
ctx.response.status = 500; ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" }; ctx.response.body = { error: "Internal server error" };
} }
}); },
);
// Update attendance status (mark as Absent, HalfDay, Late) // Update attendance status (mark as Absent, HalfDay, Late)
router.put("/:id/status", authenticateToken, authorize("Supervisor", "SuperAdmin"), async (ctx) => { router.put(
"/:id/status",
authenticateToken,
authorize("Supervisor", "SuperAdmin"),
async (ctx) => {
try { try {
const attendanceId = ctx.params.id; const attendanceId = ctx.params.id;
const body = await ctx.request.body.json() as UpdateAttendanceStatusRequest; const body = await ctx.request.body
.json() as UpdateAttendanceStatusRequest;
const { status, remark } = body; const { status, remark } = body;
// Validate status // Validate status
const validStatuses: AttendanceStatus[] = ["CheckedIn", "CheckedOut", "Absent", "HalfDay", "Late"]; const validStatuses: AttendanceStatus[] = [
"CheckedIn",
"CheckedOut",
"Absent",
"HalfDay",
"Late",
];
if (!validStatuses.includes(status)) { if (!validStatuses.includes(status)) {
ctx.response.status = 400; ctx.response.status = 400;
ctx.response.body = { error: "Invalid status. Must be one of: CheckedIn, CheckedOut, Absent, HalfDay, Late" }; ctx.response.body = {
error:
"Invalid status. Must be one of: CheckedIn, CheckedOut, Absent, HalfDay, Late",
};
return; return;
} }
// Check if record exists // Check if record exists
const existing = await db.query<Attendance[]>( const existing = await db.query<Attendance[]>(
"SELECT * FROM attendance WHERE id = ?", "SELECT * FROM attendance WHERE id = ?",
[attendanceId] [attendanceId],
); );
if (existing.length === 0) { if (existing.length === 0) {
@@ -267,7 +317,7 @@ router.put("/:id/status", authenticateToken, authorize("Supervisor", "SuperAdmin
// Update the status // Update the status
await db.execute( await db.execute(
"UPDATE attendance SET status = ?, remark = ? WHERE id = ?", "UPDATE attendance SET status = ?, remark = ? WHERE id = ?",
[status, remark || null, attendanceId] [status, remark || null, attendanceId],
); );
const updatedRecord = await db.query<Attendance[]>( const updatedRecord = await db.query<Attendance[]>(
@@ -282,7 +332,7 @@ router.put("/:id/status", authenticateToken, authorize("Supervisor", "SuperAdmin
LEFT JOIN departments d ON e.department_id = d.id LEFT JOIN departments d ON e.department_id = d.id
LEFT JOIN users c ON e.contractor_id = c.id LEFT JOIN users c ON e.contractor_id = c.id
WHERE a.id = ?`, WHERE a.id = ?`,
[attendanceId] [attendanceId],
); );
ctx.response.body = updatedRecord[0]; ctx.response.body = updatedRecord[0];
@@ -291,10 +341,15 @@ router.put("/:id/status", authenticateToken, authorize("Supervisor", "SuperAdmin
ctx.response.status = 500; ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" }; ctx.response.body = { error: "Internal server error" };
} }
}); },
);
// Mark employee as absent (create absent record) // Mark employee as absent (create absent record)
router.post("/mark-absent", authenticateToken, authorize("Supervisor", "SuperAdmin"), async (ctx) => { router.post(
"/mark-absent",
authenticateToken,
authorize("Supervisor", "SuperAdmin"),
async (ctx) => {
try { try {
const currentUser = getCurrentUser(ctx); const currentUser = getCurrentUser(ctx);
const body = await ctx.request.body.json(); const body = await ctx.request.body.json();
@@ -309,14 +364,14 @@ router.post("/mark-absent", authenticateToken, authorize("Supervisor", "SuperAdm
// Check if record already exists for this date // Check if record already exists for this date
const existing = await db.query<Attendance[]>( const existing = await db.query<Attendance[]>(
"SELECT * FROM attendance WHERE employee_id = ? AND work_date = ?", "SELECT * FROM attendance WHERE employee_id = ? AND work_date = ?",
[employeeId, workDate] [employeeId, workDate],
); );
if (existing.length > 0) { if (existing.length > 0) {
// Update existing record to Absent // Update existing record to Absent
await db.execute( await db.execute(
"UPDATE attendance SET status = ?, remark = ? WHERE id = ?", "UPDATE attendance SET status = ?, remark = ? WHERE id = ?",
["Absent", remark || "Marked absent", existing[0].id] ["Absent", remark || "Marked absent", existing[0].id],
); );
const updatedRecord = await db.query<Attendance[]>( const updatedRecord = await db.query<Attendance[]>(
@@ -331,7 +386,7 @@ router.post("/mark-absent", authenticateToken, authorize("Supervisor", "SuperAdm
LEFT JOIN departments d ON e.department_id = d.id LEFT JOIN departments d ON e.department_id = d.id
LEFT JOIN users c ON e.contractor_id = c.id LEFT JOIN users c ON e.contractor_id = c.id
WHERE a.id = ?`, WHERE a.id = ?`,
[existing[0].id] [existing[0].id],
); );
ctx.response.body = updatedRecord[0]; ctx.response.body = updatedRecord[0];
@@ -339,7 +394,13 @@ router.post("/mark-absent", authenticateToken, authorize("Supervisor", "SuperAdm
// Create new absent record // Create new absent record
const result = await db.execute( const result = await db.execute(
"INSERT INTO attendance (employee_id, supervisor_id, work_date, status, remark) VALUES (?, ?, ?, ?, ?)", "INSERT INTO attendance (employee_id, supervisor_id, work_date, status, remark) VALUES (?, ?, ?, ?, ?)",
[employeeId, currentUser.id, workDate, "Absent", remark || "Marked absent"] [
employeeId,
currentUser.id,
workDate,
"Absent",
remark || "Marked absent",
],
); );
const newRecord = await db.query<Attendance[]>( const newRecord = await db.query<Attendance[]>(
@@ -354,7 +415,7 @@ router.post("/mark-absent", authenticateToken, authorize("Supervisor", "SuperAdm
LEFT JOIN departments d ON e.department_id = d.id LEFT JOIN departments d ON e.department_id = d.id
LEFT JOIN users c ON e.contractor_id = c.id LEFT JOIN users c ON e.contractor_id = c.id
WHERE a.id = ?`, WHERE a.id = ?`,
[result.insertId] [result.insertId],
); );
ctx.response.status = 201; ctx.response.status = 201;
@@ -365,18 +426,28 @@ router.post("/mark-absent", authenticateToken, authorize("Supervisor", "SuperAdm
ctx.response.status = 500; ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" }; ctx.response.body = { error: "Internal server error" };
} }
}); },
);
// Get attendance summary // Get attendance summary
router.get("/summary/stats", authenticateToken, async (ctx) => { router.get(
"/summary/stats",
authenticateToken,
async (
ctx: RouterContext<
"/summary/stats",
Record<string | number, string | undefined>,
State
>,
) => {
try { try {
const currentUser = getCurrentUser(ctx); const currentUser: JWTPayload = getCurrentUser(ctx);
const params = ctx.request.url.searchParams; const params: URLSearchParams = ctx.request.url.searchParams;
const startDate = params.get("startDate"); const startDate: string | null = params.get("startDate");
const endDate = params.get("endDate"); const endDate: string | null = params.get("endDate");
const departmentId = params.get("departmentId"); const departmentId: string | null = params.get("departmentId");
let query = ` let query: string = `
SELECT SELECT
COUNT(DISTINCT a.employee_id) as total_employees, COUNT(DISTINCT a.employee_id) as total_employees,
COUNT(DISTINCT CASE WHEN a.status = 'CheckedIn' THEN a.employee_id END) as checked_in, COUNT(DISTINCT CASE WHEN a.status = 'CheckedIn' THEN a.employee_id END) as checked_in,
@@ -387,7 +458,7 @@ router.get("/summary/stats", authenticateToken, async (ctx) => {
LEFT JOIN departments d ON e.department_id = d.id LEFT JOIN departments d ON e.department_id = d.id
WHERE 1=1 WHERE 1=1
`; `;
const queryParams: unknown[] = []; const queryParams: (number | string)[] = [];
if (currentUser.role === "Supervisor") { if (currentUser.role === "Supervisor") {
query += " AND a.supervisor_id = ?"; query += " AND a.supervisor_id = ?";
@@ -418,6 +489,7 @@ router.get("/summary/stats", authenticateToken, async (ctx) => {
ctx.response.status = 500; ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" }; ctx.response.body = { error: "Internal server error" };
} }
}); },
);
export default router; export default router;

View File

@@ -1,16 +1,23 @@
import { Router } from "@oak/oak"; import { Router } from "@oak/oak";
import { hash, compare, genSalt } from "bcrypt"; import { compare, genSalt, hash } from "bcrypt";
import { db } from "../config/database.ts"; import { db } from "../config/database.ts";
import { config } from "../config/env.ts"; import { config } from "../config/env.ts";
// Helper function to hash password with proper salt generation
async function hashPassword(password: string): Promise<string> { async function hashPassword(password: string): Promise<string> {
const salt = await genSalt(config.BCRYPT_ROUNDS); const salt = await genSalt(config.BCRYPT_ROUNDS);
return await hash(password, salt); return await hash(password, salt);
} }
import { authenticateToken, generateToken, getCurrentUser } from "../middleware/auth.ts"; import {
import { sanitizeInput, isValidEmail, isStrongPassword } from "../middleware/security.ts"; authenticateToken,
import type { User, LoginRequest, ChangePasswordRequest } from "../types/index.ts"; generateToken,
getCurrentUser,
} from "../middleware/auth.ts";
import { isStrongPassword, sanitizeInput } from "../middleware/security.ts";
import type {
ChangePasswordRequest,
LoginRequest,
User,
} from "../types/index.ts";
const router = new Router(); const router = new Router();
@@ -33,7 +40,7 @@ router.post("/login", async (ctx) => {
// Query user // Query user
const users = await db.query<User[]>( const users = await db.query<User[]>(
"SELECT * FROM users WHERE username = ? AND is_active = TRUE", "SELECT * FROM users WHERE username = ? AND is_active = TRUE",
[sanitizedUsername] [sanitizedUsername],
); );
if (users.length === 0) { if (users.length === 0) {
@@ -83,7 +90,7 @@ router.get("/me", authenticateToken, async (ctx) => {
const users = await db.query<User[]>( const users = await db.query<User[]>(
"SELECT id, username, name, email, role, department_id, contractor_id, is_active FROM users WHERE id = ?", "SELECT id, username, name, email, role, department_id, contractor_id, is_active FROM users WHERE id = ?",
[currentUser.id] [currentUser.id],
); );
if (users.length === 0) { if (users.length === 0) {
@@ -131,7 +138,7 @@ router.post("/change-password", authenticateToken, async (ctx) => {
// Get current password hash // Get current password hash
const users = await db.query<User[]>( const users = await db.query<User[]>(
"SELECT password FROM users WHERE id = ?", "SELECT password FROM users WHERE id = ?",
[currentUser.id] [currentUser.id],
); );
if (users.length === 0) { if (users.length === 0) {
@@ -155,7 +162,7 @@ router.post("/change-password", authenticateToken, async (ctx) => {
// Update password // Update password
await db.execute( await db.execute(
"UPDATE users SET password = ? WHERE id = ?", "UPDATE users SET password = ? WHERE id = ?",
[hashedPassword, currentUser.id] [hashedPassword, currentUser.id],
); );
ctx.response.body = { message: "Password changed successfully" }; ctx.response.body = { message: "Password changed successfully" };

View File

@@ -1,19 +1,32 @@
import { Router } from "@oak/oak"; import { Router, type RouterContext, type State } from "@oak/oak";
import { db } from "../config/database.ts"; import { db } from "../config/database.ts";
import { authenticateToken, authorize, getCurrentUser } from "../middleware/auth.ts"; import {
authenticateToken,
authorize,
getCurrentUser,
} from "../middleware/auth.ts";
import { sanitizeInput } from "../middleware/security.ts"; import { sanitizeInput } from "../middleware/security.ts";
import type { ContractorRate, CreateContractorRateRequest, User } from "../types/index.ts"; import type {
ContractorRate,
CreateContractorRateRequest,
User,
} from "../types/index.ts";
const router = new Router(); const router = new Router();
// Get contractor rates // Get contractor rates
router.get("/", authenticateToken, async (ctx) => { router.get(
"/",
authenticateToken,
async (
ctx: RouterContext<"/", Record<string | number, string | undefined>, State>,
) => {
try { try {
const params = ctx.request.url.searchParams; const params: URLSearchParams = ctx.request.url.searchParams;
const contractorId = params.get("contractorId"); const contractorId: string | null = params.get("contractorId");
const subDepartmentId = params.get("subDepartmentId"); const subDepartmentId: string | null = params.get("subDepartmentId");
let query = ` let query: string = `
SELECT cr.*, SELECT cr.*,
u.name as contractor_name, u.username as contractor_username, u.name as contractor_name, u.username as contractor_username,
sd.name as sub_department_name, sd.name as sub_department_name,
@@ -47,16 +60,26 @@ router.get("/", authenticateToken, async (ctx) => {
ctx.response.status = 500; ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" }; ctx.response.body = { error: "Internal server error" };
} }
}); },
);
// Get current rate for a contractor + sub-department combination // Get current rate for a contractor + sub-department combination
router.get("/contractor/:contractorId/current", authenticateToken, async (ctx) => { router.get(
"/contractor/:contractorId/current",
authenticateToken,
async (
ctx: RouterContext<
"/contractor/:contractorId/current",
{ contractorId: string } & Record<string | number, string | undefined>,
State
>,
) => {
try { try {
const contractorId = ctx.params.contractorId; const contractorId = ctx.params.contractorId;
const params = ctx.request.url.searchParams; const params = ctx.request.url.searchParams;
const subDepartmentId = params.get("subDepartmentId"); const subDepartmentId = params.get("subDepartmentId");
let query = ` let query: string = `
SELECT cr.*, SELECT cr.*,
u.name as contractor_name, u.username as contractor_username, u.name as contractor_name, u.username as contractor_username,
sd.name as sub_department_name, sd.name as sub_department_name,
@@ -90,25 +113,35 @@ router.get("/contractor/:contractorId/current", authenticateToken, async (ctx) =
ctx.response.status = 500; ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" }; ctx.response.body = { error: "Internal server error" };
} }
}); },
);
// Set contractor rate (Supervisor or SuperAdmin) // Set contractor rate (Supervisor or SuperAdmin)
router.post("/", authenticateToken, authorize("Supervisor", "SuperAdmin"), async (ctx) => { router.post(
"/",
authenticateToken,
authorize("Supervisor", "SuperAdmin"),
async (
ctx: RouterContext<"/", Record<string | number, string | undefined>, State>,
) => {
try { try {
const currentUser = getCurrentUser(ctx); const currentUser = getCurrentUser(ctx);
const body = await ctx.request.body.json() as CreateContractorRateRequest; const body = await ctx.request.body.json() as CreateContractorRateRequest;
const { contractorId, subDepartmentId, activity, rate, effectiveDate } = body; const { contractorId, subDepartmentId, activity, rate, effectiveDate } =
body;
if (!contractorId || !rate || !effectiveDate) { if (!contractorId || !rate || !effectiveDate) {
ctx.response.status = 400; ctx.response.status = 400;
ctx.response.body = { error: "Missing required fields (contractorId, rate, effectiveDate)" }; ctx.response.body = {
error: "Missing required fields (contractorId, rate, effectiveDate)",
};
return; return;
} }
// Verify contractor exists // Verify contractor exists
const contractors = await db.query<User[]>( const contractors = await db.query<User[]>(
"SELECT * FROM users WHERE id = ? AND role = ?", "SELECT * FROM users WHERE id = ? AND role = ?",
[contractorId, "Contractor"] [contractorId, "Contractor"],
); );
if (contractors.length === 0) { if (contractors.length === 0) {
@@ -118,7 +151,10 @@ router.post("/", authenticateToken, authorize("Supervisor", "SuperAdmin"), async
} }
// Supervisors can only set rates for contractors in their department // Supervisors can only set rates for contractors in their department
if (currentUser.role === "Supervisor" && contractors[0].department_id !== currentUser.departmentId) { if (
currentUser.role === "Supervisor" &&
contractors[0].department_id !== currentUser.departmentId
) {
ctx.response.status = 403; ctx.response.status = 403;
ctx.response.body = { error: "Contractor not in your department" }; ctx.response.body = { error: "Contractor not in your department" };
return; return;
@@ -126,12 +162,19 @@ router.post("/", authenticateToken, authorize("Supervisor", "SuperAdmin"), async
const sanitizedActivity = activity ? sanitizeInput(activity) : null; const sanitizedActivity = activity ? sanitizeInput(activity) : null;
const result = await db.execute( const result: { insertId: number; affectedRows: number } = await db
.execute(
"INSERT INTO contractor_rates (contractor_id, sub_department_id, activity, rate, effective_date) VALUES (?, ?, ?, ?, ?)", "INSERT INTO contractor_rates (contractor_id, sub_department_id, activity, rate, effective_date) VALUES (?, ?, ?, ?, ?)",
[contractorId, subDepartmentId || null, sanitizedActivity, rate, effectiveDate] [
contractorId,
subDepartmentId || null,
sanitizedActivity,
rate,
effectiveDate,
],
); );
const newRate = await db.query<ContractorRate[]>( const newRate: ContractorRate[] = await db.query<ContractorRate[]>(
`SELECT cr.*, `SELECT cr.*,
u.name as contractor_name, u.username as contractor_username, u.name as contractor_name, u.username as contractor_username,
sd.name as sub_department_name, sd.name as sub_department_name,
@@ -141,7 +184,7 @@ router.post("/", authenticateToken, authorize("Supervisor", "SuperAdmin"), async
LEFT JOIN sub_departments sd ON cr.sub_department_id = sd.id LEFT JOIN sub_departments sd ON cr.sub_department_id = sd.id
LEFT JOIN activities a ON a.sub_department_id = cr.sub_department_id AND a.name = cr.activity LEFT JOIN activities a ON a.sub_department_id = cr.sub_department_id AND a.name = cr.activity
WHERE cr.id = ?`, WHERE cr.id = ?`,
[result.insertId] [result.insertId],
); );
ctx.response.status = 201; ctx.response.status = 201;
@@ -151,18 +194,33 @@ router.post("/", authenticateToken, authorize("Supervisor", "SuperAdmin"), async
ctx.response.status = 500; ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" }; ctx.response.body = { error: "Internal server error" };
} }
}); },
);
// Update contractor rate // Update contractor rate
router.put("/:id", authenticateToken, authorize("Supervisor", "SuperAdmin"), async (ctx) => { router.put(
"/:id",
authenticateToken,
authorize("Supervisor", "SuperAdmin"),
async (
ctx: RouterContext<
"/:id",
{ id: string } & Record<string | number, string | undefined>,
State
>,
) => {
try { try {
const rateId = ctx.params.id; const rateId = ctx.params.id;
const body = await ctx.request.body.json() as { rate?: number; activity?: string; effectiveDate?: string }; const body = await ctx.request.body.json() as {
rate?: number;
activity?: string;
effectiveDate?: string;
};
const { rate, activity, effectiveDate } = body; const { rate, activity, effectiveDate } = body;
const existing = await db.query<ContractorRate[]>( const existing = await db.query<ContractorRate[]>(
"SELECT * FROM contractor_rates WHERE id = ?", "SELECT * FROM contractor_rates WHERE id = ?",
[rateId] [rateId],
); );
if (existing.length === 0) { if (existing.length === 0) {
@@ -197,7 +255,7 @@ router.put("/:id", authenticateToken, authorize("Supervisor", "SuperAdmin"), asy
await db.execute( await db.execute(
`UPDATE contractor_rates SET ${updates.join(", ")} WHERE id = ?`, `UPDATE contractor_rates SET ${updates.join(", ")} WHERE id = ?`,
params params,
); );
const updatedRate = await db.query<ContractorRate[]>( const updatedRate = await db.query<ContractorRate[]>(
@@ -210,7 +268,7 @@ router.put("/:id", authenticateToken, authorize("Supervisor", "SuperAdmin"), asy
LEFT JOIN sub_departments sd ON cr.sub_department_id = sd.id LEFT JOIN sub_departments sd ON cr.sub_department_id = sd.id
LEFT JOIN activities a ON a.sub_department_id = cr.sub_department_id AND a.name = cr.activity LEFT JOIN activities a ON a.sub_department_id = cr.sub_department_id AND a.name = cr.activity
WHERE cr.id = ?`, WHERE cr.id = ?`,
[rateId] [rateId],
); );
ctx.response.body = updatedRate[0]; ctx.response.body = updatedRate[0];
@@ -219,16 +277,27 @@ router.put("/:id", authenticateToken, authorize("Supervisor", "SuperAdmin"), asy
ctx.response.status = 500; ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" }; ctx.response.body = { error: "Internal server error" };
} }
}); },
);
// Delete contractor rate // Delete contractor rate
router.delete("/:id", authenticateToken, authorize("Supervisor", "SuperAdmin"), async (ctx) => { router.delete(
"/:id",
authenticateToken,
authorize("Supervisor", "SuperAdmin"),
async (
ctx: RouterContext<
"/:id",
{ id: string } & Record<string | number, string | undefined>,
State
>,
) => {
try { try {
const rateId = ctx.params.id; const rateId = ctx.params.id;
const existing = await db.query<ContractorRate[]>( const existing = await db.query<ContractorRate[]>(
"SELECT * FROM contractor_rates WHERE id = ?", "SELECT * FROM contractor_rates WHERE id = ?",
[rateId] [rateId],
); );
if (existing.length === 0) { if (existing.length === 0) {
@@ -244,6 +313,7 @@ router.delete("/:id", authenticateToken, authorize("Supervisor", "SuperAdmin"),
ctx.response.status = 500; ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" }; ctx.response.body = { error: "Internal server error" };
} }
}); },
);
export default router; export default router;

View File

@@ -1,16 +1,20 @@
import { Router } from "@oak/oak"; import { type Context, Router, type RouterContext } from "@oak/oak";
import { db } from "../config/database.ts"; import { db } from "../config/database.ts";
import { authenticateToken, authorize, getCurrentUser } from "../middleware/auth.ts"; import {
authenticateToken,
authorize,
getCurrentUser,
} from "../middleware/auth.ts";
import { sanitizeInput } from "../middleware/security.ts"; import { sanitizeInput } from "../middleware/security.ts";
import type { Department, SubDepartment } from "../types/index.ts"; import type { Department, SubDepartment } from "../types/index.ts";
const router = new Router(); const router = new Router();
// Get all departments // Get all departments
router.get("/", authenticateToken, async (ctx) => { router.get("/", authenticateToken, async (ctx: Context) => {
try { try {
const departments = await db.query<Department[]>( const departments = await db.query<Department[]>(
"SELECT * FROM departments ORDER BY name" "SELECT * FROM departments ORDER BY name",
); );
ctx.response.body = departments; ctx.response.body = departments;
} catch (error) { } catch (error) {
@@ -21,13 +25,13 @@ router.get("/", authenticateToken, async (ctx) => {
}); });
// Get department by ID // Get department by ID
router.get("/:id", authenticateToken, async (ctx) => { router.get("/:id", authenticateToken, async (ctx: RouterContext<"/:id">) => {
try { try {
const deptId = ctx.params.id; const deptId = ctx.params.id;
const departments = await db.query<Department[]>( const departments = await db.query<Department[]>(
"SELECT * FROM departments WHERE id = ?", "SELECT * FROM departments WHERE id = ?",
[deptId] [deptId],
); );
if (departments.length === 0) { if (departments.length === 0) {
@@ -44,14 +48,36 @@ router.get("/:id", authenticateToken, async (ctx) => {
} }
}); });
// Get all sub-departments (for reporting/filtering)
router.get(
"/sub-departments/all",
authenticateToken,
async (ctx: Context) => {
try {
const subDepartments = await db.query<SubDepartment[]>(
"SELECT sd.*, d.name as department_name FROM sub_departments sd LEFT JOIN departments d ON sd.department_id = d.id ORDER BY d.name, sd.name",
);
ctx.response.body = subDepartments;
} catch (error) {
console.error("Get all sub-departments error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
},
);
// Get sub-departments by department ID // Get sub-departments by department ID
router.get("/:id/sub-departments", authenticateToken, async (ctx) => { router.get(
"/:id/sub-departments",
authenticateToken,
async (ctx: RouterContext<"/:id/sub-departments">) => {
try { try {
const deptId = ctx.params.id; const deptId = ctx.params.id;
const subDepartments = await db.query<SubDepartment[]>( const subDepartments = await db.query<SubDepartment[]>(
"SELECT * FROM sub_departments WHERE department_id = ? ORDER BY name", "SELECT * FROM sub_departments WHERE department_id = ? ORDER BY name",
[deptId] [deptId],
); );
ctx.response.body = subDepartments; ctx.response.body = subDepartments;
@@ -60,10 +86,15 @@ router.get("/:id/sub-departments", authenticateToken, async (ctx) => {
ctx.response.status = 500; ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" }; ctx.response.body = { error: "Internal server error" };
} }
}); },
);
// Create department (SuperAdmin only) // Create department (SuperAdmin only)
router.post("/", authenticateToken, authorize("SuperAdmin"), async (ctx) => { router.post(
"/",
authenticateToken,
authorize("SuperAdmin"),
async (ctx: Context) => {
try { try {
const body = await ctx.request.body.json() as { name: string }; const body = await ctx.request.body.json() as { name: string };
const { name } = body; const { name } = body;
@@ -78,12 +109,12 @@ router.post("/", authenticateToken, authorize("SuperAdmin"), async (ctx) => {
const result = await db.execute( const result = await db.execute(
"INSERT INTO departments (name) VALUES (?)", "INSERT INTO departments (name) VALUES (?)",
[sanitizedName] [sanitizedName],
); );
const newDepartment = await db.query<Department[]>( const newDepartment = await db.query<Department[]>(
"SELECT * FROM departments WHERE id = ?", "SELECT * FROM departments WHERE id = ?",
[result.insertId] [result.insertId],
); );
ctx.response.status = 201; ctx.response.status = 201;
@@ -99,13 +130,17 @@ router.post("/", authenticateToken, authorize("SuperAdmin"), async (ctx) => {
ctx.response.status = 500; ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" }; ctx.response.body = { error: "Internal server error" };
} }
}); },
);
// Create sub-department (SuperAdmin or Supervisor for their own department) // Create sub-department (SuperAdmin or Supervisor for their own department)
router.post("/sub-departments", authenticateToken, async (ctx) => { router.post("/sub-departments", authenticateToken, async (ctx: Context) => {
try { try {
const user = getCurrentUser(ctx); const user = getCurrentUser(ctx);
const body = await ctx.request.body.json() as { department_id: number; name: string }; const body = await ctx.request.body.json() as {
department_id: number;
name: string;
};
const { department_id, name } = body; const { department_id, name } = body;
if (!name || !department_id) { if (!name || !department_id) {
@@ -115,13 +150,15 @@ router.post("/sub-departments", authenticateToken, async (ctx) => {
} }
// Check authorization // Check authorization
if (user.role === 'Supervisor' && user.departmentId !== department_id) { if (user.role === "Supervisor" && user.departmentId !== department_id) {
ctx.response.status = 403; ctx.response.status = 403;
ctx.response.body = { error: "You can only create sub-departments for your own department" }; ctx.response.body = {
error: "You can only create sub-departments for your own department",
};
return; return;
} }
if (user.role !== 'SuperAdmin' && user.role !== 'Supervisor') { if (user.role !== "SuperAdmin" && user.role !== "Supervisor") {
ctx.response.status = 403; ctx.response.status = 403;
ctx.response.body = { error: "Unauthorized" }; ctx.response.body = { error: "Unauthorized" };
return; return;
@@ -131,12 +168,12 @@ router.post("/sub-departments", authenticateToken, async (ctx) => {
const result = await db.execute( const result = await db.execute(
"INSERT INTO sub_departments (department_id, name) VALUES (?, ?)", "INSERT INTO sub_departments (department_id, name) VALUES (?, ?)",
[department_id, sanitizedName] [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.lastInsertId] [result.insertId],
); );
ctx.response.status = 201; ctx.response.status = 201;
@@ -145,7 +182,9 @@ router.post("/sub-departments", authenticateToken, async (ctx) => {
const err = error as { code?: string }; const err = error as { code?: string };
if (err.code === "ER_DUP_ENTRY") { if (err.code === "ER_DUP_ENTRY") {
ctx.response.status = 400; ctx.response.status = 400;
ctx.response.body = { error: "Sub-department already exists in this department" }; ctx.response.body = {
error: "Sub-department already exists in this department",
};
return; return;
} }
console.error("Create sub-department error:", error); console.error("Create sub-department error:", error);
@@ -155,7 +194,10 @@ router.post("/sub-departments", authenticateToken, async (ctx) => {
}); });
// Delete sub-department (SuperAdmin or Supervisor for their own department) // Delete sub-department (SuperAdmin or Supervisor for their own department)
router.delete("/sub-departments/:id", authenticateToken, async (ctx) => { router.delete(
"/sub-departments/:id",
authenticateToken,
async (ctx: RouterContext<"/sub-departments/:id">) => {
try { try {
const user = getCurrentUser(ctx); const user = getCurrentUser(ctx);
const subDeptId = ctx.params.id; const subDeptId = ctx.params.id;
@@ -163,7 +205,7 @@ router.delete("/sub-departments/:id", authenticateToken, async (ctx) => {
// Get the sub-department to check department ownership // Get the sub-department to check department ownership
const subDepts = await db.query<SubDepartment[]>( const subDepts = await db.query<SubDepartment[]>(
"SELECT * FROM sub_departments WHERE id = ?", "SELECT * FROM sub_departments WHERE id = ?",
[subDeptId] [subDeptId],
); );
if (subDepts.length === 0) { if (subDepts.length === 0) {
@@ -175,20 +217,27 @@ router.delete("/sub-departments/:id", authenticateToken, async (ctx) => {
const subDept = subDepts[0]; const subDept = subDepts[0];
// Check authorization // Check authorization
if (user.role === 'Supervisor' && user.departmentId !== subDept.department_id) { if (
user.role === "Supervisor" &&
user.departmentId !== subDept.department_id
) {
ctx.response.status = 403; ctx.response.status = 403;
ctx.response.body = { error: "You can only delete sub-departments from your own department" }; ctx.response.body = {
error: "You can only delete sub-departments from your own department",
};
return; return;
} }
if (user.role !== 'SuperAdmin' && user.role !== 'Supervisor') { if (user.role !== "SuperAdmin" && user.role !== "Supervisor") {
ctx.response.status = 403; ctx.response.status = 403;
ctx.response.body = { error: "Unauthorized" }; ctx.response.body = { error: "Unauthorized" };
return; return;
} }
// Delete associated activities first (cascade should handle this, but being explicit) // Delete associated activities first (cascade should handle this, but being explicit)
await db.execute("DELETE FROM activities WHERE sub_department_id = ?", [subDeptId]); await db.execute("DELETE FROM activities WHERE sub_department_id = ?", [
subDeptId,
]);
// Delete the sub-department // Delete the sub-department
await db.execute("DELETE FROM sub_departments WHERE id = ?", [subDeptId]); await db.execute("DELETE FROM sub_departments WHERE id = ?", [subDeptId]);
@@ -199,12 +248,17 @@ router.delete("/sub-departments/:id", authenticateToken, async (ctx) => {
ctx.response.status = 500; ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" }; ctx.response.body = { error: "Internal server error" };
} }
}); },
);
// Legacy route for creating sub-department under specific department (SuperAdmin only) // Legacy route for creating sub-department under specific department (SuperAdmin only)
router.post("/:id/sub-departments", authenticateToken, authorize("SuperAdmin"), async (ctx) => { router.post(
"/:id/sub-departments",
authenticateToken,
authorize("SuperAdmin"),
async (ctx: RouterContext<"/:id/sub-departments">) => {
try { try {
const deptId = ctx.params.id; const deptId: string | number = ctx.params.id;
const body = await ctx.request.body.json() as { name: string }; const body = await ctx.request.body.json() as { name: string };
const { name } = body; const { name } = body;
@@ -218,12 +272,12 @@ router.post("/:id/sub-departments", authenticateToken, authorize("SuperAdmin"),
const result = await db.execute( const result = await db.execute(
"INSERT INTO sub_departments (department_id, name) VALUES (?, ?)", "INSERT INTO sub_departments (department_id, name) VALUES (?, ?)",
[deptId, sanitizedName] [deptId, 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.lastInsertId] [result.insertId],
); );
ctx.response.status = 201; ctx.response.status = 201;
@@ -232,13 +286,16 @@ router.post("/:id/sub-departments", authenticateToken, authorize("SuperAdmin"),
const err = error as { code?: string }; const err = error as { code?: string };
if (err.code === "ER_DUP_ENTRY") { if (err.code === "ER_DUP_ENTRY") {
ctx.response.status = 400; ctx.response.status = 400;
ctx.response.body = { error: "Sub-department already exists in this department" }; ctx.response.body = {
error: "Sub-department already exists in this department",
};
return; 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" };
} }
}); },
);
export default router; export default router;

View File

@@ -1,18 +1,28 @@
import { Router } from "@oak/oak"; import { Router, type RouterContext, type State } from "@oak/oak";
import { db } from "../config/database.ts"; import { db } from "../config/database.ts";
import { authenticateToken, authorize, getCurrentUser } from "../middleware/auth.ts"; import {
import type { EmployeeSwap, CreateSwapRequest, User } from "../types/index.ts"; authenticateToken,
authorize,
getCurrentUser,
} from "../middleware/auth.ts";
import type { CreateSwapRequest, EmployeeSwap, User } from "../types/index.ts";
const router = new Router(); const router = new Router();
// Get all employee swaps (SuperAdmin only) // Get all employee swaps (SuperAdmin only)
router.get("/", authenticateToken, authorize("SuperAdmin"), async (ctx) => { router.get(
"/",
authenticateToken,
authorize("SuperAdmin"),
async (
ctx: RouterContext<"/", Record<string | number, string | undefined>, State>,
) => {
try { try {
const params = ctx.request.url.searchParams; const params: URLSearchParams = ctx.request.url.searchParams;
const status = params.get("status"); const status: string | null = params.get("status");
const employeeId = params.get("employeeId"); const employeeId: string | null = params.get("employeeId");
const startDate = params.get("startDate"); const startDate: string | null = params.get("startDate");
const endDate = params.get("endDate"); const endDate: string | null = params.get("endDate");
let query = ` let query = `
SELECT es.*, SELECT es.*,
@@ -62,10 +72,21 @@ router.get("/", authenticateToken, authorize("SuperAdmin"), async (ctx) => {
ctx.response.status = 500; ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" }; ctx.response.body = { error: "Internal server error" };
} }
}); },
);
// Get swap by ID // Get swap by ID
router.get("/:id", authenticateToken, authorize("SuperAdmin"), async (ctx) => { router.get(
"/:id",
authenticateToken,
authorize("SuperAdmin"),
async (
ctx: RouterContext<
"/:id",
{ id: string } & Record<string | number, string | undefined>,
State
>,
) => {
try { try {
const swapId = ctx.params.id; const swapId = ctx.params.id;
@@ -85,7 +106,7 @@ router.get("/:id", authenticateToken, authorize("SuperAdmin"), async (ctx) => {
LEFT JOIN users tc ON es.target_contractor_id = tc.id LEFT JOIN users tc ON es.target_contractor_id = tc.id
JOIN users sb ON es.swapped_by = sb.id JOIN users sb ON es.swapped_by = sb.id
WHERE es.id = ?`, WHERE es.id = ?`,
[swapId] [swapId],
); );
if (swaps.length === 0) { if (swaps.length === 0) {
@@ -100,7 +121,8 @@ router.get("/:id", authenticateToken, authorize("SuperAdmin"), async (ctx) => {
ctx.response.status = 500; ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" }; ctx.response.body = { error: "Internal server error" };
} }
}); },
);
// Create new employee swap (SuperAdmin only) // Create new employee swap (SuperAdmin only)
router.post("/", authenticateToken, authorize("SuperAdmin"), async (ctx) => { router.post("/", authenticateToken, authorize("SuperAdmin"), async (ctx) => {
@@ -114,13 +136,16 @@ router.post("/", authenticateToken, authorize("SuperAdmin"), async (ctx) => {
swapReason, swapReason,
reasonDetails, reasonDetails,
workCompletionPercentage, workCompletionPercentage,
swapDate swapDate,
} = body; } = body;
// Validate required fields // Validate required fields
if (!employeeId || !targetDepartmentId || !swapReason || !swapDate) { if (!employeeId || !targetDepartmentId || !swapReason || !swapDate) {
ctx.response.status = 400; ctx.response.status = 400;
ctx.response.body = { error: "Employee ID, target department, swap reason, and swap date are required" }; ctx.response.body = {
error:
"Employee ID, target department, swap reason, and swap date are required",
};
return; return;
} }
@@ -135,7 +160,7 @@ router.post("/", authenticateToken, authorize("SuperAdmin"), async (ctx) => {
// Get employee's current department and contractor // Get employee's current department and contractor
const employees = await db.query<User[]>( const employees = await db.query<User[]>(
"SELECT * FROM users WHERE id = ? AND role = 'Employee'", "SELECT * FROM users WHERE id = ? AND role = 'Employee'",
[employeeId] [employeeId],
); );
if (employees.length === 0) { if (employees.length === 0) {
@@ -155,17 +180,22 @@ router.post("/", authenticateToken, authorize("SuperAdmin"), async (ctx) => {
// Check if there's already an active swap for this employee // Check if there's already an active swap for this employee
const activeSwaps = await db.query<EmployeeSwap[]>( const activeSwaps = await db.query<EmployeeSwap[]>(
"SELECT * FROM employee_swaps WHERE employee_id = ? AND status = 'Active'", "SELECT * FROM employee_swaps WHERE employee_id = ? AND status = 'Active'",
[employeeId] [employeeId],
); );
if (activeSwaps.length > 0) { if (activeSwaps.length > 0) {
ctx.response.status = 400; ctx.response.status = 400;
ctx.response.body = { error: "Employee already has an active swap. Complete or cancel it first." }; ctx.response.body = {
error:
"Employee already has an active swap. Complete or cancel it first.",
};
return; return;
} }
// Use transaction to ensure both operations succeed or fail together
const newSwap = await db.transaction(async (connection) => {
// Create the swap record // Create the swap record
const result = await db.execute( const [insertResult] = await connection.execute(
`INSERT INTO employee_swaps `INSERT INTO employee_swaps
(employee_id, original_department_id, target_department_id, original_contractor_id, target_contractor_id, (employee_id, original_department_id, target_department_id, original_contractor_id, target_contractor_id,
swap_reason, reason_details, work_completion_percentage, swap_date, swapped_by, status) swap_reason, reason_details, work_completion_percentage, swap_date, swapped_by, status)
@@ -180,18 +210,31 @@ router.post("/", authenticateToken, authorize("SuperAdmin"), async (ctx) => {
reasonDetails || null, reasonDetails || null,
workCompletionPercentage || 0, workCompletionPercentage || 0,
swapDate, swapDate,
currentUser.id currentUser.id,
] ],
); );
const swapInsertId = (insertResult as { insertId: number }).insertId;
if (!swapInsertId) {
throw new Error("Failed to create swap record");
}
// Update the employee's department and contractor // Update the employee's department and contractor
await db.execute( const [updateResult] = await connection.execute(
"UPDATE users SET department_id = ?, contractor_id = ? WHERE id = ?", "UPDATE users SET department_id = ?, contractor_id = ? WHERE id = ?",
[targetDepartmentId, targetContractorId || null, employeeId] [targetDepartmentId, targetContractorId || null, employeeId],
); );
const affectedRows =
(updateResult as { affectedRows: number }).affectedRows;
if (affectedRows === 0) {
throw new Error("Failed to update employee department");
}
// Fetch the created swap // Fetch the created swap
const newSwap = await db.query<EmployeeSwap[]>( const [swapRows] = await connection.query(
`SELECT es.*, `SELECT es.*,
e.name as employee_name, e.name as employee_name,
od.name as original_department_name, od.name as original_department_name,
@@ -207,11 +250,14 @@ router.post("/", authenticateToken, authorize("SuperAdmin"), async (ctx) => {
LEFT JOIN users tc ON es.target_contractor_id = tc.id LEFT JOIN users tc ON es.target_contractor_id = tc.id
JOIN users sb ON es.swapped_by = sb.id JOIN users sb ON es.swapped_by = sb.id
WHERE es.id = ?`, WHERE es.id = ?`,
[result.insertId] [swapInsertId],
); );
return (swapRows as EmployeeSwap[])[0];
});
ctx.response.status = 201; ctx.response.status = 201;
ctx.response.body = newSwap[0]; ctx.response.body = newSwap;
} catch (error) { } catch (error) {
console.error("Create swap error:", error); console.error("Create swap error:", error);
ctx.response.status = 500; ctx.response.status = 500;
@@ -220,14 +266,18 @@ router.post("/", authenticateToken, authorize("SuperAdmin"), async (ctx) => {
}); });
// Complete a swap (return employee to original department) // Complete a swap (return employee to original department)
router.put("/:id/complete", authenticateToken, authorize("SuperAdmin"), async (ctx) => { router.put(
"/:id/complete",
authenticateToken,
authorize("SuperAdmin"),
async (ctx) => {
try { try {
const swapId = ctx.params.id; const swapId = ctx.params.id;
// Get the swap record // Get the swap record
const swaps = await db.query<EmployeeSwap[]>( const swaps = await db.query<EmployeeSwap[]>(
"SELECT * FROM employee_swaps WHERE id = ? AND status = 'Active'", "SELECT * FROM employee_swaps WHERE id = ? AND status = 'Active'",
[swapId] [swapId],
); );
if (swaps.length === 0) { if (swaps.length === 0) {
@@ -238,20 +288,26 @@ router.put("/:id/complete", authenticateToken, authorize("SuperAdmin"), async (c
const swap = swaps[0]; const swap = swaps[0];
// Use transaction to ensure both operations succeed or fail together
const updatedSwap = await db.transaction(async (connection) => {
// Return employee to original department and contractor // Return employee to original department and contractor
await db.execute( await connection.execute(
"UPDATE users SET department_id = ?, contractor_id = ? WHERE id = ?", "UPDATE users SET department_id = ?, contractor_id = ? WHERE id = ?",
[swap.original_department_id, swap.original_contractor_id, swap.employee_id] [
swap.original_department_id,
swap.original_contractor_id,
swap.employee_id,
],
); );
// Mark swap as completed // Mark swap as completed
await db.execute( await connection.execute(
"UPDATE employee_swaps SET status = 'Completed', completed_at = NOW() WHERE id = ?", "UPDATE employee_swaps SET status = 'Completed', completed_at = NOW() WHERE id = ?",
[swapId] [swapId],
); );
// Fetch updated swap // Fetch updated swap
const updatedSwap = await db.query<EmployeeSwap[]>( const [swapRows] = await connection.query(
`SELECT es.*, `SELECT es.*,
e.name as employee_name, e.name as employee_name,
od.name as original_department_name, od.name as original_department_name,
@@ -267,26 +323,34 @@ router.put("/:id/complete", authenticateToken, authorize("SuperAdmin"), async (c
LEFT JOIN users tc ON es.target_contractor_id = tc.id LEFT JOIN users tc ON es.target_contractor_id = tc.id
JOIN users sb ON es.swapped_by = sb.id JOIN users sb ON es.swapped_by = sb.id
WHERE es.id = ?`, WHERE es.id = ?`,
[swapId] [swapId],
); );
ctx.response.body = updatedSwap[0]; return (swapRows as EmployeeSwap[])[0];
});
ctx.response.body = updatedSwap;
} catch (error) { } catch (error) {
console.error("Complete swap error:", error); console.error("Complete swap error:", error);
ctx.response.status = 500; ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" }; ctx.response.body = { error: "Internal server error" };
} }
}); },
);
// Cancel a swap (return employee to original department) // Cancel a swap (return employee to original department)
router.put("/:id/cancel", authenticateToken, authorize("SuperAdmin"), async (ctx) => { router.put(
"/:id/cancel",
authenticateToken,
authorize("SuperAdmin"),
async (ctx) => {
try { try {
const swapId = ctx.params.id; const swapId = ctx.params.id;
// Get the swap record // Get the swap record
const swaps = await db.query<EmployeeSwap[]>( const swaps = await db.query<EmployeeSwap[]>(
"SELECT * FROM employee_swaps WHERE id = ? AND status = 'Active'", "SELECT * FROM employee_swaps WHERE id = ? AND status = 'Active'",
[swapId] [swapId],
); );
if (swaps.length === 0) { if (swaps.length === 0) {
@@ -297,20 +361,26 @@ router.put("/:id/cancel", authenticateToken, authorize("SuperAdmin"), async (ctx
const swap = swaps[0]; const swap = swaps[0];
// Use transaction to ensure both operations succeed or fail together
const updatedSwap = await db.transaction(async (connection) => {
// Return employee to original department and contractor // Return employee to original department and contractor
await db.execute( await connection.execute(
"UPDATE users SET department_id = ?, contractor_id = ? WHERE id = ?", "UPDATE users SET department_id = ?, contractor_id = ? WHERE id = ?",
[swap.original_department_id, swap.original_contractor_id, swap.employee_id] [
swap.original_department_id,
swap.original_contractor_id,
swap.employee_id,
],
); );
// Mark swap as cancelled // Mark swap as cancelled
await db.execute( await connection.execute(
"UPDATE employee_swaps SET status = 'Cancelled', completed_at = NOW() WHERE id = ?", "UPDATE employee_swaps SET status = 'Cancelled', completed_at = NOW() WHERE id = ?",
[swapId] [swapId],
); );
// Fetch updated swap // Fetch updated swap
const updatedSwap = await db.query<EmployeeSwap[]>( const [swapRows] = await connection.query(
`SELECT es.*, `SELECT es.*,
e.name as employee_name, e.name as employee_name,
od.name as original_department_name, od.name as original_department_name,
@@ -326,15 +396,19 @@ router.put("/:id/cancel", authenticateToken, authorize("SuperAdmin"), async (ctx
LEFT JOIN users tc ON es.target_contractor_id = tc.id LEFT JOIN users tc ON es.target_contractor_id = tc.id
JOIN users sb ON es.swapped_by = sb.id JOIN users sb ON es.swapped_by = sb.id
WHERE es.id = ?`, WHERE es.id = ?`,
[swapId] [swapId],
); );
ctx.response.body = updatedSwap[0]; return (swapRows as EmployeeSwap[])[0];
});
ctx.response.body = updatedSwap;
} catch (error) { } catch (error) {
console.error("Cancel swap error:", error); console.error("Cancel swap error:", error);
ctx.response.status = 500; ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" }; ctx.response.body = { error: "Internal server error" };
} }
}); },
);
export default router; export default router;

View File

@@ -1,20 +1,28 @@
import { Router } from "@oak/oak"; import { Router } from "@oak/oak";
import { db } from "../config/database.ts"; import { db } from "../config/database.ts";
import { authenticateToken, authorize, getCurrentUser } from "../middleware/auth.ts"; import {
import type { WorkAllocation } from "../types/index.ts"; authenticateToken,
authorize,
getCurrentUser,
} from "../middleware/auth.ts";
import type { JWTPayload, WorkAllocation } from "../types/index.ts";
const router = new Router(); const router = new Router();
// Get completed work allocations for reporting (with optional filters) // Get completed work allocations for reporting (with optional filters)
router.get("/completed-allocations", authenticateToken, authorize("Supervisor", "SuperAdmin"), async (ctx) => { router.get(
"/completed-allocations",
authenticateToken,
authorize("Supervisor", "SuperAdmin"),
async (ctx) => {
try { try {
const currentUser = getCurrentUser(ctx); const currentUser: JWTPayload = getCurrentUser(ctx);
const params = ctx.request.url.searchParams; const params: URLSearchParams = ctx.request.url.searchParams;
const startDate = params.get("startDate"); const startDate: string | null = params.get("startDate");
const endDate = params.get("endDate"); const endDate: string | null = params.get("endDate");
const departmentId = params.get("departmentId"); const departmentId: string | null = params.get("departmentId");
const contractorId = params.get("contractorId"); const contractorId: string | null = params.get("contractorId");
const employeeId = params.get("employeeId"); const employeeId: string | null = params.get("employeeId");
let query = ` let query = `
SELECT wa.*, SELECT wa.*,
@@ -76,8 +84,17 @@ router.get("/completed-allocations", authenticateToken, authorize("Supervisor",
// Calculate summary stats // Calculate summary stats
const totalAllocations = allocations.length; const totalAllocations = allocations.length;
const totalAmount = allocations.reduce((sum, a) => sum + (parseFloat(String(a.total_amount)) || parseFloat(String(a.rate)) || 0), 0); const totalAmount = allocations.reduce(
const totalUnits = allocations.reduce((sum, a) => sum + (parseFloat(String(a.units)) || 0), 0); (sum, a) =>
sum +
(parseFloat(String(a.total_amount)) || parseFloat(String(a.rate)) ||
0),
0,
);
const totalUnits = allocations.reduce(
(sum, a) => sum + (parseFloat(String(a.units)) || 0),
0,
);
ctx.response.body = { ctx.response.body = {
allocations, allocations,
@@ -85,22 +102,27 @@ router.get("/completed-allocations", authenticateToken, authorize("Supervisor",
totalAllocations, totalAllocations,
totalAmount: totalAmount.toFixed(2), totalAmount: totalAmount.toFixed(2),
totalUnits: totalUnits.toFixed(2), totalUnits: totalUnits.toFixed(2),
} },
}; };
} catch (error) { } catch (error) {
console.error("Get completed allocations report error:", error); console.error("Get completed allocations report error:", error);
ctx.response.status = 500; ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" }; ctx.response.body = { error: "Internal server error" };
} }
}); },
);
// Get summary statistics for completed work // Get summary statistics for completed work
router.get("/summary", authenticateToken, authorize("Supervisor", "SuperAdmin"), async (ctx) => { router.get(
"/summary",
authenticateToken,
authorize("Supervisor", "SuperAdmin"),
async (ctx) => {
try { try {
const currentUser = getCurrentUser(ctx); const currentUser: JWTPayload = getCurrentUser(ctx);
const params = ctx.request.url.searchParams; const params: URLSearchParams = ctx.request.url.searchParams;
const startDate = params.get("startDate"); const startDate: string | null = params.get("startDate");
const endDate = params.get("endDate"); const endDate: string | null = params.get("endDate");
let departmentFilter = ""; let departmentFilter = "";
const queryParams: unknown[] = []; const queryParams: unknown[] = [];
@@ -121,7 +143,8 @@ router.get("/summary", authenticateToken, authorize("Supervisor", "SuperAdmin"),
} }
// Get summary by contractor // Get summary by contractor
const byContractor = await db.query<any[]>(` const byContractor = await db.query<any[]>(
`
SELECT SELECT
c.id as contractor_id, c.id as contractor_id,
c.name as contractor_name, c.name as contractor_name,
@@ -134,10 +157,13 @@ router.get("/summary", authenticateToken, authorize("Supervisor", "SuperAdmin"),
WHERE wa.status = 'Completed' ${departmentFilter} ${dateFilter} WHERE wa.status = 'Completed' ${departmentFilter} ${dateFilter}
GROUP BY c.id, c.name GROUP BY c.id, c.name
ORDER BY total_amount DESC ORDER BY total_amount DESC
`, queryParams); `,
queryParams,
);
// Get summary by sub-department // Get summary by sub-department
const bySubDepartment = await db.query<any[]>(` const bySubDepartment = await db.query<any[]>(
`
SELECT SELECT
sd.id as sub_department_id, sd.id as sub_department_id,
sd.name as sub_department_name, sd.name as sub_department_name,
@@ -152,10 +178,13 @@ router.get("/summary", authenticateToken, authorize("Supervisor", "SuperAdmin"),
WHERE wa.status = 'Completed' ${departmentFilter} ${dateFilter} WHERE wa.status = 'Completed' ${departmentFilter} ${dateFilter}
GROUP BY sd.id, sd.name, d.name GROUP BY sd.id, sd.name, d.name
ORDER BY total_amount DESC ORDER BY total_amount DESC
`, queryParams); `,
queryParams,
);
// Get summary by activity type // Get summary by activity type
const byActivity = await db.query<any[]>(` const byActivity = await db.query<any[]>(
`
SELECT SELECT
COALESCE(wa.activity, 'Standard') as activity, COALESCE(wa.activity, 'Standard') as activity,
COUNT(*) as total_allocations, COUNT(*) as total_allocations,
@@ -166,7 +195,9 @@ router.get("/summary", authenticateToken, authorize("Supervisor", "SuperAdmin"),
WHERE wa.status = 'Completed' ${departmentFilter} ${dateFilter} WHERE wa.status = 'Completed' ${departmentFilter} ${dateFilter}
GROUP BY wa.activity GROUP BY wa.activity
ORDER BY total_amount DESC ORDER BY total_amount DESC
`, queryParams); `,
queryParams,
);
ctx.response.body = { ctx.response.body = {
byContractor, byContractor,
@@ -178,6 +209,7 @@ router.get("/summary", authenticateToken, authorize("Supervisor", "SuperAdmin"),
ctx.response.status = 500; ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" }; ctx.response.body = { error: "Internal server error" };
} }
}); },
);
export default router; export default router;

View File

@@ -1,7 +1,12 @@
import { Router } from "@oak/oak"; import { Router } from "@oak/oak";
import { db } from "../config/database.ts"; import { db } from "../config/database.ts";
import { authenticateToken, authorize, getCurrentUser } from "../middleware/auth.ts"; import {
authenticateToken,
authorize,
getCurrentUser,
} from "../middleware/auth.ts";
import { sanitizeInput } from "../middleware/security.ts"; import { sanitizeInput } from "../middleware/security.ts";
import type { Context } from "@oak/oak";
const router = new Router(); const router = new Router();
@@ -21,13 +26,15 @@ interface StandardRate {
} }
// Get all standard rates (default rates for comparison) // Get all standard rates (default rates for comparison)
router.get("/", authenticateToken, async (ctx) => { router.get("/", authenticateToken, async (ctx: Context) => {
try { try {
const currentUser = getCurrentUser(ctx); const currentUser = getCurrentUser(ctx);
const params = ctx.request.url.searchParams; const params = ctx.request.url.searchParams;
const departmentId = params.get("departmentId"); const departmentId: string | number | null = params.get("departmentId");
const subDepartmentId = params.get("subDepartmentId"); const subDepartmentId: string | number | null = params.get(
const activity = params.get("activity"); "subDepartmentId",
);
const activity: string | null = params.get("activity");
let query = ` let query = `
SELECT sr.*, SELECT sr.*,
@@ -78,12 +85,16 @@ router.get("/", authenticateToken, async (ctx) => {
}); });
// Get all rates (contractor + standard) for SuperAdmin - all departments, sorted by date // Get all rates (contractor + standard) for SuperAdmin - all departments, sorted by date
router.get("/all-rates", authenticateToken, authorize("SuperAdmin"), async (ctx) => { router.get(
"/all-rates",
authenticateToken,
authorize("SuperAdmin"),
async (ctx: Context) => {
try { try {
const params = ctx.request.url.searchParams; const params = ctx.request.url.searchParams;
const departmentId = params.get("departmentId"); const departmentId: string | number | null = params.get("departmentId");
const startDate = params.get("startDate"); const startDate: string | null = params.get("startDate");
const endDate = params.get("endDate"); const endDate: string | null = params.get("endDate");
// Get contractor rates // Get contractor rates
let contractorQuery = ` let contractorQuery = `
@@ -165,8 +176,14 @@ router.get("/all-rates", authenticateToken, authorize("SuperAdmin"), async (ctx)
standardParams.push(endDate); standardParams.push(endDate);
} }
const contractorRates = await db.query<any[]>(contractorQuery, contractorParams); const contractorRates = await db.query<any[]>(
const standardRates = await db.query<any[]>(standardQuery, standardParams); contractorQuery,
contractorParams,
);
const standardRates = await db.query<any[]>(
standardQuery,
standardParams,
);
// Combine and sort by date // Combine and sort by date
const allRates = [...contractorRates, ...standardRates].sort((a, b) => { const allRates = [...contractorRates, ...standardRates].sort((a, b) => {
@@ -181,17 +198,22 @@ router.get("/all-rates", authenticateToken, authorize("SuperAdmin"), async (ctx)
totalContractorRates: contractorRates.length, totalContractorRates: contractorRates.length,
totalStandardRates: standardRates.length, totalStandardRates: standardRates.length,
totalRates: allRates.length, totalRates: allRates.length,
} },
}; };
} catch (error) { } catch (error) {
console.error("Get all rates error:", error); console.error("Get all rates error:", error);
ctx.response.status = 500; ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" }; ctx.response.body = { error: "Internal server error" };
} }
}); },
);
// Compare contractor rates with standard rates // Compare contractor rates with standard rates
router.get("/compare", authenticateToken, authorize("Supervisor", "SuperAdmin"), async (ctx) => { router.get(
"/compare",
authenticateToken,
authorize("Supervisor", "SuperAdmin"),
async (ctx) => {
try { try {
const currentUser = getCurrentUser(ctx); const currentUser = getCurrentUser(ctx);
const params = ctx.request.url.searchParams; const params = ctx.request.url.searchParams;
@@ -227,7 +249,10 @@ router.get("/compare", authenticateToken, authorize("Supervisor", "SuperAdmin"),
standardQuery += " ORDER BY sr.effective_date DESC"; standardQuery += " ORDER BY sr.effective_date DESC";
const standardRates = await db.query<StandardRate[]>(standardQuery, queryParams); const standardRates = await db.query<StandardRate[]>(
standardQuery,
queryParams,
);
// Get contractor rates for comparison // Get contractor rates for comparison
let contractorQuery = ` let contractorQuery = `
@@ -263,12 +288,15 @@ router.get("/compare", authenticateToken, authorize("Supervisor", "SuperAdmin"),
contractorQuery += " ORDER BY cr.effective_date DESC"; contractorQuery += " ORDER BY cr.effective_date DESC";
const contractorRates = await db.query<any[]>(contractorQuery, contractorParams); const contractorRates = await db.query<any[]>(
contractorQuery,
contractorParams,
);
// Build comparison data // Build comparison data
const comparisons = contractorRates.map(cr => { const comparisons = contractorRates.map((cr) => {
// Find matching standard rate // Find matching standard rate
const matchingStandard = standardRates.find(sr => const matchingStandard = standardRates.find((sr) =>
sr.sub_department_id === cr.sub_department_id && sr.sub_department_id === cr.sub_department_id &&
sr.activity === cr.activity sr.activity === cr.activity
); );
@@ -276,7 +304,9 @@ router.get("/compare", authenticateToken, authorize("Supervisor", "SuperAdmin"),
const standardRate = matchingStandard?.rate || 0; const standardRate = matchingStandard?.rate || 0;
const contractorRate = cr.rate || 0; const contractorRate = cr.rate || 0;
const difference = contractorRate - standardRate; const difference = contractorRate - standardRate;
const percentageDiff = standardRate > 0 ? ((difference / standardRate) * 100).toFixed(2) : null; const percentageDiff = standardRate > 0
? ((difference / standardRate) * 100).toFixed(2)
: null;
return { return {
...cr, ...cr,
@@ -298,10 +328,15 @@ router.get("/compare", authenticateToken, authorize("Supervisor", "SuperAdmin"),
ctx.response.status = 500; ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" }; ctx.response.body = { error: "Internal server error" };
} }
}); },
);
// Create standard rate (Supervisor or SuperAdmin) // Create standard rate (Supervisor or SuperAdmin)
router.post("/", authenticateToken, authorize("Supervisor", "SuperAdmin"), async (ctx) => { router.post(
"/",
authenticateToken,
authorize("Supervisor", "SuperAdmin"),
async (ctx) => {
try { try {
const currentUser = getCurrentUser(ctx); const currentUser = getCurrentUser(ctx);
const body = await ctx.request.body.json() as { const body = await ctx.request.body.json() as {
@@ -314,7 +349,9 @@ router.post("/", authenticateToken, authorize("Supervisor", "SuperAdmin"), async
if (!rate || !effectiveDate) { if (!rate || !effectiveDate) {
ctx.response.status = 400; ctx.response.status = 400;
ctx.response.body = { error: "Missing required fields (rate, effectiveDate)" }; ctx.response.body = {
error: "Missing required fields (rate, effectiveDate)",
};
return; return;
} }
@@ -322,12 +359,14 @@ router.post("/", authenticateToken, authorize("Supervisor", "SuperAdmin"), async
if (subDepartmentId && currentUser.role === "Supervisor") { if (subDepartmentId && currentUser.role === "Supervisor") {
const subDepts = await db.query<any[]>( const subDepts = await db.query<any[]>(
"SELECT sd.* FROM sub_departments sd JOIN departments d ON sd.department_id = d.id WHERE sd.id = ? AND d.id = ?", "SELECT sd.* FROM sub_departments sd JOIN departments d ON sd.department_id = d.id WHERE sd.id = ? AND d.id = ?",
[subDepartmentId, currentUser.departmentId] [subDepartmentId, currentUser.departmentId],
); );
if (subDepts.length === 0) { if (subDepts.length === 0) {
ctx.response.status = 403; ctx.response.status = 403;
ctx.response.body = { error: "Sub-department not in your department" }; ctx.response.body = {
error: "Sub-department not in your department",
};
return; return;
} }
} }
@@ -336,7 +375,13 @@ router.post("/", authenticateToken, authorize("Supervisor", "SuperAdmin"), async
const result = await db.execute( const result = await db.execute(
"INSERT INTO standard_rates (sub_department_id, activity, rate, effective_date, created_by) VALUES (?, ?, ?, ?, ?)", "INSERT INTO standard_rates (sub_department_id, activity, rate, effective_date, created_by) VALUES (?, ?, ?, ?, ?)",
[subDepartmentId || null, sanitizedActivity, rate, effectiveDate, currentUser.id] [
subDepartmentId || null,
sanitizedActivity,
rate,
effectiveDate,
currentUser.id,
],
); );
const newRate = await db.query<StandardRate[]>( const newRate = await db.query<StandardRate[]>(
@@ -351,7 +396,7 @@ router.post("/", authenticateToken, authorize("Supervisor", "SuperAdmin"), async
LEFT JOIN users u ON sr.created_by = u.id LEFT JOIN users u ON sr.created_by = u.id
LEFT JOIN activities a ON a.sub_department_id = sr.sub_department_id AND a.name = sr.activity LEFT JOIN activities a ON a.sub_department_id = sr.sub_department_id AND a.name = sr.activity
WHERE sr.id = ?`, WHERE sr.id = ?`,
[result.insertId] [result.insertId],
); );
ctx.response.status = 201; ctx.response.status = 201;
@@ -361,14 +406,23 @@ router.post("/", authenticateToken, authorize("Supervisor", "SuperAdmin"), async
ctx.response.status = 500; ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" }; ctx.response.body = { error: "Internal server error" };
} }
}); },
);
// Update standard rate // Update standard rate
router.put("/:id", authenticateToken, authorize("Supervisor", "SuperAdmin"), async (ctx) => { router.put(
"/:id",
authenticateToken,
authorize("Supervisor", "SuperAdmin"),
async (ctx) => {
try { try {
const currentUser = getCurrentUser(ctx); const currentUser = getCurrentUser(ctx);
const rateId = ctx.params.id; const rateId = ctx.params.id;
const body = await ctx.request.body.json() as { rate?: number; activity?: string; effectiveDate?: string }; const body = await ctx.request.body.json() as {
rate?: number;
activity?: string;
effectiveDate?: string;
};
const { rate, activity, effectiveDate } = body; const { rate, activity, effectiveDate } = body;
// Verify rate exists and user has access // Verify rate exists and user has access
@@ -390,9 +444,14 @@ router.put("/:id", authenticateToken, authorize("Supervisor", "SuperAdmin"), asy
} }
// Supervisors can only update rates in their department // Supervisors can only update rates in their department
if (currentUser.role === "Supervisor" && existing[0].department_id !== currentUser.departmentId) { if (
currentUser.role === "Supervisor" &&
existing[0].department_id !== currentUser.departmentId
) {
ctx.response.status = 403; ctx.response.status = 403;
ctx.response.body = { error: "Access denied - rate not in your department" }; ctx.response.body = {
error: "Access denied - rate not in your department",
};
return; return;
} }
@@ -422,7 +481,7 @@ router.put("/:id", authenticateToken, authorize("Supervisor", "SuperAdmin"), asy
await db.execute( await db.execute(
`UPDATE standard_rates SET ${updates.join(", ")} WHERE id = ?`, `UPDATE standard_rates SET ${updates.join(", ")} WHERE id = ?`,
updateParams updateParams,
); );
const updatedRate = await db.query<StandardRate[]>( const updatedRate = await db.query<StandardRate[]>(
@@ -437,7 +496,7 @@ router.put("/:id", authenticateToken, authorize("Supervisor", "SuperAdmin"), asy
LEFT JOIN users u ON sr.created_by = u.id LEFT JOIN users u ON sr.created_by = u.id
LEFT JOIN activities a ON a.sub_department_id = sr.sub_department_id AND a.name = sr.activity LEFT JOIN activities a ON a.sub_department_id = sr.sub_department_id AND a.name = sr.activity
WHERE sr.id = ?`, WHERE sr.id = ?`,
[rateId] [rateId],
); );
ctx.response.body = updatedRate[0]; ctx.response.body = updatedRate[0];
@@ -446,10 +505,15 @@ router.put("/:id", authenticateToken, authorize("Supervisor", "SuperAdmin"), asy
ctx.response.status = 500; ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" }; ctx.response.body = { error: "Internal server error" };
} }
}); },
);
// Delete standard rate // Delete standard rate
router.delete("/:id", authenticateToken, authorize("Supervisor", "SuperAdmin"), async (ctx) => { router.delete(
"/:id",
authenticateToken,
authorize("Supervisor", "SuperAdmin"),
async (ctx: Context) => {
try { try {
const currentUser = getCurrentUser(ctx); const currentUser = getCurrentUser(ctx);
const rateId = ctx.params.id; const rateId = ctx.params.id;
@@ -461,7 +525,7 @@ router.delete("/:id", authenticateToken, authorize("Supervisor", "SuperAdmin"),
LEFT JOIN sub_departments sd ON sr.sub_department_id = sd.id LEFT JOIN sub_departments sd ON sr.sub_department_id = sd.id
LEFT JOIN departments d ON sd.department_id = d.id LEFT JOIN departments d ON sd.department_id = d.id
WHERE sr.id = ?`, WHERE sr.id = ?`,
[rateId] [rateId],
); );
if (existing.length === 0) { if (existing.length === 0) {
@@ -471,9 +535,14 @@ router.delete("/:id", authenticateToken, authorize("Supervisor", "SuperAdmin"),
} }
// Supervisors can only delete rates in their department // Supervisors can only delete rates in their department
if (currentUser.role === "Supervisor" && existing[0].department_id !== currentUser.departmentId) { if (
currentUser.role === "Supervisor" &&
existing[0].department_id !== currentUser.departmentId
) {
ctx.response.status = 403; ctx.response.status = 403;
ctx.response.body = { error: "Access denied - rate not in your department" }; ctx.response.body = {
error: "Access denied - rate not in your department",
};
return; return;
} }
@@ -484,6 +553,7 @@ router.delete("/:id", authenticateToken, authorize("Supervisor", "SuperAdmin"),
ctx.response.status = 500; ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" }; ctx.response.body = { error: "Internal server error" };
} }
}); },
);
export default router; export default router;

View File

@@ -1,5 +1,5 @@
import { Router } from "@oak/oak"; import { type Context, Router } from "@oak/oak";
import { hash, genSalt } from "bcrypt"; import { genSalt, hash } from "bcrypt";
import { db } from "../config/database.ts"; import { db } from "../config/database.ts";
import { config } from "../config/env.ts"; import { config } from "../config/env.ts";
@@ -8,14 +8,22 @@ async function hashPassword(password: string): Promise<string> {
const salt = await genSalt(config.BCRYPT_ROUNDS); const salt = await genSalt(config.BCRYPT_ROUNDS);
return await hash(password, salt); return await hash(password, salt);
} }
import { authenticateToken, authorize, getCurrentUser } from "../middleware/auth.ts"; import {
import { sanitizeInput, isValidEmail } from "../middleware/security.ts"; authenticateToken,
import type { User, CreateUserRequest, UpdateUserRequest } from "../types/index.ts"; authorize,
getCurrentUser,
} from "../middleware/auth.ts";
import { isValidEmail, sanitizeInput } from "../middleware/security.ts";
import type {
CreateUserRequest,
UpdateUserRequest,
User,
} from "../types/index.ts";
const router = new Router(); const router = new Router();
// Get all users (with filters) // Get all users (with filters)
router.get("/", authenticateToken, async (ctx) => { router.get("/", authenticateToken, async (ctx: Context) => {
try { try {
const currentUser = getCurrentUser(ctx); const currentUser = getCurrentUser(ctx);
const params = ctx.request.url.searchParams; const params = ctx.request.url.searchParams;
@@ -65,7 +73,7 @@ router.get("/", authenticateToken, async (ctx) => {
}); });
// Get user by ID // Get user by ID
router.get("/:id", authenticateToken, async (ctx) => { router.get("/:id", authenticateToken, async (ctx: Context) => {
try { try {
const currentUser = getCurrentUser(ctx); const currentUser = getCurrentUser(ctx);
const userId = ctx.params.id; const userId = ctx.params.id;
@@ -82,7 +90,7 @@ router.get("/:id", authenticateToken, async (ctx) => {
LEFT JOIN departments d ON u.department_id = d.id LEFT JOIN departments d ON u.department_id = d.id
LEFT JOIN users c ON u.contractor_id = c.id LEFT JOIN users c ON u.contractor_id = c.id
WHERE u.id = ?`, WHERE u.id = ?`,
[userId] [userId],
); );
if (users.length === 0) { if (users.length === 0) {
@@ -92,7 +100,10 @@ router.get("/:id", authenticateToken, async (ctx) => {
} }
// Supervisors can only view users in their department // Supervisors can only view users in their department
if (currentUser.role === "Supervisor" && users[0].department_id !== currentUser.departmentId) { if (
currentUser.role === "Supervisor" &&
users[0].department_id !== currentUser.departmentId
) {
ctx.response.status = 403; ctx.response.status = 403;
ctx.response.body = { error: "Access denied" }; ctx.response.body = { error: "Access denied" };
return; return;
@@ -107,14 +118,30 @@ router.get("/:id", authenticateToken, async (ctx) => {
}); });
// Create user // Create user
router.post("/", authenticateToken, authorize("SuperAdmin", "Supervisor"), async (ctx) => { router.post(
"/",
authenticateToken,
authorize("SuperAdmin", "Supervisor"),
async (ctx: Context) => {
try { try {
const currentUser = getCurrentUser(ctx); const currentUser = getCurrentUser(ctx);
const body = await ctx.request.body.json() as CreateUserRequest; const body = await ctx.request.body.json() as CreateUserRequest;
const { const {
username, name, email, password, role, departmentId, contractorId, username,
phoneNumber, aadharNumber, bankAccountNumber, bankName, bankIfsc, name,
contractorAgreementNumber, pfNumber, esicNumber email,
password,
role,
departmentId,
contractorId,
phoneNumber,
aadharNumber,
bankAccountNumber,
bankName,
bankIfsc,
contractorAgreementNumber,
pfNumber,
esicNumber,
} = body; } = body;
// Input validation // Input validation
@@ -140,12 +167,16 @@ router.post("/", authenticateToken, authorize("SuperAdmin", "Supervisor"), async
if (currentUser.role === "Supervisor") { if (currentUser.role === "Supervisor") {
if (departmentId !== currentUser.departmentId) { if (departmentId !== currentUser.departmentId) {
ctx.response.status = 403; ctx.response.status = 403;
ctx.response.body = { error: "Can only create users in your department" }; ctx.response.body = {
error: "Can only create users in your department",
};
return; return;
} }
if (role === "SuperAdmin" || role === "Supervisor") { if (role === "SuperAdmin" || role === "Supervisor") {
ctx.response.status = 403; ctx.response.status = 403;
ctx.response.body = { error: "Cannot create admin or supervisor users" }; ctx.response.body = {
error: "Cannot create admin or supervisor users",
};
return; return;
} }
} }
@@ -159,12 +190,22 @@ router.post("/", authenticateToken, authorize("SuperAdmin", "Supervisor"), async
contractor_agreement_number, pf_number, esic_number) contractor_agreement_number, pf_number, esic_number)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[ [
sanitizedUsername, sanitizedName, sanitizedEmail, hashedPassword, role, sanitizedUsername,
departmentId || null, contractorId || null, sanitizedName,
phoneNumber || null, aadharNumber || null, bankAccountNumber || null, sanitizedEmail,
bankName || null, bankIfsc || null, hashedPassword,
contractorAgreementNumber || null, pfNumber || null, esicNumber || null role,
] departmentId || null,
contractorId || null,
phoneNumber || null,
aadharNumber || null,
bankAccountNumber || null,
bankName || null,
bankIfsc || null,
contractorAgreementNumber || null,
pfNumber || null,
esicNumber || null,
],
); );
const newUser = await db.query<User[]>( const newUser = await db.query<User[]>(
@@ -179,7 +220,7 @@ router.post("/", authenticateToken, authorize("SuperAdmin", "Supervisor"), async
LEFT JOIN departments d ON u.department_id = d.id LEFT JOIN departments d ON u.department_id = d.id
LEFT JOIN users c ON u.contractor_id = c.id LEFT JOIN users c ON u.contractor_id = c.id
WHERE u.id = ?`, WHERE u.id = ?`,
[result.insertId] [result.insertId],
); );
ctx.response.status = 201; ctx.response.status = 201;
@@ -195,24 +236,40 @@ router.post("/", authenticateToken, authorize("SuperAdmin", "Supervisor"), async
ctx.response.status = 500; ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" }; ctx.response.body = { error: "Internal server error" };
} }
}); },
);
// Update user // Update user
router.put("/:id", authenticateToken, authorize("SuperAdmin", "Supervisor"), async (ctx) => { router.put(
"/:id",
authenticateToken,
authorize("SuperAdmin", "Supervisor"),
async (ctx: Context) => {
try { try {
const currentUser = getCurrentUser(ctx); const currentUser = getCurrentUser(ctx);
const userId = ctx.params.id; const userId = ctx.params.id;
const body = await ctx.request.body.json() as UpdateUserRequest; const body = await ctx.request.body.json() as UpdateUserRequest;
const { const {
name, email, role, departmentId, contractorId, isActive, name,
phoneNumber, aadharNumber, bankAccountNumber, bankName, bankIfsc, email,
contractorAgreementNumber, pfNumber, esicNumber role,
departmentId,
contractorId,
isActive,
phoneNumber,
aadharNumber,
bankAccountNumber,
bankName,
bankIfsc,
contractorAgreementNumber,
pfNumber,
esicNumber,
} = body; } = body;
// Check if user exists // Check if user exists
const existingUsers = await db.query<User[]>( const existingUsers = await db.query<User[]>(
"SELECT * FROM users WHERE id = ?", "SELECT * FROM users WHERE id = ?",
[userId] [userId],
); );
if (existingUsers.length === 0) { if (existingUsers.length === 0) {
@@ -225,12 +282,16 @@ router.put("/:id", authenticateToken, authorize("SuperAdmin", "Supervisor"), asy
if (currentUser.role === "Supervisor") { if (currentUser.role === "Supervisor") {
if (existingUsers[0].department_id !== currentUser.departmentId) { if (existingUsers[0].department_id !== currentUser.departmentId) {
ctx.response.status = 403; ctx.response.status = 403;
ctx.response.body = { error: "Can only update users in your department" }; ctx.response.body = {
error: "Can only update users in your department",
};
return; return;
} }
if (role === "SuperAdmin" || role === "Supervisor") { if (role === "SuperAdmin" || role === "Supervisor") {
ctx.response.status = 403; ctx.response.status = 403;
ctx.response.body = { error: "Cannot modify admin or supervisor roles" }; ctx.response.body = {
error: "Cannot modify admin or supervisor roles",
};
return; return;
} }
} }
@@ -311,7 +372,7 @@ router.put("/:id", authenticateToken, authorize("SuperAdmin", "Supervisor"), asy
await db.execute( await db.execute(
`UPDATE users SET ${updates.join(", ")} WHERE id = ?`, `UPDATE users SET ${updates.join(", ")} WHERE id = ?`,
params params,
); );
const updatedUser = await db.query<User[]>( const updatedUser = await db.query<User[]>(
@@ -326,7 +387,7 @@ router.put("/:id", authenticateToken, authorize("SuperAdmin", "Supervisor"), asy
LEFT JOIN departments d ON u.department_id = d.id LEFT JOIN departments d ON u.department_id = d.id
LEFT JOIN users c ON u.contractor_id = c.id LEFT JOIN users c ON u.contractor_id = c.id
WHERE u.id = ?`, WHERE u.id = ?`,
[userId] [userId],
); );
ctx.response.body = updatedUser[0]; ctx.response.body = updatedUser[0];
@@ -335,17 +396,22 @@ router.put("/:id", authenticateToken, authorize("SuperAdmin", "Supervisor"), asy
ctx.response.status = 500; ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" }; ctx.response.body = { error: "Internal server error" };
} }
}); },
);
// Delete user // Delete user
router.delete("/:id", authenticateToken, authorize("SuperAdmin", "Supervisor"), async (ctx) => { router.delete(
"/:id",
authenticateToken,
authorize("SuperAdmin", "Supervisor"),
async (ctx) => {
try { try {
const currentUser = getCurrentUser(ctx); const currentUser = getCurrentUser(ctx);
const userId = ctx.params.id; const userId = ctx.params.id;
const users = await db.query<User[]>( const users = await db.query<User[]>(
"SELECT * FROM users WHERE id = ?", "SELECT * FROM users WHERE id = ?",
[userId] [userId],
); );
if (users.length === 0) { if (users.length === 0) {
@@ -358,12 +424,16 @@ router.delete("/:id", authenticateToken, authorize("SuperAdmin", "Supervisor"),
if (currentUser.role === "Supervisor") { if (currentUser.role === "Supervisor") {
if (users[0].department_id !== currentUser.departmentId) { if (users[0].department_id !== currentUser.departmentId) {
ctx.response.status = 403; ctx.response.status = 403;
ctx.response.body = { error: "Can only delete users in your department" }; ctx.response.body = {
error: "Can only delete users in your department",
};
return; return;
} }
if (users[0].role === "SuperAdmin" || users[0].role === "Supervisor") { if (users[0].role === "SuperAdmin" || users[0].role === "Supervisor") {
ctx.response.status = 403; ctx.response.status = 403;
ctx.response.body = { error: "Cannot delete admin or supervisor users" }; ctx.response.body = {
error: "Cannot delete admin or supervisor users",
};
return; return;
} }
} }
@@ -375,6 +445,7 @@ router.delete("/:id", authenticateToken, authorize("SuperAdmin", "Supervisor"),
ctx.response.status = 500; ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" }; ctx.response.body = { error: "Internal server error" };
} }
}); },
);
export default router; export default router;

View File

@@ -1,21 +1,35 @@
import { Router } from "@oak/oak"; import { Router, type RouterContext, type State } from "@oak/oak";
import { db } from "../config/database.ts"; import { db } from "../config/database.ts";
import { authenticateToken, authorize, getCurrentUser } from "../middleware/auth.ts"; import {
authenticateToken,
authorize,
getCurrentUser,
} from "../middleware/auth.ts";
import { sanitizeInput } from "../middleware/security.ts"; import { sanitizeInput } from "../middleware/security.ts";
import type { WorkAllocation, CreateWorkAllocationRequest, ContractorRate } from "../types/index.ts"; import type {
ContractorRate,
CreateWorkAllocationRequest,
JWTPayload,
WorkAllocation,
} from "../types/index.ts";
const router = new Router(); const router = new Router();
// Get all work allocations // Get all work allocations
router.get("/", authenticateToken, async (ctx) => { router.get(
"/",
authenticateToken,
async (
ctx: RouterContext<"/", Record<string | number, string | undefined>, State>,
) => {
try { try {
const currentUser = getCurrentUser(ctx); const currentUser: JWTPayload = getCurrentUser(ctx);
const params = ctx.request.url.searchParams; const params: URLSearchParams = ctx.request.url.searchParams;
const employeeId = params.get("employeeId"); const employeeId: string | null = params.get("employeeId");
const status = params.get("status"); const status: string | null = params.get("status");
const departmentId = params.get("departmentId"); const departmentId: string | null = params.get("departmentId");
let query = ` let query: string = `
SELECT wa.*, SELECT wa.*,
e.name as employee_name, e.username as employee_username, e.name as employee_name, e.username as employee_username,
s.name as supervisor_name, s.name as supervisor_name,
@@ -68,14 +82,15 @@ router.get("/", authenticateToken, async (ctx) => {
ctx.response.status = 500; ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" }; ctx.response.body = { error: "Internal server error" };
} }
}); },
);
// Get work allocation by ID // Get work allocation by ID
router.get("/:id", authenticateToken, async (ctx) => { router.get("/:id", authenticateToken, async (ctx: RouterContext<"/:id">) => {
try { try {
const allocationId = ctx.params.id; const allocationId: string | undefined = ctx.params.id;
const allocations = await db.query<WorkAllocation[]>( const allocations: WorkAllocation[] = await db.query<WorkAllocation[]>(
`SELECT wa.*, `SELECT wa.*,
e.name as employee_name, e.username as employee_username, e.name as employee_name, e.username as employee_username,
s.name as supervisor_name, s.name as supervisor_name,
@@ -89,7 +104,7 @@ router.get("/:id", authenticateToken, async (ctx) => {
LEFT JOIN sub_departments sd ON wa.sub_department_id = sd.id LEFT JOIN sub_departments sd ON wa.sub_department_id = sd.id
LEFT JOIN departments d ON e.department_id = d.id LEFT JOIN departments d ON e.department_id = d.id
WHERE wa.id = ?`, WHERE wa.id = ?`,
[allocationId] [allocationId],
); );
if (allocations.length === 0) { if (allocations.length === 0) {
@@ -107,11 +122,28 @@ router.get("/:id", authenticateToken, async (ctx) => {
}); });
// Create work allocation (Supervisor or SuperAdmin) // Create work allocation (Supervisor or SuperAdmin)
router.post("/", authenticateToken, authorize("Supervisor", "SuperAdmin"), async (ctx) => { router.post(
"/",
authenticateToken,
authorize("Supervisor", "SuperAdmin"),
async (
ctx: RouterContext<"/", Record<string | number, string | undefined>, State>,
) => {
try { try {
const currentUser = getCurrentUser(ctx); const currentUser = getCurrentUser(ctx);
const body = await ctx.request.body.json() as CreateWorkAllocationRequest; const body = await ctx.request.body.json() as CreateWorkAllocationRequest;
const { employeeId, contractorId, subDepartmentId, activity, description, assignedDate, rate, units, totalAmount, departmentId } = body; const {
employeeId,
contractorId,
subDepartmentId,
activity,
description,
assignedDate,
rate,
units,
totalAmount,
departmentId,
} = body;
if (!employeeId || !contractorId || !assignedDate) { if (!employeeId || !contractorId || !assignedDate) {
ctx.response.status = 400; ctx.response.status = 400;
@@ -128,11 +160,16 @@ router.post("/", authenticateToken, authorize("Supervisor", "SuperAdmin"), async
employeeParams.push(currentUser.departmentId); employeeParams.push(currentUser.departmentId);
} }
const employees = await db.query<{ id: number }[]>(employeeQuery, employeeParams); const employees = await db.query<{ id: number }[]>(
employeeQuery,
employeeParams,
);
if (employees.length === 0) { if (employees.length === 0) {
ctx.response.status = 403; ctx.response.status = 403;
ctx.response.body = { error: "Employee not found or not in your department" }; ctx.response.body = {
error: "Employee not found or not in your department",
};
return; return;
} }
@@ -141,19 +178,32 @@ router.post("/", authenticateToken, authorize("Supervisor", "SuperAdmin"), async
if (!finalRate) { if (!finalRate) {
const rates = await db.query<ContractorRate[]>( const rates = await db.query<ContractorRate[]>(
"SELECT rate FROM contractor_rates WHERE contractor_id = ? ORDER BY effective_date DESC LIMIT 1", "SELECT rate FROM contractor_rates WHERE contractor_id = ? ORDER BY effective_date DESC LIMIT 1",
[contractorId] [contractorId],
); );
finalRate = rates.length > 0 ? rates[0].rate : null; finalRate = rates.length > 0 ? rates[0].rate : null;
} }
const sanitizedActivity = activity ? sanitizeInput(activity) : null; const sanitizedActivity = activity ? sanitizeInput(activity) : null;
const sanitizedDescription = description ? sanitizeInput(description) : null; const sanitizedDescription = description
? sanitizeInput(description)
: null;
const result = await db.execute( const result = await db.execute(
`INSERT INTO work_allocations `INSERT INTO work_allocations
(employee_id, supervisor_id, contractor_id, sub_department_id, activity, description, assigned_date, rate, units, total_amount) (employee_id, supervisor_id, contractor_id, sub_department_id, activity, description, assigned_date, rate, units, total_amount)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[employeeId, currentUser.id, contractorId, subDepartmentId || null, sanitizedActivity, sanitizedDescription, assignedDate, finalRate, units || null, totalAmount || null] [
employeeId,
currentUser.id,
contractorId,
subDepartmentId || null,
sanitizedActivity,
sanitizedDescription,
assignedDate,
finalRate,
units || null,
totalAmount || null,
],
); );
const newAllocation = await db.query<WorkAllocation[]>( const newAllocation = await db.query<WorkAllocation[]>(
@@ -170,7 +220,7 @@ router.post("/", authenticateToken, authorize("Supervisor", "SuperAdmin"), async
LEFT JOIN sub_departments sd ON wa.sub_department_id = sd.id LEFT JOIN sub_departments sd ON wa.sub_department_id = sd.id
LEFT JOIN departments d ON e.department_id = d.id LEFT JOIN departments d ON e.department_id = d.id
WHERE wa.id = ?`, WHERE wa.id = ?`,
[result.insertId] [result.insertId],
); );
ctx.response.status = 201; ctx.response.status = 201;
@@ -180,14 +230,22 @@ router.post("/", authenticateToken, authorize("Supervisor", "SuperAdmin"), async
ctx.response.status = 500; ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" }; ctx.response.body = { error: "Internal server error" };
} }
}); },
);
// Update work allocation status (Supervisor or SuperAdmin) // Update work allocation status (Supervisor or SuperAdmin)
router.put("/:id/status", authenticateToken, authorize("Supervisor", "SuperAdmin"), async (ctx) => { router.put(
"/:id/status",
authenticateToken,
authorize("Supervisor", "SuperAdmin"),
async (ctx) => {
try { try {
const currentUser = getCurrentUser(ctx); const currentUser = getCurrentUser(ctx);
const allocationId = ctx.params.id; const allocationId = ctx.params.id;
const body = await ctx.request.body.json() as { status: string; completionDate?: string }; const body = await ctx.request.body.json() as {
status: string;
completionDate?: string;
};
const { status, completionDate } = body; const { status, completionDate } = body;
if (!status) { if (!status) {
@@ -209,13 +267,15 @@ router.put("/:id/status", authenticateToken, authorize("Supervisor", "SuperAdmin
if (allocations.length === 0) { if (allocations.length === 0) {
ctx.response.status = 403; ctx.response.status = 403;
ctx.response.body = { error: "Work allocation not found or access denied" }; ctx.response.body = {
error: "Work allocation not found or access denied",
};
return; return;
} }
await db.execute( await db.execute(
"UPDATE work_allocations SET status = ?, completion_date = ? WHERE id = ?", "UPDATE work_allocations SET status = ?, completion_date = ? WHERE id = ?",
[status, completionDate || null, allocationId] [status, completionDate || null, allocationId],
); );
const updatedAllocation = await db.query<WorkAllocation[]>( const updatedAllocation = await db.query<WorkAllocation[]>(
@@ -232,7 +292,7 @@ router.put("/:id/status", authenticateToken, authorize("Supervisor", "SuperAdmin
LEFT JOIN sub_departments sd ON wa.sub_department_id = sd.id LEFT JOIN sub_departments sd ON wa.sub_department_id = sd.id
LEFT JOIN departments d ON e.department_id = d.id LEFT JOIN departments d ON e.department_id = d.id
WHERE wa.id = ?`, WHERE wa.id = ?`,
[allocationId] [allocationId],
); );
ctx.response.body = updatedAllocation[0]; ctx.response.body = updatedAllocation[0];
@@ -241,10 +301,15 @@ router.put("/:id/status", authenticateToken, authorize("Supervisor", "SuperAdmin
ctx.response.status = 500; ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" }; ctx.response.body = { error: "Internal server error" };
} }
}); },
);
// Delete work allocation (Supervisor or SuperAdmin) // Delete work allocation (Supervisor or SuperAdmin)
router.delete("/:id", authenticateToken, authorize("Supervisor", "SuperAdmin"), async (ctx) => { router.delete(
"/:id",
authenticateToken,
authorize("Supervisor", "SuperAdmin"),
async (ctx) => {
try { try {
const currentUser = getCurrentUser(ctx); const currentUser = getCurrentUser(ctx);
const allocationId = ctx.params.id; const allocationId = ctx.params.id;
@@ -262,17 +327,22 @@ router.delete("/:id", authenticateToken, authorize("Supervisor", "SuperAdmin"),
if (allocations.length === 0) { if (allocations.length === 0) {
ctx.response.status = 403; ctx.response.status = 403;
ctx.response.body = { error: "Work allocation not found or access denied" }; ctx.response.body = {
error: "Work allocation not found or access denied",
};
return; return;
} }
await db.execute("DELETE FROM work_allocations WHERE id = ?", [allocationId]); await db.execute("DELETE FROM work_allocations WHERE id = ?", [
allocationId,
]);
ctx.response.body = { message: "Work allocation deleted successfully" }; ctx.response.body = { message: "Work allocation deleted successfully" };
} catch (error) { } catch (error) {
console.error("Delete work allocation error:", error); console.error("Delete work allocation error:", error);
ctx.response.status = 500; ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" }; ctx.response.body = { error: "Internal server error" };
} }
}); },
);
export default router; export default router;

View File

@@ -1,4 +1,4 @@
import { hash, genSalt } from "bcrypt"; import { genSalt, hash } from "bcrypt";
import { db } from "../config/database.ts"; import { db } from "../config/database.ts";
import { config } from "../config/env.ts"; import { config } from "../config/env.ts";
@@ -17,7 +17,7 @@ async function seedDatabase() {
// 1. Seed Departments // 1. Seed Departments
console.log("📁 Seeding departments..."); console.log("📁 Seeding departments...");
const existingDepts = await db.query<{ count: number }[]>( const existingDepts = await db.query<{ count: number }[]>(
"SELECT COUNT(*) as count FROM departments" "SELECT COUNT(*) as count FROM departments",
); );
if (existingDepts[0].count === 0) { if (existingDepts[0].count === 0) {
@@ -38,15 +38,15 @@ async function seedDatabase() {
// Get department IDs // Get department IDs
const tudkiDeptResult = await db.query<{ id: number }[]>( const tudkiDeptResult = await db.query<{ id: number }[]>(
"SELECT id FROM departments WHERE name = ?", "SELECT id FROM departments WHERE name = ?",
["Tudki"] ["Tudki"],
); );
const danaDeptResult = await db.query<{ id: number }[]>( const danaDeptResult = await db.query<{ id: number }[]>(
"SELECT id FROM departments WHERE name = ?", "SELECT id FROM departments WHERE name = ?",
["Dana"] ["Dana"],
); );
const groundnutDeptResult = await db.query<{ id: number }[]>( const groundnutDeptResult = await db.query<{ id: number }[]>(
"SELECT id FROM departments WHERE name = ?", "SELECT id FROM departments WHERE name = ?",
["Groundnut"] ["Groundnut"],
); );
const tudkiId = tudkiDeptResult[0]?.id; const tudkiId = tudkiDeptResult[0]?.id;
@@ -54,38 +54,79 @@ async function seedDatabase() {
const groundnutId = groundnutDeptResult[0]?.id; const groundnutId = groundnutDeptResult[0]?.id;
// Define sub-departments and activities per department based on activities.md // Define sub-departments and activities per department based on activities.md
const departmentData: { [key: number]: { subDept: string; activities: { name: string; unit: string }[] }[] } = {}; const departmentData: {
[key: number]: {
subDept: string;
activities: { name: string; unit: string }[];
}[];
} = {};
if (groundnutId) { if (groundnutId) {
departmentData[groundnutId] = [ departmentData[groundnutId] = [
{ {
subDept: "Loading/Unloading", subDept: "Loading/Unloading",
activities: [ activities: [
{ name: "Mufali Aavak Katai (Groundnut Arrival Cutting)", unit: "Per Bag" }, {
{ name: "Mufali Aavak Dhaang (Groundnut Arrival Stacking)", unit: "Per Bag" }, name: "Mufali Aavak Katai (Groundnut Arrival Cutting)",
unit: "Per Bag",
},
{
name: "Mufali Aavak Dhaang (Groundnut Arrival Stacking)",
unit: "Per Bag",
},
{ name: "Dhaang Se Katai (Cutting from Stack)", unit: "Per Bag" }, { name: "Dhaang Se Katai (Cutting from Stack)", unit: "Per Bag" },
{ name: "Guthli Bori Silai Dhaang (Kernel Bag Stitching Stack)", unit: "Per Bag" }, {
{ name: "Guthali dhada Pala Tulai Silai Dhaang / Loading", unit: "Per Bag" }, name: "Guthli Bori Silai Dhaang (Kernel Bag Stitching Stack)",
unit: "Per Bag",
},
{
name: "Guthali dhada Pala Tulai Silai Dhaang / Loading",
unit: "Per Bag",
},
{ name: "Mufali Patthar Bori silai Dhaang", unit: "Per Bag" }, { name: "Mufali Patthar Bori silai Dhaang", unit: "Per Bag" },
{ name: "Mufali Patthar Bori Utrai (Groundnut Stone Bag Unloading)", unit: "Per Bag" }, {
{ name: "Bardana Bandal Loading (Gunny Bundle Loading)", unit: "Per Bag" }, name: "Mufali Patthar Bori Utrai (Groundnut Stone Bag Unloading)",
unit: "Per Bag",
},
{
name: "Bardana Bandal Loading (Gunny Bundle Loading)",
unit: "Per Bag",
},
{ name: "Bardana Gatthi Loading/Unloading", unit: "Per Bag" }, { name: "Bardana Gatthi Loading/Unloading", unit: "Per Bag" },
{ name: "Black Dana Loading/Unloading", unit: "Per Bag" }, { name: "Black Dana Loading/Unloading", unit: "Per Bag" },
{ name: "Dala - Chomu & Jaipur (Branch)", unit: "Per Bag" }, { name: "Dala - Chomu & Jaipur (Branch)", unit: "Per Bag" },
] ],
},
{
subDept: "Pre Cleaning",
activities: [{ name: "Pre Cleaner", unit: "Fixed Rate-Per Person" }],
},
{
subDept: "Destoner",
activities: [{ name: "Destoner", unit: "Fixed Rate-Per Person" }],
},
{
subDept: "Water",
activities: [{ name: "Water", unit: "Fixed Rate-Per Person" }],
}, },
{ subDept: "Pre Cleaning", activities: [{ name: "Pre Cleaner", unit: "Fixed Rate-Per Person" }] },
{ subDept: "Destoner", activities: [{ name: "Destoner", unit: "Fixed Rate-Per Person" }] },
{ subDept: "Water", activities: [{ name: "Water", unit: "Fixed Rate-Per Person" }] },
{ {
subDept: "Decordicater & Cleaning and Round Chalna", subDept: "Decordicater & Cleaning and Round Chalna",
activities: [ activities: [
{ name: "Decordicater", unit: "Fixed Rate-Per Person" }, { name: "Decordicater", unit: "Fixed Rate-Per Person" },
{ name: "Round Chalna (Round Sieving)", unit: "Fixed Rate-Per Person" }, {
{ name: "Cleaning", unit: "Fixed Rate-Per Person" }, name: "Round Chalna (Round Sieving)",
] unit: "Fixed Rate-Per Person",
},
{ name: "Cleaning", unit: "Fixed Rate-Per Person" },
],
},
{
subDept: "Round Chalna No.1",
activities: [{
name: "Round Chalna No.1 (Round Sieving No.1)",
unit: "Fixed Rate-Per Person",
}],
}, },
{ subDept: "Round Chalna No.1", activities: [{ name: "Round Chalna No.1 (Round Sieving No.1)", unit: "Fixed Rate-Per Person" }] },
]; ];
} }
@@ -94,16 +135,41 @@ async function seedDatabase() {
{ {
subDept: "Loading/Unloading", subDept: "Loading/Unloading",
activities: [ activities: [
{ name: "Tulai Silai Loading (Weighing Stitching Loading)", unit: "Per Bag" }, {
name: "Tulai Silai Loading (Weighing Stitching Loading)",
unit: "Per Bag",
},
{ name: "Dhaang se Loading (Loading from Stack)", unit: "Per Bag" }, { name: "Dhaang se Loading (Loading from Stack)", unit: "Per Bag" },
{ name: "Silai Dhaang (Stitching Stack)", unit: "Per Bag" }, { name: "Silai Dhaang (Stitching Stack)", unit: "Per Bag" },
{ name: "Tulai Silai Dhaang Ikai No. 2 Machine ke Pass", unit: "Per Bag" }, {
{ name: "Dana Unloading/Dhaang (Grain Unloading/Stack)", unit: "Per Bag" }, name: "Tulai Silai Dhaang Ikai No. 2 Machine ke Pass",
{ name: "Dana Aavak Keep Katai (Grain Arrival Hopper Cutting)", unit: "Per Bag" }, unit: "Per Bag",
{ name: "Kachri Dhada Pala Bharai Tulai Silai Load/Dhaang 70kg", unit: "Per Bag" }, },
{ name: "Kachri Dhaang se loading (Waste Loading from Stack)", unit: "Per Bag" }, {
{ name: "Keep Katai Khulla Katta (Hopper Cutting Open Bag)", unit: "Per Bag" }, name: "Dana Unloading/Dhaang (Grain Unloading/Stack)",
{ name: "Keep Katai Silai Kholkar (Hopper Cutting Opening Stitched)", unit: "Per Bag" }, unit: "Per Bag",
},
{
name: "Dana Aavak Keep Katai (Grain Arrival Hopper Cutting)",
unit: "Per Bag",
},
{
name: "Kachri Dhada Pala Bharai Tulai Silai Load/Dhaang 70kg",
unit: "Per Bag",
},
{
name: "Kachri Dhaang se loading (Waste Loading from Stack)",
unit: "Per Bag",
},
{
name: "Keep Katai Khulla Katta (Hopper Cutting Open Bag)",
unit: "Per Bag",
},
{
name:
"Keep Katai Silai Kholkar (Hopper Cutting Opening Stitched)",
unit: "Per Bag",
},
{ name: "Bardana Paltai (Gunny Turning)", unit: "Per Bag" }, { name: "Bardana Paltai (Gunny Turning)", unit: "Per Bag" },
{ name: "Ekai No. 2 me Keep Katai Khula Bag", unit: "Per Bag" }, { name: "Ekai No. 2 me Keep Katai Khula Bag", unit: "Per Bag" },
{ name: "Ekai No. 2 me Keep Katai Silai Kholkar", unit: "Per Bag" }, { name: "Ekai No. 2 me Keep Katai Silai Kholkar", unit: "Per Bag" },
@@ -111,15 +177,39 @@ async function seedDatabase() {
{ name: "Kachri Bharai Silai Dhaang Chatt Par", unit: "Per Bag" }, { name: "Kachri Bharai Silai Dhaang Chatt Par", unit: "Per Bag" },
{ name: "Bardana Unloading (Gunny Unloading)", unit: "Per Bag" }, { name: "Bardana Unloading (Gunny Unloading)", unit: "Per Bag" },
{ name: "Grading", unit: "Per Bag" }, { name: "Grading", unit: "Per Bag" },
] ],
},
{
subDept: "Destoner",
activities: [{ name: "Destoner", unit: "Fixed Rate-Per Person" }],
},
{
subDept: "Gravity",
activities: [{ name: "Gravity", unit: "Fixed Rate-Per Person" }],
},
{
subDept: "Tank",
activities: [{ name: "Tank", unit: "Fixed Rate-Per Person" }],
},
{
subDept: "Sortex",
activities: [{ name: "Sortex", unit: "Fixed Rate-Per Person" }],
},
{
subDept: "X-Ray",
activities: [{ name: "X-Ray", unit: "Fixed Rate-Per Person" }],
},
{
subDept: "Kachri",
activities: [{
name: "Kachri (Waste)",
unit: "Fixed Rate-Per Person",
}],
},
{
subDept: "Other Works",
activities: [{ name: "Other Works", unit: "Fixed Rate-Per Person" }],
}, },
{ subDept: "Destoner", activities: [{ name: "Destoner", unit: "Fixed Rate-Per Person" }] },
{ subDept: "Gravity", activities: [{ name: "Gravity", unit: "Fixed Rate-Per Person" }] },
{ subDept: "Tank", activities: [{ name: "Tank", unit: "Fixed Rate-Per Person" }] },
{ subDept: "Sortex", activities: [{ name: "Sortex", unit: "Fixed Rate-Per Person" }] },
{ subDept: "X-Ray", activities: [{ name: "X-Ray", unit: "Fixed Rate-Per Person" }] },
{ subDept: "Kachri", activities: [{ name: "Kachri (Waste)", unit: "Fixed Rate-Per Person" }] },
{ subDept: "Other Works", activities: [{ name: "Other Works", unit: "Fixed Rate-Per Person" }] },
]; ];
} }
@@ -128,32 +218,90 @@ async function seedDatabase() {
{ {
subDept: "Loading/Unloading", subDept: "Loading/Unloading",
activities: [ activities: [
{ name: "Dana Loading/Unloading (Grain Loading/Unloading)", unit: "Per Bag" }, {
name: "Dana Loading/Unloading (Grain Loading/Unloading)",
unit: "Per Bag",
},
{ name: "Loading/Unloading 40 Kg", unit: "Per Bag" }, { name: "Loading/Unloading 40 Kg", unit: "Per Bag" },
{ name: "Grading Chalne se Maal Bharai Tulai Silai Dhaang", unit: "Per Bag" }, {
{ name: "Grading Chalne se Maal Bharai Tulai Silai Loading", unit: "Per Bag" }, name: "Grading Chalne se Maal Bharai Tulai Silai Dhaang",
{ name: "Chilka Bharai silai Dhaang (Husk Filling Stitching Stack)", unit: "Per Bag" }, unit: "Per Bag",
{ name: "Keep katai Bahar Se (Hopper Cutting from Outside)", unit: "Per Bag" }, },
{ name: "Keep katai Andar Se (Hopper Cutting from Inside)", unit: "Per Bag" }, {
{ name: "Cartoon Banai Vacume Bharai Tulai Packing and Dhaang", unit: "Per Bag" }, name: "Grading Chalne se Maal Bharai Tulai Silai Loading",
{ name: "Cartoon Banai Vacume Bharai Tulai Packing and Loading", unit: "Per Bag" }, unit: "Per Bag",
},
{
name: "Chilka Bharai silai Dhaang (Husk Filling Stitching Stack)",
unit: "Per Bag",
},
{
name: "Keep katai Bahar Se (Hopper Cutting from Outside)",
unit: "Per Bag",
},
{
name: "Keep katai Andar Se (Hopper Cutting from Inside)",
unit: "Per Bag",
},
{
name: "Cartoon Banai Vacume Bharai Tulai Packing and Dhaang",
unit: "Per Bag",
},
{
name: "Cartoon Banai Vacume Bharai Tulai Packing and Loading",
unit: "Per Bag",
},
{ name: "Katta Paltai (Bag Turning)", unit: "Per Bag" }, { name: "Katta Paltai (Bag Turning)", unit: "Per Bag" },
{ name: "Dhada Pala Bharai Tulai Silai Dhaang", unit: "Per Bag" }, { name: "Dhada Pala Bharai Tulai Silai Dhaang", unit: "Per Bag" },
{ name: "Dhada Pala Bharai Tulai Silai Loading", unit: "Per Bag" }, { name: "Dhada Pala Bharai Tulai Silai Loading", unit: "Per Bag" },
{ name: "Sike Maal Ki Silai Dhaang Andar", unit: "Per Bag" }, { name: "Sike Maal Ki Silai Dhaang Andar", unit: "Per Bag" },
{ name: "Sike Maal Ki Silai Dhaang Bahar", unit: "Per Bag" }, { name: "Sike Maal Ki Silai Dhaang Bahar", unit: "Per Bag" },
{ name: "Nakku Silai Dhaang Bahar (Rejection Stitching Stack Outside)", unit: "Per Bag" }, {
] name:
"Nakku Silai Dhaang Bahar (Rejection Stitching Stack Outside)",
unit: "Per Bag",
},
],
},
{
subDept: "Tank",
activities: [{ name: "Tank", unit: "Fixed Rate-Per Person" }],
},
{
subDept: "Grader (Machine)",
activities: [{
name: "Grader (Machine)",
unit: "Fixed Rate-Per Person",
}],
},
{
subDept: "Sortex",
activities: [{ name: "Sortex", unit: "Fixed Rate-Per Person" }],
},
{
subDept: "X-Ray",
activities: [{ name: "X-Ray", unit: "Fixed Rate-Per Person" }],
},
{
subDept: "Rejection",
activities: [{ name: "Rejection", unit: "Fixed Rate-Per Person" }],
},
{
subDept: "Store",
activities: [{ name: "Store", unit: "Fixed Rate-Per Person" }],
},
{
subDept: "Roster",
activities: [{ name: "Roster", unit: "Fixed Rate-Per Person" }],
},
{
subDept: "Blancher",
activities: [{ name: "Blancher", unit: "Fixed Rate-Per Person" }],
},
{
subDept: "Other Works",
activities: [{ name: "Other Works", unit: "Fixed Rate-Per Person" }],
}, },
{ subDept: "Tank", activities: [{ name: "Tank", unit: "Fixed Rate-Per Person" }] },
{ subDept: "Grader (Machine)", activities: [{ name: "Grader (Machine)", unit: "Fixed Rate-Per Person" }] },
{ subDept: "Sortex", activities: [{ name: "Sortex", unit: "Fixed Rate-Per Person" }] },
{ subDept: "X-Ray", activities: [{ name: "X-Ray", unit: "Fixed Rate-Per Person" }] },
{ subDept: "Rejection", activities: [{ name: "Rejection", unit: "Fixed Rate-Per Person" }] },
{ subDept: "Store", activities: [{ name: "Store", unit: "Fixed Rate-Per Person" }] },
{ subDept: "Roster", activities: [{ name: "Roster", unit: "Fixed Rate-Per Person" }] },
{ subDept: "Blancher", activities: [{ name: "Blancher", unit: "Fixed Rate-Per Person" }] },
{ subDept: "Other Works", activities: [{ name: "Other Works", unit: "Fixed Rate-Per Person" }] },
]; ];
} }
@@ -178,7 +326,7 @@ async function seedDatabase() {
for (const [deptId, subDepts] of Object.entries(departmentData)) { for (const [deptId, subDepts] of Object.entries(departmentData)) {
const existingSubDepts = await db.query<{ count: number }[]>( const existingSubDepts = await db.query<{ count: number }[]>(
"SELECT COUNT(*) as count FROM sub_departments WHERE department_id = ?", "SELECT COUNT(*) as count FROM sub_departments WHERE department_id = ?",
[deptId] [deptId],
); );
if (existingSubDepts[0].count === 0) { if (existingSubDepts[0].count === 0) {
@@ -186,13 +334,13 @@ async function seedDatabase() {
// Insert sub-department // Insert sub-department
await db.execute( await db.execute(
"INSERT INTO sub_departments (department_id, name) VALUES (?, ?)", "INSERT INTO sub_departments (department_id, name) VALUES (?, ?)",
[deptId, subDept] [deptId, subDept],
); );
// Get the sub-department ID // Get the sub-department ID
const subDeptResult = await db.query<{ id: number }[]>( const subDeptResult = await db.query<{ id: number }[]>(
"SELECT id FROM sub_departments WHERE department_id = ? AND name = ?", "SELECT id FROM sub_departments WHERE department_id = ? AND name = ?",
[deptId, subDept] [deptId, subDept],
); );
if (subDeptResult.length > 0) { if (subDeptResult.length > 0) {
@@ -202,7 +350,7 @@ async function seedDatabase() {
try { try {
await db.execute( 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 (?, ?, ?)",
[subDeptId, activity.name, activity.unit] [subDeptId, activity.name, activity.unit],
); );
} catch (_e) { } catch (_e) {
// Activity might already exist // Activity might already exist
@@ -218,7 +366,7 @@ async function seedDatabase() {
console.log("👤 Seeding SuperAdmin user..."); console.log("👤 Seeding SuperAdmin user...");
const existingAdmin = await db.query<{ id: number }[]>( const existingAdmin = await db.query<{ id: number }[]>(
"SELECT id FROM users WHERE username = ?", "SELECT id FROM users WHERE username = ?",
["admin"] ["admin"],
); );
const adminPassword = await hashPassword("admin123"); const adminPassword = await hashPassword("admin123");
@@ -226,13 +374,20 @@ async function seedDatabase() {
if (existingAdmin.length > 0) { if (existingAdmin.length > 0) {
await db.execute( await db.execute(
"UPDATE users SET password = ?, is_active = TRUE WHERE username = ?", "UPDATE users SET password = ?, is_active = TRUE WHERE username = ?",
[adminPassword, "admin"] [adminPassword, "admin"],
); );
console.log(" ✅ SuperAdmin password updated"); console.log(" ✅ SuperAdmin password updated");
} else { } else {
await db.execute( await db.execute(
"INSERT INTO users (username, name, email, password, role, is_active) VALUES (?, ?, ?, ?, ?, ?)", "INSERT INTO users (username, name, email, password, role, is_active) VALUES (?, ?, ?, ?, ?, ?)",
["admin", "Super Admin", "admin@workallocate.com", adminPassword, "SuperAdmin", true] [
"admin",
"Super Admin",
"admin@workallocate.com",
adminPassword,
"SuperAdmin",
true,
],
); );
console.log(" ✅ SuperAdmin created"); console.log(" ✅ SuperAdmin created");
} }
@@ -247,34 +402,43 @@ async function seedDatabase() {
name: "Rajesh Sharma", name: "Rajesh Sharma",
email: "rajesh.sharma@workallocate.com", email: "rajesh.sharma@workallocate.com",
deptId: tudkiId, deptId: tudkiId,
phone: "9414567890" phone: "9414567890",
}, },
{ {
username: "sunil.verma.dana", username: "sunil.verma.dana",
name: "Sunil Verma", name: "Sunil Verma",
email: "sunil.verma@workallocate.com", email: "sunil.verma@workallocate.com",
deptId: danaId, deptId: danaId,
phone: "9414567891" phone: "9414567891",
}, },
{ {
username: "mahesh.agarwal.groundnut", username: "mahesh.agarwal.groundnut",
name: "Mahesh Agarwal", name: "Mahesh Agarwal",
email: "mahesh.agarwal@workallocate.com", email: "mahesh.agarwal@workallocate.com",
deptId: groundnutId, deptId: groundnutId,
phone: "9414567892" phone: "9414567892",
} },
]; ];
for (const sup of supervisors) { for (const sup of supervisors) {
if (sup.deptId) { if (sup.deptId) {
const existing = await db.query<{ id: number }[]>( const existing = await db.query<{ id: number }[]>(
"SELECT id FROM users WHERE username = ?", "SELECT id FROM users WHERE username = ?",
[sup.username] [sup.username],
); );
if (existing.length === 0) { if (existing.length === 0) {
await db.execute( await db.execute(
"INSERT INTO users (username, name, email, password, role, department_id, is_active, phone_number) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", "INSERT INTO users (username, name, email, password, role, department_id, is_active, phone_number) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
[sup.username, sup.name, sup.email, supervisorPassword, "Supervisor", sup.deptId, true, sup.phone] [
sup.username,
sup.name,
sup.email,
supervisorPassword,
"Supervisor",
sup.deptId,
true,
sup.phone,
],
); );
console.log(`${sup.name} created`); console.log(`${sup.name} created`);
} else { } else {
@@ -301,7 +465,7 @@ async function seedDatabase() {
bankIfsc: "HDFC0001234", bankIfsc: "HDFC0001234",
agreementNo: "AGR-GN-2024-001", agreementNo: "AGR-GN-2024-001",
pfNo: "RJ/JPR/12345/001", pfNo: "RJ/JPR/12345/001",
esicNo: "12-34-567890-001-0001" esicNo: "12-34-567890-001-0001",
}, },
{ {
username: "kishan.meena.gn", username: "kishan.meena.gn",
@@ -315,7 +479,7 @@ async function seedDatabase() {
bankIfsc: "SBIN0005678", bankIfsc: "SBIN0005678",
agreementNo: "AGR-GN-2024-002", agreementNo: "AGR-GN-2024-002",
pfNo: "RJ/JPR/12345/002", pfNo: "RJ/JPR/12345/002",
esicNo: "12-34-567890-001-0002" esicNo: "12-34-567890-001-0002",
}, },
// Dana Department Contractors // Dana Department Contractors
{ {
@@ -330,7 +494,7 @@ async function seedDatabase() {
bankIfsc: "PUNB0009876", bankIfsc: "PUNB0009876",
agreementNo: "AGR-DN-2024-001", agreementNo: "AGR-DN-2024-001",
pfNo: "RJ/JPR/12345/003", pfNo: "RJ/JPR/12345/003",
esicNo: "12-34-567890-002-0001" esicNo: "12-34-567890-002-0001",
}, },
{ {
username: "mohan.yadav.dana", username: "mohan.yadav.dana",
@@ -344,7 +508,7 @@ async function seedDatabase() {
bankIfsc: "BARB0004567", bankIfsc: "BARB0004567",
agreementNo: "AGR-DN-2024-002", agreementNo: "AGR-DN-2024-002",
pfNo: "RJ/JPR/12345/004", pfNo: "RJ/JPR/12345/004",
esicNo: "12-34-567890-002-0002" esicNo: "12-34-567890-002-0002",
}, },
// Tudki Department Contractors // Tudki Department Contractors
{ {
@@ -359,7 +523,7 @@ async function seedDatabase() {
bankIfsc: "ICIC0003456", bankIfsc: "ICIC0003456",
agreementNo: "AGR-TK-2024-001", agreementNo: "AGR-TK-2024-001",
pfNo: "RJ/JPR/12345/005", pfNo: "RJ/JPR/12345/005",
esicNo: "12-34-567890-003-0001" esicNo: "12-34-567890-003-0001",
}, },
{ {
username: "dinesh.gupta.tudki", username: "dinesh.gupta.tudki",
@@ -373,14 +537,14 @@ async function seedDatabase() {
bankIfsc: "UTIB0002345", bankIfsc: "UTIB0002345",
agreementNo: "AGR-TK-2024-002", agreementNo: "AGR-TK-2024-002",
pfNo: "RJ/JPR/12345/006", pfNo: "RJ/JPR/12345/006",
esicNo: "12-34-567890-003-0002" esicNo: "12-34-567890-003-0002",
} },
]; ];
for (const con of contractors) { for (const con of contractors) {
const existing = await db.query<{ id: number }[]>( const existing = await db.query<{ id: number }[]>(
"SELECT id FROM users WHERE username = ?", "SELECT id FROM users WHERE username = ?",
[con.username] [con.username],
); );
if (existing.length === 0) { if (existing.length === 0) {
await db.execute( await db.execute(
@@ -388,9 +552,23 @@ async function seedDatabase() {
phone_number, aadhar_number, bank_account_number, bank_name, bank_ifsc, phone_number, aadhar_number, bank_account_number, bank_name, bank_ifsc,
contractor_agreement_number, pf_number, esic_number) contractor_agreement_number, pf_number, esic_number)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[con.username, con.name, con.email, contractorPassword, "Contractor", con.deptId, true, [
con.phone, con.aadhar, con.bankAccount, con.bankName, con.bankIfsc, con.username,
con.agreementNo, con.pfNo, con.esicNo] con.name,
con.email,
contractorPassword,
"Contractor",
con.deptId,
true,
con.phone,
con.aadhar,
con.bankAccount,
con.bankName,
con.bankIfsc,
con.agreementNo,
con.pfNo,
con.esicNo,
],
); );
console.log(`${con.name} created`); console.log(`${con.name} created`);
} else { } else {
@@ -407,7 +585,7 @@ async function seedDatabase() {
for (const con of contractors) { for (const con of contractors) {
const result = await db.query<{ id: number }[]>( const result = await db.query<{ id: number }[]>(
"SELECT id FROM users WHERE username = ?", "SELECT id FROM users WHERE username = ?",
[con.username] [con.username],
); );
if (result.length > 0) { if (result.length > 0) {
contractorIds[con.username] = result[0].id; contractorIds[con.username] = result[0].id;
@@ -426,7 +604,7 @@ async function seedDatabase() {
aadhar: "111122223333", aadhar: "111122223333",
bankAccount: "30100111122233", bankAccount: "30100111122233",
bankName: "State Bank of India", bankName: "State Bank of India",
bankIfsc: "SBIN0001111" bankIfsc: "SBIN0001111",
}, },
{ {
username: "amit.kumar.gn2", username: "amit.kumar.gn2",
@@ -438,7 +616,7 @@ async function seedDatabase() {
aadhar: "222233334444", aadhar: "222233334444",
bankAccount: "30100222233344", bankAccount: "30100222233344",
bankName: "Punjab National Bank", bankName: "Punjab National Bank",
bankIfsc: "PUNB0002222" bankIfsc: "PUNB0002222",
}, },
{ {
username: "vijay.meena.gn3", username: "vijay.meena.gn3",
@@ -450,7 +628,7 @@ async function seedDatabase() {
aadhar: "333344445555", aadhar: "333344445555",
bankAccount: "30100333344455", bankAccount: "30100333344455",
bankName: "HDFC Bank", bankName: "HDFC Bank",
bankIfsc: "HDFC0003333" bankIfsc: "HDFC0003333",
}, },
// Groundnut Department Employees - Under Kishan Meena // Groundnut Department Employees - Under Kishan Meena
{ {
@@ -463,7 +641,7 @@ async function seedDatabase() {
aadhar: "444455556666", aadhar: "444455556666",
bankAccount: "30100444455566", bankAccount: "30100444455566",
bankName: "Bank of Baroda", bankName: "Bank of Baroda",
bankIfsc: "BARB0004444" bankIfsc: "BARB0004444",
}, },
{ {
username: "prakash.sharma.gn5", username: "prakash.sharma.gn5",
@@ -475,7 +653,7 @@ async function seedDatabase() {
aadhar: "555566667777", aadhar: "555566667777",
bankAccount: "30100555566677", bankAccount: "30100555566677",
bankName: "ICICI Bank", bankName: "ICICI Bank",
bankIfsc: "ICIC0005555" bankIfsc: "ICIC0005555",
}, },
// Dana Department Employees - Under Gopal Sharma // Dana Department Employees - Under Gopal Sharma
{ {
@@ -488,7 +666,7 @@ async function seedDatabase() {
aadhar: "666677778888", aadhar: "666677778888",
bankAccount: "30100666677788", bankAccount: "30100666677788",
bankName: "State Bank of India", bankName: "State Bank of India",
bankIfsc: "SBIN0006666" bankIfsc: "SBIN0006666",
}, },
{ {
username: "lakhan.singh.dn2", username: "lakhan.singh.dn2",
@@ -500,7 +678,7 @@ async function seedDatabase() {
aadhar: "777788889999", aadhar: "777788889999",
bankAccount: "30100777788899", bankAccount: "30100777788899",
bankName: "Punjab National Bank", bankName: "Punjab National Bank",
bankIfsc: "PUNB0007777" bankIfsc: "PUNB0007777",
}, },
{ {
username: "bharat.meena.dn3", username: "bharat.meena.dn3",
@@ -512,7 +690,7 @@ async function seedDatabase() {
aadhar: "888899990000", aadhar: "888899990000",
bankAccount: "30100888899900", bankAccount: "30100888899900",
bankName: "HDFC Bank", bankName: "HDFC Bank",
bankIfsc: "HDFC0008888" bankIfsc: "HDFC0008888",
}, },
// Dana Department Employees - Under Mohan Yadav // Dana Department Employees - Under Mohan Yadav
{ {
@@ -525,7 +703,7 @@ async function seedDatabase() {
aadhar: "999900001111", aadhar: "999900001111",
bankAccount: "30100999900011", bankAccount: "30100999900011",
bankName: "Bank of Baroda", bankName: "Bank of Baroda",
bankIfsc: "BARB0009999" bankIfsc: "BARB0009999",
}, },
{ {
username: "shyam.gupta.dn5", username: "shyam.gupta.dn5",
@@ -537,7 +715,7 @@ async function seedDatabase() {
aadhar: "000011112222", aadhar: "000011112222",
bankAccount: "30100000011122", bankAccount: "30100000011122",
bankName: "ICICI Bank", bankName: "ICICI Bank",
bankIfsc: "ICIC0000000" bankIfsc: "ICIC0000000",
}, },
// Tudki Department Employees - Under Suresh Kumar // Tudki Department Employees - Under Suresh Kumar
{ {
@@ -550,7 +728,7 @@ async function seedDatabase() {
aadhar: "112233445566", aadhar: "112233445566",
bankAccount: "30100112233445", bankAccount: "30100112233445",
bankName: "State Bank of India", bankName: "State Bank of India",
bankIfsc: "SBIN0001122" bankIfsc: "SBIN0001122",
}, },
{ {
username: "naresh.yadav.tk2", username: "naresh.yadav.tk2",
@@ -562,7 +740,7 @@ async function seedDatabase() {
aadhar: "223344556677", aadhar: "223344556677",
bankAccount: "30100223344556", bankAccount: "30100223344556",
bankName: "Punjab National Bank", bankName: "Punjab National Bank",
bankIfsc: "PUNB0002233" bankIfsc: "PUNB0002233",
}, },
{ {
username: "mukesh.sharma.tk3", username: "mukesh.sharma.tk3",
@@ -574,7 +752,7 @@ async function seedDatabase() {
aadhar: "334455667788", aadhar: "334455667788",
bankAccount: "30100334455667", bankAccount: "30100334455667",
bankName: "HDFC Bank", bankName: "HDFC Bank",
bankIfsc: "HDFC0003344" bankIfsc: "HDFC0003344",
}, },
// Tudki Department Employees - Under Dinesh Gupta // Tudki Department Employees - Under Dinesh Gupta
{ {
@@ -587,7 +765,7 @@ async function seedDatabase() {
aadhar: "445566778899", aadhar: "445566778899",
bankAccount: "30100445566778", bankAccount: "30100445566778",
bankName: "Bank of Baroda", bankName: "Bank of Baroda",
bankIfsc: "BARB0004455" bankIfsc: "BARB0004455",
}, },
{ {
username: "deepak.verma.tk5", username: "deepak.verma.tk5",
@@ -599,7 +777,7 @@ async function seedDatabase() {
aadhar: "556677889900", aadhar: "556677889900",
bankAccount: "30100556677889", bankAccount: "30100556677889",
bankName: "ICICI Bank", bankName: "ICICI Bank",
bankIfsc: "ICIC0005566" bankIfsc: "ICIC0005566",
}, },
{ {
username: "rahul.meena.tk6", username: "rahul.meena.tk6",
@@ -611,14 +789,14 @@ async function seedDatabase() {
aadhar: "667788990011", aadhar: "667788990011",
bankAccount: "30100667788990", bankAccount: "30100667788990",
bankName: "Axis Bank", bankName: "Axis Bank",
bankIfsc: "UTIB0006677" bankIfsc: "UTIB0006677",
} },
]; ];
for (const emp of employees) { for (const emp of employees) {
const existing = await db.query<{ id: number }[]>( const existing = await db.query<{ id: number }[]>(
"SELECT id FROM users WHERE username = ?", "SELECT id FROM users WHERE username = ?",
[emp.username] [emp.username],
); );
if (existing.length === 0) { if (existing.length === 0) {
const contractorId = contractorIds[emp.contractorUsername]; const contractorId = contractorIds[emp.contractorUsername];
@@ -627,8 +805,21 @@ async function seedDatabase() {
`INSERT INTO users (username, name, email, password, role, department_id, contractor_id, is_active, `INSERT INTO users (username, name, email, password, role, department_id, contractor_id, is_active,
phone_number, aadhar_number, bank_account_number, bank_name, bank_ifsc) phone_number, aadhar_number, bank_account_number, bank_name, bank_ifsc)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[emp.username, emp.name, emp.email, employeePassword, "Employee", emp.deptId, contractorId, true, [
emp.phone, emp.aadhar, emp.bankAccount, emp.bankName, emp.bankIfsc] emp.username,
emp.name,
emp.email,
employeePassword,
"Employee",
emp.deptId,
contractorId,
true,
emp.phone,
emp.aadhar,
emp.bankAccount,
emp.bankName,
emp.bankIfsc,
],
); );
console.log(`${emp.name} created`); console.log(`${emp.name} created`);
} }
@@ -642,49 +833,63 @@ async function seedDatabase() {
const today = new Date().toISOString().split("T")[0]; const today = new Date().toISOString().split("T")[0];
// Get all sub-departments for rate assignment // Get all sub-departments for rate assignment
const allSubDepts = await db.query<{ id: number; name: string; department_id: number }[]>( const allSubDepts = await db.query<
"SELECT id, name, department_id FROM sub_departments" { id: number; name: string; department_id: number }[]
>(
"SELECT id, name, department_id FROM sub_departments",
); );
// Create rates for each contractor based on their department // Create rates for each contractor based on their department
for (const [username, contractorId] of Object.entries(contractorIds)) { for (const [username, contractorId] of Object.entries(contractorIds)) {
const existingRate = await db.query<{ id: number }[]>( const existingRate = await db.query<{ id: number }[]>(
"SELECT id FROM contractor_rates WHERE contractor_id = ?", "SELECT id FROM contractor_rates WHERE contractor_id = ?",
[contractorId] [contractorId],
); );
if (existingRate.length === 0) { if (existingRate.length === 0) {
// Find the contractor's department // Find the contractor's department
const contractor = contractors.find(c => c.username === username); const contractor = contractors.find((c) => c.username === username);
if (contractor) { if (contractor) {
// Get sub-departments for this contractor's department // Get sub-departments for this contractor's department
const deptSubDepts = allSubDepts.filter(sd => sd.department_id === contractor.deptId); const deptSubDepts = allSubDepts.filter((sd) =>
sd.department_id === contractor.deptId
);
// Create rates for Loading/Unloading sub-department (Per Bag rates) // Create rates for Loading/Unloading sub-department (Per Bag rates)
const loadingSubDept = deptSubDepts.find(sd => sd.name === "Loading/Unloading"); const loadingSubDept = deptSubDepts.find((sd) =>
sd.name === "Loading/Unloading"
);
if (loadingSubDept) { if (loadingSubDept) {
// Get activities for this sub-department // Get activities for this sub-department
const activities = await db.query<{ id: number; name: string }[]>( const activities = await db.query<{ id: number; name: string }[]>(
"SELECT id, name FROM activities WHERE sub_department_id = ? LIMIT 3", "SELECT id, name FROM activities WHERE sub_department_id = ? LIMIT 3",
[loadingSubDept.id] [loadingSubDept.id],
); );
for (const activity of activities) { for (const activity of activities) {
const rate = 5 + Math.random() * 3; // Random rate between 5-8 per bag const rate = 5 + Math.random() * 3; // Random rate between 5-8 per bag
await db.execute( await db.execute(
"INSERT INTO contractor_rates (contractor_id, sub_department_id, activity, rate, effective_date) VALUES (?, ?, ?, ?, ?)", "INSERT INTO contractor_rates (contractor_id, sub_department_id, activity, rate, effective_date) VALUES (?, ?, ?, ?, ?)",
[contractorId, loadingSubDept.id, activity.name, rate.toFixed(2), today] [
contractorId,
loadingSubDept.id,
activity.name,
rate.toFixed(2),
today,
],
); );
} }
} }
// Create fixed rates for other sub-departments // Create fixed rates for other sub-departments
const fixedSubDepts = deptSubDepts.filter(sd => sd.name !== "Loading/Unloading"); const fixedSubDepts = deptSubDepts.filter((sd) =>
sd.name !== "Loading/Unloading"
);
for (const subDept of fixedSubDepts.slice(0, 2)) { // Limit to 2 fixed rate sub-depts per contractor for (const subDept of fixedSubDepts.slice(0, 2)) { // Limit to 2 fixed rate sub-depts per contractor
const rate = 300 + Math.random() * 200; // Random rate between 300-500 per person const rate = 300 + Math.random() * 200; // Random rate between 300-500 per person
await db.execute( await db.execute(
"INSERT INTO contractor_rates (contractor_id, sub_department_id, rate, effective_date) VALUES (?, ?, ?, ?)", "INSERT INTO contractor_rates (contractor_id, sub_department_id, rate, effective_date) VALUES (?, ?, ?, ?)",
[contractorId, subDept.id, rate.toFixed(2), today] [contractorId, subDept.id, rate.toFixed(2), today],
); );
} }
} }
@@ -716,7 +921,6 @@ async function seedDatabase() {
- Use any employee username like ravi.singh.gn1, rampal.verma.dn1, ganesh.kumar.tk1 - Use any employee username like ravi.singh.gn1, rampal.verma.dn1, ganesh.kumar.tk1
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
`); `);
} catch (error) { } catch (error) {
console.error("❌ Error seeding database:", (error as Error).message); console.error("❌ Error seeding database:", (error as Error).message);
Deno.exit(1); Deno.exit(1);

View File

@@ -51,7 +51,11 @@ export interface SubDepartment {
} }
// Work allocation types // Work allocation types
export type AllocationStatus = "Pending" | "InProgress" | "Completed" | "Cancelled"; export type AllocationStatus =
| "Pending"
| "InProgress"
| "Completed"
| "Cancelled";
export interface WorkAllocation { export interface WorkAllocation {
id: number; id: number;
@@ -76,7 +80,12 @@ export interface WorkAllocation {
} }
// Attendance types // Attendance types
export type AttendanceStatus = "CheckedIn" | "CheckedOut" | "Absent" | "HalfDay" | "Late"; export type AttendanceStatus =
| "CheckedIn"
| "CheckedOut"
| "Absent"
| "HalfDay"
| "Late";
export interface Attendance { export interface Attendance {
id: number; id: number;

View File

@@ -12,7 +12,16 @@ services:
- mysql_data:/var/lib/mysql - mysql_data:/var/lib/mysql
- ./backend/database/init-schema.sql:/docker-entrypoint-initdb.d/01-init-schema.sql:ro - ./backend/database/init-schema.sql:/docker-entrypoint-initdb.d/01-init-schema.sql:ro
healthcheck: healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-padmin123"] test: [
"CMD",
"mysqladmin",
"ping",
"-h",
"localhost",
"-u",
"root",
"-padmin123",
]
interval: 5s interval: 5s
timeout: 5s timeout: 5s
retries: 10 retries: 10

View File

@@ -1,13 +1,13 @@
import js from '@eslint/js' import js from "@eslint/js";
import globals from 'globals' import globals from "globals";
import reactHooks from 'eslint-plugin-react-hooks' import reactHooks from "eslint-plugin-react-hooks";
import reactRefresh from 'eslint-plugin-react-refresh' import reactRefresh from "eslint-plugin-react-refresh";
import tseslint from 'typescript-eslint' import tseslint from "typescript-eslint";
export default tseslint.config( export default tseslint.config(
{ ignores: ['dist', 'node_modules', 'backend', 'backend-deno'] }, { ignores: ["dist", "node_modules", "backend", "backend-deno"] },
{ {
files: ['**/*.{ts,tsx}'], files: ["**/*.{ts,tsx}"],
extends: [ extends: [
js.configs.recommended, js.configs.recommended,
...tseslint.configs.recommended, ...tseslint.configs.recommended,
@@ -16,21 +16,26 @@ export default tseslint.config(
ecmaVersion: 2020, ecmaVersion: 2020,
globals: globals.browser, globals: globals.browser,
parserOptions: { parserOptions: {
ecmaVersion: 'latest', ecmaVersion: "latest",
ecmaFeatures: { jsx: true }, ecmaFeatures: { tsx: true },
sourceType: 'module', sourceType: "module",
}, },
}, },
plugins: { plugins: {
'react-hooks': reactHooks, "react-hooks": reactHooks,
'react-refresh': reactRefresh, "react-refresh": reactRefresh,
}, },
rules: { rules: {
...reactHooks.configs.recommended.rules, ...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': ['warn', { allowConstantExport: true }], "react-refresh/only-export-components": ["warn", {
'@typescript-eslint/no-unused-vars': ['warn', { varsIgnorePattern: '^_', argsIgnorePattern: '^_' }], allowConstantExport: true,
'@typescript-eslint/no-explicit-any': 'warn', }],
'no-unused-vars': 'off', "@typescript-eslint/no-unused-vars": ["warn", {
varsIgnorePattern: "^_",
argsIgnorePattern: "^_",
}],
"@typescript-eslint/no-explicit-any": "warn",
"no-unused-vars": "off",
}, },
}, },
) );

View File

@@ -1,4 +1,4 @@
<!doctype html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />

8
package-lock.json generated
View File

@@ -8,10 +8,10 @@
"name": "my-dashboard", "name": "my-dashboard",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"lucide-react": "^0.555.0", "lucide-react": "0.555.0",
"react": "^19.2.0", "react": "19.2.0",
"react-dom": "^19.2.0", "react-dom": "19.2.0",
"recharts": "^3.5.0", "recharts": "3.5.0",
"xlsx": "^0.18.5" "xlsx": "^0.18.5"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -10,10 +10,10 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"lucide-react": "^0.555.0", "lucide-react": "0.555.0",
"react": "^19.2.0", "react": "19.2.0",
"react-dom": "^19.2.0", "react-dom": "19.2.0",
"recharts": "^3.5.0", "recharts": "3.5.0",
"xlsx": "^0.18.5" "xlsx": "^0.18.5"
}, },
"devDependencies": { "devDependencies": {

2365
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,4 +3,4 @@ export default {
tailwindcss: {}, tailwindcss: {},
autoprefixer: {}, autoprefixer: {},
}, },
} };

View File

@@ -1 +1,40 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg> <svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
aria-hidden="true"
role="img"
class="iconify iconify--logos"
width="31.88"
height="32"
preserveAspectRatio="xMidYMid meet"
viewBox="0 0 256 257"
>
<defs><linearGradient
id="IconifyId1813088fe1fbc01fb466"
x1="-.828%"
x2="57.636%"
y1="7.652%"
y2="78.411%"
><stop offset="0%" stop-color="#41D1FF"></stop><stop
offset="100%"
stop-color="#BD34FE"
></stop></linearGradient><linearGradient
id="IconifyId1813088fe1fbc01fb467"
x1="43.376%"
x2="50.316%"
y1="2.242%"
y2="89.03%"
><stop offset="0%" stop-color="#FFEA83"></stop><stop
offset="8.333%"
stop-color="#FFDD35"
></stop><stop
offset="100%"
stop-color="#FFA800"
></stop></linearGradient></defs><path
fill="url(#IconifyId1813088fe1fbc01fb466)"
d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"
></path><path
fill="url(#IconifyId1813088fe1fbc01fb467)"
d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"
></path>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -1,46 +1,56 @@
import React, { useState } from 'react'; import React, { useState } from "react";
import { AuthProvider, useAuth } from './contexts/AuthContext'; import { AuthProvider, useAuth } from "./contexts/AuthContext.tsx";
import { Sidebar } from './components/layout/Sidebar'; import { Sidebar } from "./components/layout/Sidebar.tsx";
import { Header } from './components/layout/Header'; import { Header } from "./components/layout/Header.tsx";
import { DashboardPage } from './pages/DashboardPage'; import { DashboardPage } from "./pages/DashboardPage.tsx";
import { UsersPage } from './pages/UsersPage'; import { UsersPage } from "./pages/UsersPage.tsx";
import { WorkAllocationPage } from './pages/WorkAllocationPage'; import { WorkAllocationPage } from "./pages/WorkAllocationPage.tsx";
import { AttendancePage } from './pages/AttendancePage'; import { AttendancePage } from "./pages/AttendancePage.tsx";
import { RatesPage } from './pages/RatesPage'; import { RatesPage } from "./pages/RatesPage.tsx";
import { EmployeeSwapPage } from './pages/EmployeeSwapPage'; import { EmployeeSwapPage } from "./pages/EmployeeSwapPage.tsx";
import { LoginPage } from './pages/LoginPage'; import { LoginPage } from "./pages/LoginPage.tsx";
import { ReportingPage } from './pages/ReportingPage'; import { ReportingPage } from "./pages/ReportingPage.tsx";
import { StandardRatesPage } from './pages/StandardRatesPage'; import { StandardRatesPage } from "./pages/StandardRatesPage.tsx";
import { AllRatesPage } from './pages/AllRatesPage'; import { AllRatesPage } from "./pages/AllRatesPage.tsx";
import { ActivitiesPage } from './pages/ActivitiesPage'; import { ActivitiesPage } from "./pages/ActivitiesPage.tsx";
type PageType = 'dashboard' | 'users' | 'allocation' | 'attendance' | 'rates' | 'swaps' | 'reports' | 'standard-rates' | 'all-rates' | 'activities'; 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");
const { isAuthenticated, isLoading } = useAuth(); const { isAuthenticated, isLoading } = useAuth();
const renderPage = () => { const renderPage = () => {
switch (activePage) { switch (activePage) {
case 'dashboard': case "dashboard":
return <DashboardPage />; return <DashboardPage />;
case 'users': case "users":
return <UsersPage />; return <UsersPage />;
case 'allocation': case "allocation":
return <WorkAllocationPage />; return <WorkAllocationPage />;
case 'attendance': case "attendance":
return <AttendancePage />; return <AttendancePage />;
case 'rates': case "rates":
return <RatesPage />; return <RatesPage />;
case 'swaps': case "swaps":
return <EmployeeSwapPage />; return <EmployeeSwapPage />;
case 'reports': case "reports":
return <ReportingPage />; return <ReportingPage />;
case 'standard-rates': case "standard-rates":
return <StandardRatesPage />; return <StandardRatesPage />;
case 'all-rates': case "all-rates":
return <AllRatesPage />; return <AllRatesPage />;
case 'activities': case "activities":
return <ActivitiesPage />; return <ActivitiesPage />;
default: default:
return <DashboardPage />; return <DashboardPage />;
@@ -52,7 +62,8 @@ const AppContent: React.FC = () => {
return ( return (
<div className="flex h-screen items-center justify-center bg-gray-100"> <div className="flex h-screen items-center justify-center bg-gray-100">
<div className="text-center"> <div className="text-center">
<div className="inline-block animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mb-4"></div> <div className="inline-block animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mb-4">
</div>
<p className="text-gray-600">Loading...</p> <p className="text-gray-600">Loading...</p>
</div> </div>
</div> </div>
@@ -67,7 +78,10 @@ const AppContent: React.FC = () => {
// Show main app if authenticated // Show main app if authenticated
return ( return (
<div className="flex h-screen bg-gray-100"> <div className="flex h-screen bg-gray-100">
<Sidebar activePage={activePage} onNavigate={(page) => setActivePage(page as PageType)} /> <Sidebar
activePage={activePage}
onNavigate={(page) => setActivePage(page as PageType)}
/>
<div className="flex-1 flex flex-col overflow-hidden"> <div className="flex-1 flex flex-col overflow-hidden">
<Header /> <Header />

View File

@@ -1 +1,16 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg> <svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
aria-hidden="true"
role="img"
class="iconify iconify--logos"
width="35.93"
height="32"
preserveAspectRatio="xMidYMid meet"
viewBox="0 0 256 228"
>
<path
fill="#00D8FF"
d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"
></path>
</svg>

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

@@ -1,9 +1,24 @@
import React, { useState, useEffect } from 'react'; import React, { useEffect, useState } from "react";
import { Bell, LogOut, X, Camera, Shield, User, Mail, Building2, ChevronDown, ChevronUp, Phone, CreditCard, Landmark, FileText } from 'lucide-react'; import {
import { useAuth } from '../../contexts/AuthContext'; Bell,
import { useDepartments } from '../../hooks/useDepartments'; Building2,
import { api } from '../../services/api'; Camera,
import type { User as UserType } from '../../types'; ChevronDown,
ChevronUp,
CreditCard,
FileText,
Landmark,
LogOut,
Mail,
Phone,
Shield,
User,
X,
} from "lucide-react";
import { useAuth } from "../../contexts/AuthContext.tsx";
import { useDepartments } from "../../hooks/useDepartments.ts";
import { api } from "../../services/api.ts";
import type { User as UserType } from "../../types.ts";
interface ProfilePopupProps { interface ProfilePopupProps {
isOpen: boolean; isOpen: boolean;
@@ -12,47 +27,52 @@ interface ProfilePopupProps {
} }
// Permission definitions for each role // Permission definitions for each role
const rolePermissions: Record<string, { title: string; permissions: string[] }> = { const rolePermissions: Record<
string,
{ title: string; permissions: string[] }
> = {
Supervisor: { Supervisor: {
title: 'Supervisor Permissions', title: "Supervisor Permissions",
permissions: [ permissions: [
'View and manage employees in your department', "View and manage employees in your department",
'Create and manage work allocations', "Create and manage work allocations",
'Set contractor rates for your department', "Set contractor rates for your department",
'View attendance records', "View attendance records",
'Manage check-in/check-out for employees', "Manage check-in/check-out for employees",
] ],
}, },
Employee: { Employee: {
title: 'Employee Permissions', title: "Employee Permissions",
permissions: [ permissions: [
'View your work allocations', "View your work allocations",
'View your attendance records', "View your attendance records",
'Check-in and check-out', "Check-in and check-out",
'View assigned tasks', "View assigned tasks",
] ],
}, },
Contractor: { Contractor: {
title: 'Contractor Permissions', title: "Contractor Permissions",
permissions: [ permissions: [
'View assigned work allocations', "View assigned work allocations",
'View your rate configurations', "View your rate configurations",
'Track work completion status', "Track work completion status",
] ],
}, },
SuperAdmin: { SuperAdmin: {
title: 'Super Admin Permissions', title: "Super Admin Permissions",
permissions: [ permissions: [
'Full system access', "Full system access",
'Manage all users and departments', "Manage all users and departments",
'Configure all contractor rates', "Configure all contractor rates",
'View all work allocations and reports', "View all work allocations and reports",
'System configuration and settings', "System configuration and settings",
] ],
} },
}; };
const ProfilePopup: React.FC<ProfilePopupProps> = ({ isOpen, onClose, onLogout }) => { const ProfilePopup: React.FC<ProfilePopupProps> = (
{ isOpen, onClose, onLogout },
) => {
const { user } = useAuth(); const { user } = useAuth();
const { departments } = useDepartments(); const { departments } = useDepartments();
const [showPermissions, setShowPermissions] = useState(false); const [showPermissions, setShowPermissions] = useState(false);
@@ -68,10 +88,11 @@ const ProfilePopup: React.FC<ProfilePopupProps> = ({ isOpen, onClose, onLogout }
if (!isOpen) return null; if (!isOpen) return null;
const userDepartment = departments.find(d => d.id === user?.department_id); const userDepartment = departments.find((d) => d.id === user?.department_id);
const userPermissions = rolePermissions[user?.role || 'Employee']; const userPermissions = rolePermissions[user?.role || "Employee"];
const isEmployeeOrContractor = user?.role === 'Employee' || user?.role === 'Contractor'; const isEmployeeOrContractor = user?.role === "Employee" ||
const isContractor = user?.role === 'Contractor'; user?.role === "Contractor";
const isContractor = user?.role === "Contractor";
return ( return (
<div className="absolute right-4 top-16 w-[400px] bg-gradient-to-b from-slate-50 to-white rounded-2xl shadow-2xl z-50 border border-gray-200 overflow-hidden font-sans text-sm text-gray-800 max-h-[85vh] overflow-y-auto"> <div className="absolute right-4 top-16 w-[400px] bg-gradient-to-b from-slate-50 to-white rounded-2xl shadow-2xl z-50 border border-gray-200 overflow-hidden font-sans text-sm text-gray-800 max-h-[85vh] overflow-y-auto">
@@ -79,22 +100,27 @@ const ProfilePopup: React.FC<ProfilePopupProps> = ({ isOpen, onClose, onLogout }
<div className="bg-gradient-to-r from-teal-600 to-teal-500 px-6 py-4"> <div className="bg-gradient-to-r from-teal-600 to-teal-500 px-6 py-4">
<div className="flex justify-between items-start"> <div className="flex justify-between items-start">
<div className="flex-1" /> <div className="flex-1" />
<button onClick={onClose} className="text-white/80 hover:text-white hover:bg-white/20 rounded-full p-1 transition-colors"> <button
onClick={onClose}
className="text-white/80 hover:text-white hover:bg-white/20 rounded-full p-1 transition-colors"
>
<X size={20} /> <X size={20} />
</button> </button>
</div> </div>
<div className="flex flex-col items-center -mt-2"> <div className="flex flex-col items-center -mt-2">
<div className="relative mb-3"> <div className="relative mb-3">
<div className="w-20 h-20 bg-white rounded-full flex items-center justify-center text-teal-600 text-4xl font-bold shadow-lg"> <div className="w-20 h-20 bg-white rounded-full flex items-center justify-center text-teal-600 text-4xl font-bold shadow-lg">
{user?.name?.charAt(0).toUpperCase() || 'U'} {user?.name?.charAt(0).toUpperCase() || "U"}
</div> </div>
<div className="absolute bottom-0 right-0 bg-teal-700 rounded-full p-1.5 shadow-md cursor-pointer hover:bg-teal-800 transition-colors"> <div className="absolute bottom-0 right-0 bg-teal-700 rounded-full p-1.5 shadow-md cursor-pointer hover:bg-teal-800 transition-colors">
<Camera size={12} className="text-white" /> <Camera size={12} className="text-white" />
</div> </div>
</div> </div>
<h3 className="text-xl text-white font-semibold">Hi, {user?.name || 'User'}!</h3> <h3 className="text-xl text-white font-semibold">
Hi, {user?.name || "User"}!
</h3>
<span className="mt-1 px-3 py-1 bg-white/20 text-white text-xs font-semibold rounded-full uppercase tracking-wider"> <span className="mt-1 px-3 py-1 bg-white/20 text-white text-xs font-semibold rounded-full uppercase tracking-wider">
{user?.role || 'User'} {user?.role || "User"}
</span> </span>
</div> </div>
</div> </div>
@@ -107,7 +133,9 @@ const ProfilePopup: React.FC<ProfilePopupProps> = ({ isOpen, onClose, onLogout }
</div> </div>
<div> <div>
<p className="text-xs text-gray-500 font-medium">Username</p> <p className="text-xs text-gray-500 font-medium">Username</p>
<p className="text-sm font-semibold text-gray-800">{user?.username || 'N/A'}</p> <p className="text-sm font-semibold text-gray-800">
{user?.username || "N/A"}
</p>
</div> </div>
</div> </div>
@@ -117,18 +145,22 @@ const ProfilePopup: React.FC<ProfilePopupProps> = ({ isOpen, onClose, onLogout }
</div> </div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<p className="text-xs text-gray-500 font-medium">Email</p> <p className="text-xs text-gray-500 font-medium">Email</p>
<p className="text-sm font-semibold text-gray-800 truncate">{user?.email || 'No email'}</p> <p className="text-sm font-semibold text-gray-800 truncate">
{user?.email || "No email"}
</p>
</div> </div>
</div> </div>
{user?.role !== 'SuperAdmin' && userDepartment && ( {user?.role !== "SuperAdmin" && userDepartment && (
<div className="flex items-center gap-3 p-3 bg-gray-50 rounded-xl"> <div className="flex items-center gap-3 p-3 bg-gray-50 rounded-xl">
<div className="w-10 h-10 bg-green-100 rounded-full flex items-center justify-center"> <div className="w-10 h-10 bg-green-100 rounded-full flex items-center justify-center">
<Building2 size={18} className="text-green-600" /> <Building2 size={18} className="text-green-600" />
</div> </div>
<div> <div>
<p className="text-xs text-gray-500 font-medium">Department</p> <p className="text-xs text-gray-500 font-medium">Department</p>
<p className="text-sm font-semibold text-gray-800">{userDepartment.name}</p> <p className="text-sm font-semibold text-gray-800">
{userDepartment.name}
</p>
</div> </div>
</div> </div>
)} )}
@@ -144,11 +176,17 @@ const ProfilePopup: React.FC<ProfilePopupProps> = ({ isOpen, onClose, onLogout }
<CreditCard size={18} className="text-teal-600" /> <CreditCard size={18} className="text-teal-600" />
</div> </div>
<div className="text-left"> <div className="text-left">
<p className="text-xs text-gray-500 font-medium">Personal & Bank Details</p> <p className="text-xs text-gray-500 font-medium">
<p className="text-sm font-semibold text-gray-800">View your information</p> Personal & Bank Details
</p>
<p className="text-sm font-semibold text-gray-800">
View your information
</p>
</div> </div>
</div> </div>
{showDetails ? <ChevronUp size={18} className="text-teal-600" /> : <ChevronDown size={18} className="text-teal-600" />} {showDetails
? <ChevronUp size={18} className="text-teal-600" />
: <ChevronDown size={18} className="text-teal-600" />}
</button> </button>
)} )}
@@ -162,14 +200,16 @@ const ProfilePopup: React.FC<ProfilePopupProps> = ({ isOpen, onClose, onLogout }
<div className="space-y-2 text-sm"> <div className="space-y-2 text-sm">
<div className="flex justify-between"> <div className="flex justify-between">
<span className="text-gray-600">Phone Number</span> <span className="text-gray-600">Phone Number</span>
<span className="font-medium text-gray-800">{fullUserData.phone_number || 'Not provided'}</span> <span className="font-medium text-gray-800">
{fullUserData.phone_number || "Not provided"}
</span>
</div> </div>
<div className="flex justify-between"> <div className="flex justify-between">
<span className="text-gray-600">Aadhar Number</span> <span className="text-gray-600">Aadhar Number</span>
<span className="font-medium text-gray-800"> <span className="font-medium text-gray-800">
{fullUserData.aadhar_number {fullUserData.aadhar_number
? `XXXX-XXXX-${fullUserData.aadhar_number.slice(-4)}` ? `XXXX-XXXX-${fullUserData.aadhar_number.slice(-4)}`
: 'Not provided'} : "Not provided"}
</span> </span>
</div> </div>
</div> </div>
@@ -183,19 +223,23 @@ const ProfilePopup: React.FC<ProfilePopupProps> = ({ isOpen, onClose, onLogout }
<div className="space-y-2 text-sm"> <div className="space-y-2 text-sm">
<div className="flex justify-between"> <div className="flex justify-between">
<span className="text-gray-600">Bank Name</span> <span className="text-gray-600">Bank Name</span>
<span className="font-medium text-gray-800">{fullUserData.bank_name || 'Not provided'}</span> <span className="font-medium text-gray-800">
{fullUserData.bank_name || "Not provided"}
</span>
</div> </div>
<div className="flex justify-between"> <div className="flex justify-between">
<span className="text-gray-600">Account Number</span> <span className="text-gray-600">Account Number</span>
<span className="font-medium text-gray-800"> <span className="font-medium text-gray-800">
{fullUserData.bank_account_number {fullUserData.bank_account_number
? `XXXX${fullUserData.bank_account_number.slice(-4)}` ? `XXXX${fullUserData.bank_account_number.slice(-4)}`
: 'Not provided'} : "Not provided"}
</span> </span>
</div> </div>
<div className="flex justify-between"> <div className="flex justify-between">
<span className="text-gray-600">IFSC Code</span> <span className="text-gray-600">IFSC Code</span>
<span className="font-medium text-gray-800">{fullUserData.bank_ifsc || 'Not provided'}</span> <span className="font-medium text-gray-800">
{fullUserData.bank_ifsc || "Not provided"}
</span>
</div> </div>
</div> </div>
</div> </div>
@@ -209,15 +253,22 @@ const ProfilePopup: React.FC<ProfilePopupProps> = ({ isOpen, onClose, onLogout }
<div className="space-y-2 text-sm"> <div className="space-y-2 text-sm">
<div className="flex justify-between"> <div className="flex justify-between">
<span className="text-gray-600">Agreement No.</span> <span className="text-gray-600">Agreement No.</span>
<span className="font-medium text-gray-800">{fullUserData.contractor_agreement_number || 'Not provided'}</span> <span className="font-medium text-gray-800">
{fullUserData.contractor_agreement_number ||
"Not provided"}
</span>
</div> </div>
<div className="flex justify-between"> <div className="flex justify-between">
<span className="text-gray-600">PF Number</span> <span className="text-gray-600">PF Number</span>
<span className="font-medium text-gray-800">{fullUserData.pf_number || 'Not provided'}</span> <span className="font-medium text-gray-800">
{fullUserData.pf_number || "Not provided"}
</span>
</div> </div>
<div className="flex justify-between"> <div className="flex justify-between">
<span className="text-gray-600">ESIC Number</span> <span className="text-gray-600">ESIC Number</span>
<span className="font-medium text-gray-800">{fullUserData.esic_number || 'Not provided'}</span> <span className="font-medium text-gray-800">
{fullUserData.esic_number || "Not provided"}
</span>
</div> </div>
</div> </div>
</div> </div>
@@ -235,19 +286,30 @@ const ProfilePopup: React.FC<ProfilePopupProps> = ({ isOpen, onClose, onLogout }
<Shield size={18} className="text-amber-600" /> <Shield size={18} className="text-amber-600" />
</div> </div>
<div className="text-left"> <div className="text-left">
<p className="text-xs text-gray-500 font-medium">Your Permissions</p> <p className="text-xs text-gray-500 font-medium">
<p className="text-sm font-semibold text-gray-800">View what you can do</p> Your Permissions
</p>
<p className="text-sm font-semibold text-gray-800">
View what you can do
</p>
</div> </div>
</div> </div>
{showPermissions ? <ChevronUp size={18} className="text-amber-600" /> : <ChevronDown size={18} className="text-amber-600" />} {showPermissions
? <ChevronUp size={18} className="text-amber-600" />
: <ChevronDown size={18} className="text-amber-600" />}
</button> </button>
{showPermissions && userPermissions && ( {showPermissions && userPermissions && (
<div className="bg-amber-50 rounded-xl p-4 border border-amber-200"> <div className="bg-amber-50 rounded-xl p-4 border border-amber-200">
<h4 className="font-semibold text-amber-800 mb-2">{userPermissions.title}</h4> <h4 className="font-semibold text-amber-800 mb-2">
{userPermissions.title}
</h4>
<ul className="space-y-2"> <ul className="space-y-2">
{userPermissions.permissions.map((perm, idx) => ( {userPermissions.permissions.map((perm, idx) => (
<li key={idx} className="flex items-start gap-2 text-sm text-amber-700"> <li
key={idx}
className="flex items-start gap-2 text-sm text-amber-700"
>
<span className="text-amber-500 mt-0.5"></span> <span className="text-amber-500 mt-0.5"></span>
{perm} {perm}
</li> </li>
@@ -284,18 +346,21 @@ export const Header: React.FC = () => {
<header className="bg-white border-b border-gray-200 px-6 py-4 relative"> <header className="bg-white border-b border-gray-200 px-6 py-4 relative">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex-1"> <div className="flex-1">
<h1 className="text-2xl font-bold text-gray-800">Work Allocation System</h1> <h1 className="text-2xl font-bold text-gray-800">
Work Allocation System
</h1>
</div> </div>
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
<button className="p-2 text-gray-600 hover:bg-gray-100 rounded-full relative"> <button className="p-2 text-gray-600 hover:bg-gray-100 rounded-full relative">
<Bell size={20} /> <Bell size={20} />
<span className="absolute top-1 right-1 w-2 h-2 bg-red-500 rounded-full"></span> <span className="absolute top-1 right-1 w-2 h-2 bg-red-500 rounded-full">
</span>
</button> </button>
<button <button
onClick={() => setIsProfileOpen(!isProfileOpen)} onClick={() => setIsProfileOpen(!isProfileOpen)}
className="w-10 h-10 bg-teal-600 rounded-full flex items-center justify-center text-white font-medium hover:bg-teal-700" className="w-10 h-10 bg-teal-600 rounded-full flex items-center justify-center text-white font-medium hover:bg-teal-700"
> >
{user?.name?.charAt(0).toUpperCase() || 'U'} {user?.name?.charAt(0).toUpperCase() || "U"}
</button> </button>
</div> </div>
</div> </div>

View File

@@ -1,6 +1,18 @@
import React from 'react'; import React from "react";
import { LayoutDashboard, Users, Briefcase, CalendarCheck, DollarSign, ClipboardList, ArrowRightLeft, FileSpreadsheet, Scale, Eye, Layers } from 'lucide-react'; import {
import { useAuth } from '../../contexts/AuthContext'; ArrowRightLeft,
Briefcase,
CalendarCheck,
ClipboardList,
DollarSign,
Eye,
FileSpreadsheet,
Layers,
LayoutDashboard,
Scale,
Users,
} from "lucide-react";
import { useAuth } from "../../contexts/AuthContext.tsx";
interface SidebarItemProps { interface SidebarItemProps {
icon: React.ElementType; icon: React.ElementType;
@@ -9,13 +21,15 @@ interface SidebarItemProps {
onClick: () => void; onClick: () => void;
} }
const SidebarItem: React.FC<SidebarItemProps> = ({ icon: Icon, label, active, onClick }) => ( const SidebarItem: React.FC<SidebarItemProps> = (
{ icon: Icon, label, active, onClick },
) => (
<button <button
onClick={onClick} onClick={onClick}
className={`w-full flex items-center space-x-3 px-6 py-4 cursor-pointer transition-colors duration-200 outline-none focus:outline-none ${ className={`w-full flex items-center space-x-3 px-6 py-4 cursor-pointer transition-colors duration-200 outline-none focus:outline-none ${
active active
? 'bg-blue-900 border-l-4 border-blue-400 text-white' ? "bg-blue-900 border-l-4 border-blue-400 text-white"
: 'text-gray-400 hover:bg-gray-800 hover:text-white border-l-4 border-transparent' : "text-gray-400 hover:bg-gray-800 hover:text-white border-l-4 border-transparent"
}`} }`}
> >
<Icon size={20} /> <Icon size={20} />
@@ -30,10 +44,10 @@ interface SidebarProps {
export const Sidebar: React.FC<SidebarProps> = ({ activePage, onNavigate }) => { export const Sidebar: React.FC<SidebarProps> = ({ activePage, onNavigate }) => {
const { user } = useAuth(); const { user } = useAuth();
const isSuperAdmin = user?.role === 'SuperAdmin'; const isSuperAdmin = user?.role === "SuperAdmin";
const isSupervisor = user?.role === 'Supervisor'; const isSupervisor = user?.role === "Supervisor";
const isContractor = user?.role === 'Contractor'; const isContractor = user?.role === "Contractor";
const isEmployee = user?.role === 'Employee'; const isEmployee = user?.role === "Employee";
// Role-based access // Role-based access
const canManageUsers = isSuperAdmin || isSupervisor; const canManageUsers = isSuperAdmin || isSupervisor;
@@ -49,7 +63,9 @@ export const Sidebar: React.FC<SidebarProps> = ({ activePage, onNavigate }) => {
<ClipboardList size={24} className="text-white" /> <ClipboardList size={24} className="text-white" />
</div> </div>
<div> <div>
<h1 className="text-white text-lg font-bold tracking-wide">Work Allocation</h1> <h1 className="text-white text-lg font-bold tracking-wide">
Work Allocation
</h1>
<p className="text-gray-400 text-xs">Management System</p> <p className="text-gray-400 text-xs">Management System</p>
</div> </div>
</div> </div>
@@ -59,8 +75,8 @@ export const Sidebar: React.FC<SidebarProps> = ({ activePage, onNavigate }) => {
<SidebarItem <SidebarItem
icon={LayoutDashboard} icon={LayoutDashboard}
label="Dashboard" label="Dashboard"
active={activePage === 'dashboard'} active={activePage === "dashboard"}
onClick={() => onNavigate('dashboard')} onClick={() => onNavigate("dashboard")}
/> />
{/* User Management - SuperAdmin and Supervisor only */} {/* User Management - SuperAdmin and Supervisor only */}
@@ -68,8 +84,8 @@ export const Sidebar: React.FC<SidebarProps> = ({ activePage, onNavigate }) => {
<SidebarItem <SidebarItem
icon={Users} icon={Users}
label="User Management" label="User Management"
active={activePage === 'users'} active={activePage === "users"}
onClick={() => onNavigate('users')} onClick={() => onNavigate("users")}
/> />
)} )}
@@ -78,8 +94,8 @@ export const Sidebar: React.FC<SidebarProps> = ({ activePage, onNavigate }) => {
<SidebarItem <SidebarItem
icon={Briefcase} icon={Briefcase}
label="Work Allocation" label="Work Allocation"
active={activePage === 'allocation'} active={activePage === "allocation"}
onClick={() => onNavigate('allocation')} onClick={() => onNavigate("allocation")}
/> />
)} )}
@@ -88,8 +104,8 @@ export const Sidebar: React.FC<SidebarProps> = ({ activePage, onNavigate }) => {
<SidebarItem <SidebarItem
icon={CalendarCheck} icon={CalendarCheck}
label="Attendance" label="Attendance"
active={activePage === 'attendance'} active={activePage === "attendance"}
onClick={() => onNavigate('attendance')} onClick={() => onNavigate("attendance")}
/> />
)} )}
@@ -98,8 +114,8 @@ export const Sidebar: React.FC<SidebarProps> = ({ activePage, onNavigate }) => {
<SidebarItem <SidebarItem
icon={DollarSign} icon={DollarSign}
label="Contractor Rates" label="Contractor Rates"
active={activePage === 'rates'} active={activePage === "rates"}
onClick={() => onNavigate('rates')} onClick={() => onNavigate("rates")}
/> />
)} )}
@@ -108,8 +124,8 @@ export const Sidebar: React.FC<SidebarProps> = ({ activePage, onNavigate }) => {
<SidebarItem <SidebarItem
icon={ArrowRightLeft} icon={ArrowRightLeft}
label="Employee Swap" label="Employee Swap"
active={activePage === 'swaps'} active={activePage === "swaps"}
onClick={() => onNavigate('swaps')} onClick={() => onNavigate("swaps")}
/> />
)} )}
@@ -118,8 +134,8 @@ export const Sidebar: React.FC<SidebarProps> = ({ activePage, onNavigate }) => {
<SidebarItem <SidebarItem
icon={FileSpreadsheet} icon={FileSpreadsheet}
label="Reports" label="Reports"
active={activePage === 'reports'} active={activePage === "reports"}
onClick={() => onNavigate('reports')} onClick={() => onNavigate("reports")}
/> />
)} )}
@@ -128,8 +144,8 @@ export const Sidebar: React.FC<SidebarProps> = ({ activePage, onNavigate }) => {
<SidebarItem <SidebarItem
icon={Scale} icon={Scale}
label="Standard Rates" label="Standard Rates"
active={activePage === 'standard-rates'} active={activePage === "standard-rates"}
onClick={() => onNavigate('standard-rates')} onClick={() => onNavigate("standard-rates")}
/> />
)} )}
@@ -138,8 +154,8 @@ export const Sidebar: React.FC<SidebarProps> = ({ activePage, onNavigate }) => {
<SidebarItem <SidebarItem
icon={Eye} icon={Eye}
label="All Rates" label="All Rates"
active={activePage === 'all-rates'} active={activePage === "all-rates"}
onClick={() => onNavigate('all-rates')} onClick={() => onNavigate("all-rates")}
/> />
)} )}
@@ -148,22 +164,31 @@ export const Sidebar: React.FC<SidebarProps> = ({ activePage, onNavigate }) => {
<SidebarItem <SidebarItem
icon={Layers} icon={Layers}
label="Activities" label="Activities"
active={activePage === 'activities'} active={activePage === "activities"}
onClick={() => onNavigate('activities')} onClick={() => onNavigate("activities")}
/> />
)} )}
</nav> </nav>
{/* Role indicator at bottom */} {/* Role indicator at bottom */}
<div className="p-4 border-t border-gray-700"> <div className="p-4 border-t border-gray-700">
<div className="text-xs text-gray-500 uppercase tracking-wide mb-1">Logged in as</div> <div className="text-xs text-gray-500 uppercase tracking-wide mb-1">
<div className={`text-sm font-medium ${ Logged in as
isSuperAdmin ? 'text-purple-400' : </div>
isSupervisor ? 'text-blue-400' : <div
isContractor ? 'text-orange-400' : className={`text-sm font-medium ${
isEmployee ? 'text-green-400' : 'text-gray-400' isSuperAdmin
}`}> ? "text-purple-400"
{user?.role || 'Unknown'} : isSupervisor
? "text-blue-400"
: isContractor
? "text-orange-400"
: isEmployee
? "text-green-400"
: "text-gray-400"
}`}
>
{user?.role || "Unknown"}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,40 +1,45 @@
import React, { ReactNode, ButtonHTMLAttributes } from 'react'; import React, { ButtonHTMLAttributes, ReactNode } from "react";
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> { interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
children: ReactNode; children: ReactNode;
variant?: 'primary' | 'secondary' | 'danger' | 'ghost'; variant?: "primary" | "secondary" | "danger" | "ghost";
size?: 'sm' | 'md' | 'lg'; size?: "sm" | "md" | "lg";
fullWidth?: boolean; fullWidth?: boolean;
} }
export const Button: React.FC<ButtonProps> = ({ export const Button: React.FC<ButtonProps> = ({
children, children,
variant = 'primary', variant = "primary",
size = 'md', size = "md",
fullWidth = false, fullWidth = false,
className = '', className = "",
...props ...props
}) => { }) => {
const baseStyles = 'inline-flex items-center justify-center font-medium rounded-md transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2'; const baseStyles =
"inline-flex items-center justify-center font-medium rounded-md transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2";
const variantStyles = { const variantStyles = {
primary: 'bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500', primary: "bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500",
secondary: 'bg-gray-200 text-gray-800 hover:bg-gray-300 focus:ring-gray-500', secondary:
danger: 'bg-red-600 text-white hover:bg-red-700 focus:ring-red-500', "bg-gray-200 text-gray-800 hover:bg-gray-300 focus:ring-gray-500",
ghost: 'bg-transparent border border-gray-300 text-gray-700 hover:bg-gray-50 focus:ring-gray-500', danger: "bg-red-600 text-white hover:bg-red-700 focus:ring-red-500",
ghost:
"bg-transparent border border-gray-300 text-gray-700 hover:bg-gray-50 focus:ring-gray-500",
}; };
const sizeStyles = { const sizeStyles = {
sm: 'px-3 py-1.5 text-sm', sm: "px-3 py-1.5 text-sm",
md: 'px-4 py-2 text-sm', md: "px-4 py-2 text-sm",
lg: 'px-6 py-3 text-base', lg: "px-6 py-3 text-base",
}; };
const widthStyle = fullWidth ? 'w-full' : ''; const widthStyle = fullWidth ? "w-full" : "";
return ( return (
<button <button
className={`${baseStyles} ${variantStyles[variant]} ${sizeStyles[size]} ${widthStyle} ${className}`} className={`${baseStyles} ${variantStyles[variant]} ${
sizeStyles[size]
} ${widthStyle} ${className}`}
{...props} {...props}
> >
{children} {children}

View File

@@ -1,11 +1,11 @@
import React, { ReactNode } from 'react'; import React, { ReactNode } from "react";
interface CardProps { interface CardProps {
children: ReactNode; children: ReactNode;
className?: string; className?: string;
} }
export const Card: React.FC<CardProps> = ({ children, className = '' }) => { export const Card: React.FC<CardProps> = ({ children, className = "" }) => {
return ( return (
<div className={`bg-white rounded-lg shadow-sm ${className}`}> <div className={`bg-white rounded-lg shadow-sm ${className}`}>
{children} {children}
@@ -19,9 +19,13 @@ interface CardHeaderProps {
className?: string; className?: string;
} }
export const CardHeader: React.FC<CardHeaderProps> = ({ title, action, className = '' }) => { export const CardHeader: React.FC<CardHeaderProps> = (
{ title, action, className = "" },
) => {
return ( return (
<div className={`flex justify-between items-center p-6 border-b border-gray-200 ${className}`}> <div
className={`flex justify-between items-center p-6 border-b border-gray-200 ${className}`}
>
<h2 className="text-xl font-semibold text-gray-800">{title}</h2> <h2 className="text-xl font-semibold text-gray-800">{title}</h2>
{action && <div>{action}</div>} {action && <div>{action}</div>}
</div> </div>
@@ -33,6 +37,8 @@ interface CardContentProps {
className?: string; className?: string;
} }
export const CardContent: React.FC<CardContentProps> = ({ children, className = '' }) => { export const CardContent: React.FC<CardContentProps> = (
{ children, className = "" },
) => {
return <div className={`p-6 ${className}`}>{children}</div>; return <div className={`p-6 ${className}`}>{children}</div>;
}; };

View File

@@ -1,5 +1,5 @@
import React, { InputHTMLAttributes, useState } from 'react'; import React, { InputHTMLAttributes, useState } from "react";
import { Eye, EyeOff } from 'lucide-react'; import { Eye, EyeOff } from "lucide-react";
interface InputProps extends InputHTMLAttributes<HTMLInputElement> { interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
label?: string; label?: string;
@@ -7,7 +7,9 @@ interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
required?: boolean; required?: boolean;
} }
export const Input: React.FC<InputProps> = ({ label, error, required, className = '', disabled, ...props }) => { export const Input: React.FC<InputProps> = (
{ label, error, required, className = "", disabled, ...props },
) => {
return ( return (
<div className="w-full"> <div className="w-full">
{label && ( {label && (
@@ -17,8 +19,10 @@ export const Input: React.FC<InputProps> = ({ label, error, required, className
)} )}
<input <input
className={`w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent ${ className={`w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent ${
error ? 'border-red-500' : '' error ? "border-red-500" : ""
} ${disabled ? 'bg-gray-100 text-gray-600 cursor-not-allowed' : ''} ${className}`} } ${
disabled ? "bg-gray-100 text-gray-600 cursor-not-allowed" : ""
} ${className}`}
disabled={disabled} disabled={disabled}
{...props} {...props}
/> />
@@ -27,13 +31,16 @@ export const Input: React.FC<InputProps> = ({ label, error, required, className
); );
}; };
interface PasswordInputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'type'> { interface PasswordInputProps
extends Omit<InputHTMLAttributes<HTMLInputElement>, "type"> {
label?: string; label?: string;
error?: string; error?: string;
required?: boolean; required?: boolean;
} }
export const PasswordInput: React.FC<PasswordInputProps> = ({ label, error, required, className = '', disabled, ...props }) => { export const PasswordInput: React.FC<PasswordInputProps> = (
{ label, error, required, className = "", disabled, ...props },
) => {
const [showPassword, setShowPassword] = useState(false); const [showPassword, setShowPassword] = useState(false);
return ( return (
@@ -45,10 +52,12 @@ export const PasswordInput: React.FC<PasswordInputProps> = ({ label, error, requ
)} )}
<div className="relative"> <div className="relative">
<input <input
type={showPassword ? 'text' : 'password'} type={showPassword ? "text" : "password"}
className={`w-full px-4 py-2 pr-10 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent ${ className={`w-full px-4 py-2 pr-10 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent ${
error ? 'border-red-500' : '' error ? "border-red-500" : ""
} ${disabled ? 'bg-gray-100 text-gray-600 cursor-not-allowed' : ''} ${className}`} } ${
disabled ? "bg-gray-100 text-gray-600 cursor-not-allowed" : ""
} ${className}`}
disabled={disabled} disabled={disabled}
{...props} {...props}
/> />
@@ -73,7 +82,9 @@ interface SelectProps extends InputHTMLAttributes<HTMLSelectElement> {
options: { value: string; label: string }[]; options: { value: string; label: string }[];
} }
export const Select: React.FC<SelectProps> = ({ label, error, required, options, className = '', disabled, ...props }) => { export const Select: React.FC<SelectProps> = (
{ label, error, required, options, className = "", disabled, ...props },
) => {
return ( return (
<div className="w-full"> <div className="w-full">
{label && ( {label && (
@@ -83,8 +94,10 @@ export const Select: React.FC<SelectProps> = ({ label, error, required, options,
)} )}
<select <select
className={`w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent ${ className={`w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent ${
error ? 'border-red-500' : '' error ? "border-red-500" : ""
} ${disabled ? 'bg-gray-100 text-gray-600 cursor-not-allowed' : ''} ${className}`} } ${
disabled ? "bg-gray-100 text-gray-600 cursor-not-allowed" : ""
} ${className}`}
disabled={disabled} disabled={disabled}
{...props} {...props}
> >
@@ -106,7 +119,9 @@ interface TextAreaProps extends InputHTMLAttributes<HTMLTextAreaElement> {
rows?: number; rows?: number;
} }
export const TextArea: React.FC<TextAreaProps> = ({ label, error, required, rows = 3, className = '', ...props }) => { export const TextArea: React.FC<TextAreaProps> = (
{ label, error, required, rows = 3, className = "", ...props },
) => {
return ( return (
<div className="w-full"> <div className="w-full">
{label && ( {label && (
@@ -117,7 +132,7 @@ export const TextArea: React.FC<TextAreaProps> = ({ label, error, required, rows
<textarea <textarea
rows={rows} rows={rows}
className={`w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent ${ className={`w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent ${
error ? 'border-red-500' : '' error ? "border-red-500" : ""
} ${className}`} } ${className}`}
{...props} {...props}
/> />

View File

@@ -1,11 +1,11 @@
import React, { ReactNode } from 'react'; import React, { ReactNode } from "react";
interface TableProps { interface TableProps {
children: ReactNode; children: ReactNode;
className?: string; className?: string;
} }
export const Table: React.FC<TableProps> = ({ children, className = '' }) => { export const Table: React.FC<TableProps> = ({ children, className = "" }) => {
return ( return (
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className={`w-full ${className}`}>{children}</table> <table className={`w-full ${className}`}>{children}</table>
@@ -39,11 +39,15 @@ interface TableRowProps {
className?: string; className?: string;
} }
export const TableRow: React.FC<TableRowProps> = ({ children, onClick, className = '' }) => { export const TableRow: React.FC<TableRowProps> = (
{ children, onClick, className = "" },
) => {
return ( return (
<tr <tr
onClick={onClick} onClick={onClick}
className={`border-b border-gray-100 hover:bg-gray-50 ${onClick ? 'cursor-pointer' : ''} ${className}`} className={`border-b border-gray-100 hover:bg-gray-50 ${
onClick ? "cursor-pointer" : ""
} ${className}`}
> >
{children} {children}
</tr> </tr>
@@ -55,9 +59,13 @@ interface TableHeadProps {
className?: string; className?: string;
} }
export const TableHead: React.FC<TableHeadProps> = ({ children, className = '' }) => { export const TableHead: React.FC<TableHeadProps> = (
{ children, className = "" },
) => {
return ( return (
<th className={`text-left py-3 px-4 text-sm font-medium text-gray-600 ${className}`}> <th
className={`text-left py-3 px-4 text-sm font-medium text-gray-600 ${className}`}
>
{children} {children}
</th> </th>
); );
@@ -68,6 +76,12 @@ interface TableCellProps {
className?: string; className?: string;
} }
export const TableCell: React.FC<TableCellProps> = ({ children, className = '' }) => { export const TableCell: React.FC<TableCellProps> = (
return <td className={`py-3 px-4 text-sm text-gray-700 ${className}`}>{children}</td>; { children, className = "" },
) => {
return (
<td className={`py-3 px-4 text-sm text-gray-700 ${className}`}>
{children}
</td>
);
}; };

View File

@@ -1,76 +1,60 @@
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react'; import React, { ReactNode, useState } from "react";
import { api } from '../services/api'; import { api } from "../services/api.ts";
import type { User } from '../types'; import type { User } from "../types.ts";
import { AuthContext } from "./authContext.ts";
interface AuthContextType { // Re-export useAuth for convenience
user: User | null; export { useAuth } from "./authContext.ts";
isAuthenticated: boolean;
isLoading: boolean;
login: (username: string, password: string) => Promise<void>;
logout: () => void;
updateUser: (user: User) => void;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export const useAuth = () => {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within AuthProvider');
}
return context;
};
interface AuthProviderProps { interface AuthProviderProps {
children: ReactNode; children: ReactNode;
} }
export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => { // Helper to get initial user from localStorage
const [user, setUser] = useState<User | null>(null); const getInitialUser = (): User | null => {
const [isLoading, setIsLoading] = useState(true); const token = localStorage.getItem("token");
const storedUser = localStorage.getItem("user");
useEffect(() => {
// Check for existing session
const token = localStorage.getItem('token');
const storedUser = localStorage.getItem('user');
if (token && storedUser) { if (token && storedUser) {
try { try {
const parsedUser = JSON.parse(storedUser); return JSON.parse(storedUser);
setUser(parsedUser);
} catch (error) { } catch (error) {
console.error('Failed to parse stored user:', error); console.error("Failed to parse stored user:", error);
localStorage.removeItem('user'); localStorage.removeItem("user");
localStorage.removeItem('token'); localStorage.removeItem("token");
} }
} }
return null;
};
setIsLoading(false); export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
}, []); const [user, setUser] = useState<User | null>(getInitialUser);
const [isLoading] = useState(false);
const login = async (username: string, password: string) => { const login = async (username: string, password: string) => {
try { try {
const response = await api.login(username, password); const response = await api.login(username, password);
// Store token and user // Store token and user
localStorage.setItem('token', response.token); localStorage.setItem("token", response.token);
localStorage.setItem('user', JSON.stringify(response.user)); localStorage.setItem("user", JSON.stringify(response.user));
setUser(response.user); setUser(response.user);
} catch (error) { } catch (error) {
console.error('Login failed:', error); console.error("Login failed:", error);
throw error; throw error;
} }
}; };
const logout = () => { const logout = () => {
localStorage.removeItem('token'); localStorage.removeItem("token");
localStorage.removeItem('user'); localStorage.removeItem("user");
setUser(null); setUser(null);
}; };
const updateUser = (updatedUser: User) => { const updateUser = (updatedUser: User) => {
setUser(updatedUser); setUser(updatedUser);
localStorage.setItem('user', JSON.stringify(updatedUser)); localStorage.setItem("user", JSON.stringify(updatedUser));
}; };
return ( return (

View File

@@ -0,0 +1,23 @@
import { createContext, useContext } from "react";
import type { User } from "../types";
export interface AuthContextType {
user: User | null;
isAuthenticated: boolean;
isLoading: boolean;
login: (username: string, password: string) => Promise<void>;
logout: () => void;
updateUser: (user: User) => void;
}
export const AuthContext = createContext<AuthContextType | undefined>(
undefined,
);
export const useAuth = () => {
const context = useContext(AuthContext);
if (!context) {
throw new Error("useAuth must be used within AuthProvider");
}
return context;
};

View File

@@ -1,6 +1,6 @@
import { useState, useEffect, useCallback } from 'react'; import { useCallback, useEffect, useState } from "react";
import { api } from '../services/api'; import { api } from "../services/api.ts";
import { Activity } from '../types'; import { Activity } from "../types.ts";
export const useActivities = (subDepartmentId?: string | number) => { export const useActivities = (subDepartmentId?: string | number) => {
const [activities, setActivities] = useState<Activity[]>([]); const [activities, setActivities] = useState<Activity[]>([]);
@@ -18,7 +18,9 @@ export const useActivities = (subDepartmentId?: string | number) => {
const data = await api.getActivities(params); const data = await api.getActivities(params);
setActivities(data); setActivities(data);
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : 'Failed to fetch activities'); setError(
err instanceof Error ? err.message : "Failed to fetch activities",
);
setActivities([]); setActivities([]);
} finally { } finally {
setLoading(false); setLoading(false);
@@ -46,10 +48,14 @@ export const useActivitiesByDepartment = (departmentId?: string | number) => {
setLoading(true); setLoading(true);
setError(null); setError(null);
try { try {
const data = await api.getActivities({ departmentId: Number(departmentId) }); const data = await api.getActivities({
departmentId: Number(departmentId),
});
setActivities(data); setActivities(data);
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : 'Failed to fetch activities'); setError(
err instanceof Error ? err.message : "Failed to fetch activities",
);
setActivities([]); setActivities([]);
} finally { } finally {
setLoading(false); setLoading(false);

View File

@@ -1,6 +1,6 @@
import { useState, useEffect } from 'react'; import { useCallback, useEffect, useState } from "react";
import { api } from '../services/api'; import { api } from "../services/api.ts";
import type { Department, SubDepartment } from '../types'; import type { Department, SubDepartment } from "../types.ts";
export const useDepartments = () => { export const useDepartments = () => {
const [departments, setDepartments] = useState<Department[]>([]); const [departments, setDepartments] = useState<Department[]>([]);
@@ -13,8 +13,8 @@ export const useDepartments = () => {
try { try {
const data = await api.getDepartments(); const data = await api.getDepartments();
setDepartments(data); setDepartments(data);
} catch (err: any) { } catch (err: never) {
setError(err.message || 'Failed to fetch departments'); setError(err.message || "Failed to fetch departments");
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -37,7 +37,7 @@ export const useSubDepartments = (departmentId?: string) => {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const fetchSubDepartments = async () => { const fetchSubDepartments = useCallback(async () => {
if (!departmentId) { if (!departmentId) {
setSubDepartments([]); setSubDepartments([]);
return; return;
@@ -48,16 +48,16 @@ export const useSubDepartments = (departmentId?: string) => {
try { try {
const data = await api.getSubDepartments(parseInt(departmentId)); const data = await api.getSubDepartments(parseInt(departmentId));
setSubDepartments(data); setSubDepartments(data);
} catch (err: any) { } catch (err: never) {
setError(err.message || 'Failed to fetch subdepartments'); setError(err.message || "Failed to fetch subdepartments");
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; }, [departmentId]);
useEffect(() => { useEffect(() => {
fetchSubDepartments(); fetchSubDepartments();
}, [departmentId]); }, [fetchSubDepartments]);
return { return {
subDepartments, subDepartments,

View File

@@ -1,56 +1,61 @@
import { useState, useEffect } from 'react'; import { useCallback, useEffect, useState } from "react";
import { api } from '../services/api'; import { api } from "../services/api";
import type { User } from '../types'; import type { User } from "../types";
export const useEmployees = (filters?: { role?: string; departmentId?: number }) => { export const useEmployees = (
filters?: { role?: string; departmentId?: number },
) => {
const [employees, setEmployees] = useState<User[]>([]); const [employees, setEmployees] = useState<User[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const fetchEmployees = async () => { const fetchEmployees = useCallback(async () => {
setLoading(true); setLoading(true);
setError(null); setError(null);
try { try {
const data = await api.getUsers(filters); const data = await api.getUsers(filters);
setEmployees(data); setEmployees(data);
} catch (err: any) { } catch (err: never) {
setError(err.message || 'Failed to fetch employees'); setError(err.message || "Failed to fetch employees");
console.error('Failed to fetch employees:', err); console.error("Failed to fetch employees:", err);
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; }, [filters]);
useEffect(() => { useEffect(() => {
fetchEmployees(); fetchEmployees();
}, [JSON.stringify(filters)]); }, [fetchEmployees]);
const createEmployee = async (data: any) => { const createEmployee = async (data: Omit<User, "id">) => {
setLoading(true); setLoading(true);
setError(null); setError(null);
try { try {
const newEmployee = await api.createUser(data); const newEmployee = await api.createUser(data);
await fetchEmployees(); // Refresh list await fetchEmployees(); // Refresh list
return newEmployee; return newEmployee;
} catch (err: any) { } catch (err: never) {
setError(err.message || 'Failed to create employee'); setError(err.message || "Failed to create employee");
console.error('Failed to create employee:', err); console.error("Failed to create employee:", err);
throw err; throw err;
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; };
const updateEmployee = async (id: number, data: any) => { const updateEmployee = async (
id: number,
data: Partial<Omit<User, "id">>,
) => {
setLoading(true); setLoading(true);
setError(null); setError(null);
try { try {
const updated = await api.updateUser(id, data); const updated = await api.updateUser(id, data);
await fetchEmployees(); // Refresh list await fetchEmployees(); // Refresh list
return updated; return updated;
} catch (err: any) { } catch (err: never) {
setError(err.message || 'Failed to update employee'); setError(err.message || "Failed to update employee");
console.error('Failed to update employee:', err); console.error("Failed to update employee:", err);
throw err; throw err;
} finally { } finally {
setLoading(false); setLoading(false);
@@ -63,9 +68,9 @@ export const useEmployees = (filters?: { role?: string; departmentId?: number })
try { try {
await api.deleteUser(id); await api.deleteUser(id);
await fetchEmployees(); // Refresh list await fetchEmployees(); // Refresh list
} catch (err: any) { } catch (err: never) {
setError(err.message || 'Failed to delete employee'); setError(err.message || "Failed to delete employee");
console.error('Failed to delete employee:', err); console.error("Failed to delete employee:", err);
throw err; throw err;
} finally { } finally {
setLoading(false); setLoading(false);

View File

@@ -1,56 +1,66 @@
import { useState, useEffect } from 'react'; import { useCallback, useEffect, useState } from "react";
import { api } from '../services/api'; import { api } from "../services/api.ts";
import type { WorkAllocation } from '../types'; import type { WorkAllocation } from "../types.ts";
export const useWorkAllocations = (filters?: { employeeId?: number; status?: string; departmentId?: number }) => { export const useWorkAllocations = (
filters?: { employeeId?: number; status?: string; departmentId?: number },
) => {
const [allocations, setAllocations] = useState<WorkAllocation[]>([]); const [allocations, setAllocations] = useState<WorkAllocation[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const fetchAllocations = async () => { const fetchAllocations = useCallback(async () => {
setLoading(true); setLoading(true);
setError(null); setError(null);
try { try {
const data = await api.getWorkAllocations(filters); const data = await api.getWorkAllocations(filters);
setAllocations(data); setAllocations(data);
} catch (err: any) { } catch (err: never) {
setError(err.message || 'Failed to fetch work allocations'); setError(err.message || "Failed to fetch work allocations");
console.error('Failed to fetch work allocations:', err); console.error("Failed to fetch work allocations:", err);
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; }, [filters]);
useEffect(() => { useEffect(() => {
fetchAllocations(); fetchAllocations();
}, [JSON.stringify(filters)]); }, [fetchAllocations]);
const createAllocation = async (data: any) => { const createAllocation = async (data: Omit<WorkAllocation, "id">) => {
setLoading(true); setLoading(true);
setError(null); setError(null);
try { try {
const newAllocation = await api.createWorkAllocation(data); const newAllocation = await api.createWorkAllocation(data);
await fetchAllocations(); // Refresh list await fetchAllocations(); // Refresh list
return newAllocation; return newAllocation;
} catch (err: any) { } catch (err: never) {
setError(err.message || 'Failed to create work allocation'); setError(err.message || "Failed to create work allocation");
console.error('Failed to create work allocation:', err); console.error("Failed to create work allocation:", err);
throw err; throw err;
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; };
const updateAllocation = async (id: number, status: string, completionDate?: string) => { const updateAllocation = async (
id: number,
status: string,
completionDate?: string,
) => {
setLoading(true); setLoading(true);
setError(null); setError(null);
try { try {
const updated = await api.updateWorkAllocationStatus(id, status, completionDate); const updated = await api.updateWorkAllocationStatus(
id,
status,
completionDate,
);
await fetchAllocations(); // Refresh list await fetchAllocations(); // Refresh list
return updated; return updated;
} catch (err: any) { } catch (err: never) {
setError(err.message || 'Failed to update work allocation'); setError(err.message || "Failed to update work allocation");
console.error('Failed to update work allocation:', err); console.error("Failed to update work allocation:", err);
throw err; throw err;
} finally { } finally {
setLoading(false); setLoading(false);
@@ -63,9 +73,9 @@ export const useWorkAllocations = (filters?: { employeeId?: number; status?: str
try { try {
await api.deleteWorkAllocation(id); await api.deleteWorkAllocation(id);
await fetchAllocations(); // Refresh list await fetchAllocations(); // Refresh list
} catch (err: any) { } catch (err: never) {
setError(err.message || 'Failed to delete work allocation'); setError(err.message || "Failed to delete work allocation");
console.error('Failed to delete work allocation:', err); console.error("Failed to delete work allocation:", err);
throw err; throw err;
} finally { } finally {
setLoading(false); setLoading(false);

View File

@@ -1,9 +1,9 @@
import React from 'react'; import React from "react";
import { createRoot } from 'react-dom/client'; import { createRoot } from "react-dom/client";
import './index.css'; import "./index.css";
import App from './App'; import App from "./App";
createRoot(document.getElementById('root')!).render( createRoot(document.getElementById("root")!).render(
<React.StrictMode> <React.StrictMode>
<App /> <App />
</React.StrictMode>, </React.StrictMode>,

View File

@@ -1,43 +1,62 @@
import React, { useState, useEffect } from 'react'; import React, { useEffect, useState } from "react";
import { Plus, RefreshCw, Trash2, Layers, Activity as ActivityIcon } from 'lucide-react'; import {
import { Card, CardHeader, CardContent } from '../components/ui/Card'; Activity as ActivityIcon,
import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from '../components/ui/Table'; Layers,
import { Button } from '../components/ui/Button'; Plus,
import { Input, Select } from '../components/ui/Input'; RefreshCw,
import { useDepartments, useSubDepartments } from '../hooks/useDepartments'; Trash2,
import { useActivitiesByDepartment } from '../hooks/useActivities'; } from "lucide-react";
import { useAuth } from '../contexts/AuthContext'; import { Card, CardContent, CardHeader } from "../components/ui/Card.tsx";
import { api } from '../services/api'; import {
import { SubDepartment, Activity } from '../types'; 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 = () => { export const ActivitiesPage: React.FC = () => {
const [activeTab, setActiveTab] = useState<'subDepartments' | 'activities'>('subDepartments'); const [activeTab, setActiveTab] = useState<"subDepartments" | "activities">(
"subDepartments",
);
const { user } = useAuth(); const { user } = useAuth();
const { departments } = useDepartments(); const { departments } = useDepartments();
// Role-based access // Role-based access
const isSupervisor = user?.role === 'Supervisor'; const isSupervisor = user?.role === "Supervisor";
const isSuperAdmin = user?.role === 'SuperAdmin'; const isSuperAdmin = user?.role === "SuperAdmin";
const canAccess = isSupervisor || isSuperAdmin; const canAccess = isSupervisor || isSuperAdmin;
// Department selection - supervisors are locked to their department // 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 // Get sub-departments and activities for selected department
const { subDepartments, refresh: refreshSubDepts } = useSubDepartments(selectedDeptId); const { subDepartments, refresh: refreshSubDepts } = useSubDepartments(
const { activities, refresh: refreshActivities } = useActivitiesByDepartment(selectedDeptId); selectedDeptId,
);
const { activities, refresh: refreshActivities } = useActivitiesByDepartment(
selectedDeptId,
);
// Form states // Form states
const [subDeptForm, setSubDeptForm] = useState({ name: '' }); const [subDeptForm, setSubDeptForm] = useState({ name: "" });
const [activityForm, setActivityForm] = useState({ const [activityForm, setActivityForm] = useState({
subDepartmentId: '', subDepartmentId: "",
name: '', name: "",
unitOfMeasurement: 'Per Bag' as 'Per Bag' | 'Fixed Rate-Per Person' unitOfMeasurement: "Per Bag" as "Per Bag" | "Fixed Rate-Per Person",
}); });
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState(''); const [error, setError] = useState("");
const [success, setSuccess] = useState(''); const [success, setSuccess] = useState("");
// Auto-select department for supervisors // Auto-select department for supervisors
useEffect(() => { useEffect(() => {
@@ -50,8 +69,8 @@ export const ActivitiesPage: React.FC = () => {
useEffect(() => { useEffect(() => {
if (success || error) { if (success || error) {
const timer = setTimeout(() => { const timer = setTimeout(() => {
setSuccess(''); setSuccess("");
setError(''); setError("");
}, 3000); }, 3000);
return () => clearTimeout(timer); return () => clearTimeout(timer);
} }
@@ -59,44 +78,52 @@ export const ActivitiesPage: React.FC = () => {
const handleCreateSubDepartment = async () => { const handleCreateSubDepartment = async () => {
if (!subDeptForm.name.trim()) { if (!subDeptForm.name.trim()) {
setError('Sub-department name is required'); setError("Sub-department name is required");
return; return;
} }
if (!selectedDeptId) { if (!selectedDeptId) {
setError('Please select a department first'); setError("Please select a department first");
return; return;
} }
setLoading(true); setLoading(true);
setError(''); setError("");
try { try {
await api.createSubDepartment({ await api.createSubDepartment({
department_id: parseInt(selectedDeptId), department_id: parseInt(selectedDeptId),
name: subDeptForm.name.trim() name: subDeptForm.name.trim(),
}); });
setSuccess('Sub-department created successfully'); setSuccess("Sub-department created successfully");
setSubDeptForm({ name: '' }); setSubDeptForm({ name: "" });
refreshSubDepts(); refreshSubDepts();
} catch (err) { } 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 { } finally {
setLoading(false); setLoading(false);
} }
}; };
const handleDeleteSubDepartment = async (id: number) => { 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; return;
} }
setLoading(true); setLoading(true);
try { try {
await api.deleteSubDepartment(id); await api.deleteSubDepartment(id);
setSuccess('Sub-department deleted successfully'); setSuccess("Sub-department deleted successfully");
refreshSubDepts(); refreshSubDepts();
refreshActivities(); refreshActivities();
} catch (err) { } 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 { } finally {
setLoading(false); setLoading(false);
} }
@@ -104,44 +131,52 @@ export const ActivitiesPage: React.FC = () => {
const handleCreateActivity = async () => { const handleCreateActivity = async () => {
if (!activityForm.name.trim()) { if (!activityForm.name.trim()) {
setError('Activity name is required'); setError("Activity name is required");
return; return;
} }
if (!activityForm.subDepartmentId) { if (!activityForm.subDepartmentId) {
setError('Please select a sub-department'); setError("Please select a sub-department");
return; return;
} }
setLoading(true); setLoading(true);
setError(''); setError("");
try { try {
await api.createActivity({ await api.createActivity({
sub_department_id: parseInt(activityForm.subDepartmentId), sub_department_id: parseInt(activityForm.subDepartmentId),
name: activityForm.name.trim(), 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(); refreshActivities();
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : 'Failed to create activity'); setError(
err instanceof Error ? err.message : "Failed to create activity",
);
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; };
const handleDeleteActivity = async (id: number) => { 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; return;
} }
setLoading(true); setLoading(true);
try { try {
await api.deleteActivity(id); await api.deleteActivity(id);
setSuccess('Activity deleted successfully'); setSuccess("Activity deleted successfully");
refreshActivities(); refreshActivities();
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : 'Failed to delete activity'); setError(
err instanceof Error ? err.message : "Failed to delete activity",
);
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -152,14 +187,17 @@ export const ActivitiesPage: React.FC = () => {
<div className="p-6"> <div className="p-6">
<Card> <Card>
<CardContent> <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> </CardContent>
</Card> </Card>
</div> </div>
); );
} }
const selectedDeptName = departments.find(d => d.id === parseInt(selectedDeptId))?.name || ''; const selectedDeptName =
departments.find((d) => d.id === parseInt(selectedDeptId))?.name || "";
return ( return (
<div className="p-6 space-y-6"> <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 justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Layers className="h-6 w-6 text-blue-600" /> <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> </div>
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
onClick={() => { refreshSubDepts(); refreshActivities(); }} onClick={() => {
refreshSubDepts();
refreshActivities();
}}
disabled={loading} 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 Refresh
</Button> </Button>
</div> </div>
@@ -185,20 +230,30 @@ export const ActivitiesPage: React.FC = () => {
<CardContent> <CardContent>
{/* Department Selection */} {/* Department Selection */}
<div className="mb-6"> <div className="mb-6">
{isSupervisor ? ( {isSupervisor
? (
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1">Department</label> <label className="block text-sm font-medium text-gray-700 mb-1">
<Input value={selectedDeptName || 'Loading...'} disabled /> Department
<p className="text-xs text-gray-500 mt-1">As a supervisor, you can only manage your department's activities.</p> </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> </div>
) : ( )
: (
<Select <Select
label="Select Department" label="Select Department"
value={selectedDeptId} value={selectedDeptId}
onChange={(e) => setSelectedDeptId(e.target.value)} onChange={(e) => setSelectedDeptId(e.target.value)}
options={[ options={[
{ value: '', label: 'Select a Department' }, { value: "", label: "Select a Department" },
...departments.map(d => ({ value: String(d.id), label: d.name })) ...departments.map((d) => ({
value: String(d.id),
label: d.name,
})),
]} ]}
/> />
)} )}
@@ -220,22 +275,22 @@ export const ActivitiesPage: React.FC = () => {
<div className="border-b border-gray-200 mb-6"> <div className="border-b border-gray-200 mb-6">
<div className="flex space-x-8"> <div className="flex space-x-8">
<button <button
onClick={() => setActiveTab('subDepartments')} onClick={() => setActiveTab("subDepartments")}
className={`py-3 px-1 border-b-2 font-medium text-sm ${ className={`py-3 px-1 border-b-2 font-medium text-sm ${
activeTab === 'subDepartments' activeTab === "subDepartments"
? 'border-blue-500 text-blue-600' ? "border-blue-500 text-blue-600"
: 'border-transparent text-gray-500 hover:text-gray-700' : "border-transparent text-gray-500 hover:text-gray-700"
}`} }`}
> >
<Layers className="h-4 w-4 inline mr-2" /> <Layers className="h-4 w-4 inline mr-2" />
Sub-Departments ({subDepartments.length}) Sub-Departments ({subDepartments.length})
</button> </button>
<button <button
onClick={() => setActiveTab('activities')} onClick={() => setActiveTab("activities")}
className={`py-3 px-1 border-b-2 font-medium text-sm ${ className={`py-3 px-1 border-b-2 font-medium text-sm ${
activeTab === 'activities' activeTab === "activities"
? 'border-blue-500 text-blue-600' ? "border-blue-500 text-blue-600"
: 'border-transparent text-gray-500 hover:text-gray-700' : "border-transparent text-gray-500 hover:text-gray-700"
}`} }`}
> >
<ActivityIcon className="h-4 w-4 inline mr-2" /> <ActivityIcon className="h-4 w-4 inline mr-2" />
@@ -244,26 +299,37 @@ export const ActivitiesPage: React.FC = () => {
</div> </div>
</div> </div>
{!selectedDeptId ? ( {!selectedDeptId
<p className="text-gray-500 text-center py-8">Please select a department to manage sub-departments and activities.</p> ? (
) : ( <p className="text-gray-500 text-center py-8">
Please select a department to manage sub-departments and
activities.
</p>
)
: (
<> <>
{/* Sub-Departments Tab */} {/* Sub-Departments Tab */}
{activeTab === 'subDepartments' && ( {activeTab === "subDepartments" && (
<div className="space-y-6"> <div className="space-y-6">
{/* Create Sub-Department Form */} {/* Create Sub-Department Form */}
<div className="bg-gray-50 p-4 rounded-lg"> <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> <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 gap-4 items-end">
<div className="flex-1"> <div className="flex-1">
<Input <Input
label="Sub-Department Name" label="Sub-Department Name"
value={subDeptForm.name} value={subDeptForm.name}
onChange={(e) => setSubDeptForm({ name: e.target.value })} onChange={(e) =>
setSubDeptForm({ name: e.target.value })}
placeholder="e.g., Loading/Unloading, Destoner, Tank" placeholder="e.g., Loading/Unloading, Destoner, Tank"
/> />
</div> </div>
<Button onClick={handleCreateSubDepartment} disabled={loading}> <Button
onClick={handleCreateSubDepartment}
disabled={loading}
>
<Plus className="h-4 w-4 mr-2" /> <Plus className="h-4 w-4 mr-2" />
Add Sub-Department Add Sub-Department
</Button> </Button>
@@ -279,25 +345,38 @@ export const ActivitiesPage: React.FC = () => {
<TableHead className="text-right">Actions</TableHead> <TableHead className="text-right">Actions</TableHead>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{subDepartments.length === 0 ? ( {subDepartments.length === 0
? (
<TableRow> <TableRow>
<TableCell colSpan={4} className="text-center text-gray-500 py-8"> <TableCell
colSpan={4}
className="text-center text-gray-500 py-8"
>
No sub-departments found. Create one above. No sub-departments found. Create one above.
</TableCell> </TableCell>
</TableRow> </TableRow>
) : ( )
: (
subDepartments.map((subDept: SubDepartment) => { subDepartments.map((subDept: SubDepartment) => {
const activityCount = activities.filter(a => a.sub_department_id === subDept.id).length; const activityCount = activities.filter((a) =>
a.sub_department_id === subDept.id
).length;
return ( return (
<TableRow key={subDept.id}> <TableRow key={subDept.id}>
<TableCell className="font-medium">{subDept.name}</TableCell> <TableCell className="font-medium">
{subDept.name}
</TableCell>
<TableCell>{activityCount}</TableCell> <TableCell>{activityCount}</TableCell>
<TableCell>{new Date(subDept.created_at).toLocaleDateString()}</TableCell> <TableCell>
{new Date(subDept.created_at)
.toLocaleDateString()}
</TableCell>
<TableCell className="text-right"> <TableCell className="text-right">
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
onClick={() => handleDeleteSubDepartment(subDept.id)} onClick={() =>
handleDeleteSubDepartment(subDept.id)}
className="text-red-600 hover:text-red-800" className="text-red-600 hover:text-red-800"
> >
<Trash2 className="h-4 w-4" /> <Trash2 className="h-4 w-4" />
@@ -313,40 +392,62 @@ export const ActivitiesPage: React.FC = () => {
)} )}
{/* Activities Tab */} {/* Activities Tab */}
{activeTab === 'activities' && ( {activeTab === "activities" && (
<div className="space-y-6"> <div className="space-y-6">
{/* Create Activity Form */} {/* Create Activity Form */}
<div className="bg-gray-50 p-4 rounded-lg"> <div className="bg-gray-50 p-4 rounded-lg">
<h3 className="text-md font-semibold text-gray-700 mb-4">Add New Activity</h3> <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"> <div className="grid grid-cols-1 md:grid-cols-4 gap-4 items-end">
<Select <Select
label="Sub-Department" label="Sub-Department"
value={activityForm.subDepartmentId} value={activityForm.subDepartmentId}
onChange={(e) => setActivityForm(prev => ({ ...prev, subDepartmentId: e.target.value }))} onChange={(e) =>
setActivityForm((prev) => ({
...prev,
subDepartmentId: e.target.value,
}))}
options={[ options={[
{ value: '', label: 'Select Sub-Department' }, { value: "", label: "Select Sub-Department" },
...subDepartments.map(s => ({ value: String(s.id), label: s.name })) ...subDepartments.map((s) => ({
value: String(s.id),
label: s.name,
})),
]} ]}
/> />
<Input <Input
label="Activity Name" label="Activity Name"
value={activityForm.name} value={activityForm.name}
onChange={(e) => setActivityForm(prev => ({ ...prev, name: e.target.value }))} onChange={(e) =>
setActivityForm((prev) => ({
...prev,
name: e.target.value,
}))}
placeholder="e.g., Mufali Aavak Katai" placeholder="e.g., Mufali Aavak Katai"
/> />
<Select <Select
label="Unit of Measurement" label="Unit of Measurement"
value={activityForm.unitOfMeasurement} value={activityForm.unitOfMeasurement}
onChange={(e) => setActivityForm(prev => ({ onChange={(e) =>
setActivityForm((prev) => ({
...prev, ...prev,
unitOfMeasurement: e.target.value as 'Per Bag' | 'Fixed Rate-Per Person' unitOfMeasurement: e.target.value as
| "Per Bag"
| "Fixed Rate-Per Person",
}))} }))}
options={[ options={[
{ value: 'Per Bag', label: 'Per Bag' }, { value: "Per Bag", label: "Per Bag" },
{ value: 'Fixed Rate-Per Person', label: 'Fixed Rate-Per Person' } {
value: "Fixed Rate-Per Person",
label: "Fixed Rate-Per Person",
},
]} ]}
/> />
<Button onClick={handleCreateActivity} disabled={loading}> <Button
onClick={handleCreateActivity}
disabled={loading}
>
<Plus className="h-4 w-4 mr-2" /> <Plus className="h-4 w-4 mr-2" />
Add Activity Add Activity
</Button> </Button>
@@ -363,32 +464,47 @@ export const ActivitiesPage: React.FC = () => {
<TableHead className="text-right">Actions</TableHead> <TableHead className="text-right">Actions</TableHead>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{activities.length === 0 ? ( {activities.length === 0
? (
<TableRow> <TableRow>
<TableCell colSpan={5} className="text-center text-gray-500 py-8"> <TableCell
colSpan={5}
className="text-center text-gray-500 py-8"
>
No activities found. Create one above. No activities found. Create one above.
</TableCell> </TableCell>
</TableRow> </TableRow>
) : ( )
: (
activities.map((activity: Activity) => ( activities.map((activity: Activity) => (
<TableRow key={activity.id}> <TableRow key={activity.id}>
<TableCell className="font-medium">{activity.name}</TableCell> <TableCell className="font-medium">
<TableCell>{activity.sub_department_name}</TableCell> {activity.name}
</TableCell>
<TableCell> <TableCell>
<span className={`px-2 py-1 rounded-full text-xs ${ {activity.sub_department_name}
activity.unit_of_measurement === 'Per Bag' </TableCell>
? 'bg-blue-100 text-blue-800' <TableCell>
: 'bg-green-100 text-green-800' <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} {activity.unit_of_measurement}
</span> </span>
</TableCell> </TableCell>
<TableCell>{new Date(activity.created_at).toLocaleDateString()}</TableCell> <TableCell>
{new Date(activity.created_at)
.toLocaleDateString()}
</TableCell>
<TableCell className="text-right"> <TableCell className="text-right">
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
onClick={() => handleDeleteActivity(activity.id)} onClick={() =>
handleDeleteActivity(activity.id)}
className="text-red-600 hover:text-red-800" className="text-red-600 hover:text-red-800"
> >
<Trash2 className="h-4 w-4" /> <Trash2 className="h-4 w-4" />

View File

@@ -1,39 +1,54 @@
import React, { useState, useEffect, useMemo } from 'react'; import React, { useEffect, useMemo, useState } from "react";
import { RefreshCw, Search, Filter, Eye, Calendar } from 'lucide-react'; import { Calendar, Eye, Filter, RefreshCw, Search } from "lucide-react";
import { Card, CardContent } from '../components/ui/Card'; import { Card, CardContent } from "../components/ui/Card.tsx";
import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from '../components/ui/Table'; import {
import { Button } from '../components/ui/Button'; Table,
import { Input, Select } from '../components/ui/Input'; TableBody,
import { api } from '../services/api'; TableCell,
import { useDepartments } from '../hooks/useDepartments'; TableHead,
import { useAuth } from '../contexts/AuthContext'; TableHeader,
TableRow,
} from "../components/ui/Table.tsx";
import { Button } from "../components/ui/Button.tsx";
import { Input, Select } from "../components/ui/Input.tsx";
import { api } from "../services/api.ts";
import { useDepartments } from "../hooks/useDepartments.ts";
import { useAuth } from "../contexts/AuthContext.tsx";
export const AllRatesPage: React.FC = () => { export const AllRatesPage: React.FC = () => {
const { user } = useAuth(); const { user } = useAuth();
const { departments } = useDepartments(); const { departments } = useDepartments();
const [allRates, setAllRates] = useState<any[]>([]); const [allRates, setAllRates] = useState<any[]>([]);
const [summary, setSummary] = useState<{ totalContractorRates: number; totalStandardRates: number; totalRates: number } | null>(null); const [summary, setSummary] = useState<
{
totalContractorRates: number;
totalStandardRates: number;
totalRates: number;
} | null
>(null);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState(''); const [error, setError] = useState("");
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState("");
// Filters // Filters
const [filters, setFilters] = useState({ const [filters, setFilters] = useState({
departmentId: '', departmentId: "",
startDate: '', startDate: "",
endDate: '', endDate: "",
rateType: '', // 'contractor' | 'standard' | '' rateType: "", // 'contractor' | 'standard' | ''
}); });
const isSuperAdmin = user?.role === 'SuperAdmin'; const isSuperAdmin = user?.role === "SuperAdmin";
// Fetch all rates // Fetch all rates
const fetchAllRates = async () => { const fetchAllRates = async () => {
setLoading(true); setLoading(true);
setError(''); setError("");
try { try {
const params: any = {}; const params: any = {};
if (filters.departmentId) params.departmentId = parseInt(filters.departmentId); if (filters.departmentId) {
params.departmentId = parseInt(filters.departmentId);
}
if (filters.startDate) params.startDate = filters.startDate; if (filters.startDate) params.startDate = filters.startDate;
if (filters.endDate) params.endDate = filters.endDate; if (filters.endDate) params.endDate = filters.endDate;
@@ -41,7 +56,7 @@ export const AllRatesPage: React.FC = () => {
setAllRates(data.allRates); setAllRates(data.allRates);
setSummary(data.summary); setSummary(data.summary);
} catch (err: any) { } catch (err: any) {
setError(err.message || 'Failed to fetch rates'); setError(err.message || "Failed to fetch rates");
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -53,9 +68,11 @@ export const AllRatesPage: React.FC = () => {
} }
}, [isSuperAdmin]); }, [isSuperAdmin]);
const handleFilterChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => { const handleFilterChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>,
) => {
const { name, value } = e.target; const { name, value } = e.target;
setFilters(prev => ({ ...prev, [name]: value })); setFilters((prev) => ({ ...prev, [name]: value }));
}; };
const applyFilters = () => { const applyFilters = () => {
@@ -64,10 +81,10 @@ export const AllRatesPage: React.FC = () => {
const clearFilters = () => { const clearFilters = () => {
setFilters({ setFilters({
departmentId: '', departmentId: "",
startDate: '', startDate: "",
endDate: '', endDate: "",
rateType: '', rateType: "",
}); });
setTimeout(fetchAllRates, 0); setTimeout(fetchAllRates, 0);
}; };
@@ -78,13 +95,13 @@ export const AllRatesPage: React.FC = () => {
// Filter by rate type // Filter by rate type
if (filters.rateType) { if (filters.rateType) {
rates = rates.filter(r => r.rate_type === filters.rateType); rates = rates.filter((r) => r.rate_type === filters.rateType);
} }
// Filter by search query // Filter by search query
if (searchQuery) { if (searchQuery) {
const query = searchQuery.toLowerCase(); const query = searchQuery.toLowerCase();
rates = rates.filter(r => rates = rates.filter((r) =>
r.contractor_name?.toLowerCase().includes(query) || r.contractor_name?.toLowerCase().includes(query) ||
r.sub_department_name?.toLowerCase().includes(query) || r.sub_department_name?.toLowerCase().includes(query) ||
r.department_name?.toLowerCase().includes(query) || r.department_name?.toLowerCase().includes(query) ||
@@ -104,7 +121,9 @@ export const AllRatesPage: React.FC = () => {
<CardContent> <CardContent>
<div className="text-center py-12"> <div className="text-center py-12">
<Eye size={48} className="mx-auto text-gray-400 mb-4" /> <Eye size={48} className="mx-auto text-gray-400 mb-4" />
<h2 className="text-xl font-semibold text-gray-700 mb-2">Access Restricted</h2> <h2 className="text-xl font-semibold text-gray-700 mb-2">
Access Restricted
</h2>
<p className="text-gray-500"> <p className="text-gray-500">
This page is only accessible to Super Admin accounts. This page is only accessible to Super Admin accounts.
</p> </p>
@@ -123,8 +142,12 @@ export const AllRatesPage: React.FC = () => {
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Eye className="text-purple-600" size={24} /> <Eye className="text-purple-600" size={24} />
<div> <div>
<h2 className="text-xl font-semibold text-gray-800">All Rates Overview</h2> <h2 className="text-xl font-semibold text-gray-800">
<p className="text-sm text-gray-500">View all contractor and standard rates across all departments</p> All Rates Overview
</h2>
<p className="text-sm text-gray-500">
View all contractor and standard rates across all departments
</p>
</div> </div>
</div> </div>
</div> </div>
@@ -144,8 +167,11 @@ export const AllRatesPage: React.FC = () => {
value={filters.departmentId} value={filters.departmentId}
onChange={handleFilterChange} onChange={handleFilterChange}
options={[ options={[
{ value: '', label: 'All Departments' }, { value: "", label: "All Departments" },
...departments.map(d => ({ value: String(d.id), label: d.name })) ...departments.map((d) => ({
value: String(d.id),
label: d.name,
})),
]} ]}
/> />
<Select <Select
@@ -154,13 +180,15 @@ export const AllRatesPage: React.FC = () => {
value={filters.rateType} value={filters.rateType}
onChange={handleFilterChange} onChange={handleFilterChange}
options={[ options={[
{ value: '', label: 'All Types' }, { value: "", label: "All Types" },
{ value: 'contractor', label: 'Contractor Rates' }, { value: "contractor", label: "Contractor Rates" },
{ value: 'standard', label: 'Standard Rates' }, { value: "standard", label: "Standard Rates" },
]} ]}
/> />
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1">Start Date</label> <label className="block text-sm font-medium text-gray-700 mb-1">
Start Date
</label>
<Input <Input
type="date" type="date"
name="startDate" name="startDate"
@@ -169,7 +197,9 @@ export const AllRatesPage: React.FC = () => {
/> />
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1">End Date</label> <label className="block text-sm font-medium text-gray-700 mb-1">
End Date
</label>
<Input <Input
type="date" type="date"
name="endDate" name="endDate"
@@ -192,16 +222,28 @@ export const AllRatesPage: React.FC = () => {
{summary && ( {summary && (
<div className="grid grid-cols-3 gap-4 mb-6"> <div className="grid grid-cols-3 gap-4 mb-6">
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4"> <div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<div className="text-sm text-blue-600 font-medium">Total Rates</div> <div className="text-sm text-blue-600 font-medium">
<div className="text-2xl font-bold text-blue-800">{summary.totalRates}</div> Total Rates
</div>
<div className="text-2xl font-bold text-blue-800">
{summary.totalRates}
</div>
</div> </div>
<div className="bg-orange-50 border border-orange-200 rounded-lg p-4"> <div className="bg-orange-50 border border-orange-200 rounded-lg p-4">
<div className="text-sm text-orange-600 font-medium">Contractor Rates</div> <div className="text-sm text-orange-600 font-medium">
<div className="text-2xl font-bold text-orange-800">{summary.totalContractorRates}</div> Contractor Rates
</div>
<div className="text-2xl font-bold text-orange-800">
{summary.totalContractorRates}
</div>
</div> </div>
<div className="bg-green-50 border border-green-200 rounded-lg p-4"> <div className="bg-green-50 border border-green-200 rounded-lg p-4">
<div className="text-sm text-green-600 font-medium">Standard Rates</div> <div className="text-sm text-green-600 font-medium">
<div className="text-2xl font-bold text-green-800">{summary.totalStandardRates}</div> Standard Rates
</div>
<div className="text-2xl font-bold text-green-800">
{summary.totalStandardRates}
</div>
</div> </div>
</div> </div>
)} )}
@@ -209,7 +251,10 @@ export const AllRatesPage: React.FC = () => {
{/* Search and Refresh */} {/* Search and Refresh */}
<div className="flex gap-4 mb-4"> <div className="flex gap-4 mb-4">
<div className="relative flex-1"> <div className="relative flex-1">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400" size={18} /> <Search
className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"
size={18}
/>
<input <input
type="text" type="text"
placeholder="Search by contractor, department, activity..." placeholder="Search by contractor, department, activity..."
@@ -232,9 +277,10 @@ export const AllRatesPage: React.FC = () => {
)} )}
{/* Table */} {/* Table */}
{loading ? ( {loading
<div className="text-center py-8">Loading all rates...</div> ? <div className="text-center py-8">Loading all rates...</div>
) : filteredRates.length > 0 ? ( : filteredRates.length > 0
? (
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<Table> <Table>
<TableHeader> <TableHeader>
@@ -251,30 +297,39 @@ export const AllRatesPage: React.FC = () => {
{filteredRates.map((rate, idx) => ( {filteredRates.map((rate, idx) => (
<TableRow key={`${rate.rate_type}-${rate.id}-${idx}`}> <TableRow key={`${rate.rate_type}-${rate.id}-${idx}`}>
<TableCell> <TableCell>
<span className={`px-2 py-1 rounded text-xs font-medium ${ <span
rate.rate_type === 'contractor' className={`px-2 py-1 rounded text-xs font-medium ${
? 'bg-orange-100 text-orange-700' rate.rate_type === "contractor"
: 'bg-green-100 text-green-700' ? "bg-orange-100 text-orange-700"
}`}> : "bg-green-100 text-green-700"
{rate.rate_type === 'contractor' ? 'Contractor' : 'Standard'} }`}
>
{rate.rate_type === "contractor"
? "Contractor"
: "Standard"}
</span> </span>
</TableCell> </TableCell>
<TableCell className="font-medium"> <TableCell className="font-medium">
{rate.contractor_name || '-'} {rate.contractor_name || "-"}
</TableCell> </TableCell>
<TableCell>{rate.department_name || '-'}</TableCell> <TableCell>{rate.department_name || "-"}</TableCell>
<TableCell>{rate.sub_department_name || '-'}</TableCell> <TableCell>{rate.sub_department_name || "-"}</TableCell>
<TableCell> <TableCell>
<span className={`px-2 py-1 rounded text-xs font-medium ${ <span
rate.activity === 'Loading' || rate.activity === 'Unloading' className={`px-2 py-1 rounded text-xs font-medium ${
? 'bg-purple-100 text-purple-700' rate.activity === "Loading" ||
: 'bg-gray-100 text-gray-700' rate.activity === "Unloading"
}`}> ? "bg-purple-100 text-purple-700"
{rate.activity || 'Standard'} : "bg-gray-100 text-gray-700"
}`}
>
{rate.activity || "Standard"}
</span> </span>
</TableCell> </TableCell>
<TableCell> <TableCell>
<span className="text-green-600 font-semibold">{rate.rate}</span> <span className="text-green-600 font-semibold">
{rate.rate}
</span>
</TableCell> </TableCell>
<TableCell> <TableCell>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
@@ -283,14 +338,15 @@ export const AllRatesPage: React.FC = () => {
</div> </div>
</TableCell> </TableCell>
<TableCell className="text-gray-500"> <TableCell className="text-gray-500">
{rate.created_by_name || '-'} {rate.created_by_name || "-"}
</TableCell> </TableCell>
</TableRow> </TableRow>
))} ))}
</TableBody> </TableBody>
</Table> </Table>
</div> </div>
) : ( )
: (
<div className="text-center py-8 text-gray-500"> <div className="text-center py-8 text-gray-500">
No rates found. Adjust your filters or check back later. No rates found. Adjust your filters or check back later.
</div> </div>

View File

@@ -1,43 +1,68 @@
import React, { useState, useEffect, useMemo } from 'react'; import React, { useEffect, useMemo, useState } from "react";
import { AlertTriangle, CheckCircle, Clock, RefreshCw, LogIn, LogOut, Search, ArrowUpDown, ArrowUp, ArrowDown, UserX, Edit2, X } from 'lucide-react'; import {
import { Card, CardContent } from '../components/ui/Card'; AlertTriangle,
import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from '../components/ui/Table'; ArrowDown,
import { Button } from '../components/ui/Button'; ArrowUp,
import { Select, Input } from '../components/ui/Input'; ArrowUpDown,
import { api } from '../services/api'; CheckCircle,
import { useEmployees } from '../hooks/useEmployees'; Clock,
import { useAuth } from '../contexts/AuthContext'; Edit2,
import type { AttendanceStatus } from '../types'; LogIn,
LogOut,
RefreshCw,
Search,
UserX,
X,
} from "lucide-react";
import { Card, CardContent } 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 { api } from "../services/api.ts";
import { useEmployees } from "../hooks/useEmployees.ts";
import { useAuth } from "../contexts/AuthContext.tsx";
import type { AttendanceStatus } from "../types.ts";
export const AttendancePage: React.FC = () => { export const AttendancePage: React.FC = () => {
const [activeTab, setActiveTab] = useState<'records' | 'checkin'>('records'); const [activeTab, setActiveTab] = useState<"records" | "checkin">("records");
const [attendance, setAttendance] = useState<any[]>([]); const [attendance, setAttendance] = useState<any[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState(''); const [error, setError] = useState("");
const { employees } = useEmployees(); const { employees } = useEmployees();
// Check-in form state // Check-in form state
const [selectedEmployee, setSelectedEmployee] = useState(''); const [selectedEmployee, setSelectedEmployee] = useState("");
const [workDate, setWorkDate] = useState(new Date().toISOString().split('T')[0]); const [workDate, setWorkDate] = useState(
new Date().toISOString().split("T")[0],
);
const [checkInLoading, setCheckInLoading] = useState(false); const [checkInLoading, setCheckInLoading] = useState(false);
const [employeeStatus, setEmployeeStatus] = useState<any>(null); const [employeeStatus, setEmployeeStatus] = useState<any>(null);
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState("");
const [sortField, setSortField] = useState<'date' | 'employee' | 'status'>('date'); const [sortField, setSortField] = useState<"date" | "employee" | "status">(
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc'); "date",
);
const [sortDirection, setSortDirection] = useState<"asc" | "desc">("desc");
const [editingRecord, setEditingRecord] = useState<number | null>(null); const [editingRecord, setEditingRecord] = useState<number | null>(null);
const [editStatus, setEditStatus] = useState<AttendanceStatus>('CheckedIn'); const [editStatus, setEditStatus] = useState<AttendanceStatus>("CheckedIn");
const [editRemark, setEditRemark] = useState(''); const [editRemark, setEditRemark] = useState("");
const { user } = useAuth(); const { user } = useAuth();
// Fetch attendance records // Fetch attendance records
const fetchAttendance = async () => { const fetchAttendance = async () => {
setLoading(true); setLoading(true);
setError(''); setError("");
try { try {
const data = await api.getAttendance(); const data = await api.getAttendance();
setAttendance(data); setAttendance(data);
} catch (err: any) { } catch (err: any) {
setError(err.message || 'Failed to fetch attendance'); setError(err.message || "Failed to fetch attendance");
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -51,8 +76,9 @@ export const AttendancePage: React.FC = () => {
useEffect(() => { useEffect(() => {
if (selectedEmployee && workDate) { if (selectedEmployee && workDate) {
const record = attendance.find( const record = attendance.find(
a => a.employee_id === parseInt(selectedEmployee) && (a) =>
a.work_date?.split('T')[0] === workDate a.employee_id === parseInt(selectedEmployee) &&
a.work_date?.split("T")[0] === workDate,
); );
setEmployeeStatus(record || null); setEmployeeStatus(record || null);
} else { } else {
@@ -62,16 +88,16 @@ export const AttendancePage: React.FC = () => {
const handleCheckIn = async () => { const handleCheckIn = async () => {
if (!selectedEmployee) { if (!selectedEmployee) {
alert('Please select an employee'); alert("Please select an employee");
return; return;
} }
setCheckInLoading(true); setCheckInLoading(true);
try { try {
await api.checkIn(parseInt(selectedEmployee), workDate); await api.checkIn(parseInt(selectedEmployee), workDate);
await fetchAttendance(); await fetchAttendance();
setEmployeeStatus({ status: 'CheckedIn' }); setEmployeeStatus({ status: "CheckedIn" });
} catch (err: any) { } catch (err: any) {
alert(err.message || 'Failed to check in'); alert(err.message || "Failed to check in");
} finally { } finally {
setCheckInLoading(false); setCheckInLoading(false);
} }
@@ -79,16 +105,16 @@ export const AttendancePage: React.FC = () => {
const handleCheckOut = async () => { const handleCheckOut = async () => {
if (!selectedEmployee) { if (!selectedEmployee) {
alert('Please select an employee'); alert("Please select an employee");
return; return;
} }
setCheckInLoading(true); setCheckInLoading(true);
try { try {
await api.checkOut(parseInt(selectedEmployee), workDate); await api.checkOut(parseInt(selectedEmployee), workDate);
await fetchAttendance(); await fetchAttendance();
setEmployeeStatus({ status: 'CheckedOut' }); setEmployeeStatus({ status: "CheckedOut" });
} catch (err: any) { } catch (err: any) {
alert(err.message || 'Failed to check out'); alert(err.message || "Failed to check out");
} finally { } finally {
setCheckInLoading(false); setCheckInLoading(false);
} }
@@ -96,16 +122,20 @@ export const AttendancePage: React.FC = () => {
const handleMarkAbsent = async () => { const handleMarkAbsent = async () => {
if (!selectedEmployee) { if (!selectedEmployee) {
alert('Please select an employee'); alert("Please select an employee");
return; return;
} }
setCheckInLoading(true); setCheckInLoading(true);
try { try {
await api.markAbsent(parseInt(selectedEmployee), workDate, 'Marked absent by supervisor'); await api.markAbsent(
parseInt(selectedEmployee),
workDate,
"Marked absent by supervisor",
);
await fetchAttendance(); await fetchAttendance();
setEmployeeStatus({ status: 'Absent' }); setEmployeeStatus({ status: "Absent" });
} catch (err: any) { } catch (err: any) {
alert(err.message || 'Failed to mark absent'); alert(err.message || "Failed to mark absent");
} finally { } finally {
setCheckInLoading(false); setCheckInLoading(false);
} }
@@ -116,31 +146,32 @@ export const AttendancePage: React.FC = () => {
await api.updateAttendanceStatus(recordId, editStatus, editRemark); await api.updateAttendanceStatus(recordId, editStatus, editRemark);
await fetchAttendance(); await fetchAttendance();
setEditingRecord(null); setEditingRecord(null);
setEditRemark(''); setEditRemark("");
} catch (err: any) { } catch (err: any) {
alert(err.message || 'Failed to update status'); alert(err.message || "Failed to update status");
} }
}; };
const startEditing = (record: any) => { const startEditing = (record: any) => {
setEditingRecord(record.id); setEditingRecord(record.id);
setEditStatus(record.status); setEditStatus(record.status);
setEditRemark(record.remark || ''); setEditRemark(record.remark || "");
}; };
const cancelEditing = () => { const cancelEditing = () => {
setEditingRecord(null); setEditingRecord(null);
setEditRemark(''); setEditRemark("");
}; };
const canEditAttendance = user?.role === 'SuperAdmin' || user?.role === 'Supervisor'; const canEditAttendance = user?.role === "SuperAdmin" ||
user?.role === "Supervisor";
const employeeOptions = [ const employeeOptions = [
{ value: '', label: 'Select Employee' }, { value: "", label: "Select Employee" },
...employees.filter(e => e.role === 'Employee').map(e => ({ ...employees.filter((e) => e.role === "Employee").map((e) => ({
value: String(e.id), value: String(e.id),
label: `${e.name} (${e.username})` label: `${e.name} (${e.username})`,
})) })),
]; ];
// Filter and sort attendance records // Filter and sort attendance records
@@ -150,7 +181,7 @@ export const AttendancePage: React.FC = () => {
// Apply search filter // Apply search filter
if (searchQuery) { if (searchQuery) {
const query = searchQuery.toLowerCase(); const query = searchQuery.toLowerCase();
filtered = filtered.filter(record => filtered = filtered.filter((record) =>
record.employee_name?.toLowerCase().includes(query) || record.employee_name?.toLowerCase().includes(query) ||
record.status?.toLowerCase().includes(query) record.status?.toLowerCase().includes(query)
); );
@@ -160,32 +191,37 @@ export const AttendancePage: React.FC = () => {
return [...filtered].sort((a, b) => { return [...filtered].sort((a, b) => {
let comparison = 0; let comparison = 0;
switch (sortField) { switch (sortField) {
case 'date': case "date":
comparison = new Date(a.work_date).getTime() - new Date(b.work_date).getTime(); comparison = new Date(a.work_date).getTime() -
new Date(b.work_date).getTime();
break; break;
case 'employee': case "employee":
comparison = (a.employee_name || '').localeCompare(b.employee_name || ''); comparison = (a.employee_name || "").localeCompare(
b.employee_name || "",
);
break; break;
case 'status': case "status":
comparison = (a.status || '').localeCompare(b.status || ''); comparison = (a.status || "").localeCompare(b.status || "");
break; break;
} }
return sortDirection === 'asc' ? comparison : -comparison; return sortDirection === "asc" ? comparison : -comparison;
}); });
}, [attendance, searchQuery, sortField, sortDirection]); }, [attendance, searchQuery, sortField, sortDirection]);
const handleSort = (field: 'date' | 'employee' | 'status') => { const handleSort = (field: "date" | "employee" | "status") => {
if (sortField === field) { if (sortField === field) {
setSortDirection(prev => prev === 'asc' ? 'desc' : 'asc'); setSortDirection((prev) => prev === "asc" ? "desc" : "asc");
} else { } else {
setSortField(field); setSortField(field);
setSortDirection('asc'); setSortDirection("asc");
} }
}; };
const SortIcon = ({ field }: { field: 'date' | 'employee' | 'status' }) => { const SortIcon = ({ field }: { field: "date" | "employee" | "status" }) => {
if (sortField !== field) return <ArrowUpDown size={14} className="ml-1 text-gray-400" />; if (sortField !== field) {
return sortDirection === 'asc' return <ArrowUpDown size={14} className="ml-1 text-gray-400" />;
}
return sortDirection === "asc"
? <ArrowUp size={14} className="ml-1 text-blue-600" /> ? <ArrowUp size={14} className="ml-1 text-blue-600" />
: <ArrowDown size={14} className="ml-1 text-blue-600" />; : <ArrowDown size={14} className="ml-1 text-blue-600" />;
}; };
@@ -196,21 +232,21 @@ export const AttendancePage: React.FC = () => {
<div className="border-b border-gray-200"> <div className="border-b border-gray-200">
<div className="flex space-x-8 px-6"> <div className="flex space-x-8 px-6">
<button <button
onClick={() => setActiveTab('records')} onClick={() => setActiveTab("records")}
className={`py-4 px-2 border-b-2 font-medium text-sm ${ className={`py-4 px-2 border-b-2 font-medium text-sm ${
activeTab === 'records' activeTab === "records"
? 'border-blue-500 text-blue-600' ? "border-blue-500 text-blue-600"
: 'border-transparent text-gray-500 hover:text-gray-700' : "border-transparent text-gray-500 hover:text-gray-700"
}`} }`}
> >
Attendance Records Attendance Records
</button> </button>
<button <button
onClick={() => setActiveTab('checkin')} onClick={() => setActiveTab("checkin")}
className={`py-4 px-2 border-b-2 font-medium text-sm ${ className={`py-4 px-2 border-b-2 font-medium text-sm ${
activeTab === 'checkin' activeTab === "checkin"
? 'border-blue-500 text-blue-600' ? "border-blue-500 text-blue-600"
: 'border-transparent text-gray-500 hover:text-gray-700' : "border-transparent text-gray-500 hover:text-gray-700"
}`} }`}
> >
Check In/Out Check In/Out
@@ -219,11 +255,14 @@ export const AttendancePage: React.FC = () => {
</div> </div>
<CardContent> <CardContent>
{activeTab === 'records' && ( {activeTab === "records" && (
<div> <div>
<div className="flex gap-4 mb-4"> <div className="flex gap-4 mb-4">
<div className="relative min-w-[300px] flex-1"> <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} /> <Search
className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"
size={18}
/>
<input <input
type="text" type="text"
placeholder="Search by employee name or status..." placeholder="Search by employee name or status..."
@@ -248,15 +287,20 @@ export const AttendancePage: React.FC = () => {
</div> </div>
)} )}
{loading ? ( {loading
<div className="text-center py-8">Loading attendance records...</div> ? (
) : filteredAndSortedAttendance.length > 0 ? ( <div className="text-center py-8">
Loading attendance records...
</div>
)
: filteredAndSortedAttendance.length > 0
? (
<Table> <Table>
<TableHeader> <TableHeader>
<TableHead>ID</TableHead> <TableHead>ID</TableHead>
<TableHead> <TableHead>
<button <button
onClick={() => handleSort('employee')} onClick={() => handleSort("employee")}
className="flex items-center hover:text-blue-600 transition-colors" className="flex items-center hover:text-blue-600 transition-colors"
> >
Employee <SortIcon field="employee" /> Employee <SortIcon field="employee" />
@@ -264,7 +308,7 @@ export const AttendancePage: React.FC = () => {
</TableHead> </TableHead>
<TableHead> <TableHead>
<button <button
onClick={() => handleSort('date')} onClick={() => handleSort("date")}
className="flex items-center hover:text-blue-600 transition-colors" className="flex items-center hover:text-blue-600 transition-colors"
> >
Date <SortIcon field="date" /> Date <SortIcon field="date" />
@@ -274,7 +318,7 @@ export const AttendancePage: React.FC = () => {
<TableHead>Check Out</TableHead> <TableHead>Check Out</TableHead>
<TableHead> <TableHead>
<button <button
onClick={() => handleSort('status')} onClick={() => handleSort("status")}
className="flex items-center hover:text-blue-600 transition-colors" className="flex items-center hover:text-blue-600 transition-colors"
> >
Status <SortIcon field="status" /> Status <SortIcon field="status" />
@@ -287,65 +331,94 @@ export const AttendancePage: React.FC = () => {
{filteredAndSortedAttendance.map((record) => ( {filteredAndSortedAttendance.map((record) => (
<TableRow key={record.id}> <TableRow key={record.id}>
<TableCell>{record.id}</TableCell> <TableCell>{record.id}</TableCell>
<TableCell>{record.employee_name || '-'}</TableCell> <TableCell>{record.employee_name || "-"}</TableCell>
<TableCell>{new Date(record.work_date).toLocaleDateString()}</TableCell> <TableCell>
{new Date(record.work_date).toLocaleDateString()}
</TableCell>
<TableCell> <TableCell>
{record.check_in_time {record.check_in_time
? new Date(record.check_in_time).toLocaleTimeString() ? new Date(record.check_in_time)
: '-'} .toLocaleTimeString()
: "-"}
</TableCell> </TableCell>
<TableCell> <TableCell>
{record.check_out_time {record.check_out_time
? new Date(record.check_out_time).toLocaleTimeString() ? new Date(record.check_out_time)
: '-'} .toLocaleTimeString()
: "-"}
</TableCell> </TableCell>
<TableCell> <TableCell>
{editingRecord === record.id ? ( {editingRecord === record.id
? (
<select <select
value={editStatus} value={editStatus}
onChange={(e) => setEditStatus(e.target.value as AttendanceStatus)} onChange={(e) =>
setEditStatus(
e.target.value as AttendanceStatus,
)}
className="px-2 py-1 border border-gray-300 rounded text-sm" className="px-2 py-1 border border-gray-300 rounded text-sm"
> >
<option value="CheckedIn">Checked In</option> <option value="CheckedIn">Checked In</option>
<option value="CheckedOut">Checked Out</option> <option value="CheckedOut">
Checked Out
</option>
<option value="Absent">Absent</option> <option value="Absent">Absent</option>
<option value="HalfDay">Half Day</option> <option value="HalfDay">Half Day</option>
<option value="Late">Late</option> <option value="Late">Late</option>
</select> </select>
) : ( )
<span className={`px-2 py-1 rounded text-xs font-medium ${ : (
record.status === 'CheckedOut' ? 'bg-green-100 text-green-700' : <span
record.status === 'CheckedIn' ? 'bg-blue-100 text-blue-700' : className={`px-2 py-1 rounded text-xs font-medium ${
record.status === 'Absent' ? 'bg-red-100 text-red-700' : record.status === "CheckedOut"
record.status === 'HalfDay' ? 'bg-orange-100 text-orange-700' : ? "bg-green-100 text-green-700"
record.status === 'Late' ? 'bg-yellow-100 text-yellow-700' : : record.status === "CheckedIn"
'bg-gray-100 text-gray-700' ? "bg-blue-100 text-blue-700"
}`}> : record.status === "Absent"
{record.status === 'CheckedOut' ? 'Completed' : ? "bg-red-100 text-red-700"
record.status === 'CheckedIn' ? 'Checked In' : : record.status === "HalfDay"
record.status === 'HalfDay' ? 'Half Day' : record.status} ? "bg-orange-100 text-orange-700"
: record.status === "Late"
? "bg-yellow-100 text-yellow-700"
: "bg-gray-100 text-gray-700"
}`}
>
{record.status === "CheckedOut"
? "Completed"
: record.status === "CheckedIn"
? "Checked In"
: record.status === "HalfDay"
? "Half Day"
: record.status}
</span> </span>
)} )}
</TableCell> </TableCell>
<TableCell> <TableCell>
{editingRecord === record.id ? ( {editingRecord === record.id
? (
<input <input
type="text" type="text"
value={editRemark} value={editRemark}
onChange={(e) => setEditRemark(e.target.value)} onChange={(e) =>
setEditRemark(e.target.value)}
placeholder="Add remark..." placeholder="Add remark..."
className="px-2 py-1 border border-gray-300 rounded text-sm w-32" className="px-2 py-1 border border-gray-300 rounded text-sm w-32"
/> />
) : ( )
<span className="text-gray-500 text-sm">{record.remark || '-'}</span> : (
<span className="text-gray-500 text-sm">
{record.remark || "-"}
</span>
)} )}
</TableCell> </TableCell>
{canEditAttendance && ( {canEditAttendance && (
<TableCell> <TableCell>
{editingRecord === record.id ? ( {editingRecord === record.id
? (
<div className="flex gap-1"> <div className="flex gap-1">
<button <button
onClick={() => handleUpdateStatus(record.id)} onClick={() =>
handleUpdateStatus(record.id)}
className="p-1 text-green-600 hover:bg-green-50 rounded" className="p-1 text-green-600 hover:bg-green-50 rounded"
title="Save" title="Save"
> >
@@ -359,7 +432,8 @@ export const AttendancePage: React.FC = () => {
<X size={16} /> <X size={16} />
</button> </button>
</div> </div>
) : ( )
: (
<button <button
onClick={() => startEditing(record)} onClick={() => startEditing(record)}
className="p-1 text-blue-600 hover:bg-blue-50 rounded" className="p-1 text-blue-600 hover:bg-blue-50 rounded"
@@ -374,18 +448,25 @@ export const AttendancePage: React.FC = () => {
))} ))}
</TableBody> </TableBody>
</Table> </Table>
) : ( )
: (
<div className="text-center py-8 text-gray-500"> <div className="text-center py-8 text-gray-500">
{searchQuery ? 'No matching records found' : 'No attendance records found'} {searchQuery
? "No matching records found"
: "No attendance records found"}
</div> </div>
)} )}
</div> </div>
)} )}
{activeTab === 'checkin' && ( {activeTab === "checkin" && (
<div className="max-w-2xl"> <div className="max-w-2xl">
<h3 className="text-lg font-semibold text-gray-800 mb-2">Check In / Check Out Management</h3> <h3 className="text-lg font-semibold text-gray-800 mb-2">
<p className="text-sm text-gray-600 mb-6">Manage employee attendance</p> Check In / Check Out Management
</h3>
<p className="text-sm text-gray-600 mb-6">
Manage employee attendance
</p>
<div className="space-y-6"> <div className="space-y-6">
<div className="grid grid-cols-2 gap-6"> <div className="grid grid-cols-2 gap-6">
@@ -404,35 +485,53 @@ export const AttendancePage: React.FC = () => {
</div> </div>
{selectedEmployee && ( {selectedEmployee && (
<div className={`border rounded-md p-4 flex items-start ${ <div
employeeStatus?.status === 'CheckedIn' className={`border rounded-md p-4 flex items-start ${
? 'bg-blue-50 border-blue-200' employeeStatus?.status === "CheckedIn"
: employeeStatus?.status === 'CheckedOut' ? "bg-blue-50 border-blue-200"
? 'bg-green-50 border-green-200' : employeeStatus?.status === "CheckedOut"
: 'bg-yellow-50 border-yellow-200' ? "bg-green-50 border-green-200"
}`}> : "bg-yellow-50 border-yellow-200"
{employeeStatus?.status === 'CheckedIn' ? ( }`}
>
{employeeStatus?.status === "CheckedIn"
? (
<> <>
<Clock size={20} className="text-blue-600 mr-2 flex-shrink-0 mt-0.5" /> <Clock
size={20}
className="text-blue-600 mr-2 flex-shrink-0 mt-0.5"
/>
<p className="text-sm text-blue-800"> <p className="text-sm text-blue-800">
Employee is currently checked in. Check-in time: { Employee is currently checked in. Check-in time:
employeeStatus.check_in_time {" "}
? new Date(employeeStatus.check_in_time).toLocaleTimeString() {employeeStatus.check_in_time
: 'N/A' ? new Date(employeeStatus.check_in_time)
} .toLocaleTimeString()
: "N/A"}
</p> </p>
</> </>
) : employeeStatus?.status === 'CheckedOut' ? ( )
: employeeStatus?.status === "CheckedOut"
? (
<> <>
<CheckCircle size={20} className="text-green-600 mr-2 flex-shrink-0 mt-0.5" /> <CheckCircle
size={20}
className="text-green-600 mr-2 flex-shrink-0 mt-0.5"
/>
<p className="text-sm text-green-800"> <p className="text-sm text-green-800">
Employee has completed attendance for this date. Employee has completed attendance for this date.
</p> </p>
</> </>
) : ( )
: (
<> <>
<AlertTriangle size={20} className="text-yellow-600 mr-2 flex-shrink-0 mt-0.5" /> <AlertTriangle
<p className="text-sm text-yellow-800">Employee has not checked in for this date</p> size={20}
className="text-yellow-600 mr-2 flex-shrink-0 mt-0.5"
/>
<p className="text-sm text-yellow-800">
Employee has not checked in for this date
</p>
</> </>
)} )}
</div> </div>
@@ -442,28 +541,35 @@ export const AttendancePage: React.FC = () => {
<Button <Button
size="lg" size="lg"
onClick={handleCheckIn} onClick={handleCheckIn}
disabled={checkInLoading || !selectedEmployee || employeeStatus?.status === 'CheckedIn' || employeeStatus?.status === 'CheckedOut' || employeeStatus?.status === 'Absent'} disabled={checkInLoading || !selectedEmployee ||
employeeStatus?.status === "CheckedIn" ||
employeeStatus?.status === "CheckedOut" ||
employeeStatus?.status === "Absent"}
> >
<LogIn size={16} className="mr-2" /> <LogIn size={16} className="mr-2" />
{checkInLoading ? 'Processing...' : 'Check In'} {checkInLoading ? "Processing..." : "Check In"}
</Button> </Button>
<Button <Button
size="lg" size="lg"
variant="outline" variant="outline"
onClick={handleCheckOut} onClick={handleCheckOut}
disabled={checkInLoading || !selectedEmployee || employeeStatus?.status !== 'CheckedIn'} disabled={checkInLoading || !selectedEmployee ||
employeeStatus?.status !== "CheckedIn"}
> >
<LogOut size={16} className="mr-2" /> <LogOut size={16} className="mr-2" />
{checkInLoading ? 'Processing...' : 'Check Out'} {checkInLoading ? "Processing..." : "Check Out"}
</Button> </Button>
<Button <Button
size="lg" size="lg"
variant="danger" variant="danger"
onClick={handleMarkAbsent} onClick={handleMarkAbsent}
disabled={checkInLoading || !selectedEmployee || employeeStatus?.status === 'CheckedIn' || employeeStatus?.status === 'CheckedOut' || employeeStatus?.status === 'Absent'} disabled={checkInLoading || !selectedEmployee ||
employeeStatus?.status === "CheckedIn" ||
employeeStatus?.status === "CheckedOut" ||
employeeStatus?.status === "Absent"}
> >
<UserX size={16} className="mr-2" /> <UserX size={16} className="mr-2" />
{checkInLoading ? 'Processing...' : 'Mark Absent'} {checkInLoading ? "Processing..." : "Mark Absent"}
</Button> </Button>
</div> </div>
</div> </div>

File diff suppressed because it is too large Load Diff

View File

@@ -1,72 +1,82 @@
import React, { useState, useEffect } from 'react'; import React, { useCallback, useEffect, useState } from "react";
import { import {
ArrowRightLeft,
Plus,
CheckCircle,
XCircle,
Clock,
Building2,
User,
AlertCircle, AlertCircle,
ArrowRightLeft,
Building2,
CheckCircle,
Clock,
Filter,
Plus,
RefreshCw, RefreshCw,
Search, Search,
Filter User,
} from 'lucide-react'; XCircle,
import { Card, CardContent } from '../components/ui/Card'; } from "lucide-react";
import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from '../components/ui/Table'; import { Card, CardContent } from "../components/ui/Card.tsx";
import { Button } from '../components/ui/Button'; import {
import { Select, Input } from '../components/ui/Input'; Table,
import { api } from '../services/api'; TableBody,
import { useEmployees } from '../hooks/useEmployees'; TableCell,
import { useDepartments } from '../hooks/useDepartments'; TableHead,
import type { EmployeeSwap, SwapReason, SwapStatus } from '../types'; TableHeader,
TableRow,
} from "../components/ui/Table.tsx";
import { Button } from "../components/ui/Button.tsx";
import { Input, Select } from "../components/ui/Input.tsx";
import { api } from "../services/api.ts";
import { useEmployees } from "../hooks/useEmployees.ts";
import { useDepartments } from "../hooks/useDepartments.ts";
import type { EmployeeSwap, SwapReason, SwapStatus } from "../types.ts";
export const EmployeeSwapPage: React.FC = () => { export const EmployeeSwapPage: React.FC = () => {
const [activeTab, setActiveTab] = useState<'list' | 'create'>('list'); const [activeTab, setActiveTab] = useState<"list" | "create">("list");
const [swaps, setSwaps] = useState<EmployeeSwap[]>([]); const [swaps, setSwaps] = useState<EmployeeSwap[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState(''); const [error, setError] = useState("");
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState("");
const [statusFilter, setStatusFilter] = useState<SwapStatus | ''>(''); const [statusFilter, setStatusFilter] = useState<SwapStatus | "">("");
const { employees } = useEmployees(); const { employees } = useEmployees();
const { departments } = useDepartments(); const { departments } = useDepartments();
// Form state // Form state
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
employeeId: '', employeeId: "",
targetDepartmentId: '', targetDepartmentId: "",
targetContractorId: '', targetContractorId: "",
swapReason: '' as SwapReason | '', swapReason: "" as SwapReason | "",
reasonDetails: '', reasonDetails: "",
workCompletionPercentage: 0, workCompletionPercentage: 0,
swapDate: new Date().toISOString().split('T')[0], swapDate: new Date().toISOString().split("T")[0],
}); });
const [submitting, setSubmitting] = useState(false); const [submitting, setSubmitting] = useState(false);
const fetchSwaps = async () => { const fetchSwaps = useCallback(async () => {
setLoading(true); setLoading(true);
setError(''); setError("");
try { try {
const params: { status?: string } = {}; const params: { status?: string } = {};
if (statusFilter) params.status = statusFilter; if (statusFilter) params.status = statusFilter;
const data = await api.getEmployeeSwaps(params); const data = await api.getEmployeeSwaps(params);
setSwaps(data); setSwaps(data);
} catch (err: any) { } catch (err: any) {
setError(err.message || 'Failed to fetch swaps'); setError(err.message || "Failed to fetch swaps");
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; }, [statusFilter]);
useEffect(() => { useEffect(() => {
fetchSwaps(); fetchSwaps();
}, [statusFilter]); }, [fetchSwaps]);
const handleCreateSwap = async (e: React.FormEvent) => { const handleCreateSwap = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
if (!formData.employeeId || !formData.targetDepartmentId || !formData.swapReason) { if (
alert('Please fill in all required fields'); !formData.employeeId || !formData.targetDepartmentId ||
!formData.swapReason
) {
alert("Please fill in all required fields");
return; return;
} }
@@ -75,7 +85,9 @@ export const EmployeeSwapPage: React.FC = () => {
await api.createEmployeeSwap({ await api.createEmployeeSwap({
employeeId: parseInt(formData.employeeId), employeeId: parseInt(formData.employeeId),
targetDepartmentId: parseInt(formData.targetDepartmentId), targetDepartmentId: parseInt(formData.targetDepartmentId),
targetContractorId: formData.targetContractorId ? parseInt(formData.targetContractorId) : undefined, targetContractorId: formData.targetContractorId
? parseInt(formData.targetContractorId)
: undefined,
swapReason: formData.swapReason as SwapReason, swapReason: formData.swapReason as SwapReason,
reasonDetails: formData.reasonDetails || undefined, reasonDetails: formData.reasonDetails || undefined,
workCompletionPercentage: formData.workCompletionPercentage, workCompletionPercentage: formData.workCompletionPercentage,
@@ -84,57 +96,64 @@ export const EmployeeSwapPage: React.FC = () => {
// Reset form and switch to list // Reset form and switch to list
setFormData({ setFormData({
employeeId: '', employeeId: "",
targetDepartmentId: '', targetDepartmentId: "",
targetContractorId: '', targetContractorId: "",
swapReason: '', swapReason: "",
reasonDetails: '', reasonDetails: "",
workCompletionPercentage: 0, workCompletionPercentage: 0,
swapDate: new Date().toISOString().split('T')[0], swapDate: new Date().toISOString().split("T")[0],
}); });
setActiveTab('list'); setActiveTab("list");
await fetchSwaps(); await fetchSwaps();
} catch (err: any) { } catch (err: any) {
alert(err.message || 'Failed to create swap'); alert(err.message || "Failed to create swap");
} finally { } finally {
setSubmitting(false); setSubmitting(false);
} }
}; };
const handleCompleteSwap = async (id: number) => { const handleCompleteSwap = async (id: number) => {
if (!confirm('Complete this swap and return employee to original department?')) return; if (
!confirm("Complete this swap and return employee to original department?")
) return;
try { try {
await api.completeEmployeeSwap(id); await api.completeEmployeeSwap(id);
await fetchSwaps(); await fetchSwaps();
} catch (err: any) { } catch (err: any) {
alert(err.message || 'Failed to complete swap'); alert(err.message || "Failed to complete swap");
} }
}; };
const handleCancelSwap = async (id: number) => { const handleCancelSwap = async (id: number) => {
if (!confirm('Cancel this swap and return employee to original department?')) return; if (
!confirm("Cancel this swap and return employee to original department?")
) return;
try { try {
await api.cancelEmployeeSwap(id); await api.cancelEmployeeSwap(id);
await fetchSwaps(); await fetchSwaps();
} catch (err: any) { } catch (err: any) {
alert(err.message || 'Failed to cancel swap'); alert(err.message || "Failed to cancel swap");
} }
}; };
// Filter employees (only show employees) // Filter employees (only show employees)
const employeeList = employees.filter(e => e.role === 'Employee'); const employeeList = employees.filter((e) => e.role === "Employee");
// Get contractors for selected target department // Get contractors for selected target department
const targetContractors = employees.filter( const targetContractors = employees.filter(
e => e.role === 'Contractor' && (e) =>
e.department_id === parseInt(formData.targetDepartmentId) e.role === "Contractor" &&
e.department_id === parseInt(formData.targetDepartmentId),
); );
// Get selected employee details // Get selected employee details
const selectedEmployee = employeeList.find(e => e.id === parseInt(formData.employeeId)); const selectedEmployee = employeeList.find((e) =>
e.id === parseInt(formData.employeeId)
);
// Filter swaps based on search // Filter swaps based on search
const filteredSwaps = swaps.filter(swap => { const filteredSwaps = swaps.filter((swap) => {
if (!searchQuery) return true; if (!searchQuery) return true;
const query = searchQuery.toLowerCase(); const query = searchQuery.toLowerCase();
return ( return (
@@ -146,29 +165,47 @@ export const EmployeeSwapPage: React.FC = () => {
const getStatusBadge = (status: SwapStatus) => { const getStatusBadge = (status: SwapStatus) => {
switch (status) { switch (status) {
case 'Active': case "Active":
return <span className="px-2 py-1 rounded text-xs font-medium bg-blue-100 text-blue-700">Active</span>; return (
case 'Completed': <span className="px-2 py-1 rounded text-xs font-medium bg-blue-100 text-blue-700">
return <span className="px-2 py-1 rounded text-xs font-medium bg-green-100 text-green-700">Completed</span>; Active
case 'Cancelled': </span>
return <span className="px-2 py-1 rounded text-xs font-medium bg-red-100 text-red-700">Cancelled</span>; );
case "Completed":
return (
<span className="px-2 py-1 rounded text-xs font-medium bg-green-100 text-green-700">
Completed
</span>
);
case "Cancelled":
return (
<span className="px-2 py-1 rounded text-xs font-medium bg-red-100 text-red-700">
Cancelled
</span>
);
} }
}; };
const getReasonBadge = (reason: SwapReason) => { const getReasonBadge = (reason: SwapReason) => {
const colors: Record<SwapReason, string> = { const colors: Record<SwapReason, string> = {
'LeftWork': 'bg-orange-100 text-orange-700', "LeftWork": "bg-orange-100 text-orange-700",
'Sick': 'bg-red-100 text-red-700', "Sick": "bg-red-100 text-red-700",
'FinishedEarly': 'bg-green-100 text-green-700', "FinishedEarly": "bg-green-100 text-green-700",
'Other': 'bg-gray-100 text-gray-700', "Other": "bg-gray-100 text-gray-700",
}; };
const labels: Record<SwapReason, string> = { const labels: Record<SwapReason, string> = {
'LeftWork': 'Left Work', "LeftWork": "Left Work",
'Sick': 'Sick', "Sick": "Sick",
'FinishedEarly': 'Finished Early', "FinishedEarly": "Finished Early",
'Other': 'Other', "Other": "Other",
}; };
return <span className={`px-2 py-1 rounded text-xs font-medium ${colors[reason]}`}>{labels[reason]}</span>; return (
<span
className={`px-2 py-1 rounded text-xs font-medium ${colors[reason]}`}
>
{labels[reason]}
</span>
);
}; };
return ( return (
@@ -182,8 +219,12 @@ export const EmployeeSwapPage: React.FC = () => {
<ArrowRightLeft className="text-purple-600" size={24} /> <ArrowRightLeft className="text-purple-600" size={24} />
</div> </div>
<div> <div>
<h1 className="text-xl font-bold text-gray-800">Employee Work Swap</h1> <h1 className="text-xl font-bold text-gray-800">
<p className="text-sm text-gray-500">Transfer employees between departments temporarily</p> Employee Work Swap
</h1>
<p className="text-sm text-gray-500">
Transfer employees between departments temporarily
</p>
</div> </div>
</div> </div>
</div> </div>
@@ -193,21 +234,21 @@ export const EmployeeSwapPage: React.FC = () => {
<div className="border-b border-gray-200"> <div className="border-b border-gray-200">
<div className="flex space-x-8 px-6"> <div className="flex space-x-8 px-6">
<button <button
onClick={() => setActiveTab('list')} onClick={() => setActiveTab("list")}
className={`py-4 px-2 border-b-2 font-medium text-sm ${ className={`py-4 px-2 border-b-2 font-medium text-sm ${
activeTab === 'list' activeTab === "list"
? 'border-purple-500 text-purple-600' ? "border-purple-500 text-purple-600"
: 'border-transparent text-gray-500 hover:text-gray-700' : "border-transparent text-gray-500 hover:text-gray-700"
}`} }`}
> >
Swap History Swap History
</button> </button>
<button <button
onClick={() => setActiveTab('create')} onClick={() => setActiveTab("create")}
className={`py-4 px-2 border-b-2 font-medium text-sm flex items-center gap-2 ${ className={`py-4 px-2 border-b-2 font-medium text-sm flex items-center gap-2 ${
activeTab === 'create' activeTab === "create"
? 'border-purple-500 text-purple-600' ? "border-purple-500 text-purple-600"
: 'border-transparent text-gray-500 hover:text-gray-700' : "border-transparent text-gray-500 hover:text-gray-700"
}`} }`}
> >
<Plus size={16} /> <Plus size={16} />
@@ -217,12 +258,15 @@ export const EmployeeSwapPage: React.FC = () => {
</div> </div>
<CardContent> <CardContent>
{activeTab === 'list' && ( {activeTab === "list" && (
<div> <div>
{/* Filters */} {/* Filters */}
<div className="flex gap-4 mb-6"> <div className="flex gap-4 mb-6">
<div className="relative flex-1 max-w-md"> <div className="relative flex-1 max-w-md">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400" size={18} /> <Search
className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"
size={18}
/>
<input <input
type="text" type="text"
placeholder="Search by employee or department..." placeholder="Search by employee or department..."
@@ -235,7 +279,8 @@ export const EmployeeSwapPage: React.FC = () => {
<Filter size={18} className="text-gray-400" /> <Filter size={18} className="text-gray-400" />
<select <select
value={statusFilter} value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value as SwapStatus | '')} onChange={(e) =>
setStatusFilter(e.target.value as SwapStatus | "")}
className="px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-purple-500" className="px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-purple-500"
> >
<option value="">All Status</option> <option value="">All Status</option>
@@ -258,7 +303,7 @@ export const EmployeeSwapPage: React.FC = () => {
<span className="text-sm font-medium">Active</span> <span className="text-sm font-medium">Active</span>
</div> </div>
<div className="text-2xl font-bold text-blue-700"> <div className="text-2xl font-bold text-blue-700">
{swaps.filter(s => s.status === 'Active').length} {swaps.filter((s) => s.status === "Active").length}
</div> </div>
</div> </div>
<div className="bg-green-50 rounded-lg p-4"> <div className="bg-green-50 rounded-lg p-4">
@@ -267,7 +312,7 @@ export const EmployeeSwapPage: React.FC = () => {
<span className="text-sm font-medium">Completed</span> <span className="text-sm font-medium">Completed</span>
</div> </div>
<div className="text-2xl font-bold text-green-700"> <div className="text-2xl font-bold text-green-700">
{swaps.filter(s => s.status === 'Completed').length} {swaps.filter((s) => s.status === "Completed").length}
</div> </div>
</div> </div>
<div className="bg-red-50 rounded-lg p-4"> <div className="bg-red-50 rounded-lg p-4">
@@ -276,7 +321,7 @@ export const EmployeeSwapPage: React.FC = () => {
<span className="text-sm font-medium">Cancelled</span> <span className="text-sm font-medium">Cancelled</span>
</div> </div>
<div className="text-2xl font-bold text-red-700"> <div className="text-2xl font-bold text-red-700">
{swaps.filter(s => s.status === 'Cancelled').length} {swaps.filter((s) => s.status === "Cancelled").length}
</div> </div>
</div> </div>
<div className="bg-purple-50 rounded-lg p-4"> <div className="bg-purple-50 rounded-lg p-4">
@@ -284,7 +329,9 @@ export const EmployeeSwapPage: React.FC = () => {
<ArrowRightLeft size={18} /> <ArrowRightLeft size={18} />
<span className="text-sm font-medium">Total Swaps</span> <span className="text-sm font-medium">Total Swaps</span>
</div> </div>
<div className="text-2xl font-bold text-purple-700">{swaps.length}</div> <div className="text-2xl font-bold text-purple-700">
{swaps.length}
</div>
</div> </div>
</div> </div>
@@ -294,12 +341,16 @@ export const EmployeeSwapPage: React.FC = () => {
</div> </div>
)} )}
{loading ? ( {loading
? (
<div className="text-center py-8"> <div className="text-center py-8">
<div className="inline-block animate-spin rounded-full h-6 w-6 border-b-2 border-purple-600"></div> <div className="inline-block animate-spin rounded-full h-6 w-6 border-b-2 border-purple-600">
</div>
<span className="ml-2 text-gray-600">Loading swaps...</span> <span className="ml-2 text-gray-600">Loading swaps...</span>
</div> </div>
) : filteredSwaps.length > 0 ? ( )
: filteredSwaps.length > 0
? (
<Table> <Table>
<TableHeader> <TableHeader>
<TableHead>Employee</TableHead> <TableHead>Employee</TableHead>
@@ -319,18 +370,28 @@ export const EmployeeSwapPage: React.FC = () => {
<User size={16} className="text-purple-600" /> <User size={16} className="text-purple-600" />
</div> </div>
<div> <div>
<div className="font-medium text-gray-800">{swap.employee_name}</div> <div className="font-medium text-gray-800">
{swap.employee_name}
</div>
<div className="text-xs text-gray-500"> <div className="text-xs text-gray-500">
{swap.original_contractor_name && `Under: ${swap.original_contractor_name}`} {swap.original_contractor_name &&
`Under: ${swap.original_contractor_name}`}
</div> </div>
</div> </div>
</div> </div>
</TableCell> </TableCell>
<TableCell> <TableCell>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-gray-600">{swap.original_department_name}</span> <span className="text-gray-600">
<ArrowRightLeft size={14} className="text-gray-400" /> {swap.original_department_name}
<span className="font-medium text-purple-600">{swap.target_department_name}</span> </span>
<ArrowRightLeft
size={14}
className="text-gray-400"
/>
<span className="font-medium text-purple-600">
{swap.target_department_name}
</span>
</div> </div>
{swap.target_contractor_name && ( {swap.target_contractor_name && (
<div className="text-xs text-gray-500 mt-1"> <div className="text-xs text-gray-500 mt-1">
@@ -342,7 +403,10 @@ export const EmployeeSwapPage: React.FC = () => {
<div className="space-y-1"> <div className="space-y-1">
{getReasonBadge(swap.swap_reason)} {getReasonBadge(swap.swap_reason)}
{swap.reason_details && ( {swap.reason_details && (
<div className="text-xs text-gray-500 max-w-[150px] truncate" title={swap.reason_details}> <div
className="text-xs text-gray-500 max-w-[150px] truncate"
title={swap.reason_details}
>
{swap.reason_details} {swap.reason_details}
</div> </div>
)} )}
@@ -353,10 +417,15 @@ export const EmployeeSwapPage: React.FC = () => {
<div className="w-16 bg-gray-200 rounded-full h-2"> <div className="w-16 bg-gray-200 rounded-full h-2">
<div <div
className="bg-purple-600 h-2 rounded-full" className="bg-purple-600 h-2 rounded-full"
style={{ width: `${swap.work_completion_percentage}%` }} style={{
width:
`${swap.work_completion_percentage}%`,
}}
/> />
</div> </div>
<span className="text-sm text-gray-600">{swap.work_completion_percentage}%</span> <span className="text-sm text-gray-600">
{swap.work_completion_percentage}%
</span>
</div> </div>
</TableCell> </TableCell>
<TableCell> <TableCell>
@@ -369,7 +438,7 @@ export const EmployeeSwapPage: React.FC = () => {
</TableCell> </TableCell>
<TableCell>{getStatusBadge(swap.status)}</TableCell> <TableCell>{getStatusBadge(swap.status)}</TableCell>
<TableCell> <TableCell>
{swap.status === 'Active' && ( {swap.status === "Active" && (
<div className="flex gap-1"> <div className="flex gap-1">
<button <button
onClick={() => handleCompleteSwap(swap.id)} onClick={() => handleCompleteSwap(swap.id)}
@@ -392,13 +461,17 @@ export const EmployeeSwapPage: React.FC = () => {
))} ))}
</TableBody> </TableBody>
</Table> </Table>
) : ( )
: (
<div className="text-center py-12 text-gray-500"> <div className="text-center py-12 text-gray-500">
<ArrowRightLeft size={48} className="mx-auto mb-4 text-gray-300" /> <ArrowRightLeft
size={48}
className="mx-auto mb-4 text-gray-300"
/>
<p>No swap records found</p> <p>No swap records found</p>
<Button <Button
className="mt-4" className="mt-4"
onClick={() => setActiveTab('create')} onClick={() => setActiveTab("create")}
> >
<Plus size={16} className="mr-2" /> <Plus size={16} className="mr-2" />
Create First Swap Create First Swap
@@ -408,11 +481,15 @@ export const EmployeeSwapPage: React.FC = () => {
</div> </div>
)} )}
{activeTab === 'create' && ( {activeTab === "create" && (
<div className="max-w-3xl mx-auto"> <div className="max-w-3xl mx-auto">
<div className="mb-6"> <div className="mb-6">
<h2 className="text-lg font-semibold text-gray-800">Create Employee Swap</h2> <h2 className="text-lg font-semibold text-gray-800">
<p className="text-sm text-gray-500">Transfer an employee to a different department temporarily</p> Create Employee Swap
</h2>
<p className="text-sm text-gray-500">
Transfer an employee to a different department temporarily
</p>
</div> </div>
<form onSubmit={handleCreateSwap} className="space-y-6"> <form onSubmit={handleCreateSwap} className="space-y-6">
@@ -425,13 +502,14 @@ export const EmployeeSwapPage: React.FC = () => {
<Select <Select
label="Employee" label="Employee"
value={formData.employeeId} value={formData.employeeId}
onChange={(e) => setFormData({ ...formData, employeeId: e.target.value })} onChange={(e) =>
setFormData({ ...formData, employeeId: e.target.value })}
options={[ options={[
{ value: '', label: 'Select an employee...' }, { value: "", label: "Select an employee..." },
...employeeList.map(e => ({ ...employeeList.map((e) => ({
value: String(e.id), value: String(e.id),
label: `${e.name} - ${e.department_name || 'No Dept'}` label: `${e.name} - ${e.department_name || "No Dept"}`,
})) })),
]} ]}
required required
/> />
@@ -443,10 +521,14 @@ export const EmployeeSwapPage: React.FC = () => {
<User size={20} className="text-purple-600" /> <User size={20} className="text-purple-600" />
</div> </div>
<div> <div>
<div className="font-medium text-gray-800">{selectedEmployee.name}</div> <div className="font-medium text-gray-800">
{selectedEmployee.name}
</div>
<div className="text-sm text-gray-500"> <div className="text-sm text-gray-500">
Current: {selectedEmployee.department_name || 'No Department'} Current: {selectedEmployee.department_name ||
{selectedEmployee.contractor_name && ` • Under: ${selectedEmployee.contractor_name}`} "No Department"}
{selectedEmployee.contractor_name &&
` • Under: ${selectedEmployee.contractor_name}`}
</div> </div>
</div> </div>
</div> </div>
@@ -464,26 +546,36 @@ export const EmployeeSwapPage: React.FC = () => {
<Select <Select
label="Department" label="Department"
value={formData.targetDepartmentId} value={formData.targetDepartmentId}
onChange={(e) => setFormData({ onChange={(e) =>
setFormData({
...formData, ...formData,
targetDepartmentId: e.target.value, targetDepartmentId: e.target.value,
targetContractorId: '' // Reset contractor when department changes targetContractorId: "", // Reset contractor when department changes
})} })}
options={[ options={[
{ value: '', label: 'Select department...' }, { value: "", label: "Select department..." },
...departments ...departments
.filter(d => d.id !== selectedEmployee?.department_id) .filter((d) =>
.map(d => ({ value: String(d.id), label: d.name })) d.id !== selectedEmployee?.department_id
)
.map((d) => ({ value: String(d.id), label: d.name })),
]} ]}
required required
/> />
<Select <Select
label="Assign to Contractor (Optional)" label="Assign to Contractor (Optional)"
value={formData.targetContractorId} value={formData.targetContractorId}
onChange={(e) => setFormData({ ...formData, targetContractorId: e.target.value })} onChange={(e) =>
setFormData({
...formData,
targetContractorId: e.target.value,
})}
options={[ options={[
{ value: '', label: 'No contractor' }, { value: "", label: "No contractor" },
...targetContractors.map(c => ({ value: String(c.id), label: c.name })) ...targetContractors.map((c) => ({
value: String(c.id),
label: c.name,
})),
]} ]}
disabled={!formData.targetDepartmentId} disabled={!formData.targetDepartmentId}
/> />
@@ -500,13 +592,20 @@ export const EmployeeSwapPage: React.FC = () => {
<Select <Select
label="Reason" label="Reason"
value={formData.swapReason} value={formData.swapReason}
onChange={(e) => setFormData({ ...formData, swapReason: e.target.value as SwapReason })} onChange={(e) =>
setFormData({
...formData,
swapReason: e.target.value as SwapReason,
})}
options={[ options={[
{ value: '', label: 'Select reason...' }, { value: "", label: "Select reason..." },
{ value: 'LeftWork', label: 'Left Work Early' }, { value: "LeftWork", label: "Left Work Early" },
{ value: 'Sick', label: 'Sick / Unwell' }, { value: "Sick", label: "Sick / Unwell" },
{ value: 'FinishedEarly', label: 'Finished Work Early' }, {
{ value: 'Other', label: 'Other Reason' }, value: "FinishedEarly",
label: "Finished Work Early",
},
{ value: "Other", label: "Other Reason" },
]} ]}
required required
/> />
@@ -521,9 +620,12 @@ export const EmployeeSwapPage: React.FC = () => {
max="100" max="100"
step="5" step="5"
value={formData.workCompletionPercentage} value={formData.workCompletionPercentage}
onChange={(e) => setFormData({ onChange={(e) =>
setFormData({
...formData, ...formData,
workCompletionPercentage: parseInt(e.target.value) workCompletionPercentage: parseInt(
e.target.value,
),
})} })}
className="flex-1" className="flex-1"
/> />
@@ -537,7 +639,11 @@ export const EmployeeSwapPage: React.FC = () => {
<Input <Input
label="Additional Details (Optional)" label="Additional Details (Optional)"
value={formData.reasonDetails} value={formData.reasonDetails}
onChange={(e) => setFormData({ ...formData, reasonDetails: e.target.value })} onChange={(e) =>
setFormData({
...formData,
reasonDetails: e.target.value,
})}
placeholder="Provide more context about the swap..." placeholder="Provide more context about the swap..."
/> />
</div> </div>
@@ -553,7 +659,8 @@ export const EmployeeSwapPage: React.FC = () => {
label="Date" label="Date"
type="date" type="date"
value={formData.swapDate} value={formData.swapDate}
onChange={(e) => setFormData({ ...formData, swapDate: e.target.value })} onChange={(e) =>
setFormData({ ...formData, swapDate: e.target.value })}
required required
/> />
</div> </div>
@@ -563,20 +670,24 @@ export const EmployeeSwapPage: React.FC = () => {
<Button <Button
type="button" type="button"
variant="outline" variant="outline"
onClick={() => setActiveTab('list')} onClick={() => setActiveTab("list")}
> >
Cancel Cancel
</Button> </Button>
<Button <Button
type="submit" type="submit"
disabled={submitting || !formData.employeeId || !formData.targetDepartmentId || !formData.swapReason} disabled={submitting || !formData.employeeId ||
!formData.targetDepartmentId || !formData.swapReason}
> >
{submitting ? ( {submitting
? (
<> <>
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div> <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2">
</div>
Creating... Creating...
</> </>
) : ( )
: (
<> <>
<ArrowRightLeft size={16} className="mr-2" /> <ArrowRightLeft size={16} className="mr-2" />
Create Swap Create Swap

View File

@@ -1,34 +1,42 @@
import React, { useState, useEffect } from 'react'; import React, { useEffect, useState } from "react";
import { useAuth } from '../contexts/AuthContext'; import { useAuth } from "../contexts/AuthContext.tsx";
import { import {
Users, Lock, Eye, EyeOff, XCircle, Mail, ArrowRight, ArrowRight,
CheckCircle, X, Sparkles, Shield, KeyRound CheckCircle,
} from 'lucide-react'; Eye,
EyeOff,
KeyRound,
Lock,
Mail,
Shield,
Sparkles,
Users,
X,
XCircle,
} from "lucide-react";
export const LoginPage: React.FC = () => { export const LoginPage: React.FC = () => {
const [username, setUsername] = useState(''); const [username, setUsername] = useState("");
const [password, setPassword] = useState(''); const [password, setPassword] = useState("");
const [showPassword, setShowPassword] = useState(false); const [showPassword, setShowPassword] = useState(false);
const [rememberMe, setRememberMe] = useState(false); const [rememberMe, setRememberMe] = useState(false);
const [error, setError] = useState(''); const [error, setError] = useState("");
const [showError, setShowError] = useState(false); const [showError, setShowError] = useState(false);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const { login } = useAuth(); const { login } = useAuth();
// Forgot password modal state
const [showForgotModal, setShowForgotModal] = useState(false); const [showForgotModal, setShowForgotModal] = useState(false);
const [forgotEmail, setForgotEmail] = useState(''); const [forgotEmail, setForgotEmail] = useState("");
const [forgotLoading, setForgotLoading] = useState(false); const [forgotLoading, setForgotLoading] = useState(false);
const [forgotSuccess, setForgotSuccess] = useState(false); const [forgotSuccess, setForgotSuccess] = useState(false);
const [forgotError, setForgotError] = useState(''); const [forgotError, setForgotError] = useState("");
// Auto-hide error after 5 seconds
useEffect(() => { useEffect(() => {
if (error) { if (error) {
setShowError(true); setShowError(true);
const timer = setTimeout(() => { const timer = setTimeout(() => {
setShowError(false); setShowError(false);
setTimeout(() => setError(''), 300); setTimeout(() => setError(""), 300);
}, 5000); }, 5000);
return () => clearTimeout(timer); return () => clearTimeout(timer);
} }
@@ -36,18 +44,20 @@ export const LoginPage: React.FC = () => {
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
setError(''); setError("");
setLoading(true); setLoading(true);
try { try {
await login(username, password); await login(username, password);
} catch (err: unknown) { } catch (err: unknown) {
const error = err as Error; const error = err as Error;
const errorMessage = error.message?.includes('401') || error.message?.includes('Unauthorized') || error.message?.includes('Invalid') const errorMessage = error.message?.includes("401") ||
? 'Invalid username or password' error.message?.includes("Unauthorized") ||
: error.message || 'Login failed. Please check your credentials.'; error.message?.includes("Invalid")
? "Invalid username or password"
: error.message || "Login failed. Please check your credentials.";
setError(errorMessage); setError(errorMessage);
console.error('Login error:', err); console.error("Login error:", err);
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -56,15 +66,15 @@ export const LoginPage: React.FC = () => {
const handleForgotPassword = async (e: React.FormEvent) => { const handleForgotPassword = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
setForgotLoading(true); setForgotLoading(true);
setForgotError(''); setForgotError("");
// Simulate API call (replace with actual API call) // Simulate API call (replace with actual API call)
try { try {
await new Promise(resolve => setTimeout(resolve, 1500)); await new Promise((resolve) => setTimeout(resolve, 1500));
// In a real app, you'd call: await api.requestPasswordReset(forgotEmail); // In a real app, you'd call: await api.requestPasswordReset(forgotEmail);
setForgotSuccess(true); setForgotSuccess(true);
} catch { } catch {
setForgotError('Failed to send reset email. Please try again.'); setForgotError("Failed to send reset email. Please try again.");
} finally { } finally {
setForgotLoading(false); setForgotLoading(false);
} }
@@ -72,9 +82,9 @@ export const LoginPage: React.FC = () => {
const closeForgotModal = () => { const closeForgotModal = () => {
setShowForgotModal(false); setShowForgotModal(false);
setForgotEmail(''); setForgotEmail("");
setForgotSuccess(false); setForgotSuccess(false);
setForgotError(''); setForgotError("");
}; };
return ( return (
@@ -109,10 +119,15 @@ export const LoginPage: React.FC = () => {
<div className="text-center mb-8"> <div className="text-center mb-8">
<div className="inline-flex items-center justify-center w-20 h-20 bg-gradient-to-br from-blue-500 to-purple-600 rounded-2xl shadow-lg mb-4 relative"> <div className="inline-flex items-center justify-center w-20 h-20 bg-gradient-to-br from-blue-500 to-purple-600 rounded-2xl shadow-lg mb-4 relative">
<Shield size={40} className="text-white" strokeWidth={1.5} /> <Shield size={40} className="text-white" strokeWidth={1.5} />
<Sparkles size={16} className="text-yellow-300 absolute -top-1 -right-1 animate-pulse" /> <Sparkles
size={16}
className="text-yellow-300 absolute -top-1 -right-1 animate-pulse"
/>
</div> </div>
<h1 className="text-2xl font-bold text-white mb-1">Welcome Back</h1> <h1 className="text-2xl font-bold text-white mb-1">Welcome Back</h1>
<p className="text-blue-200/70 text-sm">Sign in to your account to continue</p> <p className="text-blue-200/70 text-sm">
Sign in to your account to continue
</p>
</div> </div>
{/* Login Form */} {/* Login Form */}
@@ -139,7 +154,7 @@ export const LoginPage: React.FC = () => {
<Lock size={20} /> <Lock size={20} />
</div> </div>
<input <input
type={showPassword ? 'text' : 'password'} type={showPassword ? "text" : "password"}
value={password} value={password}
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
placeholder="Password" placeholder="Password"
@@ -165,7 +180,9 @@ export const LoginPage: React.FC = () => {
onChange={(e) => setRememberMe(e.target.checked)} onChange={(e) => setRememberMe(e.target.checked)}
className="w-4 h-4 bg-white/10 border-white/30 rounded text-blue-500 focus:ring-blue-400/50 focus:ring-offset-0" className="w-4 h-4 bg-white/10 border-white/30 rounded text-blue-500 focus:ring-blue-400/50 focus:ring-offset-0"
/> />
<span className="ml-2 text-blue-200/70 group-hover:text-blue-200 transition-colors">Remember me</span> <span className="ml-2 text-blue-200/70 group-hover:text-blue-200 transition-colors">
Remember me
</span>
</label> </label>
<button <button
type="button" type="button"
@@ -182,15 +199,20 @@ export const LoginPage: React.FC = () => {
disabled={loading || !username || !password} disabled={loading || !username || !password}
className="w-full bg-gradient-to-r from-blue-500 via-blue-600 to-purple-600 hover:from-blue-600 hover:via-blue-700 hover:to-purple-700 text-white font-semibold py-4 rounded-xl shadow-lg hover:shadow-blue-500/25 transition-all duration-300 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2 group" className="w-full bg-gradient-to-r from-blue-500 via-blue-600 to-purple-600 hover:from-blue-600 hover:via-blue-700 hover:to-purple-700 text-white font-semibold py-4 rounded-xl shadow-lg hover:shadow-blue-500/25 transition-all duration-300 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2 group"
> >
{loading ? ( {loading
? (
<> <>
<div className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin" /> <div className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin" />
Signing in... Signing in...
</> </>
) : ( )
: (
<> <>
Sign In Sign In
<ArrowRight size={18} className="group-hover:translate-x-1 transition-transform" /> <ArrowRight
size={18}
className="group-hover:translate-x-1 transition-transform"
/>
</> </>
)} )}
</button> </button>
@@ -217,7 +239,11 @@ export const LoginPage: React.FC = () => {
{/* Error Toast */} {/* Error Toast */}
{error && ( {error && (
<div className={`fixed top-6 left-1/2 transform -translate-x-1/2 z-50 transition-all duration-300 ${showError ? 'opacity-100 translate-y-0' : 'opacity-0 -translate-y-4'}`}> <div
className={`fixed top-6 left-1/2 transform -translate-x-1/2 z-50 transition-all duration-300 ${
showError ? "opacity-100 translate-y-0" : "opacity-0 -translate-y-4"
}`}
>
<div className="bg-gradient-to-r from-red-500 to-red-600 text-white px-6 py-4 rounded-2xl shadow-2xl flex items-center gap-3 min-w-[320px] border border-red-400/30"> <div className="bg-gradient-to-r from-red-500 to-red-600 text-white px-6 py-4 rounded-2xl shadow-2xl flex items-center gap-3 min-w-[320px] border border-red-400/30">
<div className="w-10 h-10 bg-white/20 rounded-full flex items-center justify-center flex-shrink-0"> <div className="w-10 h-10 bg-white/20 rounded-full flex items-center justify-center flex-shrink-0">
<XCircle size={24} /> <XCircle size={24} />
@@ -249,16 +275,20 @@ export const LoginPage: React.FC = () => {
<X size={24} /> <X size={24} />
</button> </button>
{!forgotSuccess ? ( {!forgotSuccess
? (
<> <>
{/* Header */} {/* Header */}
<div className="text-center mb-6"> <div className="text-center mb-6">
<div className="inline-flex items-center justify-center w-16 h-16 bg-gradient-to-br from-amber-500 to-orange-600 rounded-2xl shadow-lg mb-4"> <div className="inline-flex items-center justify-center w-16 h-16 bg-gradient-to-br from-amber-500 to-orange-600 rounded-2xl shadow-lg mb-4">
<KeyRound size={32} className="text-white" /> <KeyRound size={32} className="text-white" />
</div> </div>
<h2 className="text-xl font-bold text-white mb-2">Forgot Password?</h2> <h2 className="text-xl font-bold text-white mb-2">
Forgot Password?
</h2>
<p className="text-gray-400 text-sm"> <p className="text-gray-400 text-sm">
Enter your email address and we'll send you instructions to reset your password. Enter your email address and we'll send you instructions
to reset your password.
</p> </p>
</div> </div>
@@ -279,7 +309,9 @@ export const LoginPage: React.FC = () => {
</div> </div>
{forgotError && ( {forgotError && (
<p className="text-red-400 text-sm text-center">{forgotError}</p> <p className="text-red-400 text-sm text-center">
{forgotError}
</p>
)} )}
<button <button
@@ -287,12 +319,14 @@ export const LoginPage: React.FC = () => {
disabled={forgotLoading || !forgotEmail} disabled={forgotLoading || !forgotEmail}
className="w-full bg-gradient-to-r from-amber-500 to-orange-600 hover:from-amber-600 hover:to-orange-700 text-white font-semibold py-4 rounded-xl shadow-lg transition-all duration-300 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2" className="w-full bg-gradient-to-r from-amber-500 to-orange-600 hover:from-amber-600 hover:to-orange-700 text-white font-semibold py-4 rounded-xl shadow-lg transition-all duration-300 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
> >
{forgotLoading ? ( {forgotLoading
? (
<> <>
<div className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin" /> <div className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin" />
Sending... Sending...
</> </>
) : ( )
: (
<> <>
<Mail size={18} /> <Mail size={18} />
Send Reset Link Send Reset Link
@@ -309,16 +343,21 @@ export const LoginPage: React.FC = () => {
← Back to login ← Back to login
</button> </button>
</> </>
) : ( )
: (
/* Success State */ /* Success State */
<div className="text-center py-4"> <div className="text-center py-4">
<div className="inline-flex items-center justify-center w-16 h-16 bg-gradient-to-br from-green-500 to-emerald-600 rounded-full shadow-lg mb-4"> <div className="inline-flex items-center justify-center w-16 h-16 bg-gradient-to-br from-green-500 to-emerald-600 rounded-full shadow-lg mb-4">
<CheckCircle size={32} className="text-white" /> <CheckCircle size={32} className="text-white" />
</div> </div>
<h2 className="text-xl font-bold text-white mb-2">Check Your Email</h2> <h2 className="text-xl font-bold text-white mb-2">
Check Your Email
</h2>
<p className="text-gray-400 text-sm mb-6"> <p className="text-gray-400 text-sm mb-6">
We've sent password reset instructions to<br /> We've sent password reset instructions to<br />
<span className="text-white font-medium">{forgotEmail}</span> <span className="text-white font-medium">
{forgotEmail}
</span>
</p> </p>
<button <button
onClick={closeForgotModal} onClick={closeForgotModal}
@@ -333,7 +372,8 @@ export const LoginPage: React.FC = () => {
)} )}
{/* CSS for floating animation */} {/* CSS for floating animation */}
<style>{` <style>
{`
@keyframes float { @keyframes float {
0%, 100% { transform: translateY(0px) rotate(0deg); opacity: 0.2; } 0%, 100% { transform: translateY(0px) rotate(0deg); opacity: 0.2; }
50% { transform: translateY(-20px) rotate(180deg); opacity: 0.5; } 50% { transform: translateY(-20px) rotate(180deg); opacity: 0.5; }
@@ -341,7 +381,8 @@ export const LoginPage: React.FC = () => {
.animate-float { .animate-float {
animation: float linear infinite; animation: float linear infinite;
} }
`}</style> `}
</style>
</div> </div>
); );
}; };

View File

@@ -1,36 +1,43 @@
import React, { useState, useEffect, useMemo } from 'react'; import React, { useEffect, useMemo, useState } from "react";
import { Plus, RefreshCw, Trash2, Edit, DollarSign, Search } from 'lucide-react'; import { DollarSign, Edit, RefreshCw, Search, Trash2 } from "lucide-react";
import { Card, CardHeader, CardContent } from '../components/ui/Card'; import { Card, CardContent } from "../components/ui/Card.tsx";
import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from '../components/ui/Table'; import {
import { Button } from '../components/ui/Button'; Table,
import { Input, Select } from '../components/ui/Input'; TableBody,
import { api } from '../services/api'; TableCell,
import { useDepartments, useSubDepartments } from '../hooks/useDepartments'; TableHead,
import { useActivities } from '../hooks/useActivities'; TableHeader,
import { useAuth } from '../contexts/AuthContext'; TableRow,
} from "../components/ui/Table.tsx";
import { Button } from "../components/ui/Button.tsx";
import { Input, Select } from "../components/ui/Input.tsx";
import { api } from "../services/api.ts";
import { useDepartments, useSubDepartments } from "../hooks/useDepartments.ts";
import { useActivities } from "../hooks/useActivities.ts";
import { useAuth } from "../contexts/AuthContext.tsx";
export const RatesPage: React.FC = () => { export const RatesPage: React.FC = () => {
const [activeTab, setActiveTab] = useState<'list' | 'add'>('list'); const [activeTab, setActiveTab] = useState<"list" | "add">("list");
const { user } = useAuth(); const { user } = useAuth();
const { departments } = useDepartments(); const { departments } = useDepartments();
const [rates, setRates] = useState<any[]>([]); const [rates, setRates] = useState<any[]>([]);
const [contractors, setContractors] = useState<any[]>([]); const [contractors, setContractors] = useState<any[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState(''); const [error, setError] = useState("");
// Form state // Form state
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
contractorId: '', contractorId: "",
subDepartmentId: '', subDepartmentId: "",
activity: '', activity: "",
rate: '', rate: "",
effectiveDate: new Date().toISOString().split('T')[0], effectiveDate: new Date().toISOString().split("T")[0],
}); });
const [selectedDept, setSelectedDept] = useState(''); const [selectedDept, setSelectedDept] = useState("");
const { subDepartments } = useSubDepartments(selectedDept); const { subDepartments } = useSubDepartments(selectedDept);
const { activities } = useActivities(formData.subDepartmentId); const { activities } = useActivities(formData.subDepartmentId);
const [formError, setFormError] = useState(''); const [formError, setFormError] = useState("");
const [formLoading, setFormLoading] = useState(false); const [formLoading, setFormLoading] = useState(false);
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState("");
// Edit mode // Edit mode
const [editingId, setEditingId] = useState<number | null>(null); const [editingId, setEditingId] = useState<number | null>(null);
@@ -38,12 +45,12 @@ export const RatesPage: React.FC = () => {
// Fetch rates // Fetch rates
const fetchRates = async () => { const fetchRates = async () => {
setLoading(true); setLoading(true);
setError(''); setError("");
try { try {
const data = await api.getContractorRates(); const data = await api.getContractorRates();
setRates(data); setRates(data);
} catch (err: any) { } catch (err: any) {
setError(err.message || 'Failed to fetch rates'); setError(err.message || "Failed to fetch rates");
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -52,10 +59,10 @@ export const RatesPage: React.FC = () => {
// Fetch contractors // Fetch contractors
const fetchContractors = async () => { const fetchContractors = async () => {
try { try {
const data = await api.getUsers({ role: 'Contractor' }); const data = await api.getUsers({ role: "Contractor" });
setContractors(data); setContractors(data);
} catch (err) { } catch (err) {
console.error('Failed to fetch contractors:', err); console.error("Failed to fetch contractors:", err);
} }
}; };
@@ -66,54 +73,62 @@ export const RatesPage: React.FC = () => {
// Auto-select department for supervisors // Auto-select department for supervisors
useEffect(() => { useEffect(() => {
if (user?.role === 'Supervisor' && user?.department_id) { if (user?.role === "Supervisor" && user?.department_id) {
setSelectedDept(String(user.department_id)); setSelectedDept(String(user.department_id));
} }
}, [user]); }, [user]);
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => { const handleInputChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>,
) => {
const { name, value } = e.target; const { name, value } = e.target;
// Auto-select department when contractor is selected // Auto-select department when contractor is selected
if (name === 'contractorId' && value) { if (name === "contractorId" && value) {
const selectedContractor = contractors.find(c => String(c.id) === value); const selectedContractor = contractors.find((c) =>
String(c.id) === value
);
if (selectedContractor?.department_id) { if (selectedContractor?.department_id) {
setSelectedDept(String(selectedContractor.department_id)); setSelectedDept(String(selectedContractor.department_id));
// Clear sub-department and activity when contractor changes // Clear sub-department and activity when contractor changes
setFormData(prev => ({ ...prev, [name]: value, subDepartmentId: '', activity: '' })); setFormData((prev) => ({
...prev,
[name]: value,
subDepartmentId: "",
activity: "",
}));
} else { } else {
setFormData(prev => ({ ...prev, [name]: value })); setFormData((prev) => ({ ...prev, [name]: value }));
} }
} } // Clear activity when sub-department changes
// Clear activity when sub-department changes else if (name === "subDepartmentId") {
else if (name === 'subDepartmentId') { setFormData((prev) => ({ ...prev, [name]: value, activity: "" }));
setFormData(prev => ({ ...prev, [name]: value, activity: '' }));
} else { } else {
setFormData(prev => ({ ...prev, [name]: value })); setFormData((prev) => ({ ...prev, [name]: value }));
} }
setFormError(''); setFormError("");
}; };
const resetForm = () => { const resetForm = () => {
setFormData({ setFormData({
contractorId: '', contractorId: "",
subDepartmentId: '', subDepartmentId: "",
activity: '', activity: "",
rate: '', rate: "",
effectiveDate: new Date().toISOString().split('T')[0], effectiveDate: new Date().toISOString().split("T")[0],
}); });
setEditingId(null); setEditingId(null);
setFormError(''); setFormError("");
}; };
const handleSubmit = async () => { const handleSubmit = async () => {
if (!formData.contractorId || !formData.rate || !formData.effectiveDate) { if (!formData.contractorId || !formData.rate || !formData.effectiveDate) {
setFormError('Contractor, rate, and effective date are required'); setFormError("Contractor, rate, and effective date are required");
return; return;
} }
setFormLoading(true); setFormLoading(true);
setFormError(''); setFormError("");
try { try {
if (editingId) { if (editingId) {
@@ -125,7 +140,9 @@ export const RatesPage: React.FC = () => {
} else { } else {
await api.setContractorRate({ await api.setContractorRate({
contractorId: parseInt(formData.contractorId), contractorId: parseInt(formData.contractorId),
subDepartmentId: formData.subDepartmentId ? parseInt(formData.subDepartmentId) : undefined, subDepartmentId: formData.subDepartmentId
? parseInt(formData.subDepartmentId)
: undefined,
activity: formData.activity || undefined, activity: formData.activity || undefined,
rate: parseFloat(formData.rate), rate: parseFloat(formData.rate),
effectiveDate: formData.effectiveDate, effectiveDate: formData.effectiveDate,
@@ -133,10 +150,10 @@ export const RatesPage: React.FC = () => {
} }
resetForm(); resetForm();
setActiveTab('list'); setActiveTab("list");
fetchRates(); fetchRates();
} catch (err: any) { } catch (err: any) {
setFormError(err.message || 'Failed to save rate'); setFormError(err.message || "Failed to save rate");
} finally { } finally {
setFormLoading(false); setFormLoading(false);
} }
@@ -145,32 +162,36 @@ export const RatesPage: React.FC = () => {
const handleEdit = (rate: any) => { const handleEdit = (rate: any) => {
setFormData({ setFormData({
contractorId: String(rate.contractor_id), contractorId: String(rate.contractor_id),
subDepartmentId: rate.sub_department_id ? String(rate.sub_department_id) : '', subDepartmentId: rate.sub_department_id
activity: rate.activity || '', ? String(rate.sub_department_id)
: "",
activity: rate.activity || "",
rate: String(rate.rate), rate: String(rate.rate),
effectiveDate: rate.effective_date?.split('T')[0] || new Date().toISOString().split('T')[0], effectiveDate: rate.effective_date?.split("T")[0] ||
new Date().toISOString().split("T")[0],
}); });
setEditingId(rate.id); setEditingId(rate.id);
setActiveTab('add'); setActiveTab("add");
}; };
const handleDelete = async (id: number) => { const handleDelete = async (id: number) => {
if (!confirm('Are you sure you want to delete this rate?')) return; if (!confirm("Are you sure you want to delete this rate?")) return;
try { try {
await api.deleteContractorRate(id); await api.deleteContractorRate(id);
fetchRates(); fetchRates();
} catch (err: any) { } catch (err: any) {
alert(err.message || 'Failed to delete rate'); alert(err.message || "Failed to delete rate");
} }
}; };
const canManageRates = user?.role === 'SuperAdmin' || user?.role === 'Supervisor'; const canManageRates = user?.role === "SuperAdmin" ||
user?.role === "Supervisor";
// Filter rates based on search // Filter rates based on search
const filteredRates = useMemo(() => { const filteredRates = useMemo(() => {
if (!searchQuery) return rates; if (!searchQuery) return rates;
const query = searchQuery.toLowerCase(); const query = searchQuery.toLowerCase();
return rates.filter(rate => return rates.filter((rate) =>
rate.contractor_name?.toLowerCase().includes(query) || rate.contractor_name?.toLowerCase().includes(query) ||
rate.sub_department_name?.toLowerCase().includes(query) || rate.sub_department_name?.toLowerCase().includes(query) ||
rate.activity?.toLowerCase().includes(query) rate.activity?.toLowerCase().includes(query)
@@ -183,36 +204,42 @@ export const RatesPage: React.FC = () => {
<div className="border-b border-gray-200"> <div className="border-b border-gray-200">
<div className="flex space-x-8 px-6"> <div className="flex space-x-8 px-6">
<button <button
onClick={() => { setActiveTab('list'); resetForm(); }} onClick={() => {
setActiveTab("list");
resetForm();
}}
className={`py-4 px-2 border-b-2 font-medium text-sm ${ className={`py-4 px-2 border-b-2 font-medium text-sm ${
activeTab === 'list' activeTab === "list"
? 'border-blue-500 text-blue-600' ? "border-blue-500 text-blue-600"
: 'border-transparent text-gray-500 hover:text-gray-700' : "border-transparent text-gray-500 hover:text-gray-700"
}`} }`}
> >
Rate List Rate List
</button> </button>
{canManageRates && ( {canManageRates && (
<button <button
onClick={() => setActiveTab('add')} onClick={() => setActiveTab("add")}
className={`py-4 px-2 border-b-2 font-medium text-sm ${ className={`py-4 px-2 border-b-2 font-medium text-sm ${
activeTab === 'add' activeTab === "add"
? 'border-blue-500 text-blue-600' ? "border-blue-500 text-blue-600"
: 'border-transparent text-gray-500 hover:text-gray-700' : "border-transparent text-gray-500 hover:text-gray-700"
}`} }`}
> >
{editingId ? 'Edit Rate' : 'Add Rate'} {editingId ? "Edit Rate" : "Add Rate"}
</button> </button>
)} )}
</div> </div>
</div> </div>
<CardContent> <CardContent>
{activeTab === 'list' && ( {activeTab === "list" && (
<div> <div>
<div className="flex gap-4 mb-4"> <div className="flex gap-4 mb-4">
<div className="relative min-w-[300px] flex-1"> <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} /> <Search
className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"
size={18}
/>
<input <input
type="text" type="text"
placeholder="Search by contractor, sub-department, activity..." placeholder="Search by contractor, sub-department, activity..."
@@ -237,9 +264,10 @@ export const RatesPage: React.FC = () => {
</div> </div>
)} )}
{loading ? ( {loading
<div className="text-center py-8">Loading rates...</div> ? <div className="text-center py-8">Loading rates...</div>
) : filteredRates.length > 0 ? ( : filteredRates.length > 0
? (
<Table> <Table>
<TableHeader> <TableHeader>
<TableHead>Contractor</TableHead> <TableHead>Contractor</TableHead>
@@ -253,28 +281,38 @@ export const RatesPage: React.FC = () => {
<TableBody> <TableBody>
{filteredRates.map((rate) => ( {filteredRates.map((rate) => (
<TableRow key={rate.id}> <TableRow key={rate.id}>
<TableCell className="font-medium">{rate.contractor_name}</TableCell> <TableCell className="font-medium">
<TableCell>{rate.sub_department_name || '-'}</TableCell> {rate.contractor_name}
</TableCell>
<TableCell> <TableCell>
<span className={`px-2 py-1 rounded text-xs font-medium ${ {rate.sub_department_name || "-"}
rate.unit_of_measurement === 'Per Bag' </TableCell>
? 'bg-blue-100 text-blue-700' <TableCell>
: 'bg-gray-100 text-gray-700' <span
}`}> className={`px-2 py-1 rounded text-xs font-medium ${
{rate.activity || 'Standard'} rate.unit_of_measurement === "Per Bag"
? "bg-blue-100 text-blue-700"
: "bg-gray-100 text-gray-700"
}`}
>
{rate.activity || "Standard"}
</span> </span>
</TableCell> </TableCell>
<TableCell> <TableCell>
<span className="text-xs text-gray-500"> <span className="text-xs text-gray-500">
{rate.unit_of_measurement === 'Per Bag' {rate.unit_of_measurement === "Per Bag"
? 'Per Unit' ? "Per Unit"
: 'Flat Rate'} : "Flat Rate"}
</span> </span>
</TableCell> </TableCell>
<TableCell> <TableCell>
<span className="text-green-600 font-semibold">{rate.rate}</span> <span className="text-green-600 font-semibold">
{rate.rate}
</span>
</TableCell>
<TableCell>
{new Date(rate.effective_date).toLocaleDateString()}
</TableCell> </TableCell>
<TableCell>{new Date(rate.effective_date).toLocaleDateString()}</TableCell>
{canManageRates && ( {canManageRates && (
<TableCell> <TableCell>
<div className="flex gap-2"> <div className="flex gap-2">
@@ -303,25 +341,36 @@ export const RatesPage: React.FC = () => {
))} ))}
</TableBody> </TableBody>
</Table> </Table>
) : ( )
: (
<div className="text-center py-8 text-gray-500"> <div className="text-center py-8 text-gray-500">
{searchQuery ? 'No matching rates found' : 'No rates configured yet. Add one to get started!'} {searchQuery
? "No matching rates found"
: "No rates configured yet. Add one to get started!"}
</div> </div>
)} )}
</div> </div>
)} )}
{activeTab === 'add' && canManageRates && ( {activeTab === "add" && canManageRates && (
<div className="max-w-2xl space-y-6"> <div className="max-w-2xl space-y-6">
<h3 className="text-lg font-semibold text-gray-800"> <h3 className="text-lg font-semibold text-gray-800">
{editingId ? 'Edit Rate' : 'Add New Rate'} {editingId ? "Edit Rate" : "Add New Rate"}
</h3> </h3>
<div className="p-4 bg-blue-50 border border-blue-200 rounded-md"> <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> <h4 className="font-medium text-blue-800 mb-2">
Rate Calculation Info
</h4>
<ul className="text-sm text-blue-700 space-y-1"> <ul className="text-sm text-blue-700 space-y-1">
<li><strong>Per Bag Activities:</strong> Total = Units × Rate per Unit</li> <li>
<li><strong>Fixed Rate Activities:</strong> Total = Flat Rate (no unit calculation)</li> <strong>Per Bag Activities:</strong>{" "}
Total = Units × Rate per Unit
</li>
<li>
<strong>Fixed Rate Activities:</strong>{" "}
Total = Flat Rate (no unit calculation)
</li>
</ul> </ul>
</div> </div>
@@ -340,24 +389,34 @@ export const RatesPage: React.FC = () => {
required required
disabled={!!editingId} disabled={!!editingId}
options={[ options={[
{ value: '', label: 'Select Contractor' }, { value: "", label: "Select Contractor" },
...contractors.map(c => ({ value: String(c.id), label: c.name })) ...contractors.map((c) => ({
value: String(c.id),
label: c.name,
})),
]} ]}
/> />
{user?.role === 'Supervisor' ? ( {user?.role === "Supervisor"
? (
<Input <Input
label="Department" label="Department"
value={departments.find(d => d.id === user?.department_id)?.name || 'Loading...'} value={departments.find((d) =>
d.id === user?.department_id
)?.name || "Loading..."}
disabled disabled
/> />
) : ( )
: (
<Select <Select
label="Department" label="Department"
value={selectedDept} value={selectedDept}
onChange={(e) => setSelectedDept(e.target.value)} onChange={(e) => setSelectedDept(e.target.value)}
options={[ options={[
{ value: '', label: 'Select Department' }, { value: "", label: "Select Department" },
...departments.map(d => ({ value: String(d.id), label: d.name })) ...departments.map((d) => ({
value: String(d.id),
label: d.name,
})),
]} ]}
/> />
)} )}
@@ -368,8 +427,11 @@ export const RatesPage: React.FC = () => {
onChange={handleInputChange} onChange={handleInputChange}
disabled={!!editingId} disabled={!!editingId}
options={[ options={[
{ value: '', label: 'Select Sub-Department (Optional)' }, { value: "", label: "Select Sub-Department (Optional)" },
...subDepartments.map(s => ({ value: String(s.id), label: s.name })) ...subDepartments.map((s) => ({
value: String(s.id),
label: s.name,
})),
]} ]}
/> />
<Select <Select
@@ -379,17 +441,28 @@ export const RatesPage: React.FC = () => {
onChange={handleInputChange} onChange={handleInputChange}
disabled={!formData.subDepartmentId} disabled={!formData.subDepartmentId}
options={[ options={[
{ value: '', label: formData.subDepartmentId ? 'Select Activity (Optional)' : 'Select Sub-Department First' }, {
...activities.map(a => ({ value: "",
label: formData.subDepartmentId
? "Select Activity (Optional)"
: "Select Sub-Department First",
},
...activities.map((a) => ({
value: a.name, value: a.name,
label: `${a.name} (${a.unit_of_measurement === 'Per Bag' ? 'per unit × rate' : 'flat rate'})` label: `${a.name} (${
})) a.unit_of_measurement === "Per Bag"
? "per unit × rate"
: "flat rate"
})`,
})),
]} ]}
/> />
<Input <Input
label={(() => { label={(() => {
const selectedActivity = activities.find(a => a.name === formData.activity); const selectedActivity = activities.find((a) =>
return selectedActivity?.unit_of_measurement === 'Per Bag' a.name === formData.activity
);
return selectedActivity?.unit_of_measurement === "Per Bag"
? "Rate per Unit (₹)" ? "Rate per Unit (₹)"
: "Rate Amount (₹)"; : "Rate Amount (₹)";
})()} })()}
@@ -411,14 +484,20 @@ export const RatesPage: React.FC = () => {
</div> </div>
<div className="flex justify-end gap-4"> <div className="flex justify-end gap-4">
<Button variant="outline" onClick={() => { setActiveTab('list'); resetForm(); }}> <Button
variant="outline"
onClick={() => {
setActiveTab("list");
resetForm();
}}
>
Cancel Cancel
</Button> </Button>
<Button onClick={handleSubmit} disabled={formLoading}> <Button onClick={handleSubmit} disabled={formLoading}>
{formLoading ? 'Saving...' : ( {formLoading ? "Saving..." : (
<> <>
<DollarSign size={16} className="mr-2" /> <DollarSign size={16} className="mr-2" />
{editingId ? 'Update Rate' : 'Add Rate'} {editingId ? "Update Rate" : "Add Rate"}
</> </>
)} )}
</Button> </Button>

View File

@@ -1,55 +1,104 @@
import React, { useState, useEffect, useMemo } from 'react'; import React, { useEffect, useMemo, useState } from "react";
import { Download, RefreshCw, Search, FileSpreadsheet, Filter } from 'lucide-react'; import {
import { Card, CardContent } from '../components/ui/Card'; Download,
import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from '../components/ui/Table'; FileSpreadsheet,
import { Button } from '../components/ui/Button'; Filter,
import { Input, Select } from '../components/ui/Input'; RefreshCw,
import { api } from '../services/api'; Search,
import { useDepartments } from '../hooks/useDepartments'; } from "lucide-react";
import { useAuth } from '../contexts/AuthContext'; import { Card, CardContent } from "../components/ui/Card.tsx";
import { exportWorkReportToXLSX, exportAllocationsToXLSX } from '../utils/excelExport'; 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 { api } from "../services/api.ts";
import { useDepartments } from "../hooks/useDepartments.ts";
import { useAuth } from "../contexts/AuthContext.tsx";
import {
exportAllocationsToXLSX,
exportWorkReportToXLSX,
} from "../utils/excelExport.ts";
export const ReportingPage: React.FC = () => { export const ReportingPage: React.FC = () => {
const { user } = useAuth(); const { user } = useAuth();
const { departments } = useDepartments(); const { departments } = useDepartments();
const [allocations, setAllocations] = useState<any[]>([]); const [allocations, setAllocations] = useState<any[]>([]);
const [summary, setSummary] = useState<{ totalAllocations: number; totalAmount: string; totalUnits: string } | null>(null); const [summary, setSummary] = useState<
{ totalAllocations: number; totalAmount: string; totalUnits: string } | null
>(null);
const [contractors, setContractors] = useState<any[]>([]); const [contractors, setContractors] = useState<any[]>([]);
const [employees, setEmployees] = useState<any[]>([]);
const [subDepartments, setSubDepartments] = useState<any[]>([]);
const [activities, setActivities] = useState<string[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState(''); const [error, setError] = useState("");
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState("");
// Filters // Filters
const [filters, setFilters] = useState({ const [filters, setFilters] = useState({
startDate: '', startDate: "",
endDate: '', endDate: "",
departmentId: '', departmentId: "",
contractorId: '', contractorId: "",
employeeId: "",
subDepartmentId: "",
activity: "",
}); });
const isSuperAdmin = user?.role === 'SuperAdmin'; const isSupervisor = user?.role === "Supervisor";
const isContractor = user?.role === "Contractor";
// Fetch contractors // Fetch filter options
useEffect(() => { useEffect(() => {
api.getUsers({ role: 'Contractor' }).then(setContractors).catch(console.error); api.getUsers({ role: "Contractor" }).then(setContractors).catch(
console.error,
);
api.getUsers({ role: "Employee" }).then(setEmployees).catch(console.error);
api.getAllSubDepartments().then(setSubDepartments).catch(console.error);
}, []); }, []);
// Fetch report data // Fetch report data
const fetchReport = async () => { const fetchReport = async () => {
setLoading(true); setLoading(true);
setError(''); setError("");
try { try {
const params: any = {}; const params: any = {};
if (filters.startDate) params.startDate = filters.startDate; if (filters.startDate) params.startDate = filters.startDate;
if (filters.endDate) params.endDate = filters.endDate; if (filters.endDate) params.endDate = filters.endDate;
if (filters.departmentId && isSuperAdmin) params.departmentId = parseInt(filters.departmentId);
if (filters.contractorId) params.contractorId = parseInt(filters.contractorId); // Department filter - use user's department if Supervisor, otherwise use filter
const deptId = isSupervisor
? user?.department_id
: (filters.departmentId ? parseInt(filters.departmentId) : null);
if (deptId) params.departmentId = deptId;
// Contractor filter - use user's id if Contractor, otherwise use filter
const contractorIdValue = isContractor
? user?.id
: (filters.contractorId ? parseInt(filters.contractorId) : null);
if (contractorIdValue) params.contractorId = contractorIdValue;
if (filters.employeeId) params.employeeId = parseInt(filters.employeeId);
const data = await api.getCompletedAllocationsReport(params); const data = await api.getCompletedAllocationsReport(params);
setAllocations(data.allocations); setAllocations(data.allocations);
setSummary(data.summary); setSummary(data.summary);
// Extract unique activities from allocations for the filter dropdown
const uniqueActivities = [
...new Set(
data.allocations.map((a: any) => a.activity).filter(Boolean),
),
] as string[];
setActivities(uniqueActivities);
} catch (err: any) { } catch (err: any) {
setError(err.message || 'Failed to fetch report'); setError(err.message || "Failed to fetch report");
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -59,53 +108,75 @@ export const ReportingPage: React.FC = () => {
fetchReport(); fetchReport();
}, []); }, []);
// Filter allocations based on search // Filter allocations based on search and dropdown filters
const filteredAllocations = useMemo(() => { const filteredAllocations = useMemo(() => {
if (!searchQuery) return allocations; let result = allocations;
// Apply search filter
if (searchQuery) {
const query = searchQuery.toLowerCase(); const query = searchQuery.toLowerCase();
return allocations.filter(a => result = result.filter((a) =>
a.employee_name?.toLowerCase().includes(query) || a.employee_name?.toLowerCase().includes(query) ||
a.contractor_name?.toLowerCase().includes(query) || a.contractor_name?.toLowerCase().includes(query) ||
a.sub_department_name?.toLowerCase().includes(query) || a.sub_department_name?.toLowerCase().includes(query) ||
a.activity?.toLowerCase().includes(query) || a.activity?.toLowerCase().includes(query) ||
a.department_name?.toLowerCase().includes(query) a.department_name?.toLowerCase().includes(query)
); );
}, [allocations, searchQuery]); }
// Apply sub-department filter (client-side)
if (filters.subDepartmentId) {
result = result.filter((a) =>
a.sub_department_id === parseInt(filters.subDepartmentId)
);
}
// Apply activity filter (client-side)
if (filters.activity) {
result = result.filter((a) => a.activity === filters.activity);
}
return result;
}, [allocations, searchQuery, filters.subDepartmentId, filters.activity]);
// Get selected department name // Get selected department name
const selectedDeptName = filters.departmentId const selectedDeptName = filters.departmentId
? departments.find(d => d.id === parseInt(filters.departmentId))?.name || 'All Departments' ? departments.find((d) => d.id === parseInt(filters.departmentId))?.name ||
: user?.role === 'Supervisor' "All Departments"
? departments.find(d => d.id === user?.department_id)?.name || 'Department' : user?.role === "Supervisor"
: 'All Departments'; ? departments.find((d) => d.id === user?.department_id)?.name ||
"Department"
: "All Departments";
// Export to Excel (XLSX format) - Formatted Report // Export to Excel (XLSX format) - Formatted Report
const exportFormattedReport = () => { const exportFormattedReport = () => {
if (filteredAllocations.length === 0) { if (filteredAllocations.length === 0) {
alert('No data to export'); alert("No data to export");
return; return;
} }
exportWorkReportToXLSX( exportWorkReportToXLSX(
filteredAllocations, filteredAllocations,
selectedDeptName, selectedDeptName,
{ startDate: filters.startDate, endDate: filters.endDate } { startDate: filters.startDate, endDate: filters.endDate },
); );
}; };
// Export to Excel (XLSX format) - Simple List // Export to Excel (XLSX format) - Simple List
const exportSimpleList = () => { const exportSimpleList = () => {
if (filteredAllocations.length === 0) { if (filteredAllocations.length === 0) {
alert('No data to export'); alert("No data to export");
return; return;
} }
exportAllocationsToXLSX(filteredAllocations); exportAllocationsToXLSX(filteredAllocations);
}; };
const handleFilterChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => { const handleFilterChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>,
) => {
const { name, value } = e.target; const { name, value } = e.target;
setFilters(prev => ({ ...prev, [name]: value })); setFilters((prev) => ({ ...prev, [name]: value }));
}; };
const applyFilters = () => { const applyFilters = () => {
@@ -114,10 +185,13 @@ export const ReportingPage: React.FC = () => {
const clearFilters = () => { const clearFilters = () => {
setFilters({ setFilters({
startDate: '', startDate: "",
endDate: '', endDate: "",
departmentId: '', departmentId: "",
contractorId: '', contractorId: "",
employeeId: "",
subDepartmentId: "",
activity: "",
}); });
setTimeout(fetchReport, 0); setTimeout(fetchReport, 0);
}; };
@@ -129,14 +203,23 @@ export const ReportingPage: React.FC = () => {
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<FileSpreadsheet className="text-green-600" size={24} /> <FileSpreadsheet className="text-green-600" size={24} />
<h2 className="text-xl font-semibold text-gray-800">Work Allocation Reports</h2> <h2 className="text-xl font-semibold text-gray-800">
Work Allocation Reports
</h2>
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<Button onClick={exportFormattedReport} disabled={filteredAllocations.length === 0}> <Button
onClick={exportFormattedReport}
disabled={filteredAllocations.length === 0}
>
<Download size={16} className="mr-2" /> <Download size={16} className="mr-2" />
Export Report (XLSX) Export Report (XLSX)
</Button> </Button>
<Button variant="outline" onClick={exportSimpleList} disabled={filteredAllocations.length === 0}> <Button
variant="outline"
onClick={exportSimpleList}
disabled={filteredAllocations.length === 0}
>
<Download size={16} className="mr-2" /> <Download size={16} className="mr-2" />
Export List Export List
</Button> </Button>
@@ -153,7 +236,9 @@ export const ReportingPage: React.FC = () => {
</div> </div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4"> <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1">Start Date</label> <label className="block text-sm font-medium text-gray-700 mb-1">
Start Date
</label>
<Input <Input
type="date" type="date"
name="startDate" name="startDate"
@@ -162,7 +247,9 @@ export const ReportingPage: React.FC = () => {
/> />
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1">End Date</label> <label className="block text-sm font-medium text-gray-700 mb-1">
End Date
</label>
<Input <Input
type="date" type="date"
name="endDate" name="endDate"
@@ -170,26 +257,74 @@ export const ReportingPage: React.FC = () => {
onChange={handleFilterChange} onChange={handleFilterChange}
/> />
</div> </div>
{isSuperAdmin && (
<Select <Select
label="Department" label="Department"
name="departmentId" name="departmentId"
value={filters.departmentId} value={isSupervisor
? String(user?.department_id || "")
: filters.departmentId}
onChange={handleFilterChange} onChange={handleFilterChange}
disabled={isSupervisor}
options={[ options={[
{ value: '', label: 'All Departments' }, { value: "", label: "All Departments" },
...departments.map(d => ({ value: String(d.id), label: d.name })) ...departments.map((d) => ({
value: String(d.id),
label: d.name,
})),
]} ]}
/> />
)}
<Select <Select
label="Contractor" label="Contractor"
name="contractorId" name="contractorId"
value={filters.contractorId} value={isContractor
? String(user?.id || "")
: filters.contractorId}
onChange={handleFilterChange}
disabled={isContractor}
options={[
{ value: "", label: "All Contractors" },
...contractors.map((c) => ({
value: String(c.id),
label: c.name,
})),
]}
/>
</div>
<div className="grid grid-cols-2 md:grid-cols-3 gap-4 mt-4">
<Select
label="Employee"
name="employeeId"
value={filters.employeeId}
onChange={handleFilterChange} onChange={handleFilterChange}
options={[ options={[
{ value: '', label: 'All Contractors' }, { value: "", label: "All Employees" },
...contractors.map(c => ({ value: String(c.id), label: c.name })) ...employees.map((e) => ({
value: String(e.id),
label: e.name,
})),
]}
/>
<Select
label="Sub-Department"
name="subDepartmentId"
value={filters.subDepartmentId}
onChange={handleFilterChange}
options={[
{ value: "", label: "All Sub-Departments" },
...subDepartments.map((sd) => ({
value: String(sd.id),
label: sd.name,
})),
]}
/>
<Select
label="Activity"
name="activity"
value={filters.activity}
onChange={handleFilterChange}
options={[
{ value: "", label: "All Activities" },
...activities.map((a) => ({ value: a, label: a })),
]} ]}
/> />
</div> </div>
@@ -207,16 +342,28 @@ export const ReportingPage: React.FC = () => {
{summary && ( {summary && (
<div className="grid grid-cols-3 gap-4 mb-6"> <div className="grid grid-cols-3 gap-4 mb-6">
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4"> <div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<div className="text-sm text-blue-600 font-medium">Total Completed</div> <div className="text-sm text-blue-600 font-medium">
<div className="text-2xl font-bold text-blue-800">{summary.totalAllocations}</div> Total Completed
</div>
<div className="text-2xl font-bold text-blue-800">
{summary.totalAllocations}
</div>
</div> </div>
<div className="bg-green-50 border border-green-200 rounded-lg p-4"> <div className="bg-green-50 border border-green-200 rounded-lg p-4">
<div className="text-sm text-green-600 font-medium">Total Amount</div> <div className="text-sm text-green-600 font-medium">
<div className="text-2xl font-bold text-green-800">{parseFloat(summary.totalAmount).toLocaleString()}</div> Total Amount
</div>
<div className="text-2xl font-bold text-green-800">
{parseFloat(summary.totalAmount).toLocaleString()}
</div>
</div> </div>
<div className="bg-purple-50 border border-purple-200 rounded-lg p-4"> <div className="bg-purple-50 border border-purple-200 rounded-lg p-4">
<div className="text-sm text-purple-600 font-medium">Total Units</div> <div className="text-sm text-purple-600 font-medium">
<div className="text-2xl font-bold text-purple-800">{parseFloat(summary.totalUnits).toLocaleString()}</div> Total Units
</div>
<div className="text-2xl font-bold text-purple-800">
{parseFloat(summary.totalUnits).toLocaleString()}
</div>
</div> </div>
</div> </div>
)} )}
@@ -224,7 +371,10 @@ export const ReportingPage: React.FC = () => {
{/* Search and Refresh */} {/* Search and Refresh */}
<div className="flex gap-4 mb-4"> <div className="flex gap-4 mb-4">
<div className="relative flex-1"> <div className="relative flex-1">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400" size={18} /> <Search
className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"
size={18}
/>
<input <input
type="text" type="text"
placeholder="Search by employee, contractor, department..." placeholder="Search by employee, contractor, department..."
@@ -247,9 +397,10 @@ export const ReportingPage: React.FC = () => {
)} )}
{/* Table */} {/* Table */}
{loading ? ( {loading
<div className="text-center py-8">Loading report data...</div> ? <div className="text-center py-8">Loading report data...</div>
) : filteredAllocations.length > 0 ? ( : filteredAllocations.length > 0
? (
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<Table> <Table>
<TableHeader> <TableHeader>
@@ -274,37 +425,56 @@ export const ReportingPage: React.FC = () => {
return ( return (
<TableRow key={allocation.id}> <TableRow key={allocation.id}>
<TableCell>{allocation.id}</TableCell> <TableCell>{allocation.id}</TableCell>
<TableCell className="font-medium">{allocation.employee_name || '-'}</TableCell> <TableCell className="font-medium">
<TableCell>{allocation.contractor_name || '-'}</TableCell> {allocation.employee_name || "-"}
<TableCell>{allocation.department_name || '-'}</TableCell> </TableCell>
<TableCell>{allocation.sub_department_name || '-'}</TableCell>
<TableCell> <TableCell>
<span className={`px-2 py-1 rounded text-xs font-medium ${ {allocation.contractor_name || "-"}
allocation.activity === 'Loading' || allocation.activity === 'Unloading' </TableCell>
? 'bg-purple-100 text-purple-700' <TableCell>
: 'bg-gray-100 text-gray-700' {allocation.department_name || "-"}
}`}> </TableCell>
{allocation.activity || 'Standard'} <TableCell>
{allocation.sub_department_name || "-"}
</TableCell>
<TableCell>
<span
className={`px-2 py-1 rounded text-xs font-medium ${
allocation.activity === "Loading" ||
allocation.activity === "Unloading"
? "bg-purple-100 text-purple-700"
: "bg-gray-100 text-gray-700"
}`}
>
{allocation.activity || "Standard"}
</span> </span>
</TableCell> </TableCell>
<TableCell>{new Date(allocation.assigned_date).toLocaleDateString()}</TableCell> <TableCell>
{new Date(allocation.assigned_date)
.toLocaleDateString()}
</TableCell>
<TableCell> <TableCell>
{allocation.completion_date {allocation.completion_date
? new Date(allocation.completion_date).toLocaleDateString() ? new Date(allocation.completion_date)
: '-'} .toLocaleDateString()
: "-"}
</TableCell> </TableCell>
<TableCell>{rate.toFixed(2)}</TableCell> <TableCell>{rate.toFixed(2)}</TableCell>
<TableCell>{units > 0 ? units : '-'}</TableCell> <TableCell>{units > 0 ? units : "-"}</TableCell>
<TableCell className="font-semibold text-green-600">{total.toFixed(2)}</TableCell> <TableCell className="font-semibold text-green-600">
{total.toFixed(2)}
</TableCell>
</TableRow> </TableRow>
); );
})} })}
</TableBody> </TableBody>
</Table> </Table>
</div> </div>
) : ( )
: (
<div className="text-center py-8 text-gray-500"> <div className="text-center py-8 text-gray-500">
No completed work allocations found. Adjust your filters or check back later. No completed work allocations found. Adjust your filters or
check back later.
</div> </div>
)} )}
</CardContent> </CardContent>

View File

@@ -1,54 +1,72 @@
import React, { useState, useEffect, useMemo } from 'react'; import React, { useEffect, useMemo, useState } from "react";
import { RefreshCw, Trash2, Edit, DollarSign, Search, Scale, ArrowUpDown } from 'lucide-react'; import {
import { Card, CardContent } from '../components/ui/Card'; ArrowUpDown,
import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from '../components/ui/Table'; DollarSign,
import { Button } from '../components/ui/Button'; Edit,
import { Input, Select } from '../components/ui/Input'; RefreshCw,
import { api } from '../services/api'; Scale,
import { useDepartments, useSubDepartments } from '../hooks/useDepartments'; Search,
import { useActivities } from '../hooks/useActivities'; Trash2,
import { useAuth } from '../contexts/AuthContext'; } from "lucide-react";
import { Card, CardContent } 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 { api } from "../services/api.ts";
import { useDepartments, useSubDepartments } from "../hooks/useDepartments.ts";
import { useActivities } from "../hooks/useActivities.ts";
import { useAuth } from "../contexts/AuthContext.tsx";
export const StandardRatesPage: React.FC = () => { export const StandardRatesPage: React.FC = () => {
const [activeTab, setActiveTab] = useState<'list' | 'add' | 'compare'>('list'); const [activeTab, setActiveTab] = useState<"list" | "add" | "compare">(
"list",
);
const { user } = useAuth(); const { user } = useAuth();
const { departments } = useDepartments(); const { departments } = useDepartments();
const [standardRates, setStandardRates] = useState<any[]>([]); const [standardRates, setStandardRates] = useState<any[]>([]);
const [contractors, setContractors] = useState<any[]>([]); const [contractors, setContractors] = useState<any[]>([]);
const [comparisons, setComparisons] = useState<any[]>([]); const [comparisons, setComparisons] = useState<any[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState(''); const [error, setError] = useState("");
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState("");
// Form state // Form state
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
subDepartmentId: '', subDepartmentId: "",
activity: '', activity: "",
rate: '', rate: "",
effectiveDate: new Date().toISOString().split('T')[0], effectiveDate: new Date().toISOString().split("T")[0],
}); });
const [selectedDept, setSelectedDept] = useState(''); const [selectedDept, setSelectedDept] = useState("");
const { subDepartments } = useSubDepartments(selectedDept); const { subDepartments } = useSubDepartments(selectedDept);
const { activities } = useActivities(formData.subDepartmentId); const { activities } = useActivities(formData.subDepartmentId);
const [formError, setFormError] = useState(''); const [formError, setFormError] = useState("");
const [formLoading, setFormLoading] = useState(false); const [formLoading, setFormLoading] = useState(false);
const [editingId, setEditingId] = useState<number | null>(null); const [editingId, setEditingId] = useState<number | null>(null);
// Compare filters // Compare filters
const [compareContractorId, setCompareContractorId] = useState(''); const [compareContractorId, setCompareContractorId] = useState("");
const isSupervisor = user?.role === 'Supervisor'; const isSupervisor = user?.role === "Supervisor";
const canManageRates = user?.role === 'SuperAdmin' || user?.role === 'Supervisor'; const canManageRates = user?.role === "SuperAdmin" ||
user?.role === "Supervisor";
// Fetch standard rates // Fetch standard rates
const fetchStandardRates = async () => { const fetchStandardRates = async () => {
setLoading(true); setLoading(true);
setError(''); setError("");
try { try {
const data = await api.getStandardRates(); const data = await api.getStandardRates();
setStandardRates(data); setStandardRates(data);
} catch (err: any) { } catch (err: any) {
setError(err.message || 'Failed to fetch standard rates'); setError(err.message || "Failed to fetch standard rates");
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -57,10 +75,10 @@ export const StandardRatesPage: React.FC = () => {
// Fetch contractors // Fetch contractors
const fetchContractors = async () => { const fetchContractors = async () => {
try { try {
const data = await api.getUsers({ role: 'Contractor' }); const data = await api.getUsers({ role: "Contractor" });
setContractors(data); setContractors(data);
} catch (err) { } catch (err) {
console.error('Failed to fetch contractors:', err); console.error("Failed to fetch contractors:", err);
} }
}; };
@@ -72,10 +90,12 @@ export const StandardRatesPage: React.FC = () => {
} }
setLoading(true); setLoading(true);
try { try {
const data = await api.compareRates({ contractorId: parseInt(compareContractorId) }); const data = await api.compareRates({
contractorId: parseInt(compareContractorId),
});
setComparisons(data.comparisons); setComparisons(data.comparisons);
} catch (err: any) { } catch (err: any) {
setError(err.message || 'Failed to fetch comparison'); setError(err.message || "Failed to fetch comparison");
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -93,41 +113,43 @@ export const StandardRatesPage: React.FC = () => {
}, [isSupervisor, user?.department_id]); }, [isSupervisor, user?.department_id]);
useEffect(() => { useEffect(() => {
if (activeTab === 'compare' && compareContractorId) { if (activeTab === "compare" && compareContractorId) {
fetchComparison(); fetchComparison();
} }
}, [activeTab, compareContractorId]); }, [activeTab, compareContractorId]);
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => { const handleInputChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>,
) => {
const { name, value } = e.target; const { name, value } = e.target;
// Clear activity when sub-department changes // Clear activity when sub-department changes
if (name === 'subDepartmentId') { if (name === "subDepartmentId") {
setFormData(prev => ({ ...prev, [name]: value, activity: '' })); setFormData((prev) => ({ ...prev, [name]: value, activity: "" }));
} else { } else {
setFormData(prev => ({ ...prev, [name]: value })); setFormData((prev) => ({ ...prev, [name]: value }));
} }
setFormError(''); setFormError("");
}; };
const resetForm = () => { const resetForm = () => {
setFormData({ setFormData({
subDepartmentId: '', subDepartmentId: "",
activity: '', activity: "",
rate: '', rate: "",
effectiveDate: new Date().toISOString().split('T')[0], effectiveDate: new Date().toISOString().split("T")[0],
}); });
setEditingId(null); setEditingId(null);
setFormError(''); setFormError("");
}; };
const handleSubmit = async () => { const handleSubmit = async () => {
if (!formData.rate || !formData.effectiveDate) { if (!formData.rate || !formData.effectiveDate) {
setFormError('Rate and effective date are required'); setFormError("Rate and effective date are required");
return; return;
} }
setFormLoading(true); setFormLoading(true);
setFormError(''); setFormError("");
try { try {
if (editingId) { if (editingId) {
@@ -138,7 +160,9 @@ export const StandardRatesPage: React.FC = () => {
}); });
} else { } else {
await api.createStandardRate({ await api.createStandardRate({
subDepartmentId: formData.subDepartmentId ? parseInt(formData.subDepartmentId) : undefined, subDepartmentId: formData.subDepartmentId
? parseInt(formData.subDepartmentId)
: undefined,
activity: formData.activity || undefined, activity: formData.activity || undefined,
rate: parseFloat(formData.rate), rate: parseFloat(formData.rate),
effectiveDate: formData.effectiveDate, effectiveDate: formData.effectiveDate,
@@ -146,10 +170,10 @@ export const StandardRatesPage: React.FC = () => {
} }
resetForm(); resetForm();
setActiveTab('list'); setActiveTab("list");
fetchStandardRates(); fetchStandardRates();
} catch (err: any) { } catch (err: any) {
setFormError(err.message || 'Failed to save rate'); setFormError(err.message || "Failed to save rate");
} finally { } finally {
setFormLoading(false); setFormLoading(false);
} }
@@ -157,25 +181,28 @@ export const StandardRatesPage: React.FC = () => {
const handleEdit = (rate: any) => { const handleEdit = (rate: any) => {
setFormData({ setFormData({
subDepartmentId: rate.sub_department_id ? String(rate.sub_department_id) : '', subDepartmentId: rate.sub_department_id
activity: rate.activity || '', ? String(rate.sub_department_id)
: "",
activity: rate.activity || "",
rate: String(rate.rate), rate: String(rate.rate),
effectiveDate: rate.effective_date?.split('T')[0] || new Date().toISOString().split('T')[0], effectiveDate: rate.effective_date?.split("T")[0] ||
new Date().toISOString().split("T")[0],
}); });
if (rate.department_id) { if (rate.department_id) {
setSelectedDept(String(rate.department_id)); setSelectedDept(String(rate.department_id));
} }
setEditingId(rate.id); setEditingId(rate.id);
setActiveTab('add'); setActiveTab("add");
}; };
const handleDelete = async (id: number) => { const handleDelete = async (id: number) => {
if (!confirm('Are you sure you want to delete this standard rate?')) return; if (!confirm("Are you sure you want to delete this standard rate?")) return;
try { try {
await api.deleteStandardRate(id); await api.deleteStandardRate(id);
fetchStandardRates(); fetchStandardRates();
} catch (err: any) { } catch (err: any) {
alert(err.message || 'Failed to delete rate'); alert(err.message || "Failed to delete rate");
} }
}; };
@@ -183,7 +210,7 @@ export const StandardRatesPage: React.FC = () => {
const filteredRates = useMemo(() => { const filteredRates = useMemo(() => {
if (!searchQuery) return standardRates; if (!searchQuery) return standardRates;
const query = searchQuery.toLowerCase(); const query = searchQuery.toLowerCase();
return standardRates.filter(rate => return standardRates.filter((rate) =>
rate.sub_department_name?.toLowerCase().includes(query) || rate.sub_department_name?.toLowerCase().includes(query) ||
rate.department_name?.toLowerCase().includes(query) || rate.department_name?.toLowerCase().includes(query) ||
rate.activity?.toLowerCase().includes(query) || rate.activity?.toLowerCase().includes(query) ||
@@ -197,33 +224,36 @@ export const StandardRatesPage: React.FC = () => {
<div className="border-b border-gray-200"> <div className="border-b border-gray-200">
<div className="flex space-x-8 px-6"> <div className="flex space-x-8 px-6">
<button <button
onClick={() => { setActiveTab('list'); resetForm(); }} onClick={() => {
setActiveTab("list");
resetForm();
}}
className={`py-4 px-2 border-b-2 font-medium text-sm ${ className={`py-4 px-2 border-b-2 font-medium text-sm ${
activeTab === 'list' activeTab === "list"
? 'border-blue-500 text-blue-600' ? "border-blue-500 text-blue-600"
: 'border-transparent text-gray-500 hover:text-gray-700' : "border-transparent text-gray-500 hover:text-gray-700"
}`} }`}
> >
Standard Rates Standard Rates
</button> </button>
{canManageRates && ( {canManageRates && (
<button <button
onClick={() => setActiveTab('add')} onClick={() => setActiveTab("add")}
className={`py-4 px-2 border-b-2 font-medium text-sm ${ className={`py-4 px-2 border-b-2 font-medium text-sm ${
activeTab === 'add' activeTab === "add"
? 'border-blue-500 text-blue-600' ? "border-blue-500 text-blue-600"
: 'border-transparent text-gray-500 hover:text-gray-700' : "border-transparent text-gray-500 hover:text-gray-700"
}`} }`}
> >
{editingId ? 'Edit Rate' : 'Add Standard Rate'} {editingId ? "Edit Rate" : "Add Standard Rate"}
</button> </button>
)} )}
<button <button
onClick={() => setActiveTab('compare')} onClick={() => setActiveTab("compare")}
className={`py-4 px-2 border-b-2 font-medium text-sm ${ className={`py-4 px-2 border-b-2 font-medium text-sm ${
activeTab === 'compare' activeTab === "compare"
? 'border-blue-500 text-blue-600' ? "border-blue-500 text-blue-600"
: 'border-transparent text-gray-500 hover:text-gray-700' : "border-transparent text-gray-500 hover:text-gray-700"
}`} }`}
> >
<Scale size={16} className="inline mr-1" /> <Scale size={16} className="inline mr-1" />
@@ -233,11 +263,14 @@ export const StandardRatesPage: React.FC = () => {
</div> </div>
<CardContent> <CardContent>
{activeTab === 'list' && ( {activeTab === "list" && (
<div> <div>
<div className="flex gap-4 mb-4"> <div className="flex gap-4 mb-4">
<div className="relative min-w-[300px] flex-1"> <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} /> <Search
className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"
size={18}
/>
<input <input
type="text" type="text"
placeholder="Search by sub-department, activity..." placeholder="Search by sub-department, activity..."
@@ -254,8 +287,10 @@ export const StandardRatesPage: React.FC = () => {
<div className="mb-4 p-3 bg-blue-50 border border-blue-200 rounded-md"> <div className="mb-4 p-3 bg-blue-50 border border-blue-200 rounded-md">
<p className="text-sm text-blue-700"> <p className="text-sm text-blue-700">
<strong>Standard Rates</strong> are default rates set by supervisors for sub-departments and activities. <strong>Standard Rates</strong>{" "}
These are used as benchmarks to compare against contractor-specific rates. are default rates set by supervisors for sub-departments and
activities. These are used as benchmarks to compare against
contractor-specific rates.
</p> </p>
</div> </div>
@@ -265,9 +300,14 @@ export const StandardRatesPage: React.FC = () => {
</div> </div>
)} )}
{loading ? ( {loading
<div className="text-center py-8">Loading standard rates...</div> ? (
) : filteredRates.length > 0 ? ( <div className="text-center py-8">
Loading standard rates...
</div>
)
: filteredRates.length > 0
? (
<Table> <Table>
<TableHeader> <TableHeader>
<TableHead>Department</TableHead> <TableHead>Department</TableHead>
@@ -281,22 +321,32 @@ export const StandardRatesPage: React.FC = () => {
<TableBody> <TableBody>
{filteredRates.map((rate) => ( {filteredRates.map((rate) => (
<TableRow key={rate.id}> <TableRow key={rate.id}>
<TableCell>{rate.department_name || '-'}</TableCell> <TableCell>{rate.department_name || "-"}</TableCell>
<TableCell className="font-medium">{rate.sub_department_name || 'All'}</TableCell> <TableCell className="font-medium">
{rate.sub_department_name || "All"}
</TableCell>
<TableCell> <TableCell>
<span className={`px-2 py-1 rounded text-xs font-medium ${ <span
rate.unit_of_measurement === 'Per Bag' className={`px-2 py-1 rounded text-xs font-medium ${
? 'bg-blue-100 text-blue-700' rate.unit_of_measurement === "Per Bag"
: 'bg-gray-100 text-gray-700' ? "bg-blue-100 text-blue-700"
}`}> : "bg-gray-100 text-gray-700"
{rate.activity || 'Standard'} }`}
>
{rate.activity || "Standard"}
</span> </span>
</TableCell> </TableCell>
<TableCell> <TableCell>
<span className="text-green-600 font-semibold">{rate.rate}</span> <span className="text-green-600 font-semibold">
{rate.rate}
</span>
</TableCell>
<TableCell>
{new Date(rate.effective_date).toLocaleDateString()}
</TableCell>
<TableCell className="text-gray-500">
{rate.created_by_name || "-"}
</TableCell> </TableCell>
<TableCell>{new Date(rate.effective_date).toLocaleDateString()}</TableCell>
<TableCell className="text-gray-500">{rate.created_by_name || '-'}</TableCell>
{canManageRates && ( {canManageRates && (
<TableCell> <TableCell>
<div className="flex gap-2"> <div className="flex gap-2">
@@ -325,7 +375,8 @@ export const StandardRatesPage: React.FC = () => {
))} ))}
</TableBody> </TableBody>
</Table> </Table>
) : ( )
: (
<div className="text-center py-8 text-gray-500"> <div className="text-center py-8 text-gray-500">
No standard rates configured yet. Add one to get started! No standard rates configured yet. Add one to get started!
</div> </div>
@@ -333,17 +384,20 @@ export const StandardRatesPage: React.FC = () => {
</div> </div>
)} )}
{activeTab === 'add' && canManageRates && ( {activeTab === "add" && canManageRates && (
<div className="max-w-2xl space-y-6"> <div className="max-w-2xl space-y-6">
<h3 className="text-lg font-semibold text-gray-800"> <h3 className="text-lg font-semibold text-gray-800">
{editingId ? 'Edit Standard Rate' : 'Add New Standard Rate'} {editingId ? "Edit Standard Rate" : "Add New Standard Rate"}
</h3> </h3>
<div className="p-4 bg-yellow-50 border border-yellow-200 rounded-md"> <div className="p-4 bg-yellow-50 border border-yellow-200 rounded-md">
<h4 className="font-medium text-yellow-800 mb-2">About Standard Rates</h4> <h4 className="font-medium text-yellow-800 mb-2">
About Standard Rates
</h4>
<p className="text-sm text-yellow-700"> <p className="text-sm text-yellow-700">
Standard rates serve as default benchmarks for sub-departments and activities. Standard rates serve as default benchmarks for sub-departments
Contractor rates can be compared against these to identify deviations. and activities. Contractor rates can be compared against these
to identify deviations.
</p> </p>
</div> </div>
@@ -354,20 +408,27 @@ export const StandardRatesPage: React.FC = () => {
)} )}
<div className="grid grid-cols-2 gap-6"> <div className="grid grid-cols-2 gap-6">
{isSupervisor ? ( {isSupervisor
? (
<Input <Input
label="Department" label="Department"
value={departments.find(d => d.id === user?.department_id)?.name || 'Loading...'} value={departments.find((d) =>
d.id === user?.department_id
)?.name || "Loading..."}
disabled disabled
/> />
) : ( )
: (
<Select <Select
label="Department" label="Department"
value={selectedDept} value={selectedDept}
onChange={(e) => setSelectedDept(e.target.value)} onChange={(e) => setSelectedDept(e.target.value)}
options={[ options={[
{ value: '', label: 'Select Department' }, { value: "", label: "Select Department" },
...departments.map(d => ({ value: String(d.id), label: d.name })) ...departments.map((d) => ({
value: String(d.id),
label: d.name,
})),
]} ]}
/> />
)} )}
@@ -378,8 +439,11 @@ export const StandardRatesPage: React.FC = () => {
onChange={handleInputChange} onChange={handleInputChange}
disabled={!!editingId} disabled={!!editingId}
options={[ options={[
{ value: '', label: 'All Sub-Departments' }, { value: "", label: "All Sub-Departments" },
...subDepartments.map(s => ({ value: String(s.id), label: s.name })) ...subDepartments.map((s) => ({
value: String(s.id),
label: s.name,
})),
]} ]}
/> />
<Select <Select
@@ -389,17 +453,28 @@ export const StandardRatesPage: React.FC = () => {
onChange={handleInputChange} onChange={handleInputChange}
disabled={!formData.subDepartmentId} disabled={!formData.subDepartmentId}
options={[ options={[
{ value: '', label: formData.subDepartmentId ? 'Standard (Default)' : 'Select Sub-Department First' }, {
...activities.map(a => ({ value: "",
label: formData.subDepartmentId
? "Standard (Default)"
: "Select Sub-Department First",
},
...activities.map((a) => ({
value: a.name, value: a.name,
label: `${a.name} (${a.unit_of_measurement === 'Per Bag' ? 'per unit' : 'flat rate'})` label: `${a.name} (${
})) a.unit_of_measurement === "Per Bag"
? "per unit"
: "flat rate"
})`,
})),
]} ]}
/> />
<Input <Input
label={(() => { label={(() => {
const selectedActivity = activities.find(a => a.name === formData.activity); const selectedActivity = activities.find((a) =>
return selectedActivity?.unit_of_measurement === 'Per Bag' a.name === formData.activity
);
return selectedActivity?.unit_of_measurement === "Per Bag"
? "Rate per Unit (₹)" ? "Rate per Unit (₹)"
: "Standard Rate (₹)"; : "Standard Rate (₹)";
})()} })()}
@@ -421,14 +496,20 @@ export const StandardRatesPage: React.FC = () => {
</div> </div>
<div className="flex justify-end gap-4"> <div className="flex justify-end gap-4">
<Button variant="outline" onClick={() => { setActiveTab('list'); resetForm(); }}> <Button
variant="outline"
onClick={() => {
setActiveTab("list");
resetForm();
}}
>
Cancel Cancel
</Button> </Button>
<Button onClick={handleSubmit} disabled={formLoading}> <Button onClick={handleSubmit} disabled={formLoading}>
{formLoading ? 'Saving...' : ( {formLoading ? "Saving..." : (
<> <>
<DollarSign size={16} className="mr-2" /> <DollarSign size={16} className="mr-2" />
{editingId ? 'Update Rate' : 'Add Standard Rate'} {editingId ? "Update Rate" : "Add Standard Rate"}
</> </>
)} )}
</Button> </Button>
@@ -436,7 +517,7 @@ export const StandardRatesPage: React.FC = () => {
</div> </div>
)} )}
{activeTab === 'compare' && ( {activeTab === "compare" && (
<div> <div>
<div className="mb-6"> <div className="mb-6">
<h3 className="text-lg font-semibold text-gray-800 mb-4"> <h3 className="text-lg font-semibold text-gray-800 mb-4">
@@ -451,20 +532,27 @@ export const StandardRatesPage: React.FC = () => {
value={compareContractorId} value={compareContractorId}
onChange={(e) => setCompareContractorId(e.target.value)} onChange={(e) => setCompareContractorId(e.target.value)}
options={[ options={[
{ value: '', label: 'Select Contractor' }, { value: "", label: "Select Contractor" },
...contractors.map(c => ({ value: String(c.id), label: c.name })) ...contractors.map((c) => ({
value: String(c.id),
label: c.name,
})),
]} ]}
/> />
</div> </div>
<Button onClick={fetchComparison} disabled={!compareContractorId}> <Button
onClick={fetchComparison}
disabled={!compareContractorId}
>
Compare Compare
</Button> </Button>
</div> </div>
</div> </div>
{loading ? ( {loading
<div className="text-center py-8">Loading comparison...</div> ? <div className="text-center py-8">Loading comparison...</div>
) : comparisons.length > 0 ? ( : comparisons.length > 0
? (
<Table> <Table>
<TableHeader> <TableHeader>
<TableHead>Sub-Department</TableHead> <TableHead>Sub-Department</TableHead>
@@ -477,37 +565,54 @@ export const StandardRatesPage: React.FC = () => {
<TableBody> <TableBody>
{comparisons.map((comp, idx) => ( {comparisons.map((comp, idx) => (
<TableRow key={idx}> <TableRow key={idx}>
<TableCell className="font-medium">{comp.sub_department_name || 'All'}</TableCell> <TableCell className="font-medium">
{comp.sub_department_name || "All"}
</TableCell>
<TableCell> <TableCell>
<span className={`px-2 py-1 rounded text-xs font-medium ${ <span
comp.unit_of_measurement === 'Per Bag' className={`px-2 py-1 rounded text-xs font-medium ${
? 'bg-blue-100 text-blue-700' comp.unit_of_measurement === "Per Bag"
: 'bg-gray-100 text-gray-700' ? "bg-blue-100 text-blue-700"
}`}> : "bg-gray-100 text-gray-700"
{comp.activity || 'Standard'} }`}
>
{comp.activity || "Standard"}
</span> </span>
</TableCell> </TableCell>
<TableCell className="font-semibold">{comp.rate}</TableCell> <TableCell className="font-semibold">
<TableCell className="text-gray-600">{comp.standard_rate}</TableCell> {comp.rate}
</TableCell>
<TableCell className="text-gray-600">
{comp.standard_rate}
</TableCell>
<TableCell> <TableCell>
<span className={`font-semibold ${ <span
comp.difference > 0 ? 'text-red-600' : className={`font-semibold ${
comp.difference < 0 ? 'text-green-600' : comp.difference > 0
'text-gray-600' ? "text-red-600"
}`}> : comp.difference < 0
{comp.difference > 0 ? '+' : ''}{comp.difference.toFixed(2)} ? "text-green-600"
: "text-gray-600"
}`}
>
{comp.difference > 0 ? "+" : ""}{comp.difference
.toFixed(2)}
</span> </span>
</TableCell> </TableCell>
<TableCell> <TableCell>
{comp.is_above_standard ? ( {comp.is_above_standard
? (
<span className="px-2 py-1 rounded text-xs font-medium bg-red-100 text-red-700"> <span className="px-2 py-1 rounded text-xs font-medium bg-red-100 text-red-700">
Above Standard ({comp.percentage_difference}%) Above Standard ({comp.percentage_difference}%)
</span> </span>
) : comp.is_below_standard ? ( )
: comp.is_below_standard
? (
<span className="px-2 py-1 rounded text-xs font-medium bg-green-100 text-green-700"> <span className="px-2 py-1 rounded text-xs font-medium bg-green-100 text-green-700">
Below Standard ({comp.percentage_difference}%) Below Standard ({comp.percentage_difference}%)
</span> </span>
) : ( )
: (
<span className="px-2 py-1 rounded text-xs font-medium bg-gray-100 text-gray-700"> <span className="px-2 py-1 rounded text-xs font-medium bg-gray-100 text-gray-700">
At Standard At Standard
</span> </span>
@@ -517,13 +622,17 @@ export const StandardRatesPage: React.FC = () => {
))} ))}
</TableBody> </TableBody>
</Table> </Table>
) : compareContractorId ? ( )
: compareContractorId
? (
<div className="text-center py-8 text-gray-500"> <div className="text-center py-8 text-gray-500">
No rates found for this contractor to compare. No rates found for this contractor to compare.
</div> </div>
) : ( )
: (
<div className="text-center py-8 text-gray-500"> <div className="text-center py-8 text-gray-500">
Select a contractor to compare their rates against standard rates. Select a contractor to compare their rates against standard
rates.
</div> </div>
)} )}
</div> </div>

File diff suppressed because it is too large Load Diff

View File

@@ -1,48 +1,65 @@
import React, { useState, useEffect } from 'react'; import React, { useEffect, useState } from "react";
import { Plus, RefreshCw, CheckCircle, Trash2, Search } from 'lucide-react'; import { CheckCircle, Plus, RefreshCw, Search, Trash2 } from "lucide-react";
import { Card, CardContent } from '../components/ui/Card'; import { Card, CardContent } from "../components/ui/Card";
import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from '../components/ui/Table'; import {
import { Button } from '../components/ui/Button'; Table,
import { Input, Select, TextArea } from '../components/ui/Input'; TableBody,
import { useWorkAllocations } from '../hooks/useWorkAllocations'; TableCell,
import { useDepartments, useSubDepartments } from '../hooks/useDepartments'; TableHead,
import { useEmployees } from '../hooks/useEmployees'; TableHeader,
import { useActivities } from '../hooks/useActivities'; TableRow,
import { useAuth } from '../contexts/AuthContext'; } from "../components/ui/Table.tsx";
import { api } from '../services/api'; import { Button } from "../components/ui/Button.tsx";
import { Input, Select, TextArea } from "../components/ui/Input.tsx";
import { useWorkAllocations } from "../hooks/useWorkAllocations.ts";
import { useDepartments, useSubDepartments } from "../hooks/useDepartments.ts";
import { useEmployees } from "../hooks/useEmployees.ts";
import { useActivities } from "../hooks/useActivities.ts";
import { useAuth } from "../contexts/AuthContext.tsx";
import { api } from "../services/api.ts";
export const WorkAllocationPage: React.FC = () => { export const WorkAllocationPage: React.FC = () => {
const [activeTab, setActiveTab] = useState<'create' | 'view' | 'summary'>('view'); const [activeTab, setActiveTab] = useState<"create" | "view" | "summary">(
const [searchQuery, setSearchQuery] = useState(''); "view",
const { allocations, loading, error, refresh, createAllocation, updateAllocation, deleteAllocation } = useWorkAllocations(); );
const [searchQuery, setSearchQuery] = useState("");
const {
allocations,
loading,
error,
refresh,
createAllocation,
updateAllocation,
deleteAllocation,
} = useWorkAllocations();
const { departments } = useDepartments(); const { departments } = useDepartments();
const { employees } = useEmployees(); const { employees } = useEmployees();
const { user } = useAuth(); const { user } = useAuth();
const [contractors, setContractors] = useState<any[]>([]); const [contractors, setContractors] = useState<any[]>([]);
// Check if user is supervisor (limited to their department) // Check if user is supervisor (limited to their department)
const isSupervisor = user?.role === 'Supervisor'; const isSupervisor = user?.role === "Supervisor";
const canCreateAllocation = user?.role === 'SuperAdmin' || user?.role === 'Supervisor';
// Get supervisor's department name // Get supervisor's department name
const supervisorDeptName = departments.find(d => d.id === user?.department_id)?.name || ''; const supervisorDeptName =
departments.find((d) => d.id === user?.department_id)?.name || "";
// Form state // Form state
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
employeeId: '', employeeId: "",
contractorId: '', contractorId: "",
subDepartmentId: '', subDepartmentId: "",
activity: '', activity: "",
description: '', description: "",
assignedDate: new Date().toISOString().split('T')[0], assignedDate: new Date().toISOString().split("T")[0],
rateId: '', rateId: "",
departmentId: '', departmentId: "",
units: '', units: "",
}); });
const [selectedDept, setSelectedDept] = useState(''); const [selectedDept, setSelectedDept] = useState("");
const { subDepartments } = useSubDepartments(selectedDept); const { subDepartments } = useSubDepartments(selectedDept);
const { activities } = useActivities(formData.subDepartmentId); 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[]>([]);
@@ -58,14 +75,16 @@ export const WorkAllocationPage: React.FC = () => {
}, [formData.contractorId]); }, [formData.contractorId]);
// Get selected rate details // Get selected rate details
const selectedRate = contractorRates.find(r => r.id === parseInt(formData.rateId)); const selectedRate = contractorRates.find((r) =>
r.id === parseInt(formData.rateId)
);
// Get selected activity details // Get selected activity details
const selectedActivity = activities.find(a => a.name === formData.activity); const selectedActivity = activities.find((a) => a.name === formData.activity);
// Check if rate is per unit based on activity's unit_of_measurement // Check if rate is per unit based on activity's unit_of_measurement
const isPerUnitRate = selectedActivity?.unit_of_measurement === 'Per Bag' || const isPerUnitRate = selectedActivity?.unit_of_measurement === "Per Bag" ||
selectedRate?.unit_of_measurement === 'Per Bag'; selectedRate?.unit_of_measurement === "Per Bag";
// Calculate total amount // Calculate total amount
const unitCount = parseFloat(formData.units) || 0; const unitCount = parseFloat(formData.units) || 0;
@@ -77,57 +96,74 @@ export const WorkAllocationPage: React.FC = () => {
if (isSupervisor && user?.department_id) { if (isSupervisor && user?.department_id) {
const deptId = String(user.department_id); const deptId = String(user.department_id);
setSelectedDept(deptId); setSelectedDept(deptId);
setFormData(prev => ({ ...prev, departmentId: deptId })); setFormData((prev) => ({ ...prev, departmentId: deptId }));
} }
}, [isSupervisor, user?.department_id]); }, [isSupervisor, user?.department_id]);
// Load contractors // Load contractors
useEffect(() => { useEffect(() => {
api.getUsers({ role: 'Contractor' }).then(setContractors).catch(console.error); api.getUsers({ role: "Contractor" }).then(setContractors).catch(
console.error,
);
}, []); }, []);
// Filter employees by selected contractor // Filter employees by selected contractor
const filteredEmployees = formData.contractorId const filteredEmployees = formData.contractorId
? employees.filter(e => e.contractor_id === parseInt(formData.contractorId)) ? employees.filter((e) =>
: employees.filter(e => e.role === 'Employee'); e.contractor_id === parseInt(formData.contractorId)
)
: employees.filter((e) => e.role === "Employee");
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => { const handleInputChange = (
e: React.ChangeEvent<
HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement
>,
) => {
const { name, value } = e.target; const { name, value } = e.target;
// Auto-select department when contractor is selected // Auto-select department when contractor is selected
if (name === 'contractorId' && value) { if (name === "contractorId" && value) {
const selectedContractor = contractors.find(c => String(c.id) === value); const selectedContractor = contractors.find((c) =>
String(c.id) === value
);
if (selectedContractor?.department_id) { if (selectedContractor?.department_id) {
setSelectedDept(String(selectedContractor.department_id)); setSelectedDept(String(selectedContractor.department_id));
// Clear sub-department and activity when contractor changes // Clear sub-department and activity when contractor changes
setFormData(prev => ({ ...prev, [name]: value, subDepartmentId: '', activity: '', rateId: '' })); setFormData((prev) => ({
...prev,
[name]: value,
subDepartmentId: "",
activity: "",
rateId: "",
}));
} else { } else {
setFormData(prev => ({ ...prev, [name]: value })); setFormData((prev) => ({ ...prev, [name]: value }));
} }
} } // Clear activity when sub-department changes
// Clear activity when sub-department changes else if (name === "subDepartmentId") {
else if (name === 'subDepartmentId') { setFormData((prev) => ({ ...prev, [name]: value, activity: "" }));
setFormData(prev => ({ ...prev, [name]: value, activity: '' }));
} else { } else {
setFormData(prev => ({ ...prev, [name]: value })); setFormData((prev) => ({ ...prev, [name]: value }));
} }
setFormError(''); setFormError("");
}; };
const handleCreateAllocation = async () => { const handleCreateAllocation = async () => {
if (!formData.employeeId || !formData.contractorId) { if (!formData.employeeId || !formData.contractorId) {
setFormError('Please select employee and contractor'); setFormError("Please select employee and contractor");
return; return;
} }
setFormLoading(true); setFormLoading(true);
setFormError(''); setFormError("");
try { try {
await createAllocation({ await createAllocation({
employeeId: parseInt(formData.employeeId), employeeId: parseInt(formData.employeeId),
contractorId: parseInt(formData.contractorId), contractorId: parseInt(formData.contractorId),
subDepartmentId: formData.subDepartmentId ? parseInt(formData.subDepartmentId) : null, subDepartmentId: formData.subDepartmentId
? parseInt(formData.subDepartmentId)
: null,
activity: formData.activity || null, activity: formData.activity || null,
description: formData.description, description: formData.description,
assignedDate: formData.assignedDate, assignedDate: formData.assignedDate,
@@ -138,19 +174,19 @@ export const WorkAllocationPage: React.FC = () => {
// Reset form // Reset form
setFormData({ setFormData({
employeeId: '', employeeId: "",
contractorId: '', contractorId: "",
subDepartmentId: '', subDepartmentId: "",
activity: '', activity: "",
description: '', description: "",
assignedDate: new Date().toISOString().split('T')[0], assignedDate: new Date().toISOString().split("T")[0],
rateId: '', rateId: "",
departmentId: isSupervisor ? String(user?.department_id) : '', departmentId: isSupervisor ? String(user?.department_id) : "",
units: '', units: "",
}); });
setActiveTab('view'); setActiveTab("view");
} catch (err: any) { } catch (err: any) {
setFormError(err.message || 'Failed to create allocation'); setFormError(err.message || "Failed to create allocation");
} finally { } finally {
setFormLoading(false); setFormLoading(false);
} }
@@ -158,27 +194,31 @@ export const WorkAllocationPage: React.FC = () => {
const handleMarkComplete = async (id: number) => { const handleMarkComplete = async (id: number) => {
try { try {
await updateAllocation(id, 'Completed', new Date().toISOString().split('T')[0]); await updateAllocation(
id,
"Completed",
new Date().toISOString().split("T")[0],
);
} catch (err: any) { } catch (err: any) {
alert(err.message || 'Failed to update allocation'); alert(err.message || "Failed to update allocation");
} }
}; };
const handleDelete = async (id: number) => { const handleDelete = async (id: number) => {
if (!confirm('Are you sure you want to delete this allocation?')) return; if (!confirm("Are you sure you want to delete this allocation?")) return;
try { try {
await deleteAllocation(id); await deleteAllocation(id);
} catch (err: any) { } catch (err: any) {
alert(err.message || 'Failed to delete allocation'); alert(err.message || "Failed to delete allocation");
} }
}; };
// Calculate summary stats // Calculate summary stats
const stats = { const stats = {
total: allocations.length, total: allocations.length,
completed: allocations.filter(a => a.status === 'Completed').length, completed: allocations.filter((a) => a.status === "Completed").length,
inProgress: allocations.filter(a => a.status === 'InProgress').length, inProgress: allocations.filter((a) => a.status === "InProgress").length,
pending: allocations.filter(a => a.status === 'Pending').length, pending: allocations.filter((a) => a.status === "Pending").length,
}; };
return ( return (
@@ -186,28 +226,30 @@ export const WorkAllocationPage: React.FC = () => {
<Card> <Card>
<div className="border-b border-gray-200"> <div className="border-b border-gray-200">
<div className="flex space-x-8 px-6"> <div className="flex space-x-8 px-6">
{['create', 'view', 'summary'].map((tab) => ( {["create", "view", "summary"].map((tab) => (
<button <button
key={tab} key={tab}
onClick={() => setActiveTab(tab as any)} onClick={() => setActiveTab(tab as any)}
className={`py-4 px-2 border-b-2 font-medium text-sm ${ className={`py-4 px-2 border-b-2 font-medium text-sm ${
activeTab === tab activeTab === tab
? 'border-blue-500 text-blue-600' ? "border-blue-500 text-blue-600"
: 'border-transparent text-gray-500 hover:text-gray-700' : "border-transparent text-gray-500 hover:text-gray-700"
}`} }`}
> >
{tab === 'create' && 'Create Allocation'} {tab === "create" && "Create Allocation"}
{tab === 'view' && 'View Allocations'} {tab === "view" && "View Allocations"}
{tab === 'summary' && 'Work Summary'} {tab === "summary" && "Work Summary"}
</button> </button>
))} ))}
</div> </div>
</div> </div>
<CardContent> <CardContent>
{activeTab === 'create' && ( {activeTab === "create" && (
<div className="max-w-3xl space-y-6"> <div className="max-w-3xl space-y-6">
<h3 className="text-lg font-semibold text-gray-800">Create New Work Allocation</h3> <h3 className="text-lg font-semibold text-gray-800">
Create New Work Allocation
</h3>
{formError && ( {formError && (
<div className="p-3 bg-red-100 text-red-700 rounded-md"> <div className="p-3 bg-red-100 text-red-700 rounded-md">
@@ -223,8 +265,11 @@ export const WorkAllocationPage: React.FC = () => {
onChange={handleInputChange} onChange={handleInputChange}
required required
options={[ options={[
{ value: '', label: 'Select Contractor' }, { value: "", label: "Select Contractor" },
...contractors.map(c => ({ value: String(c.id), label: c.name })) ...contractors.map((c) => ({
value: String(c.id),
label: c.name,
})),
]} ]}
/> />
<Select <Select
@@ -234,24 +279,32 @@ export const WorkAllocationPage: React.FC = () => {
onChange={handleInputChange} onChange={handleInputChange}
required required
options={[ options={[
{ value: '', label: 'Select Employee' }, { value: "", label: "Select Employee" },
...filteredEmployees.map(e => ({ value: String(e.id), label: e.name })) ...filteredEmployees.map((e) => ({
value: String(e.id),
label: e.name,
})),
]} ]}
/> />
{isSupervisor ? ( {isSupervisor
? (
<Input <Input
label="Department" label="Department"
value={supervisorDeptName || 'Loading...'} value={supervisorDeptName || "Loading..."}
disabled disabled
/> />
) : ( )
: (
<Select <Select
label="Department" label="Department"
value={selectedDept} value={selectedDept}
onChange={(e) => setSelectedDept(e.target.value)} onChange={(e) => setSelectedDept(e.target.value)}
options={[ options={[
{ value: '', label: 'Select Department' }, { value: "", label: "Select Department" },
...departments.map(d => ({ value: String(d.id), label: d.name })) ...departments.map((d) => ({
value: String(d.id),
label: d.name,
})),
]} ]}
/> />
)} )}
@@ -261,8 +314,11 @@ export const WorkAllocationPage: React.FC = () => {
value={formData.subDepartmentId} value={formData.subDepartmentId}
onChange={handleInputChange} onChange={handleInputChange}
options={[ options={[
{ value: '', label: 'Select Sub-Department' }, { value: "", label: "Select Sub-Department" },
...subDepartments.map(s => ({ value: String(s.id), label: s.name })) ...subDepartments.map((s) => ({
value: String(s.id),
label: s.name,
})),
]} ]}
/> />
<Select <Select
@@ -272,11 +328,20 @@ export const WorkAllocationPage: React.FC = () => {
onChange={handleInputChange} onChange={handleInputChange}
disabled={!formData.subDepartmentId} disabled={!formData.subDepartmentId}
options={[ options={[
{ value: '', label: formData.subDepartmentId ? 'Select Activity' : 'Select Sub-Department First' }, {
...activities.map(a => ({ value: "",
label: formData.subDepartmentId
? "Select Activity"
: "Select Sub-Department First",
},
...activities.map((a) => ({
value: a.name, value: a.name,
label: `${a.name} (${a.unit_of_measurement === 'Per Bag' ? 'per unit' : 'flat rate'})` label: `${a.name} (${
})) a.unit_of_measurement === "Per Bag"
? "per unit"
: "flat rate"
})`,
})),
]} ]}
/> />
<Input <Input
@@ -294,11 +359,20 @@ export const WorkAllocationPage: React.FC = () => {
onChange={handleInputChange} onChange={handleInputChange}
disabled={!formData.contractorId} disabled={!formData.contractorId}
options={[ options={[
{ value: '', label: formData.contractorId ? 'Select Rate' : 'Select Contractor First' }, {
...contractorRates.map(r => ({ value: "",
label: formData.contractorId
? "Select Rate"
: "Select Contractor First",
},
...contractorRates.map((r) => ({
value: String(r.id), value: String(r.id),
label: `${r.rate} - ${r.activity || 'Standard'} ${r.sub_department_name ? `(${r.sub_department_name})` : ''}` label: `${r.rate} - ${r.activity || "Standard"} ${
})) r.sub_department_name
? `(${r.sub_department_name})`
: ""
}`,
})),
]} ]}
/> />
{isPerUnitRate && ( {isPerUnitRate && (
@@ -326,33 +400,47 @@ export const WorkAllocationPage: React.FC = () => {
{/* Calculation Box */} {/* Calculation Box */}
{selectedRate && ( {selectedRate && (
<div className="col-span-2 p-4 bg-blue-50 border border-blue-200 rounded-lg"> <div className="col-span-2 p-4 bg-blue-50 border border-blue-200 rounded-lg">
<h4 className="font-semibold text-blue-800 mb-3">Rate Calculation</h4> <h4 className="font-semibold text-blue-800 mb-3">
Rate Calculation
</h4>
<div className="grid grid-cols-2 gap-4 text-sm"> <div className="grid grid-cols-2 gap-4 text-sm">
<div> <div>
<span className="text-gray-600">Rate Type:</span> <span className="text-gray-600">Rate Type:</span>
<span className="ml-2 font-medium">{isPerUnitRate ? 'Per Unit' : 'Flat Rate'}</span> <span className="ml-2 font-medium">
{isPerUnitRate ? "Per Unit" : "Flat Rate"}
</span>
</div> </div>
<div> <div>
<span className="text-gray-600">Rate:</span> <span className="text-gray-600">Rate:</span>
<span className="ml-2 font-medium">{rateAmount.toFixed(2)}</span> <span className="ml-2 font-medium">
{rateAmount.toFixed(2)}
</span>
</div> </div>
{isPerUnitRate && ( {isPerUnitRate && (
<> <>
<div> <div>
<span className="text-gray-600">Units:</span> <span className="text-gray-600">Units:</span>
<span className="ml-2 font-medium">{unitCount || 0}</span> <span className="ml-2 font-medium">
{unitCount || 0}
</span>
</div> </div>
<div> <div>
<span className="text-gray-600">Calculation:</span> <span className="text-gray-600">Calculation:</span>
<span className="ml-2 font-medium">{unitCount} × {rateAmount.toFixed(2)}</span> <span className="ml-2 font-medium">
{unitCount} × {rateAmount.toFixed(2)}
</span>
</div> </div>
</> </>
)} )}
</div> </div>
<div className="mt-4 pt-3 border-t border-blue-300"> <div className="mt-4 pt-3 border-t border-blue-300">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<span className="text-lg font-semibold text-blue-800">Total Amount:</span> <span className="text-lg font-semibold text-blue-800">
<span className="text-2xl font-bold text-green-600">{totalAmount.toFixed(2)}</span> Total Amount:
</span>
<span className="text-2xl font-bold text-green-600">
{totalAmount.toFixed(2)}
</span>
</div> </div>
</div> </div>
</div> </div>
@@ -360,11 +448,11 @@ export const WorkAllocationPage: React.FC = () => {
</div> </div>
<div className="flex justify-end gap-4 mt-6"> <div className="flex justify-end gap-4 mt-6">
<Button variant="outline" onClick={() => setActiveTab('view')}> <Button variant="outline" onClick={() => setActiveTab("view")}>
Cancel Cancel
</Button> </Button>
<Button onClick={handleCreateAllocation} disabled={formLoading}> <Button onClick={handleCreateAllocation} disabled={formLoading}>
{formLoading ? 'Creating...' : ( {formLoading ? "Creating..." : (
<> <>
<Plus size={16} className="mr-2" /> <Plus size={16} className="mr-2" />
Create Allocation Create Allocation
@@ -375,11 +463,14 @@ export const WorkAllocationPage: React.FC = () => {
</div> </div>
)} )}
{activeTab === 'view' && ( {activeTab === "view" && (
<div> <div>
<div className="flex gap-4 mb-4"> <div className="flex gap-4 mb-4">
<div className="flex-1 relative"> <div className="flex-1 relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400" size={18} /> <Search
className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"
size={18}
/>
<input <input
type="text" type="text"
placeholder="Search by employee, contractor, sub-department..." placeholder="Search by employee, contractor, sub-department..."
@@ -401,7 +492,7 @@ export const WorkAllocationPage: React.FC = () => {
)} )}
{(() => { {(() => {
const filteredAllocations = allocations.filter(a => { const filteredAllocations = allocations.filter((a) => {
if (!searchQuery) return true; if (!searchQuery) return true;
const query = searchQuery.toLowerCase(); const query = searchQuery.toLowerCase();
return ( return (
@@ -413,9 +504,14 @@ export const WorkAllocationPage: React.FC = () => {
); );
}); });
return loading ? ( return loading
<div className="text-center py-8">Loading work allocations...</div> ? (
) : filteredAllocations.length > 0 ? ( <div className="text-center py-8">
Loading work allocations...
</div>
)
: filteredAllocations.length > 0
? (
<Table> <Table>
<TableHeader> <TableHeader>
<TableHead>ID</TableHead> <TableHead>ID</TableHead>
@@ -430,60 +526,92 @@ export const WorkAllocationPage: React.FC = () => {
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{filteredAllocations.map((allocation) => { {filteredAllocations.map((allocation) => {
const isPerUnit = allocation.activity === 'Loading' || allocation.activity === 'Unloading'; const isPerUnit = allocation.activity === "Loading" ||
allocation.activity === "Unloading";
const units = parseFloat(allocation.units) || 0; const units = parseFloat(allocation.units) || 0;
const rate = parseFloat(allocation.rate) || 0; const rate = parseFloat(allocation.rate) || 0;
const total = parseFloat(allocation.total_amount) || (isPerUnit ? units * rate : rate); const total = parseFloat(allocation.total_amount) ||
(isPerUnit ? units * rate : rate);
return ( return (
<TableRow key={allocation.id}> <TableRow key={allocation.id}>
<TableCell>{allocation.id}</TableCell> <TableCell>{allocation.id}</TableCell>
<TableCell>{allocation.employee_name || '-'}</TableCell>
<TableCell>{allocation.contractor_name || '-'}</TableCell>
<TableCell>{allocation.sub_department_name || '-'}</TableCell>
<TableCell> <TableCell>
{allocation.activity ? ( {allocation.employee_name || "-"}
<span className={`px-2 py-1 rounded text-xs font-medium ${ </TableCell>
allocation.activity === 'Loading' || allocation.activity === 'Unloading' <TableCell>
? 'bg-purple-100 text-purple-700' {allocation.contractor_name || "-"}
: 'bg-gray-100 text-gray-700' </TableCell>
}`}> <TableCell>
{allocation.sub_department_name || "-"}
</TableCell>
<TableCell>
{allocation.activity
? (
<span
className={`px-2 py-1 rounded text-xs font-medium ${
allocation.activity === "Loading" ||
allocation.activity === "Unloading"
? "bg-purple-100 text-purple-700"
: "bg-gray-100 text-gray-700"
}`}
>
{allocation.activity} {allocation.activity}
</span> </span>
) : '-'} )
: "-"}
</TableCell> </TableCell>
<TableCell>{new Date(allocation.assigned_date).toLocaleDateString()}</TableCell>
<TableCell> <TableCell>
{rate > 0 ? ( {new Date(allocation.assigned_date)
.toLocaleDateString()}
</TableCell>
<TableCell>
{rate > 0
? (
<div className="text-sm"> <div className="text-sm">
{isPerUnit && units > 0 ? ( {isPerUnit && units > 0
? (
<div> <div>
<div className="text-gray-500">{units} × {rate.toFixed(2)}</div> <div className="text-gray-500">
<div className="font-semibold text-green-600">= {total.toFixed(2)}</div> {units} × {rate.toFixed(2)}
</div>
<div className="font-semibold text-green-600">
= {total.toFixed(2)}
</div>
</div>
)
: (
<div className="font-semibold text-green-600">
{rate.toFixed(2)}
</div> </div>
) : (
<div className="font-semibold text-green-600">{rate.toFixed(2)}</div>
)} )}
</div> </div>
) : '-'} )
: "-"}
</TableCell> </TableCell>
<TableCell> <TableCell>
<span className={`px-2 py-1 rounded text-xs font-medium ${ <span
allocation.status === 'Completed' ? 'bg-green-100 text-green-700' : className={`px-2 py-1 rounded text-xs font-medium ${
allocation.status === 'InProgress' ? 'bg-blue-100 text-blue-700' : allocation.status === "Completed"
allocation.status === 'Cancelled' ? 'bg-red-100 text-red-700' : ? "bg-green-100 text-green-700"
'bg-yellow-100 text-yellow-700' : allocation.status === "InProgress"
}`}> ? "bg-blue-100 text-blue-700"
: allocation.status === "Cancelled"
? "bg-red-100 text-red-700"
: "bg-yellow-100 text-yellow-700"
}`}
>
{allocation.status} {allocation.status}
</span> </span>
</TableCell> </TableCell>
<TableCell> <TableCell>
<div className="flex gap-2"> <div className="flex gap-2">
{allocation.status !== 'Completed' && ( {allocation.status !== "Completed" && (
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
onClick={() => handleMarkComplete(allocation.id)} onClick={() =>
handleMarkComplete(allocation.id)}
className="text-green-600" className="text-green-600"
title="Mark Complete" title="Mark Complete"
> >
@@ -506,28 +634,56 @@ export const WorkAllocationPage: React.FC = () => {
})} })}
</TableBody> </TableBody>
</Table> </Table>
) : (
<div className="text-center py-8 text-gray-500">
{searchQuery ? 'No matching allocations found.' : 'No work allocations found. Create one to get started!'}
</div>
) )
: (
<div className="text-center py-8 text-gray-500">
{searchQuery
? "No matching allocations found."
: "No work allocations found. Create one to get started!"}
</div>
);
})()} })()}
</div> </div>
)} )}
{activeTab === 'summary' && ( {activeTab === "summary" && (
<div> <div>
<h3 className="text-lg font-semibold text-gray-800 mb-6">Work Summary & Statistics</h3> <h3 className="text-lg font-semibold text-gray-800 mb-6">
Work Summary & Statistics
</h3>
<div className="grid grid-cols-4 gap-6"> <div className="grid grid-cols-4 gap-6">
{[ {[
{ label: 'TOTAL ALLOCATIONS', value: stats.total, color: 'bg-gray-100' }, {
{ label: 'COMPLETED', value: stats.completed, color: 'bg-green-100' }, label: "TOTAL ALLOCATIONS",
{ label: 'IN PROGRESS', value: stats.inProgress, color: 'bg-blue-100' }, value: stats.total,
{ label: 'PENDING', value: stats.pending, color: 'bg-yellow-100' }, color: "bg-gray-100",
},
{
label: "COMPLETED",
value: stats.completed,
color: "bg-green-100",
},
{
label: "IN PROGRESS",
value: stats.inProgress,
color: "bg-blue-100",
},
{
label: "PENDING",
value: stats.pending,
color: "bg-yellow-100",
},
].map((stat) => ( ].map((stat) => (
<div key={stat.label} className={`${stat.color} border border-gray-200 rounded-lg p-6`}> <div
<div className="text-xs text-gray-500 mb-2">{stat.label}</div> key={stat.label}
<div className="text-3xl font-bold text-gray-800">{stat.value}</div> className={`${stat.color} border border-gray-200 rounded-lg p-6`}
>
<div className="text-xs text-gray-500 mb-2">
{stat.label}
</div>
<div className="text-3xl font-bold text-gray-800">
{stat.value}
</div>
</div> </div>
))} ))}
</div> </div>

View File

@@ -1,4 +1,5 @@
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000/api'; const API_BASE_URL = import.meta.env.VITE_API_BASE_URL ||
"http://localhost:3000/api";
class ApiService { class ApiService {
private baseURL: string; private baseURL: string;
@@ -8,18 +9,21 @@ class ApiService {
} }
private getToken(): string | null { private getToken(): string | null {
return localStorage.getItem('token'); return localStorage.getItem("token");
} }
private async request<T>(endpoint: string, options: RequestInit = {}): Promise<T> { private async request<T>(
endpoint: string,
options: RequestInit = {},
): Promise<T> {
const token = this.getToken(); const token = this.getToken();
const headers: HeadersInit = { const headers: Record<string, string> = {
'Content-Type': 'application/json', "Content-Type": "application/json",
...options.headers, ...(options.headers as Record<string, string>),
}; };
if (token) { if (token) {
headers['Authorization'] = `Bearer ${token}`; headers["Authorization"] = `Bearer ${token}`;
} }
const response = await fetch(`${this.baseURL}${endpoint}`, { const response = await fetch(`${this.baseURL}${endpoint}`, {
@@ -28,183 +32,216 @@ class ApiService {
}); });
if (response.status === 401) { if (response.status === 401) {
localStorage.removeItem('token'); localStorage.removeItem("token");
localStorage.removeItem('user'); localStorage.removeItem("user");
window.location.href = '/'; globalThis.location.href = "/";
throw new Error('Unauthorized'); throw new Error("Unauthorized");
} }
if (!response.ok) { if (!response.ok) {
const error = await response.json().catch(() => ({ error: 'Request failed' })); const error = await response.json().catch(() => ({
throw new Error(error.error || 'Request failed'); error: "Request failed",
}));
throw new Error(error.error || "Request failed");
} }
return response.json(); return response.json();
} }
// Auth // Auth
async login(username: string, password: string) { login(username: string, password: string) {
return this.request<{ token: string; user: any }>('/auth/login', { return this.request<{ token: string; user: any }>("/auth/login", {
method: 'POST', method: "POST",
body: JSON.stringify({ username, password }), body: JSON.stringify({ username, password }),
}); });
} }
async getMe() { getMe() {
return this.request<any>('/auth/me'); return this.request<any>("/auth/me");
} }
async changePassword(currentPassword: string, newPassword: string) { changePassword(currentPassword: string, newPassword: string) {
return this.request<{ message: string }>('/auth/change-password', { return this.request<{ message: string }>("/auth/change-password", {
method: 'POST', method: "POST",
body: JSON.stringify({ currentPassword, newPassword }), body: JSON.stringify({ currentPassword, newPassword }),
}); });
} }
// Users // Users
async getUsers(params?: { role?: string; departmentId?: number }) { getUsers(params?: { role?: string; departmentId?: number }) {
const query = new URLSearchParams(params as any).toString(); const query = new URLSearchParams(params as any).toString();
return this.request<any[]>(`/users${query ? `?${query}` : ''}`); return this.request<any[]>(`/users${query ? `?${query}` : ""}`);
} }
async getUser(id: number) { getUser(id: number) {
return this.request<any>(`/users/${id}`); return this.request<any>(`/users/${id}`);
} }
async createUser(data: any) { createUser(data: any) {
return this.request<any>('/users', { return this.request<any>("/users", {
method: 'POST', method: "POST",
body: JSON.stringify(data), body: JSON.stringify(data),
}); });
} }
async updateUser(id: number, data: any) { updateUser(id: number, data: any) {
return this.request<any>(`/users/${id}`, { return this.request<any>(`/users/${id}`, {
method: 'PUT', method: "PUT",
body: JSON.stringify(data), body: JSON.stringify(data),
}); });
} }
async deleteUser(id: number) { deleteUser(id: number) {
return this.request<{ message: string }>(`/users/${id}`, { return this.request<{ message: string }>(`/users/${id}`, {
method: 'DELETE', method: "DELETE",
}); });
} }
// Departments // Departments
async getDepartments() { getDepartments() {
return this.request<any[]>('/departments'); return this.request<any[]>("/departments");
} }
async getDepartment(id: number) { getDepartment(id: number) {
return this.request<any>(`/departments/${id}`); return this.request<any>(`/departments/${id}`);
} }
async getSubDepartments(departmentId: number) { getSubDepartments(departmentId: number) {
return this.request<any[]>(`/departments/${departmentId}/sub-departments`); return this.request<any[]>(`/departments/${departmentId}/sub-departments`);
} }
async createDepartment(name: string) { getAllSubDepartments() {
return this.request<any>('/departments', { return this.request<any[]>("/departments/sub-departments/all");
method: 'POST', }
createDepartment(name: string) {
return this.request<any>("/departments", {
method: "POST",
body: JSON.stringify({ name }), body: JSON.stringify({ name }),
}); });
} }
// Sub-Departments // Sub-Departments
async createSubDepartment(data: { department_id: number; name: string }) { createSubDepartment(data: { department_id: number; name: string }) {
return this.request<any>('/departments/sub-departments', { return this.request<any>("/departments/sub-departments", {
method: 'POST', method: "POST",
body: JSON.stringify(data), body: JSON.stringify(data),
}); });
} }
async deleteSubDepartment(id: number) { deleteSubDepartment(id: number) {
return this.request<{ message: string }>(`/departments/sub-departments/${id}`, { return this.request<{ message: string }>(
method: 'DELETE', `/departments/sub-departments/${id}`,
}); {
method: "DELETE",
},
);
} }
// Work Allocations // Work Allocations
async getWorkAllocations(params?: { employeeId?: number; status?: string; departmentId?: number }) { getWorkAllocations(
params?: { employeeId?: number; status?: string; departmentId?: number },
) {
const query = new URLSearchParams(params as any).toString(); const query = new URLSearchParams(params as any).toString();
return this.request<any[]>(`/work-allocations${query ? `?${query}` : ''}`); return this.request<any[]>(`/work-allocations${query ? `?${query}` : ""}`);
} }
async getWorkAllocation(id: number) { getWorkAllocation(id: number) {
return this.request<any>(`/work-allocations/${id}`); return this.request<any>(`/work-allocations/${id}`);
} }
async createWorkAllocation(data: any) { createWorkAllocation(data: any) {
return this.request<any>('/work-allocations', { return this.request<any>("/work-allocations", {
method: 'POST', method: "POST",
body: JSON.stringify(data), body: JSON.stringify(data),
}); });
} }
async updateWorkAllocationStatus(id: number, status: string, completionDate?: string) { updateWorkAllocationStatus(
id: number,
status: string,
completionDate?: string,
) {
return this.request<any>(`/work-allocations/${id}/status`, { return this.request<any>(`/work-allocations/${id}/status`, {
method: 'PUT', method: "PUT",
body: JSON.stringify({ status, completionDate }), body: JSON.stringify({ status, completionDate }),
}); });
} }
async deleteWorkAllocation(id: number) { deleteWorkAllocation(id: number) {
return this.request<{ message: string }>(`/work-allocations/${id}`, { return this.request<{ message: string }>(`/work-allocations/${id}`, {
method: 'DELETE', method: "DELETE",
}); });
} }
// Attendance // Attendance
async getAttendance(params?: { employeeId?: number; startDate?: string; endDate?: string; status?: string }) { getAttendance(
params?: {
employeeId?: number;
startDate?: string;
endDate?: string;
status?: string;
},
) {
const query = new URLSearchParams(params as any).toString(); const query = new URLSearchParams(params as any).toString();
return this.request<any[]>(`/attendance${query ? `?${query}` : ''}`); return this.request<any[]>(`/attendance${query ? `?${query}` : ""}`);
} }
async checkIn(employeeId: number, workDate: string) { checkIn(employeeId: number, workDate: string) {
return this.request<any>('/attendance/check-in', { return this.request<any>("/attendance/check-in", {
method: 'POST', method: "POST",
body: JSON.stringify({ employeeId, workDate }), body: JSON.stringify({ employeeId, workDate }),
}); });
} }
async checkOut(employeeId: number, workDate: string) { checkOut(employeeId: number, workDate: string) {
return this.request<any>('/attendance/check-out', { return this.request<any>("/attendance/check-out", {
method: 'POST', method: "POST",
body: JSON.stringify({ employeeId, workDate }), body: JSON.stringify({ employeeId, workDate }),
}); });
} }
async getAttendanceSummary(params?: { startDate?: string; endDate?: string; departmentId?: number }) { getAttendanceSummary(
params?: { startDate?: string; endDate?: string; departmentId?: number },
) {
const query = new URLSearchParams(params as any).toString(); const query = new URLSearchParams(params as any).toString();
return this.request<any[]>(`/attendance/summary/stats${query ? `?${query}` : ''}`); return this.request<any[]>(
`/attendance/summary/stats${query ? `?${query}` : ""}`,
);
} }
async updateAttendanceStatus(id: number, status: string, remark?: string) { updateAttendanceStatus(id: number, status: string, remark?: string) {
return this.request<any>(`/attendance/${id}/status`, { return this.request<any>(`/attendance/${id}/status`, {
method: 'PUT', method: "PUT",
body: JSON.stringify({ status, remark }), body: JSON.stringify({ status, remark }),
}); });
} }
async markAbsent(employeeId: number, workDate: string, remark?: string) { markAbsent(employeeId: number, workDate: string, remark?: string) {
return this.request<any>('/attendance/mark-absent', { return this.request<any>("/attendance/mark-absent", {
method: 'POST', method: "POST",
body: JSON.stringify({ employeeId, workDate, remark }), body: JSON.stringify({ employeeId, workDate, remark }),
}); });
} }
// Employee Swaps // Employee Swaps
async getEmployeeSwaps(params?: { status?: string; employeeId?: number; startDate?: string; endDate?: string }) { getEmployeeSwaps(
const query = params ? new URLSearchParams(params as any).toString() : ''; params?: {
return this.request<any[]>(`/employee-swaps${query ? `?${query}` : ''}`); status?: string;
employeeId?: number;
startDate?: string;
endDate?: string;
},
) {
const query = params ? new URLSearchParams(params as any).toString() : "";
return this.request<any[]>(`/employee-swaps${query ? `?${query}` : ""}`);
} }
async getEmployeeSwap(id: number) { getEmployeeSwap(id: number) {
return this.request<any>(`/employee-swaps/${id}`); return this.request<any>(`/employee-swaps/${id}`);
} }
async createEmployeeSwap(data: { createEmployeeSwap(data: {
employeeId: number; employeeId: number;
targetDepartmentId: number; targetDepartmentId: number;
targetContractorId?: number; targetContractorId?: number;
@@ -213,160 +250,195 @@ class ApiService {
workCompletionPercentage?: number; workCompletionPercentage?: number;
swapDate: string; swapDate: string;
}) { }) {
return this.request<any>('/employee-swaps', { return this.request<any>("/employee-swaps", {
method: 'POST', method: "POST",
body: JSON.stringify(data), body: JSON.stringify(data),
}); });
} }
async completeEmployeeSwap(id: number) { completeEmployeeSwap(id: number) {
return this.request<any>(`/employee-swaps/${id}/complete`, { return this.request<any>(`/employee-swaps/${id}/complete`, {
method: 'PUT', method: "PUT",
}); });
} }
async cancelEmployeeSwap(id: number) { cancelEmployeeSwap(id: number) {
return this.request<any>(`/employee-swaps/${id}/cancel`, { return this.request<any>(`/employee-swaps/${id}/cancel`, {
method: 'PUT', method: "PUT",
}); });
} }
// Contractor Rates // Contractor Rates
async getContractorRates(params?: { contractorId?: number; subDepartmentId?: number }) { getContractorRates(
const query = params ? new URLSearchParams(params as any).toString() : ''; params?: { contractorId?: number; subDepartmentId?: number },
return this.request<any[]>(`/contractor-rates${query ? `?${query}` : ''}`); ) {
const query = params ? new URLSearchParams(params as any).toString() : "";
return this.request<any[]>(`/contractor-rates${query ? `?${query}` : ""}`);
} }
async getCurrentRate(contractorId: number, subDepartmentId?: number) { getCurrentRate(contractorId: number, subDepartmentId?: number) {
const query = subDepartmentId ? `?subDepartmentId=${subDepartmentId}` : ''; const query = subDepartmentId ? `?subDepartmentId=${subDepartmentId}` : "";
return this.request<any>(`/contractor-rates/contractor/${contractorId}/current${query}`); return this.request<any>(
`/contractor-rates/contractor/${contractorId}/current${query}`,
);
} }
async setContractorRate(data: { setContractorRate(data: {
contractorId: number; contractorId: number;
subDepartmentId?: number; subDepartmentId?: number;
activity?: string; activity?: string;
rate: number; rate: number;
effectiveDate: string effectiveDate: string;
}) { }) {
return this.request<any>('/contractor-rates', { return this.request<any>("/contractor-rates", {
method: 'POST', method: "POST",
body: JSON.stringify(data), body: JSON.stringify(data),
}); });
} }
async updateContractorRate(id: number, data: { rate?: number; activity?: string; effectiveDate?: string }) { updateContractorRate(
id: number,
data: { rate?: number; activity?: string; effectiveDate?: string },
) {
return this.request<any>(`/contractor-rates/${id}`, { return this.request<any>(`/contractor-rates/${id}`, {
method: 'PUT', method: "PUT",
body: JSON.stringify(data), body: JSON.stringify(data),
}); });
} }
async deleteContractorRate(id: number) { deleteContractorRate(id: number) {
return this.request<{ message: string }>(`/contractor-rates/${id}`, { return this.request<{ message: string }>(`/contractor-rates/${id}`, {
method: 'DELETE', method: "DELETE",
}); });
} }
// Reports // Reports
async getCompletedAllocationsReport(params?: { getCompletedAllocationsReport(params?: {
startDate?: string; startDate?: string;
endDate?: string; endDate?: string;
departmentId?: number; departmentId?: number;
contractorId?: number; contractorId?: number;
employeeId?: number; employeeId?: number;
}) { }) {
const query = params ? new URLSearchParams(params as any).toString() : ''; const query = params ? new URLSearchParams(params as any).toString() : "";
return this.request<{ return this.request<{
allocations: any[]; allocations: any[];
summary: { totalAllocations: number; totalAmount: string; totalUnits: string } summary: {
}>(`/reports/completed-allocations${query ? `?${query}` : ''}`); totalAllocations: number;
totalAmount: string;
totalUnits: string;
};
}>(`/reports/completed-allocations${query ? `?${query}` : ""}`);
} }
async getReportSummary(params?: { startDate?: string; endDate?: string }) { getReportSummary(params?: { startDate?: string; endDate?: string }) {
const query = params ? new URLSearchParams(params as any).toString() : ''; const query = params ? new URLSearchParams(params as any).toString() : "";
return this.request<{ return this.request<{
byContractor: any[]; byContractor: any[];
bySubDepartment: any[]; bySubDepartment: any[];
byActivity: any[]; byActivity: any[];
}>(`/reports/summary${query ? `?${query}` : ''}`); }>(`/reports/summary${query ? `?${query}` : ""}`);
} }
// Standard Rates // Standard Rates
async getStandardRates(params?: { departmentId?: number; subDepartmentId?: number; activity?: string }) { getStandardRates(
const query = params ? new URLSearchParams(params as any).toString() : ''; params?: {
return this.request<any[]>(`/standard-rates${query ? `?${query}` : ''}`); departmentId?: number;
subDepartmentId?: number;
activity?: string;
},
) {
const query = params ? new URLSearchParams(params as any).toString() : "";
return this.request<any[]>(`/standard-rates${query ? `?${query}` : ""}`);
} }
async getAllRates(params?: { departmentId?: number; startDate?: string; endDate?: string }) { getAllRates(
const query = params ? new URLSearchParams(params as any).toString() : ''; params?: { departmentId?: number; startDate?: string; endDate?: string },
) {
const query = params ? new URLSearchParams(params as any).toString() : "";
return this.request<{ return this.request<{
allRates: any[]; allRates: any[];
summary: { totalContractorRates: number; totalStandardRates: number; totalRates: number }; summary: {
}>(`/standard-rates/all-rates${query ? `?${query}` : ''}`); totalContractorRates: number;
totalStandardRates: number;
totalRates: number;
};
}>(`/standard-rates/all-rates${query ? `?${query}` : ""}`);
} }
async compareRates(params?: { contractorId?: number; subDepartmentId?: number }) { compareRates(params?: { contractorId?: number; subDepartmentId?: number }) {
const query = params ? new URLSearchParams(params as any).toString() : ''; const query = params ? new URLSearchParams(params as any).toString() : "";
return this.request<{ return this.request<{
standardRates: any[]; standardRates: any[];
contractorRates: any[]; contractorRates: any[];
comparisons: any[]; comparisons: any[];
}>(`/standard-rates/compare${query ? `?${query}` : ''}`); }>(`/standard-rates/compare${query ? `?${query}` : ""}`);
} }
async createStandardRate(data: { createStandardRate(data: {
subDepartmentId?: number; subDepartmentId?: number;
activity?: string; activity?: string;
rate: number; rate: number;
effectiveDate: string effectiveDate: string;
}) { }) {
return this.request<any>('/standard-rates', { return this.request<any>("/standard-rates", {
method: 'POST', method: "POST",
body: JSON.stringify(data), body: JSON.stringify(data),
}); });
} }
async updateStandardRate(id: number, data: { rate?: number; activity?: string; effectiveDate?: string }) { updateStandardRate(
id: number,
data: { rate?: number; activity?: string; effectiveDate?: string },
) {
return this.request<any>(`/standard-rates/${id}`, { return this.request<any>(`/standard-rates/${id}`, {
method: 'PUT', method: "PUT",
body: JSON.stringify(data), body: JSON.stringify(data),
}); });
} }
async deleteStandardRate(id: number) { deleteStandardRate(id: number) {
return this.request<{ message: string }>(`/standard-rates/${id}`, { return this.request<{ message: string }>(`/standard-rates/${id}`, {
method: 'DELETE', method: "DELETE",
}); });
} }
// Activities // Activities
async getActivities(params?: { departmentId?: number; subDepartmentId?: number }) { getActivities(params?: { departmentId?: number; subDepartmentId?: number }) {
const query = params ? new URLSearchParams(params as any).toString() : ''; const query = params ? new URLSearchParams(params as any).toString() : "";
return this.request<any[]>(`/activities${query ? `?${query}` : ''}`); return this.request<any[]>(`/activities${query ? `?${query}` : ""}`);
} }
async getActivity(id: number) { getActivity(id: number) {
return this.request<any>(`/activities/${id}`); return this.request<any>(`/activities/${id}`);
} }
async createActivity(data: { sub_department_id: number; name: string; unit_of_measurement?: string }) { createActivity(
return this.request<any>('/activities', { data: {
method: 'POST', sub_department_id: number;
name: string;
unit_of_measurement?: string;
},
) {
return this.request<any>("/activities", {
method: "POST",
body: JSON.stringify(data), body: JSON.stringify(data),
}); });
} }
async updateActivity(id: number, data: { name?: string; unit_of_measurement?: string }) { updateActivity(
id: number,
data: { name?: string; unit_of_measurement?: string },
) {
return this.request<any>(`/activities/${id}`, { return this.request<any>(`/activities/${id}`, {
method: 'PUT', method: "PUT",
body: JSON.stringify(data), body: JSON.stringify(data),
}); });
} }
async deleteActivity(id: number) { deleteActivity(id: number) {
return this.request<{ message: string }>(`/activities/${id}`, { return this.request<{ message: string }>(`/activities/${id}`, {
method: 'DELETE', method: "DELETE",
}); });
} }
} }

View File

@@ -4,7 +4,7 @@ export interface Employee {
dept: string; dept: string;
sub: string; sub: string;
activity: string; activity: string;
status: 'Present' | 'Absent'; status: "Present" | "Absent";
in: string; in: string;
out: string; out: string;
remark: string; remark: string;
@@ -56,3 +56,32 @@ export interface ChartData {
color?: string; color?: string;
fill?: string; fill?: string;
} }
export type AttendanceStatus = "CheckedIn" | "CheckedOut" | "Absent" | "HalfDay" | "Late";
export type SwapStatus = "Active" | "Completed" | "Cancelled";
export type SwapReason = "LeftWork" | "Sick" | "FinishedEarly" | "Other";
export interface EmployeeSwap {
id: number;
employee_id: number;
employee_name?: string;
original_department_id: number;
original_department_name?: string;
original_contractor_id?: number;
original_contractor_name?: string;
target_department_id: number;
target_department_name?: string;
target_contractor_id?: number;
target_contractor_name?: string;
swap_reason: SwapReason;
reason_details?: string;
work_completion_percentage: number;
swap_date: string;
swapped_by_id: number;
swapped_by_name?: string;
status: SwapStatus;
created_at: string;
updated_at: string;
}

View File

@@ -3,7 +3,7 @@ export interface User {
username: string; username: string;
name: string; name: string;
email: string; email: string;
role: 'SuperAdmin' | 'Supervisor' | 'Contractor' | 'Employee'; role: "SuperAdmin" | "Supervisor" | "Contractor" | "Employee";
department_id?: number; department_id?: number;
contractor_id?: number; contractor_id?: number;
is_active: boolean; is_active: boolean;
@@ -13,13 +13,11 @@ export interface User {
sub_department_id?: number; sub_department_id?: number;
sub_department_name?: string; sub_department_name?: string;
primary_activity?: string; primary_activity?: string;
// Common fields for Employee and Contractor
phone_number?: string; phone_number?: string;
aadhar_number?: string; aadhar_number?: string;
bank_account_number?: string; bank_account_number?: string;
bank_name?: string; bank_name?: string;
bank_ifsc?: string; bank_ifsc?: string;
// Contractor-specific fields
contractor_agreement_number?: string; contractor_agreement_number?: string;
pf_number?: string; pf_number?: string;
esic_number?: string; esic_number?: string;
@@ -45,7 +43,7 @@ export interface Activity {
id: number; id: number;
sub_department_id: number; sub_department_id: number;
name: string; name: string;
unit_of_measurement: 'Per Bag' | 'Fixed Rate-Per Person'; unit_of_measurement: "Per Bag" | "Fixed Rate-Per Person";
created_at: string; created_at: string;
sub_department_name?: string; sub_department_name?: string;
department_id?: number; department_id?: number;
@@ -60,7 +58,7 @@ export interface WorkAllocation {
sub_department_id?: number; sub_department_id?: number;
description?: string; description?: string;
assigned_date: string; assigned_date: string;
status: 'Pending' | 'InProgress' | 'Completed' | 'Cancelled'; status: "Pending" | "InProgress" | "Completed" | "Cancelled";
completion_date?: string; completion_date?: string;
rate?: number; rate?: number;
created_at: string; created_at: string;
@@ -73,7 +71,12 @@ export interface WorkAllocation {
department_name?: string; department_name?: string;
} }
export type AttendanceStatus = 'CheckedIn' | 'CheckedOut' | 'Absent' | 'HalfDay' | 'Late'; export type AttendanceStatus =
| "CheckedIn"
| "CheckedOut"
| "Absent"
| "HalfDay"
| "Late";
export interface Attendance { export interface Attendance {
id: number; id: number;
@@ -93,8 +96,8 @@ export interface Attendance {
contractor_name?: string; contractor_name?: string;
} }
export type SwapReason = 'LeftWork' | 'Sick' | 'FinishedEarly' | 'Other'; export type SwapReason = "LeftWork" | "Sick" | "FinishedEarly" | "Other";
export type SwapStatus = 'Active' | 'Completed' | 'Cancelled'; export type SwapStatus = "Active" | "Completed" | "Cancelled";
export interface EmployeeSwap { export interface EmployeeSwap {
id: number; id: number;

View File

@@ -1,4 +1,4 @@
import * as XLSX from 'xlsx'; import * as XLSX from "xlsx";
interface AllocationData { interface AllocationData {
id: number; id: number;
@@ -44,15 +44,19 @@ interface WorkReportData {
export const exportWorkReportToXLSX = ( export const exportWorkReportToXLSX = (
allocations: AllocationData[], allocations: AllocationData[],
departmentName: string, departmentName: string,
_dateRange: { startDate: string; endDate: string } _dateRange: { startDate: string; endDate: string },
) => { ) => {
// Group allocations by work (activity + sub_department) and date // Group allocations by work (activity + sub_department) and date
const workDataMap = new Map<string, WorkReportData>(); const workDataMap = new Map<string, WorkReportData>();
const allDates = new Set<string>(); const allDates = new Set<string>();
allocations.forEach(allocation => { allocations.forEach((allocation) => {
const workKey = `${allocation.sub_department_name || ''} ${allocation.activity || 'Standard'}`.trim(); const workKey = `${allocation.sub_department_name || ""} ${
const date = allocation.assigned_date ? new Date(allocation.assigned_date).getDate().toString() : ''; allocation.activity || "Standard"
}`.trim();
const date = allocation.assigned_date
? new Date(allocation.assigned_date).getDate().toString()
: "";
if (date) { if (date) {
allDates.add(date); allDates.add(date);
@@ -75,7 +79,8 @@ export const exportWorkReportToXLSX = (
const bag = parseFloat(String(allocation.units)) || 0; const bag = parseFloat(String(allocation.units)) || 0;
const rate = parseFloat(String(allocation.rate)) || 0; const rate = parseFloat(String(allocation.rate)) || 0;
const total = parseFloat(String(allocation.total_amount)) || (bag * rate) || rate; const total = parseFloat(String(allocation.total_amount)) || (bag * rate) ||
rate;
workData.dates[date].bag += bag; workData.dates[date].bag += bag;
workData.dates[date].rate = rate; // Use latest rate workData.dates[date].rate = rate; // Use latest rate
@@ -85,33 +90,38 @@ export const exportWorkReportToXLSX = (
}); });
// Sort dates numerically // Sort dates numerically
const sortedDates = Array.from(allDates).sort((a, b) => parseInt(a) - parseInt(b)); const sortedDates = Array.from(allDates).sort((a, b) =>
parseInt(a) - parseInt(b)
);
// Create workbook and worksheet // Create workbook and worksheet
const wb = XLSX.utils.book_new(); const wb = XLSX.utils.book_new();
const wsData: (string | number | null)[][] = []; const wsData: (string | number | null)[][] = [];
// Row 1: DATE header with merged cells for each date // Row 1: DATE header with merged cells for each date
const dateHeaderRow: (string | number | null)[] = ['', 'DATE']; const dateHeaderRow: (string | number | null)[] = ["", "DATE"];
sortedDates.forEach(date => { sortedDates.forEach((date) => {
dateHeaderRow.push(date, '', ''); // Each date spans 3 columns (Bag, Rate, Total) dateHeaderRow.push(date, "", ""); // Each date spans 3 columns (Bag, Rate, Total)
}); });
dateHeaderRow.push('', 'Total', '', '', 'Total-As per Standered', '', ''); dateHeaderRow.push("", "Total", "", "", "Total-As per Standered", "", "");
wsData.push(dateHeaderRow); wsData.push(dateHeaderRow);
// Row 2: WORK and Bag/Rate/Total sub-headers // Row 2: WORK and Bag/Rate/Total sub-headers
const subHeaderRow: (string | number | null)[] = ['', 'WORK']; const subHeaderRow: (string | number | null)[] = ["", "WORK"];
sortedDates.forEach(() => { sortedDates.forEach(() => {
subHeaderRow.push('Bag', 'Rate', 'Total'); subHeaderRow.push("Bag", "Rate", "Total");
}); });
subHeaderRow.push('', 'Bag', 'Rate', 'Total', 'Bag', 'Rate', 'Total'); subHeaderRow.push("", "Bag", "Rate", "Total", "Bag", "Rate", "Total");
wsData.push(subHeaderRow); wsData.push(subHeaderRow);
// Row 3: Department header (yellow background) // Row 3: Department header (yellow background)
const deptHeaderRow: (string | number | null)[] = ['', `${departmentName.toUpperCase()} Department`]; const deptHeaderRow: (string | number | null)[] = [
"",
`${departmentName.toUpperCase()} Department`,
];
const deptHeaderCols = sortedDates.length * 3 + 7; const deptHeaderCols = sortedDates.length * 3 + 7;
for (let col = 0; col < deptHeaderCols; col++) { for (let col = 0; col < deptHeaderCols; col++) {
deptHeaderRow.push(''); deptHeaderRow.push("");
} }
wsData.push(deptHeaderRow); wsData.push(deptHeaderRow);
@@ -121,25 +131,25 @@ export const exportWorkReportToXLSX = (
workDataArray.forEach((workData, index) => { workDataArray.forEach((workData, index) => {
const dataRow: (string | number | null)[] = [index + 1, workData.work]; const dataRow: (string | number | null)[] = [index + 1, workData.work];
sortedDates.forEach(date => { sortedDates.forEach((date) => {
const dateData = workData.dates[date] || { bag: 0, rate: 0, total: 0 }; const dateData = workData.dates[date] || { bag: 0, rate: 0, total: 0 };
dataRow.push( dataRow.push(
dateData.bag || '', dateData.bag || "",
dateData.rate || '', dateData.rate || "",
dateData.total || '' dateData.total || "",
); );
}); });
// Total columns // Total columns
dataRow.push(''); // Empty column dataRow.push(""); // Empty column
dataRow.push(workData.totalBag || ''); dataRow.push(workData.totalBag || "");
dataRow.push(''); // Rate for total (could be average) dataRow.push(""); // Rate for total (could be average)
dataRow.push(workData.totalAmount || ''); dataRow.push(workData.totalAmount || "");
// Standard columns (placeholder - would need standard rates data) // Standard columns (placeholder - would need standard rates data)
dataRow.push(''); dataRow.push("");
dataRow.push(''); dataRow.push("");
dataRow.push(''); dataRow.push("");
wsData.push(dataRow); wsData.push(dataRow);
}); });
@@ -148,33 +158,36 @@ export const exportWorkReportToXLSX = (
wsData.push([]); wsData.push([]);
// Sub Total row // Sub Total row
const subTotalRow: (string | number | null)[] = ['', 'Sub Total']; const subTotalRow: (string | number | null)[] = ["", "Sub Total"];
// Calculate totals for each date // Calculate totals for each date
sortedDates.forEach(date => { sortedDates.forEach((date) => {
let dateBagTotal = 0; let dateBagTotal = 0;
let dateTotalAmount = 0; let dateTotalAmount = 0;
workDataArray.forEach(workData => { workDataArray.forEach((workData) => {
const dateData = workData.dates[date]; const dateData = workData.dates[date];
if (dateData) { if (dateData) {
dateBagTotal += dateData.bag; dateBagTotal += dateData.bag;
dateTotalAmount += dateData.total; dateTotalAmount += dateData.total;
} }
}); });
subTotalRow.push(dateBagTotal || '', '', dateTotalAmount || ''); subTotalRow.push(dateBagTotal || "", "", dateTotalAmount || "");
}); });
// Grand totals // Grand totals
const grandTotalBag = workDataArray.reduce((sum, w) => sum + w.totalBag, 0); const grandTotalBag = workDataArray.reduce((sum, w) => sum + w.totalBag, 0);
const grandTotalAmount = workDataArray.reduce((sum, w) => sum + w.totalAmount, 0); const grandTotalAmount = workDataArray.reduce(
(sum, w) => sum + w.totalAmount,
0,
);
subTotalRow.push(''); subTotalRow.push("");
subTotalRow.push(grandTotalBag || ''); subTotalRow.push(grandTotalBag || "");
subTotalRow.push(''); subTotalRow.push("");
subTotalRow.push(grandTotalAmount || ''); subTotalRow.push(grandTotalAmount || "");
subTotalRow.push(''); subTotalRow.push("");
subTotalRow.push(''); subTotalRow.push("");
subTotalRow.push(grandTotalAmount || ''); // Standard total same as actual for now subTotalRow.push(grandTotalAmount || ""); // Standard total same as actual for now
wsData.push(subTotalRow); wsData.push(subTotalRow);
@@ -203,7 +216,7 @@ export const exportWorkReportToXLSX = (
colWidths.push({ wch: 6 }); // Standard Rate colWidths.push({ wch: 6 }); // Standard Rate
colWidths.push({ wch: 12 }); // Standard Total colWidths.push({ wch: 12 }); // Standard Total
ws['!cols'] = colWidths; ws["!cols"] = colWidths;
// Merge cells for DATE headers // Merge cells for DATE headers
const merges: XLSX.Range[] = []; const merges: XLSX.Range[] = [];
@@ -213,7 +226,7 @@ export const exportWorkReportToXLSX = (
sortedDates.forEach(() => { sortedDates.forEach(() => {
merges.push({ merges.push({
s: { r: 0, c: colIndex }, s: { r: 0, c: colIndex },
e: { r: 0, c: colIndex + 2 } e: { r: 0, c: colIndex + 2 },
}); });
colIndex += 3; colIndex += 3;
}); });
@@ -221,28 +234,30 @@ export const exportWorkReportToXLSX = (
// Merge Total header // Merge Total header
merges.push({ merges.push({
s: { r: 0, c: colIndex + 1 }, s: { r: 0, c: colIndex + 1 },
e: { r: 0, c: colIndex + 3 } e: { r: 0, c: colIndex + 3 },
}); });
// Merge "Total-As per Standered" header // Merge "Total-As per Standered" header
merges.push({ merges.push({
s: { r: 0, c: colIndex + 4 }, s: { r: 0, c: colIndex + 4 },
e: { r: 0, c: colIndex + 6 } e: { r: 0, c: colIndex + 6 },
}); });
// Merge department header row // Merge department header row
merges.push({ merges.push({
s: { r: 2, c: 1 }, s: { r: 2, c: 1 },
e: { r: 2, c: colIndex + 6 } e: { r: 2, c: colIndex + 6 },
}); });
ws['!merges'] = merges; ws["!merges"] = merges;
// Add worksheet to workbook // Add worksheet to workbook
XLSX.utils.book_append_sheet(wb, ws, 'Work Report'); XLSX.utils.book_append_sheet(wb, ws, "Work Report");
// Generate filename // Generate filename
const filename = `work_report_${departmentName.toLowerCase().replace(/\s+/g, '_')}_${new Date().toISOString().split('T')[0]}.xlsx`; const filename = `work_report_${
departmentName.toLowerCase().replace(/\s+/g, "_")
}_${new Date().toISOString().split("T")[0]}.xlsx`;
// Write and download // Write and download
XLSX.writeFile(wb, filename); XLSX.writeFile(wb, filename);
@@ -253,27 +268,31 @@ export const exportWorkReportToXLSX = (
*/ */
export const exportAllocationsToXLSX = ( export const exportAllocationsToXLSX = (
allocations: AllocationData[], allocations: AllocationData[],
filename?: string filename?: string,
) => { ) => {
if (allocations.length === 0) { if (allocations.length === 0) {
alert('No data to export'); alert("No data to export");
return; return;
} }
// Transform data for export // Transform data for export
const exportData = allocations.map((a, index) => ({ const exportData = allocations.map((a, index) => ({
'S.No': index + 1, "S.No": index + 1,
'Employee Name': a.employee_name || '', "Employee Name": a.employee_name || "",
'Contractor': a.contractor_name || '', "Contractor": a.contractor_name || "",
'Department': a.department_name || '', "Department": a.department_name || "",
'Sub-Department': a.sub_department_name || '', "Sub-Department": a.sub_department_name || "",
'Activity': a.activity || 'Standard', "Activity": a.activity || "Standard",
'Assigned Date': a.assigned_date ? new Date(a.assigned_date).toLocaleDateString() : '', "Assigned Date": a.assigned_date
'Completion Date': a.completion_date ? new Date(a.completion_date).toLocaleDateString() : '', ? new Date(a.assigned_date).toLocaleDateString()
'Rate': a.rate || 0, : "",
'Units': a.units || '', "Completion Date": a.completion_date
'Total Amount': a.total_amount || a.rate || 0, ? new Date(a.completion_date).toLocaleDateString()
'Status': a.status || '', : "",
"Rate": a.rate || 0,
"Units": a.units || "",
"Total Amount": a.total_amount || a.rate || 0,
"Status": a.status || "",
})); }));
// Create workbook // Create workbook
@@ -281,7 +300,7 @@ export const exportAllocationsToXLSX = (
const ws = XLSX.utils.json_to_sheet(exportData); const ws = XLSX.utils.json_to_sheet(exportData);
// Set column widths // Set column widths
ws['!cols'] = [ ws["!cols"] = [
{ wch: 6 }, // S.No { wch: 6 }, // S.No
{ wch: 25 }, // Employee Name { wch: 25 }, // Employee Name
{ wch: 20 }, // Contractor { wch: 20 }, // Contractor
@@ -296,10 +315,11 @@ export const exportAllocationsToXLSX = (
{ wch: 10 }, // Status { wch: 10 }, // Status
]; ];
XLSX.utils.book_append_sheet(wb, ws, 'Allocations'); XLSX.utils.book_append_sheet(wb, ws, "Allocations");
// Generate filename // Generate filename
const outputFilename = filename || `allocations_${new Date().toISOString().split('T')[0]}.xlsx`; const outputFilename = filename ||
`allocations_${new Date().toISOString().split("T")[0]}.xlsx`;
// Write and download // Write and download
XLSX.writeFile(wb, outputFilename); XLSX.writeFile(wb, outputFilename);

View File

@@ -8,4 +8,4 @@ export default {
extend: {}, extend: {},
}, },
plugins: [], plugins: [],
} };

View File

@@ -1,11 +1,11 @@
import { defineConfig } from 'vite' import { defineConfig } from "vite";
import react from '@vitejs/plugin-react' import react from "@vitejs/plugin-react";
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [react()], plugins: [react()],
server: { server: {
host: true, // Allow access from any host host: true, // Allow access from any host
allowedHosts: ['all'], // Allow all hosts allowedHosts: ["all"], // Allow all hosts
}, },
}) });