(Feat-Fix): Lots of fixes done, reporting system fixed, stricter types
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
# 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
|
||||
|
||||
@@ -90,7 +91,8 @@ deno task seed
|
||||
- `GET /api/departments/:id` - Get department
|
||||
- `GET /api/departments/:id/sub-departments` - Get sub-departments
|
||||
- `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
|
||||
|
||||
@@ -122,21 +124,21 @@ deno task seed
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `PORT` | Server port | 3000 |
|
||||
| `DB_HOST` | Database host | localhost |
|
||||
| `DB_USER` | Database user | root |
|
||||
| `DB_PASSWORD` | Database password | admin123 |
|
||||
| `DB_NAME` | Database name | work_allocation |
|
||||
| `DB_PORT` | Database port | 3306 |
|
||||
| `JWT_SECRET` | JWT signing secret | (change in production!) |
|
||||
| `JWT_EXPIRES_IN` | Token expiration | 7d |
|
||||
| `BCRYPT_ROUNDS` | Password hash rounds | 12 |
|
||||
| `RATE_LIMIT_WINDOW_MS` | Rate limit window | 900000 (15 min) |
|
||||
| `RATE_LIMIT_MAX_REQUESTS` | Max requests per window | 100 |
|
||||
| `CORS_ORIGIN` | Allowed CORS origins | <http://localhost:5173> |
|
||||
| `NODE_ENV` | Environment | development |
|
||||
| Variable | Description | Default |
|
||||
| ------------------------- | ----------------------- | ----------------------- |
|
||||
| `PORT` | Server port | 3000 |
|
||||
| `DB_HOST` | Database host | localhost |
|
||||
| `DB_USER` | Database user | root |
|
||||
| `DB_PASSWORD` | Database password | admin123 |
|
||||
| `DB_NAME` | Database name | work_allocation |
|
||||
| `DB_PORT` | Database port | 3306 |
|
||||
| `JWT_SECRET` | JWT signing secret | (change in production!) |
|
||||
| `JWT_EXPIRES_IN` | Token expiration | 7d |
|
||||
| `BCRYPT_ROUNDS` | Password hash rounds | 12 |
|
||||
| `RATE_LIMIT_WINDOW_MS` | Rate limit window | 900000 (15 min) |
|
||||
| `RATE_LIMIT_MAX_REQUESTS` | Max requests per window | 100 |
|
||||
| `CORS_ORIGIN` | Allowed CORS origins | <http://localhost:5173> |
|
||||
| `NODE_ENV` | Environment | development |
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
@@ -196,14 +198,14 @@ backend-deno/
|
||||
|
||||
## Differences from Node.js Backend
|
||||
|
||||
| Feature | Node.js | Deno |
|
||||
|---------|---------|------|
|
||||
| Runtime | Node.js | Deno |
|
||||
| Package Manager | npm | Built-in (JSR/npm) |
|
||||
| TypeScript | Requires compilation | Native support |
|
||||
| Security | Manual setup | Secure by default |
|
||||
| Permissions | Full access | Explicit permissions |
|
||||
| Framework | Express | Oak |
|
||||
| Feature | Node.js | Deno |
|
||||
| --------------- | -------------------- | -------------------- |
|
||||
| Runtime | Node.js | Deno |
|
||||
| Package Manager | npm | Built-in (JSR/npm) |
|
||||
| TypeScript | Requires compilation | Native support |
|
||||
| Security | Manual setup | Secure by default |
|
||||
| Permissions | Full access | Explicit permissions |
|
||||
| Framework | Express | Oak |
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createPool, Pool } from "mysql2/promise";
|
||||
import { createPool, Pool, PoolConnection } from "mysql2/promise";
|
||||
import { load } from "@std/dotenv";
|
||||
|
||||
// Load environment variables
|
||||
@@ -33,14 +33,17 @@ class Database {
|
||||
async connect(): Promise<Pool> {
|
||||
if (!this.pool) {
|
||||
this.pool = createPool(config);
|
||||
|
||||
|
||||
// Test connection
|
||||
try {
|
||||
const connection = await this.pool.getConnection();
|
||||
console.log("✅ Database connected successfully");
|
||||
connection.release();
|
||||
} catch (error) {
|
||||
console.error("❌ Database connection failed:", (error as Error).message);
|
||||
console.error(
|
||||
"❌ Database connection failed:",
|
||||
(error as Error).message,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -60,12 +63,39 @@ class Database {
|
||||
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 [result] = await pool.execute(sql, params);
|
||||
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> {
|
||||
if (this.pool) {
|
||||
await this.pool.end();
|
||||
|
||||
@@ -5,36 +5,41 @@ await load({ export: true });
|
||||
export const config = {
|
||||
// Server
|
||||
PORT: parseInt(Deno.env.get("PORT") || "3000"),
|
||||
|
||||
|
||||
// Database
|
||||
DB_HOST: Deno.env.get("DB_HOST") || "localhost",
|
||||
DB_USER: Deno.env.get("DB_USER") || "root",
|
||||
DB_PASSWORD: Deno.env.get("DB_PASSWORD") || "admin123",
|
||||
DB_NAME: Deno.env.get("DB_NAME") || "work_allocation",
|
||||
DB_PORT: parseInt(Deno.env.get("DB_PORT") || "3306"),
|
||||
|
||||
|
||||
// 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",
|
||||
|
||||
|
||||
// Security settings
|
||||
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_MAX_REQUESTS: parseInt(Deno.env.get("RATE_LIMIT_MAX_REQUESTS") || "100"),
|
||||
|
||||
RATE_LIMIT_WINDOW_MS: parseInt(
|
||||
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_ORIGIN: Deno.env.get("CORS_ORIGIN") || "http://localhost:5173",
|
||||
|
||||
|
||||
// Environment
|
||||
NODE_ENV: Deno.env.get("NODE_ENV") || "development",
|
||||
|
||||
|
||||
isDevelopment(): boolean {
|
||||
return this.NODE_ENV === "development";
|
||||
},
|
||||
|
||||
|
||||
isProduction(): boolean {
|
||||
return this.NODE_ENV === "production";
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { Application, Router } from "@oak/oak";
|
||||
import { config } from "./config/env.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 authRoutes from "./routes/auth.ts";
|
||||
@@ -61,14 +66,46 @@ router.get("/health", (ctx) => {
|
||||
// Mount API routes
|
||||
router.use("/api/auth", authRoutes.routes(), authRoutes.allowedMethods());
|
||||
router.use("/api/users", userRoutes.routes(), userRoutes.allowedMethods());
|
||||
router.use("/api/departments", departmentRoutes.routes(), departmentRoutes.allowedMethods());
|
||||
router.use("/api/work-allocations", 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());
|
||||
router.use(
|
||||
"/api/departments",
|
||||
departmentRoutes.routes(),
|
||||
departmentRoutes.allowedMethods(),
|
||||
);
|
||||
router.use(
|
||||
"/api/work-allocations",
|
||||
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
|
||||
app.use(router.routes());
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 type { JWTPayload, UserRole } from "../types/index.ts";
|
||||
|
||||
@@ -12,14 +12,16 @@ const cryptoKey = await crypto.subtle.importKey(
|
||||
keyData,
|
||||
{ name: "HMAC", hash: "SHA-256" },
|
||||
false,
|
||||
["sign", "verify"]
|
||||
["sign", "verify"],
|
||||
);
|
||||
|
||||
// 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;
|
||||
let expSeconds = 7 * 24 * 60 * 60; // Default 7 days
|
||||
|
||||
|
||||
if (expiresIn.endsWith("d")) {
|
||||
expSeconds = parseInt(expiresIn) * 24 * 60 * 60;
|
||||
} else if (expiresIn.endsWith("h")) {
|
||||
@@ -27,7 +29,7 @@ export async function generateToken(payload: Omit<JWTPayload, "exp" | "iat">): P
|
||||
} else if (expiresIn.endsWith("m")) {
|
||||
expSeconds = parseInt(expiresIn) * 60;
|
||||
}
|
||||
|
||||
|
||||
const token = await create(
|
||||
{ alg: "HS256", typ: "JWT" },
|
||||
{
|
||||
@@ -35,9 +37,9 @@ export async function generateToken(payload: Omit<JWTPayload, "exp" | "iat">): P
|
||||
exp: getNumericDate(expSeconds),
|
||||
iat: getNumericDate(0),
|
||||
},
|
||||
cryptoKey
|
||||
cryptoKey,
|
||||
);
|
||||
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
@@ -52,24 +54,27 @@ export async function verifyToken(token: string): Promise<JWTPayload | null> {
|
||||
}
|
||||
|
||||
// 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 token = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : null;
|
||||
|
||||
|
||||
if (!token) {
|
||||
ctx.response.status = 401;
|
||||
ctx.response.body = { error: "Access token required" };
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const payload = await verifyToken(token);
|
||||
|
||||
|
||||
if (!payload) {
|
||||
ctx.response.status = 403;
|
||||
ctx.response.body = { error: "Invalid or expired token" };
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Attach user to context state
|
||||
ctx.state.user = payload;
|
||||
await next();
|
||||
@@ -79,19 +84,19 @@ export async function authenticateToken(ctx: Context, next: Next): Promise<void>
|
||||
export function authorize(...roles: UserRole[]) {
|
||||
return async (ctx: Context, next: Next): Promise<void> => {
|
||||
const user = ctx.state.user as JWTPayload | undefined;
|
||||
|
||||
|
||||
if (!user) {
|
||||
ctx.response.status = 401;
|
||||
ctx.response.body = { error: "Unauthorized" };
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (!roles.includes(user.role)) {
|
||||
ctx.response.status = 403;
|
||||
ctx.response.body = { error: "Insufficient permissions" };
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
await next();
|
||||
};
|
||||
}
|
||||
|
||||
@@ -10,54 +10,57 @@ export async function rateLimit(ctx: Context, next: Next): Promise<void> {
|
||||
const now = Date.now();
|
||||
const windowMs = config.RATE_LIMIT_WINDOW_MS;
|
||||
const maxRequests = config.RATE_LIMIT_MAX_REQUESTS;
|
||||
|
||||
|
||||
const record = rateLimitStore.get(ip);
|
||||
|
||||
|
||||
if (!record || now > record.resetTime) {
|
||||
rateLimitStore.set(ip, { count: 1, resetTime: now + windowMs });
|
||||
} else {
|
||||
record.count++;
|
||||
|
||||
|
||||
if (record.count > maxRequests) {
|
||||
ctx.response.status = 429;
|
||||
ctx.response.body = {
|
||||
ctx.response.body = {
|
||||
error: "Too many requests",
|
||||
retryAfter: Math.ceil((record.resetTime - now) / 1000)
|
||||
retryAfter: Math.ceil((record.resetTime - now) / 1000),
|
||||
};
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
await next();
|
||||
}
|
||||
|
||||
// Security headers middleware
|
||||
export async function securityHeaders(ctx: Context, next: Next): Promise<void> {
|
||||
await next();
|
||||
|
||||
|
||||
// Prevent clickjacking
|
||||
ctx.response.headers.set("X-Frame-Options", "DENY");
|
||||
|
||||
|
||||
// Prevent MIME type sniffing
|
||||
ctx.response.headers.set("X-Content-Type-Options", "nosniff");
|
||||
|
||||
|
||||
// XSS protection
|
||||
ctx.response.headers.set("X-XSS-Protection", "1; mode=block");
|
||||
|
||||
|
||||
// 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
|
||||
ctx.response.headers.set(
|
||||
"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)
|
||||
if (config.isProduction()) {
|
||||
ctx.response.headers.set(
|
||||
"Strict-Transport-Security",
|
||||
"max-age=31536000; includeSubDomains"
|
||||
"max-age=31536000; includeSubDomains",
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -65,27 +68,35 @@ export async function securityHeaders(ctx: Context, next: Next): Promise<void> {
|
||||
// CORS middleware
|
||||
export async function cors(ctx: Context, next: Next): Promise<void> {
|
||||
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
|
||||
if (origin && (allowedOrigins.includes(origin) || allowedOrigins.includes("*"))) {
|
||||
if (
|
||||
origin && (allowedOrigins.includes(origin) || allowedOrigins.includes("*"))
|
||||
) {
|
||||
ctx.response.headers.set("Access-Control-Allow-Origin", origin);
|
||||
} else if (config.isDevelopment()) {
|
||||
// Allow all origins in development
|
||||
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("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
||||
|
||||
ctx.response.headers.set(
|
||||
"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-Max-Age", "86400");
|
||||
|
||||
|
||||
// Handle preflight requests
|
||||
if (ctx.request.method === "OPTIONS") {
|
||||
ctx.response.status = 204;
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
await next();
|
||||
}
|
||||
|
||||
@@ -93,19 +104,21 @@ export async function cors(ctx: Context, next: Next): Promise<void> {
|
||||
export async function requestLogger(ctx: Context, next: Next): Promise<void> {
|
||||
const start = Date.now();
|
||||
const { method, url } = ctx.request;
|
||||
|
||||
|
||||
await next();
|
||||
|
||||
|
||||
const ms = Date.now() - start;
|
||||
const status = ctx.response.status;
|
||||
|
||||
|
||||
// Color code based on status
|
||||
let statusColor = "\x1b[32m"; // Green for 2xx
|
||||
if (status >= 400 && status < 500) statusColor = "\x1b[33m"; // Yellow for 4xx
|
||||
if (status >= 500) statusColor = "\x1b[31m"; // Red for 5xx
|
||||
|
||||
|
||||
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
|
||||
export function isStrongPassword(password: string): { valid: boolean; message?: string } {
|
||||
export function isStrongPassword(
|
||||
password: string,
|
||||
): { valid: boolean; message?: string } {
|
||||
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)) {
|
||||
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)) {
|
||||
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)) {
|
||||
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 };
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ router.get("/", authenticateToken, async (ctx) => {
|
||||
const params = ctx.request.url.searchParams;
|
||||
const subDepartmentId = params.get("subDepartmentId");
|
||||
const departmentId = params.get("departmentId");
|
||||
|
||||
|
||||
let query = `
|
||||
SELECT a.id, a.sub_department_id, a.name, a.unit_of_measurement, a.created_at,
|
||||
sd.name as sub_department_name,
|
||||
@@ -33,19 +33,19 @@ router.get("/", authenticateToken, async (ctx) => {
|
||||
WHERE 1=1
|
||||
`;
|
||||
const queryParams: unknown[] = [];
|
||||
|
||||
|
||||
if (subDepartmentId) {
|
||||
query += " AND a.sub_department_id = ?";
|
||||
queryParams.push(subDepartmentId);
|
||||
}
|
||||
|
||||
|
||||
if (departmentId) {
|
||||
query += " AND sd.department_id = ?";
|
||||
queryParams.push(departmentId);
|
||||
}
|
||||
|
||||
|
||||
query += " ORDER BY d.name, sd.name, a.name";
|
||||
|
||||
|
||||
const activities = await db.query<Activity[]>(query, queryParams);
|
||||
ctx.response.body = activities;
|
||||
} catch (error) {
|
||||
@@ -59,7 +59,7 @@ router.get("/", authenticateToken, async (ctx) => {
|
||||
router.get("/:id", authenticateToken, async (ctx) => {
|
||||
try {
|
||||
const activityId = ctx.params.id;
|
||||
|
||||
|
||||
const activities = await db.query<Activity[]>(
|
||||
`SELECT a.id, a.sub_department_id, a.name, a.unit_of_measurement, a.created_at,
|
||||
sd.name as sub_department_name,
|
||||
@@ -69,15 +69,15 @@ router.get("/:id", authenticateToken, async (ctx) => {
|
||||
JOIN sub_departments sd ON a.sub_department_id = sd.id
|
||||
JOIN departments d ON sd.department_id = d.id
|
||||
WHERE a.id = ?`,
|
||||
[activityId]
|
||||
[activityId],
|
||||
);
|
||||
|
||||
|
||||
if (activities.length === 0) {
|
||||
ctx.response.status = 404;
|
||||
ctx.response.body = { error: "Activity not found" };
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
ctx.response.body = activities[0];
|
||||
} catch (error) {
|
||||
console.error("Get activity error:", error);
|
||||
@@ -92,55 +92,61 @@ router.post("/", authenticateToken, async (ctx) => {
|
||||
const user = getCurrentUser(ctx);
|
||||
const body = await ctx.request.body.json();
|
||||
const { sub_department_id, name, unit_of_measurement } = body;
|
||||
|
||||
|
||||
if (!sub_department_id || !name) {
|
||||
ctx.response.status = 400;
|
||||
ctx.response.body = { error: "Sub-department ID and name are required" };
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Get the sub-department to check department ownership
|
||||
const subDepts = await db.query<{ department_id: number }[]>(
|
||||
"SELECT department_id FROM sub_departments WHERE id = ?",
|
||||
[sub_department_id]
|
||||
[sub_department_id],
|
||||
);
|
||||
|
||||
|
||||
if (subDepts.length === 0) {
|
||||
ctx.response.status = 404;
|
||||
ctx.response.body = { error: "Sub-department not found" };
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const subDeptDepartmentId = subDepts[0].department_id;
|
||||
|
||||
|
||||
// Check authorization
|
||||
if (user.role === 'Supervisor' && user.departmentId !== subDeptDepartmentId) {
|
||||
if (
|
||||
user.role === "Supervisor" && user.departmentId !== subDeptDepartmentId
|
||||
) {
|
||||
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;
|
||||
}
|
||||
|
||||
if (user.role !== 'SuperAdmin' && user.role !== 'Supervisor') {
|
||||
|
||||
if (user.role !== "SuperAdmin" && user.role !== "Supervisor") {
|
||||
ctx.response.status = 403;
|
||||
ctx.response.body = { error: "Unauthorized" };
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const result = await db.execute(
|
||||
"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.body = {
|
||||
id: result.lastInsertId,
|
||||
message: "Activity created successfully"
|
||||
ctx.response.body = {
|
||||
id: result.insertId,
|
||||
message: "Activity created successfully",
|
||||
};
|
||||
} catch (error) {
|
||||
const err = error as { code?: string };
|
||||
if (err.code === "ER_DUP_ENTRY") {
|
||||
ctx.response.status = 400;
|
||||
ctx.response.body = { error: "Activity already exists in this sub-department" };
|
||||
ctx.response.body = {
|
||||
error: "Activity already exists in this sub-department",
|
||||
};
|
||||
return;
|
||||
}
|
||||
console.error("Create activity error:", error);
|
||||
@@ -155,12 +161,12 @@ router.put("/:id", authenticateToken, async (ctx) => {
|
||||
const activityId = ctx.params.id;
|
||||
const body = await ctx.request.body.json();
|
||||
const { name, unit_of_measurement } = body;
|
||||
|
||||
|
||||
await db.execute(
|
||||
"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" };
|
||||
} catch (error) {
|
||||
console.error("Update activity error:", error);
|
||||
@@ -174,39 +180,43 @@ router.delete("/:id", authenticateToken, async (ctx) => {
|
||||
try {
|
||||
const user = getCurrentUser(ctx);
|
||||
const activityId = ctx.params.id;
|
||||
|
||||
|
||||
// Get the activity and its sub-department to check department ownership
|
||||
const activities = await db.query<Activity[]>(
|
||||
`SELECT a.*, sd.department_id
|
||||
FROM activities a
|
||||
JOIN sub_departments sd ON a.sub_department_id = sd.id
|
||||
WHERE a.id = ?`,
|
||||
[activityId]
|
||||
[activityId],
|
||||
);
|
||||
|
||||
|
||||
if (activities.length === 0) {
|
||||
ctx.response.status = 404;
|
||||
ctx.response.body = { error: "Activity not found" };
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const activity = activities[0] as Activity & { department_id: number };
|
||||
|
||||
|
||||
// Check authorization
|
||||
if (user.role === 'Supervisor' && user.departmentId !== activity.department_id) {
|
||||
if (
|
||||
user.role === "Supervisor" && user.departmentId !== activity.department_id
|
||||
) {
|
||||
ctx.response.status = 403;
|
||||
ctx.response.body = { error: "You can only delete activities from your own department" };
|
||||
ctx.response.body = {
|
||||
error: "You can only delete activities from your own department",
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
if (user.role !== 'SuperAdmin' && user.role !== 'Supervisor') {
|
||||
|
||||
if (user.role !== "SuperAdmin" && user.role !== "Supervisor") {
|
||||
ctx.response.status = 403;
|
||||
ctx.response.body = { error: "Unauthorized" };
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
await db.execute("DELETE FROM activities WHERE id = ?", [activityId]);
|
||||
|
||||
|
||||
ctx.response.body = { message: "Activity deleted successfully" };
|
||||
} catch (error) {
|
||||
console.error("Delete activity error:", error);
|
||||
|
||||
@@ -1,21 +1,37 @@
|
||||
import { Router } from "@oak/oak";
|
||||
import { Router, type RouterContext, type State } from "@oak/oak";
|
||||
import { db } from "../config/database.ts";
|
||||
import { authenticateToken, authorize, getCurrentUser } from "../middleware/auth.ts";
|
||||
import type { Attendance, CheckInOutRequest, User, UpdateAttendanceStatusRequest, AttendanceStatus } from "../types/index.ts";
|
||||
import {
|
||||
authenticateToken,
|
||||
authorize,
|
||||
getCurrentUser,
|
||||
} from "../middleware/auth.ts";
|
||||
import type {
|
||||
Attendance,
|
||||
AttendanceStatus,
|
||||
CheckInOutRequest,
|
||||
JWTPayload,
|
||||
UpdateAttendanceStatusRequest,
|
||||
User,
|
||||
} from "../types/index.ts";
|
||||
|
||||
const router = new Router();
|
||||
|
||||
// Get all attendance records
|
||||
router.get("/", authenticateToken, async (ctx) => {
|
||||
try {
|
||||
const currentUser = getCurrentUser(ctx);
|
||||
const params = ctx.request.url.searchParams;
|
||||
const employeeId = params.get("employeeId");
|
||||
const startDate = params.get("startDate");
|
||||
const endDate = params.get("endDate");
|
||||
const status = params.get("status");
|
||||
|
||||
let query = `
|
||||
router.get(
|
||||
"/",
|
||||
authenticateToken,
|
||||
async (
|
||||
ctx: RouterContext<"/", Record<string | number, string | undefined>, State>,
|
||||
) => {
|
||||
try {
|
||||
const currentUser: JWTPayload = getCurrentUser(ctx);
|
||||
const params: URLSearchParams = ctx.request.url.searchParams;
|
||||
const employeeId: string | null = params.get("employeeId");
|
||||
const startDate: string | null = params.get("startDate");
|
||||
const endDate: string | null = params.get("endDate");
|
||||
const status: string | null = params.get("status");
|
||||
|
||||
let query = `
|
||||
SELECT a.*,
|
||||
e.name as employee_name, e.username as employee_username,
|
||||
s.name as supervisor_name,
|
||||
@@ -28,53 +44,54 @@ router.get("/", authenticateToken, async (ctx) => {
|
||||
LEFT JOIN users c ON e.contractor_id = c.id
|
||||
WHERE 1=1
|
||||
`;
|
||||
const queryParams: unknown[] = [];
|
||||
|
||||
// Role-based filtering
|
||||
if (currentUser.role === "Supervisor") {
|
||||
query += " AND a.supervisor_id = ?";
|
||||
queryParams.push(currentUser.id);
|
||||
} else if (currentUser.role === "Employee") {
|
||||
query += " AND a.employee_id = ?";
|
||||
queryParams.push(currentUser.id);
|
||||
const queryParams: unknown[] = [];
|
||||
|
||||
// Role-based filtering
|
||||
if (currentUser.role === "Supervisor") {
|
||||
query += " AND a.supervisor_id = ?";
|
||||
queryParams.push(currentUser.id);
|
||||
} else if (currentUser.role === "Employee") {
|
||||
query += " AND a.employee_id = ?";
|
||||
queryParams.push(currentUser.id);
|
||||
}
|
||||
|
||||
if (employeeId) {
|
||||
query += " AND a.employee_id = ?";
|
||||
queryParams.push(employeeId);
|
||||
}
|
||||
|
||||
if (startDate) {
|
||||
query += " AND a.work_date >= ?";
|
||||
queryParams.push(startDate);
|
||||
}
|
||||
|
||||
if (endDate) {
|
||||
query += " AND a.work_date <= ?";
|
||||
queryParams.push(endDate);
|
||||
}
|
||||
|
||||
if (status) {
|
||||
query += " AND a.status = ?";
|
||||
queryParams.push(status);
|
||||
}
|
||||
|
||||
query += " ORDER BY a.work_date DESC, a.check_in_time DESC";
|
||||
|
||||
const records = await db.query<Attendance[]>(query, queryParams);
|
||||
ctx.response.body = records;
|
||||
} catch (error) {
|
||||
console.error("Get attendance error:", error);
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: "Internal server error" };
|
||||
}
|
||||
|
||||
if (employeeId) {
|
||||
query += " AND a.employee_id = ?";
|
||||
queryParams.push(employeeId);
|
||||
}
|
||||
|
||||
if (startDate) {
|
||||
query += " AND a.work_date >= ?";
|
||||
queryParams.push(startDate);
|
||||
}
|
||||
|
||||
if (endDate) {
|
||||
query += " AND a.work_date <= ?";
|
||||
queryParams.push(endDate);
|
||||
}
|
||||
|
||||
if (status) {
|
||||
query += " AND a.status = ?";
|
||||
queryParams.push(status);
|
||||
}
|
||||
|
||||
query += " ORDER BY a.work_date DESC, a.check_in_time DESC";
|
||||
|
||||
const records = await db.query<Attendance[]>(query, queryParams);
|
||||
ctx.response.body = records;
|
||||
} catch (error) {
|
||||
console.error("Get attendance error:", error);
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: "Internal server error" };
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
// Get attendance by ID
|
||||
router.get("/:id", authenticateToken, async (ctx) => {
|
||||
try {
|
||||
const attendanceId = ctx.params.id;
|
||||
|
||||
|
||||
const records = await db.query<Attendance[]>(
|
||||
`SELECT a.*,
|
||||
e.name as employee_name, e.username as employee_username,
|
||||
@@ -87,15 +104,15 @@ router.get("/:id", authenticateToken, async (ctx) => {
|
||||
LEFT JOIN departments d ON e.department_id = d.id
|
||||
LEFT JOIN users c ON e.contractor_id = c.id
|
||||
WHERE a.id = ?`,
|
||||
[attendanceId]
|
||||
[attendanceId],
|
||||
);
|
||||
|
||||
|
||||
if (records.length === 0) {
|
||||
ctx.response.status = 404;
|
||||
ctx.response.body = { error: "Attendance record not found" };
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
ctx.response.body = records[0];
|
||||
} catch (error) {
|
||||
console.error("Get attendance error:", error);
|
||||
@@ -105,245 +122,260 @@ router.get("/:id", authenticateToken, async (ctx) => {
|
||||
});
|
||||
|
||||
// Check in employee (Supervisor or SuperAdmin)
|
||||
router.post("/check-in", authenticateToken, authorize("Supervisor", "SuperAdmin"), async (ctx) => {
|
||||
try {
|
||||
const currentUser = getCurrentUser(ctx);
|
||||
const body = await ctx.request.body.json() as CheckInOutRequest;
|
||||
const { employeeId, workDate } = body;
|
||||
|
||||
if (!employeeId || !workDate) {
|
||||
ctx.response.status = 400;
|
||||
ctx.response.body = { error: "Employee ID and work date required" };
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify employee exists
|
||||
let employeeQuery = "SELECT * FROM users WHERE id = ? AND role = ?";
|
||||
const employeeParams: unknown[] = [employeeId, "Employee"];
|
||||
|
||||
if (currentUser.role === "Supervisor") {
|
||||
employeeQuery += " AND department_id = ?";
|
||||
employeeParams.push(currentUser.departmentId);
|
||||
}
|
||||
|
||||
const employees = await db.query<User[]>(employeeQuery, employeeParams);
|
||||
|
||||
if (employees.length === 0) {
|
||||
ctx.response.status = 403;
|
||||
ctx.response.body = { error: "Employee not found or not in your department" };
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if already checked in today
|
||||
const existing = await db.query<Attendance[]>(
|
||||
"SELECT * FROM attendance WHERE employee_id = ? AND work_date = ? AND status = ?",
|
||||
[employeeId, workDate, "CheckedIn"]
|
||||
);
|
||||
|
||||
if (existing.length > 0) {
|
||||
ctx.response.status = 400;
|
||||
ctx.response.body = { error: "Employee already checked in today" };
|
||||
return;
|
||||
}
|
||||
|
||||
const checkInTime = new Date().toISOString().slice(0, 19).replace("T", " ");
|
||||
|
||||
const result = await db.execute(
|
||||
"INSERT INTO attendance (employee_id, supervisor_id, check_in_time, work_date, status) VALUES (?, ?, ?, ?, ?)",
|
||||
[employeeId, currentUser.id, checkInTime, workDate, "CheckedIn"]
|
||||
);
|
||||
|
||||
const newRecord = await db.query<Attendance[]>(
|
||||
`SELECT a.*,
|
||||
e.name as employee_name, e.username as employee_username,
|
||||
s.name as supervisor_name,
|
||||
d.name as department_name,
|
||||
c.name as contractor_name
|
||||
FROM attendance a
|
||||
JOIN users e ON a.employee_id = e.id
|
||||
JOIN users s ON a.supervisor_id = s.id
|
||||
LEFT JOIN departments d ON e.department_id = d.id
|
||||
LEFT JOIN users c ON e.contractor_id = c.id
|
||||
WHERE a.id = ?`,
|
||||
[result.insertId]
|
||||
);
|
||||
|
||||
ctx.response.status = 201;
|
||||
ctx.response.body = newRecord[0];
|
||||
} catch (error) {
|
||||
console.error("Check in error:", error);
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: "Internal server error" };
|
||||
}
|
||||
});
|
||||
router.post(
|
||||
"/check-in",
|
||||
authenticateToken,
|
||||
authorize("Supervisor", "SuperAdmin"),
|
||||
async (ctx) => {
|
||||
try {
|
||||
const currentUser = getCurrentUser(ctx);
|
||||
const body = await ctx.request.body.json() as CheckInOutRequest;
|
||||
const { employeeId, workDate } = body;
|
||||
|
||||
// Check out employee (Supervisor or SuperAdmin)
|
||||
router.post("/check-out", authenticateToken, authorize("Supervisor", "SuperAdmin"), async (ctx) => {
|
||||
try {
|
||||
const currentUser = getCurrentUser(ctx);
|
||||
const body = await ctx.request.body.json() as CheckInOutRequest;
|
||||
const { employeeId, workDate } = body;
|
||||
|
||||
if (!employeeId || !workDate) {
|
||||
ctx.response.status = 400;
|
||||
ctx.response.body = { error: "Employee ID and work date required" };
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the check-in record
|
||||
let query = "SELECT * FROM attendance WHERE employee_id = ? AND work_date = ? AND status = ?";
|
||||
const params: unknown[] = [employeeId, workDate, "CheckedIn"];
|
||||
|
||||
if (currentUser.role === "Supervisor") {
|
||||
query += " AND supervisor_id = ?";
|
||||
params.push(currentUser.id);
|
||||
}
|
||||
|
||||
const records = await db.query<Attendance[]>(query, params);
|
||||
|
||||
if (records.length === 0) {
|
||||
ctx.response.status = 404;
|
||||
ctx.response.body = { error: "No check-in record found for today" };
|
||||
return;
|
||||
}
|
||||
|
||||
const checkOutTime = new Date().toISOString().slice(0, 19).replace("T", " ");
|
||||
|
||||
await db.execute(
|
||||
"UPDATE attendance SET check_out_time = ?, status = ? WHERE id = ?",
|
||||
[checkOutTime, "CheckedOut", records[0].id]
|
||||
);
|
||||
|
||||
const updatedRecord = await db.query<Attendance[]>(
|
||||
`SELECT a.*,
|
||||
e.name as employee_name, e.username as employee_username,
|
||||
s.name as supervisor_name,
|
||||
d.name as department_name,
|
||||
c.name as contractor_name
|
||||
FROM attendance a
|
||||
JOIN users e ON a.employee_id = e.id
|
||||
JOIN users s ON a.supervisor_id = s.id
|
||||
LEFT JOIN departments d ON e.department_id = d.id
|
||||
LEFT JOIN users c ON e.contractor_id = c.id
|
||||
WHERE a.id = ?`,
|
||||
[records[0].id]
|
||||
);
|
||||
|
||||
ctx.response.body = updatedRecord[0];
|
||||
} catch (error) {
|
||||
console.error("Check out error:", error);
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: "Internal server error" };
|
||||
}
|
||||
});
|
||||
if (!employeeId || !workDate) {
|
||||
ctx.response.status = 400;
|
||||
ctx.response.body = { error: "Employee ID and work date required" };
|
||||
return;
|
||||
}
|
||||
|
||||
// Update attendance status (mark as Absent, HalfDay, Late)
|
||||
router.put("/:id/status", authenticateToken, authorize("Supervisor", "SuperAdmin"), async (ctx) => {
|
||||
try {
|
||||
const attendanceId = ctx.params.id;
|
||||
const body = await ctx.request.body.json() as UpdateAttendanceStatusRequest;
|
||||
const { status, remark } = body;
|
||||
|
||||
// Validate status
|
||||
const validStatuses: AttendanceStatus[] = ["CheckedIn", "CheckedOut", "Absent", "HalfDay", "Late"];
|
||||
if (!validStatuses.includes(status)) {
|
||||
ctx.response.status = 400;
|
||||
ctx.response.body = { error: "Invalid status. Must be one of: CheckedIn, CheckedOut, Absent, HalfDay, Late" };
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if record exists
|
||||
const existing = await db.query<Attendance[]>(
|
||||
"SELECT * FROM attendance WHERE id = ?",
|
||||
[attendanceId]
|
||||
);
|
||||
|
||||
if (existing.length === 0) {
|
||||
ctx.response.status = 404;
|
||||
ctx.response.body = { error: "Attendance record not found" };
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the status
|
||||
await db.execute(
|
||||
"UPDATE attendance SET status = ?, remark = ? WHERE id = ?",
|
||||
[status, remark || null, attendanceId]
|
||||
);
|
||||
|
||||
const updatedRecord = await db.query<Attendance[]>(
|
||||
`SELECT a.*,
|
||||
e.name as employee_name, e.username as employee_username,
|
||||
s.name as supervisor_name,
|
||||
d.name as department_name,
|
||||
c.name as contractor_name
|
||||
FROM attendance a
|
||||
JOIN users e ON a.employee_id = e.id
|
||||
JOIN users s ON a.supervisor_id = s.id
|
||||
LEFT JOIN departments d ON e.department_id = d.id
|
||||
LEFT JOIN users c ON e.contractor_id = c.id
|
||||
WHERE a.id = ?`,
|
||||
[attendanceId]
|
||||
);
|
||||
|
||||
ctx.response.body = updatedRecord[0];
|
||||
} catch (error) {
|
||||
console.error("Update attendance status error:", error);
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: "Internal server error" };
|
||||
}
|
||||
});
|
||||
// Verify employee exists
|
||||
let employeeQuery = "SELECT * FROM users WHERE id = ? AND role = ?";
|
||||
const employeeParams: unknown[] = [employeeId, "Employee"];
|
||||
|
||||
// Mark employee as absent (create absent record)
|
||||
router.post("/mark-absent", authenticateToken, authorize("Supervisor", "SuperAdmin"), async (ctx) => {
|
||||
try {
|
||||
const currentUser = getCurrentUser(ctx);
|
||||
const body = await ctx.request.body.json();
|
||||
const { employeeId, workDate, remark } = body;
|
||||
|
||||
if (!employeeId || !workDate) {
|
||||
ctx.response.status = 400;
|
||||
ctx.response.body = { error: "Employee ID and work date required" };
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if record already exists for this date
|
||||
const existing = await db.query<Attendance[]>(
|
||||
"SELECT * FROM attendance WHERE employee_id = ? AND work_date = ?",
|
||||
[employeeId, workDate]
|
||||
);
|
||||
|
||||
if (existing.length > 0) {
|
||||
// Update existing record to Absent
|
||||
await db.execute(
|
||||
"UPDATE attendance SET status = ?, remark = ? WHERE id = ?",
|
||||
["Absent", remark || "Marked absent", existing[0].id]
|
||||
if (currentUser.role === "Supervisor") {
|
||||
employeeQuery += " AND department_id = ?";
|
||||
employeeParams.push(currentUser.departmentId);
|
||||
}
|
||||
|
||||
const employees = await db.query<User[]>(employeeQuery, employeeParams);
|
||||
|
||||
if (employees.length === 0) {
|
||||
ctx.response.status = 403;
|
||||
ctx.response.body = {
|
||||
error: "Employee not found or not in your department",
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if already checked in today
|
||||
const existing = await db.query<Attendance[]>(
|
||||
"SELECT * FROM attendance WHERE employee_id = ? AND work_date = ? AND status = ?",
|
||||
[employeeId, workDate, "CheckedIn"],
|
||||
);
|
||||
|
||||
const updatedRecord = await db.query<Attendance[]>(
|
||||
`SELECT a.*,
|
||||
e.name as employee_name, e.username as employee_username,
|
||||
s.name as supervisor_name,
|
||||
d.name as department_name,
|
||||
c.name as contractor_name
|
||||
FROM attendance a
|
||||
JOIN users e ON a.employee_id = e.id
|
||||
JOIN users s ON a.supervisor_id = s.id
|
||||
LEFT JOIN departments d ON e.department_id = d.id
|
||||
LEFT JOIN users c ON e.contractor_id = c.id
|
||||
WHERE a.id = ?`,
|
||||
[existing[0].id]
|
||||
|
||||
if (existing.length > 0) {
|
||||
ctx.response.status = 400;
|
||||
ctx.response.body = { error: "Employee already checked in today" };
|
||||
return;
|
||||
}
|
||||
|
||||
const checkInTime = new Date().toISOString().slice(0, 19).replace(
|
||||
"T",
|
||||
" ",
|
||||
);
|
||||
|
||||
ctx.response.body = updatedRecord[0];
|
||||
} else {
|
||||
// Create new absent record
|
||||
|
||||
const result = await db.execute(
|
||||
"INSERT INTO attendance (employee_id, supervisor_id, work_date, status, remark) VALUES (?, ?, ?, ?, ?)",
|
||||
[employeeId, currentUser.id, workDate, "Absent", remark || "Marked absent"]
|
||||
"INSERT INTO attendance (employee_id, supervisor_id, check_in_time, work_date, status) VALUES (?, ?, ?, ?, ?)",
|
||||
[employeeId, currentUser.id, checkInTime, workDate, "CheckedIn"],
|
||||
);
|
||||
|
||||
|
||||
const newRecord = await db.query<Attendance[]>(
|
||||
`SELECT a.*,
|
||||
e.name as employee_name, e.username as employee_username,
|
||||
s.name as supervisor_name,
|
||||
d.name as department_name,
|
||||
c.name as contractor_name
|
||||
FROM attendance a
|
||||
JOIN users e ON a.employee_id = e.id
|
||||
JOIN users s ON a.supervisor_id = s.id
|
||||
LEFT JOIN departments d ON e.department_id = d.id
|
||||
LEFT JOIN users c ON e.contractor_id = c.id
|
||||
WHERE a.id = ?`,
|
||||
[result.insertId],
|
||||
);
|
||||
|
||||
ctx.response.status = 201;
|
||||
ctx.response.body = newRecord[0];
|
||||
} catch (error) {
|
||||
console.error("Check in error:", error);
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: "Internal server error" };
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Check out employee (Supervisor or SuperAdmin)
|
||||
router.post(
|
||||
"/check-out",
|
||||
authenticateToken,
|
||||
authorize("Supervisor", "SuperAdmin"),
|
||||
async (ctx) => {
|
||||
try {
|
||||
const currentUser = getCurrentUser(ctx);
|
||||
const body = await ctx.request.body.json() as CheckInOutRequest;
|
||||
const { employeeId, workDate } = body;
|
||||
|
||||
if (!employeeId || !workDate) {
|
||||
ctx.response.status = 400;
|
||||
ctx.response.body = { error: "Employee ID and work date required" };
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the check-in record
|
||||
let query =
|
||||
"SELECT * FROM attendance WHERE employee_id = ? AND work_date = ? AND status = ?";
|
||||
const params: unknown[] = [employeeId, workDate, "CheckedIn"];
|
||||
|
||||
if (currentUser.role === "Supervisor") {
|
||||
query += " AND supervisor_id = ?";
|
||||
params.push(currentUser.id);
|
||||
}
|
||||
|
||||
const records = await db.query<Attendance[]>(query, params);
|
||||
|
||||
if (records.length === 0) {
|
||||
ctx.response.status = 404;
|
||||
ctx.response.body = { error: "No check-in record found for today" };
|
||||
return;
|
||||
}
|
||||
|
||||
const checkOutTime = new Date().toISOString().slice(0, 19).replace(
|
||||
"T",
|
||||
" ",
|
||||
);
|
||||
|
||||
await db.execute(
|
||||
"UPDATE attendance SET check_out_time = ?, status = ? WHERE id = ?",
|
||||
[checkOutTime, "CheckedOut", records[0].id],
|
||||
);
|
||||
|
||||
const updatedRecord = await db.query<Attendance[]>(
|
||||
`SELECT a.*,
|
||||
e.name as employee_name, e.username as employee_username,
|
||||
s.name as supervisor_name,
|
||||
d.name as department_name,
|
||||
c.name as contractor_name
|
||||
FROM attendance a
|
||||
JOIN users e ON a.employee_id = e.id
|
||||
JOIN users s ON a.supervisor_id = s.id
|
||||
LEFT JOIN departments d ON e.department_id = d.id
|
||||
LEFT JOIN users c ON e.contractor_id = c.id
|
||||
WHERE a.id = ?`,
|
||||
[records[0].id],
|
||||
);
|
||||
|
||||
ctx.response.body = updatedRecord[0];
|
||||
} catch (error) {
|
||||
console.error("Check out error:", error);
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: "Internal server error" };
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Update attendance status (mark as Absent, HalfDay, Late)
|
||||
router.put(
|
||||
"/:id/status",
|
||||
authenticateToken,
|
||||
authorize("Supervisor", "SuperAdmin"),
|
||||
async (ctx) => {
|
||||
try {
|
||||
const attendanceId = ctx.params.id;
|
||||
const body = await ctx.request.body
|
||||
.json() as UpdateAttendanceStatusRequest;
|
||||
const { status, remark } = body;
|
||||
|
||||
// Validate status
|
||||
const validStatuses: AttendanceStatus[] = [
|
||||
"CheckedIn",
|
||||
"CheckedOut",
|
||||
"Absent",
|
||||
"HalfDay",
|
||||
"Late",
|
||||
];
|
||||
if (!validStatuses.includes(status)) {
|
||||
ctx.response.status = 400;
|
||||
ctx.response.body = {
|
||||
error:
|
||||
"Invalid status. Must be one of: CheckedIn, CheckedOut, Absent, HalfDay, Late",
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if record exists
|
||||
const existing = await db.query<Attendance[]>(
|
||||
"SELECT * FROM attendance WHERE id = ?",
|
||||
[attendanceId],
|
||||
);
|
||||
|
||||
if (existing.length === 0) {
|
||||
ctx.response.status = 404;
|
||||
ctx.response.body = { error: "Attendance record not found" };
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the status
|
||||
await db.execute(
|
||||
"UPDATE attendance SET status = ?, remark = ? WHERE id = ?",
|
||||
[status, remark || null, attendanceId],
|
||||
);
|
||||
|
||||
const updatedRecord = await db.query<Attendance[]>(
|
||||
`SELECT a.*,
|
||||
e.name as employee_name, e.username as employee_username,
|
||||
s.name as supervisor_name,
|
||||
d.name as department_name,
|
||||
c.name as contractor_name
|
||||
FROM attendance a
|
||||
JOIN users e ON a.employee_id = e.id
|
||||
JOIN users s ON a.supervisor_id = s.id
|
||||
LEFT JOIN departments d ON e.department_id = d.id
|
||||
LEFT JOIN users c ON e.contractor_id = c.id
|
||||
WHERE a.id = ?`,
|
||||
[attendanceId],
|
||||
);
|
||||
|
||||
ctx.response.body = updatedRecord[0];
|
||||
} catch (error) {
|
||||
console.error("Update attendance status error:", error);
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: "Internal server error" };
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Mark employee as absent (create absent record)
|
||||
router.post(
|
||||
"/mark-absent",
|
||||
authenticateToken,
|
||||
authorize("Supervisor", "SuperAdmin"),
|
||||
async (ctx) => {
|
||||
try {
|
||||
const currentUser = getCurrentUser(ctx);
|
||||
const body = await ctx.request.body.json();
|
||||
const { employeeId, workDate, remark } = body;
|
||||
|
||||
if (!employeeId || !workDate) {
|
||||
ctx.response.status = 400;
|
||||
ctx.response.body = { error: "Employee ID and work date required" };
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if record already exists for this date
|
||||
const existing = await db.query<Attendance[]>(
|
||||
"SELECT * FROM attendance WHERE employee_id = ? AND work_date = ?",
|
||||
[employeeId, workDate],
|
||||
);
|
||||
|
||||
if (existing.length > 0) {
|
||||
// Update existing record to Absent
|
||||
await db.execute(
|
||||
"UPDATE attendance SET status = ?, remark = ? WHERE id = ?",
|
||||
["Absent", remark || "Marked absent", existing[0].id],
|
||||
);
|
||||
|
||||
const updatedRecord = await db.query<Attendance[]>(
|
||||
`SELECT a.*,
|
||||
e.name as employee_name, e.username as employee_username,
|
||||
s.name as supervisor_name,
|
||||
d.name as department_name,
|
||||
@@ -354,29 +386,68 @@ router.post("/mark-absent", authenticateToken, authorize("Supervisor", "SuperAdm
|
||||
LEFT JOIN departments d ON e.department_id = d.id
|
||||
LEFT JOIN users c ON e.contractor_id = c.id
|
||||
WHERE a.id = ?`,
|
||||
[result.insertId]
|
||||
);
|
||||
|
||||
ctx.response.status = 201;
|
||||
ctx.response.body = newRecord[0];
|
||||
[existing[0].id],
|
||||
);
|
||||
|
||||
ctx.response.body = updatedRecord[0];
|
||||
} else {
|
||||
// Create new absent record
|
||||
const result = await db.execute(
|
||||
"INSERT INTO attendance (employee_id, supervisor_id, work_date, status, remark) VALUES (?, ?, ?, ?, ?)",
|
||||
[
|
||||
employeeId,
|
||||
currentUser.id,
|
||||
workDate,
|
||||
"Absent",
|
||||
remark || "Marked absent",
|
||||
],
|
||||
);
|
||||
|
||||
const newRecord = await db.query<Attendance[]>(
|
||||
`SELECT a.*,
|
||||
e.name as employee_name, e.username as employee_username,
|
||||
s.name as supervisor_name,
|
||||
d.name as department_name,
|
||||
c.name as contractor_name
|
||||
FROM attendance a
|
||||
JOIN users e ON a.employee_id = e.id
|
||||
JOIN users s ON a.supervisor_id = s.id
|
||||
LEFT JOIN departments d ON e.department_id = d.id
|
||||
LEFT JOIN users c ON e.contractor_id = c.id
|
||||
WHERE a.id = ?`,
|
||||
[result.insertId],
|
||||
);
|
||||
|
||||
ctx.response.status = 201;
|
||||
ctx.response.body = newRecord[0];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Mark absent error:", error);
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: "Internal server error" };
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Mark absent error:", error);
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: "Internal server error" };
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
// Get attendance summary
|
||||
router.get("/summary/stats", authenticateToken, async (ctx) => {
|
||||
try {
|
||||
const currentUser = getCurrentUser(ctx);
|
||||
const params = ctx.request.url.searchParams;
|
||||
const startDate = params.get("startDate");
|
||||
const endDate = params.get("endDate");
|
||||
const departmentId = params.get("departmentId");
|
||||
|
||||
let query = `
|
||||
router.get(
|
||||
"/summary/stats",
|
||||
authenticateToken,
|
||||
async (
|
||||
ctx: RouterContext<
|
||||
"/summary/stats",
|
||||
Record<string | number, string | undefined>,
|
||||
State
|
||||
>,
|
||||
) => {
|
||||
try {
|
||||
const currentUser: JWTPayload = getCurrentUser(ctx);
|
||||
const params: URLSearchParams = ctx.request.url.searchParams;
|
||||
const startDate: string | null = params.get("startDate");
|
||||
const endDate: string | null = params.get("endDate");
|
||||
const departmentId: string | null = params.get("departmentId");
|
||||
|
||||
let query: string = `
|
||||
SELECT
|
||||
COUNT(DISTINCT a.employee_id) as total_employees,
|
||||
COUNT(DISTINCT CASE WHEN a.status = 'CheckedIn' THEN a.employee_id END) as checked_in,
|
||||
@@ -387,37 +458,38 @@ router.get("/summary/stats", authenticateToken, async (ctx) => {
|
||||
LEFT JOIN departments d ON e.department_id = d.id
|
||||
WHERE 1=1
|
||||
`;
|
||||
const queryParams: unknown[] = [];
|
||||
|
||||
if (currentUser.role === "Supervisor") {
|
||||
query += " AND a.supervisor_id = ?";
|
||||
queryParams.push(currentUser.id);
|
||||
const queryParams: (number | string)[] = [];
|
||||
|
||||
if (currentUser.role === "Supervisor") {
|
||||
query += " AND a.supervisor_id = ?";
|
||||
queryParams.push(currentUser.id);
|
||||
}
|
||||
|
||||
if (startDate) {
|
||||
query += " AND a.work_date >= ?";
|
||||
queryParams.push(startDate);
|
||||
}
|
||||
|
||||
if (endDate) {
|
||||
query += " AND a.work_date <= ?";
|
||||
queryParams.push(endDate);
|
||||
}
|
||||
|
||||
if (departmentId) {
|
||||
query += " AND e.department_id = ?";
|
||||
queryParams.push(departmentId);
|
||||
}
|
||||
|
||||
query += " GROUP BY d.id, d.name";
|
||||
|
||||
const summary = await db.query(query, queryParams);
|
||||
ctx.response.body = summary;
|
||||
} catch (error) {
|
||||
console.error("Get attendance summary error:", error);
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: "Internal server error" };
|
||||
}
|
||||
|
||||
if (startDate) {
|
||||
query += " AND a.work_date >= ?";
|
||||
queryParams.push(startDate);
|
||||
}
|
||||
|
||||
if (endDate) {
|
||||
query += " AND a.work_date <= ?";
|
||||
queryParams.push(endDate);
|
||||
}
|
||||
|
||||
if (departmentId) {
|
||||
query += " AND e.department_id = ?";
|
||||
queryParams.push(departmentId);
|
||||
}
|
||||
|
||||
query += " GROUP BY d.id, d.name";
|
||||
|
||||
const summary = await db.query(query, queryParams);
|
||||
ctx.response.body = summary;
|
||||
} catch (error) {
|
||||
console.error("Get attendance summary error:", error);
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: "Internal server error" };
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -1,16 +1,23 @@
|
||||
import { Router } from "@oak/oak";
|
||||
import { hash, compare, genSalt } from "bcrypt";
|
||||
import { compare, genSalt, hash } from "bcrypt";
|
||||
import { db } from "../config/database.ts";
|
||||
import { config } from "../config/env.ts";
|
||||
|
||||
// Helper function to hash password with proper salt generation
|
||||
async function hashPassword(password: string): Promise<string> {
|
||||
const salt = await genSalt(config.BCRYPT_ROUNDS);
|
||||
return await hash(password, salt);
|
||||
}
|
||||
import { authenticateToken, generateToken, getCurrentUser } from "../middleware/auth.ts";
|
||||
import { sanitizeInput, isValidEmail, isStrongPassword } from "../middleware/security.ts";
|
||||
import type { User, LoginRequest, ChangePasswordRequest } from "../types/index.ts";
|
||||
import {
|
||||
authenticateToken,
|
||||
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();
|
||||
|
||||
@@ -19,41 +26,41 @@ router.post("/login", async (ctx) => {
|
||||
try {
|
||||
const body = await ctx.request.body.json() as LoginRequest;
|
||||
const { username, password } = body;
|
||||
|
||||
|
||||
// Input validation
|
||||
if (!username || !password) {
|
||||
ctx.response.status = 400;
|
||||
ctx.response.body = { error: "Username and password required" };
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Sanitize input
|
||||
const sanitizedUsername = sanitizeInput(username);
|
||||
|
||||
|
||||
// Query user
|
||||
const users = await db.query<User[]>(
|
||||
"SELECT * FROM users WHERE username = ? AND is_active = TRUE",
|
||||
[sanitizedUsername]
|
||||
[sanitizedUsername],
|
||||
);
|
||||
|
||||
|
||||
if (users.length === 0) {
|
||||
// Use generic message to prevent user enumeration
|
||||
ctx.response.status = 401;
|
||||
ctx.response.body = { error: "Invalid credentials" };
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const user = users[0];
|
||||
|
||||
|
||||
// Verify password
|
||||
const validPassword = await compare(password, user.password!);
|
||||
|
||||
|
||||
if (!validPassword) {
|
||||
ctx.response.status = 401;
|
||||
ctx.response.body = { error: "Invalid credentials" };
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Generate JWT token
|
||||
const token = await generateToken({
|
||||
id: user.id,
|
||||
@@ -61,10 +68,10 @@ router.post("/login", async (ctx) => {
|
||||
role: user.role,
|
||||
departmentId: user.department_id,
|
||||
});
|
||||
|
||||
|
||||
// Return user data without password
|
||||
const { password: _, ...userWithoutPassword } = user;
|
||||
|
||||
|
||||
ctx.response.body = {
|
||||
token,
|
||||
user: userWithoutPassword,
|
||||
@@ -80,18 +87,18 @@ router.post("/login", async (ctx) => {
|
||||
router.get("/me", authenticateToken, async (ctx) => {
|
||||
try {
|
||||
const currentUser = getCurrentUser(ctx);
|
||||
|
||||
|
||||
const users = await db.query<User[]>(
|
||||
"SELECT id, username, name, email, role, department_id, contractor_id, is_active FROM users WHERE id = ?",
|
||||
[currentUser.id]
|
||||
[currentUser.id],
|
||||
);
|
||||
|
||||
|
||||
if (users.length === 0) {
|
||||
ctx.response.status = 404;
|
||||
ctx.response.body = { error: "User not found" };
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
ctx.response.body = users[0];
|
||||
} catch (error) {
|
||||
console.error("Get user error:", error);
|
||||
@@ -106,14 +113,14 @@ router.post("/change-password", authenticateToken, async (ctx) => {
|
||||
const currentUser = getCurrentUser(ctx);
|
||||
const body = await ctx.request.body.json() as ChangePasswordRequest;
|
||||
const { currentPassword, newPassword } = body;
|
||||
|
||||
|
||||
// Input validation
|
||||
if (!currentPassword || !newPassword) {
|
||||
ctx.response.status = 400;
|
||||
ctx.response.body = { error: "Current and new password required" };
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Validate new password strength (only enforce in production or if explicitly enabled)
|
||||
if (config.isProduction()) {
|
||||
const passwordCheck = isStrongPassword(newPassword);
|
||||
@@ -127,37 +134,37 @@ router.post("/change-password", authenticateToken, async (ctx) => {
|
||||
ctx.response.body = { error: "Password must be at least 6 characters" };
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Get current password hash
|
||||
const users = await db.query<User[]>(
|
||||
"SELECT password FROM users WHERE id = ?",
|
||||
[currentUser.id]
|
||||
[currentUser.id],
|
||||
);
|
||||
|
||||
|
||||
if (users.length === 0) {
|
||||
ctx.response.status = 404;
|
||||
ctx.response.body = { error: "User not found" };
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Verify current password
|
||||
const validPassword = await compare(currentPassword, users[0].password!);
|
||||
|
||||
|
||||
if (!validPassword) {
|
||||
ctx.response.status = 401;
|
||||
ctx.response.body = { error: "Current password is incorrect" };
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Hash new password with configured rounds
|
||||
const hashedPassword = await hashPassword(newPassword);
|
||||
|
||||
|
||||
// Update password
|
||||
await db.execute(
|
||||
"UPDATE users SET password = ? WHERE id = ?",
|
||||
[hashedPassword, currentUser.id]
|
||||
[hashedPassword, currentUser.id],
|
||||
);
|
||||
|
||||
|
||||
ctx.response.body = { message: "Password changed successfully" };
|
||||
} catch (error) {
|
||||
console.error("Change password error:", error);
|
||||
|
||||
@@ -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 { authenticateToken, authorize, getCurrentUser } from "../middleware/auth.ts";
|
||||
import {
|
||||
authenticateToken,
|
||||
authorize,
|
||||
getCurrentUser,
|
||||
} from "../middleware/auth.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();
|
||||
|
||||
// Get contractor rates
|
||||
router.get("/", authenticateToken, async (ctx) => {
|
||||
try {
|
||||
const params = ctx.request.url.searchParams;
|
||||
const contractorId = params.get("contractorId");
|
||||
const subDepartmentId = params.get("subDepartmentId");
|
||||
|
||||
let query = `
|
||||
router.get(
|
||||
"/",
|
||||
authenticateToken,
|
||||
async (
|
||||
ctx: RouterContext<"/", Record<string | number, string | undefined>, State>,
|
||||
) => {
|
||||
try {
|
||||
const params: URLSearchParams = ctx.request.url.searchParams;
|
||||
const contractorId: string | null = params.get("contractorId");
|
||||
const subDepartmentId: string | null = params.get("subDepartmentId");
|
||||
|
||||
let query: string = `
|
||||
SELECT cr.*,
|
||||
u.name as contractor_name, u.username as contractor_username,
|
||||
sd.name as sub_department_name,
|
||||
@@ -26,37 +39,47 @@ router.get("/", authenticateToken, async (ctx) => {
|
||||
LEFT JOIN activities a ON a.sub_department_id = cr.sub_department_id AND a.name = cr.activity
|
||||
WHERE 1=1
|
||||
`;
|
||||
const queryParams: unknown[] = [];
|
||||
|
||||
if (contractorId) {
|
||||
query += " AND cr.contractor_id = ?";
|
||||
queryParams.push(contractorId);
|
||||
const queryParams: unknown[] = [];
|
||||
|
||||
if (contractorId) {
|
||||
query += " AND cr.contractor_id = ?";
|
||||
queryParams.push(contractorId);
|
||||
}
|
||||
|
||||
if (subDepartmentId) {
|
||||
query += " AND cr.sub_department_id = ?";
|
||||
queryParams.push(subDepartmentId);
|
||||
}
|
||||
|
||||
query += " ORDER BY cr.effective_date DESC, cr.created_at DESC";
|
||||
|
||||
const rates = await db.query<ContractorRate[]>(query, queryParams);
|
||||
ctx.response.body = rates;
|
||||
} catch (error) {
|
||||
console.error("Get contractor rates error:", error);
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: "Internal server error" };
|
||||
}
|
||||
|
||||
if (subDepartmentId) {
|
||||
query += " AND cr.sub_department_id = ?";
|
||||
queryParams.push(subDepartmentId);
|
||||
}
|
||||
|
||||
query += " ORDER BY cr.effective_date DESC, cr.created_at DESC";
|
||||
|
||||
const rates = await db.query<ContractorRate[]>(query, queryParams);
|
||||
ctx.response.body = rates;
|
||||
} catch (error) {
|
||||
console.error("Get contractor rates error:", error);
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: "Internal server error" };
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
// Get current rate for a contractor + sub-department combination
|
||||
router.get("/contractor/:contractorId/current", authenticateToken, async (ctx) => {
|
||||
try {
|
||||
const contractorId = ctx.params.contractorId;
|
||||
const params = ctx.request.url.searchParams;
|
||||
const subDepartmentId = params.get("subDepartmentId");
|
||||
|
||||
let query = `
|
||||
router.get(
|
||||
"/contractor/:contractorId/current",
|
||||
authenticateToken,
|
||||
async (
|
||||
ctx: RouterContext<
|
||||
"/contractor/:contractorId/current",
|
||||
{ contractorId: string } & Record<string | number, string | undefined>,
|
||||
State
|
||||
>,
|
||||
) => {
|
||||
try {
|
||||
const contractorId = ctx.params.contractorId;
|
||||
const params = ctx.request.url.searchParams;
|
||||
const subDepartmentId = params.get("subDepartmentId");
|
||||
|
||||
let query: string = `
|
||||
SELECT cr.*,
|
||||
u.name as contractor_name, u.username as contractor_username,
|
||||
sd.name as sub_department_name,
|
||||
@@ -67,72 +90,92 @@ router.get("/contractor/:contractorId/current", authenticateToken, async (ctx) =
|
||||
LEFT JOIN activities a ON a.sub_department_id = cr.sub_department_id AND a.name = cr.activity
|
||||
WHERE cr.contractor_id = ?
|
||||
`;
|
||||
const queryParams: unknown[] = [contractorId];
|
||||
|
||||
if (subDepartmentId) {
|
||||
query += " AND cr.sub_department_id = ?";
|
||||
queryParams.push(subDepartmentId);
|
||||
const queryParams: unknown[] = [contractorId];
|
||||
|
||||
if (subDepartmentId) {
|
||||
query += " AND cr.sub_department_id = ?";
|
||||
queryParams.push(subDepartmentId);
|
||||
}
|
||||
|
||||
query += " ORDER BY cr.effective_date DESC LIMIT 1";
|
||||
|
||||
const rates = await db.query<ContractorRate[]>(query, queryParams);
|
||||
|
||||
if (rates.length === 0) {
|
||||
ctx.response.status = 404;
|
||||
ctx.response.body = { error: "No rate found for contractor" };
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.response.body = rates[0];
|
||||
} catch (error) {
|
||||
console.error("Get current rate error:", error);
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: "Internal server error" };
|
||||
}
|
||||
|
||||
query += " ORDER BY cr.effective_date DESC LIMIT 1";
|
||||
|
||||
const rates = await db.query<ContractorRate[]>(query, queryParams);
|
||||
|
||||
if (rates.length === 0) {
|
||||
ctx.response.status = 404;
|
||||
ctx.response.body = { error: "No rate found for contractor" };
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.response.body = rates[0];
|
||||
} catch (error) {
|
||||
console.error("Get current rate error:", error);
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: "Internal server error" };
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
// Set contractor rate (Supervisor or SuperAdmin)
|
||||
router.post("/", authenticateToken, authorize("Supervisor", "SuperAdmin"), async (ctx) => {
|
||||
try {
|
||||
const currentUser = getCurrentUser(ctx);
|
||||
const body = await ctx.request.body.json() as CreateContractorRateRequest;
|
||||
const { contractorId, subDepartmentId, activity, rate, effectiveDate } = body;
|
||||
|
||||
if (!contractorId || !rate || !effectiveDate) {
|
||||
ctx.response.status = 400;
|
||||
ctx.response.body = { error: "Missing required fields (contractorId, rate, effectiveDate)" };
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify contractor exists
|
||||
const contractors = await db.query<User[]>(
|
||||
"SELECT * FROM users WHERE id = ? AND role = ?",
|
||||
[contractorId, "Contractor"]
|
||||
);
|
||||
|
||||
if (contractors.length === 0) {
|
||||
ctx.response.status = 404;
|
||||
ctx.response.body = { error: "Contractor not found" };
|
||||
return;
|
||||
}
|
||||
|
||||
// Supervisors can only set rates for contractors in their department
|
||||
if (currentUser.role === "Supervisor" && contractors[0].department_id !== currentUser.departmentId) {
|
||||
ctx.response.status = 403;
|
||||
ctx.response.body = { error: "Contractor not in your department" };
|
||||
return;
|
||||
}
|
||||
|
||||
const sanitizedActivity = activity ? sanitizeInput(activity) : null;
|
||||
|
||||
const result = await db.execute(
|
||||
"INSERT INTO contractor_rates (contractor_id, sub_department_id, activity, rate, effective_date) VALUES (?, ?, ?, ?, ?)",
|
||||
[contractorId, subDepartmentId || null, sanitizedActivity, rate, effectiveDate]
|
||||
);
|
||||
|
||||
const newRate = await db.query<ContractorRate[]>(
|
||||
`SELECT cr.*,
|
||||
router.post(
|
||||
"/",
|
||||
authenticateToken,
|
||||
authorize("Supervisor", "SuperAdmin"),
|
||||
async (
|
||||
ctx: RouterContext<"/", Record<string | number, string | undefined>, State>,
|
||||
) => {
|
||||
try {
|
||||
const currentUser = getCurrentUser(ctx);
|
||||
const body = await ctx.request.body.json() as CreateContractorRateRequest;
|
||||
const { contractorId, subDepartmentId, activity, rate, effectiveDate } =
|
||||
body;
|
||||
|
||||
if (!contractorId || !rate || !effectiveDate) {
|
||||
ctx.response.status = 400;
|
||||
ctx.response.body = {
|
||||
error: "Missing required fields (contractorId, rate, effectiveDate)",
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify contractor exists
|
||||
const contractors = await db.query<User[]>(
|
||||
"SELECT * FROM users WHERE id = ? AND role = ?",
|
||||
[contractorId, "Contractor"],
|
||||
);
|
||||
|
||||
if (contractors.length === 0) {
|
||||
ctx.response.status = 404;
|
||||
ctx.response.body = { error: "Contractor not found" };
|
||||
return;
|
||||
}
|
||||
|
||||
// Supervisors can only set rates for contractors in their department
|
||||
if (
|
||||
currentUser.role === "Supervisor" &&
|
||||
contractors[0].department_id !== currentUser.departmentId
|
||||
) {
|
||||
ctx.response.status = 403;
|
||||
ctx.response.body = { error: "Contractor not in your department" };
|
||||
return;
|
||||
}
|
||||
|
||||
const sanitizedActivity = activity ? sanitizeInput(activity) : null;
|
||||
|
||||
const result: { insertId: number; affectedRows: number } = await db
|
||||
.execute(
|
||||
"INSERT INTO contractor_rates (contractor_id, sub_department_id, activity, rate, effective_date) VALUES (?, ?, ?, ?, ?)",
|
||||
[
|
||||
contractorId,
|
||||
subDepartmentId || null,
|
||||
sanitizedActivity,
|
||||
rate,
|
||||
effectiveDate,
|
||||
],
|
||||
);
|
||||
|
||||
const newRate: ContractorRate[] = await db.query<ContractorRate[]>(
|
||||
`SELECT cr.*,
|
||||
u.name as contractor_name, u.username as contractor_username,
|
||||
sd.name as sub_department_name,
|
||||
a.unit_of_measurement
|
||||
@@ -141,67 +184,82 @@ router.post("/", authenticateToken, authorize("Supervisor", "SuperAdmin"), async
|
||||
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
|
||||
WHERE cr.id = ?`,
|
||||
[result.insertId]
|
||||
);
|
||||
|
||||
ctx.response.status = 201;
|
||||
ctx.response.body = newRate[0];
|
||||
} catch (error) {
|
||||
console.error("Set contractor rate error:", error);
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: "Internal server error" };
|
||||
}
|
||||
});
|
||||
[result.insertId],
|
||||
);
|
||||
|
||||
ctx.response.status = 201;
|
||||
ctx.response.body = newRate[0];
|
||||
} catch (error) {
|
||||
console.error("Set contractor rate error:", error);
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: "Internal server error" };
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Update contractor rate
|
||||
router.put("/:id", authenticateToken, authorize("Supervisor", "SuperAdmin"), async (ctx) => {
|
||||
try {
|
||||
const rateId = ctx.params.id;
|
||||
const body = await ctx.request.body.json() as { rate?: number; activity?: string; effectiveDate?: string };
|
||||
const { rate, activity, effectiveDate } = body;
|
||||
|
||||
const existing = await db.query<ContractorRate[]>(
|
||||
"SELECT * FROM contractor_rates WHERE id = ?",
|
||||
[rateId]
|
||||
);
|
||||
|
||||
if (existing.length === 0) {
|
||||
ctx.response.status = 404;
|
||||
ctx.response.body = { error: "Rate not found" };
|
||||
return;
|
||||
}
|
||||
|
||||
const updates: string[] = [];
|
||||
const params: unknown[] = [];
|
||||
|
||||
if (rate !== undefined) {
|
||||
updates.push("rate = ?");
|
||||
params.push(rate);
|
||||
}
|
||||
if (activity !== undefined) {
|
||||
updates.push("activity = ?");
|
||||
params.push(sanitizeInput(activity));
|
||||
}
|
||||
if (effectiveDate !== undefined) {
|
||||
updates.push("effective_date = ?");
|
||||
params.push(effectiveDate);
|
||||
}
|
||||
|
||||
if (updates.length === 0) {
|
||||
ctx.response.status = 400;
|
||||
ctx.response.body = { error: "No fields to update" };
|
||||
return;
|
||||
}
|
||||
|
||||
params.push(rateId);
|
||||
|
||||
await db.execute(
|
||||
`UPDATE contractor_rates SET ${updates.join(", ")} WHERE id = ?`,
|
||||
params
|
||||
);
|
||||
|
||||
const updatedRate = await db.query<ContractorRate[]>(
|
||||
`SELECT cr.*,
|
||||
router.put(
|
||||
"/:id",
|
||||
authenticateToken,
|
||||
authorize("Supervisor", "SuperAdmin"),
|
||||
async (
|
||||
ctx: RouterContext<
|
||||
"/:id",
|
||||
{ id: string } & Record<string | number, string | undefined>,
|
||||
State
|
||||
>,
|
||||
) => {
|
||||
try {
|
||||
const rateId = ctx.params.id;
|
||||
const body = await ctx.request.body.json() as {
|
||||
rate?: number;
|
||||
activity?: string;
|
||||
effectiveDate?: string;
|
||||
};
|
||||
const { rate, activity, effectiveDate } = body;
|
||||
|
||||
const existing = await db.query<ContractorRate[]>(
|
||||
"SELECT * FROM contractor_rates WHERE id = ?",
|
||||
[rateId],
|
||||
);
|
||||
|
||||
if (existing.length === 0) {
|
||||
ctx.response.status = 404;
|
||||
ctx.response.body = { error: "Rate not found" };
|
||||
return;
|
||||
}
|
||||
|
||||
const updates: string[] = [];
|
||||
const params: unknown[] = [];
|
||||
|
||||
if (rate !== undefined) {
|
||||
updates.push("rate = ?");
|
||||
params.push(rate);
|
||||
}
|
||||
if (activity !== undefined) {
|
||||
updates.push("activity = ?");
|
||||
params.push(sanitizeInput(activity));
|
||||
}
|
||||
if (effectiveDate !== undefined) {
|
||||
updates.push("effective_date = ?");
|
||||
params.push(effectiveDate);
|
||||
}
|
||||
|
||||
if (updates.length === 0) {
|
||||
ctx.response.status = 400;
|
||||
ctx.response.body = { error: "No fields to update" };
|
||||
return;
|
||||
}
|
||||
|
||||
params.push(rateId);
|
||||
|
||||
await db.execute(
|
||||
`UPDATE contractor_rates SET ${updates.join(", ")} WHERE id = ?`,
|
||||
params,
|
||||
);
|
||||
|
||||
const updatedRate = await db.query<ContractorRate[]>(
|
||||
`SELECT cr.*,
|
||||
u.name as contractor_name, u.username as contractor_username,
|
||||
sd.name as sub_department_name,
|
||||
a.unit_of_measurement
|
||||
@@ -210,40 +268,52 @@ router.put("/:id", authenticateToken, authorize("Supervisor", "SuperAdmin"), asy
|
||||
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
|
||||
WHERE cr.id = ?`,
|
||||
[rateId]
|
||||
);
|
||||
|
||||
ctx.response.body = updatedRate[0];
|
||||
} catch (error) {
|
||||
console.error("Update contractor rate error:", error);
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: "Internal server error" };
|
||||
}
|
||||
});
|
||||
[rateId],
|
||||
);
|
||||
|
||||
ctx.response.body = updatedRate[0];
|
||||
} catch (error) {
|
||||
console.error("Update contractor rate error:", error);
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: "Internal server error" };
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Delete contractor rate
|
||||
router.delete("/:id", authenticateToken, authorize("Supervisor", "SuperAdmin"), async (ctx) => {
|
||||
try {
|
||||
const rateId = ctx.params.id;
|
||||
|
||||
const existing = await db.query<ContractorRate[]>(
|
||||
"SELECT * FROM contractor_rates WHERE id = ?",
|
||||
[rateId]
|
||||
);
|
||||
|
||||
if (existing.length === 0) {
|
||||
ctx.response.status = 404;
|
||||
ctx.response.body = { error: "Rate not found" };
|
||||
return;
|
||||
router.delete(
|
||||
"/:id",
|
||||
authenticateToken,
|
||||
authorize("Supervisor", "SuperAdmin"),
|
||||
async (
|
||||
ctx: RouterContext<
|
||||
"/:id",
|
||||
{ id: string } & Record<string | number, string | undefined>,
|
||||
State
|
||||
>,
|
||||
) => {
|
||||
try {
|
||||
const rateId = ctx.params.id;
|
||||
|
||||
const existing = await db.query<ContractorRate[]>(
|
||||
"SELECT * FROM contractor_rates WHERE id = ?",
|
||||
[rateId],
|
||||
);
|
||||
|
||||
if (existing.length === 0) {
|
||||
ctx.response.status = 404;
|
||||
ctx.response.body = { error: "Rate not found" };
|
||||
return;
|
||||
}
|
||||
|
||||
await db.execute("DELETE FROM contractor_rates WHERE id = ?", [rateId]);
|
||||
ctx.response.body = { message: "Rate deleted successfully" };
|
||||
} catch (error) {
|
||||
console.error("Delete contractor rate error:", error);
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: "Internal server error" };
|
||||
}
|
||||
|
||||
await db.execute("DELETE FROM contractor_rates WHERE id = ?", [rateId]);
|
||||
ctx.response.body = { message: "Rate deleted successfully" };
|
||||
} catch (error) {
|
||||
console.error("Delete contractor rate error:", error);
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: "Internal server error" };
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -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 { authenticateToken, authorize, getCurrentUser } from "../middleware/auth.ts";
|
||||
import {
|
||||
authenticateToken,
|
||||
authorize,
|
||||
getCurrentUser,
|
||||
} from "../middleware/auth.ts";
|
||||
import { sanitizeInput } from "../middleware/security.ts";
|
||||
import type { Department, SubDepartment } from "../types/index.ts";
|
||||
|
||||
const router = new Router();
|
||||
|
||||
// Get all departments
|
||||
router.get("/", authenticateToken, async (ctx) => {
|
||||
router.get("/", authenticateToken, async (ctx: Context) => {
|
||||
try {
|
||||
const departments = await db.query<Department[]>(
|
||||
"SELECT * FROM departments ORDER BY name"
|
||||
"SELECT * FROM departments ORDER BY name",
|
||||
);
|
||||
ctx.response.body = departments;
|
||||
} catch (error) {
|
||||
@@ -21,21 +25,21 @@ router.get("/", authenticateToken, async (ctx) => {
|
||||
});
|
||||
|
||||
// Get department by ID
|
||||
router.get("/:id", authenticateToken, async (ctx) => {
|
||||
router.get("/:id", authenticateToken, async (ctx: RouterContext<"/:id">) => {
|
||||
try {
|
||||
const deptId = ctx.params.id;
|
||||
|
||||
|
||||
const departments = await db.query<Department[]>(
|
||||
"SELECT * FROM departments WHERE id = ?",
|
||||
[deptId]
|
||||
[deptId],
|
||||
);
|
||||
|
||||
|
||||
if (departments.length === 0) {
|
||||
ctx.response.status = 404;
|
||||
ctx.response.body = { error: "Department not found" };
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
ctx.response.body = departments[0];
|
||||
} catch (error) {
|
||||
console.error("Get department error:", error);
|
||||
@@ -44,70 +48,101 @@ 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
|
||||
router.get("/:id/sub-departments", authenticateToken, async (ctx) => {
|
||||
try {
|
||||
const deptId = ctx.params.id;
|
||||
|
||||
const subDepartments = await db.query<SubDepartment[]>(
|
||||
"SELECT * FROM sub_departments WHERE department_id = ? ORDER BY name",
|
||||
[deptId]
|
||||
);
|
||||
|
||||
ctx.response.body = subDepartments;
|
||||
} catch (error) {
|
||||
console.error("Get sub-departments error:", error);
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: "Internal server error" };
|
||||
}
|
||||
});
|
||||
router.get(
|
||||
"/:id/sub-departments",
|
||||
authenticateToken,
|
||||
async (ctx: RouterContext<"/:id/sub-departments">) => {
|
||||
try {
|
||||
const deptId = ctx.params.id;
|
||||
|
||||
const subDepartments = await db.query<SubDepartment[]>(
|
||||
"SELECT * FROM sub_departments WHERE department_id = ? ORDER BY name",
|
||||
[deptId],
|
||||
);
|
||||
|
||||
ctx.response.body = subDepartments;
|
||||
} catch (error) {
|
||||
console.error("Get sub-departments error:", error);
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: "Internal server error" };
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Create department (SuperAdmin only)
|
||||
router.post("/", authenticateToken, authorize("SuperAdmin"), async (ctx) => {
|
||||
try {
|
||||
const body = await ctx.request.body.json() as { name: string };
|
||||
const { name } = body;
|
||||
|
||||
if (!name) {
|
||||
ctx.response.status = 400;
|
||||
ctx.response.body = { error: "Department name required" };
|
||||
return;
|
||||
router.post(
|
||||
"/",
|
||||
authenticateToken,
|
||||
authorize("SuperAdmin"),
|
||||
async (ctx: Context) => {
|
||||
try {
|
||||
const body = await ctx.request.body.json() as { name: string };
|
||||
const { name } = body;
|
||||
|
||||
if (!name) {
|
||||
ctx.response.status = 400;
|
||||
ctx.response.body = { error: "Department name required" };
|
||||
return;
|
||||
}
|
||||
|
||||
const sanitizedName = sanitizeInput(name);
|
||||
|
||||
const result = await db.execute(
|
||||
"INSERT INTO departments (name) VALUES (?)",
|
||||
[sanitizedName],
|
||||
);
|
||||
|
||||
const newDepartment = await db.query<Department[]>(
|
||||
"SELECT * FROM departments WHERE id = ?",
|
||||
[result.insertId],
|
||||
);
|
||||
|
||||
ctx.response.status = 201;
|
||||
ctx.response.body = newDepartment[0];
|
||||
} catch (error) {
|
||||
const err = error as { code?: string };
|
||||
if (err.code === "ER_DUP_ENTRY") {
|
||||
ctx.response.status = 400;
|
||||
ctx.response.body = { error: "Department already exists" };
|
||||
return;
|
||||
}
|
||||
console.error("Create department error:", error);
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: "Internal server error" };
|
||||
}
|
||||
|
||||
const sanitizedName = sanitizeInput(name);
|
||||
|
||||
const result = await db.execute(
|
||||
"INSERT INTO departments (name) VALUES (?)",
|
||||
[sanitizedName]
|
||||
);
|
||||
|
||||
const newDepartment = await db.query<Department[]>(
|
||||
"SELECT * FROM departments WHERE id = ?",
|
||||
[result.insertId]
|
||||
);
|
||||
|
||||
ctx.response.status = 201;
|
||||
ctx.response.body = newDepartment[0];
|
||||
} catch (error) {
|
||||
const err = error as { code?: string };
|
||||
if (err.code === "ER_DUP_ENTRY") {
|
||||
ctx.response.status = 400;
|
||||
ctx.response.body = { error: "Department already exists" };
|
||||
return;
|
||||
}
|
||||
console.error("Create department error:", error);
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: "Internal server error" };
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
// 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 {
|
||||
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;
|
||||
|
||||
|
||||
if (!name || !department_id) {
|
||||
ctx.response.status = 400;
|
||||
ctx.response.body = { error: "Department ID and name are required" };
|
||||
@@ -115,37 +150,41 @@ router.post("/sub-departments", authenticateToken, async (ctx) => {
|
||||
}
|
||||
|
||||
// Check authorization
|
||||
if (user.role === 'Supervisor' && user.departmentId !== department_id) {
|
||||
if (user.role === "Supervisor" && user.departmentId !== department_id) {
|
||||
ctx.response.status = 403;
|
||||
ctx.response.body = { error: "You can only create sub-departments for your own department" };
|
||||
ctx.response.body = {
|
||||
error: "You can only create sub-departments for your own department",
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
if (user.role !== 'SuperAdmin' && user.role !== 'Supervisor') {
|
||||
|
||||
if (user.role !== "SuperAdmin" && user.role !== "Supervisor") {
|
||||
ctx.response.status = 403;
|
||||
ctx.response.body = { error: "Unauthorized" };
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const sanitizedName = sanitizeInput(name);
|
||||
|
||||
|
||||
const result = await db.execute(
|
||||
"INSERT INTO sub_departments (department_id, name) VALUES (?, ?)",
|
||||
[department_id, sanitizedName]
|
||||
[department_id, sanitizedName],
|
||||
);
|
||||
|
||||
|
||||
const newSubDepartment = await db.query<SubDepartment[]>(
|
||||
"SELECT * FROM sub_departments WHERE id = ?",
|
||||
[result.lastInsertId]
|
||||
[result.insertId],
|
||||
);
|
||||
|
||||
|
||||
ctx.response.status = 201;
|
||||
ctx.response.body = newSubDepartment[0];
|
||||
} catch (error) {
|
||||
const err = error as { code?: string };
|
||||
if (err.code === "ER_DUP_ENTRY") {
|
||||
ctx.response.status = 400;
|
||||
ctx.response.body = { error: "Sub-department already exists in this department" };
|
||||
ctx.response.body = {
|
||||
error: "Sub-department already exists in this department",
|
||||
};
|
||||
return;
|
||||
}
|
||||
console.error("Create sub-department error:", error);
|
||||
@@ -155,90 +194,108 @@ router.post("/sub-departments", authenticateToken, async (ctx) => {
|
||||
});
|
||||
|
||||
// Delete sub-department (SuperAdmin or Supervisor for their own department)
|
||||
router.delete("/sub-departments/:id", authenticateToken, async (ctx) => {
|
||||
try {
|
||||
const user = getCurrentUser(ctx);
|
||||
const subDeptId = ctx.params.id;
|
||||
|
||||
// Get the sub-department to check department ownership
|
||||
const subDepts = await db.query<SubDepartment[]>(
|
||||
"SELECT * FROM sub_departments WHERE id = ?",
|
||||
[subDeptId]
|
||||
);
|
||||
|
||||
if (subDepts.length === 0) {
|
||||
ctx.response.status = 404;
|
||||
ctx.response.body = { error: "Sub-department not found" };
|
||||
return;
|
||||
router.delete(
|
||||
"/sub-departments/:id",
|
||||
authenticateToken,
|
||||
async (ctx: RouterContext<"/sub-departments/:id">) => {
|
||||
try {
|
||||
const user = getCurrentUser(ctx);
|
||||
const subDeptId = ctx.params.id;
|
||||
|
||||
// Get the sub-department to check department ownership
|
||||
const subDepts = await db.query<SubDepartment[]>(
|
||||
"SELECT * FROM sub_departments WHERE id = ?",
|
||||
[subDeptId],
|
||||
);
|
||||
|
||||
if (subDepts.length === 0) {
|
||||
ctx.response.status = 404;
|
||||
ctx.response.body = { error: "Sub-department not found" };
|
||||
return;
|
||||
}
|
||||
|
||||
const subDept = subDepts[0];
|
||||
|
||||
// Check authorization
|
||||
if (
|
||||
user.role === "Supervisor" &&
|
||||
user.departmentId !== subDept.department_id
|
||||
) {
|
||||
ctx.response.status = 403;
|
||||
ctx.response.body = {
|
||||
error: "You can only delete sub-departments from your own department",
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
if (user.role !== "SuperAdmin" && user.role !== "Supervisor") {
|
||||
ctx.response.status = 403;
|
||||
ctx.response.body = { error: "Unauthorized" };
|
||||
return;
|
||||
}
|
||||
|
||||
// Delete associated activities first (cascade should handle this, but being explicit)
|
||||
await db.execute("DELETE FROM activities WHERE sub_department_id = ?", [
|
||||
subDeptId,
|
||||
]);
|
||||
|
||||
// Delete the sub-department
|
||||
await db.execute("DELETE FROM sub_departments WHERE id = ?", [subDeptId]);
|
||||
|
||||
ctx.response.body = { message: "Sub-department deleted successfully" };
|
||||
} catch (error) {
|
||||
console.error("Delete sub-department error:", error);
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: "Internal server error" };
|
||||
}
|
||||
|
||||
const subDept = subDepts[0];
|
||||
|
||||
// Check authorization
|
||||
if (user.role === 'Supervisor' && user.departmentId !== subDept.department_id) {
|
||||
ctx.response.status = 403;
|
||||
ctx.response.body = { error: "You can only delete sub-departments from your own department" };
|
||||
return;
|
||||
}
|
||||
|
||||
if (user.role !== 'SuperAdmin' && user.role !== 'Supervisor') {
|
||||
ctx.response.status = 403;
|
||||
ctx.response.body = { error: "Unauthorized" };
|
||||
return;
|
||||
}
|
||||
|
||||
// Delete associated activities first (cascade should handle this, but being explicit)
|
||||
await db.execute("DELETE FROM activities WHERE sub_department_id = ?", [subDeptId]);
|
||||
|
||||
// Delete the sub-department
|
||||
await db.execute("DELETE FROM sub_departments WHERE id = ?", [subDeptId]);
|
||||
|
||||
ctx.response.body = { message: "Sub-department deleted successfully" };
|
||||
} catch (error) {
|
||||
console.error("Delete sub-department error:", error);
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: "Internal server error" };
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
// Legacy route for creating sub-department under specific department (SuperAdmin only)
|
||||
router.post("/:id/sub-departments", authenticateToken, authorize("SuperAdmin"), async (ctx) => {
|
||||
try {
|
||||
const deptId = ctx.params.id;
|
||||
const body = await ctx.request.body.json() as { name: string };
|
||||
const { name } = body;
|
||||
|
||||
if (!name) {
|
||||
ctx.response.status = 400;
|
||||
ctx.response.body = { error: "Name is required" };
|
||||
return;
|
||||
router.post(
|
||||
"/:id/sub-departments",
|
||||
authenticateToken,
|
||||
authorize("SuperAdmin"),
|
||||
async (ctx: RouterContext<"/:id/sub-departments">) => {
|
||||
try {
|
||||
const deptId: string | number = ctx.params.id;
|
||||
const body = await ctx.request.body.json() as { name: string };
|
||||
const { name } = body;
|
||||
|
||||
if (!name) {
|
||||
ctx.response.status = 400;
|
||||
ctx.response.body = { error: "Name is required" };
|
||||
return;
|
||||
}
|
||||
|
||||
const sanitizedName = sanitizeInput(name);
|
||||
|
||||
const result = await db.execute(
|
||||
"INSERT INTO sub_departments (department_id, name) VALUES (?, ?)",
|
||||
[deptId, sanitizedName],
|
||||
);
|
||||
|
||||
const newSubDepartment = await db.query<SubDepartment[]>(
|
||||
"SELECT * FROM sub_departments WHERE id = ?",
|
||||
[result.insertId],
|
||||
);
|
||||
|
||||
ctx.response.status = 201;
|
||||
ctx.response.body = newSubDepartment[0];
|
||||
} catch (error) {
|
||||
const err = error as { code?: string };
|
||||
if (err.code === "ER_DUP_ENTRY") {
|
||||
ctx.response.status = 400;
|
||||
ctx.response.body = {
|
||||
error: "Sub-department already exists in this department",
|
||||
};
|
||||
return;
|
||||
}
|
||||
console.error("Create sub-department error:", error);
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: "Internal server error" };
|
||||
}
|
||||
|
||||
const sanitizedName = sanitizeInput(name);
|
||||
|
||||
const result = await db.execute(
|
||||
"INSERT INTO sub_departments (department_id, name) VALUES (?, ?)",
|
||||
[deptId, sanitizedName]
|
||||
);
|
||||
|
||||
const newSubDepartment = await db.query<SubDepartment[]>(
|
||||
"SELECT * FROM sub_departments WHERE id = ?",
|
||||
[result.lastInsertId]
|
||||
);
|
||||
|
||||
ctx.response.status = 201;
|
||||
ctx.response.body = newSubDepartment[0];
|
||||
} catch (error) {
|
||||
const err = error as { code?: string };
|
||||
if (err.code === "ER_DUP_ENTRY") {
|
||||
ctx.response.status = 400;
|
||||
ctx.response.body = { error: "Sub-department already exists in this department" };
|
||||
return;
|
||||
}
|
||||
console.error("Create sub-department error:", error);
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: "Internal server error" };
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -1,20 +1,30 @@
|
||||
import { Router } from "@oak/oak";
|
||||
import { Router, type RouterContext, type State } from "@oak/oak";
|
||||
import { db } from "../config/database.ts";
|
||||
import { authenticateToken, authorize, getCurrentUser } from "../middleware/auth.ts";
|
||||
import type { EmployeeSwap, CreateSwapRequest, User } from "../types/index.ts";
|
||||
import {
|
||||
authenticateToken,
|
||||
authorize,
|
||||
getCurrentUser,
|
||||
} from "../middleware/auth.ts";
|
||||
import type { CreateSwapRequest, EmployeeSwap, User } from "../types/index.ts";
|
||||
|
||||
const router = new Router();
|
||||
|
||||
// Get all employee swaps (SuperAdmin only)
|
||||
router.get("/", authenticateToken, authorize("SuperAdmin"), async (ctx) => {
|
||||
try {
|
||||
const params = ctx.request.url.searchParams;
|
||||
const status = params.get("status");
|
||||
const employeeId = params.get("employeeId");
|
||||
const startDate = params.get("startDate");
|
||||
const endDate = params.get("endDate");
|
||||
|
||||
let query = `
|
||||
router.get(
|
||||
"/",
|
||||
authenticateToken,
|
||||
authorize("SuperAdmin"),
|
||||
async (
|
||||
ctx: RouterContext<"/", Record<string | number, string | undefined>, State>,
|
||||
) => {
|
||||
try {
|
||||
const params: URLSearchParams = ctx.request.url.searchParams;
|
||||
const status: string | null = params.get("status");
|
||||
const employeeId: string | null = params.get("employeeId");
|
||||
const startDate: string | null = params.get("startDate");
|
||||
const endDate: string | null = params.get("endDate");
|
||||
|
||||
let query = `
|
||||
SELECT es.*,
|
||||
e.name as employee_name,
|
||||
od.name as original_department_name,
|
||||
@@ -31,46 +41,57 @@ router.get("/", authenticateToken, authorize("SuperAdmin"), async (ctx) => {
|
||||
JOIN users sb ON es.swapped_by = sb.id
|
||||
WHERE 1=1
|
||||
`;
|
||||
const queryParams: unknown[] = [];
|
||||
|
||||
if (status) {
|
||||
query += " AND es.status = ?";
|
||||
queryParams.push(status);
|
||||
const queryParams: unknown[] = [];
|
||||
|
||||
if (status) {
|
||||
query += " AND es.status = ?";
|
||||
queryParams.push(status);
|
||||
}
|
||||
|
||||
if (employeeId) {
|
||||
query += " AND es.employee_id = ?";
|
||||
queryParams.push(employeeId);
|
||||
}
|
||||
|
||||
if (startDate) {
|
||||
query += " AND es.swap_date >= ?";
|
||||
queryParams.push(startDate);
|
||||
}
|
||||
|
||||
if (endDate) {
|
||||
query += " AND es.swap_date <= ?";
|
||||
queryParams.push(endDate);
|
||||
}
|
||||
|
||||
query += " ORDER BY es.created_at DESC";
|
||||
|
||||
const swaps = await db.query<EmployeeSwap[]>(query, queryParams);
|
||||
ctx.response.body = swaps;
|
||||
} catch (error) {
|
||||
console.error("Get employee swaps error:", error);
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: "Internal server error" };
|
||||
}
|
||||
|
||||
if (employeeId) {
|
||||
query += " AND es.employee_id = ?";
|
||||
queryParams.push(employeeId);
|
||||
}
|
||||
|
||||
if (startDate) {
|
||||
query += " AND es.swap_date >= ?";
|
||||
queryParams.push(startDate);
|
||||
}
|
||||
|
||||
if (endDate) {
|
||||
query += " AND es.swap_date <= ?";
|
||||
queryParams.push(endDate);
|
||||
}
|
||||
|
||||
query += " ORDER BY es.created_at DESC";
|
||||
|
||||
const swaps = await db.query<EmployeeSwap[]>(query, queryParams);
|
||||
ctx.response.body = swaps;
|
||||
} catch (error) {
|
||||
console.error("Get employee swaps error:", error);
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: "Internal server error" };
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
// Get swap by ID
|
||||
router.get("/:id", authenticateToken, authorize("SuperAdmin"), async (ctx) => {
|
||||
try {
|
||||
const swapId = ctx.params.id;
|
||||
|
||||
const swaps = await db.query<EmployeeSwap[]>(
|
||||
`SELECT es.*,
|
||||
router.get(
|
||||
"/:id",
|
||||
authenticateToken,
|
||||
authorize("SuperAdmin"),
|
||||
async (
|
||||
ctx: RouterContext<
|
||||
"/:id",
|
||||
{ id: string } & Record<string | number, string | undefined>,
|
||||
State
|
||||
>,
|
||||
) => {
|
||||
try {
|
||||
const swapId = ctx.params.id;
|
||||
|
||||
const swaps = await db.query<EmployeeSwap[]>(
|
||||
`SELECT es.*,
|
||||
e.name as employee_name,
|
||||
od.name as original_department_name,
|
||||
td.name as target_department_name,
|
||||
@@ -85,45 +106,49 @@ router.get("/:id", authenticateToken, authorize("SuperAdmin"), async (ctx) => {
|
||||
LEFT JOIN users tc ON es.target_contractor_id = tc.id
|
||||
JOIN users sb ON es.swapped_by = sb.id
|
||||
WHERE es.id = ?`,
|
||||
[swapId]
|
||||
);
|
||||
|
||||
if (swaps.length === 0) {
|
||||
ctx.response.status = 404;
|
||||
ctx.response.body = { error: "Swap record not found" };
|
||||
return;
|
||||
[swapId],
|
||||
);
|
||||
|
||||
if (swaps.length === 0) {
|
||||
ctx.response.status = 404;
|
||||
ctx.response.body = { error: "Swap record not found" };
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.response.body = swaps[0];
|
||||
} catch (error) {
|
||||
console.error("Get swap error:", error);
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: "Internal server error" };
|
||||
}
|
||||
|
||||
ctx.response.body = swaps[0];
|
||||
} catch (error) {
|
||||
console.error("Get swap error:", error);
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: "Internal server error" };
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
// Create new employee swap (SuperAdmin only)
|
||||
router.post("/", authenticateToken, authorize("SuperAdmin"), async (ctx) => {
|
||||
try {
|
||||
const currentUser = getCurrentUser(ctx);
|
||||
const body = await ctx.request.body.json() as CreateSwapRequest;
|
||||
const {
|
||||
employeeId,
|
||||
targetDepartmentId,
|
||||
targetContractorId,
|
||||
swapReason,
|
||||
reasonDetails,
|
||||
const {
|
||||
employeeId,
|
||||
targetDepartmentId,
|
||||
targetContractorId,
|
||||
swapReason,
|
||||
reasonDetails,
|
||||
workCompletionPercentage,
|
||||
swapDate
|
||||
swapDate,
|
||||
} = body;
|
||||
|
||||
|
||||
// Validate required fields
|
||||
if (!employeeId || !targetDepartmentId || !swapReason || !swapDate) {
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
// Validate swap reason
|
||||
const validReasons = ["LeftWork", "Sick", "FinishedEarly", "Other"];
|
||||
if (!validReasons.includes(swapReason)) {
|
||||
@@ -131,87 +156,108 @@ router.post("/", authenticateToken, authorize("SuperAdmin"), async (ctx) => {
|
||||
ctx.response.body = { error: "Invalid swap reason" };
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Get employee's current department and contractor
|
||||
const employees = await db.query<User[]>(
|
||||
"SELECT * FROM users WHERE id = ? AND role = 'Employee'",
|
||||
[employeeId]
|
||||
[employeeId],
|
||||
);
|
||||
|
||||
|
||||
if (employees.length === 0) {
|
||||
ctx.response.status = 404;
|
||||
ctx.response.body = { error: "Employee not found" };
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const employee = employees[0];
|
||||
|
||||
|
||||
if (!employee.department_id) {
|
||||
ctx.response.status = 400;
|
||||
ctx.response.body = { error: "Employee has no current department" };
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Check if there's already an active swap for this employee
|
||||
const activeSwaps = await db.query<EmployeeSwap[]>(
|
||||
"SELECT * FROM employee_swaps WHERE employee_id = ? AND status = 'Active'",
|
||||
[employeeId]
|
||||
[employeeId],
|
||||
);
|
||||
|
||||
|
||||
if (activeSwaps.length > 0) {
|
||||
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;
|
||||
}
|
||||
|
||||
// Create the swap record
|
||||
const result = await db.execute(
|
||||
`INSERT INTO employee_swaps
|
||||
(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)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'Active')`,
|
||||
[
|
||||
employeeId,
|
||||
employee.department_id,
|
||||
targetDepartmentId,
|
||||
employee.contractor_id || null,
|
||||
targetContractorId || null,
|
||||
swapReason,
|
||||
reasonDetails || null,
|
||||
workCompletionPercentage || 0,
|
||||
swapDate,
|
||||
currentUser.id
|
||||
]
|
||||
);
|
||||
|
||||
// Update the employee's department and contractor
|
||||
await db.execute(
|
||||
"UPDATE users SET department_id = ?, contractor_id = ? WHERE id = ?",
|
||||
[targetDepartmentId, targetContractorId || null, employeeId]
|
||||
);
|
||||
|
||||
// Fetch the created swap
|
||||
const newSwap = await db.query<EmployeeSwap[]>(
|
||||
`SELECT es.*,
|
||||
e.name as employee_name,
|
||||
od.name as original_department_name,
|
||||
td.name as target_department_name,
|
||||
oc.name as original_contractor_name,
|
||||
tc.name as target_contractor_name,
|
||||
sb.name as swapped_by_name
|
||||
FROM employee_swaps es
|
||||
JOIN users e ON es.employee_id = e.id
|
||||
JOIN departments od ON es.original_department_id = od.id
|
||||
JOIN departments td ON es.target_department_id = td.id
|
||||
LEFT JOIN users oc ON es.original_contractor_id = oc.id
|
||||
LEFT JOIN users tc ON es.target_contractor_id = tc.id
|
||||
JOIN users sb ON es.swapped_by = sb.id
|
||||
WHERE es.id = ?`,
|
||||
[result.insertId]
|
||||
);
|
||||
|
||||
|
||||
// Use transaction to ensure both operations succeed or fail together
|
||||
const newSwap = await db.transaction(async (connection) => {
|
||||
// Create the swap record
|
||||
const [insertResult] = await connection.execute(
|
||||
`INSERT INTO employee_swaps
|
||||
(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)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'Active')`,
|
||||
[
|
||||
employeeId,
|
||||
employee.department_id,
|
||||
targetDepartmentId,
|
||||
employee.contractor_id || null,
|
||||
targetContractorId || null,
|
||||
swapReason,
|
||||
reasonDetails || null,
|
||||
workCompletionPercentage || 0,
|
||||
swapDate,
|
||||
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
|
||||
const [updateResult] = await connection.execute(
|
||||
"UPDATE users SET department_id = ?, contractor_id = ? WHERE id = ?",
|
||||
[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
|
||||
const [swapRows] = await connection.query(
|
||||
`SELECT es.*,
|
||||
e.name as employee_name,
|
||||
od.name as original_department_name,
|
||||
td.name as target_department_name,
|
||||
oc.name as original_contractor_name,
|
||||
tc.name as target_contractor_name,
|
||||
sb.name as swapped_by_name
|
||||
FROM employee_swaps es
|
||||
JOIN users e ON es.employee_id = e.id
|
||||
JOIN departments od ON es.original_department_id = od.id
|
||||
JOIN departments td ON es.target_department_id = td.id
|
||||
LEFT JOIN users oc ON es.original_contractor_id = oc.id
|
||||
LEFT JOIN users tc ON es.target_contractor_id = tc.id
|
||||
JOIN users sb ON es.swapped_by = sb.id
|
||||
WHERE es.id = ?`,
|
||||
[swapInsertId],
|
||||
);
|
||||
|
||||
return (swapRows as EmployeeSwap[])[0];
|
||||
});
|
||||
|
||||
ctx.response.status = 201;
|
||||
ctx.response.body = newSwap[0];
|
||||
ctx.response.body = newSwap;
|
||||
} catch (error) {
|
||||
console.error("Create swap error:", error);
|
||||
ctx.response.status = 500;
|
||||
@@ -220,121 +266,149 @@ router.post("/", authenticateToken, authorize("SuperAdmin"), async (ctx) => {
|
||||
});
|
||||
|
||||
// Complete a swap (return employee to original department)
|
||||
router.put("/:id/complete", authenticateToken, authorize("SuperAdmin"), async (ctx) => {
|
||||
try {
|
||||
const swapId = ctx.params.id;
|
||||
|
||||
// Get the swap record
|
||||
const swaps = await db.query<EmployeeSwap[]>(
|
||||
"SELECT * FROM employee_swaps WHERE id = ? AND status = 'Active'",
|
||||
[swapId]
|
||||
);
|
||||
|
||||
if (swaps.length === 0) {
|
||||
ctx.response.status = 404;
|
||||
ctx.response.body = { error: "Active swap not found" };
|
||||
return;
|
||||
router.put(
|
||||
"/:id/complete",
|
||||
authenticateToken,
|
||||
authorize("SuperAdmin"),
|
||||
async (ctx) => {
|
||||
try {
|
||||
const swapId = ctx.params.id;
|
||||
|
||||
// Get the swap record
|
||||
const swaps = await db.query<EmployeeSwap[]>(
|
||||
"SELECT * FROM employee_swaps WHERE id = ? AND status = 'Active'",
|
||||
[swapId],
|
||||
);
|
||||
|
||||
if (swaps.length === 0) {
|
||||
ctx.response.status = 404;
|
||||
ctx.response.body = { error: "Active swap not found" };
|
||||
return;
|
||||
}
|
||||
|
||||
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
|
||||
await connection.execute(
|
||||
"UPDATE users SET department_id = ?, contractor_id = ? WHERE id = ?",
|
||||
[
|
||||
swap.original_department_id,
|
||||
swap.original_contractor_id,
|
||||
swap.employee_id,
|
||||
],
|
||||
);
|
||||
|
||||
// Mark swap as completed
|
||||
await connection.execute(
|
||||
"UPDATE employee_swaps SET status = 'Completed', completed_at = NOW() WHERE id = ?",
|
||||
[swapId],
|
||||
);
|
||||
|
||||
// Fetch updated swap
|
||||
const [swapRows] = await connection.query(
|
||||
`SELECT es.*,
|
||||
e.name as employee_name,
|
||||
od.name as original_department_name,
|
||||
td.name as target_department_name,
|
||||
oc.name as original_contractor_name,
|
||||
tc.name as target_contractor_name,
|
||||
sb.name as swapped_by_name
|
||||
FROM employee_swaps es
|
||||
JOIN users e ON es.employee_id = e.id
|
||||
JOIN departments od ON es.original_department_id = od.id
|
||||
JOIN departments td ON es.target_department_id = td.id
|
||||
LEFT JOIN users oc ON es.original_contractor_id = oc.id
|
||||
LEFT JOIN users tc ON es.target_contractor_id = tc.id
|
||||
JOIN users sb ON es.swapped_by = sb.id
|
||||
WHERE es.id = ?`,
|
||||
[swapId],
|
||||
);
|
||||
|
||||
return (swapRows as EmployeeSwap[])[0];
|
||||
});
|
||||
|
||||
ctx.response.body = updatedSwap;
|
||||
} catch (error) {
|
||||
console.error("Complete swap error:", error);
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: "Internal server error" };
|
||||
}
|
||||
|
||||
const swap = swaps[0];
|
||||
|
||||
// Return employee to original department and contractor
|
||||
await db.execute(
|
||||
"UPDATE users SET department_id = ?, contractor_id = ? WHERE id = ?",
|
||||
[swap.original_department_id, swap.original_contractor_id, swap.employee_id]
|
||||
);
|
||||
|
||||
// Mark swap as completed
|
||||
await db.execute(
|
||||
"UPDATE employee_swaps SET status = 'Completed', completed_at = NOW() WHERE id = ?",
|
||||
[swapId]
|
||||
);
|
||||
|
||||
// Fetch updated swap
|
||||
const updatedSwap = await db.query<EmployeeSwap[]>(
|
||||
`SELECT es.*,
|
||||
e.name as employee_name,
|
||||
od.name as original_department_name,
|
||||
td.name as target_department_name,
|
||||
oc.name as original_contractor_name,
|
||||
tc.name as target_contractor_name,
|
||||
sb.name as swapped_by_name
|
||||
FROM employee_swaps es
|
||||
JOIN users e ON es.employee_id = e.id
|
||||
JOIN departments od ON es.original_department_id = od.id
|
||||
JOIN departments td ON es.target_department_id = td.id
|
||||
LEFT JOIN users oc ON es.original_contractor_id = oc.id
|
||||
LEFT JOIN users tc ON es.target_contractor_id = tc.id
|
||||
JOIN users sb ON es.swapped_by = sb.id
|
||||
WHERE es.id = ?`,
|
||||
[swapId]
|
||||
);
|
||||
|
||||
ctx.response.body = updatedSwap[0];
|
||||
} catch (error) {
|
||||
console.error("Complete swap error:", error);
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: "Internal server error" };
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
// Cancel a swap (return employee to original department)
|
||||
router.put("/:id/cancel", authenticateToken, authorize("SuperAdmin"), async (ctx) => {
|
||||
try {
|
||||
const swapId = ctx.params.id;
|
||||
|
||||
// Get the swap record
|
||||
const swaps = await db.query<EmployeeSwap[]>(
|
||||
"SELECT * FROM employee_swaps WHERE id = ? AND status = 'Active'",
|
||||
[swapId]
|
||||
);
|
||||
|
||||
if (swaps.length === 0) {
|
||||
ctx.response.status = 404;
|
||||
ctx.response.body = { error: "Active swap not found" };
|
||||
return;
|
||||
router.put(
|
||||
"/:id/cancel",
|
||||
authenticateToken,
|
||||
authorize("SuperAdmin"),
|
||||
async (ctx) => {
|
||||
try {
|
||||
const swapId = ctx.params.id;
|
||||
|
||||
// Get the swap record
|
||||
const swaps = await db.query<EmployeeSwap[]>(
|
||||
"SELECT * FROM employee_swaps WHERE id = ? AND status = 'Active'",
|
||||
[swapId],
|
||||
);
|
||||
|
||||
if (swaps.length === 0) {
|
||||
ctx.response.status = 404;
|
||||
ctx.response.body = { error: "Active swap not found" };
|
||||
return;
|
||||
}
|
||||
|
||||
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
|
||||
await connection.execute(
|
||||
"UPDATE users SET department_id = ?, contractor_id = ? WHERE id = ?",
|
||||
[
|
||||
swap.original_department_id,
|
||||
swap.original_contractor_id,
|
||||
swap.employee_id,
|
||||
],
|
||||
);
|
||||
|
||||
// Mark swap as cancelled
|
||||
await connection.execute(
|
||||
"UPDATE employee_swaps SET status = 'Cancelled', completed_at = NOW() WHERE id = ?",
|
||||
[swapId],
|
||||
);
|
||||
|
||||
// Fetch updated swap
|
||||
const [swapRows] = await connection.query(
|
||||
`SELECT es.*,
|
||||
e.name as employee_name,
|
||||
od.name as original_department_name,
|
||||
td.name as target_department_name,
|
||||
oc.name as original_contractor_name,
|
||||
tc.name as target_contractor_name,
|
||||
sb.name as swapped_by_name
|
||||
FROM employee_swaps es
|
||||
JOIN users e ON es.employee_id = e.id
|
||||
JOIN departments od ON es.original_department_id = od.id
|
||||
JOIN departments td ON es.target_department_id = td.id
|
||||
LEFT JOIN users oc ON es.original_contractor_id = oc.id
|
||||
LEFT JOIN users tc ON es.target_contractor_id = tc.id
|
||||
JOIN users sb ON es.swapped_by = sb.id
|
||||
WHERE es.id = ?`,
|
||||
[swapId],
|
||||
);
|
||||
|
||||
return (swapRows as EmployeeSwap[])[0];
|
||||
});
|
||||
|
||||
ctx.response.body = updatedSwap;
|
||||
} catch (error) {
|
||||
console.error("Cancel swap error:", error);
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: "Internal server error" };
|
||||
}
|
||||
|
||||
const swap = swaps[0];
|
||||
|
||||
// Return employee to original department and contractor
|
||||
await db.execute(
|
||||
"UPDATE users SET department_id = ?, contractor_id = ? WHERE id = ?",
|
||||
[swap.original_department_id, swap.original_contractor_id, swap.employee_id]
|
||||
);
|
||||
|
||||
// Mark swap as cancelled
|
||||
await db.execute(
|
||||
"UPDATE employee_swaps SET status = 'Cancelled', completed_at = NOW() WHERE id = ?",
|
||||
[swapId]
|
||||
);
|
||||
|
||||
// Fetch updated swap
|
||||
const updatedSwap = await db.query<EmployeeSwap[]>(
|
||||
`SELECT es.*,
|
||||
e.name as employee_name,
|
||||
od.name as original_department_name,
|
||||
td.name as target_department_name,
|
||||
oc.name as original_contractor_name,
|
||||
tc.name as target_contractor_name,
|
||||
sb.name as swapped_by_name
|
||||
FROM employee_swaps es
|
||||
JOIN users e ON es.employee_id = e.id
|
||||
JOIN departments od ON es.original_department_id = od.id
|
||||
JOIN departments td ON es.target_department_id = td.id
|
||||
LEFT JOIN users oc ON es.original_contractor_id = oc.id
|
||||
LEFT JOIN users tc ON es.target_contractor_id = tc.id
|
||||
JOIN users sb ON es.swapped_by = sb.id
|
||||
WHERE es.id = ?`,
|
||||
[swapId]
|
||||
);
|
||||
|
||||
ctx.response.body = updatedSwap[0];
|
||||
} catch (error) {
|
||||
console.error("Cancel swap error:", error);
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: "Internal server error" };
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -1,22 +1,30 @@
|
||||
import { Router } from "@oak/oak";
|
||||
import { db } from "../config/database.ts";
|
||||
import { authenticateToken, authorize, getCurrentUser } from "../middleware/auth.ts";
|
||||
import type { WorkAllocation } from "../types/index.ts";
|
||||
import {
|
||||
authenticateToken,
|
||||
authorize,
|
||||
getCurrentUser,
|
||||
} from "../middleware/auth.ts";
|
||||
import type { JWTPayload, WorkAllocation } from "../types/index.ts";
|
||||
|
||||
const router = new Router();
|
||||
|
||||
// Get completed work allocations for reporting (with optional filters)
|
||||
router.get("/completed-allocations", authenticateToken, authorize("Supervisor", "SuperAdmin"), async (ctx) => {
|
||||
try {
|
||||
const currentUser = getCurrentUser(ctx);
|
||||
const params = ctx.request.url.searchParams;
|
||||
const startDate = params.get("startDate");
|
||||
const endDate = params.get("endDate");
|
||||
const departmentId = params.get("departmentId");
|
||||
const contractorId = params.get("contractorId");
|
||||
const employeeId = params.get("employeeId");
|
||||
|
||||
let query = `
|
||||
router.get(
|
||||
"/completed-allocations",
|
||||
authenticateToken,
|
||||
authorize("Supervisor", "SuperAdmin"),
|
||||
async (ctx) => {
|
||||
try {
|
||||
const currentUser: JWTPayload = getCurrentUser(ctx);
|
||||
const params: URLSearchParams = ctx.request.url.searchParams;
|
||||
const startDate: string | null = params.get("startDate");
|
||||
const endDate: string | null = params.get("endDate");
|
||||
const departmentId: string | null = params.get("departmentId");
|
||||
const contractorId: string | null = params.get("contractorId");
|
||||
const employeeId: string | null = params.get("employeeId");
|
||||
|
||||
let query = `
|
||||
SELECT wa.*,
|
||||
e.name as employee_name, e.username as employee_username,
|
||||
e.phone_number as employee_phone,
|
||||
@@ -33,95 +41,110 @@ router.get("/completed-allocations", authenticateToken, authorize("Supervisor",
|
||||
LEFT JOIN departments d ON e.department_id = d.id
|
||||
WHERE wa.status = 'Completed'
|
||||
`;
|
||||
const queryParams: unknown[] = [];
|
||||
|
||||
// Role-based filtering - Supervisors can only see their department
|
||||
if (currentUser.role === "Supervisor") {
|
||||
query += " AND e.department_id = ?";
|
||||
queryParams.push(currentUser.departmentId);
|
||||
}
|
||||
|
||||
// Date range filter
|
||||
if (startDate) {
|
||||
query += " AND wa.completion_date >= ?";
|
||||
queryParams.push(startDate);
|
||||
}
|
||||
|
||||
if (endDate) {
|
||||
query += " AND wa.completion_date <= ?";
|
||||
queryParams.push(endDate);
|
||||
}
|
||||
|
||||
// Department filter (for SuperAdmin)
|
||||
if (departmentId && currentUser.role === "SuperAdmin") {
|
||||
query += " AND e.department_id = ?";
|
||||
queryParams.push(departmentId);
|
||||
}
|
||||
|
||||
// Contractor filter
|
||||
if (contractorId) {
|
||||
query += " AND wa.contractor_id = ?";
|
||||
queryParams.push(contractorId);
|
||||
}
|
||||
|
||||
// Employee filter
|
||||
if (employeeId) {
|
||||
query += " AND wa.employee_id = ?";
|
||||
queryParams.push(employeeId);
|
||||
}
|
||||
|
||||
query += " ORDER BY wa.completion_date DESC, wa.created_at DESC";
|
||||
|
||||
const allocations = await db.query<WorkAllocation[]>(query, queryParams);
|
||||
|
||||
// Calculate summary stats
|
||||
const totalAllocations = allocations.length;
|
||||
const totalAmount = allocations.reduce((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 = {
|
||||
allocations,
|
||||
summary: {
|
||||
totalAllocations,
|
||||
totalAmount: totalAmount.toFixed(2),
|
||||
totalUnits: totalUnits.toFixed(2),
|
||||
const queryParams: unknown[] = [];
|
||||
|
||||
// Role-based filtering - Supervisors can only see their department
|
||||
if (currentUser.role === "Supervisor") {
|
||||
query += " AND e.department_id = ?";
|
||||
queryParams.push(currentUser.departmentId);
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Get completed allocations report error:", error);
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: "Internal server error" };
|
||||
}
|
||||
});
|
||||
|
||||
// Date range filter
|
||||
if (startDate) {
|
||||
query += " AND wa.completion_date >= ?";
|
||||
queryParams.push(startDate);
|
||||
}
|
||||
|
||||
if (endDate) {
|
||||
query += " AND wa.completion_date <= ?";
|
||||
queryParams.push(endDate);
|
||||
}
|
||||
|
||||
// Department filter (for SuperAdmin)
|
||||
if (departmentId && currentUser.role === "SuperAdmin") {
|
||||
query += " AND e.department_id = ?";
|
||||
queryParams.push(departmentId);
|
||||
}
|
||||
|
||||
// Contractor filter
|
||||
if (contractorId) {
|
||||
query += " AND wa.contractor_id = ?";
|
||||
queryParams.push(contractorId);
|
||||
}
|
||||
|
||||
// Employee filter
|
||||
if (employeeId) {
|
||||
query += " AND wa.employee_id = ?";
|
||||
queryParams.push(employeeId);
|
||||
}
|
||||
|
||||
query += " ORDER BY wa.completion_date DESC, wa.created_at DESC";
|
||||
|
||||
const allocations = await db.query<WorkAllocation[]>(query, queryParams);
|
||||
|
||||
// Calculate summary stats
|
||||
const totalAllocations = allocations.length;
|
||||
const totalAmount = allocations.reduce(
|
||||
(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 = {
|
||||
allocations,
|
||||
summary: {
|
||||
totalAllocations,
|
||||
totalAmount: totalAmount.toFixed(2),
|
||||
totalUnits: totalUnits.toFixed(2),
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Get completed allocations report error:", error);
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: "Internal server error" };
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Get summary statistics for completed work
|
||||
router.get("/summary", authenticateToken, authorize("Supervisor", "SuperAdmin"), async (ctx) => {
|
||||
try {
|
||||
const currentUser = getCurrentUser(ctx);
|
||||
const params = ctx.request.url.searchParams;
|
||||
const startDate = params.get("startDate");
|
||||
const endDate = params.get("endDate");
|
||||
|
||||
let departmentFilter = "";
|
||||
const queryParams: unknown[] = [];
|
||||
|
||||
if (currentUser.role === "Supervisor") {
|
||||
departmentFilter = " AND e.department_id = ?";
|
||||
queryParams.push(currentUser.departmentId);
|
||||
}
|
||||
|
||||
let dateFilter = "";
|
||||
if (startDate) {
|
||||
dateFilter += " AND wa.completion_date >= ?";
|
||||
queryParams.push(startDate);
|
||||
}
|
||||
if (endDate) {
|
||||
dateFilter += " AND wa.completion_date <= ?";
|
||||
queryParams.push(endDate);
|
||||
}
|
||||
|
||||
// Get summary by contractor
|
||||
const byContractor = await db.query<any[]>(`
|
||||
router.get(
|
||||
"/summary",
|
||||
authenticateToken,
|
||||
authorize("Supervisor", "SuperAdmin"),
|
||||
async (ctx) => {
|
||||
try {
|
||||
const currentUser: JWTPayload = getCurrentUser(ctx);
|
||||
const params: URLSearchParams = ctx.request.url.searchParams;
|
||||
const startDate: string | null = params.get("startDate");
|
||||
const endDate: string | null = params.get("endDate");
|
||||
|
||||
let departmentFilter = "";
|
||||
const queryParams: unknown[] = [];
|
||||
|
||||
if (currentUser.role === "Supervisor") {
|
||||
departmentFilter = " AND e.department_id = ?";
|
||||
queryParams.push(currentUser.departmentId);
|
||||
}
|
||||
|
||||
let dateFilter = "";
|
||||
if (startDate) {
|
||||
dateFilter += " AND wa.completion_date >= ?";
|
||||
queryParams.push(startDate);
|
||||
}
|
||||
if (endDate) {
|
||||
dateFilter += " AND wa.completion_date <= ?";
|
||||
queryParams.push(endDate);
|
||||
}
|
||||
|
||||
// Get summary by contractor
|
||||
const byContractor = await db.query<any[]>(
|
||||
`
|
||||
SELECT
|
||||
c.id as contractor_id,
|
||||
c.name as contractor_name,
|
||||
@@ -134,10 +157,13 @@ router.get("/summary", authenticateToken, authorize("Supervisor", "SuperAdmin"),
|
||||
WHERE wa.status = 'Completed' ${departmentFilter} ${dateFilter}
|
||||
GROUP BY c.id, c.name
|
||||
ORDER BY total_amount DESC
|
||||
`, queryParams);
|
||||
|
||||
// Get summary by sub-department
|
||||
const bySubDepartment = await db.query<any[]>(`
|
||||
`,
|
||||
queryParams,
|
||||
);
|
||||
|
||||
// Get summary by sub-department
|
||||
const bySubDepartment = await db.query<any[]>(
|
||||
`
|
||||
SELECT
|
||||
sd.id as sub_department_id,
|
||||
sd.name as sub_department_name,
|
||||
@@ -152,10 +178,13 @@ router.get("/summary", authenticateToken, authorize("Supervisor", "SuperAdmin"),
|
||||
WHERE wa.status = 'Completed' ${departmentFilter} ${dateFilter}
|
||||
GROUP BY sd.id, sd.name, d.name
|
||||
ORDER BY total_amount DESC
|
||||
`, queryParams);
|
||||
|
||||
// Get summary by activity type
|
||||
const byActivity = await db.query<any[]>(`
|
||||
`,
|
||||
queryParams,
|
||||
);
|
||||
|
||||
// Get summary by activity type
|
||||
const byActivity = await db.query<any[]>(
|
||||
`
|
||||
SELECT
|
||||
COALESCE(wa.activity, 'Standard') as activity,
|
||||
COUNT(*) as total_allocations,
|
||||
@@ -166,18 +195,21 @@ router.get("/summary", authenticateToken, authorize("Supervisor", "SuperAdmin"),
|
||||
WHERE wa.status = 'Completed' ${departmentFilter} ${dateFilter}
|
||||
GROUP BY wa.activity
|
||||
ORDER BY total_amount DESC
|
||||
`, queryParams);
|
||||
|
||||
ctx.response.body = {
|
||||
byContractor,
|
||||
bySubDepartment,
|
||||
byActivity,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Get report summary error:", error);
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: "Internal server error" };
|
||||
}
|
||||
});
|
||||
`,
|
||||
queryParams,
|
||||
);
|
||||
|
||||
ctx.response.body = {
|
||||
byContractor,
|
||||
bySubDepartment,
|
||||
byActivity,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Get report summary error:", error);
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: "Internal server error" };
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { Router } from "@oak/oak";
|
||||
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 type { Context } from "@oak/oak";
|
||||
|
||||
const router = new Router();
|
||||
|
||||
@@ -21,14 +26,16 @@ interface StandardRate {
|
||||
}
|
||||
|
||||
// Get all standard rates (default rates for comparison)
|
||||
router.get("/", authenticateToken, async (ctx) => {
|
||||
router.get("/", authenticateToken, async (ctx: Context) => {
|
||||
try {
|
||||
const currentUser = getCurrentUser(ctx);
|
||||
const params = ctx.request.url.searchParams;
|
||||
const departmentId = params.get("departmentId");
|
||||
const subDepartmentId = params.get("subDepartmentId");
|
||||
const activity = params.get("activity");
|
||||
|
||||
const departmentId: string | number | null = params.get("departmentId");
|
||||
const subDepartmentId: string | number | null = params.get(
|
||||
"subDepartmentId",
|
||||
);
|
||||
const activity: string | null = params.get("activity");
|
||||
|
||||
let query = `
|
||||
SELECT sr.*,
|
||||
sd.name as sub_department_name,
|
||||
@@ -44,30 +51,30 @@ router.get("/", authenticateToken, async (ctx) => {
|
||||
WHERE 1=1
|
||||
`;
|
||||
const queryParams: unknown[] = [];
|
||||
|
||||
|
||||
// Supervisors can only see rates for their department
|
||||
if (currentUser.role === "Supervisor") {
|
||||
query += " AND d.id = ?";
|
||||
queryParams.push(currentUser.departmentId);
|
||||
}
|
||||
|
||||
|
||||
if (departmentId) {
|
||||
query += " AND d.id = ?";
|
||||
queryParams.push(departmentId);
|
||||
}
|
||||
|
||||
|
||||
if (subDepartmentId) {
|
||||
query += " AND sr.sub_department_id = ?";
|
||||
queryParams.push(subDepartmentId);
|
||||
}
|
||||
|
||||
|
||||
if (activity) {
|
||||
query += " AND sr.activity = ?";
|
||||
queryParams.push(activity);
|
||||
}
|
||||
|
||||
|
||||
query += " ORDER BY sr.effective_date DESC, sr.created_at DESC";
|
||||
|
||||
|
||||
const rates = await db.query<StandardRate[]>(query, queryParams);
|
||||
ctx.response.body = rates;
|
||||
} catch (error) {
|
||||
@@ -78,15 +85,19 @@ router.get("/", authenticateToken, async (ctx) => {
|
||||
});
|
||||
|
||||
// Get all rates (contractor + standard) for SuperAdmin - all departments, sorted by date
|
||||
router.get("/all-rates", authenticateToken, authorize("SuperAdmin"), async (ctx) => {
|
||||
try {
|
||||
const params = ctx.request.url.searchParams;
|
||||
const departmentId = params.get("departmentId");
|
||||
const startDate = params.get("startDate");
|
||||
const endDate = params.get("endDate");
|
||||
|
||||
// Get contractor rates
|
||||
let contractorQuery = `
|
||||
router.get(
|
||||
"/all-rates",
|
||||
authenticateToken,
|
||||
authorize("SuperAdmin"),
|
||||
async (ctx: Context) => {
|
||||
try {
|
||||
const params = ctx.request.url.searchParams;
|
||||
const departmentId: string | number | null = params.get("departmentId");
|
||||
const startDate: string | null = params.get("startDate");
|
||||
const endDate: string | null = params.get("endDate");
|
||||
|
||||
// Get contractor rates
|
||||
let contractorQuery = `
|
||||
SELECT
|
||||
cr.id,
|
||||
'contractor' as rate_type,
|
||||
@@ -108,25 +119,25 @@ router.get("/all-rates", authenticateToken, authorize("SuperAdmin"), async (ctx)
|
||||
LEFT JOIN departments d ON sd.department_id = d.id
|
||||
WHERE 1=1
|
||||
`;
|
||||
const contractorParams: unknown[] = [];
|
||||
|
||||
if (departmentId) {
|
||||
contractorQuery += " AND d.id = ?";
|
||||
contractorParams.push(departmentId);
|
||||
}
|
||||
|
||||
if (startDate) {
|
||||
contractorQuery += " AND cr.effective_date >= ?";
|
||||
contractorParams.push(startDate);
|
||||
}
|
||||
|
||||
if (endDate) {
|
||||
contractorQuery += " AND cr.effective_date <= ?";
|
||||
contractorParams.push(endDate);
|
||||
}
|
||||
|
||||
// Get standard rates
|
||||
let standardQuery = `
|
||||
const contractorParams: unknown[] = [];
|
||||
|
||||
if (departmentId) {
|
||||
contractorQuery += " AND d.id = ?";
|
||||
contractorParams.push(departmentId);
|
||||
}
|
||||
|
||||
if (startDate) {
|
||||
contractorQuery += " AND cr.effective_date >= ?";
|
||||
contractorParams.push(startDate);
|
||||
}
|
||||
|
||||
if (endDate) {
|
||||
contractorQuery += " AND cr.effective_date <= ?";
|
||||
contractorParams.push(endDate);
|
||||
}
|
||||
|
||||
// Get standard rates
|
||||
let standardQuery = `
|
||||
SELECT
|
||||
sr.id,
|
||||
'standard' as rate_type,
|
||||
@@ -148,66 +159,77 @@ router.get("/all-rates", authenticateToken, authorize("SuperAdmin"), async (ctx)
|
||||
LEFT JOIN users u ON sr.created_by = u.id
|
||||
WHERE 1=1
|
||||
`;
|
||||
const standardParams: unknown[] = [];
|
||||
|
||||
if (departmentId) {
|
||||
standardQuery += " AND d.id = ?";
|
||||
standardParams.push(departmentId);
|
||||
}
|
||||
|
||||
if (startDate) {
|
||||
standardQuery += " AND sr.effective_date >= ?";
|
||||
standardParams.push(startDate);
|
||||
}
|
||||
|
||||
if (endDate) {
|
||||
standardQuery += " AND sr.effective_date <= ?";
|
||||
standardParams.push(endDate);
|
||||
}
|
||||
|
||||
const contractorRates = await db.query<any[]>(contractorQuery, contractorParams);
|
||||
const standardRates = await db.query<any[]>(standardQuery, standardParams);
|
||||
|
||||
// Combine and sort by date
|
||||
const allRates = [...contractorRates, ...standardRates].sort((a, b) => {
|
||||
const dateA = new Date(a.effective_date).getTime();
|
||||
const dateB = new Date(b.effective_date).getTime();
|
||||
return dateB - dateA; // Descending order
|
||||
});
|
||||
|
||||
ctx.response.body = {
|
||||
allRates,
|
||||
summary: {
|
||||
totalContractorRates: contractorRates.length,
|
||||
totalStandardRates: standardRates.length,
|
||||
totalRates: allRates.length,
|
||||
const standardParams: unknown[] = [];
|
||||
|
||||
if (departmentId) {
|
||||
standardQuery += " AND d.id = ?";
|
||||
standardParams.push(departmentId);
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Get all rates error:", error);
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: "Internal server error" };
|
||||
}
|
||||
});
|
||||
|
||||
if (startDate) {
|
||||
standardQuery += " AND sr.effective_date >= ?";
|
||||
standardParams.push(startDate);
|
||||
}
|
||||
|
||||
if (endDate) {
|
||||
standardQuery += " AND sr.effective_date <= ?";
|
||||
standardParams.push(endDate);
|
||||
}
|
||||
|
||||
const contractorRates = await db.query<any[]>(
|
||||
contractorQuery,
|
||||
contractorParams,
|
||||
);
|
||||
const standardRates = await db.query<any[]>(
|
||||
standardQuery,
|
||||
standardParams,
|
||||
);
|
||||
|
||||
// Combine and sort by date
|
||||
const allRates = [...contractorRates, ...standardRates].sort((a, b) => {
|
||||
const dateA = new Date(a.effective_date).getTime();
|
||||
const dateB = new Date(b.effective_date).getTime();
|
||||
return dateB - dateA; // Descending order
|
||||
});
|
||||
|
||||
ctx.response.body = {
|
||||
allRates,
|
||||
summary: {
|
||||
totalContractorRates: contractorRates.length,
|
||||
totalStandardRates: standardRates.length,
|
||||
totalRates: allRates.length,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Get all rates error:", error);
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: "Internal server error" };
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Compare contractor rates with standard rates
|
||||
router.get("/compare", authenticateToken, authorize("Supervisor", "SuperAdmin"), async (ctx) => {
|
||||
try {
|
||||
const currentUser = getCurrentUser(ctx);
|
||||
const params = ctx.request.url.searchParams;
|
||||
const contractorId = params.get("contractorId");
|
||||
const subDepartmentId = params.get("subDepartmentId");
|
||||
|
||||
let departmentFilter = "";
|
||||
const queryParams: unknown[] = [];
|
||||
|
||||
if (currentUser.role === "Supervisor") {
|
||||
departmentFilter = " AND d.id = ?";
|
||||
queryParams.push(currentUser.departmentId);
|
||||
}
|
||||
|
||||
// Get standard rates
|
||||
let standardQuery = `
|
||||
router.get(
|
||||
"/compare",
|
||||
authenticateToken,
|
||||
authorize("Supervisor", "SuperAdmin"),
|
||||
async (ctx) => {
|
||||
try {
|
||||
const currentUser = getCurrentUser(ctx);
|
||||
const params = ctx.request.url.searchParams;
|
||||
const contractorId = params.get("contractorId");
|
||||
const subDepartmentId = params.get("subDepartmentId");
|
||||
|
||||
let departmentFilter = "";
|
||||
const queryParams: unknown[] = [];
|
||||
|
||||
if (currentUser.role === "Supervisor") {
|
||||
departmentFilter = " AND d.id = ?";
|
||||
queryParams.push(currentUser.departmentId);
|
||||
}
|
||||
|
||||
// Get standard rates
|
||||
let standardQuery = `
|
||||
SELECT sr.*,
|
||||
sd.name as sub_department_name,
|
||||
d.name as department_name,
|
||||
@@ -219,18 +241,21 @@ router.get("/compare", authenticateToken, authorize("Supervisor", "SuperAdmin"),
|
||||
LEFT JOIN activities a ON a.sub_department_id = sr.sub_department_id AND a.name = sr.activity
|
||||
WHERE 1=1 ${departmentFilter}
|
||||
`;
|
||||
|
||||
if (subDepartmentId) {
|
||||
standardQuery += " AND sr.sub_department_id = ?";
|
||||
queryParams.push(subDepartmentId);
|
||||
}
|
||||
|
||||
standardQuery += " ORDER BY sr.effective_date DESC";
|
||||
|
||||
const standardRates = await db.query<StandardRate[]>(standardQuery, queryParams);
|
||||
|
||||
// Get contractor rates for comparison
|
||||
let contractorQuery = `
|
||||
|
||||
if (subDepartmentId) {
|
||||
standardQuery += " AND sr.sub_department_id = ?";
|
||||
queryParams.push(subDepartmentId);
|
||||
}
|
||||
|
||||
standardQuery += " ORDER BY sr.effective_date DESC";
|
||||
|
||||
const standardRates = await db.query<StandardRate[]>(
|
||||
standardQuery,
|
||||
queryParams,
|
||||
);
|
||||
|
||||
// Get contractor rates for comparison
|
||||
let contractorQuery = `
|
||||
SELECT cr.*,
|
||||
u.name as contractor_name,
|
||||
sd.name as sub_department_name,
|
||||
@@ -244,103 +269,123 @@ router.get("/compare", authenticateToken, authorize("Supervisor", "SuperAdmin"),
|
||||
LEFT JOIN activities a ON a.sub_department_id = cr.sub_department_id AND a.name = cr.activity
|
||||
WHERE 1=1
|
||||
`;
|
||||
const contractorParams: unknown[] = [];
|
||||
|
||||
if (currentUser.role === "Supervisor") {
|
||||
contractorQuery += " AND d.id = ?";
|
||||
contractorParams.push(currentUser.departmentId);
|
||||
}
|
||||
|
||||
if (contractorId) {
|
||||
contractorQuery += " AND cr.contractor_id = ?";
|
||||
contractorParams.push(contractorId);
|
||||
}
|
||||
|
||||
if (subDepartmentId) {
|
||||
contractorQuery += " AND cr.sub_department_id = ?";
|
||||
contractorParams.push(subDepartmentId);
|
||||
}
|
||||
|
||||
contractorQuery += " ORDER BY cr.effective_date DESC";
|
||||
|
||||
const contractorRates = await db.query<any[]>(contractorQuery, contractorParams);
|
||||
|
||||
// Build comparison data
|
||||
const comparisons = contractorRates.map(cr => {
|
||||
// Find matching standard rate
|
||||
const matchingStandard = standardRates.find(sr =>
|
||||
sr.sub_department_id === cr.sub_department_id &&
|
||||
sr.activity === cr.activity
|
||||
const contractorParams: unknown[] = [];
|
||||
|
||||
if (currentUser.role === "Supervisor") {
|
||||
contractorQuery += " AND d.id = ?";
|
||||
contractorParams.push(currentUser.departmentId);
|
||||
}
|
||||
|
||||
if (contractorId) {
|
||||
contractorQuery += " AND cr.contractor_id = ?";
|
||||
contractorParams.push(contractorId);
|
||||
}
|
||||
|
||||
if (subDepartmentId) {
|
||||
contractorQuery += " AND cr.sub_department_id = ?";
|
||||
contractorParams.push(subDepartmentId);
|
||||
}
|
||||
|
||||
contractorQuery += " ORDER BY cr.effective_date DESC";
|
||||
|
||||
const contractorRates = await db.query<any[]>(
|
||||
contractorQuery,
|
||||
contractorParams,
|
||||
);
|
||||
|
||||
const standardRate = matchingStandard?.rate || 0;
|
||||
const contractorRate = cr.rate || 0;
|
||||
const difference = contractorRate - standardRate;
|
||||
const percentageDiff = standardRate > 0 ? ((difference / standardRate) * 100).toFixed(2) : null;
|
||||
|
||||
return {
|
||||
...cr,
|
||||
standard_rate: standardRate,
|
||||
difference,
|
||||
percentage_difference: percentageDiff,
|
||||
is_above_standard: difference > 0,
|
||||
is_below_standard: difference < 0,
|
||||
|
||||
// Build comparison data
|
||||
const comparisons = contractorRates.map((cr) => {
|
||||
// Find matching standard rate
|
||||
const matchingStandard = standardRates.find((sr) =>
|
||||
sr.sub_department_id === cr.sub_department_id &&
|
||||
sr.activity === cr.activity
|
||||
);
|
||||
|
||||
const standardRate = matchingStandard?.rate || 0;
|
||||
const contractorRate = cr.rate || 0;
|
||||
const difference = contractorRate - standardRate;
|
||||
const percentageDiff = standardRate > 0
|
||||
? ((difference / standardRate) * 100).toFixed(2)
|
||||
: null;
|
||||
|
||||
return {
|
||||
...cr,
|
||||
standard_rate: standardRate,
|
||||
difference,
|
||||
percentage_difference: percentageDiff,
|
||||
is_above_standard: difference > 0,
|
||||
is_below_standard: difference < 0,
|
||||
};
|
||||
});
|
||||
|
||||
ctx.response.body = {
|
||||
standardRates,
|
||||
contractorRates,
|
||||
comparisons,
|
||||
};
|
||||
});
|
||||
|
||||
ctx.response.body = {
|
||||
standardRates,
|
||||
contractorRates,
|
||||
comparisons,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Compare rates error:", error);
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: "Internal server error" };
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Compare rates error:", error);
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: "Internal server error" };
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Create standard rate (Supervisor or SuperAdmin)
|
||||
router.post("/", authenticateToken, authorize("Supervisor", "SuperAdmin"), async (ctx) => {
|
||||
try {
|
||||
const currentUser = getCurrentUser(ctx);
|
||||
const body = await ctx.request.body.json() as {
|
||||
subDepartmentId?: number;
|
||||
activity?: string;
|
||||
rate: number;
|
||||
effectiveDate: string;
|
||||
};
|
||||
const { subDepartmentId, activity, rate, effectiveDate } = body;
|
||||
|
||||
if (!rate || !effectiveDate) {
|
||||
ctx.response.status = 400;
|
||||
ctx.response.body = { error: "Missing required fields (rate, effectiveDate)" };
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify sub-department belongs to supervisor's department if supervisor
|
||||
if (subDepartmentId && currentUser.role === "Supervisor") {
|
||||
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 = ?",
|
||||
[subDepartmentId, currentUser.departmentId]
|
||||
);
|
||||
|
||||
if (subDepts.length === 0) {
|
||||
ctx.response.status = 403;
|
||||
ctx.response.body = { error: "Sub-department not in your department" };
|
||||
router.post(
|
||||
"/",
|
||||
authenticateToken,
|
||||
authorize("Supervisor", "SuperAdmin"),
|
||||
async (ctx) => {
|
||||
try {
|
||||
const currentUser = getCurrentUser(ctx);
|
||||
const body = await ctx.request.body.json() as {
|
||||
subDepartmentId?: number;
|
||||
activity?: string;
|
||||
rate: number;
|
||||
effectiveDate: string;
|
||||
};
|
||||
const { subDepartmentId, activity, rate, effectiveDate } = body;
|
||||
|
||||
if (!rate || !effectiveDate) {
|
||||
ctx.response.status = 400;
|
||||
ctx.response.body = {
|
||||
error: "Missing required fields (rate, effectiveDate)",
|
||||
};
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const sanitizedActivity = activity ? sanitizeInput(activity) : null;
|
||||
|
||||
const result = await db.execute(
|
||||
"INSERT INTO standard_rates (sub_department_id, activity, rate, effective_date, created_by) VALUES (?, ?, ?, ?, ?)",
|
||||
[subDepartmentId || null, sanitizedActivity, rate, effectiveDate, currentUser.id]
|
||||
);
|
||||
|
||||
const newRate = await db.query<StandardRate[]>(
|
||||
`SELECT sr.*,
|
||||
|
||||
// Verify sub-department belongs to supervisor's department if supervisor
|
||||
if (subDepartmentId && currentUser.role === "Supervisor") {
|
||||
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 = ?",
|
||||
[subDepartmentId, currentUser.departmentId],
|
||||
);
|
||||
|
||||
if (subDepts.length === 0) {
|
||||
ctx.response.status = 403;
|
||||
ctx.response.body = {
|
||||
error: "Sub-department not in your department",
|
||||
};
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const sanitizedActivity = activity ? sanitizeInput(activity) : null;
|
||||
|
||||
const result = await db.execute(
|
||||
"INSERT INTO standard_rates (sub_department_id, activity, rate, effective_date, created_by) VALUES (?, ?, ?, ?, ?)",
|
||||
[
|
||||
subDepartmentId || null,
|
||||
sanitizedActivity,
|
||||
rate,
|
||||
effectiveDate,
|
||||
currentUser.id,
|
||||
],
|
||||
);
|
||||
|
||||
const newRate = await db.query<StandardRate[]>(
|
||||
`SELECT sr.*,
|
||||
sd.name as sub_department_name,
|
||||
d.name as department_name,
|
||||
u.name as created_by_name,
|
||||
@@ -351,82 +396,96 @@ router.post("/", authenticateToken, authorize("Supervisor", "SuperAdmin"), async
|
||||
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
|
||||
WHERE sr.id = ?`,
|
||||
[result.insertId]
|
||||
);
|
||||
|
||||
ctx.response.status = 201;
|
||||
ctx.response.body = newRate[0];
|
||||
} catch (error) {
|
||||
console.error("Create standard rate error:", error);
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: "Internal server error" };
|
||||
}
|
||||
});
|
||||
[result.insertId],
|
||||
);
|
||||
|
||||
ctx.response.status = 201;
|
||||
ctx.response.body = newRate[0];
|
||||
} catch (error) {
|
||||
console.error("Create standard rate error:", error);
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: "Internal server error" };
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Update standard rate
|
||||
router.put("/:id", authenticateToken, authorize("Supervisor", "SuperAdmin"), async (ctx) => {
|
||||
try {
|
||||
const currentUser = getCurrentUser(ctx);
|
||||
const rateId = ctx.params.id;
|
||||
const body = await ctx.request.body.json() as { rate?: number; activity?: string; effectiveDate?: string };
|
||||
const { rate, activity, effectiveDate } = body;
|
||||
|
||||
// Verify rate exists and user has access
|
||||
let query = `
|
||||
router.put(
|
||||
"/:id",
|
||||
authenticateToken,
|
||||
authorize("Supervisor", "SuperAdmin"),
|
||||
async (ctx) => {
|
||||
try {
|
||||
const currentUser = getCurrentUser(ctx);
|
||||
const rateId = ctx.params.id;
|
||||
const body = await ctx.request.body.json() as {
|
||||
rate?: number;
|
||||
activity?: string;
|
||||
effectiveDate?: string;
|
||||
};
|
||||
const { rate, activity, effectiveDate } = body;
|
||||
|
||||
// Verify rate exists and user has access
|
||||
let query = `
|
||||
SELECT sr.*, d.id as department_id
|
||||
FROM standard_rates sr
|
||||
LEFT JOIN sub_departments sd ON sr.sub_department_id = sd.id
|
||||
LEFT JOIN departments d ON sd.department_id = d.id
|
||||
WHERE sr.id = ?
|
||||
`;
|
||||
const params: unknown[] = [rateId];
|
||||
|
||||
const existing = await db.query<any[]>(query, params);
|
||||
|
||||
if (existing.length === 0) {
|
||||
ctx.response.status = 404;
|
||||
ctx.response.body = { error: "Standard rate not found" };
|
||||
return;
|
||||
}
|
||||
|
||||
// Supervisors can only update rates in their department
|
||||
if (currentUser.role === "Supervisor" && existing[0].department_id !== currentUser.departmentId) {
|
||||
ctx.response.status = 403;
|
||||
ctx.response.body = { error: "Access denied - rate not in your department" };
|
||||
return;
|
||||
}
|
||||
|
||||
const updates: string[] = [];
|
||||
const updateParams: unknown[] = [];
|
||||
|
||||
if (rate !== undefined) {
|
||||
updates.push("rate = ?");
|
||||
updateParams.push(rate);
|
||||
}
|
||||
if (activity !== undefined) {
|
||||
updates.push("activity = ?");
|
||||
updateParams.push(sanitizeInput(activity));
|
||||
}
|
||||
if (effectiveDate !== undefined) {
|
||||
updates.push("effective_date = ?");
|
||||
updateParams.push(effectiveDate);
|
||||
}
|
||||
|
||||
if (updates.length === 0) {
|
||||
ctx.response.status = 400;
|
||||
ctx.response.body = { error: "No fields to update" };
|
||||
return;
|
||||
}
|
||||
|
||||
updateParams.push(rateId);
|
||||
|
||||
await db.execute(
|
||||
`UPDATE standard_rates SET ${updates.join(", ")} WHERE id = ?`,
|
||||
updateParams
|
||||
);
|
||||
|
||||
const updatedRate = await db.query<StandardRate[]>(
|
||||
`SELECT sr.*,
|
||||
const params: unknown[] = [rateId];
|
||||
|
||||
const existing = await db.query<any[]>(query, params);
|
||||
|
||||
if (existing.length === 0) {
|
||||
ctx.response.status = 404;
|
||||
ctx.response.body = { error: "Standard rate not found" };
|
||||
return;
|
||||
}
|
||||
|
||||
// Supervisors can only update rates in their department
|
||||
if (
|
||||
currentUser.role === "Supervisor" &&
|
||||
existing[0].department_id !== currentUser.departmentId
|
||||
) {
|
||||
ctx.response.status = 403;
|
||||
ctx.response.body = {
|
||||
error: "Access denied - rate not in your department",
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
const updates: string[] = [];
|
||||
const updateParams: unknown[] = [];
|
||||
|
||||
if (rate !== undefined) {
|
||||
updates.push("rate = ?");
|
||||
updateParams.push(rate);
|
||||
}
|
||||
if (activity !== undefined) {
|
||||
updates.push("activity = ?");
|
||||
updateParams.push(sanitizeInput(activity));
|
||||
}
|
||||
if (effectiveDate !== undefined) {
|
||||
updates.push("effective_date = ?");
|
||||
updateParams.push(effectiveDate);
|
||||
}
|
||||
|
||||
if (updates.length === 0) {
|
||||
ctx.response.status = 400;
|
||||
ctx.response.body = { error: "No fields to update" };
|
||||
return;
|
||||
}
|
||||
|
||||
updateParams.push(rateId);
|
||||
|
||||
await db.execute(
|
||||
`UPDATE standard_rates SET ${updates.join(", ")} WHERE id = ?`,
|
||||
updateParams,
|
||||
);
|
||||
|
||||
const updatedRate = await db.query<StandardRate[]>(
|
||||
`SELECT sr.*,
|
||||
sd.name as sub_department_name,
|
||||
d.name as department_name,
|
||||
u.name as created_by_name,
|
||||
@@ -437,53 +496,64 @@ router.put("/:id", authenticateToken, authorize("Supervisor", "SuperAdmin"), asy
|
||||
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
|
||||
WHERE sr.id = ?`,
|
||||
[rateId]
|
||||
);
|
||||
|
||||
ctx.response.body = updatedRate[0];
|
||||
} catch (error) {
|
||||
console.error("Update standard rate error:", error);
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: "Internal server error" };
|
||||
}
|
||||
});
|
||||
[rateId],
|
||||
);
|
||||
|
||||
ctx.response.body = updatedRate[0];
|
||||
} catch (error) {
|
||||
console.error("Update standard rate error:", error);
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: "Internal server error" };
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Delete standard rate
|
||||
router.delete("/:id", authenticateToken, authorize("Supervisor", "SuperAdmin"), async (ctx) => {
|
||||
try {
|
||||
const currentUser = getCurrentUser(ctx);
|
||||
const rateId = ctx.params.id;
|
||||
|
||||
// Verify rate exists and user has access
|
||||
const existing = await db.query<any[]>(
|
||||
`SELECT sr.*, d.id as department_id
|
||||
router.delete(
|
||||
"/:id",
|
||||
authenticateToken,
|
||||
authorize("Supervisor", "SuperAdmin"),
|
||||
async (ctx: Context) => {
|
||||
try {
|
||||
const currentUser = getCurrentUser(ctx);
|
||||
const rateId = ctx.params.id;
|
||||
|
||||
// Verify rate exists and user has access
|
||||
const existing = await db.query<any[]>(
|
||||
`SELECT sr.*, d.id as department_id
|
||||
FROM standard_rates sr
|
||||
LEFT JOIN sub_departments sd ON sr.sub_department_id = sd.id
|
||||
LEFT JOIN departments d ON sd.department_id = d.id
|
||||
WHERE sr.id = ?`,
|
||||
[rateId]
|
||||
);
|
||||
|
||||
if (existing.length === 0) {
|
||||
ctx.response.status = 404;
|
||||
ctx.response.body = { error: "Standard rate not found" };
|
||||
return;
|
||||
[rateId],
|
||||
);
|
||||
|
||||
if (existing.length === 0) {
|
||||
ctx.response.status = 404;
|
||||
ctx.response.body = { error: "Standard rate not found" };
|
||||
return;
|
||||
}
|
||||
|
||||
// Supervisors can only delete rates in their department
|
||||
if (
|
||||
currentUser.role === "Supervisor" &&
|
||||
existing[0].department_id !== currentUser.departmentId
|
||||
) {
|
||||
ctx.response.status = 403;
|
||||
ctx.response.body = {
|
||||
error: "Access denied - rate not in your department",
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
await db.execute("DELETE FROM standard_rates WHERE id = ?", [rateId]);
|
||||
ctx.response.body = { message: "Standard rate deleted successfully" };
|
||||
} catch (error) {
|
||||
console.error("Delete standard rate error:", error);
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: "Internal server error" };
|
||||
}
|
||||
|
||||
// Supervisors can only delete rates in their department
|
||||
if (currentUser.role === "Supervisor" && existing[0].department_id !== currentUser.departmentId) {
|
||||
ctx.response.status = 403;
|
||||
ctx.response.body = { error: "Access denied - rate not in your department" };
|
||||
return;
|
||||
}
|
||||
|
||||
await db.execute("DELETE FROM standard_rates WHERE id = ?", [rateId]);
|
||||
ctx.response.body = { message: "Standard rate deleted successfully" };
|
||||
} catch (error) {
|
||||
console.error("Delete standard rate error:", error);
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: "Internal server error" };
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Router } from "@oak/oak";
|
||||
import { hash, genSalt } from "bcrypt";
|
||||
import { type Context, Router } from "@oak/oak";
|
||||
import { genSalt, hash } from "bcrypt";
|
||||
import { db } from "../config/database.ts";
|
||||
import { config } from "../config/env.ts";
|
||||
|
||||
@@ -8,20 +8,28 @@ async function hashPassword(password: string): Promise<string> {
|
||||
const salt = await genSalt(config.BCRYPT_ROUNDS);
|
||||
return await hash(password, salt);
|
||||
}
|
||||
import { authenticateToken, authorize, getCurrentUser } from "../middleware/auth.ts";
|
||||
import { sanitizeInput, isValidEmail } from "../middleware/security.ts";
|
||||
import type { User, CreateUserRequest, UpdateUserRequest } from "../types/index.ts";
|
||||
import {
|
||||
authenticateToken,
|
||||
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();
|
||||
|
||||
// Get all users (with filters)
|
||||
router.get("/", authenticateToken, async (ctx) => {
|
||||
router.get("/", authenticateToken, async (ctx: Context) => {
|
||||
try {
|
||||
const currentUser = getCurrentUser(ctx);
|
||||
const params = ctx.request.url.searchParams;
|
||||
const role = params.get("role");
|
||||
const departmentId = params.get("departmentId");
|
||||
|
||||
|
||||
let query = `
|
||||
SELECT u.id, u.username, u.name, u.email, u.role, u.department_id,
|
||||
u.contractor_id, u.is_active, u.created_at,
|
||||
@@ -36,25 +44,25 @@ router.get("/", authenticateToken, async (ctx) => {
|
||||
WHERE 1=1
|
||||
`;
|
||||
const queryParams: unknown[] = [];
|
||||
|
||||
|
||||
// Supervisors can only see users in their department
|
||||
if (currentUser.role === "Supervisor") {
|
||||
query += " AND u.department_id = ?";
|
||||
queryParams.push(currentUser.departmentId);
|
||||
}
|
||||
|
||||
|
||||
if (role) {
|
||||
query += " AND u.role = ?";
|
||||
queryParams.push(role);
|
||||
}
|
||||
|
||||
|
||||
if (departmentId) {
|
||||
query += " AND u.department_id = ?";
|
||||
queryParams.push(departmentId);
|
||||
}
|
||||
|
||||
|
||||
query += " ORDER BY u.created_at DESC";
|
||||
|
||||
|
||||
const users = await db.query<User[]>(query, queryParams);
|
||||
ctx.response.body = users;
|
||||
} catch (error) {
|
||||
@@ -65,11 +73,11 @@ router.get("/", authenticateToken, async (ctx) => {
|
||||
});
|
||||
|
||||
// Get user by ID
|
||||
router.get("/:id", authenticateToken, async (ctx) => {
|
||||
router.get("/:id", authenticateToken, async (ctx: Context) => {
|
||||
try {
|
||||
const currentUser = getCurrentUser(ctx);
|
||||
const userId = ctx.params.id;
|
||||
|
||||
|
||||
const users = await db.query<User[]>(
|
||||
`SELECT u.id, u.username, u.name, u.email, u.role, u.department_id,
|
||||
u.contractor_id, u.is_active, u.created_at,
|
||||
@@ -82,22 +90,25 @@ router.get("/:id", authenticateToken, async (ctx) => {
|
||||
LEFT JOIN departments d ON u.department_id = d.id
|
||||
LEFT JOIN users c ON u.contractor_id = c.id
|
||||
WHERE u.id = ?`,
|
||||
[userId]
|
||||
[userId],
|
||||
);
|
||||
|
||||
|
||||
if (users.length === 0) {
|
||||
ctx.response.status = 404;
|
||||
ctx.response.body = { error: "User not found" };
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// 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.body = { error: "Access denied" };
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
ctx.response.body = users[0];
|
||||
} catch (error) {
|
||||
console.error("Get user error:", error);
|
||||
@@ -107,215 +118,98 @@ router.get("/:id", authenticateToken, async (ctx) => {
|
||||
});
|
||||
|
||||
// Create user
|
||||
router.post("/", authenticateToken, authorize("SuperAdmin", "Supervisor"), async (ctx) => {
|
||||
try {
|
||||
const currentUser = getCurrentUser(ctx);
|
||||
const body = await ctx.request.body.json() as CreateUserRequest;
|
||||
const {
|
||||
username, name, email, password, role, departmentId, contractorId,
|
||||
phoneNumber, aadharNumber, bankAccountNumber, bankName, bankIfsc,
|
||||
contractorAgreementNumber, pfNumber, esicNumber
|
||||
} = body;
|
||||
|
||||
// Input validation
|
||||
if (!username || !name || !email || !password || !role) {
|
||||
ctx.response.status = 400;
|
||||
ctx.response.body = { error: "Missing required fields" };
|
||||
return;
|
||||
}
|
||||
|
||||
// Sanitize inputs
|
||||
const sanitizedUsername = sanitizeInput(username);
|
||||
const sanitizedName = sanitizeInput(name);
|
||||
const sanitizedEmail = sanitizeInput(email);
|
||||
|
||||
// Validate email
|
||||
if (!isValidEmail(sanitizedEmail)) {
|
||||
ctx.response.status = 400;
|
||||
ctx.response.body = { error: "Invalid email format" };
|
||||
return;
|
||||
}
|
||||
|
||||
// Supervisors can only create users in their department
|
||||
if (currentUser.role === "Supervisor") {
|
||||
if (departmentId !== currentUser.departmentId) {
|
||||
ctx.response.status = 403;
|
||||
ctx.response.body = { error: "Can only create users in your department" };
|
||||
return;
|
||||
}
|
||||
if (role === "SuperAdmin" || role === "Supervisor") {
|
||||
ctx.response.status = 403;
|
||||
ctx.response.body = { error: "Cannot create admin or supervisor users" };
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Hash password
|
||||
const hashedPassword = await hashPassword(password);
|
||||
|
||||
const result = await db.execute(
|
||||
`INSERT INTO users (username, name, email, password, role, department_id, contractor_id,
|
||||
phone_number, aadhar_number, bank_account_number, bank_name, bank_ifsc,
|
||||
contractor_agreement_number, pf_number, esic_number)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[
|
||||
sanitizedUsername, sanitizedName, sanitizedEmail, hashedPassword, 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[]>(
|
||||
`SELECT u.id, u.username, u.name, u.email, u.role, u.department_id,
|
||||
u.contractor_id, u.is_active, u.created_at,
|
||||
u.phone_number, u.aadhar_number, u.bank_account_number,
|
||||
u.bank_name, u.bank_ifsc,
|
||||
u.contractor_agreement_number, u.pf_number, u.esic_number,
|
||||
d.name as department_name,
|
||||
c.name as contractor_name
|
||||
FROM users u
|
||||
LEFT JOIN departments d ON u.department_id = d.id
|
||||
LEFT JOIN users c ON u.contractor_id = c.id
|
||||
WHERE u.id = ?`,
|
||||
[result.insertId]
|
||||
);
|
||||
|
||||
ctx.response.status = 201;
|
||||
ctx.response.body = newUser[0];
|
||||
} catch (error) {
|
||||
const err = error as { code?: string };
|
||||
if (err.code === "ER_DUP_ENTRY") {
|
||||
ctx.response.status = 400;
|
||||
ctx.response.body = { error: "Username or email already exists" };
|
||||
return;
|
||||
}
|
||||
console.error("Create user error:", error);
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: "Internal server error" };
|
||||
}
|
||||
});
|
||||
router.post(
|
||||
"/",
|
||||
authenticateToken,
|
||||
authorize("SuperAdmin", "Supervisor"),
|
||||
async (ctx: Context) => {
|
||||
try {
|
||||
const currentUser = getCurrentUser(ctx);
|
||||
const body = await ctx.request.body.json() as CreateUserRequest;
|
||||
const {
|
||||
username,
|
||||
name,
|
||||
email,
|
||||
password,
|
||||
role,
|
||||
departmentId,
|
||||
contractorId,
|
||||
phoneNumber,
|
||||
aadharNumber,
|
||||
bankAccountNumber,
|
||||
bankName,
|
||||
bankIfsc,
|
||||
contractorAgreementNumber,
|
||||
pfNumber,
|
||||
esicNumber,
|
||||
} = body;
|
||||
|
||||
// Update user
|
||||
router.put("/:id", authenticateToken, authorize("SuperAdmin", "Supervisor"), async (ctx) => {
|
||||
try {
|
||||
const currentUser = getCurrentUser(ctx);
|
||||
const userId = ctx.params.id;
|
||||
const body = await ctx.request.body.json() as UpdateUserRequest;
|
||||
const {
|
||||
name, email, role, departmentId, contractorId, isActive,
|
||||
phoneNumber, aadharNumber, bankAccountNumber, bankName, bankIfsc,
|
||||
contractorAgreementNumber, pfNumber, esicNumber
|
||||
} = body;
|
||||
|
||||
// Check if user exists
|
||||
const existingUsers = await db.query<User[]>(
|
||||
"SELECT * FROM users WHERE id = ?",
|
||||
[userId]
|
||||
);
|
||||
|
||||
if (existingUsers.length === 0) {
|
||||
ctx.response.status = 404;
|
||||
ctx.response.body = { error: "User not found" };
|
||||
return;
|
||||
}
|
||||
|
||||
// Supervisors can only update users in their department
|
||||
if (currentUser.role === "Supervisor") {
|
||||
if (existingUsers[0].department_id !== currentUser.departmentId) {
|
||||
ctx.response.status = 403;
|
||||
ctx.response.body = { error: "Can only update users in your department" };
|
||||
// Input validation
|
||||
if (!username || !name || !email || !password || !role) {
|
||||
ctx.response.status = 400;
|
||||
ctx.response.body = { error: "Missing required fields" };
|
||||
return;
|
||||
}
|
||||
if (role === "SuperAdmin" || role === "Supervisor") {
|
||||
ctx.response.status = 403;
|
||||
ctx.response.body = { error: "Cannot modify admin or supervisor roles" };
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const updates: string[] = [];
|
||||
const params: unknown[] = [];
|
||||
|
||||
if (name !== undefined) {
|
||||
updates.push("name = ?");
|
||||
params.push(sanitizeInput(name));
|
||||
}
|
||||
if (email !== undefined) {
|
||||
if (!isValidEmail(email)) {
|
||||
|
||||
// Sanitize inputs
|
||||
const sanitizedUsername = sanitizeInput(username);
|
||||
const sanitizedName = sanitizeInput(name);
|
||||
const sanitizedEmail = sanitizeInput(email);
|
||||
|
||||
// Validate email
|
||||
if (!isValidEmail(sanitizedEmail)) {
|
||||
ctx.response.status = 400;
|
||||
ctx.response.body = { error: "Invalid email format" };
|
||||
return;
|
||||
}
|
||||
updates.push("email = ?");
|
||||
params.push(sanitizeInput(email));
|
||||
}
|
||||
if (role !== undefined) {
|
||||
updates.push("role = ?");
|
||||
params.push(role);
|
||||
}
|
||||
if (departmentId !== undefined) {
|
||||
updates.push("department_id = ?");
|
||||
params.push(departmentId);
|
||||
}
|
||||
if (contractorId !== undefined) {
|
||||
updates.push("contractor_id = ?");
|
||||
params.push(contractorId);
|
||||
}
|
||||
if (isActive !== undefined) {
|
||||
updates.push("is_active = ?");
|
||||
params.push(isActive);
|
||||
}
|
||||
// New fields
|
||||
if (phoneNumber !== undefined) {
|
||||
updates.push("phone_number = ?");
|
||||
params.push(phoneNumber);
|
||||
}
|
||||
if (aadharNumber !== undefined) {
|
||||
updates.push("aadhar_number = ?");
|
||||
params.push(aadharNumber);
|
||||
}
|
||||
if (bankAccountNumber !== undefined) {
|
||||
updates.push("bank_account_number = ?");
|
||||
params.push(bankAccountNumber);
|
||||
}
|
||||
if (bankName !== undefined) {
|
||||
updates.push("bank_name = ?");
|
||||
params.push(bankName);
|
||||
}
|
||||
if (bankIfsc !== undefined) {
|
||||
updates.push("bank_ifsc = ?");
|
||||
params.push(bankIfsc);
|
||||
}
|
||||
if (contractorAgreementNumber !== undefined) {
|
||||
updates.push("contractor_agreement_number = ?");
|
||||
params.push(contractorAgreementNumber);
|
||||
}
|
||||
if (pfNumber !== undefined) {
|
||||
updates.push("pf_number = ?");
|
||||
params.push(pfNumber);
|
||||
}
|
||||
if (esicNumber !== undefined) {
|
||||
updates.push("esic_number = ?");
|
||||
params.push(esicNumber);
|
||||
}
|
||||
|
||||
if (updates.length === 0) {
|
||||
ctx.response.status = 400;
|
||||
ctx.response.body = { error: "No fields to update" };
|
||||
return;
|
||||
}
|
||||
|
||||
params.push(userId);
|
||||
|
||||
await db.execute(
|
||||
`UPDATE users SET ${updates.join(", ")} WHERE id = ?`,
|
||||
params
|
||||
);
|
||||
|
||||
const updatedUser = await db.query<User[]>(
|
||||
`SELECT u.id, u.username, u.name, u.email, u.role, u.department_id,
|
||||
|
||||
// Supervisors can only create users in their department
|
||||
if (currentUser.role === "Supervisor") {
|
||||
if (departmentId !== currentUser.departmentId) {
|
||||
ctx.response.status = 403;
|
||||
ctx.response.body = {
|
||||
error: "Can only create users in your department",
|
||||
};
|
||||
return;
|
||||
}
|
||||
if (role === "SuperAdmin" || role === "Supervisor") {
|
||||
ctx.response.status = 403;
|
||||
ctx.response.body = {
|
||||
error: "Cannot create admin or supervisor users",
|
||||
};
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Hash password
|
||||
const hashedPassword = await hashPassword(password);
|
||||
|
||||
const result = await db.execute(
|
||||
`INSERT INTO users (username, name, email, password, role, department_id, contractor_id,
|
||||
phone_number, aadhar_number, bank_account_number, bank_name, bank_ifsc,
|
||||
contractor_agreement_number, pf_number, esic_number)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[
|
||||
sanitizedUsername,
|
||||
sanitizedName,
|
||||
sanitizedEmail,
|
||||
hashedPassword,
|
||||
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[]>(
|
||||
`SELECT u.id, u.username, u.name, u.email, u.role, u.department_id,
|
||||
u.contractor_id, u.is_active, u.created_at,
|
||||
u.phone_number, u.aadhar_number, u.bank_account_number,
|
||||
u.bank_name, u.bank_ifsc,
|
||||
@@ -326,55 +220,232 @@ router.put("/:id", authenticateToken, authorize("SuperAdmin", "Supervisor"), asy
|
||||
LEFT JOIN departments d ON u.department_id = d.id
|
||||
LEFT JOIN users c ON u.contractor_id = c.id
|
||||
WHERE u.id = ?`,
|
||||
[userId]
|
||||
);
|
||||
|
||||
ctx.response.body = updatedUser[0];
|
||||
} catch (error) {
|
||||
console.error("Update user error:", error);
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: "Internal server error" };
|
||||
}
|
||||
});
|
||||
[result.insertId],
|
||||
);
|
||||
|
||||
ctx.response.status = 201;
|
||||
ctx.response.body = newUser[0];
|
||||
} catch (error) {
|
||||
const err = error as { code?: string };
|
||||
if (err.code === "ER_DUP_ENTRY") {
|
||||
ctx.response.status = 400;
|
||||
ctx.response.body = { error: "Username or email already exists" };
|
||||
return;
|
||||
}
|
||||
console.error("Create user error:", error);
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: "Internal server error" };
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Update user
|
||||
router.put(
|
||||
"/:id",
|
||||
authenticateToken,
|
||||
authorize("SuperAdmin", "Supervisor"),
|
||||
async (ctx: Context) => {
|
||||
try {
|
||||
const currentUser = getCurrentUser(ctx);
|
||||
const userId = ctx.params.id;
|
||||
const body = await ctx.request.body.json() as UpdateUserRequest;
|
||||
const {
|
||||
name,
|
||||
email,
|
||||
role,
|
||||
departmentId,
|
||||
contractorId,
|
||||
isActive,
|
||||
phoneNumber,
|
||||
aadharNumber,
|
||||
bankAccountNumber,
|
||||
bankName,
|
||||
bankIfsc,
|
||||
contractorAgreementNumber,
|
||||
pfNumber,
|
||||
esicNumber,
|
||||
} = body;
|
||||
|
||||
// Check if user exists
|
||||
const existingUsers = await db.query<User[]>(
|
||||
"SELECT * FROM users WHERE id = ?",
|
||||
[userId],
|
||||
);
|
||||
|
||||
if (existingUsers.length === 0) {
|
||||
ctx.response.status = 404;
|
||||
ctx.response.body = { error: "User not found" };
|
||||
return;
|
||||
}
|
||||
|
||||
// Supervisors can only update users in their department
|
||||
if (currentUser.role === "Supervisor") {
|
||||
if (existingUsers[0].department_id !== currentUser.departmentId) {
|
||||
ctx.response.status = 403;
|
||||
ctx.response.body = {
|
||||
error: "Can only update users in your department",
|
||||
};
|
||||
return;
|
||||
}
|
||||
if (role === "SuperAdmin" || role === "Supervisor") {
|
||||
ctx.response.status = 403;
|
||||
ctx.response.body = {
|
||||
error: "Cannot modify admin or supervisor roles",
|
||||
};
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const updates: string[] = [];
|
||||
const params: unknown[] = [];
|
||||
|
||||
if (name !== undefined) {
|
||||
updates.push("name = ?");
|
||||
params.push(sanitizeInput(name));
|
||||
}
|
||||
if (email !== undefined) {
|
||||
if (!isValidEmail(email)) {
|
||||
ctx.response.status = 400;
|
||||
ctx.response.body = { error: "Invalid email format" };
|
||||
return;
|
||||
}
|
||||
updates.push("email = ?");
|
||||
params.push(sanitizeInput(email));
|
||||
}
|
||||
if (role !== undefined) {
|
||||
updates.push("role = ?");
|
||||
params.push(role);
|
||||
}
|
||||
if (departmentId !== undefined) {
|
||||
updates.push("department_id = ?");
|
||||
params.push(departmentId);
|
||||
}
|
||||
if (contractorId !== undefined) {
|
||||
updates.push("contractor_id = ?");
|
||||
params.push(contractorId);
|
||||
}
|
||||
if (isActive !== undefined) {
|
||||
updates.push("is_active = ?");
|
||||
params.push(isActive);
|
||||
}
|
||||
// New fields
|
||||
if (phoneNumber !== undefined) {
|
||||
updates.push("phone_number = ?");
|
||||
params.push(phoneNumber);
|
||||
}
|
||||
if (aadharNumber !== undefined) {
|
||||
updates.push("aadhar_number = ?");
|
||||
params.push(aadharNumber);
|
||||
}
|
||||
if (bankAccountNumber !== undefined) {
|
||||
updates.push("bank_account_number = ?");
|
||||
params.push(bankAccountNumber);
|
||||
}
|
||||
if (bankName !== undefined) {
|
||||
updates.push("bank_name = ?");
|
||||
params.push(bankName);
|
||||
}
|
||||
if (bankIfsc !== undefined) {
|
||||
updates.push("bank_ifsc = ?");
|
||||
params.push(bankIfsc);
|
||||
}
|
||||
if (contractorAgreementNumber !== undefined) {
|
||||
updates.push("contractor_agreement_number = ?");
|
||||
params.push(contractorAgreementNumber);
|
||||
}
|
||||
if (pfNumber !== undefined) {
|
||||
updates.push("pf_number = ?");
|
||||
params.push(pfNumber);
|
||||
}
|
||||
if (esicNumber !== undefined) {
|
||||
updates.push("esic_number = ?");
|
||||
params.push(esicNumber);
|
||||
}
|
||||
|
||||
if (updates.length === 0) {
|
||||
ctx.response.status = 400;
|
||||
ctx.response.body = { error: "No fields to update" };
|
||||
return;
|
||||
}
|
||||
|
||||
params.push(userId);
|
||||
|
||||
await db.execute(
|
||||
`UPDATE users SET ${updates.join(", ")} WHERE id = ?`,
|
||||
params,
|
||||
);
|
||||
|
||||
const updatedUser = await db.query<User[]>(
|
||||
`SELECT u.id, u.username, u.name, u.email, u.role, u.department_id,
|
||||
u.contractor_id, u.is_active, u.created_at,
|
||||
u.phone_number, u.aadhar_number, u.bank_account_number,
|
||||
u.bank_name, u.bank_ifsc,
|
||||
u.contractor_agreement_number, u.pf_number, u.esic_number,
|
||||
d.name as department_name,
|
||||
c.name as contractor_name
|
||||
FROM users u
|
||||
LEFT JOIN departments d ON u.department_id = d.id
|
||||
LEFT JOIN users c ON u.contractor_id = c.id
|
||||
WHERE u.id = ?`,
|
||||
[userId],
|
||||
);
|
||||
|
||||
ctx.response.body = updatedUser[0];
|
||||
} catch (error) {
|
||||
console.error("Update user error:", error);
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: "Internal server error" };
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Delete user
|
||||
router.delete("/:id", authenticateToken, authorize("SuperAdmin", "Supervisor"), async (ctx) => {
|
||||
try {
|
||||
const currentUser = getCurrentUser(ctx);
|
||||
const userId = ctx.params.id;
|
||||
|
||||
const users = await db.query<User[]>(
|
||||
"SELECT * FROM users WHERE id = ?",
|
||||
[userId]
|
||||
);
|
||||
|
||||
if (users.length === 0) {
|
||||
ctx.response.status = 404;
|
||||
ctx.response.body = { error: "User not found" };
|
||||
return;
|
||||
}
|
||||
|
||||
// Supervisors can only delete users in their department
|
||||
if (currentUser.role === "Supervisor") {
|
||||
if (users[0].department_id !== currentUser.departmentId) {
|
||||
ctx.response.status = 403;
|
||||
ctx.response.body = { error: "Can only delete users in your department" };
|
||||
router.delete(
|
||||
"/:id",
|
||||
authenticateToken,
|
||||
authorize("SuperAdmin", "Supervisor"),
|
||||
async (ctx) => {
|
||||
try {
|
||||
const currentUser = getCurrentUser(ctx);
|
||||
const userId = ctx.params.id;
|
||||
|
||||
const users = await db.query<User[]>(
|
||||
"SELECT * FROM users WHERE id = ?",
|
||||
[userId],
|
||||
);
|
||||
|
||||
if (users.length === 0) {
|
||||
ctx.response.status = 404;
|
||||
ctx.response.body = { error: "User not found" };
|
||||
return;
|
||||
}
|
||||
if (users[0].role === "SuperAdmin" || users[0].role === "Supervisor") {
|
||||
ctx.response.status = 403;
|
||||
ctx.response.body = { error: "Cannot delete admin or supervisor users" };
|
||||
return;
|
||||
|
||||
// Supervisors can only delete users in their department
|
||||
if (currentUser.role === "Supervisor") {
|
||||
if (users[0].department_id !== currentUser.departmentId) {
|
||||
ctx.response.status = 403;
|
||||
ctx.response.body = {
|
||||
error: "Can only delete users in your department",
|
||||
};
|
||||
return;
|
||||
}
|
||||
if (users[0].role === "SuperAdmin" || users[0].role === "Supervisor") {
|
||||
ctx.response.status = 403;
|
||||
ctx.response.body = {
|
||||
error: "Cannot delete admin or supervisor users",
|
||||
};
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await db.execute("DELETE FROM users WHERE id = ?", [userId]);
|
||||
ctx.response.body = { message: "User deleted successfully" };
|
||||
} catch (error) {
|
||||
console.error("Delete user error:", error);
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: "Internal server error" };
|
||||
}
|
||||
|
||||
await db.execute("DELETE FROM users WHERE id = ?", [userId]);
|
||||
ctx.response.body = { message: "User deleted successfully" };
|
||||
} catch (error) {
|
||||
console.error("Delete user error:", error);
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: "Internal server error" };
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -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 { authenticateToken, authorize, getCurrentUser } from "../middleware/auth.ts";
|
||||
import {
|
||||
authenticateToken,
|
||||
authorize,
|
||||
getCurrentUser,
|
||||
} from "../middleware/auth.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();
|
||||
|
||||
// Get all work allocations
|
||||
router.get("/", authenticateToken, async (ctx) => {
|
||||
try {
|
||||
const currentUser = getCurrentUser(ctx);
|
||||
const params = ctx.request.url.searchParams;
|
||||
const employeeId = params.get("employeeId");
|
||||
const status = params.get("status");
|
||||
const departmentId = params.get("departmentId");
|
||||
|
||||
let query = `
|
||||
router.get(
|
||||
"/",
|
||||
authenticateToken,
|
||||
async (
|
||||
ctx: RouterContext<"/", Record<string | number, string | undefined>, State>,
|
||||
) => {
|
||||
try {
|
||||
const currentUser: JWTPayload = getCurrentUser(ctx);
|
||||
const params: URLSearchParams = ctx.request.url.searchParams;
|
||||
const employeeId: string | null = params.get("employeeId");
|
||||
const status: string | null = params.get("status");
|
||||
const departmentId: string | null = params.get("departmentId");
|
||||
|
||||
let query: string = `
|
||||
SELECT wa.*,
|
||||
e.name as employee_name, e.username as employee_username,
|
||||
s.name as supervisor_name,
|
||||
@@ -30,52 +44,53 @@ router.get("/", authenticateToken, async (ctx) => {
|
||||
LEFT JOIN departments d ON e.department_id = d.id
|
||||
WHERE 1=1
|
||||
`;
|
||||
const queryParams: unknown[] = [];
|
||||
|
||||
// Role-based filtering
|
||||
if (currentUser.role === "Supervisor") {
|
||||
query += " AND wa.supervisor_id = ?";
|
||||
queryParams.push(currentUser.id);
|
||||
} else if (currentUser.role === "Employee") {
|
||||
query += " AND wa.employee_id = ?";
|
||||
queryParams.push(currentUser.id);
|
||||
} else if (currentUser.role === "Contractor") {
|
||||
query += " AND wa.contractor_id = ?";
|
||||
queryParams.push(currentUser.id);
|
||||
const queryParams: unknown[] = [];
|
||||
|
||||
// Role-based filtering
|
||||
if (currentUser.role === "Supervisor") {
|
||||
query += " AND wa.supervisor_id = ?";
|
||||
queryParams.push(currentUser.id);
|
||||
} else if (currentUser.role === "Employee") {
|
||||
query += " AND wa.employee_id = ?";
|
||||
queryParams.push(currentUser.id);
|
||||
} else if (currentUser.role === "Contractor") {
|
||||
query += " AND wa.contractor_id = ?";
|
||||
queryParams.push(currentUser.id);
|
||||
}
|
||||
|
||||
if (employeeId) {
|
||||
query += " AND wa.employee_id = ?";
|
||||
queryParams.push(employeeId);
|
||||
}
|
||||
|
||||
if (status) {
|
||||
query += " AND wa.status = ?";
|
||||
queryParams.push(status);
|
||||
}
|
||||
|
||||
if (departmentId) {
|
||||
query += " AND e.department_id = ?";
|
||||
queryParams.push(departmentId);
|
||||
}
|
||||
|
||||
query += " ORDER BY wa.assigned_date DESC, wa.created_at DESC";
|
||||
|
||||
const allocations = await db.query<WorkAllocation[]>(query, queryParams);
|
||||
ctx.response.body = allocations;
|
||||
} catch (error) {
|
||||
console.error("Get work allocations error:", error);
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: "Internal server error" };
|
||||
}
|
||||
|
||||
if (employeeId) {
|
||||
query += " AND wa.employee_id = ?";
|
||||
queryParams.push(employeeId);
|
||||
}
|
||||
|
||||
if (status) {
|
||||
query += " AND wa.status = ?";
|
||||
queryParams.push(status);
|
||||
}
|
||||
|
||||
if (departmentId) {
|
||||
query += " AND e.department_id = ?";
|
||||
queryParams.push(departmentId);
|
||||
}
|
||||
|
||||
query += " ORDER BY wa.assigned_date DESC, wa.created_at DESC";
|
||||
|
||||
const allocations = await db.query<WorkAllocation[]>(query, queryParams);
|
||||
ctx.response.body = allocations;
|
||||
} catch (error) {
|
||||
console.error("Get work allocations error:", error);
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: "Internal server error" };
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
// Get work allocation by ID
|
||||
router.get("/:id", authenticateToken, async (ctx) => {
|
||||
router.get("/:id", authenticateToken, async (ctx: RouterContext<"/:id">) => {
|
||||
try {
|
||||
const allocationId = ctx.params.id;
|
||||
|
||||
const allocations = await db.query<WorkAllocation[]>(
|
||||
const allocationId: string | undefined = ctx.params.id;
|
||||
|
||||
const allocations: WorkAllocation[] = await db.query<WorkAllocation[]>(
|
||||
`SELECT wa.*,
|
||||
e.name as employee_name, e.username as employee_username,
|
||||
s.name as supervisor_name,
|
||||
@@ -89,15 +104,15 @@ router.get("/:id", authenticateToken, async (ctx) => {
|
||||
LEFT JOIN sub_departments sd ON wa.sub_department_id = sd.id
|
||||
LEFT JOIN departments d ON e.department_id = d.id
|
||||
WHERE wa.id = ?`,
|
||||
[allocationId]
|
||||
[allocationId],
|
||||
);
|
||||
|
||||
|
||||
if (allocations.length === 0) {
|
||||
ctx.response.status = 404;
|
||||
ctx.response.body = { error: "Work allocation not found" };
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
ctx.response.body = allocations[0];
|
||||
} catch (error) {
|
||||
console.error("Get work allocation error:", error);
|
||||
@@ -107,57 +122,92 @@ router.get("/:id", authenticateToken, async (ctx) => {
|
||||
});
|
||||
|
||||
// Create work allocation (Supervisor or SuperAdmin)
|
||||
router.post("/", authenticateToken, authorize("Supervisor", "SuperAdmin"), async (ctx) => {
|
||||
try {
|
||||
const currentUser = getCurrentUser(ctx);
|
||||
const body = await ctx.request.body.json() as CreateWorkAllocationRequest;
|
||||
const { employeeId, contractorId, subDepartmentId, activity, description, assignedDate, rate, units, totalAmount, departmentId } = body;
|
||||
|
||||
if (!employeeId || !contractorId || !assignedDate) {
|
||||
ctx.response.status = 400;
|
||||
ctx.response.body = { error: "Missing required fields" };
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify employee exists
|
||||
let employeeQuery = "SELECT * FROM users WHERE id = ?";
|
||||
const employeeParams: unknown[] = [employeeId];
|
||||
|
||||
if (currentUser.role === "Supervisor") {
|
||||
employeeQuery += " AND department_id = ?";
|
||||
employeeParams.push(currentUser.departmentId);
|
||||
}
|
||||
|
||||
const employees = await db.query<{ id: number }[]>(employeeQuery, employeeParams);
|
||||
|
||||
if (employees.length === 0) {
|
||||
ctx.response.status = 403;
|
||||
ctx.response.body = { error: "Employee not found or not in your department" };
|
||||
return;
|
||||
}
|
||||
|
||||
// Use provided rate or get contractor's current rate
|
||||
let finalRate = rate;
|
||||
if (!finalRate) {
|
||||
const rates = await db.query<ContractorRate[]>(
|
||||
"SELECT rate FROM contractor_rates WHERE contractor_id = ? ORDER BY effective_date DESC LIMIT 1",
|
||||
[contractorId]
|
||||
router.post(
|
||||
"/",
|
||||
authenticateToken,
|
||||
authorize("Supervisor", "SuperAdmin"),
|
||||
async (
|
||||
ctx: RouterContext<"/", Record<string | number, string | undefined>, State>,
|
||||
) => {
|
||||
try {
|
||||
const currentUser = getCurrentUser(ctx);
|
||||
const body = await ctx.request.body.json() as CreateWorkAllocationRequest;
|
||||
const {
|
||||
employeeId,
|
||||
contractorId,
|
||||
subDepartmentId,
|
||||
activity,
|
||||
description,
|
||||
assignedDate,
|
||||
rate,
|
||||
units,
|
||||
totalAmount,
|
||||
departmentId,
|
||||
} = body;
|
||||
|
||||
if (!employeeId || !contractorId || !assignedDate) {
|
||||
ctx.response.status = 400;
|
||||
ctx.response.body = { error: "Missing required fields" };
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify employee exists
|
||||
let employeeQuery = "SELECT * FROM users WHERE id = ?";
|
||||
const employeeParams: unknown[] = [employeeId];
|
||||
|
||||
if (currentUser.role === "Supervisor") {
|
||||
employeeQuery += " AND department_id = ?";
|
||||
employeeParams.push(currentUser.departmentId);
|
||||
}
|
||||
|
||||
const employees = await db.query<{ id: number }[]>(
|
||||
employeeQuery,
|
||||
employeeParams,
|
||||
);
|
||||
finalRate = rates.length > 0 ? rates[0].rate : null;
|
||||
}
|
||||
|
||||
const sanitizedActivity = activity ? sanitizeInput(activity) : null;
|
||||
const sanitizedDescription = description ? sanitizeInput(description) : null;
|
||||
|
||||
const result = await db.execute(
|
||||
`INSERT INTO work_allocations
|
||||
|
||||
if (employees.length === 0) {
|
||||
ctx.response.status = 403;
|
||||
ctx.response.body = {
|
||||
error: "Employee not found or not in your department",
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
// Use provided rate or get contractor's current rate
|
||||
let finalRate = rate;
|
||||
if (!finalRate) {
|
||||
const rates = await db.query<ContractorRate[]>(
|
||||
"SELECT rate FROM contractor_rates WHERE contractor_id = ? ORDER BY effective_date DESC LIMIT 1",
|
||||
[contractorId],
|
||||
);
|
||||
finalRate = rates.length > 0 ? rates[0].rate : null;
|
||||
}
|
||||
|
||||
const sanitizedActivity = activity ? sanitizeInput(activity) : null;
|
||||
const sanitizedDescription = description
|
||||
? sanitizeInput(description)
|
||||
: null;
|
||||
|
||||
const result = await db.execute(
|
||||
`INSERT INTO work_allocations
|
||||
(employee_id, supervisor_id, contractor_id, sub_department_id, activity, description, assigned_date, rate, units, total_amount)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[employeeId, currentUser.id, contractorId, subDepartmentId || null, sanitizedActivity, sanitizedDescription, assignedDate, finalRate, units || null, totalAmount || null]
|
||||
);
|
||||
|
||||
const newAllocation = await db.query<WorkAllocation[]>(
|
||||
`SELECT wa.*,
|
||||
[
|
||||
employeeId,
|
||||
currentUser.id,
|
||||
contractorId,
|
||||
subDepartmentId || null,
|
||||
sanitizedActivity,
|
||||
sanitizedDescription,
|
||||
assignedDate,
|
||||
finalRate,
|
||||
units || null,
|
||||
totalAmount || null,
|
||||
],
|
||||
);
|
||||
|
||||
const newAllocation = await db.query<WorkAllocation[]>(
|
||||
`SELECT wa.*,
|
||||
e.name as employee_name, e.username as employee_username,
|
||||
s.name as supervisor_name,
|
||||
c.name as contractor_name,
|
||||
@@ -170,56 +220,66 @@ router.post("/", authenticateToken, authorize("Supervisor", "SuperAdmin"), async
|
||||
LEFT JOIN sub_departments sd ON wa.sub_department_id = sd.id
|
||||
LEFT JOIN departments d ON e.department_id = d.id
|
||||
WHERE wa.id = ?`,
|
||||
[result.insertId]
|
||||
);
|
||||
|
||||
ctx.response.status = 201;
|
||||
ctx.response.body = newAllocation[0];
|
||||
} catch (error) {
|
||||
console.error("Create work allocation error:", error);
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: "Internal server error" };
|
||||
}
|
||||
});
|
||||
[result.insertId],
|
||||
);
|
||||
|
||||
ctx.response.status = 201;
|
||||
ctx.response.body = newAllocation[0];
|
||||
} catch (error) {
|
||||
console.error("Create work allocation error:", error);
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: "Internal server error" };
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Update work allocation status (Supervisor or SuperAdmin)
|
||||
router.put("/:id/status", authenticateToken, authorize("Supervisor", "SuperAdmin"), async (ctx) => {
|
||||
try {
|
||||
const currentUser = getCurrentUser(ctx);
|
||||
const allocationId = ctx.params.id;
|
||||
const body = await ctx.request.body.json() as { status: string; completionDate?: string };
|
||||
const { status, completionDate } = body;
|
||||
|
||||
if (!status) {
|
||||
ctx.response.status = 400;
|
||||
ctx.response.body = { error: "Status required" };
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify allocation exists and user has access
|
||||
let query = "SELECT * FROM work_allocations WHERE id = ?";
|
||||
const params: unknown[] = [allocationId];
|
||||
|
||||
if (currentUser.role === "Supervisor") {
|
||||
query += " AND supervisor_id = ?";
|
||||
params.push(currentUser.id);
|
||||
}
|
||||
|
||||
const allocations = await db.query<WorkAllocation[]>(query, params);
|
||||
|
||||
if (allocations.length === 0) {
|
||||
ctx.response.status = 403;
|
||||
ctx.response.body = { error: "Work allocation not found or access denied" };
|
||||
return;
|
||||
}
|
||||
|
||||
await db.execute(
|
||||
"UPDATE work_allocations SET status = ?, completion_date = ? WHERE id = ?",
|
||||
[status, completionDate || null, allocationId]
|
||||
);
|
||||
|
||||
const updatedAllocation = await db.query<WorkAllocation[]>(
|
||||
`SELECT wa.*,
|
||||
router.put(
|
||||
"/:id/status",
|
||||
authenticateToken,
|
||||
authorize("Supervisor", "SuperAdmin"),
|
||||
async (ctx) => {
|
||||
try {
|
||||
const currentUser = getCurrentUser(ctx);
|
||||
const allocationId = ctx.params.id;
|
||||
const body = await ctx.request.body.json() as {
|
||||
status: string;
|
||||
completionDate?: string;
|
||||
};
|
||||
const { status, completionDate } = body;
|
||||
|
||||
if (!status) {
|
||||
ctx.response.status = 400;
|
||||
ctx.response.body = { error: "Status required" };
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify allocation exists and user has access
|
||||
let query = "SELECT * FROM work_allocations WHERE id = ?";
|
||||
const params: unknown[] = [allocationId];
|
||||
|
||||
if (currentUser.role === "Supervisor") {
|
||||
query += " AND supervisor_id = ?";
|
||||
params.push(currentUser.id);
|
||||
}
|
||||
|
||||
const allocations = await db.query<WorkAllocation[]>(query, params);
|
||||
|
||||
if (allocations.length === 0) {
|
||||
ctx.response.status = 403;
|
||||
ctx.response.body = {
|
||||
error: "Work allocation not found or access denied",
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
await db.execute(
|
||||
"UPDATE work_allocations SET status = ?, completion_date = ? WHERE id = ?",
|
||||
[status, completionDate || null, allocationId],
|
||||
);
|
||||
|
||||
const updatedAllocation = await db.query<WorkAllocation[]>(
|
||||
`SELECT wa.*,
|
||||
e.name as employee_name, e.username as employee_username,
|
||||
s.name as supervisor_name,
|
||||
c.name as contractor_name,
|
||||
@@ -232,47 +292,57 @@ router.put("/:id/status", authenticateToken, authorize("Supervisor", "SuperAdmin
|
||||
LEFT JOIN sub_departments sd ON wa.sub_department_id = sd.id
|
||||
LEFT JOIN departments d ON e.department_id = d.id
|
||||
WHERE wa.id = ?`,
|
||||
[allocationId]
|
||||
);
|
||||
|
||||
ctx.response.body = updatedAllocation[0];
|
||||
} catch (error) {
|
||||
console.error("Update work allocation error:", error);
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: "Internal server error" };
|
||||
}
|
||||
});
|
||||
[allocationId],
|
||||
);
|
||||
|
||||
ctx.response.body = updatedAllocation[0];
|
||||
} catch (error) {
|
||||
console.error("Update work allocation error:", error);
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: "Internal server error" };
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Delete work allocation (Supervisor or SuperAdmin)
|
||||
router.delete("/:id", authenticateToken, authorize("Supervisor", "SuperAdmin"), async (ctx) => {
|
||||
try {
|
||||
const currentUser = getCurrentUser(ctx);
|
||||
const allocationId = ctx.params.id;
|
||||
|
||||
// Verify allocation exists and user has access
|
||||
let query = "SELECT * FROM work_allocations WHERE id = ?";
|
||||
const params: unknown[] = [allocationId];
|
||||
|
||||
if (currentUser.role === "Supervisor") {
|
||||
query += " AND supervisor_id = ?";
|
||||
params.push(currentUser.id);
|
||||
router.delete(
|
||||
"/:id",
|
||||
authenticateToken,
|
||||
authorize("Supervisor", "SuperAdmin"),
|
||||
async (ctx) => {
|
||||
try {
|
||||
const currentUser = getCurrentUser(ctx);
|
||||
const allocationId = ctx.params.id;
|
||||
|
||||
// Verify allocation exists and user has access
|
||||
let query = "SELECT * FROM work_allocations WHERE id = ?";
|
||||
const params: unknown[] = [allocationId];
|
||||
|
||||
if (currentUser.role === "Supervisor") {
|
||||
query += " AND supervisor_id = ?";
|
||||
params.push(currentUser.id);
|
||||
}
|
||||
|
||||
const allocations = await db.query<WorkAllocation[]>(query, params);
|
||||
|
||||
if (allocations.length === 0) {
|
||||
ctx.response.status = 403;
|
||||
ctx.response.body = {
|
||||
error: "Work allocation not found or access denied",
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
await db.execute("DELETE FROM work_allocations WHERE id = ?", [
|
||||
allocationId,
|
||||
]);
|
||||
ctx.response.body = { message: "Work allocation deleted successfully" };
|
||||
} catch (error) {
|
||||
console.error("Delete work allocation error:", error);
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: "Internal server error" };
|
||||
}
|
||||
|
||||
const allocations = await db.query<WorkAllocation[]>(query, params);
|
||||
|
||||
if (allocations.length === 0) {
|
||||
ctx.response.status = 403;
|
||||
ctx.response.body = { error: "Work allocation not found or access denied" };
|
||||
return;
|
||||
}
|
||||
|
||||
await db.execute("DELETE FROM work_allocations WHERE id = ?", [allocationId]);
|
||||
ctx.response.body = { message: "Work allocation deleted successfully" };
|
||||
} catch (error) {
|
||||
console.error("Delete work allocation error:", error);
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: "Internal server error" };
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
export default router;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -51,7 +51,11 @@ export interface SubDepartment {
|
||||
}
|
||||
|
||||
// Work allocation types
|
||||
export type AllocationStatus = "Pending" | "InProgress" | "Completed" | "Cancelled";
|
||||
export type AllocationStatus =
|
||||
| "Pending"
|
||||
| "InProgress"
|
||||
| "Completed"
|
||||
| "Cancelled";
|
||||
|
||||
export interface WorkAllocation {
|
||||
id: number;
|
||||
@@ -76,7 +80,12 @@ export interface WorkAllocation {
|
||||
}
|
||||
|
||||
// Attendance types
|
||||
export type AttendanceStatus = "CheckedIn" | "CheckedOut" | "Absent" | "HalfDay" | "Late";
|
||||
export type AttendanceStatus =
|
||||
| "CheckedIn"
|
||||
| "CheckedOut"
|
||||
| "Absent"
|
||||
| "HalfDay"
|
||||
| "Late";
|
||||
|
||||
export interface Attendance {
|
||||
id: number;
|
||||
|
||||
Reference in New Issue
Block a user