Compare commits
No commits in common. "bf85053ba43cc92b7f77c4e37204581e0afe179c" and "c8c645f7a6b9819e95a038359df02b89ded6fec5" have entirely different histories.
bf85053ba4
...
c8c645f7a6
@ -4,33 +4,50 @@ const pool = getDbPool("hrms_ems");
|
|||||||
|
|
||||||
export const getEmployeeContracts = async (ctx: any) => {
|
export const getEmployeeContracts = async (ctx: any) => {
|
||||||
const employeeId = ctx.params.id;
|
const employeeId = ctx.params.id;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Fetches combined history of terms and assignments
|
|
||||||
const [rows] = await pool.query(
|
const [rows] = await pool.query(
|
||||||
`SELECT
|
`SELECT
|
||||||
et.term_id, et.work_email, et.date_joining, et.probation_days, et.status, et.salary_structure_id,
|
c.contract_id,
|
||||||
ea.assignment_id, ea.change_reason, ea.effective_from, ea.effective_to, ea.is_current,
|
c.work_email,
|
||||||
d.name AS department, j.title AS designation
|
c.date_joining,
|
||||||
FROM employment_terms et
|
c.probation_days,
|
||||||
JOIN employee_assignments ea ON et.employee_id = ea.employee_id
|
c.status,
|
||||||
JOIN departments d ON ea.department_id = d.department_id
|
c.salary_structure_id,
|
||||||
JOIN job_positions j ON ea.job_id = j.job_id
|
d.name AS department,
|
||||||
WHERE et.employee_id = ?
|
j.title AS designation
|
||||||
ORDER BY et.date_joining DESC, ea.effective_from DESC`,
|
FROM contracts c
|
||||||
|
JOIN employees e ON c.employee_id = e.employee_id
|
||||||
|
JOIN departments d ON c.department_id = d.department_id
|
||||||
|
JOIN job_positions j ON c.job_id = j.job_id
|
||||||
|
WHERE c.employee_id = ?
|
||||||
|
ORDER BY c.date_joining DESC`,
|
||||||
[employeeId]
|
[employeeId]
|
||||||
);
|
);
|
||||||
|
|
||||||
ctx.response.status = 200;
|
ctx.response.status = 200;
|
||||||
ctx.response.body = { success: true, count: (rows as any[]).length, data: rows };
|
ctx.response.body = {
|
||||||
|
success: true,
|
||||||
|
count: (rows as any[]).length,
|
||||||
|
data: rows
|
||||||
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ctx.response.status = 500;
|
ctx.response.status = 500;
|
||||||
ctx.response.body = { success: false, error: (error as Error).message };
|
ctx.response.body = {
|
||||||
|
success: false,
|
||||||
|
error: (error as Error).message
|
||||||
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createContract = async (ctx: any) => {
|
export const createContract = async (ctx: any) => {
|
||||||
const body = ctx.request.body;
|
const body = ctx.request.body;
|
||||||
if (body.type() !== "json") { ctx.response.status = 400; ctx.response.body = { success: false, error: "Invalid JSON body" }; return; }
|
|
||||||
|
if (body.type() !== "json") {
|
||||||
|
ctx.response.status = 400;
|
||||||
|
ctx.response.body = { success: false, error: "Invalid JSON body" };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const data = await body.json();
|
const data = await body.json();
|
||||||
const connection = await pool.getConnection();
|
const connection = await pool.getConnection();
|
||||||
@ -38,31 +55,46 @@ export const createContract = async (ctx: any) => {
|
|||||||
try {
|
try {
|
||||||
await connection.beginTransaction();
|
await connection.beginTransaction();
|
||||||
|
|
||||||
// 1. Expire old assignment
|
// 1. Expire any currently active contracts for this specific employee
|
||||||
await connection.execute(
|
await connection.execute(
|
||||||
`UPDATE employee_assignments SET is_current = FALSE, effective_to = CURDATE() WHERE employee_id = ? AND is_current = TRUE`,
|
`UPDATE contracts SET status = 'EXPIRED' WHERE employee_id = ? AND status = 'ACTIVE'`,
|
||||||
[data.employeeId]
|
[data.employeeId]
|
||||||
);
|
);
|
||||||
|
|
||||||
// 2. Insert new assignment (Promotion/Transfer)
|
// 2. Insert the brand new active contract
|
||||||
const [result] = await connection.execute(
|
const [result] = await connection.execute(
|
||||||
`INSERT INTO employee_assignments (employee_id, department_id, job_id, reporting_to_id, change_reason, effective_from, is_current)
|
`INSERT INTO contracts (employee_id, department_id, job_id, work_email, reporting_to_id, date_joining, probation_days, status, salary_structure_id)
|
||||||
VALUES (?, ?, ?, ?, ?, CURDATE(), TRUE)`,
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
[data.employeeId, data.departmentId, data.jobId, data.reportingToId || null, data.changeReason || 'TRANSFER']
|
[
|
||||||
|
data.employeeId,
|
||||||
|
data.departmentId,
|
||||||
|
data.jobId,
|
||||||
|
data.workEmail, // <-- Added
|
||||||
|
data.reportingToId, // <-- Added
|
||||||
|
data.dateJoining,
|
||||||
|
data.probationDays,
|
||||||
|
'ACTIVE',
|
||||||
|
data.salaryStructureId
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Optional: If compensation changes, you would expire old employment_terms and insert new ones here.
|
|
||||||
|
|
||||||
await connection.commit();
|
await connection.commit();
|
||||||
|
|
||||||
ctx.response.status = 201;
|
ctx.response.status = 201;
|
||||||
ctx.response.body = {
|
ctx.response.body = {
|
||||||
success: true,
|
success: true,
|
||||||
message: "New assignment executed successfully. Previous assignments expired.",
|
message: "New contract executed successfully. Previous contracts expired.",
|
||||||
assignment_id: (result as any).insertId
|
contract_id: (result as any).insertId
|
||||||
};
|
};
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await connection.rollback();
|
await connection.rollback();
|
||||||
ctx.response.status = 500;
|
ctx.response.status = 500;
|
||||||
ctx.response.body = { success: false, error: "Transaction failed: " + (error as Error).message };
|
ctx.response.body = {
|
||||||
} finally { connection.release(); }
|
success: false,
|
||||||
|
error: "Transaction failed: " + (error as Error).message
|
||||||
|
};
|
||||||
|
} finally {
|
||||||
|
connection.release();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
@ -1,66 +0,0 @@
|
|||||||
// dashboard.controller.ts
|
|
||||||
import { getDbPool } from "../../shared/db.ts";
|
|
||||||
|
|
||||||
const pool = getDbPool("hrms_ems");
|
|
||||||
|
|
||||||
export const getDashboardMetrics = async (ctx: any) => {
|
|
||||||
try {
|
|
||||||
const params = ctx.request.url.searchParams;
|
|
||||||
const companyId = params.get('company_id');
|
|
||||||
const branchId = params.get('branch_id');
|
|
||||||
const departmentId = params.get('department_id');
|
|
||||||
|
|
||||||
// Build dynamic WHERE clauses for each entity type based on the filters provided
|
|
||||||
let companyFilter = " WHERE 1=1";
|
|
||||||
let branchFilter = " WHERE 1=1";
|
|
||||||
let deptFilter = " WHERE 1=1";
|
|
||||||
let jobFilter = " WHERE 1=1";
|
|
||||||
let empFilter = " WHERE e.is_active = TRUE";
|
|
||||||
|
|
||||||
const valuesC: any[] = [];
|
|
||||||
const valuesB: any[] = [];
|
|
||||||
const valuesD: any[] = [];
|
|
||||||
const valuesJ: any[] = [];
|
|
||||||
const valuesE: any[] = [];
|
|
||||||
|
|
||||||
if (companyId) {
|
|
||||||
branchFilter += " AND company_id = ?"; valuesB.push(companyId);
|
|
||||||
deptFilter += " AND company_id = ?"; valuesD.push(companyId);
|
|
||||||
jobFilter += " AND d.company_id = ?"; valuesJ.push(companyId);
|
|
||||||
empFilter += " AND e.company_id = ?"; valuesE.push(companyId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (branchId) {
|
|
||||||
deptFilter += " AND branch_id = ?"; valuesD.push(branchId);
|
|
||||||
jobFilter += " AND d.branch_id = ?"; valuesJ.push(branchId);
|
|
||||||
empFilter += " AND e.branch_id = ?"; valuesE.push(branchId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (departmentId) {
|
|
||||||
jobFilter += " AND j.department_id = ?"; valuesJ.push(departmentId);
|
|
||||||
empFilter += " AND ea.department_id = ?"; valuesE.push(departmentId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Execute all 5 count queries in parallel for maximum speed
|
|
||||||
const [cRows]: any = await pool.query(`SELECT COUNT(*) as count FROM companies ${companyFilter}`, valuesC);
|
|
||||||
const [bRows]: any = await pool.query(`SELECT COUNT(*) as count FROM branches ${branchFilter}`, valuesB);
|
|
||||||
const [dRows]: any = await pool.query(`SELECT COUNT(*) as count FROM departments ${deptFilter}`, valuesD);
|
|
||||||
const [jRows]: any = await pool.query(`SELECT COUNT(*) as count FROM job_positions j JOIN departments d ON j.department_id = d.department_id ${jobFilter}`, valuesJ);
|
|
||||||
const [eRows]: any = await pool.query(`SELECT COUNT(*) as count FROM employees e JOIN employee_assignments ea ON e.employee_id = ea.employee_id AND ea.is_current = TRUE ${empFilter}`, valuesE);
|
|
||||||
|
|
||||||
ctx.response.body = {
|
|
||||||
success: true,
|
|
||||||
data: {
|
|
||||||
total_companies: cRows[0].count,
|
|
||||||
total_branches: bRows[0].count,
|
|
||||||
total_departments: dRows[0].count,
|
|
||||||
total_jobs: jRows[0].count,
|
|
||||||
total_employees: eRows[0].count
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
ctx.response.status = 500;
|
|
||||||
ctx.response.body = { success: false, error: (error as Error).message };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@ -5,158 +5,229 @@ const pool = getDbPool("hrms_ems");
|
|||||||
|
|
||||||
export const getEmployees = async (ctx: any) => {
|
export const getEmployees = async (ctx: any) => {
|
||||||
try {
|
try {
|
||||||
const params = ctx.request.url.searchParams;
|
// FIX: Filter by ACTIVE contract to prevent duplicate rows for employees with past contracts
|
||||||
let query = `SELECT e.employee_id, e.employee_code, e.is_active,
|
const [rows] = await pool.query(
|
||||||
CONCAT(p.first_name, ' ', p.last_name) AS full_name,
|
`SELECT
|
||||||
et.work_email, -- <-- ADDED WORK EMAIL HERE
|
e.employee_id,
|
||||||
j.title AS job_name, d.name AS department_name,
|
e.employee_code,
|
||||||
b.branch_name, c.name AS company_name
|
p.first_name,
|
||||||
|
p.last_name,
|
||||||
|
d.name AS department,
|
||||||
|
j.title AS designation,
|
||||||
|
c.work_email
|
||||||
FROM employees e
|
FROM employees e
|
||||||
JOIN partners p ON e.partner_id = p.partner_id
|
JOIN partners p ON e.partner_id = p.partner_id
|
||||||
JOIN companies c ON e.company_id = c.company_id
|
JOIN contracts c ON e.employee_id = c.employee_id AND c.status = 'ACTIVE'
|
||||||
JOIN branches b ON e.branch_id = b.branch_id
|
JOIN departments d ON c.department_id = d.department_id
|
||||||
JOIN employment_terms et ON e.employee_id = et.employee_id AND et.status = 'ACTIVE' -- <-- ADDED JOIN
|
JOIN job_positions j ON c.job_id = j.job_id
|
||||||
JOIN employee_assignments ea ON e.employee_id = ea.employee_id AND ea.is_current = TRUE
|
WHERE e.is_active = true`,
|
||||||
JOIN departments d ON ea.department_id = d.department_id
|
);
|
||||||
JOIN job_positions j ON ea.job_id = j.job_id
|
|
||||||
WHERE 1=1`;
|
|
||||||
const values: any[] = [];
|
|
||||||
|
|
||||||
if (params.get('company_id')) { query += ` AND e.company_id = ?`; values.push(params.get('company_id')); }
|
ctx.response.body = {
|
||||||
if (params.get('branch_id')) { query += ` AND e.branch_id = ?`; values.push(params.get('branch_id')); }
|
success: true,
|
||||||
if (params.get('department_id')) { query += ` AND ea.department_id = ?`; values.push(params.get('department_id')); }
|
count: (rows as any[]).length,
|
||||||
if (params.get('is_active')) { query += ` AND e.is_active = ?`; values.push(params.get('is_active') === 'true'); }
|
data: rows,
|
||||||
|
};
|
||||||
const [rows] = await pool.query(query, values);
|
|
||||||
ctx.response.body = { success: true, count: (rows as any[]).length, data: rows };
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ctx.response.status = 500;
|
ctx.response.status = 500;
|
||||||
ctx.response.body = { success: false, error: (error as Error).message };
|
ctx.response.body = {
|
||||||
|
success: false,
|
||||||
|
error: (error as Error).message,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createEmployee = async (ctx: any) => {
|
export const createEmployee = async (ctx: any) => {
|
||||||
const body = ctx.request.body;
|
const body = ctx.request.body;
|
||||||
if (body.type() !== "json") { ctx.response.status = 400; ctx.response.body = { success: false, error: "Invalid JSON body" }; return; }
|
if (body.type() !== "json") {
|
||||||
|
ctx.response.status = 400;
|
||||||
|
ctx.response.body = { success: false, error: "Invalid JSON body" };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const data = await body.json();
|
const data = await body.json();
|
||||||
let empCode = data.employeeCode || await generateNextCode("EMPLOYEE_MAIN", "CLRI", 3);
|
let empCode = data.employeeCode;
|
||||||
|
|
||||||
|
if (!empCode || empCode.trim() === "") {
|
||||||
|
empCode = await generateNextCode("EMPLOYEE_MAIN", "CLRI", 3);
|
||||||
|
}
|
||||||
|
|
||||||
const connection = await pool.getConnection();
|
const connection = await pool.getConnection();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await connection.beginTransaction();
|
await connection.beginTransaction();
|
||||||
|
|
||||||
const [partnerResult]: any = await connection.execute(
|
const [partnerResult] = await connection.execute(
|
||||||
`INSERT INTO partners (first_name, last_name, dob, gender, personal_email, personal_phone) VALUES (?, ?, ?, ?, ?, ?)`,
|
`INSERT INTO partners (first_name, last_name, dob, gender, personal_email, personal_phone)
|
||||||
[data.firstName, data.lastName, data.dob, data.gender, data.personalEmail, data.personalPhone]
|
VALUES (?, ?, ?, ?, ?, ?)`,
|
||||||
|
[
|
||||||
|
data.firstName,
|
||||||
|
data.lastName,
|
||||||
|
data.dob,
|
||||||
|
data.gender,
|
||||||
|
data.personalEmail,
|
||||||
|
data.personalPhone,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
const partnerId = partnerResult.insertId;
|
const partnerId = (partnerResult as any).insertId;
|
||||||
|
|
||||||
const [empResult]: any = await connection.execute(
|
|
||||||
`INSERT INTO employees (employee_code, partner_id, company_id, branch_id, is_active) VALUES (?, ?, ?, ?, ?)`,
|
|
||||||
[empCode, partnerId, data.companyId, data.branchId, true]
|
|
||||||
);
|
|
||||||
const employeeId = empResult.insertId;
|
|
||||||
|
|
||||||
await connection.execute(
|
await connection.execute(
|
||||||
`INSERT INTO employment_terms (employee_id, work_email, date_joining, probation_days, status, salary_structure_id)
|
`INSERT INTO addresses (partner_id, address_type, door_number, landmark, address_line, pincode, district, state)
|
||||||
VALUES (?, ?, ?, ?, 'ACTIVE', ?)`,
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
[employeeId, data.work_email, data.dateJoining, data.probationDays, data.salaryStructureId]
|
[
|
||||||
|
partnerId,
|
||||||
|
data.address.type,
|
||||||
|
data.address.doorNumber,
|
||||||
|
data.address.landmark,
|
||||||
|
data.address.line,
|
||||||
|
data.address.pincode,
|
||||||
|
data.address.district,
|
||||||
|
data.address.state,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const [employeeResult] = await connection.execute(
|
||||||
|
`INSERT INTO employees (employee_code, partner_id, company_id, branch_id, is_active)
|
||||||
|
VALUES (?, ?, ?, ?, ?)`,
|
||||||
|
[empCode, partnerId, data.companyId, data.branchId, true],
|
||||||
|
);
|
||||||
|
const employeeId = (employeeResult as any).insertId;
|
||||||
|
|
||||||
await connection.execute(
|
await connection.execute(
|
||||||
`INSERT INTO employee_assignments (employee_id, department_id, job_id, reporting_to_id, change_reason, effective_from, is_current)
|
`INSERT INTO contracts (employee_id, department_id, job_id, work_email, date_joining, probation_days, status, salary_structure_id, reporting_to_id)
|
||||||
VALUES (?, ?, ?, ?, 'HIRE', ?, TRUE)`,
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
[employeeId, data.departmentId, data.jobId, data.reportingToId || null, data.dateJoining]
|
[
|
||||||
|
employeeId,
|
||||||
|
data.departmentId,
|
||||||
|
data.jobId,
|
||||||
|
data.work_email,
|
||||||
|
data.dateJoining,
|
||||||
|
data.probationDays,
|
||||||
|
"ACTIVE",
|
||||||
|
data.salaryStructureId,
|
||||||
|
data.reportingToId || null,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
await connection.commit();
|
await connection.commit();
|
||||||
|
|
||||||
ctx.response.status = 201;
|
ctx.response.status = 201;
|
||||||
ctx.response.body = { success: true, message: "Employee created", employee_id: employeeId, employee_code: empCode };
|
ctx.response.body = {
|
||||||
|
success: true,
|
||||||
|
message: "Employee created successfully",
|
||||||
|
employee_id: employeeId,
|
||||||
|
employee_code: empCode,
|
||||||
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await connection.rollback();
|
await connection.rollback();
|
||||||
let errorMessage = (error as Error).message;
|
let errorMessage = (error as Error).message;
|
||||||
if (errorMessage.includes("Duplicate entry")) {
|
if (errorMessage.includes("Duplicate entry")) {
|
||||||
ctx.response.status = 409;
|
ctx.response.status = 409;
|
||||||
ctx.response.body = { success: false, error: "A unique constraint failed. Employee Code, Email, or Phone already exists." };
|
ctx.response.body = {
|
||||||
|
success: false,
|
||||||
|
error:
|
||||||
|
"A unique constraint failed. The Employee Code, Personal Email, or Phone number already exists.",
|
||||||
|
};
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ctx.response.status = 500;
|
ctx.response.status = 500;
|
||||||
ctx.response.body = { success: false, error: "Transaction failed: " + errorMessage };
|
ctx.response.body = {
|
||||||
} finally { connection.release(); }
|
success: false,
|
||||||
|
error: "Transaction failed: " + errorMessage,
|
||||||
};
|
};
|
||||||
|
} finally {
|
||||||
export const getEmployeeHistory = async (ctx: any) => {
|
connection.release();
|
||||||
const id = ctx.params.id;
|
|
||||||
try {
|
|
||||||
const [rows] = await pool.query(
|
|
||||||
`SELECT ea.assignment_id, ea.change_reason, ea.effective_from, ea.effective_to, ea.is_current,
|
|
||||||
d.name AS department, j.title AS job_title,
|
|
||||||
mgr_p.first_name AS manager_first_name, mgr_p.last_name AS manager_last_name
|
|
||||||
FROM employee_assignments ea
|
|
||||||
JOIN departments d ON ea.department_id = d.department_id
|
|
||||||
JOIN job_positions j ON ea.job_id = j.job_id
|
|
||||||
LEFT JOIN employees mgr_e ON ea.reporting_to_id = mgr_e.employee_id
|
|
||||||
LEFT JOIN partners mgr_p ON mgr_e.partner_id = mgr_p.partner_id
|
|
||||||
WHERE ea.employee_id = ?
|
|
||||||
ORDER BY ea.effective_from DESC`,
|
|
||||||
[id]
|
|
||||||
);
|
|
||||||
ctx.response.body = { success: true, history: rows };
|
|
||||||
} catch (error) {
|
|
||||||
ctx.response.status = 500;
|
|
||||||
ctx.response.body = { success: false, error: (error as Error).message };
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getEmployeeById = async (ctx: any) => {
|
export const getEmployeeById = async (ctx: any) => {
|
||||||
const id = ctx.params.id;
|
const id = ctx.params.id;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// FIX: Querying the new split tables (employment_terms & employee_assignments)
|
// 1. Fetch core employee, partner, contract, and manager details
|
||||||
const [empRows] = await pool.query(
|
const [empRows] = await pool.query(
|
||||||
`SELECT
|
`SELECT
|
||||||
e.employee_id, e.employee_code, e.is_active,
|
e.employee_id,
|
||||||
p.first_name, p.last_name, p.dob, p.gender, p.personal_email, p.personal_phone,
|
e.employee_code,
|
||||||
et.work_email, et.date_joining, et.probation_days, et.status AS contract_status, et.salary_structure_id,
|
e.is_active,
|
||||||
d.name AS department, d.department_id, j.title AS designation, j.job_id,
|
p.first_name,
|
||||||
mgr_p.first_name AS manager_first_name, mgr_p.last_name AS manager_last_name, mgr_e.employee_code AS manager_employee_code
|
p.last_name,
|
||||||
|
p.dob,
|
||||||
|
p.gender,
|
||||||
|
p.personal_email,
|
||||||
|
p.personal_phone,
|
||||||
|
c.work_email,
|
||||||
|
c.date_joining,
|
||||||
|
c.probation_days,
|
||||||
|
c.status AS contract_status,
|
||||||
|
d.name AS department,
|
||||||
|
d.department_id,
|
||||||
|
j.title AS designation,
|
||||||
|
j.job_id,
|
||||||
|
mgr_p.first_name AS manager_first_name,
|
||||||
|
mgr_p.last_name AS manager_last_name,
|
||||||
|
mgr_e.employee_code AS manager_employee_code
|
||||||
FROM employees e
|
FROM employees e
|
||||||
JOIN partners p ON e.partner_id = p.partner_id
|
JOIN partners p ON e.partner_id = p.partner_id
|
||||||
JOIN employment_terms et ON e.employee_id = et.employee_id AND et.status = 'ACTIVE'
|
JOIN contracts c ON e.employee_id = c.employee_id AND c.status = 'ACTIVE'
|
||||||
JOIN employee_assignments ea ON e.employee_id = ea.employee_id AND ea.is_current = TRUE
|
JOIN departments d ON c.department_id = d.department_id
|
||||||
JOIN departments d ON ea.department_id = d.department_id
|
JOIN job_positions j ON c.job_id = j.job_id
|
||||||
JOIN job_positions j ON ea.job_id = j.job_id
|
LEFT JOIN employees mgr_e ON c.reporting_to_id = mgr_e.employee_id
|
||||||
LEFT JOIN employees mgr_e ON ea.reporting_to_id = mgr_e.employee_id
|
|
||||||
LEFT JOIN partners mgr_p ON mgr_e.partner_id = mgr_p.partner_id
|
LEFT JOIN partners mgr_p ON mgr_e.partner_id = mgr_p.partner_id
|
||||||
WHERE e.employee_id = ?`,
|
WHERE e.employee_id = ?`,
|
||||||
[id]
|
[id],
|
||||||
);
|
);
|
||||||
|
|
||||||
const empData = empRows as any[];
|
const empData = empRows as any[];
|
||||||
if (empData.length === 0) { ctx.response.status = 404; ctx.response.body = { success: false, message: "Employee not found" }; return; }
|
|
||||||
|
if (empData.length === 0) {
|
||||||
|
ctx.response.status = 404;
|
||||||
|
ctx.response.body = { success: false, message: "Employee not found" };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const employee = empData[0];
|
const employee = empData[0];
|
||||||
|
|
||||||
|
// 2. Fetch all addresses for this partner separately to return as an array
|
||||||
const [addrRows] = await pool.query(
|
const [addrRows] = await pool.query(
|
||||||
`SELECT address_type, door_number, landmark, address_line, pincode, district, state
|
`SELECT address_type, door_number, landmark, address_line, pincode, district, state
|
||||||
FROM addresses
|
FROM addresses
|
||||||
WHERE partner_id = (SELECT partner_id FROM employees WHERE employee_id = ?)`,
|
WHERE partner_id = (SELECT partner_id FROM employees WHERE employee_id = ?)`,
|
||||||
[id]
|
[id],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 3. Combine into a single structured JSON response
|
||||||
ctx.response.status = 200;
|
ctx.response.status = 200;
|
||||||
ctx.response.body = {
|
ctx.response.body = {
|
||||||
success: true,
|
success: true,
|
||||||
data: {
|
data: {
|
||||||
employee_id: employee.employee_id, employee_code: employee.employee_code, is_active: employee.is_active,
|
employee_id: employee.employee_id,
|
||||||
first_name: employee.first_name, last_name: employee.last_name, dob: employee.dob, gender: employee.gender,
|
employee_code: employee.employee_code,
|
||||||
personal_email: employee.personal_email, personal_phone: employee.personal_phone,
|
is_active: employee.is_active,
|
||||||
|
first_name: employee.first_name,
|
||||||
|
last_name: employee.last_name,
|
||||||
|
dob: employee.dob,
|
||||||
|
gender: employee.gender,
|
||||||
|
personal_email: employee.personal_email,
|
||||||
|
personal_phone: employee.personal_phone,
|
||||||
contract: {
|
contract: {
|
||||||
work_email: employee.work_email, date_joining: employee.date_joining, probation_days: employee.probation_days,
|
work_email: employee.work_email,
|
||||||
status: employee.contract_status, salary_structure_id: employee.salary_structure_id,
|
date_joining: employee.date_joining,
|
||||||
department: employee.department, department_id: employee.department_id, designation: employee.designation, job_id: employee.job_id,
|
probation_days: employee.probation_days,
|
||||||
manager: employee.manager_employee_code ? { employee_code: employee.manager_employee_code, first_name: employee.manager_first_name, last_name: employee.manager_last_name } : null,
|
status: employee.contract_status,
|
||||||
|
department: employee.department,
|
||||||
|
department_id: employee.department_id,
|
||||||
|
designation: employee.designation,
|
||||||
|
job_id: employee.job_id,
|
||||||
|
manager: employee.manager_employee_code
|
||||||
|
? {
|
||||||
|
employee_code: employee.manager_employee_code,
|
||||||
|
first_name: employee.manager_first_name,
|
||||||
|
last_name: employee.manager_last_name,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
},
|
},
|
||||||
addresses: addrRows,
|
addresses: addrRows, // Array of all addresses (PERMANENT, CURRENT, EMERGENCY)
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -181,7 +252,6 @@ export const updateEmployee = async (ctx: any) => {
|
|||||||
try {
|
try {
|
||||||
await connection.beginTransaction();
|
await connection.beginTransaction();
|
||||||
|
|
||||||
// 1. Check if employee exists and get partner_id
|
|
||||||
const [employeeRows] = await connection.execute(
|
const [employeeRows] = await connection.execute(
|
||||||
`SELECT partner_id FROM employees WHERE employee_id = ?`,
|
`SELECT partner_id FROM employees WHERE employee_id = ?`,
|
||||||
[id],
|
[id],
|
||||||
@ -191,30 +261,41 @@ export const updateEmployee = async (ctx: any) => {
|
|||||||
if (employees.length === 0) {
|
if (employees.length === 0) {
|
||||||
ctx.response.status = 404;
|
ctx.response.status = 404;
|
||||||
ctx.response.body = { success: false, message: "Employee not found" };
|
ctx.response.body = { success: false, message: "Employee not found" };
|
||||||
await connection.rollback();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const partnerId = employees[0].partner_id;
|
const partnerId = employees[0].partner_id;
|
||||||
|
|
||||||
// 2. Update Partners table (Added dob and gender)
|
|
||||||
await connection.execute(
|
await connection.execute(
|
||||||
`UPDATE partners
|
`UPDATE partners
|
||||||
SET first_name = ?, last_name = ?, dob = ?, gender = ?, personal_email = ?, personal_phone = ?
|
SET first_name = ?, last_name = ?, personal_email = ?, personal_phone = ?
|
||||||
WHERE partner_id = ?`,
|
WHERE partner_id = ?`,
|
||||||
[
|
[
|
||||||
data.firstName,
|
data.firstName,
|
||||||
data.lastName,
|
data.lastName,
|
||||||
data.dob,
|
|
||||||
data.gender,
|
|
||||||
data.personalEmail,
|
data.personalEmail,
|
||||||
data.personalPhone,
|
data.personalPhone,
|
||||||
partnerId,
|
partnerId,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
// 3. Upsert Address
|
// await connection.execute(
|
||||||
if (data.address) {
|
// `UPDATE addresses
|
||||||
|
// SET door_number = ?, landmark = ?, address_line = ?, pincode = ?, district = ?, state = ?
|
||||||
|
// WHERE partner_id = ? AND address_type = ?`,
|
||||||
|
// [
|
||||||
|
// data.address.doorNumber,
|
||||||
|
// data.address.landmark,
|
||||||
|
// data.address.line,
|
||||||
|
// data.address.pincode,
|
||||||
|
// data.address.district,
|
||||||
|
// data.address.state,
|
||||||
|
// partnerId,
|
||||||
|
// data.address.type,
|
||||||
|
// ],
|
||||||
|
// );
|
||||||
|
|
||||||
|
// Upsert Address (Fixes the missing address bug)
|
||||||
await connection.execute(
|
await connection.execute(
|
||||||
`INSERT INTO addresses (partner_id, address_type, door_number, landmark, address_line, pincode, district, state)
|
`INSERT INTO addresses (partner_id, address_type, door_number, landmark, address_line, pincode, district, state)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
@ -236,34 +317,6 @@ export const updateEmployee = async (ctx: any) => {
|
|||||||
data.address.state,
|
data.address.state,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Update Employment Terms (Work Email)
|
|
||||||
if (data.work_email) {
|
|
||||||
await connection.execute(
|
|
||||||
`UPDATE employment_terms
|
|
||||||
SET work_email = ?
|
|
||||||
WHERE employee_id = ? AND status = 'ACTIVE'`,
|
|
||||||
[data.work_email, id]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. Update Employee Assignments (Department, Job, Manager)
|
|
||||||
// We update the current active assignment directly.
|
|
||||||
// (If this is a formal promotion/transfer, the frontend should use the POST /contracts endpoint instead to preserve history)
|
|
||||||
if (data.departmentId || data.jobId || data.reportingToId) {
|
|
||||||
await connection.execute(
|
|
||||||
`UPDATE employee_assignments
|
|
||||||
SET department_id = ?, job_id = ?, reporting_to_id = ?
|
|
||||||
WHERE employee_id = ? AND is_current = TRUE`,
|
|
||||||
[
|
|
||||||
data.departmentId,
|
|
||||||
data.jobId,
|
|
||||||
data.reportingToId || null,
|
|
||||||
id
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await connection.commit();
|
await connection.commit();
|
||||||
|
|
||||||
@ -274,22 +327,10 @@ export const updateEmployee = async (ctx: any) => {
|
|||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await connection.rollback();
|
await connection.rollback();
|
||||||
|
|
||||||
// Gracefully catch unique constraint violations on Email/Phone
|
|
||||||
const err = error as any;
|
|
||||||
if (err.code === 'ER_DUP_ENTRY' || err.message.includes('Duplicate entry')) {
|
|
||||||
ctx.response.status = 409;
|
|
||||||
ctx.response.body = {
|
|
||||||
success: false,
|
|
||||||
error: "Update failed: The Personal Email, Phone, or Work Email already belongs to another employee."
|
|
||||||
};
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.response.status = 500;
|
ctx.response.status = 500;
|
||||||
ctx.response.body = {
|
ctx.response.body = {
|
||||||
success: false,
|
success: false,
|
||||||
error: "Update failed: " + err.message,
|
error: "Update failed: " + (error as Error).message,
|
||||||
};
|
};
|
||||||
} finally {
|
} finally {
|
||||||
connection.release();
|
connection.release();
|
||||||
@ -298,13 +339,31 @@ export const updateEmployee = async (ctx: any) => {
|
|||||||
|
|
||||||
export const deleteEmployee = async (ctx: any) => {
|
export const deleteEmployee = async (ctx: any) => {
|
||||||
const id = ctx.params.id;
|
const id = ctx.params.id;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [result] = await pool.execute(`UPDATE employees SET is_active = false WHERE employee_id = ?`, [id]);
|
const [result] = await pool.execute(
|
||||||
if ((result as any).affectedRows === 0) { ctx.response.status = 404; ctx.response.body = { success: false, message: "Employee not found" }; return; }
|
`UPDATE employees SET is_active = false WHERE employee_id = ?`,
|
||||||
|
[id],
|
||||||
|
);
|
||||||
|
|
||||||
|
const updateResult = result as any;
|
||||||
|
|
||||||
|
if (updateResult.affectedRows === 0) {
|
||||||
|
ctx.response.status = 404;
|
||||||
|
ctx.response.body = { success: false, message: "Employee not found" };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
ctx.response.status = 200;
|
ctx.response.status = 200;
|
||||||
ctx.response.body = { success: true, message: "Employee account deactivated successfully" };
|
ctx.response.body = {
|
||||||
|
success: true,
|
||||||
|
message: "Employee account deactivated successfully",
|
||||||
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ctx.response.status = 500;
|
ctx.response.status = 500;
|
||||||
ctx.response.body = { success: false, error: "Deactivation failed: " + (error as Error).message };
|
ctx.response.body = {
|
||||||
|
success: false,
|
||||||
|
error: "Deactivation failed: " + (error as Error).message,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -3,341 +3,441 @@ import { generateNextCode } from "../../shared/sequence.ts"
|
|||||||
|
|
||||||
const pool = getDbPool("hrms_ems");
|
const pool = getDbPool("hrms_ems");
|
||||||
|
|
||||||
// Dynamic helper to build WHERE clauses
|
|
||||||
const buildFilter = (params: URLSearchParams, allowedFilters: string[]) => {
|
|
||||||
let clause = " WHERE 1=1";
|
|
||||||
const values: any[] = [];
|
|
||||||
allowedFilters.forEach(filter => {
|
|
||||||
const value = params.get(filter);
|
|
||||||
if (value) { clause += ` AND ${filter} = ?`; values.push(value); }
|
|
||||||
});
|
|
||||||
return { clause, values };
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getCompanies = async (ctx: any) => {
|
export const getCompanies = async (ctx: any) => {
|
||||||
try {
|
try {
|
||||||
const { clause, values } = buildFilter(ctx.request.url.searchParams, ['is_active']);
|
const [rows] = await pool.query(`SELECT company_id, name FROM companies`);
|
||||||
const [rows] = await pool.query(`SELECT company_id, company_code, name, is_active FROM companies ${clause}`, values);
|
ctx.response.status = 200;
|
||||||
ctx.response.body = { success: true, data: rows };
|
ctx.response.body = { success: true, data: rows };
|
||||||
} catch (error) { ctx.response.status = 500; ctx.response.body = { success: false, error: (error as Error).message }; }
|
} catch (error) {
|
||||||
|
ctx.response.status = 500;
|
||||||
|
ctx.response.body = { success: false, error: (error as Error).message };
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getBranches = async (ctx: any) => {
|
export const getBranches = async (ctx: any) => {
|
||||||
try {
|
try {
|
||||||
const params = ctx.request.url.searchParams;
|
const [rows] = await pool.query(`SELECT branch_id, branch_name,code, company_id FROM branches`);
|
||||||
let query = `SELECT b.branch_id, b.code, b.branch_name, b.is_active, c.company_code, c.name AS company_name
|
ctx.response.status = 200;
|
||||||
FROM branches b JOIN companies c ON b.company_id = c.company_id WHERE 1=1`;
|
|
||||||
const values: any[] = [];
|
|
||||||
if (params.get('company_id')) { query += ` AND b.company_id = ?`; values.push(params.get('company_id')); }
|
|
||||||
if (params.get('is_active')) { query += ` AND b.is_active = ?`; values.push(params.get('is_active') === 'true'); }
|
|
||||||
const [rows] = await pool.query(query, values);
|
|
||||||
ctx.response.body = { success: true, data: rows };
|
ctx.response.body = { success: true, data: rows };
|
||||||
} catch (error) { ctx.response.status = 500; ctx.response.body = { success: false, error: (error as Error).message }; }
|
} catch (error) {
|
||||||
|
ctx.response.status = 500;
|
||||||
|
ctx.response.body = { success: false, error: (error as Error).message };
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getDepartments = async (ctx: any) => {
|
export const getDepartments = async (ctx: any) => {
|
||||||
try {
|
try {
|
||||||
const params = ctx.request.url.searchParams;
|
const [rows] = await pool.query(`SELECT department_id, name, parent_id FROM departments`);
|
||||||
let query = `SELECT d.department_id, d.department_code, d.name AS department_name, d.is_active,
|
ctx.response.status = 200;
|
||||||
b.branch_name, c.name AS company_name,
|
ctx.response.body = { success: true, data: rows };
|
||||||
(SELECT GROUP_CONCAT(CONCAT(p.first_name, ' ', p.last_name) SEPARATOR ', ')
|
} catch (error) {
|
||||||
FROM department_managers dm
|
ctx.response.status = 500;
|
||||||
JOIN employees e ON dm.employee_id = e.employee_id
|
ctx.response.body = { success: false, error: (error as Error).message };
|
||||||
JOIN partners p ON e.partner_id = p.partner_id
|
}
|
||||||
WHERE dm.department_id = d.department_id AND dm.is_current = TRUE) AS managers
|
|
||||||
FROM departments d JOIN branches b ON d.branch_id = b.branch_id JOIN companies c ON d.company_id = c.company_id WHERE 1=1`;
|
|
||||||
const values: any[] = [];
|
|
||||||
|
|
||||||
// Filtering
|
|
||||||
if (params.get('company_id')) { query += ` AND d.company_id = ?`; values.push(params.get('company_id')); }
|
|
||||||
if (params.get('branch_id')) { query += ` AND d.branch_id = ?`; values.push(params.get('branch_id')); }
|
|
||||||
if (params.get('is_active')) { query += ` AND d.is_active = ?`; values.push(params.get('is_active') === 'true'); }
|
|
||||||
|
|
||||||
// Sorting (Safe mapping to prevent SQL injection)
|
|
||||||
const sortByParam = params.get('sort_by') || 'department_name';
|
|
||||||
const sortOrderParam = params.get('sort_order') === 'desc' ? 'DESC' : 'ASC';
|
|
||||||
const validSortColumns: Record<string, string> = {
|
|
||||||
'department_name': 'd.name', 'department_code': 'd.department_code', 'company_name': 'c.name', 'branch_name': 'b.branch_name'
|
|
||||||
};
|
|
||||||
const sortColumn = validSortColumns[sortByParam] || 'd.name';
|
|
||||||
query += ` ORDER BY ${sortColumn} ${sortOrderParam}`;
|
|
||||||
|
|
||||||
const [rows] = await pool.query(query, values);
|
|
||||||
ctx.response.body = { success: true, count: rows.length, data: rows };
|
|
||||||
} catch (error) { ctx.response.status = 500; ctx.response.body = { success: false, error: (error as Error).message }; }
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getJobs = async (ctx: any) => {
|
export const getJobs = async (ctx: any) => {
|
||||||
try {
|
try {
|
||||||
const params = ctx.request.url.searchParams;
|
const [rows] = await pool.query(`SELECT job_id, title, department_id FROM job_positions`);
|
||||||
let query = `SELECT j.job_id, j.job_code, j.title AS job_name, j.is_active,
|
ctx.response.status = 200;
|
||||||
d.name AS department_name, b.branch_name, c.name AS company_name
|
ctx.response.body = { success: true, data: rows };
|
||||||
FROM job_positions j JOIN departments d ON j.department_id = d.department_id
|
} catch (error) {
|
||||||
JOIN branches b ON d.branch_id = b.branch_id JOIN companies c ON d.company_id = c.company_id WHERE 1=1`;
|
ctx.response.status = 500;
|
||||||
const values: any[] = [];
|
ctx.response.body = { success: false, error: (error as Error).message };
|
||||||
|
}
|
||||||
// Filtering
|
|
||||||
if (params.get('company_id')) { query += ` AND d.company_id = ?`; values.push(params.get('company_id')); }
|
|
||||||
if (params.get('branch_id')) { query += ` AND d.branch_id = ?`; values.push(params.get('branch_id')); }
|
|
||||||
if (params.get('department_id')) { query += ` AND j.department_id = ?`; values.push(params.get('department_id')); }
|
|
||||||
|
|
||||||
// Sorting (Safe mapping)
|
|
||||||
const sortByParam = params.get('sort_by') || 'job_name';
|
|
||||||
const sortOrderParam = params.get('sort_order') === 'desc' ? 'DESC' : 'ASC';
|
|
||||||
const validSortColumns: Record<string, string> = {
|
|
||||||
'job_name': 'j.title', 'job_code': 'j.job_code', 'department_name': 'd.name'
|
|
||||||
};
|
};
|
||||||
const sortColumn = validSortColumns[sortByParam] || 'j.title';
|
|
||||||
query += ` ORDER BY ${sortColumn} ${sortOrderParam}`;
|
|
||||||
|
|
||||||
const [rows] = await pool.query(query, values);
|
|
||||||
ctx.response.body = { success: true, count: rows.length, data: rows };
|
|
||||||
} catch (error) { ctx.response.status = 500; ctx.response.body = { success: false, error: (error as Error).message }; }
|
|
||||||
};
|
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// COMPANIES MANAGEMENT
|
// COMPANIES MANAGEMENT
|
||||||
// ==========================================
|
// ==========================================
|
||||||
|
|
||||||
export const createCompany = async (ctx: any) => {
|
export const createCompany = async (ctx: any) => {
|
||||||
const body = ctx.request.body;
|
const body = ctx.request.body;
|
||||||
if (body.type() !== "json") { ctx.response.status = 400; ctx.response.body = { success: false, error: "Invalid JSON body" }; return; }
|
if (body.type() !== "json") {
|
||||||
const data = await body.json();
|
ctx.response.status = 400;
|
||||||
|
ctx.response.body = { success: false, error: "Invalid JSON body" };
|
||||||
let companyCode = data.companyCode || await generateNextCode("COMPANY_MAIN", "CO-", 3);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const [result] = await pool.execute(
|
|
||||||
`INSERT INTO companies (company_code, name, parent_id, is_active) VALUES (?, ?, ?, ?)`,
|
|
||||||
[companyCode, data.name, data.parentId || null, data.isActive ?? true]
|
|
||||||
);
|
|
||||||
ctx.response.status = 201;
|
|
||||||
ctx.response.body = { success: true, message: "Company created", company_id: (result as any).insertId, company_code: companyCode };
|
|
||||||
} catch (error) {
|
|
||||||
const err = error as any;
|
|
||||||
if (err.code === 'ER_DUP_ENTRY' || err.message.includes('Duplicate entry')) {
|
|
||||||
ctx.response.status = 409;
|
|
||||||
ctx.response.body = { success: false, error: `The generated company code '${companyCode}' already exists.` };
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ctx.response.status = 500; ctx.response.body = { success: false, error: err.message };
|
|
||||||
|
const data = await body.json();
|
||||||
|
try {
|
||||||
|
const [result] = await pool.execute(
|
||||||
|
`INSERT INTO companies (name, parent_id) VALUES (?, ?)`,
|
||||||
|
[data.name, data.parentId || null]
|
||||||
|
);
|
||||||
|
ctx.response.status = 201;
|
||||||
|
ctx.response.body = { success: true, message: "Company created", company_id: (result as any).insertId };
|
||||||
|
} catch (error) {
|
||||||
|
ctx.response.status = 500;
|
||||||
|
ctx.response.body = { success: false, error: (error as Error).message };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateCompany = async (ctx: any) => {
|
export const updateCompany = async (ctx: any) => {
|
||||||
const id = ctx.params.id;
|
const id = ctx.params.id;
|
||||||
const body = ctx.request.body;
|
const body = ctx.request.body;
|
||||||
if (body.type() !== "json") { ctx.response.status = 400; ctx.response.body = { success: false, error: "Invalid JSON body" }; return; }
|
if (body.type() !== "json") {
|
||||||
|
ctx.response.status = 400;
|
||||||
|
ctx.response.body = { success: false, error: "Invalid JSON body" };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const data = await body.json();
|
const data = await body.json();
|
||||||
try {
|
try {
|
||||||
const [result] = await pool.execute(
|
const [result] = await pool.execute(
|
||||||
`UPDATE companies SET name = ?, parent_id = ?, is_active = ? WHERE company_id = ?`,
|
`UPDATE companies SET name = ?, parent_id = ? WHERE company_id = ?`,
|
||||||
[data.name, data.parentId || null, data.isActive, id]
|
[data.name, data.parentId || null, id]
|
||||||
);
|
);
|
||||||
if ((result as any).affectedRows === 0) { ctx.response.status = 404; ctx.response.body = { success: false, message: "Company not found" }; return; }
|
if ((result as any).affectedRows === 0) {
|
||||||
ctx.response.status = 200; ctx.response.body = { success: true, message: "Company updated successfully" };
|
ctx.response.status = 404;
|
||||||
} catch (error) { ctx.response.status = 500; ctx.response.body = { success: false, error: (error as Error).message }; }
|
ctx.response.body = { success: false, message: "Company not found" };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ctx.response.status = 200;
|
||||||
|
ctx.response.body = { success: true, message: "Company updated successfully" };
|
||||||
|
} catch (error) {
|
||||||
|
ctx.response.status = 500;
|
||||||
|
ctx.response.body = { success: false, error: (error as Error).message };
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deleteCompany = async (ctx: any) => {
|
export const deleteCompany = async (ctx: any) => {
|
||||||
const id = ctx.params.id;
|
const id = ctx.params.id;
|
||||||
try {
|
try {
|
||||||
const [result] = await pool.execute(`DELETE FROM companies WHERE company_id = ?`, [id]);
|
const [result] = await pool.execute(`DELETE FROM companies WHERE company_id = ?`, [id]);
|
||||||
if ((result as any).affectedRows === 0) { ctx.response.status = 404; ctx.response.body = { success: false, message: "Company not found" }; return; }
|
if ((result as any).affectedRows === 0) {
|
||||||
ctx.response.status = 200; ctx.response.body = { success: true, message: "Company deleted successfully" };
|
ctx.response.status = 404;
|
||||||
|
ctx.response.body = { success: false, message: "Company not found" };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ctx.response.status = 200;
|
||||||
|
ctx.response.body = { success: true, message: "Company deleted successfully" };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
let errorMessage = (error as Error).message;
|
let errorMessage = (error as Error).message;
|
||||||
if (errorMessage.includes("foreign key constraint fails")) errorMessage = "Cannot delete company. It is currently assigned to one or more employees.";
|
// Catch ON DELETE RESTRICT from employees table
|
||||||
ctx.response.status = 409; ctx.response.body = { success: false, error: errorMessage };
|
if (errorMessage.includes("foreign key constraint fails")) {
|
||||||
|
errorMessage = "Cannot delete company. It is currently assigned to one or more employees.";
|
||||||
|
}
|
||||||
|
ctx.response.status = 409;
|
||||||
|
ctx.response.body = { success: false, error: errorMessage };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// BRANCHES MANAGEMENT
|
// BRANCHES MANAGEMENT
|
||||||
// ==========================================
|
// ==========================================
|
||||||
|
|
||||||
export const createBranch = async (ctx: any) => {
|
export const createBranch = async (ctx: any) => {
|
||||||
const body = ctx.request.body;
|
const body = ctx.request.body;
|
||||||
if (body.type() !== "json") { ctx.response.status = 400; ctx.response.body = { success: false, error: "Invalid JSON body" }; return; }
|
if (body.type() !== "json") {
|
||||||
|
ctx.response.status = 400;
|
||||||
|
ctx.response.body = { success: false, error: "Invalid JSON body" };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const data = await body.json();
|
const data = await body.json();
|
||||||
let branchCode = data.code;
|
let branchCode = data.code;
|
||||||
|
|
||||||
|
// If no code is provided, automatically generate a sequential one
|
||||||
if (!branchCode || branchCode.trim() === "") {
|
if (!branchCode || branchCode.trim() === "") {
|
||||||
|
// Get first 3 letters (e.g., "Bengaluru" -> "BEN")
|
||||||
const shortName = data.branchName ? data.branchName.substring(0, 3).toUpperCase() : 'BRN';
|
const shortName = data.branchName ? data.branchName.substring(0, 3).toUpperCase() : 'BRN';
|
||||||
|
|
||||||
|
// Generate: Sequence ID 'BRANCH_BEN', Prefix 'BEN-', Padding 3 -> Output: 'BEN-001'
|
||||||
branchCode = await generateNextCode(`BRANCH_${shortName}`, `${shortName}-`, 3);
|
branchCode = await generateNextCode(`BRANCH_${shortName}`, `${shortName}-`, 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [result] = await pool.execute(
|
const [result] = await pool.execute(
|
||||||
`INSERT INTO branches (company_id, branch_name, code, is_active) VALUES (?, ?, ?, ?)`,
|
`INSERT INTO branches (company_id, branch_name, code) VALUES (?, ?, ?)`,
|
||||||
[data.companyId, data.branchName, branchCode, data.isActive ?? true]
|
[data.companyId, data.branchName, branchCode]
|
||||||
);
|
);
|
||||||
ctx.response.status = 201; ctx.response.body = { success: true, message: "Branch created successfully", branch_id: (result as any).insertId, code: branchCode };
|
|
||||||
|
ctx.response.status = 201;
|
||||||
|
ctx.response.body = {
|
||||||
|
success: true,
|
||||||
|
message: "Branch created successfully",
|
||||||
|
branch_id: (result as any).insertId,
|
||||||
|
code: branchCode
|
||||||
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const err = error as any;
|
let errorMessage = (error as Error).message;
|
||||||
if (err.code === 'ER_DUP_ENTRY' || err.message.includes('Duplicate entry')) {
|
if (errorMessage.includes("Duplicate entry")) {
|
||||||
ctx.response.status = 409; ctx.response.body = { success: false, error: `The branch code '${branchCode}' already exists.` }; return;
|
ctx.response.status = 409;
|
||||||
|
ctx.response.body = { success: false, error: `The branch code '${branchCode}' already exists.` };
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
ctx.response.status = 500; ctx.response.body = { success: false, error: err.message };
|
ctx.response.status = 500;
|
||||||
|
ctx.response.body = { success: false, error: errorMessage };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateBranch = async (ctx: any) => {
|
export const updateBranch = async (ctx: any) => {
|
||||||
const id = ctx.params.id; const body = ctx.request.body;
|
const id = ctx.params.id;
|
||||||
if (body.type() !== "json") { ctx.response.status = 400; ctx.response.body = { success: false, error: "Invalid JSON body" }; return; }
|
const body = ctx.request.body;
|
||||||
|
if (body.type() !== "json") {
|
||||||
|
ctx.response.status = 400;
|
||||||
|
ctx.response.body = { success: false, error: "Invalid JSON body" };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const data = await body.json();
|
const data = await body.json();
|
||||||
try {
|
try {
|
||||||
const [result] = await pool.execute(
|
const [result] = await pool.execute(
|
||||||
`UPDATE branches SET company_id = ?, branch_name = ?, code = ?, is_active = ? WHERE branch_id = ?`,
|
`UPDATE branches SET company_id = ?, branch_name = ?, code = ? WHERE branch_id = ?`,
|
||||||
[data.companyId, data.branchName, data.code, data.isActive, id]
|
[data.companyId, data.branchName, data.code, id]
|
||||||
);
|
);
|
||||||
if ((result as any).affectedRows === 0) { ctx.response.status = 404; ctx.response.body = { success: false, message: "Branch not found" }; return; }
|
if ((result as any).affectedRows === 0) {
|
||||||
ctx.response.status = 200; ctx.response.body = { success: true, message: "Branch updated successfully" };
|
ctx.response.status = 404;
|
||||||
} catch (error) { ctx.response.status = 500; ctx.response.body = { success: false, error: (error as Error).message }; }
|
ctx.response.body = { success: false, message: "Branch not found" };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ctx.response.status = 200;
|
||||||
|
ctx.response.body = { success: true, message: "Branch updated successfully" };
|
||||||
|
} catch (error) {
|
||||||
|
ctx.response.status = 500;
|
||||||
|
ctx.response.body = { success: false, error: (error as Error).message };
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deleteBranch = async (ctx: any) => {
|
export const deleteBranch = async (ctx: any) => {
|
||||||
const id = ctx.params.id;
|
const id = ctx.params.id;
|
||||||
try {
|
try {
|
||||||
const [result] = await pool.execute(`DELETE FROM branches WHERE branch_id = ?`, [id]);
|
const [result] = await pool.execute(`DELETE FROM branches WHERE branch_id = ?`, [id]);
|
||||||
if ((result as any).affectedRows === 0) { ctx.response.status = 404; ctx.response.body = { success: false, message: "Branch not found" }; return; }
|
if ((result as any).affectedRows === 0) {
|
||||||
ctx.response.status = 200; ctx.response.body = { success: true, message: "Branch deleted successfully" };
|
ctx.response.status = 404;
|
||||||
|
ctx.response.body = { success: false, message: "Branch not found" };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ctx.response.status = 200;
|
||||||
|
ctx.response.body = { success: true, message: "Branch deleted successfully" };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
let errorMessage = (error as Error).message;
|
let errorMessage = (error as Error).message;
|
||||||
if (errorMessage.includes("foreign key constraint fails")) errorMessage = "Cannot delete branch. It is currently assigned to one or more employees.";
|
// Catch ON DELETE RESTRICT from employees table
|
||||||
ctx.response.status = 409; ctx.response.body = { success: false, error: errorMessage };
|
if (errorMessage.includes("foreign key constraint fails")) {
|
||||||
|
errorMessage = "Cannot delete branch. It is currently assigned to one or more employees.";
|
||||||
|
}
|
||||||
|
ctx.response.status = 409;
|
||||||
|
ctx.response.body = { success: false, error: errorMessage };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// DEPARTMENTS MANAGEMENT
|
// DEPARTMENTS MANAGEMENT
|
||||||
// ==========================================
|
// ==========================================
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const createDepartment = async (ctx: any) => {
|
export const createDepartment = async (ctx: any) => {
|
||||||
const body = ctx.request.body;
|
const body = ctx.request.body;
|
||||||
if (body.type() !== "json") { ctx.response.status = 400; ctx.response.body = { success: false, error: "Invalid JSON body" }; return; }
|
if (body.type() !== "json") {
|
||||||
const data = await body.json();
|
ctx.response.status = 400;
|
||||||
if (!data.companyId || !data.branchId || !data.name) { ctx.response.status = 400; ctx.response.body = { success: false, error: "companyId, branchId, and name are required." }; return; }
|
ctx.response.body = { success: false, error: "Invalid JSON body" };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await body.json();
|
||||||
|
|
||||||
|
if (!data.companyId || !data.branchId || !data.name) {
|
||||||
|
ctx.response.status = 400;
|
||||||
|
ctx.response.body = { success: false, error: "companyId, branchId, and name are required." };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let deptCode = data.departmentCode || await generateNextCode("DEPARTMENT_MAIN", "DEPT-", 3);
|
|
||||||
const connection = await pool.getConnection();
|
const connection = await pool.getConnection();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await connection.beginTransaction();
|
await connection.beginTransaction();
|
||||||
|
|
||||||
|
// 1. Insert the department
|
||||||
const [result] = await connection.execute(
|
const [result] = await connection.execute(
|
||||||
`INSERT INTO departments (company_id, branch_id, department_code, name, parent_id, is_active) VALUES (?, ?, ?, ?, ?, ?)`,
|
`INSERT INTO departments (company_id, branch_id, name, parent_id) VALUES (?, ?, ?, ?)`,
|
||||||
[data.companyId, data.branchId, deptCode, data.name, data.parentId ?? null, data.isActive ?? true]
|
[data.companyId, data.branchId, data.name, data.parentId ?? null]
|
||||||
);
|
);
|
||||||
const departmentId = (result as any).insertId;
|
const departmentId = (result as any).insertId;
|
||||||
|
|
||||||
|
// 2. If a manager is provided, add them to the junction table
|
||||||
if (data.managerId) {
|
if (data.managerId) {
|
||||||
await connection.execute(
|
await connection.execute(
|
||||||
`INSERT INTO department_managers (department_id, employee_id, is_current) VALUES (?, ?, TRUE)`,
|
`INSERT INTO department_managers (department_id, employee_id) VALUES (?, ?)`,
|
||||||
[departmentId, data.managerId]
|
[departmentId, data.managerId]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await connection.commit();
|
await connection.commit();
|
||||||
ctx.response.status = 201; ctx.response.body = { success: true, message: "Department created", department_id: departmentId, department_code: deptCode };
|
|
||||||
|
ctx.response.status = 201;
|
||||||
|
ctx.response.body = {
|
||||||
|
success: true,
|
||||||
|
message: "Department created",
|
||||||
|
department_id: departmentId
|
||||||
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await connection.rollback();
|
await connection.rollback();
|
||||||
const err = error as any;
|
ctx.response.status = 500;
|
||||||
if (err.code === 'ER_DUP_ENTRY' || err.message.includes('Duplicate entry')) {
|
ctx.response.body = { success: false, error: (error as Error).message };
|
||||||
ctx.response.status = 409;
|
} finally {
|
||||||
ctx.response.body = { success: false, error: `The generated department code '${deptCode}' already exists. Please sync your database sequences.` };
|
connection.release();
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
ctx.response.status = 500; ctx.response.body = { success: false, error: err.message };
|
|
||||||
} finally { connection.release(); }
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateDepartment = async (ctx: any) => {
|
export const updateDepartment = async (ctx: any) => {
|
||||||
const id = ctx.params.id; const body = ctx.request.body;
|
const id = ctx.params.id;
|
||||||
if (body.type() !== "json") { ctx.response.status = 400; ctx.response.body = { success: false, error: "Invalid JSON body" }; return; }
|
const body = ctx.request.body;
|
||||||
|
if (body.type() !== "json") {
|
||||||
|
ctx.response.status = 400;
|
||||||
|
ctx.response.body = { success: false, error: "Invalid JSON body" };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const data = await body.json();
|
const data = await body.json();
|
||||||
const connection = await pool.getConnection();
|
const connection = await pool.getConnection();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await connection.beginTransaction();
|
await connection.beginTransaction();
|
||||||
|
|
||||||
|
// 1. Update core department details
|
||||||
const [result] = await connection.execute(
|
const [result] = await connection.execute(
|
||||||
`UPDATE departments SET company_id = ?, branch_id = ?, name = ?, parent_id = ?, is_active = ? WHERE department_id = ?`,
|
`UPDATE departments SET company_id = ?, branch_id = ?, name = ?, parent_id = ? WHERE department_id = ?`,
|
||||||
[data.companyId ?? null, data.branchId ?? null, data.name ?? null, data.parentId ?? null, data.isActive, id]
|
[data.companyId ?? null, data.branchId ?? null, data.name ?? null, data.parentId ?? null, id]
|
||||||
);
|
);
|
||||||
|
|
||||||
if ((result as any).affectedRows === 0) { ctx.response.status = 404; ctx.response.body = { success: false, message: "Department not found" }; await connection.rollback(); connection.release(); return; }
|
if ((result as any).affectedRows === 0) {
|
||||||
|
ctx.response.status = 404;
|
||||||
|
ctx.response.body = { success: false, message: "Department not found" };
|
||||||
|
await connection.rollback();
|
||||||
|
connection.release();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Manage reassignment if a new manager is passed in the update
|
||||||
if (data.managerId) {
|
if (data.managerId) {
|
||||||
await connection.execute(`UPDATE department_managers SET is_current = FALSE, removed_at = NOW() WHERE department_id = ? AND is_current = TRUE`, [id]);
|
// For simplicity in this update endpoint, we overwrite the existing managers.
|
||||||
|
// You can create a dedicated POST /departments/:id/managers endpoint later for multi-manager logic.
|
||||||
|
await connection.execute(`DELETE FROM department_managers WHERE department_id = ?`, [id]);
|
||||||
await connection.execute(
|
await connection.execute(
|
||||||
`INSERT INTO department_managers (department_id, employee_id, is_current) VALUES (?, ?, TRUE)
|
`INSERT INTO department_managers (department_id, employee_id) VALUES (?, ?)`,
|
||||||
ON DUPLICATE KEY UPDATE is_current = TRUE, removed_at = NULL`,
|
|
||||||
[id, data.managerId]
|
[id, data.managerId]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
await connection.commit(); ctx.response.status = 200; ctx.response.body = { success: true, message: "Department updated successfully" };
|
|
||||||
|
await connection.commit();
|
||||||
|
ctx.response.status = 200;
|
||||||
|
ctx.response.body = { success: true, message: "Department updated successfully" };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await connection.rollback(); ctx.response.status = 500; ctx.response.body = { success: false, error: (error as Error).message };
|
await connection.rollback();
|
||||||
} finally { connection.release(); }
|
ctx.response.status = 500;
|
||||||
|
ctx.response.body = { success: false, error: (error as Error).message };
|
||||||
|
} finally {
|
||||||
|
connection.release();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export const deleteDepartment = async (ctx: any) => {
|
export const deleteDepartment = async (ctx: any) => {
|
||||||
const id = ctx.params.id;
|
const id = ctx.params.id;
|
||||||
try {
|
try {
|
||||||
const [result] = await pool.execute(`DELETE FROM departments WHERE department_id = ?`, [id]);
|
const [result] = await pool.execute(`DELETE FROM departments WHERE department_id = ?`, [id]);
|
||||||
if ((result as any).affectedRows === 0) { ctx.response.status = 404; ctx.response.body = { success: false, message: "Department not found" }; return; }
|
if ((result as any).affectedRows === 0) {
|
||||||
ctx.response.status = 200; ctx.response.body = { success: true, message: "Department deleted successfully" };
|
ctx.response.status = 404;
|
||||||
|
ctx.response.body = { success: false, message: "Department not found" };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ctx.response.status = 200;
|
||||||
|
ctx.response.body = { success: true, message: "Department deleted successfully" };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
let errorMessage = (error as Error).message;
|
let errorMessage = (error as Error).message;
|
||||||
if (errorMessage.includes("foreign key constraint fails")) errorMessage = "Cannot delete department. There are active assignments tied to it.";
|
// Catch ON DELETE RESTRICT from contracts table
|
||||||
ctx.response.status = 409; ctx.response.body = { success: false, error: errorMessage };
|
if (errorMessage.includes("foreign key constraint fails")) {
|
||||||
|
errorMessage = "Cannot delete department. There are active or historical contracts tied to this department.";
|
||||||
|
}
|
||||||
|
ctx.response.status = 409;
|
||||||
|
ctx.response.body = { success: false, error: errorMessage };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// JOB POSITIONS MANAGEMENT
|
// JOB POSITIONS MANAGEMENT
|
||||||
// ==========================================
|
// ==========================================
|
||||||
|
|
||||||
export const createJob = async (ctx: any) => {
|
export const createJob = async (ctx: any) => {
|
||||||
const body = ctx.request.body;
|
const body = ctx.request.body;
|
||||||
if (body.type() !== "json") { ctx.response.status = 400; ctx.response.body = { success: false, error: "Invalid JSON body" }; return; }
|
if (body.type() !== "json") {
|
||||||
const data = await body.json();
|
ctx.response.status = 400;
|
||||||
if (!data.departmentId || !data.title) { ctx.response.status = 400; ctx.response.body = { success: false, error: "departmentId and title are required fields." }; return; }
|
ctx.response.body = { success: false, error: "Invalid JSON body" };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let jobCode = data.jobCode || await generateNextCode("JOB_MAIN", "JOB-", 3);
|
const data = await body.json();
|
||||||
|
|
||||||
|
if (!data.departmentId || !data.title) {
|
||||||
|
ctx.response.status = 400;
|
||||||
|
ctx.response.body = { success: false, error: "departmentId and title are required fields." };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [result] = await pool.execute(
|
const [result] = await pool.execute(
|
||||||
`INSERT INTO job_positions (department_id, job_code, title, description, is_active) VALUES (?, ?, ?, ?, ?)`,
|
`INSERT INTO job_positions (department_id, title, description) VALUES (?, ?, ?)`,
|
||||||
[data.departmentId, jobCode, data.title, data.description ?? null, data.isActive ?? true]
|
[data.departmentId, data.title, data.description ?? null]
|
||||||
);
|
);
|
||||||
ctx.response.status = 201; ctx.response.body = { success: true, message: "Job created", job_id: (result as any).insertId, job_code: jobCode };
|
ctx.response.status = 201;
|
||||||
|
ctx.response.body = { success: true, message: "Job created", job_id: (result as any).insertId };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const err = error as any;
|
ctx.response.status = 500;
|
||||||
if (err.code === 'ER_DUP_ENTRY' || err.message.includes('Duplicate entry')) {
|
ctx.response.body = { success: false, error: (error as Error).message };
|
||||||
ctx.response.status = 409;
|
|
||||||
ctx.response.body = { success: false, error: `The generated job code '${jobCode}' already exists. Please sync your database sequences.` };
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
ctx.response.status = 500; ctx.response.body = { success: false, error: err.message };
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateJob = async (ctx: any) => {
|
export const updateJob = async (ctx: any) => {
|
||||||
const id = ctx.params.id; const body = ctx.request.body;
|
const id = ctx.params.id;
|
||||||
if (body.type() !== "json") { ctx.response.status = 400; ctx.response.body = { success: false, error: "Invalid JSON body" }; return; }
|
const body = ctx.request.body;
|
||||||
|
if (body.type() !== "json") {
|
||||||
|
ctx.response.status = 400;
|
||||||
|
ctx.response.body = { success: false, error: "Invalid JSON body" };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const data = await body.json();
|
const data = await body.json();
|
||||||
try {
|
try {
|
||||||
const [result] = await pool.execute(
|
const [result] = await pool.execute(
|
||||||
`UPDATE job_positions SET department_id = ?, title = ?, description = ?, is_active = ? WHERE job_id = ?`,
|
`UPDATE job_positions SET department_id = ?, title = ?, description = ? WHERE job_id = ?`,
|
||||||
[data.departmentId ?? null, data.title ?? null, data.description ?? null, data.isActive, id]
|
[data.departmentId ?? null, data.title ?? null, data.description ?? null, id]
|
||||||
);
|
);
|
||||||
if ((result as any).affectedRows === 0) { ctx.response.status = 404; ctx.response.body = { success: false, message: "Job not found" }; return; }
|
if ((result as any).affectedRows === 0) {
|
||||||
ctx.response.status = 200; ctx.response.body = { success: true, message: "Job updated successfully" };
|
ctx.response.status = 404;
|
||||||
} catch (error) { ctx.response.status = 500; ctx.response.body = { success: false, error: (error as Error).message }; }
|
ctx.response.body = { success: false, message: "Job not found" };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ctx.response.status = 200;
|
||||||
|
ctx.response.body = { success: true, message: "Job updated successfully" };
|
||||||
|
} catch (error) {
|
||||||
|
ctx.response.status = 500;
|
||||||
|
ctx.response.body = { success: false, error: (error as Error).message };
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Note: deleteJob function remains the same as before.
|
||||||
|
|
||||||
export const deleteJob = async (ctx: any) => {
|
export const deleteJob = async (ctx: any) => {
|
||||||
const id = ctx.params.id;
|
const id = ctx.params.id;
|
||||||
try {
|
try {
|
||||||
const [result] = await pool.execute(`DELETE FROM job_positions WHERE job_id = ?`, [id]);
|
const [result] = await pool.execute(`DELETE FROM job_positions WHERE job_id = ?`, [id]);
|
||||||
if ((result as any).affectedRows === 0) { ctx.response.status = 404; ctx.response.body = { success: false, message: "Job not found" }; return; }
|
if ((result as any).affectedRows === 0) {
|
||||||
ctx.response.status = 200; ctx.response.body = { success: true, message: "Job deleted successfully" };
|
ctx.response.status = 404;
|
||||||
|
ctx.response.body = { success: false, message: "Job not found" };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ctx.response.status = 200;
|
||||||
|
ctx.response.body = { success: true, message: "Job deleted successfully" };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
let errorMessage = (error as Error).message;
|
let errorMessage = (error as Error).message;
|
||||||
if (errorMessage.includes("foreign key constraint fails")) errorMessage = "Cannot delete job position. It is currently linked to active employee assignments.";
|
// Catch ON DELETE RESTRICT from contracts table
|
||||||
ctx.response.status = 409; ctx.response.body = { success: false, error: errorMessage };
|
if (errorMessage.includes("foreign key constraint fails")) {
|
||||||
|
errorMessage = "Cannot delete job position. It is currently linked to one or more employee contracts.";
|
||||||
|
}
|
||||||
|
ctx.response.status = 409;
|
||||||
|
ctx.response.body = { success: false, error: errorMessage };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -5,7 +5,12 @@ const emsPool = getDbPool("hrms_ems");
|
|||||||
|
|
||||||
export const bulkSeedEmployees = async (ctx: Context) => {
|
export const bulkSeedEmployees = async (ctx: Context) => {
|
||||||
const body = ctx.request.hasBody ? await ctx.request.body.json() : [];
|
const body = ctx.request.hasBody ? await ctx.request.body.json() : [];
|
||||||
if (!Array.isArray(body) || body.length === 0) { ctx.response.status = 400; ctx.response.body = { success: false, message: "Provide an array of employee logs." }; return; }
|
|
||||||
|
if (!Array.isArray(body) || body.length === 0) {
|
||||||
|
ctx.response.status = 400;
|
||||||
|
ctx.response.body = { success: false, message: "Provide an array of employee logs." };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const connection = await emsPool.getConnection();
|
const connection = await emsPool.getConnection();
|
||||||
let insertedCount = 0;
|
let insertedCount = 0;
|
||||||
@ -15,11 +20,12 @@ export const bulkSeedEmployees = async (ctx: Context) => {
|
|||||||
|
|
||||||
for (const item of body) {
|
for (const item of body) {
|
||||||
const { emp_code, name, designation, department, email } = item;
|
const { emp_code, name, designation, department, email } = item;
|
||||||
|
|
||||||
const nameParts = name.trim().split(" ");
|
const nameParts = name.trim().split(" ");
|
||||||
const firstName = nameParts[0] || "Employee";
|
const firstName = nameParts[0] || "Employee";
|
||||||
const lastName = nameParts.slice(1).join(" ") || "LNU";
|
const lastName = nameParts.slice(1).join(" ") || "LNU";
|
||||||
|
|
||||||
// 1. Partner
|
// 1. Insert into partners
|
||||||
const [partnerResult]: any = await connection.execute(
|
const [partnerResult]: any = await connection.execute(
|
||||||
`INSERT INTO partners (first_name, last_name, dob, gender, personal_email, personal_phone)
|
`INSERT INTO partners (first_name, last_name, dob, gender, personal_email, personal_phone)
|
||||||
VALUES (?, ?, '1995-01-01', 'Not Specified', ?, ?)
|
VALUES (?, ?, '1995-01-01', 'Not Specified', ?, ?)
|
||||||
@ -28,25 +34,25 @@ export const bulkSeedEmployees = async (ctx: Context) => {
|
|||||||
);
|
);
|
||||||
const partnerId = partnerResult.insertId;
|
const partnerId = partnerResult.insertId;
|
||||||
|
|
||||||
// 2. Department
|
// 2. Ensure department exists (FIXED: Added branch_id to satisfy NOT NULL constraint)
|
||||||
const [deptResult]: any = await connection.execute(
|
const [deptResult]: any = await connection.execute(
|
||||||
`INSERT INTO departments (company_id, branch_id, department_code, name)
|
`INSERT INTO departments (company_id, branch_id, name)
|
||||||
VALUES (1, 1, CONCAT('DEPT-', LEFT(?, 3)), ?)
|
VALUES (1, 1, ?)
|
||||||
ON DUPLICATE KEY UPDATE department_id = LAST_INSERT_ID(department_id)`,
|
ON DUPLICATE KEY UPDATE department_id = LAST_INSERT_ID(department_id)`,
|
||||||
[department || "General", department || "General"]
|
[department || "General"]
|
||||||
);
|
);
|
||||||
const departmentId = deptResult.insertId;
|
const departmentId = deptResult.insertId;
|
||||||
|
|
||||||
// 3. Job
|
// 3. Ensure job position exists (FIXED: Linked to department_id instead of company_id)
|
||||||
const [jobResult]: any = await connection.execute(
|
const [jobResult]: any = await connection.execute(
|
||||||
`INSERT INTO job_positions (department_id, job_code, title)
|
`INSERT INTO job_positions (department_id, title)
|
||||||
VALUES (?, CONCAT('JOB-', LEFT(?, 3)), ?)
|
VALUES (?, ?)
|
||||||
ON DUPLICATE KEY UPDATE job_id = LAST_INSERT_ID(job_id)`,
|
ON DUPLICATE KEY UPDATE job_id = LAST_INSERT_ID(job_id)`,
|
||||||
[departmentId, designation || "Trainee", designation || "Trainee"]
|
[departmentId, designation || "Trainee"]
|
||||||
);
|
);
|
||||||
const jobId = jobResult.insertId;
|
const jobId = jobResult.insertId;
|
||||||
|
|
||||||
// 4. Employee
|
// 4. Create core Employee record
|
||||||
const [empResult]: any = await connection.execute(
|
const [empResult]: any = await connection.execute(
|
||||||
`INSERT INTO employees (employee_code, partner_id, company_id, branch_id, is_active)
|
`INSERT INTO employees (employee_code, partner_id, company_id, branch_id, is_active)
|
||||||
VALUES (?, ?, 1, 1, TRUE)
|
VALUES (?, ?, 1, 1, TRUE)
|
||||||
@ -55,63 +61,66 @@ export const bulkSeedEmployees = async (ctx: Context) => {
|
|||||||
);
|
);
|
||||||
const employeeId = empResult.insertId;
|
const employeeId = empResult.insertId;
|
||||||
|
|
||||||
// 5. Terms & Assignments (Replaces old contracts insert)
|
// 5. Establish operational Contract
|
||||||
await connection.execute(
|
await connection.execute(
|
||||||
`INSERT INTO employment_terms (employee_id, work_email, date_joining, probation_days, status, salary_structure_id)
|
`INSERT INTO contracts (employee_id, department_id, job_id, work_email, date_joining, status, salary_structure_id)
|
||||||
VALUES (?, ?, '2026-01-01', 90, 'ACTIVE', 100)
|
VALUES (?, ?, ?, ?, '2026-01-01', 'ACTIVE', 100)
|
||||||
ON DUPLICATE KEY UPDATE work_email = VALUES(work_email)`,
|
ON DUPLICATE KEY UPDATE work_email = VALUES(work_email)`,
|
||||||
[employeeId, email]
|
[employeeId, departmentId, jobId, email]
|
||||||
);
|
|
||||||
|
|
||||||
await connection.execute(
|
|
||||||
`INSERT INTO employee_assignments (employee_id, department_id, job_id, reporting_to_id, change_reason, effective_from, is_current)
|
|
||||||
VALUES (?, ?, ?, NULL, 'HIRE', '2026-01-01', TRUE)
|
|
||||||
ON DUPLICATE KEY UPDATE department_id = VALUES(department_id), job_id = VALUES(job_id)`,
|
|
||||||
[employeeId, departmentId, jobId]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
insertedCount++;
|
insertedCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
await connection.commit();
|
await connection.commit();
|
||||||
|
|
||||||
ctx.response.status = 200;
|
ctx.response.status = 200;
|
||||||
ctx.response.body = { success: true, message: `Successfully seeded/aligned ${insertedCount} normalized employee profiles.` };
|
ctx.response.body = {
|
||||||
|
success: true,
|
||||||
|
message: `Successfully seeded/aligned ${insertedCount} normalized employee profiles.`,
|
||||||
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await connection.rollback();
|
await connection.rollback();
|
||||||
console.error("Seeding failed, changes rolled back:", error);
|
console.error("Seeding failed, changes rolled back:", error);
|
||||||
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
||||||
ctx.response.status = 500;
|
ctx.response.status = 500;
|
||||||
ctx.response.body = { success: false, error: errorMessage };
|
ctx.response.body = { success: false, error: errorMessage };
|
||||||
} finally { connection.release(); }
|
} finally {
|
||||||
|
connection.release();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const bulkMapHierarchy = async (ctx: Context) => {
|
export const bulkMapHierarchy = async (ctx: Context) => {
|
||||||
const body = ctx.request.hasBody ? await ctx.request.body.json() : {};
|
const body = ctx.request.hasBody ? await ctx.request.body.json() : {};
|
||||||
const { employeeHierarchy = [], departmentHierarchy = [] } = body;
|
const { employeeHierarchy = [], departmentHierarchy = [] } = body;
|
||||||
|
|
||||||
if (employeeHierarchy.length === 0 && departmentHierarchy.length === 0) { ctx.response.status = 400; ctx.response.body = { success: false, message: "Provide hierarchy mapping arrays." }; return; }
|
if (employeeHierarchy.length === 0 && departmentHierarchy.length === 0) {
|
||||||
|
ctx.response.status = 400;
|
||||||
|
ctx.response.body = { success: false, message: "Provide hierarchy mapping arrays." };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const connection = await emsPool.getConnection();
|
const connection = await emsPool.getConnection();
|
||||||
let assignmentsUpdated = 0;
|
let contractsUpdated = 0;
|
||||||
let departmentsUpdated = 0;
|
let departmentsUpdated = 0;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await connection.beginTransaction();
|
await connection.beginTransaction();
|
||||||
|
|
||||||
// 1. Map Employees to their Managers (Updating active assignments)
|
// 1. Map Employees to their Managers (Contracts table)
|
||||||
for (const mapping of employeeHierarchy) {
|
for (const mapping of employeeHierarchy) {
|
||||||
const { emp_code, manager_code } = mapping;
|
const { emp_code, manager_code } = mapping;
|
||||||
if (!emp_code || !manager_code) continue;
|
if (!emp_code || !manager_code) continue;
|
||||||
|
|
||||||
const [result]: any = await connection.execute(
|
const [result]: any = await connection.execute(
|
||||||
`UPDATE employee_assignments ea
|
`UPDATE contracts c
|
||||||
JOIN employees e ON ea.employee_id = e.employee_id
|
JOIN employees e ON c.employee_id = e.employee_id
|
||||||
JOIN employees m ON m.employee_code = ?
|
JOIN employees m ON m.employee_code = ?
|
||||||
SET ea.reporting_to_id = m.employee_id
|
SET c.reporting_to_id = m.employee_id
|
||||||
WHERE e.employee_code = ? AND ea.is_current = TRUE`,
|
WHERE e.employee_code = ? AND c.status = 'ACTIVE'`,
|
||||||
[manager_code, emp_code]
|
[manager_code, emp_code]
|
||||||
);
|
);
|
||||||
if (result.affectedRows > 0) assignmentsUpdated++;
|
if (result.affectedRows > 0) contractsUpdated++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Map Departments to Managers and Parent Departments
|
// 2. Map Departments to Managers and Parent Departments
|
||||||
@ -119,10 +128,11 @@ export const bulkMapHierarchy = async (ctx: Context) => {
|
|||||||
const { department_name, manager_code, parent_department_name } = dept;
|
const { department_name, manager_code, parent_department_name } = dept;
|
||||||
if (!department_name) continue;
|
if (!department_name) continue;
|
||||||
|
|
||||||
|
// FIXED: Use department_managers junction table instead of departments.manager_id
|
||||||
if (manager_code) {
|
if (manager_code) {
|
||||||
await connection.execute(
|
await connection.execute(
|
||||||
`INSERT IGNORE INTO department_managers (department_id, employee_id, is_current)
|
`INSERT IGNORE INTO department_managers (department_id, employee_id)
|
||||||
SELECT d.department_id, m.employee_id, TRUE
|
SELECT d.department_id, m.employee_id
|
||||||
FROM departments d
|
FROM departments d
|
||||||
JOIN employees m ON m.employee_code = ?
|
JOIN employees m ON m.employee_code = ?
|
||||||
WHERE d.name = ?`,
|
WHERE d.name = ?`,
|
||||||
@ -130,6 +140,7 @@ export const bulkMapHierarchy = async (ctx: Context) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update Parent Department Hierarchy
|
||||||
if (parent_department_name) {
|
if (parent_department_name) {
|
||||||
await connection.execute(
|
await connection.execute(
|
||||||
`UPDATE departments d
|
`UPDATE departments d
|
||||||
@ -143,13 +154,20 @@ export const bulkMapHierarchy = async (ctx: Context) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await connection.commit();
|
await connection.commit();
|
||||||
|
|
||||||
ctx.response.status = 200;
|
ctx.response.status = 200;
|
||||||
ctx.response.body = { success: true, message: "Hierarchy mapping completed successfully.", metrics: { assignmentsUpdated, departmentsProcessed: departmentsUpdated } };
|
ctx.response.body = {
|
||||||
|
success: true,
|
||||||
|
message: "Hierarchy mapping completed successfully.",
|
||||||
|
metrics: { contractsUpdated, departmentsProcessed: departmentsUpdated }
|
||||||
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await connection.rollback();
|
await connection.rollback();
|
||||||
console.error("Hierarchy mapping failed:", error);
|
console.error("Hierarchy mapping failed:", error);
|
||||||
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
||||||
ctx.response.status = 500;
|
ctx.response.status = 500;
|
||||||
ctx.response.body = { success: false, error: errorMessage };
|
ctx.response.body = { success: false, error: errorMessage };
|
||||||
} finally { connection.release(); }
|
} finally {
|
||||||
|
connection.release();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
@ -1,7 +1,10 @@
|
|||||||
import { Router } from "@oak/oak";
|
import { Router } from "@oak/oak";
|
||||||
import {getDashboardMetrics} from "./controllers/dashboard.controller.ts"
|
|
||||||
import {
|
import {
|
||||||
getEmployees, createEmployee, getEmployeeById, updateEmployee, deleteEmployee, getEmployeeHistory
|
getEmployees,
|
||||||
|
createEmployee,
|
||||||
|
getEmployeeById,
|
||||||
|
updateEmployee,
|
||||||
|
deleteEmployee
|
||||||
} from "./controllers/employee.controller.ts";
|
} from "./controllers/employee.controller.ts";
|
||||||
import {
|
import {
|
||||||
getCompanies, createCompany, updateCompany, deleteCompany,
|
getCompanies, createCompany, updateCompany, deleteCompany,
|
||||||
@ -10,54 +13,88 @@ import {
|
|||||||
getJobs, createJob, updateJob, deleteJob
|
getJobs, createJob, updateJob, deleteJob
|
||||||
} from "./controllers/lookup.controller.ts";
|
} from "./controllers/lookup.controller.ts";
|
||||||
import {
|
import {
|
||||||
getEmployeeContracts, createContract
|
getEmployeeContracts,
|
||||||
|
createContract
|
||||||
} from "./controllers/contract.controller.ts";
|
} from "./controllers/contract.controller.ts";
|
||||||
import {
|
import {
|
||||||
bulkSeedEmployees, bulkMapHierarchy
|
bulkSeedEmployees,
|
||||||
|
bulkMapHierarchy
|
||||||
} from "./controllers/system.controller.ts";
|
} from "./controllers/system.controller.ts";
|
||||||
|
|
||||||
|
// Shared authentication and authorization middleware
|
||||||
import { requireAuth, requireRole, AppRole } from "../shared/auth.ts";
|
import { requireAuth, requireRole, AppRole } from "../shared/auth.ts";
|
||||||
|
|
||||||
const router = new Router();
|
const router = new Router();
|
||||||
|
|
||||||
// EMPLOYEE CORE
|
// ============================================================================
|
||||||
router.get("/employees", requireAuth, requireRole([AppRole.SUPER_ADMIN, AppRole.ADMIN, AppRole.MANAGER]), getEmployees);
|
// EMPLOYEE CORE DOMAIN
|
||||||
router.get("/employees/:id", requireAuth, getEmployeeById);
|
// Manages the corporate identity (employees) and personal data (partners).
|
||||||
router.get("/employees/:id/history", requireAuth, getEmployeeHistory);
|
// ============================================================================
|
||||||
router.post("/employees", requireAuth, requireRole([AppRole.SUPER_ADMIN, AppRole.ADMIN]), createEmployee);
|
|
||||||
router.put("/employees/:id", requireAuth, requireRole([AppRole.SUPER_ADMIN, AppRole.ADMIN]), updateEmployee);
|
|
||||||
router.delete("/employees/:id", requireAuth, requireRole([AppRole.SUPER_ADMIN, AppRole.ADMIN]), deleteEmployee);
|
|
||||||
|
|
||||||
// CONTRACT & ASSIGNMENT DOMAIN (Promotions/Transfers)
|
// Retrieve a list of all active employees. Visible to management and administrators.
|
||||||
router.get("/employees/:id/contracts", requireAuth, getEmployeeContracts);
|
router.get("/api/v1/employees", requireAuth, requireRole([AppRole.SUPER_ADMIN, AppRole.ADMIN, AppRole.MANAGER]), getEmployees);
|
||||||
router.post("/contracts", requireAuth, requireRole([AppRole.SUPER_ADMIN, AppRole.ADMIN]), createContract);
|
|
||||||
|
|
||||||
|
// Retrieve a single employee's comprehensive profile (identity, address, contract). Accessible to any authenticated user.
|
||||||
|
router.get("/api/v1/employees/:id", requireAuth, getEmployeeById);
|
||||||
|
|
||||||
|
// Create a new employee profile and initial contract. Restricted to administrators.
|
||||||
|
router.post("/api/v1/employees", requireAuth, requireRole([AppRole.SUPER_ADMIN, AppRole.ADMIN]), createEmployee);
|
||||||
|
|
||||||
|
// Update an existing employee's personal or address data. Restricted to administrators.
|
||||||
|
router.put("/api/v1/employees/:id", requireAuth, requireRole([AppRole.SUPER_ADMIN, AppRole.ADMIN]), updateEmployee);
|
||||||
|
|
||||||
|
// Soft-delete (deactivate) an employee account. Restricted to administrators.
|
||||||
|
router.delete("/api/v1/employees/:id", requireAuth, requireRole([AppRole.SUPER_ADMIN, AppRole.ADMIN]), deleteEmployee);
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// CONTRACT & ASSIGNMENT DOMAIN
|
||||||
|
// Manages operational assignments, reporting hierarchies, and job roles.
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// Retrieve the contract history for an individual employee.
|
||||||
|
router.get("/api/v1/employees/:id/contracts", requireAuth, getEmployeeContracts);
|
||||||
|
|
||||||
|
// Expire current active contracts and execute a new employment agreement.
|
||||||
|
router.post("/api/v1/contracts", requireAuth, requireRole([AppRole.SUPER_ADMIN, AppRole.ADMIN]), createContract);
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
// ORGANIZATIONAL LOOKUP DOMAIN
|
// ORGANIZATIONAL LOOKUP DOMAIN
|
||||||
router.get("/companies", requireAuth, getCompanies);
|
// Read-only reference endpoints for the frontend UI to populate select options.
|
||||||
router.post("/companies", requireAuth, requireRole([AppRole.SUPER_ADMIN, AppRole.ADMIN]), createCompany);
|
// ============================================================================
|
||||||
router.put("/companies/:id", requireAuth, requireRole([AppRole.SUPER_ADMIN, AppRole.ADMIN]), updateCompany);
|
|
||||||
router.delete("/companies/:id", requireAuth, requireRole([AppRole.SUPER_ADMIN, AppRole.ADMIN]), deleteCompany);
|
|
||||||
|
|
||||||
router.get("/branches", requireAuth, getBranches);
|
// Fetch legal entities and parent group structures.
|
||||||
router.post("/branches", requireAuth, requireRole([AppRole.SUPER_ADMIN, AppRole.ADMIN]), createBranch);
|
router.get("/api/v1/companies", requireAuth, getCompanies);
|
||||||
router.put("/branches/:id", requireAuth, requireRole([AppRole.SUPER_ADMIN, AppRole.ADMIN]), updateBranch);
|
router.post("/api/v1/companies", requireAuth, requireRole([AppRole.SUPER_ADMIN, AppRole.ADMIN]), createCompany);
|
||||||
router.delete("/branches/:id", requireAuth, requireRole([AppRole.SUPER_ADMIN, AppRole.ADMIN]), deleteBranch);
|
router.put("/api/v1/companies/:id", requireAuth, requireRole([AppRole.SUPER_ADMIN, AppRole.ADMIN]), updateCompany);
|
||||||
|
router.delete("/api/v1/companies/:id", requireAuth, requireRole([AppRole.SUPER_ADMIN, AppRole.ADMIN]), deleteCompany);
|
||||||
|
|
||||||
router.get("/departments", requireAuth, getDepartments);
|
// Fetch structural branch offices and physical locations.
|
||||||
router.post("/departments", requireAuth, requireRole([AppRole.SUPER_ADMIN, AppRole.ADMIN]), createDepartment);
|
router.get("/api/v1/branches", requireAuth, getBranches);
|
||||||
router.put("/departments/:id", requireAuth, requireRole([AppRole.SUPER_ADMIN, AppRole.ADMIN]), updateDepartment);
|
router.post("/api/v1/branches", requireAuth, requireRole([AppRole.SUPER_ADMIN, AppRole.ADMIN]), createBranch);
|
||||||
router.delete("/departments/:id", requireAuth, requireRole([AppRole.SUPER_ADMIN, AppRole.ADMIN]), deleteDepartment);
|
router.put("/api/v1/branches/:id", requireAuth, requireRole([AppRole.SUPER_ADMIN, AppRole.ADMIN]), updateBranch);
|
||||||
|
router.delete("/api/v1/branches/:id", requireAuth, requireRole([AppRole.SUPER_ADMIN, AppRole.ADMIN]), deleteBranch);
|
||||||
|
|
||||||
router.get("/jobs", requireAuth, getJobs);
|
// List corporate departments and organizational chart reporting lines.
|
||||||
router.post("/jobs", requireAuth, requireRole([AppRole.SUPER_ADMIN, AppRole.ADMIN]), createJob);
|
router.get("/api/v1/departments", requireAuth, getDepartments);
|
||||||
router.put("/jobs/:id", requireAuth, requireRole([AppRole.SUPER_ADMIN, AppRole.ADMIN]), updateJob);
|
router.post("/api/v1/departments", requireAuth, requireRole([AppRole.SUPER_ADMIN, AppRole.ADMIN]), createDepartment);
|
||||||
router.delete("/jobs/:id", requireAuth, requireRole([AppRole.SUPER_ADMIN, AppRole.ADMIN]), deleteJob);
|
router.put("/api/v1/departments/:id", requireAuth, requireRole([AppRole.SUPER_ADMIN, AppRole.ADMIN]), updateDepartment);
|
||||||
|
router.delete("/api/v1/departments/:id", requireAuth, requireRole([AppRole.SUPER_ADMIN, AppRole.ADMIN]), deleteDepartment);
|
||||||
|
|
||||||
// DASHBOARD METRICS
|
// List company designations and employment titles.
|
||||||
// Example: GET /dashboard/metrics?company_id=1&branch_id=2
|
router.get("/api/v1/jobs", requireAuth, getJobs);
|
||||||
router.get("/dashboard/metrics", requireAuth, getDashboardMetrics);
|
router.post("/api/v1/jobs", requireAuth, requireRole([AppRole.SUPER_ADMIN, AppRole.ADMIN]), createJob);
|
||||||
|
router.put("/api/v1/jobs/:id", requireAuth, requireRole([AppRole.SUPER_ADMIN, AppRole.ADMIN]), updateJob);
|
||||||
|
router.delete("/api/v1/jobs/:id", requireAuth, requireRole([AppRole.SUPER_ADMIN, AppRole.ADMIN]), deleteJob);
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
// SYSTEM & MIGRATION DOMAIN
|
// SYSTEM & MIGRATION DOMAIN
|
||||||
router.post("/system/seed-employees", requireAuth, requireRole([AppRole.SUPER_ADMIN]), bulkSeedEmployees);
|
// High-risk administrative endpoints for bulk data execution.
|
||||||
router.post("/system/map-hierarchy", requireAuth, requireRole([AppRole.SUPER_ADMIN]), bulkMapHierarchy);
|
// ============================================================================
|
||||||
|
|
||||||
|
// Process raw bulk data to seed initial employee/partner structures into the DB.
|
||||||
|
router.post("/api/v1/system/seed-employees", requireAuth, requireRole([AppRole.SUPER_ADMIN]), bulkSeedEmployees);
|
||||||
|
|
||||||
|
// Execute mapping script to establish reporting_to_id and manager_id links.
|
||||||
|
router.post("/api/v1/system/map-hierarchy", requireAuth, requireRole([AppRole.SUPER_ADMIN]), bulkMapHierarchy);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
@ -4,10 +4,8 @@ USE hrms_ems;
|
|||||||
-- 1. Legal Entities
|
-- 1. Legal Entities
|
||||||
CREATE TABLE companies (
|
CREATE TABLE companies (
|
||||||
company_id INT AUTO_INCREMENT PRIMARY KEY,
|
company_id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
company_code VARCHAR(20) NOT NULL UNIQUE,
|
|
||||||
name VARCHAR(100) NOT NULL,
|
name VARCHAR(100) NOT NULL,
|
||||||
parent_id INT NULL,
|
parent_id INT NULL,
|
||||||
is_active BOOLEAN DEFAULT TRUE,
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
FOREIGN KEY (parent_id) REFERENCES companies(company_id) ON DELETE SET NULL
|
FOREIGN KEY (parent_id) REFERENCES companies(company_id) ON DELETE SET NULL
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
@ -18,7 +16,6 @@ CREATE TABLE branches (
|
|||||||
company_id INT NOT NULL,
|
company_id INT NOT NULL,
|
||||||
branch_name VARCHAR(100) NOT NULL,
|
branch_name VARCHAR(100) NOT NULL,
|
||||||
code VARCHAR(20) NOT NULL UNIQUE,
|
code VARCHAR(20) NOT NULL UNIQUE,
|
||||||
is_active BOOLEAN DEFAULT TRUE,
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
FOREIGN KEY (company_id) REFERENCES companies(company_id) ON DELETE CASCADE
|
FOREIGN KEY (company_id) REFERENCES companies(company_id) ON DELETE CASCADE
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
@ -47,7 +44,6 @@ CREATE TABLE addresses (
|
|||||||
pincode VARCHAR(10) NOT NULL,
|
pincode VARCHAR(10) NOT NULL,
|
||||||
district VARCHAR(50) NOT NULL,
|
district VARCHAR(50) NOT NULL,
|
||||||
state VARCHAR(50) NOT NULL,
|
state VARCHAR(50) NOT NULL,
|
||||||
UNIQUE KEY uq_partner_address_type (partner_id, address_type),
|
|
||||||
FOREIGN KEY (partner_id) REFERENCES partners(partner_id) ON DELETE CASCADE
|
FOREIGN KEY (partner_id) REFERENCES partners(partner_id) ON DELETE CASCADE
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
@ -56,10 +52,8 @@ CREATE TABLE departments (
|
|||||||
department_id INT AUTO_INCREMENT PRIMARY KEY,
|
department_id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
company_id INT NOT NULL,
|
company_id INT NOT NULL,
|
||||||
branch_id INT NOT NULL,
|
branch_id INT NOT NULL,
|
||||||
department_code VARCHAR(20) NOT NULL UNIQUE,
|
|
||||||
name VARCHAR(50) NOT NULL,
|
name VARCHAR(50) NOT NULL,
|
||||||
parent_id INT NULL,
|
parent_id INT NULL,
|
||||||
is_active BOOLEAN DEFAULT TRUE,
|
|
||||||
FOREIGN KEY (company_id) REFERENCES companies(company_id) ON DELETE CASCADE,
|
FOREIGN KEY (company_id) REFERENCES companies(company_id) ON DELETE CASCADE,
|
||||||
FOREIGN KEY (branch_id) REFERENCES branches(branch_id) ON DELETE CASCADE,
|
FOREIGN KEY (branch_id) REFERENCES branches(branch_id) ON DELETE CASCADE,
|
||||||
FOREIGN KEY (parent_id) REFERENCES departments(department_id) ON DELETE SET NULL
|
FOREIGN KEY (parent_id) REFERENCES departments(department_id) ON DELETE SET NULL
|
||||||
@ -69,10 +63,8 @@ CREATE TABLE departments (
|
|||||||
CREATE TABLE job_positions (
|
CREATE TABLE job_positions (
|
||||||
job_id INT AUTO_INCREMENT PRIMARY KEY,
|
job_id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
department_id INT NOT NULL,
|
department_id INT NOT NULL,
|
||||||
job_code VARCHAR(20) NOT NULL UNIQUE,
|
|
||||||
title VARCHAR(50) NOT NULL,
|
title VARCHAR(50) NOT NULL,
|
||||||
description TEXT NULL,
|
description TEXT NULL,
|
||||||
is_active BOOLEAN DEFAULT TRUE,
|
|
||||||
FOREIGN KEY (department_id) REFERENCES departments(department_id) ON DELETE CASCADE
|
FOREIGN KEY (department_id) REFERENCES departments(department_id) ON DELETE CASCADE
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
@ -90,61 +82,34 @@ CREATE TABLE employees (
|
|||||||
FOREIGN KEY (branch_id) REFERENCES branches(branch_id) ON DELETE RESTRICT
|
FOREIGN KEY (branch_id) REFERENCES branches(branch_id) ON DELETE RESTRICT
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
-- 8. Legal & Compensation Terms (Changes rarely)
|
-- 8. Legal & Operational Contracts
|
||||||
CREATE TABLE employment_terms (
|
CREATE TABLE contracts (
|
||||||
term_id INT AUTO_INCREMENT PRIMARY KEY,
|
contract_id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
employee_id INT NOT NULL,
|
|
||||||
work_email VARCHAR(100) NOT NULL,
|
|
||||||
date_joining DATE NOT NULL,
|
|
||||||
date_ended DATE NULL,
|
|
||||||
probation_days INT DEFAULT 90,
|
|
||||||
salary_structure_id INT NOT NULL,
|
|
||||||
status ENUM('DRAFT', 'ACTIVE', 'EXPIRED', 'TERMINATED') DEFAULT 'DRAFT',
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
FOREIGN KEY (employee_id) REFERENCES employees(employee_id) ON DELETE CASCADE
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
-- 9. Organizational Assignments (Changes on promotion/transfer)
|
|
||||||
CREATE TABLE employee_assignments (
|
|
||||||
assignment_id INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
employee_id INT NOT NULL,
|
employee_id INT NOT NULL,
|
||||||
department_id INT NOT NULL,
|
department_id INT NOT NULL,
|
||||||
job_id INT NOT NULL,
|
job_id INT NOT NULL,
|
||||||
reporting_to_id INT NULL,
|
reporting_to_id INT NULL,
|
||||||
change_reason ENUM('HIRE', 'TRANSFER', 'PROMOTION', 'MANAGER_CHANGE', 'REORG') NOT NULL,
|
work_email VARCHAR(100) NOT NULL UNIQUE,
|
||||||
effective_from DATE NOT NULL,
|
date_joining DATE NOT NULL,
|
||||||
effective_to DATE NULL,
|
probation_days INT DEFAULT 90,
|
||||||
is_current BOOLEAN NOT NULL DEFAULT TRUE,
|
status ENUM('DRAFT', 'ACTIVE', 'EXPIRED', 'TERMINATED') DEFAULT 'DRAFT',
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
salary_structure_id INT NOT NULL,
|
||||||
FOREIGN KEY (employee_id) REFERENCES employees(employee_id) ON DELETE CASCADE,
|
FOREIGN KEY (employee_id) REFERENCES employees(employee_id) ON DELETE CASCADE,
|
||||||
FOREIGN KEY (department_id) REFERENCES departments(department_id) ON DELETE RESTRICT,
|
FOREIGN KEY (department_id) REFERENCES departments(department_id) ON DELETE RESTRICT,
|
||||||
FOREIGN KEY (job_id) REFERENCES job_positions(job_id) ON DELETE RESTRICT,
|
FOREIGN KEY (job_id) REFERENCES job_positions(job_id) ON DELETE RESTRICT
|
||||||
FOREIGN KEY (reporting_to_id) REFERENCES employees(employee_id) ON DELETE SET NULL
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
-- 10. Department Managers (Junction Table with History)
|
-- 9. Department Managers (Junction Table)
|
||||||
CREATE TABLE department_managers (
|
CREATE TABLE department_managers (
|
||||||
department_id INT NOT NULL,
|
department_id INT NOT NULL,
|
||||||
employee_id INT NOT NULL,
|
employee_id INT NOT NULL,
|
||||||
assigned_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
assigned_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
is_current BOOLEAN NOT NULL DEFAULT TRUE,
|
|
||||||
removed_at TIMESTAMP NULL,
|
|
||||||
PRIMARY KEY (department_id, employee_id),
|
PRIMARY KEY (department_id, employee_id),
|
||||||
FOREIGN KEY (department_id) REFERENCES departments(department_id) ON DELETE CASCADE,
|
FOREIGN KEY (department_id) REFERENCES departments(department_id) ON DELETE CASCADE,
|
||||||
FOREIGN KEY (employee_id) REFERENCES employees(employee_id) ON DELETE CASCADE
|
FOREIGN KEY (employee_id) REFERENCES employees(employee_id) ON DELETE CASCADE
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
-- 11. Branch Admins (For AMS/LMS routing)
|
-- 10. System Sequences
|
||||||
CREATE TABLE branch_admins (
|
|
||||||
employee_id INT NOT NULL,
|
|
||||||
branch_id INT NOT NULL,
|
|
||||||
assigned_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
PRIMARY KEY (employee_id, branch_id),
|
|
||||||
FOREIGN KEY (employee_id) REFERENCES employees(employee_id) ON DELETE CASCADE,
|
|
||||||
FOREIGN KEY (branch_id) REFERENCES branches(branch_id) ON DELETE CASCADE
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
-- 12. System Sequences
|
|
||||||
CREATE TABLE system_sequences (
|
CREATE TABLE system_sequences (
|
||||||
sequence_id VARCHAR(50) PRIMARY KEY,
|
sequence_id VARCHAR(50) PRIMARY KEY,
|
||||||
prefix VARCHAR(10) NOT NULL,
|
prefix VARCHAR(10) NOT NULL,
|
||||||
|
|||||||
@ -97,6 +97,45 @@ export const getLeaveHistory = async (ctx: Context) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* EMPLOYEE: Fetch upcoming optional holidays
|
||||||
|
*/
|
||||||
|
// export const getValidOptionalHolidays = async (ctx: Context) => {
|
||||||
|
// const user = ctx.state.user;
|
||||||
|
// const currentYear = new Date().getFullYear();
|
||||||
|
// const today = new Date().toISOString().split('T')[0];
|
||||||
|
|
||||||
|
// try {
|
||||||
|
// // Fetch optional holidays for the employee's company/branch that are today or in the future
|
||||||
|
// const [rows]: any = await db.execute(
|
||||||
|
// `SELECT
|
||||||
|
// holiday_id,
|
||||||
|
// holiday_name,
|
||||||
|
// DATE_FORMAT(holiday_date, '%Y-%m-%d') as holiday_date
|
||||||
|
// FROM company_holidays
|
||||||
|
// WHERE company_id = ?
|
||||||
|
// AND calendar_year = ?
|
||||||
|
// AND holiday_type = 'OPTIONAL'
|
||||||
|
// AND (branch_id = ? OR branch_id IS NULL)
|
||||||
|
// AND holiday_date >= ?
|
||||||
|
// ORDER BY holiday_date ASC`,
|
||||||
|
// [user.company_id, currentYear, user.branch_id, today] // Assuming auth.ts injects company_id and branch_id
|
||||||
|
// );
|
||||||
|
|
||||||
|
// ctx.response.status = 200;
|
||||||
|
// ctx.response.body = {
|
||||||
|
// success: true,
|
||||||
|
// count: rows.length,
|
||||||
|
// holidays: rows
|
||||||
|
// };
|
||||||
|
// } catch (error) {
|
||||||
|
// console.error("Failed to fetch optional holidays:", error);
|
||||||
|
// ctx.response.status = 500;
|
||||||
|
// ctx.response.body = { success: false, error: error instanceof Error ? error.message : "Unknown error" };
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* EMPLOYEE: Fetch upcoming optional holidays
|
* EMPLOYEE: Fetch upcoming optional holidays
|
||||||
*/
|
*/
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user