Compare commits
2 Commits
6187ec4afc
...
4d70938a81
| Author | SHA1 | Date | |
|---|---|---|---|
| 4d70938a81 | |||
| 1b69e9f14c |
@ -1,11 +1,14 @@
|
|||||||
import { getDbPool } from "../../shared/db.ts"
|
import { getDbPool } from "../../shared/db.ts";
|
||||||
|
import { generateNextCode } from "../../shared/sequence.ts";
|
||||||
|
|
||||||
const pool = getDbPool("hrms_ems");
|
const pool = getDbPool("hrms_ems");
|
||||||
|
|
||||||
export const getEmployees = async (ctx: any) => {
|
export const getEmployees = async (ctx: any) => {
|
||||||
try {
|
try {
|
||||||
|
// FIX: Filter by ACTIVE contract to prevent duplicate rows for employees with past contracts
|
||||||
const [rows] = await pool.query(
|
const [rows] = await pool.query(
|
||||||
`SELECT
|
`SELECT
|
||||||
|
e.employee_id,
|
||||||
e.employee_code,
|
e.employee_code,
|
||||||
p.first_name,
|
p.first_name,
|
||||||
p.last_name,
|
p.last_name,
|
||||||
@ -14,94 +17,125 @@ export const getEmployees = async (ctx: any) => {
|
|||||||
c.work_email
|
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 contracts c ON e.employee_id = c.employee_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 departments d ON c.department_id = d.department_id
|
||||||
JOIN job_positions j ON c.job_id = j.job_id
|
JOIN job_positions j ON c.job_id = j.job_id
|
||||||
WHERE e.is_active = true`
|
WHERE e.is_active = true`,
|
||||||
);
|
);
|
||||||
|
|
||||||
ctx.response.body = {
|
ctx.response.body = {
|
||||||
success: true,
|
success: true,
|
||||||
count: (rows as any[]).length,
|
count: (rows as any[]).length,
|
||||||
data: rows
|
data: rows,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ctx.response.status = 500;
|
ctx.response.status = 500;
|
||||||
ctx.response.body = {
|
ctx.response.body = {
|
||||||
success: false,
|
success: false,
|
||||||
error: (error as Error).message
|
error: (error as Error).message,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createEmployee = async (ctx: any) => {
|
export const createEmployee = async (ctx: any) => {
|
||||||
// 1. Extract the JSON payload from the request
|
|
||||||
const body = ctx.request.body;
|
const body = ctx.request.body;
|
||||||
if (body.type() !== "json") {
|
if (body.type() !== "json") {
|
||||||
ctx.response.status = 400;
|
ctx.response.status = 400;
|
||||||
ctx.response.body = { success: false, error: "Invalid JSON body" };
|
ctx.response.body = { success: false, error: "Invalid JSON body" };
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await body.json();
|
const data = await body.json();
|
||||||
|
let empCode = data.employeeCode;
|
||||||
// 2. Acquire a dedicated connection from the pool for our transaction
|
|
||||||
|
if (!empCode || empCode.trim() === "") {
|
||||||
|
empCode = await generateNextCode("EMPLOYEE_MAIN", "CLRI", 3);
|
||||||
|
}
|
||||||
|
|
||||||
const connection = await pool.getConnection();
|
const connection = await pool.getConnection();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 3. Start the transaction
|
|
||||||
await connection.beginTransaction();
|
await connection.beginTransaction();
|
||||||
|
|
||||||
// STEP A: Insert into Partners
|
|
||||||
const [partnerResult] = await connection.execute(
|
const [partnerResult] = 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 (?, ?, ?, ?, ?, ?)`,
|
VALUES (?, ?, ?, ?, ?, ?)`,
|
||||||
[data.firstName, data.lastName, data.dob, data.gender, data.personalEmail, data.personalPhone]
|
[
|
||||||
|
data.firstName,
|
||||||
|
data.lastName,
|
||||||
|
data.dob,
|
||||||
|
data.gender,
|
||||||
|
data.personalEmail,
|
||||||
|
data.personalPhone,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
const partnerId = (partnerResult as any).insertId;
|
const partnerId = (partnerResult as any).insertId;
|
||||||
|
|
||||||
// STEP B: Insert into Addresses
|
|
||||||
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 (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
[partnerId, data.address.type, data.address.doorNumber, data.address.landmark, data.address.line, data.address.pincode, data.address.district, data.address.state]
|
[
|
||||||
|
partnerId,
|
||||||
|
data.address.type,
|
||||||
|
data.address.doorNumber,
|
||||||
|
data.address.landmark,
|
||||||
|
data.address.line,
|
||||||
|
data.address.pincode,
|
||||||
|
data.address.district,
|
||||||
|
data.address.state,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
// STEP C: Insert into Employees
|
|
||||||
const [employeeResult] = await connection.execute(
|
const [employeeResult] = await connection.execute(
|
||||||
`INSERT INTO employees (employee_code, partner_id, company_id, branch_id, work_email, is_active)
|
`INSERT INTO employees (employee_code, partner_id, company_id, branch_id, is_active)
|
||||||
VALUES (?, ?, ?, ?, ?)`,
|
VALUES (?, ?, ?, ?, ?)`,
|
||||||
[data.employeeCode, partnerId, data.companyId, data.branchId, data.work_email, true]
|
[empCode, partnerId, data.companyId, data.branchId, true],
|
||||||
);
|
);
|
||||||
const employeeId = (employeeResult as any).insertId;
|
const employeeId = (employeeResult as any).insertId;
|
||||||
|
|
||||||
// STEP D: Insert into Contracts
|
|
||||||
await connection.execute(
|
await connection.execute(
|
||||||
`INSERT INTO contracts (employee_id, department_id, job_id, date_joining, probation_days, status, salary_structure_id)
|
`INSERT INTO contracts (employee_id, department_id, job_id, work_email, date_joining, probation_days, status, salary_structure_id, reporting_to_id)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
[employeeId, data.departmentId, data.jobId, data.dateJoining, data.probationDays, 'ACTIVE', data.salaryStructureId]
|
[
|
||||||
|
employeeId,
|
||||||
|
data.departmentId,
|
||||||
|
data.jobId,
|
||||||
|
data.work_email,
|
||||||
|
data.dateJoining,
|
||||||
|
data.probationDays,
|
||||||
|
"ACTIVE",
|
||||||
|
data.salaryStructureId,
|
||||||
|
data.reportingToId || null,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
// 4. Commit the transaction if all steps succeed
|
|
||||||
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: "Employee created successfully",
|
message: "Employee created successfully",
|
||||||
employee_id: employeeId
|
employee_id: employeeId,
|
||||||
|
employee_code: empCode,
|
||||||
};
|
};
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// 5. Rollback everything if any step fails
|
|
||||||
await connection.rollback();
|
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.",
|
||||||
|
};
|
||||||
|
return;
|
||||||
|
}
|
||||||
ctx.response.status = 500;
|
ctx.response.status = 500;
|
||||||
ctx.response.body = {
|
ctx.response.body = {
|
||||||
success: false,
|
success: false,
|
||||||
error: "Transaction failed: " + (error as Error).message
|
error: "Transaction failed: " + errorMessage,
|
||||||
};
|
};
|
||||||
} finally {
|
} finally {
|
||||||
// 6. Release the connection back to the pool
|
|
||||||
connection.release();
|
connection.release();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -110,7 +144,8 @@ export const getEmployeeById = async (ctx: any) => {
|
|||||||
const id = ctx.params.id;
|
const id = ctx.params.id;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [rows] = await pool.query(
|
// 1. Fetch core employee, partner, contract, and manager details
|
||||||
|
const [empRows] = await pool.query(
|
||||||
`SELECT
|
`SELECT
|
||||||
e.employee_id,
|
e.employee_id,
|
||||||
e.employee_code,
|
e.employee_code,
|
||||||
@ -121,51 +156,83 @@ export const getEmployeeById = async (ctx: any) => {
|
|||||||
p.gender,
|
p.gender,
|
||||||
p.personal_email,
|
p.personal_email,
|
||||||
p.personal_phone,
|
p.personal_phone,
|
||||||
a.address_type,
|
|
||||||
a.door_number,
|
|
||||||
a.landmark,
|
|
||||||
a.address_line,
|
|
||||||
a.pincode,
|
|
||||||
a.district,
|
|
||||||
a.state,
|
|
||||||
c.work_email,
|
c.work_email,
|
||||||
c.date_joining,
|
c.date_joining,
|
||||||
c.probation_days,
|
c.probation_days,
|
||||||
c.status AS contract_status,
|
c.status AS contract_status,
|
||||||
d.name AS department,
|
d.name AS department,
|
||||||
j.title AS designation
|
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
|
||||||
LEFT JOIN addresses a ON p.partner_id = a.partner_id
|
JOIN contracts c ON e.employee_id = c.employee_id AND c.status = 'ACTIVE'
|
||||||
JOIN contracts c ON e.employee_id = c.employee_id
|
|
||||||
JOIN departments d ON c.department_id = d.department_id
|
JOIN departments d ON c.department_id = d.department_id
|
||||||
JOIN job_positions j ON c.job_id = j.job_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
|
||||||
|
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 data = rows as any[];
|
const empData = empRows as any[];
|
||||||
|
|
||||||
if (data.length === 0) {
|
if (empData.length === 0) {
|
||||||
ctx.response.status = 404;
|
ctx.response.status = 404;
|
||||||
ctx.response.body = {
|
ctx.response.body = { success: false, message: "Employee not found" };
|
||||||
success: false,
|
|
||||||
message: "Employee not found"
|
|
||||||
};
|
|
||||||
return;
|
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],
|
||||||
|
);
|
||||||
|
|
||||||
|
// 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[0]
|
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,
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
addresses: addrRows, // Array of all addresses (PERMANENT, CURRENT, EMERGENCY)
|
||||||
|
},
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ctx.response.status = 500;
|
ctx.response.status = 500;
|
||||||
ctx.response.body = {
|
ctx.response.body = { success: false, error: (error as Error).message };
|
||||||
success: false,
|
|
||||||
error: (error as Error).message
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -185,10 +252,9 @@ export const updateEmployee = async (ctx: any) => {
|
|||||||
try {
|
try {
|
||||||
await connection.beginTransaction();
|
await connection.beginTransaction();
|
||||||
|
|
||||||
// 1. Fetch the partner_id linked to this employee
|
|
||||||
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],
|
||||||
);
|
);
|
||||||
|
|
||||||
const employees = employeeRows as any[];
|
const employees = employeeRows as any[];
|
||||||
@ -200,45 +266,48 @@ export const updateEmployee = async (ctx: any) => {
|
|||||||
|
|
||||||
const partnerId = employees[0].partner_id;
|
const partnerId = employees[0].partner_id;
|
||||||
|
|
||||||
// 2. Update the Partners table (Personal Data)
|
|
||||||
await connection.execute(
|
await connection.execute(
|
||||||
`UPDATE partners
|
`UPDATE partners
|
||||||
SET first_name = ?, last_name = ?, personal_email = ?, personal_phone = ?
|
SET first_name = ?, last_name = ?, personal_email = ?, personal_phone = ?
|
||||||
WHERE partner_id = ?`,
|
WHERE partner_id = ?`,
|
||||||
[data.firstName, data.lastName, data.personalEmail, data.personalPhone, partnerId]
|
[
|
||||||
|
data.firstName,
|
||||||
|
data.lastName,
|
||||||
|
data.personalEmail,
|
||||||
|
data.personalPhone,
|
||||||
|
partnerId,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
// 3. Update the Addresses table
|
|
||||||
await connection.execute(
|
await connection.execute(
|
||||||
`UPDATE addresses
|
`UPDATE addresses
|
||||||
SET door_number = ?, landmark = ?, address_line = ?, pincode = ?, district = ?, state = ?
|
SET door_number = ?, landmark = ?, address_line = ?, pincode = ?, district = ?, state = ?
|
||||||
WHERE partner_id = ? AND address_type = ?`,
|
WHERE partner_id = ? AND address_type = ?`,
|
||||||
[
|
[
|
||||||
data.address.doorNumber,
|
data.address.doorNumber,
|
||||||
data.address.landmark,
|
data.address.landmark,
|
||||||
data.address.line,
|
data.address.line,
|
||||||
data.address.pincode,
|
data.address.pincode,
|
||||||
data.address.district,
|
data.address.district,
|
||||||
data.address.state,
|
data.address.state,
|
||||||
partnerId,
|
partnerId,
|
||||||
data.address.type
|
data.address.type,
|
||||||
]
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
await connection.commit();
|
await connection.commit();
|
||||||
|
|
||||||
ctx.response.status = 200;
|
ctx.response.status = 200;
|
||||||
ctx.response.body = {
|
ctx.response.body = {
|
||||||
success: true,
|
success: true,
|
||||||
message: "Employee profile updated successfully"
|
message: "Employee profile updated successfully",
|
||||||
};
|
};
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await connection.rollback();
|
await connection.rollback();
|
||||||
ctx.response.status = 500;
|
ctx.response.status = 500;
|
||||||
ctx.response.body = {
|
ctx.response.body = {
|
||||||
success: false,
|
success: false,
|
||||||
error: "Update failed: " + (error as Error).message
|
error: "Update failed: " + (error as Error).message,
|
||||||
};
|
};
|
||||||
} finally {
|
} finally {
|
||||||
connection.release();
|
connection.release();
|
||||||
@ -251,7 +320,7 @@ export const deleteEmployee = async (ctx: any) => {
|
|||||||
try {
|
try {
|
||||||
const [result] = await pool.execute(
|
const [result] = await pool.execute(
|
||||||
`UPDATE employees SET is_active = false WHERE employee_id = ?`,
|
`UPDATE employees SET is_active = false WHERE employee_id = ?`,
|
||||||
[id]
|
[id],
|
||||||
);
|
);
|
||||||
|
|
||||||
const updateResult = result as any;
|
const updateResult = result as any;
|
||||||
@ -263,16 +332,15 @@ export const deleteEmployee = async (ctx: any) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ctx.response.status = 200;
|
ctx.response.status = 200;
|
||||||
ctx.response.body = {
|
ctx.response.body = {
|
||||||
success: true,
|
success: true,
|
||||||
message: "Employee account deactivated successfully"
|
message: "Employee account deactivated successfully",
|
||||||
};
|
};
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ctx.response.status = 500;
|
ctx.response.status = 500;
|
||||||
ctx.response.body = {
|
ctx.response.body = {
|
||||||
success: false,
|
success: false,
|
||||||
error: "Deactivation failed: " + (error as Error).message
|
error: "Deactivation failed: " + (error as Error).message,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { getDbPool } from "../../shared/db.ts"
|
import { getDbPool } from "../../shared/db.ts"
|
||||||
|
import {generateNextCode} from "../../shared/sequence.ts"
|
||||||
|
|
||||||
const pool = getDbPool("hrms_ems");
|
const pool = getDbPool("hrms_ems");
|
||||||
|
|
||||||
@ -37,11 +38,406 @@ export const getDepartments = async (ctx: any) => {
|
|||||||
|
|
||||||
export const getJobs = async (ctx: any) => {
|
export const getJobs = async (ctx: any) => {
|
||||||
try {
|
try {
|
||||||
const [rows] = await pool.query(`SELECT job_id, title, company_id FROM job_positions`);
|
const [rows] = await pool.query(`SELECT job_id, title, department_id FROM job_positions`);
|
||||||
ctx.response.status = 200;
|
ctx.response.status = 200;
|
||||||
ctx.response.body = { success: true, data: rows };
|
ctx.response.body = { success: true, 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 };
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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) => {
|
||||||
|
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 companies SET name = ?, parent_id = ? WHERE company_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;
|
||||||
|
}
|
||||||
|
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" };
|
||||||
|
} 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 };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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]
|
||||||
|
);
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
ctx.response.status = 500;
|
||||||
|
ctx.response.body = { success: false, error: errorMessage };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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 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]
|
||||||
|
);
|
||||||
|
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" };
|
||||||
|
} 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 };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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]
|
||||||
|
);
|
||||||
|
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 (?, ?)`,
|
||||||
|
[departmentId, data.managerId]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await connection.commit();
|
||||||
|
|
||||||
|
ctx.response.status = 201;
|
||||||
|
ctx.response.body = {
|
||||||
|
success: true,
|
||||||
|
message: "Department created",
|
||||||
|
department_id: departmentId
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
await connection.rollback();
|
||||||
|
ctx.response.status = 500;
|
||||||
|
ctx.response.body = { success: false, error: (error as Error).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 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]
|
||||||
|
);
|
||||||
|
|
||||||
|
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(
|
||||||
|
`INSERT INTO department_managers (department_id, employee_id) VALUES (?, ?)`,
|
||||||
|
[id, data.managerId]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
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" };
|
||||||
|
} 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 };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
const [result] = await pool.execute(
|
||||||
|
`INSERT INTO job_positions (department_id, title, description) VALUES (?, ?, ?)`,
|
||||||
|
[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 };
|
||||||
|
} catch (error) {
|
||||||
|
ctx.response.status = 500;
|
||||||
|
ctx.response.body = { success: false, error: (error as Error).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 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]
|
||||||
|
);
|
||||||
|
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" };
|
||||||
|
} 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 };
|
||||||
|
}
|
||||||
};
|
};
|
||||||
@ -16,18 +16,16 @@ export const bulkSeedEmployees = async (ctx: Context) => {
|
|||||||
let insertedCount = 0;
|
let insertedCount = 0;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Start transaction to keep data clean
|
|
||||||
await connection.beginTransaction();
|
await connection.beginTransaction();
|
||||||
|
|
||||||
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;
|
||||||
|
|
||||||
// Split first and last name if possible, fallback if singular name
|
|
||||||
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. Insert into partners (identity layer) with fallback defaults
|
// 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', ?, ?)
|
||||||
@ -36,25 +34,25 @@ export const bulkSeedEmployees = async (ctx: Context) => {
|
|||||||
);
|
);
|
||||||
const partnerId = partnerResult.insertId;
|
const partnerId = partnerResult.insertId;
|
||||||
|
|
||||||
// 2. Ensure the department exists, dynamically fetch or create its ID
|
// 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, name)
|
`INSERT INTO departments (company_id, branch_id, name)
|
||||||
VALUES (1, ?)
|
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"]
|
||||||
);
|
);
|
||||||
const departmentId = deptResult.insertId;
|
const departmentId = deptResult.insertId;
|
||||||
|
|
||||||
// 3. Ensure the job position exists, dynamically fetch or create its ID
|
// 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 (company_id, title)
|
`INSERT INTO job_positions (department_id, title)
|
||||||
VALUES (1, ?)
|
VALUES (?, ?)
|
||||||
ON DUPLICATE KEY UPDATE job_id = LAST_INSERT_ID(job_id)`,
|
ON DUPLICATE KEY UPDATE job_id = LAST_INSERT_ID(job_id)`,
|
||||||
[designation || "Trainee"]
|
[departmentId, designation || "Trainee"]
|
||||||
);
|
);
|
||||||
const jobId = jobResult.insertId;
|
const jobId = jobResult.insertId;
|
||||||
|
|
||||||
// 4. Create the core Employee record mapping to the KENT code
|
// 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)
|
||||||
@ -63,7 +61,7 @@ export const bulkSeedEmployees = async (ctx: Context) => {
|
|||||||
);
|
);
|
||||||
const employeeId = empResult.insertId;
|
const employeeId = empResult.insertId;
|
||||||
|
|
||||||
// 5. Establish the operational Contract record with the corporate work email
|
// 5. Establish operational Contract
|
||||||
await connection.execute(
|
await connection.execute(
|
||||||
`INSERT INTO contracts (employee_id, department_id, job_id, work_email, date_joining, 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', 'ACTIVE', 100)
|
VALUES (?, ?, ?, ?, '2026-01-01', 'ACTIVE', 100)
|
||||||
@ -74,7 +72,6 @@ export const bulkSeedEmployees = async (ctx: Context) => {
|
|||||||
insertedCount++;
|
insertedCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Commit changes safely to EMS
|
|
||||||
await connection.commit();
|
await connection.commit();
|
||||||
|
|
||||||
ctx.response.status = 200;
|
ctx.response.status = 200;
|
||||||
@ -85,10 +82,7 @@ export const bulkSeedEmployees = async (ctx: Context) => {
|
|||||||
} 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);
|
||||||
|
|
||||||
// Fixed: Cast error to 'any' or fallback to a string to satisfy deno-ts(18046)
|
|
||||||
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 {
|
} finally {
|
||||||
@ -98,7 +92,6 @@ export const bulkSeedEmployees = async (ctx: Context) => {
|
|||||||
|
|
||||||
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) {
|
if (employeeHierarchy.length === 0 && departmentHierarchy.length === 0) {
|
||||||
@ -114,10 +107,9 @@ export const bulkMapHierarchy = async (ctx: Context) => {
|
|||||||
try {
|
try {
|
||||||
await connection.beginTransaction();
|
await connection.beginTransaction();
|
||||||
|
|
||||||
// 1. Map Employees to their Managers
|
// 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(
|
||||||
@ -128,22 +120,21 @@ export const bulkMapHierarchy = async (ctx: Context) => {
|
|||||||
WHERE e.employee_code = ? AND c.status = 'ACTIVE'`,
|
WHERE e.employee_code = ? AND c.status = 'ACTIVE'`,
|
||||||
[manager_code, emp_code]
|
[manager_code, emp_code]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (result.affectedRows > 0) contractsUpdated++;
|
if (result.affectedRows > 0) contractsUpdated++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Map Departments to their Managers and Parent Departments
|
// 2. Map Departments to Managers and Parent Departments
|
||||||
for (const dept of departmentHierarchy) {
|
for (const dept of departmentHierarchy) {
|
||||||
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;
|
||||||
|
|
||||||
// Update Department Manager
|
// FIXED: Use department_managers junction table instead of departments.manager_id
|
||||||
if (manager_code) {
|
if (manager_code) {
|
||||||
await connection.execute(
|
await connection.execute(
|
||||||
`UPDATE departments d
|
`INSERT IGNORE INTO department_managers (department_id, employee_id)
|
||||||
JOIN employees m ON m.employee_code = ?
|
SELECT d.department_id, m.employee_id
|
||||||
SET d.manager_id = m.employee_id
|
FROM departments d
|
||||||
|
JOIN employees m ON m.employee_code = ?
|
||||||
WHERE d.name = ?`,
|
WHERE d.name = ?`,
|
||||||
[manager_code, department_name]
|
[manager_code, department_name]
|
||||||
);
|
);
|
||||||
@ -168,15 +159,11 @@ export const bulkMapHierarchy = async (ctx: Context) => {
|
|||||||
ctx.response.body = {
|
ctx.response.body = {
|
||||||
success: true,
|
success: true,
|
||||||
message: "Hierarchy mapping completed successfully.",
|
message: "Hierarchy mapping completed successfully.",
|
||||||
metrics: {
|
metrics: { contractsUpdated, departmentsProcessed: departmentsUpdated }
|
||||||
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 };
|
||||||
|
|||||||
@ -7,10 +7,10 @@ import {
|
|||||||
deleteEmployee
|
deleteEmployee
|
||||||
} from "./controllers/employee.controller.ts";
|
} from "./controllers/employee.controller.ts";
|
||||||
import {
|
import {
|
||||||
getCompanies,
|
getCompanies, createCompany, updateCompany, deleteCompany,
|
||||||
getBranches,
|
getBranches, createBranch, updateBranch, deleteBranch,
|
||||||
getDepartments,
|
getDepartments, createDepartment, updateDepartment, deleteDepartment,
|
||||||
getJobs
|
getJobs, createJob, updateJob, deleteJob
|
||||||
} from "./controllers/lookup.controller.ts";
|
} from "./controllers/lookup.controller.ts";
|
||||||
import {
|
import {
|
||||||
getEmployeeContracts,
|
getEmployeeContracts,
|
||||||
@ -64,15 +64,27 @@ router.post("/api/v1/contracts", requireAuth, requireRole([AppRole.SUPER_ADMIN,
|
|||||||
|
|
||||||
// Fetch legal entities and parent group structures.
|
// Fetch legal entities and parent group structures.
|
||||||
router.get("/api/v1/companies", requireAuth, getCompanies);
|
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);
|
||||||
|
|
||||||
// Fetch structural branch offices and physical locations.
|
// Fetch structural branch offices and physical locations.
|
||||||
router.get("/api/v1/branches", requireAuth, getBranches);
|
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);
|
||||||
|
|
||||||
// List corporate departments and organizational chart reporting lines.
|
// List corporate departments and organizational chart reporting lines.
|
||||||
router.get("/api/v1/departments", requireAuth, getDepartments);
|
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);
|
||||||
|
|
||||||
// List company designations and employment titles.
|
// List company designations and employment titles.
|
||||||
router.get("/api/v1/jobs", requireAuth, getJobs);
|
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);
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// SYSTEM & MIGRATION DOMAIN
|
// SYSTEM & MIGRATION DOMAIN
|
||||||
|
|||||||
@ -15,7 +15,7 @@ CREATE TABLE branches (
|
|||||||
branch_id INT AUTO_INCREMENT PRIMARY KEY,
|
branch_id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
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, -- e.g., 'BLR-HQ'
|
code VARCHAR(20) NOT NULL UNIQUE,
|
||||||
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;
|
||||||
@ -51,26 +51,27 @@ CREATE TABLE addresses (
|
|||||||
CREATE TABLE departments (
|
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,
|
||||||
name VARCHAR(50) NOT NULL,
|
name VARCHAR(50) NOT NULL,
|
||||||
parent_id INT NULL,
|
parent_id INT NULL,
|
||||||
manager_id INT NULL, -- Logical Reference to employee_id
|
|
||||||
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 (parent_id) REFERENCES departments(department_id) ON DELETE SET NULL
|
FOREIGN KEY (parent_id) REFERENCES departments(department_id) ON DELETE SET NULL
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
-- 6. Functional Designations
|
-- 6. Functional Designations
|
||||||
CREATE TABLE job_positions (
|
CREATE TABLE job_positions (
|
||||||
job_id INT AUTO_INCREMENT PRIMARY KEY,
|
job_id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
company_id INT NOT NULL,
|
department_id INT NOT NULL,
|
||||||
title VARCHAR(50) NOT NULL,
|
title VARCHAR(50) NOT NULL,
|
||||||
description TEXT NULL,
|
description TEXT NULL,
|
||||||
FOREIGN KEY (company_id) REFERENCES companies(company_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;
|
||||||
|
|
||||||
-- 7. Master Employee Mapping Engine
|
-- 7. Master Employee Mapping Engine
|
||||||
CREATE TABLE employees (
|
CREATE TABLE employees (
|
||||||
employee_id INT AUTO_INCREMENT PRIMARY KEY,
|
employee_id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
employee_code VARCHAR(20) NOT NULL UNIQUE, -- Matches your Excel Base Column
|
employee_code VARCHAR(20) NOT NULL UNIQUE,
|
||||||
partner_id INT NOT NULL,
|
partner_id INT NOT NULL,
|
||||||
company_id INT NOT NULL,
|
company_id INT NOT NULL,
|
||||||
branch_id INT NOT NULL,
|
branch_id INT NOT NULL,
|
||||||
@ -87,13 +88,30 @@ CREATE TABLE contracts (
|
|||||||
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, -- Logical Reference to employee_id
|
reporting_to_id INT NULL,
|
||||||
work_email VARCHAR(100) NOT NULL UNIQUE,
|
work_email VARCHAR(100) NOT NULL UNIQUE,
|
||||||
date_joining DATE NOT NULL,
|
date_joining DATE NOT NULL,
|
||||||
probation_days INT DEFAULT 90,
|
probation_days INT DEFAULT 90,
|
||||||
status ENUM('DRAFT', 'ACTIVE', 'EXPIRED', 'TERMINATED') DEFAULT 'DRAFT',
|
status ENUM('DRAFT', 'ACTIVE', 'EXPIRED', 'TERMINATED') DEFAULT 'DRAFT',
|
||||||
salary_structure_id INT NOT NULL, -- Logical Reference to PMS Microservice database
|
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
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- 9. Department Managers (Junction Table)
|
||||||
|
CREATE TABLE department_managers (
|
||||||
|
department_id INT NOT NULL,
|
||||||
|
employee_id INT NOT NULL,
|
||||||
|
assigned_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
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
|
||||||
|
CREATE TABLE system_sequences (
|
||||||
|
sequence_id VARCHAR(50) PRIMARY KEY,
|
||||||
|
prefix VARCHAR(10) NOT NULL,
|
||||||
|
current_value INT NOT NULL DEFAULT 0
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
51
shared/auth.ts
Normal file
51
shared/auth.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
// shared/auth.ts
|
||||||
|
import { Context, Next } from "@oak/oak";
|
||||||
|
|
||||||
|
export enum AppRole {
|
||||||
|
SUPER_ADMIN = "SUPER_ADMIN",
|
||||||
|
ADMIN = "ADMIN",
|
||||||
|
MANAGER = "MANAGER",
|
||||||
|
EMPLOYEE = "EMPLOYEE",
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Authentication Middleware (Who are you?)
|
||||||
|
export const requireAuth = async (ctx: Context, next: Next) => {
|
||||||
|
// Mocking the authorization for now.
|
||||||
|
// Later, we will extract the JWT from ctx.request.headers.get("Authorization")
|
||||||
|
// and verify it with your SSO provider here.
|
||||||
|
|
||||||
|
const isAuthenticated = true; // Simulating a successful login
|
||||||
|
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
ctx.response.status = 401;
|
||||||
|
ctx.response.body = { success: false, message: "Missing or invalid token" };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Injecting a mock user state so the next middleware can read it
|
||||||
|
ctx.state.user = {
|
||||||
|
employee_id: 135, // Example ID
|
||||||
|
role: AppRole.SUPER_ADMIN, // Change this to test different access levels
|
||||||
|
branch_id: 1
|
||||||
|
};
|
||||||
|
|
||||||
|
await next();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 2. Authorization Middleware (What are you allowed to do?)
|
||||||
|
export const requireRole = (allowedRoles: AppRole[]) => {
|
||||||
|
return async (ctx: Context, next: Next) => {
|
||||||
|
const userRole = ctx.state.user?.role;
|
||||||
|
|
||||||
|
if (!userRole || !allowedRoles.includes(userRole)) {
|
||||||
|
ctx.response.status = 403;
|
||||||
|
ctx.response.body = {
|
||||||
|
success: false,
|
||||||
|
message: "Access Denied: You do not have the required permissions."
|
||||||
|
};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await next();
|
||||||
|
};
|
||||||
|
};
|
||||||
50
shared/sequence.ts
Normal file
50
shared/sequence.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
// shared/sequence.ts
|
||||||
|
import { getDbPool } from "./db.ts";
|
||||||
|
|
||||||
|
const pool = getDbPool("hrms_ems");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a sequential code for a given entity.
|
||||||
|
* @param sequenceId Unique identifier for the counter (e.g., 'EMPLOYEE', 'BRANCH_BEN')
|
||||||
|
* @param prefix The string to prepend to the number (e.g., 'CLRI', 'BEN-')
|
||||||
|
* @param padding How many digits the number should be (e.g., 3 -> '001')
|
||||||
|
*/
|
||||||
|
export const generateNextCode = async (
|
||||||
|
sequenceId: string,
|
||||||
|
prefix: string,
|
||||||
|
padding: number = 3
|
||||||
|
): Promise<string> => {
|
||||||
|
const connection = await pool.getConnection();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await connection.beginTransaction();
|
||||||
|
|
||||||
|
// 1. Insert a new counter if it doesn't exist, OR increment the existing one atomically
|
||||||
|
await connection.execute(
|
||||||
|
`INSERT INTO system_sequences (sequence_id, prefix, current_value)
|
||||||
|
VALUES (?, ?, 1)
|
||||||
|
ON DUPLICATE KEY UPDATE current_value = current_value + 1, prefix = ?`,
|
||||||
|
[sequenceId, prefix, prefix]
|
||||||
|
);
|
||||||
|
|
||||||
|
// 2. Safely retrieve the updated value
|
||||||
|
const [rows] = await connection.execute(
|
||||||
|
`SELECT prefix, current_value FROM system_sequences WHERE sequence_id = ?`,
|
||||||
|
[sequenceId]
|
||||||
|
);
|
||||||
|
|
||||||
|
await connection.commit();
|
||||||
|
|
||||||
|
const data = (rows as any[])[0];
|
||||||
|
|
||||||
|
// 3. Format the result (e.g., prefix "CLRI" + value 1 + padding 3 = "CLRI001")
|
||||||
|
const paddedValue = data.current_value.toString().padStart(padding, '0');
|
||||||
|
return `${data.prefix}${paddedValue}`;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
await connection.rollback();
|
||||||
|
throw new Error(`Failed to generate sequence for ${sequenceId}: ${(error as Error).message}`);
|
||||||
|
} finally {
|
||||||
|
connection.release();
|
||||||
|
}
|
||||||
|
};
|
||||||
Loading…
x
Reference in New Issue
Block a user