Compare commits
2 Commits
c8c645f7a6
...
bf85053ba4
| Author | SHA1 | Date | |
|---|---|---|---|
| bf85053ba4 | |||
| 34cee06952 |
@ -4,50 +4,33 @@ const pool = getDbPool("hrms_ems");
|
||||
|
||||
export const getEmployeeContracts = async (ctx: any) => {
|
||||
const employeeId = ctx.params.id;
|
||||
|
||||
try {
|
||||
// Fetches combined history of terms and assignments
|
||||
const [rows] = await pool.query(
|
||||
`SELECT
|
||||
c.contract_id,
|
||||
c.work_email,
|
||||
c.date_joining,
|
||||
c.probation_days,
|
||||
c.status,
|
||||
c.salary_structure_id,
|
||||
d.name AS department,
|
||||
j.title AS designation
|
||||
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`,
|
||||
et.term_id, et.work_email, et.date_joining, et.probation_days, et.status, et.salary_structure_id,
|
||||
ea.assignment_id, ea.change_reason, ea.effective_from, ea.effective_to, ea.is_current,
|
||||
d.name AS department, j.title AS designation
|
||||
FROM employment_terms et
|
||||
JOIN employee_assignments ea ON et.employee_id = ea.employee_id
|
||||
JOIN departments d ON ea.department_id = d.department_id
|
||||
JOIN job_positions j ON ea.job_id = j.job_id
|
||||
WHERE et.employee_id = ?
|
||||
ORDER BY et.date_joining DESC, ea.effective_from DESC`,
|
||||
[employeeId]
|
||||
);
|
||||
|
||||
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) {
|
||||
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) => {
|
||||
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 connection = await pool.getConnection();
|
||||
@ -55,46 +38,31 @@ export const createContract = async (ctx: any) => {
|
||||
try {
|
||||
await connection.beginTransaction();
|
||||
|
||||
// 1. Expire any currently active contracts for this specific employee
|
||||
// 1. Expire old assignment
|
||||
await connection.execute(
|
||||
`UPDATE contracts SET status = 'EXPIRED' WHERE employee_id = ? AND status = 'ACTIVE'`,
|
||||
`UPDATE employee_assignments SET is_current = FALSE, effective_to = CURDATE() WHERE employee_id = ? AND is_current = TRUE`,
|
||||
[data.employeeId]
|
||||
);
|
||||
|
||||
// 2. Insert the brand new active contract
|
||||
const [result] = await connection.execute(
|
||||
`INSERT INTO contracts (employee_id, department_id, job_id, work_email, reporting_to_id, date_joining, probation_days, status, salary_structure_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[
|
||||
data.employeeId,
|
||||
data.departmentId,
|
||||
data.jobId,
|
||||
data.workEmail, // <-- Added
|
||||
data.reportingToId, // <-- Added
|
||||
data.dateJoining,
|
||||
data.probationDays,
|
||||
'ACTIVE',
|
||||
data.salaryStructureId
|
||||
]
|
||||
// 2. Insert new assignment (Promotion/Transfer)
|
||||
const [result] = await connection.execute(
|
||||
`INSERT INTO employee_assignments (employee_id, department_id, job_id, reporting_to_id, change_reason, effective_from, is_current)
|
||||
VALUES (?, ?, ?, ?, ?, CURDATE(), TRUE)`,
|
||||
[data.employeeId, data.departmentId, data.jobId, data.reportingToId || null, data.changeReason || 'TRANSFER']
|
||||
);
|
||||
|
||||
await connection.commit();
|
||||
// Optional: If compensation changes, you would expire old employment_terms and insert new ones here.
|
||||
|
||||
await connection.commit();
|
||||
ctx.response.status = 201;
|
||||
ctx.response.body = {
|
||||
success: true,
|
||||
message: "New contract executed successfully. Previous contracts expired.",
|
||||
contract_id: (result as any).insertId
|
||||
message: "New assignment executed successfully. Previous assignments expired.",
|
||||
assignment_id: (result as any).insertId
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
await connection.rollback();
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = {
|
||||
success: false,
|
||||
error: "Transaction failed: " + (error as Error).message
|
||||
};
|
||||
} finally {
|
||||
connection.release();
|
||||
}
|
||||
ctx.response.body = { success: false, error: "Transaction failed: " + (error as Error).message };
|
||||
} finally { connection.release(); }
|
||||
};
|
||||
66
ems-service/controllers/dashboard.controller.ts
Normal file
66
ems-service/controllers/dashboard.controller.ts
Normal file
@ -0,0 +1,66 @@
|
||||
// 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,229 +5,158 @@ const pool = getDbPool("hrms_ems");
|
||||
|
||||
export const getEmployees = async (ctx: any) => {
|
||||
try {
|
||||
// FIX: Filter by ACTIVE contract to prevent duplicate rows for employees with past contracts
|
||||
const [rows] = await pool.query(
|
||||
`SELECT
|
||||
e.employee_id,
|
||||
e.employee_code,
|
||||
p.first_name,
|
||||
p.last_name,
|
||||
d.name AS department,
|
||||
j.title AS designation,
|
||||
c.work_email
|
||||
FROM employees e
|
||||
JOIN partners p ON e.partner_id = p.partner_id
|
||||
JOIN contracts c ON e.employee_id = c.employee_id AND c.status = 'ACTIVE'
|
||||
JOIN departments d ON c.department_id = d.department_id
|
||||
JOIN job_positions j ON c.job_id = j.job_id
|
||||
WHERE e.is_active = true`,
|
||||
);
|
||||
const params = ctx.request.url.searchParams;
|
||||
let query = `SELECT e.employee_id, e.employee_code, e.is_active,
|
||||
CONCAT(p.first_name, ' ', p.last_name) AS full_name,
|
||||
et.work_email, -- <-- ADDED WORK EMAIL HERE
|
||||
j.title AS job_name, d.name AS department_name,
|
||||
b.branch_name, c.name AS company_name
|
||||
FROM employees e
|
||||
JOIN partners p ON e.partner_id = p.partner_id
|
||||
JOIN companies c ON e.company_id = c.company_id
|
||||
JOIN branches b ON e.branch_id = b.branch_id
|
||||
JOIN employment_terms et ON e.employee_id = et.employee_id AND et.status = 'ACTIVE' -- <-- ADDED JOIN
|
||||
JOIN employee_assignments ea ON e.employee_id = ea.employee_id AND ea.is_current = 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[] = [];
|
||||
|
||||
ctx.response.body = {
|
||||
success: true,
|
||||
count: (rows as any[]).length,
|
||||
data: rows,
|
||||
};
|
||||
} catch (error) {
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = {
|
||||
success: false,
|
||||
error: (error as Error).message,
|
||||
};
|
||||
if (params.get('company_id')) { query += ` AND e.company_id = ?`; values.push(params.get('company_id')); }
|
||||
if (params.get('branch_id')) { query += ` AND e.branch_id = ?`; values.push(params.get('branch_id')); }
|
||||
if (params.get('department_id')) { query += ` AND ea.department_id = ?`; values.push(params.get('department_id')); }
|
||||
if (params.get('is_active')) { query += ` AND e.is_active = ?`; values.push(params.get('is_active') === 'true'); }
|
||||
|
||||
const [rows] = await pool.query(query, values);
|
||||
ctx.response.body = { success: true, count: (rows as any[]).length, data: rows };
|
||||
} catch (error) {
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { success: false, error: (error as Error).message };
|
||||
}
|
||||
};
|
||||
|
||||
export const createEmployee = async (ctx: any) => {
|
||||
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();
|
||||
let empCode = data.employeeCode;
|
||||
|
||||
if (!empCode || empCode.trim() === "") {
|
||||
empCode = await generateNextCode("EMPLOYEE_MAIN", "CLRI", 3);
|
||||
}
|
||||
let empCode = data.employeeCode || await generateNextCode("EMPLOYEE_MAIN", "CLRI", 3);
|
||||
|
||||
const connection = await pool.getConnection();
|
||||
|
||||
try {
|
||||
await connection.beginTransaction();
|
||||
|
||||
const [partnerResult] = await connection.execute(
|
||||
`INSERT INTO partners (first_name, last_name, dob, gender, personal_email, personal_phone)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`,
|
||||
[
|
||||
data.firstName,
|
||||
data.lastName,
|
||||
data.dob,
|
||||
data.gender,
|
||||
data.personalEmail,
|
||||
data.personalPhone,
|
||||
],
|
||||
const [partnerResult]: any = await connection.execute(
|
||||
`INSERT INTO partners (first_name, last_name, dob, gender, personal_email, personal_phone) VALUES (?, ?, ?, ?, ?, ?)`,
|
||||
[data.firstName, data.lastName, data.dob, data.gender, data.personalEmail, data.personalPhone]
|
||||
);
|
||||
const partnerId = (partnerResult as any).insertId;
|
||||
const partnerId = partnerResult.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(
|
||||
`INSERT INTO addresses (partner_id, address_type, door_number, landmark, address_line, pincode, district, state)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[
|
||||
partnerId,
|
||||
data.address.type,
|
||||
data.address.doorNumber,
|
||||
data.address.landmark,
|
||||
data.address.line,
|
||||
data.address.pincode,
|
||||
data.address.district,
|
||||
data.address.state,
|
||||
],
|
||||
`INSERT INTO employment_terms (employee_id, work_email, date_joining, probation_days, status, salary_structure_id)
|
||||
VALUES (?, ?, ?, ?, 'ACTIVE', ?)`,
|
||||
[employeeId, data.work_email, data.dateJoining, data.probationDays, data.salaryStructureId]
|
||||
);
|
||||
|
||||
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(
|
||||
`INSERT INTO contracts (employee_id, department_id, job_id, work_email, date_joining, probation_days, status, salary_structure_id, reporting_to_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[
|
||||
employeeId,
|
||||
data.departmentId,
|
||||
data.jobId,
|
||||
data.work_email,
|
||||
data.dateJoining,
|
||||
data.probationDays,
|
||||
"ACTIVE",
|
||||
data.salaryStructureId,
|
||||
data.reportingToId || null,
|
||||
],
|
||||
`INSERT INTO employee_assignments (employee_id, department_id, job_id, reporting_to_id, change_reason, effective_from, is_current)
|
||||
VALUES (?, ?, ?, ?, 'HIRE', ?, TRUE)`,
|
||||
[employeeId, data.departmentId, data.jobId, data.reportingToId || null, data.dateJoining]
|
||||
);
|
||||
|
||||
await connection.commit();
|
||||
|
||||
ctx.response.status = 201;
|
||||
ctx.response.body = {
|
||||
success: true,
|
||||
message: "Employee created successfully",
|
||||
employee_id: employeeId,
|
||||
employee_code: empCode,
|
||||
};
|
||||
ctx.response.body = { success: true, message: "Employee created", employee_id: employeeId, employee_code: empCode };
|
||||
} catch (error) {
|
||||
await connection.rollback();
|
||||
let errorMessage = (error as Error).message;
|
||||
if (errorMessage.includes("Duplicate entry")) {
|
||||
ctx.response.status = 409;
|
||||
ctx.response.body = {
|
||||
success: false,
|
||||
error:
|
||||
"A unique constraint failed. The Employee Code, Personal Email, or Phone number already exists.",
|
||||
};
|
||||
ctx.response.body = { success: false, error: "A unique constraint failed. Employee Code, Email, or Phone already exists." };
|
||||
return;
|
||||
}
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = {
|
||||
success: false,
|
||||
error: "Transaction failed: " + errorMessage,
|
||||
};
|
||||
} finally {
|
||||
connection.release();
|
||||
ctx.response.body = { success: false, error: "Transaction failed: " + errorMessage };
|
||||
} finally { connection.release(); }
|
||||
};
|
||||
|
||||
export const getEmployeeHistory = async (ctx: any) => {
|
||||
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) => {
|
||||
const id = ctx.params.id;
|
||||
|
||||
try {
|
||||
// 1. Fetch core employee, partner, contract, and manager details
|
||||
// FIX: Querying the new split tables (employment_terms & employee_assignments)
|
||||
const [empRows] = await pool.query(
|
||||
`SELECT
|
||||
e.employee_id,
|
||||
e.employee_code,
|
||||
e.is_active,
|
||||
p.first_name,
|
||||
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
|
||||
e.employee_id, e.employee_code, e.is_active,
|
||||
p.first_name, p.last_name, p.dob, p.gender, p.personal_email, p.personal_phone,
|
||||
et.work_email, et.date_joining, et.probation_days, et.status AS contract_status, et.salary_structure_id,
|
||||
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
|
||||
JOIN partners p ON e.partner_id = p.partner_id
|
||||
JOIN contracts c ON e.employee_id = c.employee_id AND c.status = 'ACTIVE'
|
||||
JOIN departments d ON c.department_id = d.department_id
|
||||
JOIN job_positions j ON c.job_id = j.job_id
|
||||
LEFT JOIN employees mgr_e ON c.reporting_to_id = mgr_e.employee_id
|
||||
JOIN employment_terms et ON e.employee_id = et.employee_id AND et.status = 'ACTIVE'
|
||||
JOIN employee_assignments ea ON e.employee_id = ea.employee_id AND ea.is_current = TRUE
|
||||
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 e.employee_id = ?`,
|
||||
[id],
|
||||
[id]
|
||||
);
|
||||
|
||||
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];
|
||||
|
||||
// 2. Fetch all addresses for this partner separately to return as an array
|
||||
const [addrRows] = await pool.query(
|
||||
`SELECT address_type, door_number, landmark, address_line, pincode, district, state
|
||||
FROM addresses
|
||||
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.body = {
|
||||
success: true,
|
||||
data: {
|
||||
employee_id: employee.employee_id,
|
||||
employee_code: employee.employee_code,
|
||||
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,
|
||||
employee_id: employee.employee_id, employee_code: employee.employee_code, 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: {
|
||||
work_email: employee.work_email,
|
||||
date_joining: employee.date_joining,
|
||||
probation_days: employee.probation_days,
|
||||
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,
|
||||
work_email: employee.work_email, date_joining: employee.date_joining, probation_days: employee.probation_days,
|
||||
status: employee.contract_status, salary_structure_id: employee.salary_structure_id,
|
||||
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, // Array of all addresses (PERMANENT, CURRENT, EMERGENCY)
|
||||
addresses: addrRows,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
@ -240,10 +169,10 @@ export const updateEmployee = async (ctx: any) => {
|
||||
const id = ctx.params.id;
|
||||
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();
|
||||
@ -252,71 +181,89 @@ export const updateEmployee = async (ctx: any) => {
|
||||
try {
|
||||
await connection.beginTransaction();
|
||||
|
||||
// 1. Check if employee exists and get partner_id
|
||||
const [employeeRows] = await connection.execute(
|
||||
`SELECT partner_id FROM employees WHERE employee_id = ?`,
|
||||
[id],
|
||||
);
|
||||
|
||||
const employees = employeeRows as any[];
|
||||
if (employees.length === 0) {
|
||||
ctx.response.status = 404;
|
||||
ctx.response.body = { success: false, message: "Employee not found" };
|
||||
return;
|
||||
if (employees.length === 0) {
|
||||
ctx.response.status = 404;
|
||||
ctx.response.body = { success: false, message: "Employee not found" };
|
||||
await connection.rollback();
|
||||
return;
|
||||
}
|
||||
|
||||
const partnerId = employees[0].partner_id;
|
||||
|
||||
// 2. Update Partners table (Added dob and gender)
|
||||
await connection.execute(
|
||||
`UPDATE partners
|
||||
SET first_name = ?, last_name = ?, personal_email = ?, personal_phone = ?
|
||||
SET first_name = ?, last_name = ?, dob = ?, gender = ?, personal_email = ?, personal_phone = ?
|
||||
WHERE partner_id = ?`,
|
||||
[
|
||||
data.firstName,
|
||||
data.lastName,
|
||||
data.dob,
|
||||
data.gender,
|
||||
data.personalEmail,
|
||||
data.personalPhone,
|
||||
partnerId,
|
||||
],
|
||||
);
|
||||
|
||||
// await connection.execute(
|
||||
// `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,
|
||||
// ],
|
||||
// );
|
||||
// 3. Upsert Address
|
||||
if (data.address) {
|
||||
await connection.execute(
|
||||
`INSERT INTO addresses (partner_id, address_type, door_number, landmark, address_line, pincode, district, state)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
door_number = VALUES(door_number),
|
||||
landmark = VALUES(landmark),
|
||||
address_line = VALUES(address_line),
|
||||
pincode = VALUES(pincode),
|
||||
district = VALUES(district),
|
||||
state = VALUES(state)`,
|
||||
[
|
||||
partnerId,
|
||||
data.address.type,
|
||||
data.address.doorNumber,
|
||||
data.address.landmark,
|
||||
data.address.line,
|
||||
data.address.pincode,
|
||||
data.address.district,
|
||||
data.address.state,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
// Upsert Address (Fixes the missing address bug)
|
||||
await connection.execute(
|
||||
`INSERT INTO addresses (partner_id, address_type, door_number, landmark, address_line, pincode, district, state)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
door_number = VALUES(door_number),
|
||||
landmark = VALUES(landmark),
|
||||
address_line = VALUES(address_line),
|
||||
pincode = VALUES(pincode),
|
||||
district = VALUES(district),
|
||||
state = VALUES(state)`,
|
||||
[
|
||||
partnerId,
|
||||
data.address.type,
|
||||
data.address.doorNumber,
|
||||
data.address.landmark,
|
||||
data.address.line,
|
||||
data.address.pincode,
|
||||
data.address.district,
|
||||
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();
|
||||
|
||||
@ -327,10 +274,22 @@ export const updateEmployee = async (ctx: any) => {
|
||||
};
|
||||
} catch (error) {
|
||||
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.body = {
|
||||
success: false,
|
||||
error: "Update failed: " + (error as Error).message,
|
||||
error: "Update failed: " + err.message,
|
||||
};
|
||||
} finally {
|
||||
connection.release();
|
||||
@ -339,31 +298,13 @@ export const updateEmployee = async (ctx: any) => {
|
||||
|
||||
export const deleteEmployee = async (ctx: any) => {
|
||||
const id = ctx.params.id;
|
||||
|
||||
try {
|
||||
const [result] = await pool.execute(
|
||||
`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;
|
||||
}
|
||||
|
||||
const [result] = await pool.execute(`UPDATE employees SET is_active = false WHERE employee_id = ?`, [id]);
|
||||
if ((result as any).affectedRows === 0) { ctx.response.status = 404; ctx.response.body = { success: false, message: "Employee not found" }; return; }
|
||||
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) {
|
||||
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 };
|
||||
}
|
||||
};
|
||||
};
|
||||
@ -1,443 +1,343 @@
|
||||
import { getDbPool } from "../../shared/db.ts"
|
||||
import {generateNextCode} from "../../shared/sequence.ts"
|
||||
import { generateNextCode } from "../../shared/sequence.ts"
|
||||
|
||||
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) => {
|
||||
try {
|
||||
const [rows] = await pool.query(`SELECT company_id, name FROM companies`);
|
||||
ctx.response.status = 200;
|
||||
const { clause, values } = buildFilter(ctx.request.url.searchParams, ['is_active']);
|
||||
const [rows] = await pool.query(`SELECT company_id, company_code, name, is_active FROM companies ${clause}`, values);
|
||||
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) => {
|
||||
try {
|
||||
const [rows] = await pool.query(`SELECT branch_id, branch_name,code, company_id FROM branches`);
|
||||
ctx.response.status = 200;
|
||||
const params = ctx.request.url.searchParams;
|
||||
let query = `SELECT b.branch_id, b.code, b.branch_name, b.is_active, c.company_code, c.name AS company_name
|
||||
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 };
|
||||
} 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) => {
|
||||
try {
|
||||
const [rows] = await pool.query(`SELECT department_id, name, parent_id FROM departments`);
|
||||
ctx.response.status = 200;
|
||||
ctx.response.body = { success: true, data: rows };
|
||||
} catch (error) {
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { success: false, error: (error as Error).message };
|
||||
}
|
||||
const params = ctx.request.url.searchParams;
|
||||
let query = `SELECT d.department_id, d.department_code, d.name AS department_name, d.is_active,
|
||||
b.branch_name, c.name AS company_name,
|
||||
(SELECT GROUP_CONCAT(CONCAT(p.first_name, ' ', p.last_name) SEPARATOR ', ')
|
||||
FROM department_managers dm
|
||||
JOIN employees e ON dm.employee_id = e.employee_id
|
||||
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) => {
|
||||
try {
|
||||
const [rows] = await pool.query(`SELECT job_id, title, department_id FROM job_positions`);
|
||||
ctx.response.status = 200;
|
||||
ctx.response.body = { success: true, data: rows };
|
||||
} catch (error) {
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { success: false, error: (error as Error).message };
|
||||
}
|
||||
};
|
||||
const params = ctx.request.url.searchParams;
|
||||
let query = `SELECT j.job_id, j.job_code, j.title AS job_name, j.is_active,
|
||||
d.name AS department_name, b.branch_name, c.name AS company_name
|
||||
FROM job_positions j JOIN departments d ON j.department_id = d.department_id
|
||||
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('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
|
||||
// ==========================================
|
||||
|
||||
export const createCompany = async (ctx: any) => {
|
||||
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();
|
||||
|
||||
let companyCode = data.companyCode || await generateNextCode("COMPANY_MAIN", "CO-", 3);
|
||||
|
||||
try {
|
||||
const [result] = await pool.execute(
|
||||
`INSERT INTO companies (name, parent_id) VALUES (?, ?)`,
|
||||
[data.name, data.parentId || null]
|
||||
`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 };
|
||||
ctx.response.body = { success: true, message: "Company created", company_id: (result as any).insertId, company_code: companyCode };
|
||||
} catch (error) {
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { success: false, error: (error as Error).message };
|
||||
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;
|
||||
}
|
||||
ctx.response.status = 500; ctx.response.body = { success: false, error: err.message };
|
||||
}
|
||||
};
|
||||
|
||||
export const updateCompany = async (ctx: any) => {
|
||||
const id = ctx.params.id;
|
||||
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();
|
||||
try {
|
||||
const [result] = await pool.execute(
|
||||
`UPDATE companies SET name = ?, parent_id = ? WHERE company_id = ?`,
|
||||
[data.name, data.parentId || null, id]
|
||||
`UPDATE companies SET name = ?, parent_id = ?, is_active = ? WHERE company_id = ?`,
|
||||
[data.name, data.parentId || null, data.isActive, id]
|
||||
);
|
||||
if ((result as any).affectedRows === 0) {
|
||||
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 updated successfully" };
|
||||
} catch (error) {
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { success: false, error: (error as Error).message };
|
||||
}
|
||||
if ((result as any).affectedRows === 0) { 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 updated successfully" };
|
||||
} catch (error) { ctx.response.status = 500; ctx.response.body = { success: false, error: (error as Error).message }; }
|
||||
};
|
||||
|
||||
export const deleteCompany = async (ctx: any) => {
|
||||
const id = ctx.params.id;
|
||||
try {
|
||||
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;
|
||||
}
|
||||
ctx.response.status = 200;
|
||||
ctx.response.body = { success: true, message: "Company deleted successfully" };
|
||||
if ((result as any).affectedRows === 0) { 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) {
|
||||
let errorMessage = (error as Error).message;
|
||||
// Catch ON DELETE RESTRICT from employees table
|
||||
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 };
|
||||
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
|
||||
// ==========================================
|
||||
|
||||
export const createBranch = async (ctx: any) => {
|
||||
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();
|
||||
let branchCode = data.code;
|
||||
|
||||
// If no code is provided, automatically generate a sequential one
|
||||
if (!branchCode || branchCode.trim() === "") {
|
||||
// Get first 3 letters (e.g., "Bengaluru" -> "BEN")
|
||||
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);
|
||||
}
|
||||
|
||||
try {
|
||||
const [result] = await pool.execute(
|
||||
`INSERT INTO branches (company_id, branch_name, code) VALUES (?, ?, ?)`,
|
||||
[data.companyId, data.branchName, branchCode]
|
||||
`INSERT INTO branches (company_id, branch_name, code, is_active) VALUES (?, ?, ?, ?)`,
|
||||
[data.companyId, data.branchName, branchCode, data.isActive ?? true]
|
||||
);
|
||||
|
||||
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) {
|
||||
let errorMessage = (error as Error).message;
|
||||
if (errorMessage.includes("Duplicate entry")) {
|
||||
ctx.response.status = 409;
|
||||
ctx.response.body = { success: false, error: `The branch code '${branchCode}' already exists.` };
|
||||
return;
|
||||
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 branch code '${branchCode}' already exists.` }; return;
|
||||
}
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { success: false, error: errorMessage };
|
||||
ctx.response.status = 500; ctx.response.body = { success: false, error: err.message };
|
||||
}
|
||||
};
|
||||
|
||||
export const updateBranch = async (ctx: any) => {
|
||||
const id = ctx.params.id;
|
||||
const body = ctx.request.body;
|
||||
if (body.type() !== "json") {
|
||||
ctx.response.status = 400;
|
||||
ctx.response.body = { success: false, error: "Invalid JSON body" };
|
||||
return;
|
||||
}
|
||||
|
||||
const id = ctx.params.id; 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();
|
||||
try {
|
||||
const [result] = await pool.execute(
|
||||
`UPDATE branches SET company_id = ?, branch_name = ?, code = ? WHERE branch_id = ?`,
|
||||
[data.companyId, data.branchName, data.code, id]
|
||||
`UPDATE branches SET company_id = ?, branch_name = ?, code = ?, is_active = ? WHERE branch_id = ?`,
|
||||
[data.companyId, data.branchName, data.code, data.isActive, id]
|
||||
);
|
||||
if ((result as any).affectedRows === 0) {
|
||||
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 updated successfully" };
|
||||
} catch (error) {
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { success: false, error: (error as Error).message };
|
||||
}
|
||||
if ((result as any).affectedRows === 0) { 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 updated successfully" };
|
||||
} catch (error) { ctx.response.status = 500; ctx.response.body = { success: false, error: (error as Error).message }; }
|
||||
};
|
||||
|
||||
export const deleteBranch = async (ctx: any) => {
|
||||
const id = ctx.params.id;
|
||||
try {
|
||||
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;
|
||||
}
|
||||
ctx.response.status = 200;
|
||||
ctx.response.body = { success: true, message: "Branch deleted successfully" };
|
||||
if ((result as any).affectedRows === 0) { 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) {
|
||||
let errorMessage = (error as Error).message;
|
||||
// Catch ON DELETE RESTRICT from employees table
|
||||
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 };
|
||||
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
|
||||
// ==========================================
|
||||
|
||||
|
||||
|
||||
export const createDepartment = async (ctx: any) => {
|
||||
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();
|
||||
|
||||
if (!data.companyId || !data.branchId || !data.name) {
|
||||
ctx.response.status = 400;
|
||||
ctx.response.body = { success: false, error: "companyId, branchId, and name are required." };
|
||||
return;
|
||||
}
|
||||
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();
|
||||
|
||||
try {
|
||||
await connection.beginTransaction();
|
||||
|
||||
// 1. Insert the department
|
||||
const [result] = await connection.execute(
|
||||
`INSERT INTO departments (company_id, branch_id, name, parent_id) VALUES (?, ?, ?, ?)`,
|
||||
[data.companyId, data.branchId, data.name, data.parentId ?? null]
|
||||
`INSERT INTO departments (company_id, branch_id, department_code, name, parent_id, is_active) VALUES (?, ?, ?, ?, ?, ?)`,
|
||||
[data.companyId, data.branchId, deptCode, data.name, data.parentId ?? null, data.isActive ?? true]
|
||||
);
|
||||
const departmentId = (result as any).insertId;
|
||||
|
||||
// 2. If a manager is provided, add them to the junction table
|
||||
if (data.managerId) {
|
||||
await connection.execute(
|
||||
`INSERT INTO department_managers (department_id, employee_id) VALUES (?, ?)`,
|
||||
`INSERT INTO department_managers (department_id, employee_id, is_current) VALUES (?, ?, TRUE)`,
|
||||
[departmentId, data.managerId]
|
||||
);
|
||||
}
|
||||
|
||||
await connection.commit();
|
||||
|
||||
ctx.response.status = 201;
|
||||
ctx.response.body = {
|
||||
success: true,
|
||||
message: "Department created",
|
||||
department_id: departmentId
|
||||
};
|
||||
ctx.response.status = 201; ctx.response.body = { success: true, message: "Department created", department_id: departmentId, department_code: deptCode };
|
||||
} catch (error) {
|
||||
await connection.rollback();
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { success: false, error: (error as Error).message };
|
||||
} finally {
|
||||
connection.release();
|
||||
}
|
||||
await connection.rollback();
|
||||
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 department code '${deptCode}' already exists. Please sync your database sequences.` };
|
||||
return;
|
||||
}
|
||||
ctx.response.status = 500; ctx.response.body = { success: false, error: err.message };
|
||||
} finally { connection.release(); }
|
||||
};
|
||||
|
||||
export const updateDepartment = async (ctx: any) => {
|
||||
const id = ctx.params.id;
|
||||
const body = ctx.request.body;
|
||||
if (body.type() !== "json") {
|
||||
ctx.response.status = 400;
|
||||
ctx.response.body = { success: false, error: "Invalid JSON body" };
|
||||
return;
|
||||
}
|
||||
|
||||
const id = ctx.params.id; 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 connection = await pool.getConnection();
|
||||
|
||||
try {
|
||||
await connection.beginTransaction();
|
||||
|
||||
// 1. Update core department details
|
||||
const [result] = await connection.execute(
|
||||
`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, id]
|
||||
`UPDATE departments SET company_id = ?, branch_id = ?, name = ?, parent_id = ?, is_active = ? WHERE department_id = ?`,
|
||||
[data.companyId ?? null, data.branchId ?? null, data.name ?? null, data.parentId ?? null, data.isActive, 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) {
|
||||
// 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(`UPDATE department_managers SET is_current = FALSE, removed_at = NOW() WHERE department_id = ? AND is_current = TRUE`, [id]);
|
||||
await connection.execute(
|
||||
`INSERT INTO department_managers (department_id, employee_id) VALUES (?, ?)`,
|
||||
`INSERT INTO department_managers (department_id, employee_id, is_current) VALUES (?, ?, TRUE)
|
||||
ON DUPLICATE KEY UPDATE is_current = TRUE, removed_at = NULL`,
|
||||
[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) {
|
||||
await connection.rollback();
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { success: false, error: (error as Error).message };
|
||||
} finally {
|
||||
connection.release();
|
||||
}
|
||||
await connection.rollback(); ctx.response.status = 500; ctx.response.body = { success: false, error: (error as Error).message };
|
||||
} finally { connection.release(); }
|
||||
};
|
||||
|
||||
|
||||
export const deleteDepartment = async (ctx: any) => {
|
||||
const id = ctx.params.id;
|
||||
try {
|
||||
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;
|
||||
}
|
||||
ctx.response.status = 200;
|
||||
ctx.response.body = { success: true, message: "Department deleted successfully" };
|
||||
if ((result as any).affectedRows === 0) { 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) {
|
||||
let errorMessage = (error as Error).message;
|
||||
// Catch ON DELETE RESTRICT from contracts table
|
||||
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 };
|
||||
if (errorMessage.includes("foreign key constraint fails")) errorMessage = "Cannot delete department. There are active assignments tied to it.";
|
||||
ctx.response.status = 409; ctx.response.body = { success: false, error: errorMessage };
|
||||
}
|
||||
};
|
||||
|
||||
// ==========================================
|
||||
// JOB POSITIONS MANAGEMENT
|
||||
// ==========================================
|
||||
|
||||
export const createJob = async (ctx: any) => {
|
||||
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();
|
||||
|
||||
if (!data.departmentId || !data.title) {
|
||||
ctx.response.status = 400;
|
||||
ctx.response.body = { success: false, error: "departmentId and title are required fields." };
|
||||
return;
|
||||
}
|
||||
if (!data.departmentId || !data.title) { ctx.response.status = 400; ctx.response.body = { success: false, error: "departmentId and title are required fields." }; return; }
|
||||
|
||||
let jobCode = data.jobCode || await generateNextCode("JOB_MAIN", "JOB-", 3);
|
||||
|
||||
try {
|
||||
const [result] = await pool.execute(
|
||||
`INSERT INTO job_positions (department_id, title, description) VALUES (?, ?, ?)`,
|
||||
[data.departmentId, data.title, data.description ?? null]
|
||||
`INSERT INTO job_positions (department_id, job_code, title, description, is_active) VALUES (?, ?, ?, ?, ?)`,
|
||||
[data.departmentId, jobCode, data.title, data.description ?? null, data.isActive ?? true]
|
||||
);
|
||||
ctx.response.status = 201;
|
||||
ctx.response.body = { success: true, message: "Job created", job_id: (result as any).insertId };
|
||||
ctx.response.status = 201; ctx.response.body = { success: true, message: "Job created", job_id: (result as any).insertId, job_code: jobCode };
|
||||
} catch (error) {
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { success: false, error: (error as Error).message };
|
||||
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 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) => {
|
||||
const id = ctx.params.id;
|
||||
const body = ctx.request.body;
|
||||
if (body.type() !== "json") {
|
||||
ctx.response.status = 400;
|
||||
ctx.response.body = { success: false, error: "Invalid JSON body" };
|
||||
return;
|
||||
}
|
||||
|
||||
const id = ctx.params.id; 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();
|
||||
try {
|
||||
const [result] = await pool.execute(
|
||||
`UPDATE job_positions SET department_id = ?, title = ?, description = ? WHERE job_id = ?`,
|
||||
[data.departmentId ?? null, data.title ?? null, data.description ?? null, id]
|
||||
`UPDATE job_positions SET department_id = ?, title = ?, description = ?, is_active = ? WHERE job_id = ?`,
|
||||
[data.departmentId ?? null, data.title ?? null, data.description ?? null, data.isActive, id]
|
||||
);
|
||||
if ((result as any).affectedRows === 0) {
|
||||
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 updated successfully" };
|
||||
} catch (error) {
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { success: false, error: (error as Error).message };
|
||||
}
|
||||
if ((result as any).affectedRows === 0) { 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 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) => {
|
||||
const id = ctx.params.id;
|
||||
try {
|
||||
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;
|
||||
}
|
||||
ctx.response.status = 200;
|
||||
ctx.response.body = { success: true, message: "Job deleted successfully" };
|
||||
if ((result as any).affectedRows === 0) { 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) {
|
||||
let errorMessage = (error as Error).message;
|
||||
// Catch ON DELETE RESTRICT from contracts table
|
||||
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 };
|
||||
if (errorMessage.includes("foreign key constraint fails")) errorMessage = "Cannot delete job position. It is currently linked to active employee assignments.";
|
||||
ctx.response.status = 409; ctx.response.body = { success: false, error: errorMessage };
|
||||
}
|
||||
};
|
||||
@ -5,12 +5,7 @@ const emsPool = getDbPool("hrms_ems");
|
||||
|
||||
export const bulkSeedEmployees = async (ctx: Context) => {
|
||||
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();
|
||||
let insertedCount = 0;
|
||||
@ -20,12 +15,11 @@ export const bulkSeedEmployees = async (ctx: Context) => {
|
||||
|
||||
for (const item of body) {
|
||||
const { emp_code, name, designation, department, email } = item;
|
||||
|
||||
const nameParts = name.trim().split(" ");
|
||||
const firstName = nameParts[0] || "Employee";
|
||||
const lastName = nameParts.slice(1).join(" ") || "LNU";
|
||||
|
||||
// 1. Insert into partners
|
||||
// 1. Partner
|
||||
const [partnerResult]: any = await connection.execute(
|
||||
`INSERT INTO partners (first_name, last_name, dob, gender, personal_email, personal_phone)
|
||||
VALUES (?, ?, '1995-01-01', 'Not Specified', ?, ?)
|
||||
@ -34,25 +28,25 @@ export const bulkSeedEmployees = async (ctx: Context) => {
|
||||
);
|
||||
const partnerId = partnerResult.insertId;
|
||||
|
||||
// 2. Ensure department exists (FIXED: Added branch_id to satisfy NOT NULL constraint)
|
||||
// 2. Department
|
||||
const [deptResult]: any = await connection.execute(
|
||||
`INSERT INTO departments (company_id, branch_id, name)
|
||||
VALUES (1, 1, ?)
|
||||
`INSERT INTO departments (company_id, branch_id, department_code, name)
|
||||
VALUES (1, 1, CONCAT('DEPT-', LEFT(?, 3)), ?)
|
||||
ON DUPLICATE KEY UPDATE department_id = LAST_INSERT_ID(department_id)`,
|
||||
[department || "General"]
|
||||
[department || "General", department || "General"]
|
||||
);
|
||||
const departmentId = deptResult.insertId;
|
||||
|
||||
// 3. Ensure job position exists (FIXED: Linked to department_id instead of company_id)
|
||||
// 3. Job
|
||||
const [jobResult]: any = await connection.execute(
|
||||
`INSERT INTO job_positions (department_id, title)
|
||||
VALUES (?, ?)
|
||||
`INSERT INTO job_positions (department_id, job_code, title)
|
||||
VALUES (?, CONCAT('JOB-', LEFT(?, 3)), ?)
|
||||
ON DUPLICATE KEY UPDATE job_id = LAST_INSERT_ID(job_id)`,
|
||||
[departmentId, designation || "Trainee"]
|
||||
[departmentId, designation || "Trainee", designation || "Trainee"]
|
||||
);
|
||||
const jobId = jobResult.insertId;
|
||||
|
||||
// 4. Create core Employee record
|
||||
// 4. Employee
|
||||
const [empResult]: any = await connection.execute(
|
||||
`INSERT INTO employees (employee_code, partner_id, company_id, branch_id, is_active)
|
||||
VALUES (?, ?, 1, 1, TRUE)
|
||||
@ -61,66 +55,63 @@ export const bulkSeedEmployees = async (ctx: Context) => {
|
||||
);
|
||||
const employeeId = empResult.insertId;
|
||||
|
||||
// 5. Establish operational Contract
|
||||
// 5. Terms & Assignments (Replaces old contracts insert)
|
||||
await connection.execute(
|
||||
`INSERT INTO contracts (employee_id, department_id, job_id, work_email, date_joining, status, salary_structure_id)
|
||||
VALUES (?, ?, ?, ?, '2026-01-01', 'ACTIVE', 100)
|
||||
`INSERT INTO employment_terms (employee_id, work_email, date_joining, probation_days, status, salary_structure_id)
|
||||
VALUES (?, ?, '2026-01-01', 90, 'ACTIVE', 100)
|
||||
ON DUPLICATE KEY UPDATE work_email = VALUES(work_email)`,
|
||||
[employeeId, departmentId, jobId, email]
|
||||
[employeeId, 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++;
|
||||
}
|
||||
|
||||
await connection.commit();
|
||||
|
||||
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) {
|
||||
await connection.rollback();
|
||||
console.error("Seeding failed, changes rolled back:", error);
|
||||
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { success: false, error: errorMessage };
|
||||
} finally {
|
||||
connection.release();
|
||||
}
|
||||
} finally { connection.release(); }
|
||||
};
|
||||
|
||||
export const bulkMapHierarchy = async (ctx: Context) => {
|
||||
const body = ctx.request.hasBody ? await ctx.request.body.json() : {};
|
||||
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();
|
||||
let contractsUpdated = 0;
|
||||
let assignmentsUpdated = 0;
|
||||
let departmentsUpdated = 0;
|
||||
|
||||
try {
|
||||
await connection.beginTransaction();
|
||||
|
||||
// 1. Map Employees to their Managers (Contracts table)
|
||||
// 1. Map Employees to their Managers (Updating active assignments)
|
||||
for (const mapping of employeeHierarchy) {
|
||||
const { emp_code, manager_code } = mapping;
|
||||
if (!emp_code || !manager_code) continue;
|
||||
|
||||
const [result]: any = await connection.execute(
|
||||
`UPDATE contracts c
|
||||
JOIN employees e ON c.employee_id = e.employee_id
|
||||
`UPDATE employee_assignments ea
|
||||
JOIN employees e ON ea.employee_id = e.employee_id
|
||||
JOIN employees m ON m.employee_code = ?
|
||||
SET c.reporting_to_id = m.employee_id
|
||||
WHERE e.employee_code = ? AND c.status = 'ACTIVE'`,
|
||||
SET ea.reporting_to_id = m.employee_id
|
||||
WHERE e.employee_code = ? AND ea.is_current = TRUE`,
|
||||
[manager_code, emp_code]
|
||||
);
|
||||
if (result.affectedRows > 0) contractsUpdated++;
|
||||
if (result.affectedRows > 0) assignmentsUpdated++;
|
||||
}
|
||||
|
||||
// 2. Map Departments to Managers and Parent Departments
|
||||
@ -128,11 +119,10 @@ export const bulkMapHierarchy = async (ctx: Context) => {
|
||||
const { department_name, manager_code, parent_department_name } = dept;
|
||||
if (!department_name) continue;
|
||||
|
||||
// FIXED: Use department_managers junction table instead of departments.manager_id
|
||||
if (manager_code) {
|
||||
await connection.execute(
|
||||
`INSERT IGNORE INTO department_managers (department_id, employee_id)
|
||||
SELECT d.department_id, m.employee_id
|
||||
`INSERT IGNORE INTO department_managers (department_id, employee_id, is_current)
|
||||
SELECT d.department_id, m.employee_id, TRUE
|
||||
FROM departments d
|
||||
JOIN employees m ON m.employee_code = ?
|
||||
WHERE d.name = ?`,
|
||||
@ -140,7 +130,6 @@ export const bulkMapHierarchy = async (ctx: Context) => {
|
||||
);
|
||||
}
|
||||
|
||||
// Update Parent Department Hierarchy
|
||||
if (parent_department_name) {
|
||||
await connection.execute(
|
||||
`UPDATE departments d
|
||||
@ -154,20 +143,13 @@ export const bulkMapHierarchy = async (ctx: Context) => {
|
||||
}
|
||||
|
||||
await connection.commit();
|
||||
|
||||
ctx.response.status = 200;
|
||||
ctx.response.body = {
|
||||
success: true,
|
||||
message: "Hierarchy mapping completed successfully.",
|
||||
metrics: { contractsUpdated, departmentsProcessed: departmentsUpdated }
|
||||
};
|
||||
ctx.response.body = { success: true, message: "Hierarchy mapping completed successfully.", metrics: { assignmentsUpdated, departmentsProcessed: departmentsUpdated } };
|
||||
} catch (error) {
|
||||
await connection.rollback();
|
||||
console.error("Hierarchy mapping failed:", error);
|
||||
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { success: false, error: errorMessage };
|
||||
} finally {
|
||||
connection.release();
|
||||
}
|
||||
} finally { connection.release(); }
|
||||
};
|
||||
@ -1,10 +1,7 @@
|
||||
import { Router } from "@oak/oak";
|
||||
import {getDashboardMetrics} from "./controllers/dashboard.controller.ts"
|
||||
import {
|
||||
getEmployees,
|
||||
createEmployee,
|
||||
getEmployeeById,
|
||||
updateEmployee,
|
||||
deleteEmployee
|
||||
getEmployees, createEmployee, getEmployeeById, updateEmployee, deleteEmployee, getEmployeeHistory
|
||||
} from "./controllers/employee.controller.ts";
|
||||
import {
|
||||
getCompanies, createCompany, updateCompany, deleteCompany,
|
||||
@ -13,88 +10,54 @@ import {
|
||||
getJobs, createJob, updateJob, deleteJob
|
||||
} from "./controllers/lookup.controller.ts";
|
||||
import {
|
||||
getEmployeeContracts,
|
||||
createContract
|
||||
getEmployeeContracts, createContract
|
||||
} from "./controllers/contract.controller.ts";
|
||||
import {
|
||||
bulkSeedEmployees,
|
||||
bulkMapHierarchy
|
||||
bulkSeedEmployees, bulkMapHierarchy
|
||||
} from "./controllers/system.controller.ts";
|
||||
|
||||
// Shared authentication and authorization middleware
|
||||
import { requireAuth, requireRole, AppRole } from "../shared/auth.ts";
|
||||
|
||||
const router = new Router();
|
||||
|
||||
// ============================================================================
|
||||
// EMPLOYEE CORE DOMAIN
|
||||
// Manages the corporate identity (employees) and personal data (partners).
|
||||
// ============================================================================
|
||||
// EMPLOYEE CORE
|
||||
router.get("/employees", requireAuth, requireRole([AppRole.SUPER_ADMIN, AppRole.ADMIN, AppRole.MANAGER]), getEmployees);
|
||||
router.get("/employees/:id", requireAuth, getEmployeeById);
|
||||
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);
|
||||
|
||||
// Retrieve a list of all active employees. Visible to management and administrators.
|
||||
router.get("/api/v1/employees", requireAuth, requireRole([AppRole.SUPER_ADMIN, AppRole.ADMIN, AppRole.MANAGER]), getEmployees);
|
||||
// CONTRACT & ASSIGNMENT DOMAIN (Promotions/Transfers)
|
||||
router.get("/employees/:id/contracts", requireAuth, getEmployeeContracts);
|
||||
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
|
||||
// Read-only reference endpoints for the frontend UI to populate select options.
|
||||
// ============================================================================
|
||||
router.get("/companies", requireAuth, getCompanies);
|
||||
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);
|
||||
|
||||
// Fetch legal entities and parent group structures.
|
||||
router.get("/api/v1/companies", requireAuth, getCompanies);
|
||||
router.post("/api/v1/companies", requireAuth, requireRole([AppRole.SUPER_ADMIN, AppRole.ADMIN]), createCompany);
|
||||
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("/branches", requireAuth, getBranches);
|
||||
router.post("/branches", requireAuth, requireRole([AppRole.SUPER_ADMIN, AppRole.ADMIN]), createBranch);
|
||||
router.put("/branches/:id", requireAuth, requireRole([AppRole.SUPER_ADMIN, AppRole.ADMIN]), updateBranch);
|
||||
router.delete("/branches/:id", requireAuth, requireRole([AppRole.SUPER_ADMIN, AppRole.ADMIN]), deleteBranch);
|
||||
|
||||
// Fetch structural branch offices and physical locations.
|
||||
router.get("/api/v1/branches", requireAuth, getBranches);
|
||||
router.post("/api/v1/branches", requireAuth, requireRole([AppRole.SUPER_ADMIN, AppRole.ADMIN]), createBranch);
|
||||
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("/departments", requireAuth, getDepartments);
|
||||
router.post("/departments", requireAuth, requireRole([AppRole.SUPER_ADMIN, AppRole.ADMIN]), createDepartment);
|
||||
router.put("/departments/:id", requireAuth, requireRole([AppRole.SUPER_ADMIN, AppRole.ADMIN]), updateDepartment);
|
||||
router.delete("/departments/:id", requireAuth, requireRole([AppRole.SUPER_ADMIN, AppRole.ADMIN]), deleteDepartment);
|
||||
|
||||
// List corporate departments and organizational chart reporting lines.
|
||||
router.get("/api/v1/departments", requireAuth, getDepartments);
|
||||
router.post("/api/v1/departments", requireAuth, requireRole([AppRole.SUPER_ADMIN, AppRole.ADMIN]), createDepartment);
|
||||
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);
|
||||
router.get("/jobs", requireAuth, getJobs);
|
||||
router.post("/jobs", requireAuth, requireRole([AppRole.SUPER_ADMIN, AppRole.ADMIN]), createJob);
|
||||
router.put("/jobs/:id", requireAuth, requireRole([AppRole.SUPER_ADMIN, AppRole.ADMIN]), updateJob);
|
||||
router.delete("/jobs/:id", requireAuth, requireRole([AppRole.SUPER_ADMIN, AppRole.ADMIN]), deleteJob);
|
||||
|
||||
// List company designations and employment titles.
|
||||
router.get("/api/v1/jobs", requireAuth, getJobs);
|
||||
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);
|
||||
// DASHBOARD METRICS
|
||||
// Example: GET /dashboard/metrics?company_id=1&branch_id=2
|
||||
router.get("/dashboard/metrics", requireAuth, getDashboardMetrics);
|
||||
|
||||
// ============================================================================
|
||||
// SYSTEM & MIGRATION DOMAIN
|
||||
// High-risk administrative endpoints for bulk data execution.
|
||||
// ============================================================================
|
||||
|
||||
// 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);
|
||||
router.post("/system/seed-employees", requireAuth, requireRole([AppRole.SUPER_ADMIN]), bulkSeedEmployees);
|
||||
router.post("/system/map-hierarchy", requireAuth, requireRole([AppRole.SUPER_ADMIN]), bulkMapHierarchy);
|
||||
|
||||
export default router;
|
||||
@ -4,8 +4,10 @@ USE hrms_ems;
|
||||
-- 1. Legal Entities
|
||||
CREATE TABLE companies (
|
||||
company_id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
company_code VARCHAR(20) NOT NULL UNIQUE,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
parent_id INT NULL,
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (parent_id) REFERENCES companies(company_id) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
@ -16,6 +18,7 @@ CREATE TABLE branches (
|
||||
company_id INT NOT NULL,
|
||||
branch_name VARCHAR(100) NOT NULL,
|
||||
code VARCHAR(20) NOT NULL UNIQUE,
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (company_id) REFERENCES companies(company_id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
@ -44,6 +47,7 @@ CREATE TABLE addresses (
|
||||
pincode VARCHAR(10) NOT NULL,
|
||||
district 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
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
@ -52,8 +56,10 @@ CREATE TABLE departments (
|
||||
department_id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
company_id INT NOT NULL,
|
||||
branch_id INT NOT NULL,
|
||||
department_code VARCHAR(20) NOT NULL UNIQUE,
|
||||
name VARCHAR(50) NOT NULL,
|
||||
parent_id INT NULL,
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
FOREIGN KEY (company_id) REFERENCES companies(company_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
|
||||
@ -63,8 +69,10 @@ CREATE TABLE departments (
|
||||
CREATE TABLE job_positions (
|
||||
job_id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
department_id INT NOT NULL,
|
||||
job_code VARCHAR(20) NOT NULL UNIQUE,
|
||||
title VARCHAR(50) NOT NULL,
|
||||
description TEXT NULL,
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
FOREIGN KEY (department_id) REFERENCES departments(department_id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
@ -82,34 +90,61 @@ CREATE TABLE employees (
|
||||
FOREIGN KEY (branch_id) REFERENCES branches(branch_id) ON DELETE RESTRICT
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- 8. Legal & Operational Contracts
|
||||
CREATE TABLE contracts (
|
||||
contract_id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
-- 8. Legal & Compensation Terms (Changes rarely)
|
||||
CREATE TABLE employment_terms (
|
||||
term_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,
|
||||
department_id INT NOT NULL,
|
||||
job_id INT NOT NULL,
|
||||
reporting_to_id INT NULL,
|
||||
work_email VARCHAR(100) NOT NULL UNIQUE,
|
||||
date_joining DATE NOT NULL,
|
||||
probation_days INT DEFAULT 90,
|
||||
status ENUM('DRAFT', 'ACTIVE', 'EXPIRED', 'TERMINATED') DEFAULT 'DRAFT',
|
||||
salary_structure_id INT NOT NULL,
|
||||
change_reason ENUM('HIRE', 'TRANSFER', 'PROMOTION', 'MANAGER_CHANGE', 'REORG') NOT NULL,
|
||||
effective_from DATE NOT NULL,
|
||||
effective_to DATE NULL,
|
||||
is_current BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (employee_id) REFERENCES employees(employee_id) ON DELETE CASCADE,
|
||||
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;
|
||||
|
||||
-- 9. Department Managers (Junction Table)
|
||||
-- 10. Department Managers (Junction Table with History)
|
||||
CREATE TABLE department_managers (
|
||||
department_id INT NOT NULL,
|
||||
employee_id INT NOT NULL,
|
||||
assigned_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
is_current BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
removed_at TIMESTAMP NULL,
|
||||
PRIMARY KEY (department_id, employee_id),
|
||||
FOREIGN KEY (department_id) REFERENCES departments(department_id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (employee_id) REFERENCES employees(employee_id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- 10. System Sequences
|
||||
-- 11. Branch Admins (For AMS/LMS routing)
|
||||
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 (
|
||||
sequence_id VARCHAR(50) PRIMARY KEY,
|
||||
prefix VARCHAR(10) NOT NULL,
|
||||
|
||||
@ -97,45 +97,6 @@ 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
|
||||
*/
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user