diff --git a/.env.example b/.env.example
index 3be6766..8368363 100644
--- a/.env.example
+++ b/.env.example
@@ -1,18 +1,18 @@
-# # SQL DB Connection Colo
-# SQL_HOST=117.102.231.130
+# SQL DB Connection Colo
+SQL_HOST=117.102.231.130
+SQL_DATABASE=cod_piu
+SQL_USERNAME=sa
+SQL_PASSWORD=@R3M4niA.
+SQL_PORT=1433
+
+# SQL_HOST=203.153.114.226
+# SQL_PORT=1112
# SQL_DATABASE=piu
# SQL_USERNAME=sa
-# SQL_PASSWORD=@R3M4niA.
-# SQL_PORT=1433
-
-SQL_HOST=203.153.114.226
-SQL_PORT=1112
-SQL_DATABASE=piu
-SQL_USERNAME=sa
-SQL_PASSWORD=piu123
+# SQL_PASSWORD=piu123
# Application Port - express server listens on this port (default 9000).
-PORT=9528
+PORT=9530
ENDPOINT_WA=http://203.153.114.226:9529/send
# ENDPOINT_WA=http://localhost:9529/send
ENDPOINT_FE=http://203.153.114.226:9527
diff --git a/.gitignore b/.gitignore
index ee0f965..51d619b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,3 +3,4 @@ node_modules
.vscode
request.http
*.rest
+package-lock.json
\ No newline at end of file
diff --git a/app.js b/app.js
index 8674820..c83b7ff 100644
--- a/app.js
+++ b/app.js
@@ -8,6 +8,7 @@ const helmet = require("helmet");
const compression = require("compression");
const unknownEndpoint = require("./middleware/unKnownEndpoint");
const { handleError } = require("./helpers/error");
+const { checkConnection } = require("./config");
const app = express();
@@ -24,6 +25,25 @@ app.use("/api", routes);
app.get("/", (req, res) =>
res.send("
HAHALO
")
);
+
+app.get("/check-db", async (req, res) => {
+ try {
+ const isConnected = await checkConnection();
+ res.json({
+ success: isConnected,
+ message: isConnected
+ ? "Koneksi database OK"
+ : "Koneksi database gagal",
+ });
+ } catch (error) {
+ res.status(500).json({
+ success: false,
+ message: "Terjadi kesalahan saat cek koneksi database",
+ error: error.message,
+ });
+ }
+});
+
app.use(unknownEndpoint);
app.use(handleError);
diff --git a/config/index.js b/config/index.js
index 8ed5673..fe0b713 100644
--- a/config/index.js
+++ b/config/index.js
@@ -28,6 +28,18 @@ const poolPromise = new sql.ConnectionPool(config)
process.exit(1);
});
+async function checkConnection() {
+ try {
+ const pool = await poolPromise;
+ await pool.request().query('SELECT 1 AS isConnected');
+ console.log('🔍 SQL Server terkoneksi dengan baik');
+ return true;
+ } catch (error) {
+ console.error('⚠️ Gagal cek koneksi SQL Server:', error);
+ return false;
+ }
+}
+
/**
* Wrapper query (auto konversi $1 → @p1)
*/
@@ -46,6 +58,11 @@ async function query(text, params = []) {
return request.query(sqlText);
}
+function isValidDate(dateStr) {
+ const d = new Date(dateStr);
+ return !isNaN(d.getTime()); // true kalau valid
+}
+
/**
* Build filter query
*/
@@ -71,10 +88,24 @@ function buildFilterQuery(filterQuery = [], fixedParams = []) {
queryParams.push(f.param ? 1 : 0);
whereConditions.push(`${f.column} = $${queryParams.length}`);
break;
+
+ case 'between':
+ if (Array.isArray(f.param) && f.param.length === 2) {
+ const from = f.param[0];
+ const to = f.param[1];
+ if (isValidDate(from) && isValidDate(to)) {
+ queryParams.push(from);
+ queryParams.push(to);
+ whereConditions.push(
+ `${f.column} BETWEEN $${queryParams.length - 1} AND $${queryParams.length}`
+ );
+ }
+ }
+ break;
}
});
- return { whereConditions, queryParams };
+ return { whereConditions, whereParamAnd: queryParams };
}
/**
@@ -96,13 +127,17 @@ function buildStringOrIlike(columnParam, criteria, fixedParams = []) {
? `AND (${orStringConditions.join(" OR ")})`
: "";
- return { whereOrConditions: whereClause, whereParam: queryParams };
+ return { whereOrConditions: whereClause, whereParamOr: queryParams };
}
/**
* Build dynamic UPDATE
*/
function buildDynamicUpdate(table, data, where) {
+
+ data.updated_by = data.userId
+ delete data.userId;
+
const setParts = [];
const values = [];
let index = 1;
@@ -118,8 +153,8 @@ function buildDynamicUpdate(table, data, where) {
throw new Error("Tidak ada kolom untuk diupdate");
}
- // updated_at otomatis pakai GETDATE()
- setParts.push(`updated_at = GETDATE()`);
+ // updated_at otomatis pakai CURRENT_TIMESTAMP
+ setParts.push(`updated_at = CURRENT_TIMESTAMP`);
const whereParts = [];
for (const [key, value] of Object.entries(where)) {
@@ -140,6 +175,11 @@ function buildDynamicUpdate(table, data, where) {
* Build dynamic INSERT
*/
function buildDynamicInsert(table, data) {
+
+ data.created_by = data.userId
+ data.updated_by = data.userId
+ delete data.userId;
+
const columns = [];
const placeholders = [];
const values = [];
@@ -159,7 +199,7 @@ function buildDynamicInsert(table, data) {
// created_at & updated_at otomatis
columns.push("created_at", "updated_at");
- placeholders.push("GETDATE()", "GETDATE()");
+ placeholders.push("CURRENT_TIMESTAMP", "CURRENT_TIMESTAMP");
const query = `
INSERT INTO ${table} (${columns.join(", ")})
@@ -195,6 +235,7 @@ async function generateKode(prefix, tableName, columnName) {
}
module.exports = {
+ checkConnection,
query,
buildFilterQuery,
buildStringOrIlike,
diff --git a/controllers/auth.controller.js b/controllers/auth.controller.js
index 464b3cd..de1a9dd 100644
--- a/controllers/auth.controller.js
+++ b/controllers/auth.controller.js
@@ -1,26 +1,99 @@
-const authService = require("../services/auth.service");
+const AuthService = require('../services/auth.service');
+const { setResponse, checkValidate } = require('../helpers/utils');
+const { registerSchema, loginSchema } = require('../validate/auth.schema');
+const { createCaptcha } = require('../utils/captcha');
-const loginUser = async (req, res) => {
- const { username, password, role, tenant } = req.body;
- const { token, refreshToken, user } = await authService.login(
- username,
- password,
- tenant
- );
+class AuthController {
+ // Register
+ static async register(req, res) {
+ const { error, value } = await checkValidate(registerSchema, req);
- res.header("auth-token", token);
- res.cookie("refreshToken", refreshToken, {
- httpOnly: true,
- sameSite: process.env.NODE_ENV === "development" ? true : "none",
- secure: process.env.NODE_ENV === "development" ? false : true,
- });
- res.status(200).json({
- token,
- refreshToken,
- user,
- });
-};
+ if (error) {
+ return res.status(400).json(setResponse(error, 'Validation failed', 400));
+ }
-module.exports = {
- loginUser,
-};
+ // Format nomor HP Indonesia
+ if (value.user_phone && value.user_phone.startsWith('0')) {
+ value.user_phone = '+62' + value.user_phone.slice(1);
+ }
+
+ const results = await AuthService.register(value);
+
+ const response = await setResponse(
+ {
+ user: { ...results.user, approved: false },
+ },
+ 'User registered successfully. Waiting for admin approval.'
+ );
+
+ res.status(response.statusCode).json(response);
+ }
+
+ // Login
+ static async login(req, res) {
+ const { error, value } = await checkValidate(loginSchema, req);
+
+ if (error) {
+ return res.status(400).json(setResponse(error, 'Validation failed', 400));
+ }
+
+ const results = await AuthService.login(value);
+
+ // Simpan refresh token di cookie
+ res.cookie('refreshToken', results.tokens.refreshToken, {
+ httpOnly: true,
+ secure: false,
+ sameSite: 'lax',
+ maxAge: 7 * 24 * 60 * 60 * 1000
+ });
+
+ const response = await setResponse(
+ {
+ user: { ...results.user, approved: true },
+ accessToken: results.tokens.accessToken
+ },
+ 'Login successful'
+ );
+
+ res.status(response.statusCode).json(response);
+ }
+
+ // Refresh Token
+ static async refreshToken(req, res) {
+ const refreshToken = req.cookies?.refreshToken;
+
+ if (!refreshToken) {
+ return res.status(401).json(setResponse(null, 'Refresh token is required', 401));
+ }
+
+ const results = await AuthService.refreshToken(refreshToken);
+ const response = await setResponse(results, 'Token refreshed successfully');
+
+ res.status(response.statusCode).json(response);
+ }
+
+ // Logout
+ static async logout(req, res) {
+ res.clearCookie('refreshToken', {
+ httpOnly: true,
+ sameSite: 'none',
+ secure: true
+ });
+
+ const response = await setResponse(null, 'Logged out successfully');
+ res.status(response.statusCode).json(response);
+ }
+
+ // Captcha
+ static async generateCaptcha(req, res) {
+ const { svg, text } = createCaptcha();
+
+ // Tampilkan captcha di header untuk dev
+ res.setHeader('X-Captcha-Text', text);
+
+ const response = await setResponse({ svg, text }, 'Captcha generated');
+ res.status(response.statusCode).json(response);
+ }
+}
+
+module.exports = AuthController;
diff --git a/controllers/device.controller.js b/controllers/device.controller.js
new file mode 100644
index 0000000..7b2481c
--- /dev/null
+++ b/controllers/device.controller.js
@@ -0,0 +1,71 @@
+const DeviceService = require('../services/device.service');
+const { setResponse, setResponsePaging, checkValidate } = require('../helpers/utils');
+const { insertDeviceSchema, updateDeviceSchema } = require('../validate/device.schema');
+
+class DeviceController {
+ // Get all devices
+ static async getAll(req, res) {
+ const queryParams = req.query;
+
+ const results = await DeviceService.getAllDevices(queryParams);
+ const response = await setResponsePaging(queryParams, results, 'Device found')
+
+ res.status(response.statusCode).json(response);
+ }
+
+ // Get device by ID
+ static async getById(req, res) {
+ const { id } = req.params;
+
+ const results = await DeviceService.getDeviceById(id);
+ const response = await setResponse(results, 'Device found')
+
+ res.status(response.statusCode).json(response);
+ }
+
+ // Create device
+ static async create(req, res) {
+ const { error, value } = await checkValidate(insertDeviceSchema, req)
+
+ if (error) {
+ return res.status(400).json(setResponse(error, 'Validation failed', 400));
+ }
+
+ value.userId = req.user.user_id
+
+ const results = await DeviceService.createDevice(value);
+ const response = await setResponse(results, 'Device created successfully')
+
+ return res.status(response.statusCode).json(response);
+ }
+
+ // Update device
+ static async update(req, res) {
+ const { id } = req.params;
+
+ const { error, value } = checkValidate(updateDeviceSchema, req)
+
+ if (error) {
+ return res.status(400).json(setResponse(error, 'Validation failed', 400));
+ }
+
+ value.userId = req.user.user_id
+
+ const results = await DeviceService.updateDevice(id, value);
+ const response = await setResponse(results, 'Device updated successfully')
+
+ res.status(response.statusCode).json(response);
+ }
+
+ // Soft delete device
+ static async delete(req, res) {
+ const { id } = req.params;
+
+ const results = await DeviceService.deleteDevice(id, req.user.user_id);
+ const response = await setResponse(results, 'Device deleted successfully')
+
+ res.status(response.statusCode).json(response);
+ }
+}
+
+module.exports = DeviceController;
diff --git a/controllers/roles.controller.js b/controllers/roles.controller.js
new file mode 100644
index 0000000..d7ee09b
--- /dev/null
+++ b/controllers/roles.controller.js
@@ -0,0 +1,71 @@
+const RolesService = require('../services/roles.service');
+const { setResponse, setResponsePaging, checkValidate } = require('../helpers/utils');
+const { updateRolesSchema, insertRolesSchema } = require('../validate/roles.schema');
+
+class RolesController {
+ // Get all Roles
+ static async getAll(req, res) {
+ const queryParams = req.query;
+
+ const results = await RolesService.getAllRoles(queryParams);
+ const response = await setResponsePaging(queryParams, results, 'Roles found')
+
+ res.status(response.statusCode).json(response);
+ }
+
+ // Get Roles by ID
+ static async getById(req, res) {
+ const { id } = req.params;
+
+ const results = await RolesService.getRolesById(id);
+ const response = await setResponse(results, 'Roles found')
+
+ res.status(response.statusCode).json(response);
+ }
+
+ // Create Roles
+ static async create(req, res) {
+ const { error, value } = await checkValidate(insertRolesSchema, req)
+
+ if (error) {
+ return res.status(400).json(setResponse(error, 'Validation failed', 400));
+ }
+
+ value.userId = req.user.user_id
+
+ const results = await RolesService.createRoles(value);
+ const response = await setResponse(results, 'Roles created successfully')
+
+ return res.status(response.statusCode).json(response);
+ }
+
+ // Update Roles
+ static async update(req, res) {
+ const { id } = req.params;
+
+ const { error, value } = checkValidate(updateRolesSchema, req)
+
+ if (error) {
+ return res.status(400).json(setResponse(error, 'Validation failed', 400));
+ }
+
+ value.userId = req.user.user_id
+
+ const results = await RolesService.updateRoles(id, value);
+ const response = await setResponse(results, 'Roles updated successfully')
+
+ res.status(response.statusCode).json(response);
+ }
+
+ // Soft delete Roles
+ static async delete(req, res) {
+ const { id } = req.params;
+
+ const results = await RolesService.deleteRoles(id, req.user.user_id);
+ const response = await setResponse(results, 'Roles deleted successfully')
+
+ res.status(response.statusCode).json(response);
+ }
+}
+
+module.exports = RolesController;
diff --git a/controllers/schedule.controller.js b/controllers/schedule.controller.js
new file mode 100644
index 0000000..f8798b3
--- /dev/null
+++ b/controllers/schedule.controller.js
@@ -0,0 +1,71 @@
+const ScheduleService = require('../services/schedule.service');
+const { setResponse, setResponsePaging, checkValidate } = require('../helpers/utils');
+const { updateScheduleSchema, insertScheduleSchema } = require('../validate/schedule.schema');
+
+class ScheduleController {
+ // Get all Schedule
+ static async getAll(req, res) {
+ const queryParams = req.query;
+
+ const results = await ScheduleService.getAllSchedule(queryParams);
+ const response = await setResponsePaging(queryParams, results, 'Schedule found')
+
+ res.status(response.statusCode).json(response);
+ }
+
+ // Get Schedule by ID
+ static async getById(req, res) {
+ const { id } = req.params;
+
+ const results = await ScheduleService.getScheduleById(id);
+ const response = await setResponse(results, 'Schedule found')
+
+ res.status(response.statusCode).json(response);
+ }
+
+ // Create Schedule
+ static async create(req, res) {
+ const { error, value } = await checkValidate(insertScheduleSchema, req)
+
+ if (error) {
+ return res.status(400).json(setResponse(error, 'Validation failed', 400));
+ }
+
+ value.userId = req.user.user_id
+
+ const results = await ScheduleService.insertScheduleDb(value);
+ const response = await setResponse(results, 'Schedule created successfully')
+
+ return res.status(response.statusCode).json(response);
+ }
+
+ // Update Schedule
+ static async update(req, res) {
+ const { id } = req.params;
+
+ const { error, value } = checkValidate(updateScheduleSchema, req)
+
+ if (error) {
+ return res.status(400).json(setResponse(error, 'Validation failed', 400));
+ }
+
+ value.userId = req.user.user_id
+
+ const results = await ScheduleService.updateSchedule(id, value);
+ const response = await setResponse(results, 'Schedule updated successfully')
+
+ res.status(response.statusCode).json(response);
+ }
+
+ // Soft delete Schedule
+ static async delete(req, res) {
+ const { id } = req.params;
+
+ const results = await ScheduleService.deleteSchedule(id, req.user.user_id);
+ const response = await setResponse(results, 'Schedule deleted successfully')
+
+ res.status(response.statusCode).json(response);
+ }
+}
+
+module.exports = ScheduleController;
diff --git a/controllers/shift.controller.js b/controllers/shift.controller.js
new file mode 100644
index 0000000..ce5ae5c
--- /dev/null
+++ b/controllers/shift.controller.js
@@ -0,0 +1,71 @@
+const ShiftService = require('../services/shift.service');
+const { setResponse, setResponsePaging, checkValidate } = require('../helpers/utils');
+const { updateShiftSchema, insertShiftSchema } = require('../validate/shift.schema');
+
+class ShiftController {
+ // Get all Shift
+ static async getAll(req, res) {
+ const queryParams = req.query;
+
+ const results = await ShiftService.getAllShift(queryParams);
+ const response = await setResponsePaging(queryParams, results, 'Shift found')
+
+ res.status(response.statusCode).json(response);
+ }
+
+ // Get Shift by ID
+ static async getById(req, res) {
+ const { id } = req.params;
+
+ const results = await ShiftService.getShiftById(id);
+ const response = await setResponse(results, 'Shift found')
+
+ res.status(response.statusCode).json(response);
+ }
+
+ // Create Shift
+ static async create(req, res) {
+ const { error, value } = await checkValidate(insertShiftSchema, req)
+
+ if (error) {
+ return res.status(400).json(setResponse(error, 'Validation failed', 400));
+ }
+
+ value.userId = req.user.user_id
+
+ const results = await ShiftService.createShift(value);
+ const response = await setResponse(results, 'Shift created successfully')
+
+ return res.status(response.statusCode).json(response);
+ }
+
+ // Update Shift
+ static async update(req, res) {
+ const { id } = req.params;
+
+ const { error, value } = checkValidate(updateShiftSchema, req)
+
+ if (error) {
+ return res.status(400).json(setResponse(error, 'Validation failed', 400));
+ }
+
+ value.userId = req.user.user_id
+
+ const results = await ShiftService.updateShift(id, value);
+ const response = await setResponse(results, 'Shift updated successfully')
+
+ res.status(response.statusCode).json(response);
+ }
+
+ // Soft delete Shift
+ static async delete(req, res) {
+ const { id } = req.params;
+
+ const results = await ShiftService.deleteShift(id, req.user.user_id);
+ const response = await setResponse(results, 'Shift deleted successfully')
+
+ res.status(response.statusCode).json(response);
+ }
+}
+
+module.exports = ShiftController;
diff --git a/controllers/status.controller.js b/controllers/status.controller.js
new file mode 100644
index 0000000..859be33
--- /dev/null
+++ b/controllers/status.controller.js
@@ -0,0 +1,73 @@
+const StatusService = require('../services/status.service');
+const { setResponse, setResponsePaging, checkValidate } = require('../helpers/utils');
+const { insertStatusSchema, updateStatusSchema } = require('../validate/status.schema');
+
+class StatusController {
+ // Get all status
+ static async getAll(req, res) {
+ const queryParams = req.query;
+
+ const results = await StatusService.getAllStatus(queryParams);
+ const response = await setResponsePaging(queryParams, results, 'Status found');
+
+ res.status(response.statusCode).json(response);
+ }
+
+ // Get status by ID
+ static async getById(req, res) {
+ const { id } = req.params;
+
+ const results = await StatusService.getStatusById(id);
+ const response = await setResponse(results, 'Status found');
+
+ res.status(response.statusCode).json(response);
+ }
+
+ // Create status
+ static async create(req, res) {
+ const { error, value } = await checkValidate(insertStatusSchema, req);
+
+ if (error) {
+ return res.status(400).json(setResponse(error, 'Validation failed', 400));
+ }
+
+ value.userId = req.user.user_id;
+
+ const results = await StatusService.createStatus(value);
+ const response = await setResponse(results, 'Status created successfully');
+
+ return res.status(response.statusCode).json(response);
+ }
+
+ // Update status
+ static async update(req, res) {
+ const { id } = req.params;
+
+ console.log("REQ BODY:", req.body);
+
+ const { error, value } = await checkValidate(updateStatusSchema, req);
+
+ if (error) {
+ return res.status(400).json(setResponse(error, 'Validation failed', 400));
+ }
+
+ value.userId = req.user.user_id;
+
+ const results = await StatusService.updateStatus(id, value);
+ const response = await setResponse(results, 'Status updated successfully');
+
+ res.status(response.statusCode).json(response);
+ }
+
+ // Soft delete status
+ static async delete(req, res) {
+ const { id } = req.params;
+
+ const results = await StatusService.deleteStatus(id, req.user.user_id);
+ const response = await setResponse(results, 'Status deleted successfully');
+
+ res.status(response.statusCode).json(response);
+ }
+}
+
+module.exports = StatusController;
diff --git a/controllers/sub_section.controller.js b/controllers/sub_section.controller.js
new file mode 100644
index 0000000..f2a9655
--- /dev/null
+++ b/controllers/sub_section.controller.js
@@ -0,0 +1,71 @@
+const SubSectionService = require('../services/sub_section.service');
+const { setResponse, setResponsePaging, checkValidate } = require('../helpers/utils');
+const { insertSubSectionSchema, updateSubSectionSchema } = require('../validate/sub_section.schema');
+
+class SubSectionController {
+ // Get all sub sections
+ static async getAll(req, res) {
+ const queryParams = req.query;
+
+ const results = await SubSectionService.getAll(queryParams);
+ const response = await setResponsePaging(queryParams, results, 'Sub section found');
+
+ res.status(response.statusCode).json(response);
+ }
+
+ // Get sub section by ID
+ static async getById(req, res) {
+ const { id } = req.params;
+
+ const results = await SubSectionService.getById(id);
+ const response = await setResponse(results, 'Sub section found');
+
+ res.status(response.statusCode).json(response);
+ }
+
+ // Create sub section
+ static async create(req, res) {
+ const { error, value } = await checkValidate(insertSubSectionSchema, req);
+
+ if (error) {
+ return res.status(400).json(setResponse(error, 'Validation failed', 400));
+ }
+
+ value.userId = req.user.user_id;
+
+ const results = await SubSectionService.create(value);
+ const response = await setResponse(results, 'Sub section created successfully');
+
+ return res.status(response.statusCode).json(response);
+ }
+
+ // Update sub section
+ static async update(req, res) {
+ const { id } = req.params;
+
+ const { error, value } = checkValidate(updateSubSectionSchema, req);
+
+ if (error) {
+ return res.status(400).json(setResponse(error, 'Validation failed', 400));
+ }
+
+ value.userId = req.user.user_id;
+
+ const results = await SubSectionService.update(id, value);
+ const response = await setResponse(results, 'Sub section updated successfully');
+
+ res.status(response.statusCode).json(response);
+ }
+
+ // Soft delete sub section
+ static async delete(req, res) {
+ const { id } = req.params;
+
+ const results = await SubSectionService.delete(id, req.user.user_id);
+ const response = await setResponse(results, 'Sub section deleted successfully');
+
+ res.status(response.statusCode).json(response);
+ }
+}
+
+module.exports = SubSectionController;
\ No newline at end of file
diff --git a/controllers/tags.controller.js b/controllers/tags.controller.js
new file mode 100644
index 0000000..fa3dc16
--- /dev/null
+++ b/controllers/tags.controller.js
@@ -0,0 +1,71 @@
+const TagsService = require('../services/tags.service');
+const { setResponse, setResponsePaging, checkValidate } = require('../helpers/utils');
+const { insertTagsSchema, updateTagsSchema } = require('../validate/tags.schema');
+
+class TagsController {
+ // Get all devices
+ static async getAll(req, res) {
+ const queryParams = req.query;
+
+ const results = await TagsService.getAllTags(queryParams);
+ const response = await setResponsePaging(queryParams, results, 'Tags found')
+
+ res.status(response.statusCode).json(response);
+ }
+
+ // Get device by ID
+ static async getById(req, res) {
+ const { id } = req.params;
+
+ const results = await TagsService.getTagByID(id);
+ const response = await setResponse(results, 'Tags found')
+
+ res.status(response.statusCode).json(response);
+ }
+
+ // Create device
+ static async create(req, res) {
+ const { error, value } = await checkValidate(insertTagsSchema, req)
+
+ if (error) {
+ return res.status(400).json(setResponse(error, 'Validation failed', 400));
+ }
+
+ value.userId = req.user.user_id
+
+ const results = await TagsService.createTags(value);
+ const response = await setResponse(results, 'Tags created successfully')
+
+ return res.status(response.statusCode).json(response);
+ }
+
+ // Update device
+ static async update(req, res) {
+ const { id } = req.params;
+
+ const { error, value } = checkValidate(updateTagsSchema, req)
+
+ if (error) {
+ return res.status(400).json(setResponse(error, 'Validation failed', 400));
+ }
+
+ value.userId = req.user.user_id
+
+ const results = await TagsService.updateTags(id, value);
+ const response = await setResponse(results, 'Tags updated successfully')
+
+ res.status(response.statusCode).json(response);
+ }
+
+ // Soft delete device
+ static async delete(req, res) {
+ const { id } = req.params;
+
+ const results = await TagsService.deleteTags(id, req.user.user_id);
+ const response = await setResponse(results, 'Tags deleted successfully')
+
+ res.status(response.statusCode).json(response);
+ }
+}
+
+module.exports = TagsController;
diff --git a/controllers/unit.controller.js b/controllers/unit.controller.js
new file mode 100644
index 0000000..4e323f6
--- /dev/null
+++ b/controllers/unit.controller.js
@@ -0,0 +1,71 @@
+const UnitService = require('../services/unit.service');
+const { setResponse, setResponsePaging, checkValidate } = require('../helpers/utils');
+const { insertUnitSchema, updateUnitSchema } = require('../validate/unit.schema');
+
+class UnitController {
+ // Get all units
+ static async getAll(req, res) {
+ const queryParams = req.query;
+
+ const results = await UnitService.getAllUnits(queryParams);
+ const response = await setResponsePaging(queryParams, results, 'Unit found');
+
+ res.status(response.statusCode).json(response);
+ }
+
+ // Get unit by ID
+ static async getById(req, res) {
+ const { id } = req.params;
+
+ const results = await UnitService.getUnitById(id);
+ const response = await setResponse(results, 'Unit found');
+
+ res.status(response.statusCode).json(response);
+ }
+
+ // Create unit
+ static async create(req, res) {
+ const { error, value } = await checkValidate(insertUnitSchema, req);
+
+ if (error) {
+ return res.status(400).json(setResponse(error, 'Validation failed', 400));
+ }
+
+ value.created_by = req.user.user_id;
+
+ const results = await UnitService.createUnit(value);
+ const response = await setResponse(results, 'Unit created successfully');
+
+ return res.status(response.statusCode).json(response);
+ }
+
+ // Update unit
+ static async update(req, res) {
+ const { id } = req.params;
+
+ const { error, value } = checkValidate(updateUnitSchema, req);
+
+ if (error) {
+ return res.status(400).json(setResponse(error, 'Validation failed', 400));
+ }
+
+ value.updated_by = req.user.user_id;
+
+ const results = await UnitService.updateUnit(id, value);
+ const response = await setResponse(results, 'Unit updated successfully');
+
+ res.status(response.statusCode).json(response);
+ }
+
+ // Soft delete unit
+ static async delete(req, res) {
+ const { id } = req.params;
+
+ const results = await UnitService.deleteUnit(id, req.user.user_id);
+ const response = await setResponse(results, 'Unit deleted successfully');
+
+ res.status(response.statusCode).json(response);
+ }
+}
+
+module.exports = UnitController;
diff --git a/controllers/users.controller.js b/controllers/users.controller.js
index ae9548e..aef8858 100644
--- a/controllers/users.controller.js
+++ b/controllers/users.controller.js
@@ -1,172 +1,108 @@
-const userService = require("../services/user.service");
-const { ErrorHandler } = require("../helpers/error");
-const { hashPassword } = require("../helpers/hashPassword");
-const { setResponse, setPaging, setResponsePaging } = require("../helpers/utils");
-const Joi = require("joi");
+const UserService = require("../services/user.service");
+const { setResponse, setResponsePaging, checkValidate } = require("../helpers/utils");
+const { userSchema, updateUserSchema, newPasswordSchema } = require("../validate/user.schema");
-// Definisikan skema validasi
-const validateTerm = Joi.object({
- user_fullname: Joi.string().max(255).required(),
- user_name: Joi.string().max(255).required(),
- user_email: Joi.string().max(255).email().allow(null),
- user_password: Joi.string().max(255).required(),
- role_id: Joi.number().integer().allow(null),
- is_active: Joi.boolean().required()
-});
+class UserController {
+ // Get all users
+ static async getAll(req, res) {
+ const queryParams = req.query;
-const getAllUsers = async (req, res) => {
+ const results = await UserService.getAllUsers(queryParams);
+ const response = await setResponsePaging(queryParams, results, 'Users found');
- const {
- page = 1,
- limit = 10,
- fullname: userFullname,
- username: userName,
- is_active: isActive,
- criteria,
- tenantID,
- } = req.query
+ res.status(response.statusCode).json(response);
+ }
- const offset = (page - 1) * limit;
+ // Get user by ID
+ static async getById(req, res) {
+ const { id } = req.params;
- const filterQuery = {
- fixed: {
- limit, offset, tenantID
- },
- filterQuery: [
- {
- type: 'string',
- column: 'user_fullname',
- param: userFullname
- },
- {
- type: 'string',
- column: 'user_name',
- param: userName
- },
- {
- type: 'number',
- column: 'is_active',
- param: isActive
- }
- ],
- filterCriteria:
- {
- criteria,
- column: [
- 'user_fullname', 'user_name'
- ]
+ const results = await UserService.getUserById(id);
+ const response = await setResponse(results, 'User found');
+
+ res.status(response.statusCode).json(response);
+ }
+
+ // Create user
+ static async create(req, res) {
+ const { error, value } = await checkValidate(userSchema, req);
+
+ if (error) {
+ return res.status(400).json(setResponse(error, 'Validation failed', 400));
}
+
+ value.approved_by = req.user.user_id;
+
+ const results = await UserService.createUser(value);
+ const response = await setResponse(results, 'User created successfully');
+
+ res.status(response.statusCode).json(response);
}
- const results = await userService.getAllUsers(filterQuery)
- const response = await setResponsePaging(results.data, results.total, parseInt(limit), parseInt(page))
+ // Update user
+ static async update(req, res) {
+ const { id } = req.params;
- res.status(response.statusCode).json(response)
-};
+ const { error, value } = await checkValidate(updateUserSchema, req);
-const getAllStatusUsers = async (req, res) => {
+ if (error) {
+ return res.status(400).json(setResponse(error, 'Validation failed', 400));
+ }
- const results = await userService.getAllStatusUsers();
- const response = await setResponse(results)
+ value.userId = req.user.user_id;
- res.status(response.statusCode).json(response);
-};
+ const results = await UserService.updateUser(id, value);
+ const response = await setResponse(results, 'User updated successfully');
-const createUser = async (req, res) => {
+ res.status(response.statusCode).json(response);
+ }
+
+ // Approve user
+ static async approve(req, res) {
+ const { id } = req.params;
+ const approverId = req.user.user_id;
+
+ const updatedUser = await UserService.approveUser(id, approverId);
+ const response = await setResponse(updatedUser, 'User approved successfully');
- // Lakukan validasi
- const { error } = validateTerm.validate(req.body, { stripUnknown: true });
- if (error) {
- const response = await setResponse([], error.details[0].message, 400)
return res.status(response.statusCode).json(response);
}
- const results = await userService.createUser({
- userFullname: req.body.user_fullname,
- userName: req.body.user_name,
- userEmail: req.body.user_email,
- userPassword: req.body.user_password,
- roleId: req.body.role_id,
- isActive: req.body.is_active, // default 1 jika tidak dikirim
- userID: req.body.userID,
- tenantID: req.body.tenantID
- });
+ // Reject user
+ static async reject(req, res) {
+ const { id } = req.params;
+ const approverId = req.user.user_id;
- const response = await setResponse(results);
+ const updatedUser = await UserService.rejectUser(id, approverId);
+ const response = await setResponse(updatedUser, 'User rejected successfully');
- res.status(response.statusCode).json(response);
-};
-
-const getUserById = async (req, res) => {
- const { id } = req.params;
-
- const results = await userService.getUserById(id);
- const response = await setResponse(results)
-
- res.status(response.statusCode).json(response);
-};
-
-const getUserProfile = async (req, res) => {
- const { id } = req.user;
-
- const results = await userService.getUserById(id);
- const response = await setResponse(results)
-
- res.status(response.statusCode).json(response);
-};
-
-const updateUser = async (req, res) => {
-
- const { id } = req.params;
-
- // Lakukan validasi
- const { error } = validateTerm.validate(req.body, { stripUnknown: true });
- if (error) {
- const response = await setResponse([], error.details[0].message, 400)
return res.status(response.statusCode).json(response);
}
- const results = await userService.updateUser({
- userFullname: req.body.user_fullname,
- userName: req.body.user_name,
- userEmail: req.body.user_email,
- userPassword: req.body.user_password,
- roleId: req.body.role_id,
- isActive: req.body.is_active, // default 1 jika tidak dikirim
- userID: req.body.userID,
- tenantID: req.body.tenantID,
- id
- });
+ // Soft delete user
+ static async delete(req, res) {
+ const { id } = req.params;
- const response = await setResponse(results);
+ const results = await UserService.deleteUser(id, req.user.user_id);
+ const response = await setResponse(results, 'User deleted successfully');
- res.status(response.statusCode).json(response);
-};
+ res.status(response.statusCode).json(response);
+ }
-const deleteUser = async (req, res) => {
- const { id } = req.params;
- const userID = req.userID
+ // Change password
+ static async changePassword(req, res) {
+ const { id } = req.params;
+ const { error, value } = await checkValidate(newPasswordSchema, req);
- const results = await userService.deleteUser(id, userID);
- const response = await setResponse(results)
+ if (error) {
+ return res.status(400).json(setResponse(error, 'Validation failed', 400));
+ }
- res.status(response.statusCode).json(response);
-};
+ const results = await UserService.changeUserPassword(id, value.new_password);
+ const response = await setResponse(results, 'Password changed successfully');
-const getAllRoles = async (req, res) => {
- const results = await userService.getAllRoles(req.body.tenantID);
- const response = await setResponse(results)
+ res.status(response.statusCode).json(response);
+ }
+}
- res.status(response.statusCode).json(response);
-};
-
-module.exports = {
- getAllUsers,
- createUser,
- getUserById,
- updateUser,
- deleteUser,
- getUserProfile,
- getAllRoles,
- getAllStatusUsers
-};
+module.exports = UserController;
diff --git a/db/brand.db.js b/db/brand.db.js
new file mode 100644
index 0000000..6182e1a
--- /dev/null
+++ b/db/brand.db.js
@@ -0,0 +1,116 @@
+const pool = require("../config");
+
+// Get all brands
+const getAllBrandsDb = async (searchParams = {}) => {
+ let queryParams = [];
+
+ if (searchParams.limit) {
+ const page = Number(searchParams.page ?? 1) - 1;
+ queryParams = [Number(searchParams.limit ?? 10), page];
+ }
+
+ const { whereOrConditions, whereParamOr } = pool.buildStringOrIlike(
+ ["b.brand_name"],
+ searchParams.criteria,
+ queryParams
+ );
+
+ queryParams = whereParamOr ? whereParamOr : queryParams;
+
+ const { whereConditions, whereParamAnd } = pool.buildFilterQuery(
+ [
+ { column: "b.brand_name", param: searchParams.name, type: "string" },
+ { column: "b.created_by", param: searchParams.created_by, type: "number" },
+ ],
+ queryParams
+ );
+
+ queryParams = whereParamAnd ? whereParamAnd : queryParams;
+
+ const queryText = `
+ SELECT COUNT(*) OVER() AS total_data, b.*
+ FROM m_brands b
+ WHERE b.deleted_at IS NULL
+ ${whereConditions.length > 0 ? ` AND ${whereConditions.join(" AND ")}` : ""}
+ ${whereOrConditions ? whereOrConditions : ""}
+ ORDER BY b.brand_id ASC
+ ${searchParams.limit ? `OFFSET $2 ROWS FETCH NEXT $1 ROWS ONLY` : ""};
+ `;
+
+ const result = await pool.query(queryText, queryParams);
+
+ const total = result?.recordset.length > 0 ? parseInt(result.recordset[0].total_data, 10) : 0;
+
+ return { data: result.recordset, total };
+};
+
+// Get brand by ID
+const getBrandByIdDb = async (id) => {
+ const queryText = `
+ SELECT b.*
+ FROM m_brands b
+ WHERE b.brand_id = $1 AND b.deleted_at IS NULL
+ `;
+ const result = await pool.query(queryText, [id]);
+ return result.recordset;
+};
+
+// Get brand by name
+const getBrandByNameDb = async (name) => {
+ const queryText = `
+ SELECT b.*
+ FROM m_brands b
+ WHERE b.brand_name = $1 AND b.deleted_at IS NULL
+ `;
+ const result = await pool.query(queryText, [name]);
+ return result.recordset[0];
+};
+
+// Create brand
+const createBrandDb = async (data) => {
+ const store = {
+ ...data,
+ created_at: new Date(),
+ };
+
+ const { query: queryText, values } = pool.buildDynamicInsert("m_brands", store);
+ const result = await pool.query(queryText, values);
+ const insertedId = result.recordset[0]?.inserted_id;
+ return insertedId ? await getBrandByIdDb(insertedId) : null;
+};
+
+// Update brand
+const updateBrandDb = async (id, data) => {
+ const store = {
+ ...data,
+ updated_at: new Date(),
+ };
+
+ const whereData = {
+ brand_id: id,
+ };
+
+ const { query: queryText, values } = pool.buildDynamicUpdate("m_brands", store, whereData);
+ await pool.query(`${queryText} AND deleted_at IS NULL`, values);
+ return getBrandByIdDb(id);
+};
+
+// Soft delete brand
+const deleteBrandDb = async (id, deletedBy) => {
+ const queryText = `
+ UPDATE m_brands
+ SET deleted_at = CURRENT_TIMESTAMP, deleted_by = $1
+ WHERE brand_id = $2 AND deleted_at IS NULL
+ `;
+ await pool.query(queryText, [deletedBy, id]);
+ return true;
+};
+
+module.exports = {
+ getAllBrandsDb,
+ getBrandByIdDb,
+ getBrandByNameDb,
+ createBrandDb,
+ updateBrandDb,
+ deleteBrandDb,
+};
diff --git a/db/device.db.js b/db/device.db.js
new file mode 100644
index 0000000..c28d9a0
--- /dev/null
+++ b/db/device.db.js
@@ -0,0 +1,133 @@
+const pool = require("../config");
+
+// Get all devices
+const getAllDevicesDb = async (searchParams = {}) => {
+ let queryParams = [];
+
+ // Pagination
+ if (searchParams.limit) {
+ const page = Number(searchParams.page ?? 1) - 1;
+ queryParams = [Number(searchParams.limit ?? 10), page];
+ }
+
+ // Search
+ const { whereOrConditions, whereParamOr } = pool.buildStringOrIlike(
+ [
+ "a.device_name",
+ "a.device_code",
+ "a.device_location",
+ "a.ip_address",
+ "b.brand_name",
+ ],
+ searchParams.criteria,
+ queryParams
+ );
+
+ queryParams = whereParamOr ? whereParamOr : queryParams;
+
+ // Filter
+ const { whereConditions, whereParamAnd } = pool.buildFilterQuery(
+ [
+ { column: "a.device_code", param: searchParams.code, type: "string" },
+ { column: "a.device_location", param: searchParams.location, type: "string" },
+ { column: "b.brand_name", param: searchParams.brand, type: "string" },
+ { column: "a.is_active", param: searchParams.status, type: "string" },
+ ],
+ queryParams
+ );
+
+ queryParams = whereParamAnd ? whereParamAnd : queryParams;
+
+ const queryText = `
+ SELECT
+ COUNT(*) OVER() AS total_data,
+ a.*,
+ b.brand_name,
+ COALESCE(a.device_code, '') + ' - ' + COALESCE(a.device_name, '') AS device_code_name
+ FROM m_device a
+ LEFT JOIN m_brands b ON a.brand_id = b.brand_id
+ WHERE a.deleted_at IS NULL
+ ${whereConditions.length > 0 ? `AND ${whereConditions.join(' AND ')}` : ''}
+ ${whereOrConditions ? whereOrConditions : ''}
+ ORDER BY a.device_id ASC
+ ${searchParams.limit ? `OFFSET $2 ROWS FETCH NEXT $1 ROWS ONLY` : ''};
+ `;
+
+ const result = await pool.query(queryText, queryParams);
+
+ const total =
+ result?.recordset.length > 0
+ ? parseInt(result.recordset[0].total_data, 10)
+ : 0;
+
+ return { data: result.recordset, total };
+};
+
+
+const getDeviceByIdDb = async (id) => {
+ const queryText = `
+ SELECT
+ a.*,
+ b.brand_name,
+ COALESCE(a.device_code, '') + ' - ' + COALESCE(a.device_name, '') AS device_code_name
+ FROM m_device a
+ LEFT JOIN m_brands b ON a.brand_id = b.brand_id
+ WHERE a.device_id = $1 AND a.deleted_at IS NULL
+ `;
+ const result = await pool.query(queryText, [id]);
+ return result.recordset;
+};
+
+const createDeviceDb = async (data) => {
+ const newCode = await pool.generateKode("DVC", "m_device", "device_code");
+
+ const store = {
+ ...data,
+ device_code: newCode,
+ };
+
+ const { query: queryText, values } = pool.buildDynamicInsert(
+ "m_device",
+ store
+ );
+ const result = await pool.query(queryText, values);
+ const insertedId = result.recordset[0]?.inserted_id;
+ return insertedId ? await getDeviceByIdDb(insertedId) : null;
+};
+
+const updateDeviceDb = async (id, data) => {
+ const store = {
+ ...data,
+ };
+
+ // Kondisi WHERE
+ const whereData = {
+ device_id: id,
+ };
+
+ const { query: queryText, values } = pool.buildDynamicUpdate(
+ "m_device",
+ store,
+ whereData
+ );
+ await pool.query(`${queryText} AND deleted_at IS NULL`, values);
+ return getDeviceByIdDb(id);
+};
+
+const deleteDeviceDb = async (id, deletedBy) => {
+ const queryText = `
+ UPDATE m_device
+ SET deleted_at = CURRENT_TIMESTAMP, deleted_by = $1
+ WHERE device_id = $2 AND deleted_at IS NULL
+ `;
+ await pool.query(queryText, [deletedBy, id]);
+ return true;
+};
+
+module.exports = {
+ getAllDevicesDb,
+ getDeviceByIdDb,
+ createDeviceDb,
+ updateDeviceDb,
+ deleteDeviceDb,
+};
diff --git a/db/roles.db.js b/db/roles.db.js
new file mode 100644
index 0000000..56c42c5
--- /dev/null
+++ b/db/roles.db.js
@@ -0,0 +1,99 @@
+const pool = require("../config");
+
+const getAllRolesDb = async (searchParams = {}) => {
+ let queryParams = [];
+
+ // Handle pagination
+ if (searchParams.limit) {
+ const page = Number(searchParams.page ?? 1) - 1;
+ queryParams = [Number(searchParams.limit ?? 10), page];
+ }
+
+ const { whereOrConditions, whereParamOr } = pool.buildStringOrIlike(
+ ["a.role_name", "a.role_level", "a.role_description"],
+ searchParams.criteria,
+ queryParams
+ );
+ if (whereParamOr) queryParams = whereParamOr;
+
+ const { whereConditions, whereParamAnd } = pool.buildFilterQuery(
+ [
+ { column: "a.role_name", param: searchParams.role_name, type: "string" },
+ { column: "a.role_level", param: searchParams.start_time, type: "string" },
+ { column: "a.role_description", param: searchParams.role_description, type: "string" },
+ ],
+ queryParams
+ );
+ if (whereParamAnd) queryParams = whereParamAnd;
+
+ const queryText = `
+ SELECT
+ COUNT(*) OVER() AS total_data,
+ a.*
+ FROM m_roles a
+ WHERE a.deleted_at IS NULL
+ ${whereConditions.length > 0 ? ` AND ${whereConditions.join(" AND ")}` : ""}
+ ${whereOrConditions ? ` ${whereOrConditions}` : ""}
+ ORDER BY a.role_id ASC
+ ${searchParams.limit ? `OFFSET $2 ROWS FETCH NEXT $1 ROWS ONLY` : ""}
+ `;
+
+ const result = await pool.query(queryText, queryParams);
+ const total =
+ result?.recordset?.length > 0
+ ? parseInt(result.recordset[0].total_data, 10)
+ : 0;
+
+ return { data: result.recordset, total };
+};
+
+const getRolesByIdDb = async (id) => {
+ const queryText = `
+ SELECT
+ a.*
+ FROM m_roles a
+ WHERE a.role_id = $1 AND a.deleted_at IS NULL
+ `;
+ const result = await pool.query(queryText, [id]);
+ return result.recordset?.[0] || null;
+};
+
+const insertRolesDb = async (store) => {
+ const { query: queryText, values } = pool.buildDynamicInsert("m_roles", store);
+ const result = await pool.query(queryText, values);
+ const insertedId = result.recordset?.[0]?.inserted_id;
+
+ return insertedId ? await getRolesByIdDb(insertedId) : null;
+};
+
+const updateRolesDb = async (id, data) => {
+ const store = { ...data };
+ const whereData = { role_id: id };
+
+ const { query: queryText, values } = pool.buildDynamicUpdate(
+ "m_roles",
+ store,
+ whereData
+ );
+
+ await pool.query(`${queryText} AND deleted_at IS NULL`, values);
+ return getRolesByIdDb(id);
+};
+
+const deleteRolesDb = async (id, deletedBy) => {
+ const queryText = `
+ UPDATE m_roles
+ SET deleted_at = CURRENT_TIMESTAMP, deleted_by = $1
+ WHERE role_id = $2 AND deleted_at IS NULL
+ `;
+ await pool.query(queryText, [deletedBy, id]);
+ return true;
+};
+
+module.exports = {
+ getAllRolesDb,
+ getRolesByIdDb,
+ insertRolesDb,
+ updateRolesDb,
+ deleteRolesDb,
+};
diff --git a/db/schedule.db.js b/db/schedule.db.js
new file mode 100644
index 0000000..d3a598c
--- /dev/null
+++ b/db/schedule.db.js
@@ -0,0 +1,126 @@
+const pool = require("../config");
+const { formattedDate } = require("../utils/date");
+
+// Get all schedules
+const getAllScheduleDb = async (searchParams = {}) => {
+ let queryParams = [];
+
+ if (searchParams.limit) {
+ const page = Number(searchParams.page ?? 1) - 1;
+ queryParams = [Number(searchParams.limit ?? 10), page];
+ }
+
+ const { whereOrConditions, whereParamOr } = pool.buildStringOrIlike(
+ ["a.schedule_date"],
+ searchParams.criteria,
+ queryParams
+ );
+ if (whereParamOr) queryParams = whereParamOr;
+
+ const { whereConditions, whereParamAnd } = pool.buildFilterQuery(
+ [{ column: "a.schedule_date", param: searchParams.name, type: "date" }],
+ queryParams
+ );
+ if (whereParamAnd) queryParams = whereParamAnd;
+
+ const queryText = `
+ SELECT
+ COUNT(*) OVER() AS total_data,
+ a.*,
+ b.shift_name,
+ b.start_time,
+ b.end_time
+ FROM schedule a
+ LEFT JOIN m_shift b ON a.shift_id = b.shift_id
+ WHERE a.deleted_at IS NULL
+ ${whereConditions.length > 0 ? ` AND ${whereConditions.join(" AND ")}` : ""}
+ ${whereOrConditions ? ` ${whereOrConditions}` : ""}
+ ORDER BY a.schedule_id ASC
+ ${searchParams.limit ? `OFFSET $2 ROWS FETCH NEXT $1 ROWS ONLY` : ""}
+ `;
+
+ const result = await pool.query(queryText, queryParams);
+ const total =
+ result?.recordset?.length > 0
+ ? parseInt(result.recordset[0].total_data, 10)
+ : 0;
+
+ return { data: result.recordset, total };
+};
+
+const getScheduleByIdDb = async (id) => {
+ const queryText = `
+ SELECT
+ a.*,
+ b.shift_name,
+ b.start_time,
+ b.end_time
+ FROM schedule a
+ LEFT JOIN m_shift b ON a.shift_id = b.shift_id
+ WHERE a.schedule_id = $1 AND a.deleted_at IS NULL
+ `;
+ const result = await pool.query(queryText, [id]);
+ return result.recordset?.[0] || null;
+};
+
+const insertScheduleDb = async (store) => {
+ const nextDays = Number(store.next_day ?? 0); // default 0 kalau tidak diisi
+ const insertedRecords = [];
+
+ for (let i = 0; i <= nextDays; i++) {
+ const nextDate = new Date(store.schedule_date);
+ nextDate.setDate(nextDate.getDate() + i);
+
+ const formatted = formattedDate(nextDate);
+
+ const newStore = {
+ ...store,
+ schedule_date: formatted,
+ };
+ delete newStore.next_day;
+
+ const { query: queryText, values } = pool.buildDynamicInsert("schedule", newStore);
+ const result = await pool.query(queryText, values);
+ const insertedId = result.recordset?.[0]?.inserted_id;
+
+ if (insertedId) {
+ const record = await getScheduleByIdDb(insertedId);
+ insertedRecords.push(record);
+ }
+ }
+
+ return insertedRecords;
+};
+
+const updateScheduleDb = async (id, data) => {
+ const store = { ...data };
+ const whereData = { schedule_id: id };
+
+ const { query: queryText, values } = pool.buildDynamicUpdate(
+ "schedule",
+ store,
+ whereData
+ );
+
+ await pool.query(`${queryText} AND deleted_at IS NULL`, values);
+ return getScheduleByIdDb(id);
+};
+
+// Soft delete schedule
+const deleteScheduleDb = async (id, deletedBy) => {
+ const queryText = `
+ UPDATE schedule
+ SET deleted_at = CURRENT_TIMESTAMP, deleted_by = $1
+ WHERE schedule_id = $2 AND deleted_at IS NULL
+ `;
+ await pool.query(queryText, [deletedBy, id]);
+ return true;
+};
+
+module.exports = {
+ getAllScheduleDb,
+ getScheduleByIdDb,
+ insertScheduleDb,
+ updateScheduleDb,
+ deleteScheduleDb,
+};
diff --git a/db/shift.db.js b/db/shift.db.js
new file mode 100644
index 0000000..4dc4192
--- /dev/null
+++ b/db/shift.db.js
@@ -0,0 +1,100 @@
+const pool = require("../config");
+
+const getAllShiftDb = async (searchParams = {}) => {
+ let queryParams = [];
+
+ // Handle pagination
+ if (searchParams.limit) {
+ const page = Number(searchParams.page ?? 1) - 1;
+ queryParams = [Number(searchParams.limit ?? 10), page];
+ }
+
+ const { whereOrConditions, whereParamOr } = pool.buildStringOrIlike(
+ ["a.shift_name", "a.start_time", "a.end_time"],
+ searchParams.criteria,
+ queryParams
+ );
+ if (whereParamOr) queryParams = whereParamOr;
+
+ const { whereConditions, whereParamAnd } = pool.buildFilterQuery(
+ [
+ { column: "a.shift_name", param: searchParams.name, type: "string" },
+ { column: "a.start_time", param: searchParams.start_time, type: "time" },
+ { column: "a.end_time", param: searchParams.end_time, type: "time" },
+ { column: "a.is_active", param: searchParams.status, type: "string" },
+ ],
+ queryParams
+ );
+ if (whereParamAnd) queryParams = whereParamAnd;
+
+ const queryText = `
+ SELECT
+ COUNT(*) OVER() AS total_data,
+ a.*
+ FROM m_shift a
+ WHERE a.deleted_at IS NULL
+ ${whereConditions.length > 0 ? ` AND ${whereConditions.join(" AND ")}` : ""}
+ ${whereOrConditions ? ` ${whereOrConditions}` : ""}
+ ORDER BY a.shift_id ASC
+ ${searchParams.limit ? `OFFSET $2 ROWS FETCH NEXT $1 ROWS ONLY` : ""}
+ `;
+
+ const result = await pool.query(queryText, queryParams);
+ const total =
+ result?.recordset?.length > 0
+ ? parseInt(result.recordset[0].total_data, 10)
+ : 0;
+
+ return { data: result.recordset, total };
+};
+
+const getShiftByIdDb = async (id) => {
+ const queryText = `
+ SELECT
+ a.*
+ FROM m_shift a
+ WHERE a.shift_id = $1 AND a.deleted_at IS NULL
+ `;
+ const result = await pool.query(queryText, [id]);
+ return result.recordset?.[0] || null;
+};
+
+const insertShiftDb = async (store) => {
+ const { query: queryText, values } = pool.buildDynamicInsert("m_shift", store);
+ const result = await pool.query(queryText, values);
+ const insertedId = result.recordset?.[0]?.inserted_id;
+
+ return insertedId ? await getShiftByIdDb(insertedId) : null;
+};
+
+const updateShiftDb = async (id, data) => {
+ const store = { ...data };
+ const whereData = { shift_id: id };
+
+ const { query: queryText, values } = pool.buildDynamicUpdate(
+ "m_shift",
+ store,
+ whereData
+ );
+
+ await pool.query(`${queryText} AND deleted_at IS NULL`, values);
+ return getShiftByIdDb(id);
+};
+
+const deleteShiftDb = async (id, deletedBy) => {
+ const queryText = `
+ UPDATE m_shift
+ SET deleted_at = CURRENT_TIMESTAMP, deleted_by = $1
+ WHERE shift_id = $2 AND deleted_at IS NULL
+ `;
+ await pool.query(queryText, [deletedBy, id]);
+ return true;
+};
+
+module.exports = {
+ getAllShiftDb,
+ getShiftByIdDb,
+ insertShiftDb,
+ updateShiftDb,
+ deleteShiftDb,
+};
diff --git a/db/status.db.js b/db/status.db.js
new file mode 100644
index 0000000..37cbd2a
--- /dev/null
+++ b/db/status.db.js
@@ -0,0 +1,117 @@
+const pool = require("../config");
+
+// Get all status
+const getAllStatusDb = async (searchParams = {}) => {
+ let queryParams = [];
+
+ // Pagination
+ if (searchParams.limit) {
+ const page = Number(searchParams.page ?? 1) - 1;
+ queryParams = [Number(searchParams.limit ?? 10), page];
+ }
+
+ // Search
+ const { whereOrConditions, whereParamOr } = pool.buildStringOrIlike(
+ ["a.status_name", "a.status_description"],
+ searchParams.criteria,
+ queryParams
+ );
+
+ queryParams = whereParamOr ? whereParamOr : queryParams;
+
+ // Filter
+ const { whereConditions, whereParamAnd } = pool.buildFilterQuery(
+ [
+ { column: "a.status_number", param: searchParams.number, type: "number" },
+ { column: "a.is_active", param: searchParams.is_active, type: "boolean" },
+ ],
+ queryParams
+ );
+
+ queryParams = whereParamAnd ? whereParamAnd : queryParams;
+
+ const queryText = `
+ SELECT
+ COUNT(*) OVER() AS total_data,
+ a.*
+ FROM m_status a
+ WHERE a.deleted_at IS NULL
+ ${whereConditions.length > 0 ? `AND ${whereConditions.join(' AND ')}` : ''}
+ ${whereOrConditions ? whereOrConditions : ''}
+ ORDER BY a.status_id ASC
+ ${searchParams.limit ? `OFFSET $2 ROWS FETCH NEXT $1 ROWS ONLY` : ''};
+ `;
+
+ const result = await pool.query(queryText, queryParams);
+
+ const total =
+ result?.recordset.length > 0
+ ? parseInt(result.recordset[0].total_data, 10)
+ : 0;
+
+ return { data: result.recordset, total };
+};
+
+const getStatusByIdDb = async (id) => {
+ const queryText = `
+ SELECT *
+ FROM m_status a
+ WHERE a.status_id = $1 AND a.deleted_at IS NULL
+ `;
+ const result = await pool.query(queryText, [id]);
+ return result.recordset;
+};
+
+// Check if status_number already exists
+const checkStatusNumberExistsDb = async (status_number) => {
+ const queryText = `
+ SELECT 1
+ FROM m_status
+ WHERE status_number = $1 AND deleted_at IS NULL
+ `;
+ const result = await pool.query(queryText, [status_number]);
+ return result.recordset.length > 0;
+};
+
+
+const createStatusDb = async (data) => {
+ const { query: queryText, values } = pool.buildDynamicInsert(
+ "m_status",
+ data
+ );
+ const result = await pool.query(queryText, values);
+ const insertedId = result.recordset[0]?.inserted_id;
+ return insertedId ? await getStatusByIdDb(insertedId) : null;
+};
+
+const updateStatusDb = async (id, data) => {
+ const store = { ...data };
+ const whereData = { status_id: id };
+
+ const { query: queryText, values } = pool.buildDynamicUpdate(
+ "m_status",
+ store,
+ whereData
+ );
+ await pool.query(`${queryText} AND deleted_at IS NULL`, values);
+ return getStatusByIdDb(id);
+};
+
+const deleteStatusDb = async (id, deletedBy) => {
+ const queryText = `
+ UPDATE m_status
+ SET deleted_at = CURRENT_TIMESTAMP, deleted_by = $1
+ WHERE status_id = $2 AND deleted_at IS NULL
+ `;
+ await pool.query(queryText, [deletedBy, id]);
+ return true;
+};
+
+module.exports = {
+ getAllStatusDb,
+ getStatusByIdDb,
+ createStatusDb,
+ updateStatusDb,
+ deleteStatusDb,
+ checkStatusNumberExistsDb,
+};
diff --git a/db/sub_section.db.js b/db/sub_section.db.js
new file mode 100644
index 0000000..e4358d7
--- /dev/null
+++ b/db/sub_section.db.js
@@ -0,0 +1,108 @@
+const pool = require("../config");
+
+// Get all sub sections
+const getAllSubSectionsDb = async (searchParams = {}) => {
+ let queryParams = [];
+
+ if (searchParams.limit) {
+ const page = Number(searchParams.page ?? 1) - 1;
+ queryParams = [Number(searchParams.limit ?? 10), page];
+ }
+
+ // OR condition (pencarian bebas)
+ const { whereOrConditions, whereParamOr } = pool.buildStringOrIlike(
+ ["a.sub_section_code", "a.sub_section_name"],
+ searchParams.criteria,
+ queryParams
+ );
+
+ queryParams = whereParamOr ?? queryParams;
+
+ // AND condition (filter spesifik)
+ const { whereConditions, whereParamAnd } = pool.buildFilterQuery(
+ [
+ { column: "a.sub_section_code", param: searchParams.code, type: "string" },
+ { column: "a.sub_section_name", param: searchParams.name, type: "string" },
+ ],
+ queryParams
+ );
+
+ queryParams = whereParamAnd ?? queryParams;
+
+ // Query utama
+ const queryText = `
+ SELECT COUNT(*) OVER() AS total_data, a.*
+ FROM m_plant_sub_section a
+ WHERE a.deleted_at IS NULL
+ ${whereConditions.length > 0 ? ` AND ${whereConditions.join(" AND ")}` : ""}
+ ${whereOrConditions ? whereOrConditions : ""}
+ ORDER BY a.sub_section_id ASC
+ ${searchParams.limit ? `OFFSET $2 ROWS FETCH NEXT $1 ROWS ONLY` : ""};
+ `;
+
+ const result = await pool.query(queryText, queryParams);
+ const total =
+ result?.recordset.length > 0
+ ? parseInt(result.recordset[0].total_data, 10)
+ : 0;
+
+ return { data: result.recordset, total };
+};
+
+// Get sub section by ID
+const getSubSectionByIdDb = async (id) => {
+ const queryText = `
+ SELECT a.*
+ FROM m_plant_sub_section a
+ WHERE a.sub_section_id = $1 AND a.deleted_at IS NULL
+ `;
+ const result = await pool.query(queryText, [id]);
+ return result.recordset;
+};
+
+// Create new sub section
+const createSubSectionDb = async (data) => {
+ // Generate kode otomatis
+ const newCode = await pool.generateKode("SUB", "m_plant_sub_section", "sub_section_code");
+
+ const store = {
+ ...data,
+ sub_section_code: newCode
+ };
+
+ const { query: queryText, values } = pool.buildDynamicInsert("m_plant_sub_section", store);
+ const result = await pool.query(queryText, values);
+ const insertedId = result.recordset[0]?.inserted_id;
+
+ return insertedId ? await getSubSectionByIdDb(insertedId) : null;
+};
+
+// Update sub section
+const updateSubSectionDb = async (id, data) => {
+ const store = { ...data };
+ const whereData = { sub_section_id: id };
+
+ const { query: queryText, values } = pool.buildDynamicUpdate("m_plant_sub_section", store, whereData);
+ await pool.query(`${queryText} AND deleted_at IS NULL`, values);
+
+ return getSubSectionByIdDb(id);
+};
+
+// Soft delete sub section
+const deleteSubSectionDb = async (id, deletedBy) => {
+ const queryText = `
+ UPDATE m_plant_sub_section
+ SET deleted_at = CURRENT_TIMESTAMP, deleted_by = $1
+ WHERE sub_section_id = $2 AND deleted_at IS NULL
+ `;
+ await pool.query(queryText, [deletedBy, id]);
+ return true;
+};
+
+module.exports = {
+ getAllSubSectionsDb,
+ getSubSectionByIdDb,
+ createSubSectionDb,
+ updateSubSectionDb,
+ deleteSubSectionDb,
+};
diff --git a/db/tags.db.js b/db/tags.db.js
new file mode 100644
index 0000000..41b61c9
--- /dev/null
+++ b/db/tags.db.js
@@ -0,0 +1,140 @@
+const pool = require("../config");
+
+// Get all tags
+const getAllTagsDb = async (searchParams = {}) => {
+ let queryParams = [];
+
+ if (searchParams.limit) {
+ const page = Number(searchParams.page ?? 1) - 1;
+ queryParams = [Number(searchParams.limit ?? 10), page];
+ }
+
+ const { whereOrConditions, whereParamOr } = pool.buildStringOrIlike(
+ [
+ "a.tag_name",
+ "a.tag_code",
+ "a.tag_number",
+ "a.data_type",
+ "a.unit",
+ "b.device_name",
+ "c.sub_section_name",
+ ],
+ searchParams.criteria,
+ queryParams
+ );
+
+ if (whereParamOr) queryParams = whereParamOr;
+
+ const { whereConditions, whereParamAnd } = pool.buildFilterQuery(
+ [
+ { column: "a.tag_name", param: searchParams.name, type: "string" },
+ { column: "a.tag_code", param: searchParams.code, type: "string" },
+ { column: "a.data_type", param: searchParams.data, type: "string" },
+ { column: "a.unit", param: searchParams.unit, type: "string" },
+ { column: "b.device_name", param: searchParams.device, type: "string" },
+ {
+ column: "b.device_code",
+ param: searchParams.device,
+ type: "string",
+ },
+ {
+ column: "c.sub_section_name",
+ param: searchParams.subsection,
+ type: "string",
+ },
+ ],
+ queryParams
+ );
+
+ if (whereParamAnd) queryParams = whereParamAnd;
+
+ const queryText = `
+ SELECT
+ COUNT(*) OVER() AS total_data,
+ a.*,
+ b.device_name,
+ b.device_code,
+ c.sub_section_name
+ FROM m_tags a
+ LEFT JOIN m_device b ON a.device_id = b.device_id
+ LEFT JOIN m_plant_sub_section c ON a.sub_section_id = c.sub_section_id
+ WHERE a.deleted_at IS NULL
+ ${whereConditions.length > 0 ? ` AND ${whereConditions.join(" AND ")}` : ""}
+ ${whereOrConditions ? ` ${whereOrConditions}` : ""}
+ ORDER BY a.tag_id ASC
+ ${searchParams.limit ? `OFFSET $2 ROWS FETCH NEXT $1 ROWS ONLY` : ""}
+ `;
+
+ const result = await pool.query(queryText, queryParams);
+
+ const total =
+ result?.recordset?.length > 0
+ ? parseInt(result.recordset[0].total_data, 10)
+ : 0;
+
+ return { data: result.recordset, total };
+};
+
+const getTagsByIdDb = async (id) => {
+ const queryText = `
+ SELECT
+ a.*,
+ b.device_name,
+ b.device_code,
+ c.sub_section_name
+ FROM m_tags a
+ LEFT JOIN m_device b ON a.device_id = b.device_id
+ LEFT JOIN m_plant_sub_section c ON a.sub_section_id = c.sub_section_id
+ WHERE a.tag_id = $1 AND a.deleted_at IS NULL
+ `;
+ const result = await pool.query(queryText, [id]);
+ return result.recordset;
+};
+
+const createTagsDb = async (data) => {
+ const newCode = await pool.generateKode("TAG", "m_tags", "tag_code");
+
+ const store = {
+ ...data,
+ tag_code: newCode,
+ };
+
+ const { query: queryText, values } = pool.buildDynamicInsert("m_tags", store);
+ const result = await pool.query(queryText, values);
+ const insertedId = result.recordset[0]?.inserted_id;
+
+ return insertedId ? await getTagsByIdDb(insertedId) : null;
+};
+
+const updateTagsDb = async (id, data) => {
+ const store = { ...data };
+ const whereData = { tag_id: id };
+
+ const { query: queryText, values } = pool.buildDynamicUpdate(
+ "m_tags",
+ store,
+ whereData
+ );
+
+ await pool.query(`${queryText} AND deleted_at IS NULL`, values);
+ return getTagsByIdDb(id);
+};
+
+// Soft delete tag
+const deleteTagsDb = async (id, deletedBy) => {
+ const queryText = `
+ UPDATE m_tags
+ SET deleted_at = CURRENT_TIMESTAMP, deleted_by = $1
+ WHERE tag_id = $2 AND deleted_at IS NULL
+ `;
+ await pool.query(queryText, [deletedBy, id]);
+ return true;
+};
+
+module.exports = {
+ getAllTagsDb,
+ getTagsByIdDb,
+ createTagsDb,
+ updateTagsDb,
+ deleteTagsDb,
+};
diff --git a/db/unit.db.js b/db/unit.db.js
new file mode 100644
index 0000000..ad96fb4
--- /dev/null
+++ b/db/unit.db.js
@@ -0,0 +1,119 @@
+const pool = require("../config");
+
+// Get all units
+const getAllUnitsDb = async (searchParams = {}) => {
+ let queryParams = [];
+
+ // Pagination
+ if (searchParams.limit) {
+ const page = Number(searchParams.page ?? 1) - 1;
+ queryParams = [Number(searchParams.limit ?? 10), page];
+ }
+
+ // Search
+ const { whereOrConditions, whereParamOr } = pool.buildStringOrIlike(
+ ["a.unit_code", "a.unit_name"],
+ searchParams.criteria,
+ queryParams
+ );
+
+ queryParams = whereParamOr ? whereParamOr : queryParams;
+
+ // Filter
+ const { whereConditions, whereParamAnd } = pool.buildFilterQuery(
+ [
+ { column: "a.unit_code", param: searchParams.code, type: "string" },
+ { column: "a.unit_name", param: searchParams.name, type: "string" },
+ { column: "a.tag_id", param: searchParams.tag, type: "number" },
+ { column: "a.is_active", param: searchParams.status, type: "string" },
+ ],
+ queryParams
+ );
+
+ queryParams = whereParamAnd ? whereParamAnd : queryParams;
+
+ const queryText = `
+ SELECT
+ COUNT(*) OVER() AS total_data,
+ a.*,
+ COALESCE(a.unit_code, '') + ' - ' + COALESCE(a.unit_name, '') AS unit_code_name
+ FROM m_unit a
+ WHERE a.deleted_at IS NULL
+ ${whereConditions.length > 0 ? `AND ${whereConditions.join(" AND ")}` : ""}
+ ${whereOrConditions ? whereOrConditions : ""}
+ ORDER BY a.unit_id ASC
+ ${searchParams.limit ? `OFFSET $2 ROWS FETCH NEXT $1 ROWS ONLY` : ""};
+ `;
+
+ const result = await pool.query(queryText, queryParams);
+
+ const total =
+ result?.recordset.length > 0
+ ? parseInt(result.recordset[0].total_data, 10)
+ : 0;
+
+ return { data: result.recordset, total };
+};
+
+// Get unit by ID
+const getUnitByIdDb = async (id) => {
+ const queryText = `
+ SELECT
+ a.*,
+ COALESCE(a.unit_code, '') + ' - ' + COALESCE(a.unit_name, '') AS unit_code_name
+ FROM m_unit a
+ WHERE a.unit_id = $1 AND a.deleted_at IS NULL
+ `;
+ const result = await pool.query(queryText, [id]);
+ return result.recordset;
+};
+
+// Create unit
+const createUnitDb = async (data) => {
+ const newCode = await pool.generateKode("UNT", "m_unit", "unit_code");
+
+ const store = {
+ ...data,
+ unit_code: newCode,
+ };
+
+ const { query: queryText, values } = pool.buildDynamicInsert("m_unit", store);
+ const result = await pool.query(queryText, values);
+ const insertedId = result.recordset[0]?.inserted_id;
+ return insertedId ? await getUnitByIdDb(insertedId) : null;
+};
+
+// Update unit
+const updateUnitDb = async (id, data) => {
+ const store = { ...data };
+ const whereData = { unit_id: id };
+
+ const { query: queryText, values } = pool.buildDynamicUpdate(
+ "m_unit",
+ store,
+ whereData
+ );
+
+ await pool.query(`${queryText} AND deleted_at IS NULL`, values);
+ return getUnitByIdDb(id);
+};
+
+// Soft delete unit
+const deleteUnitDb = async (id, deletedBy) => {
+ const queryText = `
+ UPDATE m_unit
+ SET deleted_at = CURRENT_TIMESTAMP, deleted_by = $1
+ WHERE unit_id = $2 AND deleted_at IS NULL
+ `;
+ await pool.query(queryText, [deletedBy, id]);
+ return true;
+};
+
+// Export
+module.exports = {
+ getAllUnitsDb,
+ getUnitByIdDb,
+ createUnitDb,
+ updateUnitDb,
+ deleteUnitDb,
+};
\ No newline at end of file
diff --git a/db/user.db.js b/db/user.db.js
index 46471bc..c8772b6 100644
--- a/db/user.db.js
+++ b/db/user.db.js
@@ -1,142 +1,204 @@
const pool = require("../config");
-const getAllUsersDb = async (param) => {
- // limit & offset masuk fixed param
- let fixedParams = [param.fixed.limit, param.fixed.offset, param.fixed.tenantID];
+// Get all users
+const getAllUsersDb = async (searchParams = {}) => {
+ let queryParams = [];
- const { whereOrConditions, whereParam } = pool.buildStringOrIlike(
- param.filterCriteria.column,
- param.filterCriteria.criteria,
- fixedParams
+ // Pagination
+ if (searchParams.limit) {
+ const page = Number(searchParams.page ?? 1) - 1;
+ queryParams = [Number(searchParams.limit ?? 10), page];
+ }
+
+ // Search
+ const { whereOrConditions, whereParamOr } = pool.buildStringOrIlike(
+ [
+ "u.user_fullname",
+ "u.user_name",
+ "u.user_email",
+ "r.role_name"
+ ],
+ searchParams.criteria,
+ queryParams
);
- const { whereConditions, queryParams } = pool.buildFilterQuery(param.filterQuery, whereParam);npm
- const query = `
- SELECT mut.*, mr.role_name, COUNT(*) OVER() AS total
- FROM m_users mut
- LEFT JOIN system.role_tenant mr ON mr.role_id = mut.role_id
- WHERE mut.deleted_at IS NULL AND mut.is_sa != 1 AND mut.tenant_id = $3
- ${whereConditions.length > 0 ? ` AND ${whereConditions.join(" AND ")}` : ""}
- ${whereOrConditions ? whereOrConditions : ""}
- ORDER BY mut.user_id
- OFFSET $2 ROWS FETCH NEXT $1 ROWS ONLY
+ queryParams = whereParamOr ? whereParamOr : queryParams;
+
+ // Filter
+ const { whereConditions, whereParamAnd } = pool.buildFilterQuery(
+ [
+ { column: "u.user_fullname", param: searchParams.fullname, type: "string" },
+ { column: "u.user_name", param: searchParams.username, type: "string" },
+ { column: "u.user_email", param: searchParams.email, type: "string" },
+ { column: "r.role_name", param: searchParams.role, type: "string" },
+ ],
+ queryParams
+ );
+
+ queryParams = whereParamAnd ? whereParamAnd : queryParams;
+
+ const queryText = `
+ SELECT
+ COUNT(*) OVER() AS total_data,
+ u.user_id, u.user_fullname, u.user_name, u.user_email, u.user_phone,
+ u.is_active, u.is_sa, u.is_approve, u.approved_by,
+ approver.user_fullname AS approved_by_name,
+ u.approved_at, u.created_at, u.updated_at, u.deleted_at,
+ u.updated_by, u.deleted_by,
+ r.role_id, r.role_name, r.role_description, r.role_level
+ FROM m_users u
+ LEFT JOIN m_roles r ON u.role_id = r.role_id
+ LEFT JOIN m_users approver ON u.approved_by = approver.user_id
+ WHERE u.deleted_at IS NULL
+ ${whereConditions.length > 0 ? ` AND ${whereConditions.join(' AND ')}` : ''}
+ ${whereOrConditions ? whereOrConditions : ''}
+ ORDER BY u.user_id ASC
+ ${searchParams.limit ? `OFFSET $2 ROWS FETCH NEXT $1 ROWS ONLY` : ''};
`;
- const result = await pool.query(query, queryParams);
- const rows = result.recordset;
+ const result = await pool.query(queryText, queryParams);
- const total = rows.length > 0 ? parseInt(rows[0].total, 10) : 0;
- return { data: rows, total };
-};
-
-const createUserDb = async (param) => {
- const insertData = {
- tenant_id: param.tenantID,
- user_fullname: param.userFullname,
- user_name: param.userName,
- user_email: param.userEmail ?? null,
- user_password: param.userPassword,
- role_id: param.roleId ?? null,
- is_active: param.isActive ? 1 : 0,
- created_by: param.userID,
- updated_by: param.userID,
- };
-
- const { query, values } = pool.buildDynamicInsert("m_users", insertData);
-
- const result = await pool.query(query, values);
- return result.recordset[0];
+ const total =
+ result?.recordset.length > 0
+ ? parseInt(result.recordset[0].total_data, 10)
+ : 0;
+
+ return { data: result.recordset, total };
};
+// Get user by ID
const getUserByIdDb = async (id) => {
- const query = `
- SELECT mut.*
- FROM m_users mut
- WHERE mut.user_id = $1
+ const queryText = `
+ SELECT
+ u.user_id, u.user_fullname, u.user_name, u.user_email, u.user_phone,
+ u.is_active, u.is_sa, u.is_approve, u.approved_by,
+ approver.user_fullname AS approved_by_name,
+ u.approved_at, u.created_at, u.updated_at, u.deleted_at,
+ u.updated_by, u.deleted_by,
+ r.role_id, r.role_name, r.role_description, r.role_level
+ FROM m_users u
+ LEFT JOIN m_roles r ON u.role_id = r.role_id
+ LEFT JOIN m_users approver ON u.approved_by = approver.user_id
+ WHERE u.user_id = $1 AND u.deleted_at IS NULL
`;
- const result = await pool.query(query, [id]);
+ const result = await pool.query(queryText, [id]);
return result.recordset[0];
};
+// Get user by email
+const getUserByUserEmailDb = async (email) => {
+ const queryText = `
+ SELECT
+ u.user_id, u.user_fullname, u.user_name, u.user_email, u.user_phone,
+ u.user_password, u.is_active, u.is_sa, u.is_approve, u.role_id,
+ r.role_name, r.role_description, r.role_level
+ FROM m_users u
+ LEFT JOIN m_roles r ON u.role_id = r.role_id
+ WHERE u.user_email = $1 AND u.deleted_at IS NULL
+ `;
+ const result = await pool.query(queryText, [email]);
+ return result.recordset[0];
+};
+
+// Get user by username
const getUserByUsernameDb = async (username) => {
- const query = `
- SELECT mut.*
- FROM m_users mut
- WHERE LOWER(mut.username) = LOWER($1)
+ const queryText = `
+ SELECT
+ u.user_id, u.user_fullname, u.user_name, u.user_email, u.user_phone,
+ u.user_password, u.is_active, u.is_sa, u.is_approve, u.role_id,
+ r.role_name, r.role_description, r.role_level
+ FROM m_users u
+ LEFT JOIN m_roles r ON u.role_id = r.role_id
+ WHERE u.user_name = $1 AND u.deleted_at IS NULL
`;
- const result = await pool.query(query, [username]);
+ const result = await pool.query(queryText, [username]);
return result.recordset[0];
};
-const getUserByUserEmailDb = async (userEmail) => {
- const query = `
- SELECT mut.*
- FROM m_users mut
- WHERE LOWER(mut.user_email) = LOWER($1)
- `;
- const result = await pool.query(query, [userEmail]);
- return result.recordset[0];
+// Create user
+const createUserDb = async (data) => {
+ const { query: queryText, values } = pool.buildDynamicInsert("m_users", data);
+ const result = await pool.query(queryText, values);
+ const insertedId = result.recordset[0]?.inserted_id;
+ return insertedId ? await getUserByIdDb(insertedId) : null;
};
-const updateUserDb = async (param) => {
- const updateData = {
- tenant_id: param.tenantID,
- user_fullname: param.userFullname,
- user_name: param.userName,
- user_email: param.userEmail ?? null,
- user_password: param.userPassword,
- role_id: param.roleId ?? null,
- is_active: param.isActive ? 1 : 0,
- updated_by: param.userID,
- };
-
- const whereData = { user_id: param.id };
-
- const { query, values } = pool.buildDynamicUpdate("m_users", updateData, whereData);
-
- const result = await pool.query(query, values);
- return result.recordset[0];
+// Update user
+const updateUserDb = async (userId, data) => {
+ const { query: queryText, values } = pool.buildDynamicUpdate("m_users", data, {
+ user_id: userId,
+ });
+ await pool.query(`${queryText} AND deleted_at IS NULL`, values);
+ return getUserByIdDb(userId);
};
-const deleteUserDb = async (id, userID) => {
- const query = `
- UPDATE m_users
- SET deleted_at = GETDATE(), deleted_by = $1
- WHERE user_id = $2;
-
- SELECT * FROM m_users WHERE user_id = $2
+// Approve user
+const approveUserDb = async (userId, approverId) => {
+ const queryText = `
+ UPDATE m_users
+ SET
+ is_approve = 2,
+ approved_by = $1,
+ approved_at = CURRENT_TIMESTAMP,
+ updated_by = $1,
+ updated_at = CURRENT_TIMESTAMP
+ WHERE user_id = $2 AND deleted_at IS NULL
`;
- const result = await pool.query(query, [userID, id]);
- return result.recordset[0];
+ await pool.query(queryText, [approverId, userId]);
+ return true;
};
-const changeUserPasswordDb = async (hashedPassword, userEmail, tenantId) => {
- const query = `
- UPDATE m_users
- SET user_password = $1
- WHERE user_email = $2 AND tenant_id = $3
+// Reject user
+const rejectUserDb = async (userId, approverId) => {
+ const queryText = `
+ UPDATE m_users
+ SET
+ is_approve = 0,
+ approved_by = $1,
+ approved_at = CURRENT_TIMESTAMP,
+ updated_by = $1,
+ updated_at = CURRENT_TIMESTAMP
+ WHERE user_id = $2 AND deleted_at IS NULL
`;
- return pool.query(query, [hashedPassword, userEmail, tenantId]);
+ await pool.query(queryText, [approverId, userId]);
+ return true;
+}
+
+// Change user password
+const changeUserPasswordDb = async (userId, newPassword) => {
+ const queryText = `
+ UPDATE m_users
+ SET user_password = $1, updated_at = CURRENT_TIMESTAMP
+ WHERE user_id = $2 AND deleted_at IS NULL
+ `;
+ await pool.query(queryText, [newPassword, userId]);
+ return true;
};
-const getAllRoleDb = async (tenantId) => {
- const query = `
- SELECT *
- FROM system.role_tenant
- WHERE deleted_at IS NULL AND tenant_id = $1
+// Soft delete user
+const deleteUserDb = async (userId, deletedBy) => {
+ const queryText = `
+ UPDATE m_users
+ SET
+ deleted_at = CURRENT_TIMESTAMP,
+ deleted_by = $1,
+ is_active = 0
+ WHERE user_id = $2
+ AND deleted_at IS NULL
`;
- const result = await pool.query(query, [tenantId]);
- return result.recordset;
+ await pool.query(queryText, [deletedBy, userId]);
+ return true;
};
module.exports = {
getAllUsersDb,
getUserByIdDb,
getUserByUserEmailDb,
- updateUserDb,
- createUserDb,
- deleteUserDb,
getUserByUsernameDb,
+ createUserDb,
+ updateUserDb,
+ approveUserDb,
+ rejectUserDb,
changeUserPasswordDb,
- getAllRoleDb,
+ deleteUserDb,
};
diff --git a/helpers/utils.js b/helpers/utils.js
index 41c60ed..7ed4a3e 100644
--- a/helpers/utils.js
+++ b/helpers/utils.js
@@ -1,43 +1,29 @@
-const setResponse = async (data = [], message = "success", statusCode = 200) => {
- const response = {
- data,
- total: data.length,
- message,
- statusCode
- }
+const setResponse = (data = null, message = "success", statusCode = 200) => {
+ const total = Array.isArray(data) ? data.length : null;
- return response
+ return {
+ message,
+ statusCode,
+ rows: total,
+ data,
+ };
};
-const setResponsePaging = async (data = [], total, limit, page, message = "success", statusCode = 200) => {
+const setResponsePaging = async (queryParam, data = [], message = "success", statusCode = 200) => {
- const totalPages = Math.ceil(total / limit);
+ const totalPages = Math.ceil(data?.total / Number(queryParam.limit ?? 0));
const response = {
message,
statusCode,
- data,
- total: data.length,
+ rows: data?.data?.length,
paging: {
- total,
- limit,
- page,
- page_total: totalPages
- }
- }
-
- return response
-};
-
-const setPaging = async (total, limit, page) => {
-
- const totalPages = Math.ceil(total / limit);
-
- const response = {
- total,
- limit,
- page,
- page_total: totalPages
+ current_limit: Number(queryParam.limit ?? 0),
+ current_page: Number(queryParam.page ?? 0),
+ total_limit: data?.total,
+ total_page: totalPages
+ },
+ data: data?.data ?? []
}
return response
@@ -86,4 +72,19 @@ function orderByClauseQuery(orderParams) {
return orderByClause
}
-module.exports = { setResponse, setResponsePaging, setPaging, convertId, formatToYYYYMMDD, orderByClauseQuery };
+const checkValidate = (validateSchema, req) => {
+ const { error, value } = validateSchema.validate(req.body || {}, { abortEarly: false });
+ if (error) {
+ const errors = error.details.reduce((acc, cur) => {
+ const field = Array.isArray(cur.path) ? cur.path.join('.') : String(cur.path);
+ if (!acc[field]) acc[field] = [];
+ acc[field].push(cur.message);
+ return acc;
+ }, {});
+ return { error: errors, value }
+ }
+
+ return { error, value }
+}
+
+module.exports = { setResponse, setResponsePaging, convertId, formatToYYYYMMDD, orderByClauseQuery, checkValidate };
diff --git a/helpers/validateUser.js b/helpers/validateUser.js
deleted file mode 100644
index c904bb4..0000000
--- a/helpers/validateUser.js
+++ /dev/null
@@ -1,9 +0,0 @@
-const validateUser = (email, password) => {
- const validEmail = typeof email === "string" && email.trim() !== "";
- const validPassword =
- typeof password === "string" && password.trim().length >= 6;
-
- return validEmail && validPassword;
-};
-
-module.exports = validateUser;
diff --git a/index.js b/index.js
index 552b3a5..0f5b6e1 100644
--- a/index.js
+++ b/index.js
@@ -5,6 +5,6 @@ const { logger } = require("./utils/logger");
const server = http.createServer(app);
-const PORT = process.env.PORT || 9524;
+const PORT = process.env.PORT || 9533;
server.listen(PORT, () => logger.info(`Magic happening on port: ${PORT}`));
diff --git a/middleware/verifyAccess.js b/middleware/verifyAccess.js
new file mode 100644
index 0000000..e62fee0
--- /dev/null
+++ b/middleware/verifyAccess.js
@@ -0,0 +1,38 @@
+const { ErrorHandler } = require("../helpers/error");
+const { getUserByIdDb } = require("../db/user.db");
+
+const verifyAccess = (minLevel = 1, allowUnapprovedReadOnly = false) => {
+ return async (req, res, next) => {
+ try {
+ const user = req.user;
+
+ if (!user) throw new ErrorHandler(401, "Unauthorized: User not found");
+
+ // Super Admin bypass semua
+ if (user.is_sa) return next();
+
+ const fullUser = await getUserByIdDb(user.user_id);
+ if (!fullUser) throw new ErrorHandler(403, "Forbidden: User not found");
+
+ if (!fullUser.is_approve) {
+ if (req.method !== "GET") {
+ throw new ErrorHandler(403, "Account not approved — read-only access");
+ }
+
+ if (allowUnapprovedReadOnly) return next();
+
+ throw new ErrorHandler(403, "Account not approved");
+ }
+
+ if (!fullUser.role_level || fullUser.role_level < minLevel) {
+ throw new ErrorHandler(403, "Forbidden: Insufficient role level");
+ }
+
+ next();
+ } catch (err) {
+ next(err);
+ }
+ };
+};
+
+module.exports = verifyAccess;
diff --git a/middleware/verifyAdmin.js b/middleware/verifyAdmin.js
deleted file mode 100644
index 4a04109..0000000
--- a/middleware/verifyAdmin.js
+++ /dev/null
@@ -1,14 +0,0 @@
-const { ErrorHandler } = require("../helpers/error");
-
-module.exports = (req, res, next) => {
- const { roles } = req.user;
- if (roles && roles.includes("admin")) {
- req.user = {
- ...req.user,
- roles,
- };
- return next();
- } else {
- throw new ErrorHandler(401, "require admin role");
- }
-};
diff --git a/middleware/verifyToken.js b/middleware/verifyToken.js
index 58d4352..020e967 100644
--- a/middleware/verifyToken.js
+++ b/middleware/verifyToken.js
@@ -1,47 +1,48 @@
-const jwt = require("jsonwebtoken");
-const { ErrorHandler } = require("../helpers/error");
+const JWTService = require('../utils/jwt');
+const { ErrorHandler } = require('../helpers/error');
-const verifyToken = (req, res, next) => {
- const authHeader = req.header("Authorization");
- // console.log("authHeader", authHeader)
-
- // Pastikan header Authorization ada dan berisi token
- if (!authHeader || !authHeader.startsWith("Bearer ")) {
- throw new ErrorHandler(401, "Token missing or invalid");
- }
-
- // Ambil token dari header Authorization
- const token = authHeader.split(" ")[1];
+function setUser(req, decoded) {
+ req.user = {
+ userId: decoded.user_id,
+ fullname: decoded.user_fullname,
+ username: decoded.user_name,
+ email: decoded.user_email,
+ roleId: decoded.role_id,
+ roleName: decoded.role_name,
+ is_sa: decoded.is_sa
+ };
+}
+function verifyAccessToken(req, res, next) {
try {
- // const decoded = jwt.decode(token, { complete: true });
- // console.log("decoded", decoded)
- // console.log("==============================")
- // console.log("token", token)
- // console.log("process.env.SECRET", process.env.SECRET)
- // // console.log("==============================> ", jwt.verify(token, process.env.SECRET))
- // jwt.verify(token, process.env.SECRET, (err, decoded) => {
- // if (err) {
- // console.error('Error verifying token: ==============================>', err.message);
- // } else {
- // console.log('Decoded payload: ==============================>', decoded);
- // }
- // });
+ let token = req.cookies?.accessToken;
+
+ if (!token) {
+ const authHeader = req.headers.authorization;
+ if (!authHeader || !authHeader.startsWith('Bearer')) {
+ throw new ErrorHandler(401, 'Access Token is required');
+ }
+ token = authHeader.split(' ')[1];
+ }
+
+ const decoded = JWTService.verifyToken(token);
+
+ req.user = decoded;
- const verified = jwt.verify(token, process.env.SECRET);
- req.tokenExtract = verified;
- // console.log(req.tokenExtract);
-
- req.userID = req.tokenExtract.user_id
- req.tenantID = req.tokenExtract.tenant_id
- req.roleID = req.tokenExtract.role_id
- req.body.userID = req.tokenExtract.user_id
- req.body.tenantID = req.tokenExtract.tenant_id
- req.query.tenantID = req.tokenExtract.tenant_id
next();
} catch (error) {
- throw new ErrorHandler(401, error.message || "Invalid Token");
- }
-};
+ if (error.name === 'TokenExpiredError') {
+ return next(new ErrorHandler(401, 'Access token expired'));
+ }
-module.exports = verifyToken;
+ if (error.name === 'JsonWebTokenError') {
+ return next(new ErrorHandler(401, 'Invalid access token'));
+ }
+
+ return next(new ErrorHandler(500, 'Internal authentication error'));
+ }
+}
+
+module.exports = {
+ verifyAccessToken
+};
\ No newline at end of file
diff --git a/routes/auth.js b/routes/auth.js
deleted file mode 100644
index 1c738f0..0000000
--- a/routes/auth.js
+++ /dev/null
@@ -1,8 +0,0 @@
-const router = require("express").Router();
-const {
- loginUser,
-} = require("../controllers/auth.controller");
-
-router.post("/login", loginUser);
-
-module.exports = router;
diff --git a/routes/auth.route.js b/routes/auth.route.js
new file mode 100644
index 0000000..166d68d
--- /dev/null
+++ b/routes/auth.route.js
@@ -0,0 +1,11 @@
+const express = require('express');
+const AuthController = require("../controllers/auth.controller");
+
+const router = express.Router();
+
+router.post('/login', AuthController.login);
+router.post('/register', AuthController.register);
+router.get('/generate-captcha', AuthController.generateCaptcha);
+router.post('/refresh-token', AuthController.refreshToken);
+
+module.exports = router;
\ No newline at end of file
diff --git a/routes/device.route.js b/routes/device.route.js
new file mode 100644
index 0000000..5c74061
--- /dev/null
+++ b/routes/device.route.js
@@ -0,0 +1,17 @@
+const express = require('express');
+const DeviceController = require('../controllers/device.controller');
+const verifyToken = require("../middleware/verifyToken")
+const verifyAccess = require("../middleware/verifyAccess")
+
+const router = express.Router();
+
+router.route("/")
+ .get(verifyToken.verifyAccessToken, DeviceController.getAll)
+ .post(verifyToken.verifyAccessToken, verifyAccess(), DeviceController.create);
+
+router.route("/:id")
+ .get(verifyToken.verifyAccessToken, DeviceController.getById)
+ .put(verifyToken.verifyAccessToken, verifyAccess(), DeviceController.update)
+ .delete(verifyToken.verifyAccessToken, verifyAccess(), DeviceController.delete);
+
+module.exports = router;
\ No newline at end of file
diff --git a/routes/index.js b/routes/index.js
index 92d5909..cc2c2c5 100644
--- a/routes/index.js
+++ b/routes/index.js
@@ -1,8 +1,25 @@
const router = require("express").Router();
-const auth = require("./auth");
-const users = require("./users");
+const auth = require("./auth.route");
+const users = require("./users.route");
+const device = require('./device.route');
+const roles = require('./roles.route');
+const tags = require("./tags.route");
+const subSection = require("./sub_section.route");
+const shift = require("./shift.route");
+const schedule = require("./schedule.route");
+const status = require("./status.route");
+const unit = require("./unit.route")
router.use("/auth", auth);
-router.use("/users", users);
+router.use("/user", users);
+router.use("/device", device);
+router.use("/roles", roles);
+router.use("/tags", tags);
+router.use("/plant-sub-section", subSection);
+router.use("/shift", shift);
+router.use("/schedule", schedule);
+router.use("/status", status);
+router.use("/unit", unit);
+
module.exports = router;
diff --git a/routes/roles.route.js b/routes/roles.route.js
new file mode 100644
index 0000000..0ce2d4a
--- /dev/null
+++ b/routes/roles.route.js
@@ -0,0 +1,17 @@
+const express = require('express');
+const RolesController = require('../controllers/roles.controller');
+const verifyToken = require("../middleware/verifyToken")
+const verifyAccess = require("../middleware/verifyAccess")
+
+const router = express.Router();
+
+router.route("/")
+ .get(verifyToken.verifyAccessToken, RolesController.getAll)
+ .post(verifyToken.verifyAccessToken, verifyAccess(), RolesController.create);
+
+router.route("/:id")
+ .get(verifyToken.verifyAccessToken, RolesController.getById)
+ .put(verifyToken.verifyAccessToken, verifyAccess(), RolesController.update)
+ .delete(verifyToken.verifyAccessToken, verifyAccess(), RolesController.delete);
+
+module.exports = router;
\ No newline at end of file
diff --git a/routes/schedule.route.js b/routes/schedule.route.js
new file mode 100644
index 0000000..4c5e3e2
--- /dev/null
+++ b/routes/schedule.route.js
@@ -0,0 +1,17 @@
+const express = require('express');
+const ScheduleController = require('../controllers/schedule.controller');
+const verifyToken = require("../middleware/verifyToken")
+const verifyAccess = require("../middleware/verifyAccess")
+
+const router = express.Router();
+
+router.route("/")
+ .get(verifyToken.verifyAccessToken, ScheduleController.getAll)
+ .post(verifyToken.verifyAccessToken, verifyAccess(), ScheduleController.create);
+
+router.route("/:id")
+ .get(verifyToken.verifyAccessToken, ScheduleController.getById)
+ .put(verifyToken.verifyAccessToken, verifyAccess(), ScheduleController.update)
+ .delete(verifyToken.verifyAccessToken, verifyAccess(), ScheduleController.delete);
+
+module.exports = router;
\ No newline at end of file
diff --git a/routes/shift.route.js b/routes/shift.route.js
new file mode 100644
index 0000000..322bb81
--- /dev/null
+++ b/routes/shift.route.js
@@ -0,0 +1,17 @@
+const express = require('express');
+const ShiftController = require('../controllers/shift.controller');
+const verifyToken = require("../middleware/verifyToken")
+const verifyAccess = require("../middleware/verifyAccess")
+
+const router = express.Router();
+
+router.route("/")
+ .get(verifyToken.verifyAccessToken, ShiftController.getAll)
+ .post(verifyToken.verifyAccessToken, verifyAccess(), ShiftController.create);
+
+router.route("/:id")
+ .get(verifyToken.verifyAccessToken, ShiftController.getById)
+ .put(verifyToken.verifyAccessToken, verifyAccess(), ShiftController.update)
+ .delete(verifyToken.verifyAccessToken, verifyAccess(), ShiftController.delete);
+
+module.exports = router;
\ No newline at end of file
diff --git a/routes/status.route.js b/routes/status.route.js
new file mode 100644
index 0000000..eb4995f
--- /dev/null
+++ b/routes/status.route.js
@@ -0,0 +1,17 @@
+const express = require('express');
+const StatusController = require('../controllers/status.controller');
+const verifyToken = require("../middleware/verifyToken");
+const verifyAccess = require("../middleware/verifyAccess");
+
+const router = express.Router();
+
+router.route("/")
+ .get(verifyToken.verifyAccessToken, StatusController.getAll)
+ .post(verifyToken.verifyAccessToken, verifyAccess(), StatusController.create);
+
+router.route("/:id")
+ .get(verifyToken.verifyAccessToken, StatusController.getById)
+ .put(verifyToken.verifyAccessToken, verifyAccess(), StatusController.update)
+ .delete(verifyToken.verifyAccessToken, verifyAccess(), StatusController.delete);
+
+module.exports = router;
diff --git a/routes/sub_section.route.js b/routes/sub_section.route.js
new file mode 100644
index 0000000..58adde9
--- /dev/null
+++ b/routes/sub_section.route.js
@@ -0,0 +1,17 @@
+const express = require('express');
+const PlantSubSectionController = require('../controllers/sub_section.controller');
+const verifyToken = require('../middleware/verifyToken');
+const verifyAccess = require('../middleware/verifyAccess');
+
+const router = express.Router();
+
+router.route('/')
+ .get(verifyToken.verifyAccessToken, PlantSubSectionController.getAll)
+ .post(verifyToken.verifyAccessToken, verifyAccess(), PlantSubSectionController.create);
+
+router.route('/:id')
+ .get(verifyToken.verifyAccessToken, PlantSubSectionController.getById)
+ .put(verifyToken.verifyAccessToken, verifyAccess(), PlantSubSectionController.update)
+ .delete(verifyToken.verifyAccessToken, verifyAccess(), PlantSubSectionController.delete);
+
+module.exports = router;
\ No newline at end of file
diff --git a/routes/tags.route.js b/routes/tags.route.js
new file mode 100644
index 0000000..4160284
--- /dev/null
+++ b/routes/tags.route.js
@@ -0,0 +1,17 @@
+const express = require('express');
+const TagsController = require('../controllers/tags.controller');
+const verifyToken = require("../middleware/verifyToken")
+const verifyAccess = require("../middleware/verifyAccess")
+
+const router = express.Router();
+
+router.route("/")
+ .get(verifyToken.verifyAccessToken, TagsController.getAll)
+ .post(verifyToken.verifyAccessToken, verifyAccess(), TagsController.create);
+
+router.route("/:id")
+ .get(verifyToken.verifyAccessToken, TagsController.getById)
+ .put(verifyToken.verifyAccessToken, verifyAccess(), TagsController.update)
+ .delete(verifyToken.verifyAccessToken, verifyAccess(), TagsController.delete);
+
+module.exports = router;
\ No newline at end of file
diff --git a/routes/unit.route.js b/routes/unit.route.js
new file mode 100644
index 0000000..24a3c77
--- /dev/null
+++ b/routes/unit.route.js
@@ -0,0 +1,17 @@
+const express = require('express');
+const UnitController = require('../controllers/unit.controller');
+const verifyToken = require('../middleware/verifyToken');
+const verifyAccess = require('../middleware/verifyAccess');
+
+const router = express.Router();
+
+router.route('/')
+ .get(verifyToken.verifyAccessToken, UnitController.getAll)
+ .post(verifyToken.verifyAccessToken, verifyAccess(), UnitController.create);
+
+router.route('/:id')
+ .get(verifyToken.verifyAccessToken, UnitController.getById)
+ .put(verifyToken.verifyAccessToken, verifyAccess(), UnitController.update)
+ .delete(verifyToken.verifyAccessToken, verifyAccess(), UnitController.delete);
+
+module.exports = router;
diff --git a/routes/users.js b/routes/users.js
deleted file mode 100644
index 3cbf210..0000000
--- a/routes/users.js
+++ /dev/null
@@ -1,33 +0,0 @@
-const {
- getAllUsers,
- createUser,
- deleteUser,
- getUserById,
- updateUser,
- getUserProfile,
- getAllRoles,
- getAllStatusUsers
-} = require("../controllers/users.controller");
-const router = require("express").Router();
-const verifyAdmin = require("../middleware/verifyAdmin");
-const verifyToken = require("../middleware/verifyToken");
-
-router.get("/roles", getAllRoles);
-
-router.route("/profile")
- .get(getUserProfile);
-
-router.route("/")
- .get(verifyToken, getAllUsers)
- .post(verifyToken, createUser);
-
-router
- .route("/status")
- .get(verifyToken, getAllStatusUsers);
-
-router.route("/:id")
- .get(verifyToken, getUserById)
- .put(verifyToken, updateUser)
- .delete(verifyToken, deleteUser);
-
-module.exports = router;
diff --git a/routes/users.route.js b/routes/users.route.js
new file mode 100644
index 0000000..2b7abbb
--- /dev/null
+++ b/routes/users.route.js
@@ -0,0 +1,26 @@
+const express = require('express');
+const UserController = require('../controllers/users.controller');
+const verifyToken = require('../middleware/verifyToken');
+const verifyAccess = require('../middleware/verifyAccess');
+
+const router = express.Router();
+
+router.route('/')
+ .get(verifyToken.verifyAccessToken, UserController.getAll)
+ .post(verifyToken.verifyAccessToken, verifyAccess(), UserController.create);
+
+router.route('/:id')
+ .get(verifyToken.verifyAccessToken, UserController.getById)
+ .put(verifyToken.verifyAccessToken, verifyAccess(), UserController.update)
+ .delete(verifyToken.verifyAccessToken, verifyAccess(), UserController.delete);
+
+router.route('/change-password/:id')
+ .put(verifyToken.verifyAccessToken, verifyAccess(), UserController.changePassword);
+
+router.route('/:id/approve')
+ .put(verifyToken.verifyAccessToken, verifyAccess(), UserController.approve);
+
+router.route('/:id/reject')
+ .put(verifyToken.verifyAccessToken, verifyAccess(), UserController.reject);
+
+module.exports = router;
diff --git a/services/auth.service.js b/services/auth.service.js
index 583d300..44b5948 100644
--- a/services/auth.service.js
+++ b/services/auth.service.js
@@ -1,77 +1,130 @@
-const bcrypt = require("bcrypt");
-const jwt = require("jsonwebtoken");
-const validateUser = require("../helpers/validateUser");
-const { ErrorHandler } = require("../helpers/error");
const {
+ getUserByUserEmailDb,
+ createUserDb,
getUserByUsernameDb
-} = require("../db/user.db");
-const { logger } = require("../utils/logger");
+} = require('../db/user.db');
+const { hashPassword, comparePassword } = require('../helpers/hashPassword');
+const { ErrorHandler } = require('../helpers/error');
+const JWTService = require('../utils/jwt');
class AuthService {
-
- async login(username, password, tenantId) {
+ // Register
+ static async register(data) {
try {
- // if (!validateUser(username, password)) {
- // throw new ErrorHandler(403, "Invalid login");
- // }
+ const existingEmail = await getUserByUserEmailDb(data.user_email);
+ const existingUsername = await getUserByUsernameDb(data.user_name);
- const user = await getUserByUsernameDb(username, tenantId);
- console.log(user);
-
- if (!user) {
- throw new ErrorHandler(403, "Username not found.");
+ if (existingUsername) {
+ throw new ErrorHandler(400, 'Username is already taken');
+ }
+ if (existingEmail) {
+ throw new ErrorHandler(400, 'Email is already taken');
}
- const isCorrectPassword = password === user.password
- if (!isCorrectPassword) {
- throw new ErrorHandler(403, "Username or password incorrect.");
- }
+ const hashedPassword = await hashPassword(data.user_password);
- const dataToken = {
- tenant_id: tenantId,
- user_id: user.user_id,
- username,
- fullname: user.full_name,
- role_id: user.role_id
- }
+ const userId = await createUserDb({
+ user_fullname: data.user_fullname,
+ user_name: data.user_name,
+ user_email: data.user_email,
+ user_phone: data.user_phone,
+ user_password: hashedPassword,
+ is_sa: 0,
+ is_active: 1,
+ is_approve: 1,
+ });
- const token = await this.signToken(dataToken);
- const refreshToken = await this.signRefreshToken(dataToken);
-
- return {
- token,
- refreshToken,
- role_id: dataToken.role_id,
- tenant_id: tenantId,
- user: {
- user_id: dataToken.user_id,
- fullname: dataToken.fullname,
- username: dataToken.username,
- },
+ const newUser = {
+ user_id: userId,
+ user_fullname: data.user_fullname,
+ user_name: data.user_name,
+ user_email: data.user_email,
+ user_phone: data.user_phone
};
+
+ return { user: newUser };
} catch (error) {
throw new ErrorHandler(error.statusCode, error.message);
}
}
- async signToken(data) {
+ // Login
+ static async login(data) {
try {
- // console.log("signToken process.env.SECRET", process.env.SECRET)
- return jwt.sign(data, process.env.SECRET, { expiresIn: "23h" });
+ const { identifier, password, captcha, captchaText } = data;
+
+ if (!captcha || captcha.toLowerCase() !== captchaText.toLowerCase()) {
+ throw new ErrorHandler(400, 'Invalid captcha');
+ }
+
+ let user;
+ if (identifier.includes('@')) {
+ user = await getUserByUserEmailDb(identifier);
+ } else {
+ user = await getUserByUsernameDb(identifier);
+ }
+
+ if (!user) throw new ErrorHandler(401, 'Invalid credentials');
+
+ const passwordMatch = await comparePassword(password, user.user_password);
+ if (!passwordMatch) throw new ErrorHandler(401, 'Invalid credentials');
+
+ if (!user.is_active) throw new ErrorHandler(403, 'User is inactive');
+ if (!user.is_approve) throw new ErrorHandler(403, 'Your account has not been approved by admin yet.');
+
+ const payload = {
+ user_id: user.user_id,
+ user_fullname: user.user_fullname,
+ user_name: user.user_name,
+ user_email: user.user_email,
+ user_phone: user.user_phone,
+ role_id: user.role_id,
+ role_name: user.role_name,
+ is_sa: user.is_sa
+ };
+
+ const tokens = JWTService.generateTokenPair(payload);
+ return { user: payload, tokens };
} catch (error) {
- logger.error(error);
- throw new ErrorHandler(500, "An error occurred");
+ throw new ErrorHandler(error.statusCode, error.message);
}
}
- async signRefreshToken(data) {
+ // Refresh Token
+ static async refreshToken(refreshToken) {
try {
- return jwt.sign(data, process.env.REFRESH_SECRET, { expiresIn: "23h" });
+ let decoded;
+ try {
+ decoded = JWTService.verifyRefreshToken(refreshToken);
+ } catch (err) {
+ if (err.message.includes('expired')) {
+ throw new ErrorHandler(401, 'Refresh token expired');
+ }
+ throw new ErrorHandler(401, 'Invalid refresh token');
+ }
+
+ const payload = {
+ user_id: decoded.user_id,
+ user_fullname: decoded.user_fullname,
+ user_name: decoded.user_name,
+ user_email: decoded.user_email,
+ user_phone: decoded.user_phone,
+ role_id: decoded.role_id,
+ role_name: decoded.role_name,
+ is_sa: decoded.is_sa
+ };
+
+ const accessToken = JWTService.generateAccessToken(payload);
+
+ return {
+ accessToken,
+ tokenType: 'Bearer',
+ expiresIn: 900
+ };
} catch (error) {
- logger.error(error);
- throw new ErrorHandler(500, error.message);
+ throw new ErrorHandler(error.statusCode, error.message);
}
}
}
-module.exports = new AuthService();
+module.exports = AuthService;
diff --git a/services/device.service.js b/services/device.service.js
new file mode 100644
index 0000000..978b336
--- /dev/null
+++ b/services/device.service.js
@@ -0,0 +1,88 @@
+const {
+ getAllDevicesDb,
+ getDeviceByIdDb,
+ createDeviceDb,
+ updateDeviceDb,
+ deleteDeviceDb
+} = require('../db/device.db');
+const { ErrorHandler } = require('../helpers/error');
+
+class DeviceService {
+ // Get all devices
+ static async getAllDevices(param) {
+ try {
+ const results = await getAllDevicesDb(param);
+
+ results.data.map(element => {
+ });
+
+ return results
+ } catch (error) {
+ throw new ErrorHandler(error.statusCode, error.message);
+ }
+ }
+
+ // Get device by ID
+ static async getDeviceById(id) {
+ try {
+ const result = await getDeviceByIdDb(id);
+
+ if (result.length < 1) throw new ErrorHandler(404, 'Device not found');
+
+ return result;
+ } catch (error) {
+ throw new ErrorHandler(error.statusCode, error.message);
+ }
+ }
+
+ // Create device
+ static async createDevice(data) {
+ try {
+ if (!data || typeof data !== 'object') data = {};
+
+ const result = await createDeviceDb(data);
+
+ return result;
+ } catch (error) {
+ throw new ErrorHandler(error.statusCode, error.message);
+ }
+ }
+
+ // Update device
+ static async updateDevice(id, data) {
+ try {
+ if (!data || typeof data !== 'object') data = {};
+
+ const dataExist = await getDeviceByIdDb(id);
+
+ if (dataExist.length < 1) {
+ throw new ErrorHandler(404, 'Device not found');
+ }
+
+ const result = await updateDeviceDb(id, data);
+
+ return result;
+ } catch (error) {
+ throw new ErrorHandler(error.statusCode, error.message);
+ }
+ }
+
+ // Soft delete device
+ static async deleteDevice(id, userId) {
+ try {
+ const dataExist = await getDeviceByIdDb(id);
+
+ if (dataExist.length < 1) {
+ throw new ErrorHandler(404, 'Device not found');
+ }
+
+ const result = await deleteDeviceDb(id, userId);
+
+ return result;
+ } catch (error) {
+ throw new ErrorHandler(error.statusCode, error.message);
+ }
+ }
+}
+
+module.exports = DeviceService;
diff --git a/services/roles.service.js b/services/roles.service.js
new file mode 100644
index 0000000..863ae80
--- /dev/null
+++ b/services/roles.service.js
@@ -0,0 +1,88 @@
+const {
+ getAllRolesDb,
+ getRolesByIdDb,
+ insertRolesDb,
+ updateRolesDb,
+ deleteRolesDb
+} = require('../db/roles.db');
+const { ErrorHandler } = require('../helpers/error');
+
+class RolesService {
+ // Get all Roles
+ static async getAllRoles(param) {
+ try {
+ const results = await getAllRolesDb(param);
+
+ results.data.map(element => {
+ });
+
+ return results
+ } catch (error) {
+ throw new ErrorHandler(error.statusCode, error.message);
+ }
+ }
+
+ // Get Roles by ID
+ static async getRolesById(id) {
+ try {
+ const result = await getRolesByIdDb(id);
+
+ if (result.length < 1) throw new ErrorHandler(404, 'Roles not found');
+
+ return result;
+ } catch (error) {
+ throw new ErrorHandler(error.statusCode, error.message);
+ }
+ }
+
+ // Create Roles
+ static async createRoles(data) {
+ try {
+ if (!data || typeof data !== 'object') data = {};
+
+ const result = await insertRolesDb(data);
+
+ return result;
+ } catch (error) {
+ throw new ErrorHandler(error.statusCode, error.message);
+ }
+ }
+
+ // Update Roles
+ static async updateRoles(id, data) {
+ try {
+ if (!data || typeof data !== 'object') data = {};
+
+ const dataExist = await getRolesByIdDb(id);
+
+ if (dataExist.length < 1) {
+ throw new ErrorHandler(404, 'Roles not found');
+ }
+
+ const result = await updateRolesDb(id, data);
+
+ return result;
+ } catch (error) {
+ throw new ErrorHandler(error.statusCode, error.message);
+ }
+ }
+
+ // Soft delete Roles
+ static async deleteRoles(id, userId) {
+ try {
+ const dataExist = await getRolesByIdDb(id);
+
+ if (dataExist.length < 1) {
+ throw new ErrorHandler(404, 'Roles not found');
+ }
+
+ const result = await deleteRolesDb(id, userId);
+
+ return result;
+ } catch (error) {
+ throw new ErrorHandler(error.statusCode, error.message);
+ }
+ }
+}
+
+module.exports = RolesService;
diff --git a/services/schedule.service.js b/services/schedule.service.js
new file mode 100644
index 0000000..ea33e1d
--- /dev/null
+++ b/services/schedule.service.js
@@ -0,0 +1,88 @@
+const {
+ getAllScheduleDb,
+ getScheduleByIdDb,
+ insertScheduleDb,
+ updateScheduleDb,
+ deleteScheduleDb
+} = require('../db/schedule.db');
+const { ErrorHandler } = require('../helpers/error');
+
+class ScheduleService {
+ // Get all Schedule
+ static async getAllSchedule(param) {
+ try {
+ const results = await getAllScheduleDb(param);
+
+ results.data.map(element => {
+ });
+
+ return results
+ } catch (error) {
+ throw new ErrorHandler(error.statusCode, error.message);
+ }
+ }
+
+ // Get Schedule by ID
+ static async getScheduleById(id) {
+ try {
+ const result = await getScheduleByIdDb(id);
+
+ if (result.length < 1) throw new ErrorHandler(404, 'Schedule not found');
+
+ return result;
+ } catch (error) {
+ throw new ErrorHandler(error.statusCode, error.message);
+ }
+ }
+
+ // Create Schedule
+ static async insertScheduleDb(data) {
+ try {
+ if (!data || typeof data !== 'object') data = {};
+
+ const result = await insertScheduleDb(data);
+
+ return result;
+ } catch (error) {
+ throw new ErrorHandler(error.statusCode, error.message);
+ }
+ }
+
+ // Update Schedule
+ static async updateSchedule(id, data) {
+ try {
+ if (!data || typeof data !== 'object') data = {};
+
+ const dataExist = await getScheduleByIdDb(id);
+
+ if (dataExist.length < 1) {
+ throw new ErrorHandler(404, 'Schedule not found');
+ }
+
+ const result = await updateScheduleDb(id, data);
+
+ return result;
+ } catch (error) {
+ throw new ErrorHandler(error.statusCode, error.message);
+ }
+ }
+
+ // Soft delete Schedule
+ static async deleteSchedule(id, userId) {
+ try {
+ const dataExist = await getScheduleByIdDb(id);
+
+ if (dataExist.length < 1) {
+ throw new ErrorHandler(404, 'Schedule not found');
+ }
+
+ const result = await deleteScheduleDb(id, userId);
+
+ return result;
+ } catch (error) {
+ throw new ErrorHandler(error.statusCode, error.message);
+ }
+ }
+}
+
+module.exports = ScheduleService;
diff --git a/services/shift.service.js b/services/shift.service.js
new file mode 100644
index 0000000..8706771
--- /dev/null
+++ b/services/shift.service.js
@@ -0,0 +1,88 @@
+const {
+ getAllShiftDb,
+ getShiftByIdDb,
+ insertShiftDb,
+ updateShiftDb,
+ deleteShiftDb
+} = require('../db/shift.db');
+const { ErrorHandler } = require('../helpers/error');
+
+class ShiftService {
+ // Get all Shift
+ static async getAllShift(param) {
+ try {
+ const results = await getAllShiftDb(param);
+
+ results.data.map(element => {
+ });
+
+ return results
+ } catch (error) {
+ throw new ErrorHandler(error.statusCode, error.message);
+ }
+ }
+
+ // Get Shift by ID
+ static async getShiftById(id) {
+ try {
+ const result = await getShiftByIdDb(id);
+
+ if (result.length < 1) throw new ErrorHandler(404, 'Shift not found');
+
+ return result;
+ } catch (error) {
+ throw new ErrorHandler(error.statusCode, error.message);
+ }
+ }
+
+ // Create Shift
+ static async createShift(data) {
+ try {
+ if (!data || typeof data !== 'object') data = {};
+
+ const result = await insertShiftDb(data);
+
+ return result;
+ } catch (error) {
+ throw new ErrorHandler(error.statusCode, error.message);
+ }
+ }
+
+ // Update Shift
+ static async updateShift(id, data) {
+ try {
+ if (!data || typeof data !== 'object') data = {};
+
+ const dataExist = await getShiftByIdDb(id);
+
+ if (dataExist.length < 1) {
+ throw new ErrorHandler(404, 'Shift not found');
+ }
+
+ const result = await updateShiftDb(id, data);
+
+ return result;
+ } catch (error) {
+ throw new ErrorHandler(error.statusCode, error.message);
+ }
+ }
+
+ // Soft delete Shift
+ static async deleteShift(id, userId) {
+ try {
+ const dataExist = await getShiftByIdDb(id);
+
+ if (dataExist.length < 1) {
+ throw new ErrorHandler(404, 'Shift not found');
+ }
+
+ const result = await deleteShiftDb(id, userId);
+
+ return result;
+ } catch (error) {
+ throw new ErrorHandler(error.statusCode, error.message);
+ }
+ }
+}
+
+module.exports = ShiftService;
diff --git a/services/status.service.js b/services/status.service.js
new file mode 100644
index 0000000..cfc3113
--- /dev/null
+++ b/services/status.service.js
@@ -0,0 +1,92 @@
+const {
+ getAllStatusDb,
+ getStatusByIdDb,
+ createStatusDb,
+ updateStatusDb,
+ deleteStatusDb,
+ checkStatusNumberExistsDb
+} = require('../db/status.db');
+const { ErrorHandler } = require('../helpers/error');
+
+class StatusService {
+ // Get all status
+ static async getAllStatus(param) {
+ try {
+ const results = await getAllStatusDb(param);
+
+ results.data.map(element => {
+ });
+
+ return results;
+ } catch (error) {
+ throw new ErrorHandler(error.statusCode, error.message);
+ }
+ }
+
+ // Get status by ID
+ static async getStatusById(id) {
+ try {
+ const result = await getStatusByIdDb(id);
+
+ if (result.length < 1) throw new ErrorHandler(404, 'Status not found');
+
+ return result;
+ } catch (error) {
+ throw new ErrorHandler(error.statusCode, error.message);
+ }
+ }
+
+ static async createStatus(data) {
+ try {
+ if (!data || typeof data !== 'object') data = {};
+
+ if (data.status_number) {
+ const exists = await checkStatusNumberExistsDb(data.status_number);
+ if (exists) throw new ErrorHandler(400, 'Status number already exists');
+ }
+
+ const result = await createStatusDb(data);
+ return result;
+ } catch (error) {
+ throw new ErrorHandler(error.statusCode || 500, error.message);
+ }
+ }
+
+ // Update status
+ static async updateStatus(id, data) {
+ try {
+ if (!data || typeof data !== 'object') data = {};
+
+ const dataExist = await getStatusByIdDb(id);
+
+ if (dataExist.length < 1) {
+ throw new ErrorHandler(404, 'Status not found');
+ }
+
+ const result = await updateStatusDb(id, data);
+
+ return result;
+ } catch (error) {
+ throw new ErrorHandler(error.statusCode, error.message);
+ }
+ }
+
+ // Soft delete status
+ static async deleteStatus(id, userId) {
+ try {
+ const dataExist = await getStatusByIdDb(id);
+
+ if (dataExist.length < 1) {
+ throw new ErrorHandler(404, 'Status not found');
+ }
+
+ const result = await deleteStatusDb(id, userId);
+
+ return result;
+ } catch (error) {
+ throw new ErrorHandler(error.statusCode, error.message);
+ }
+ }
+}
+
+module.exports = StatusService;
diff --git a/services/sub_section.service.js b/services/sub_section.service.js
new file mode 100644
index 0000000..8860bfd
--- /dev/null
+++ b/services/sub_section.service.js
@@ -0,0 +1,87 @@
+const {
+ getAllSubSectionsDb,
+ getSubSectionByIdDb,
+ createSubSectionDb,
+ updateSubSectionDb,
+ deleteSubSectionDb
+} = require('../db/sub_section.db');
+const { ErrorHandler } = require('../helpers/error');
+
+class SubSectionService {
+ // Get all sub sections
+ static async getAll(param) {
+ try {
+ const results = await getAllSubSectionsDb(param);
+
+ results.data.map(el => {});
+
+ return results;
+ } catch (error) {
+ throw new ErrorHandler(error.statusCode, error.message);
+ }
+ }
+
+ // Get sub section by ID
+ static async getById(id) {
+ try {
+ const result = await getSubSectionByIdDb(id);
+
+ if (result.length < 1) throw new ErrorHandler(404, 'Sub section not found');
+
+ return result;
+ } catch (error) {
+ throw new ErrorHandler(error.statusCode, error.message);
+ }
+ }
+
+ // Create sub section
+ static async create(data) {
+ try {
+ if (!data || typeof data !== 'object') data = {};
+
+ const result = await createSubSectionDb(data);
+
+ return result;
+ } catch (error) {
+ throw new ErrorHandler(error.statusCode, error.message);
+ }
+ }
+
+ // Update sub section
+ static async update(id, data) {
+ try {
+ if (!data || typeof data !== 'object') data = {};
+
+ const dataExist = await getSubSectionByIdDb(id);
+
+ if (dataExist.length < 1) {
+ throw new ErrorHandler(404, 'Sub section not found');
+ }
+
+ const result = await updateSubSectionDb(id, data);
+
+ return result;
+ } catch (error) {
+ throw new ErrorHandler(error.statusCode, error.message);
+ }
+ }
+
+ // Soft delete sub section
+ static async delete(id, userId) {
+ try {
+ const dataExist = await getSubSectionByIdDb(id);
+
+ if (dataExist.length < 1) {
+ throw new ErrorHandler(404, 'Sub section not found');
+ }
+
+ const result = await deleteSubSectionDb(id, userId);
+
+ return result;
+ } catch (error) {
+ throw new ErrorHandler(error.statusCode, error.message);
+ }
+ }
+}
+
+module.exports = SubSectionService;
diff --git a/services/tags.service.js b/services/tags.service.js
new file mode 100644
index 0000000..9f180ae
--- /dev/null
+++ b/services/tags.service.js
@@ -0,0 +1,89 @@
+const {
+ getAllTagsDb,
+ getTagsByIdDb,
+ createTagsDb,
+ updateTagsDb,
+ deleteTagsDb
+ } = require('../db/tags.db');
+ const { ErrorHandler } = require('../helpers/error');
+
+ class TagsService {
+ // Get all devices
+ static async getAllTags(param) {
+ try {
+ const results = await getAllTagsDb(param);
+
+ results.data.map(element => {
+ });
+
+ return results
+ } catch (error) {
+ throw new ErrorHandler(error.statusCode, error.message);
+ }
+ }
+
+ // Get device by ID
+ static async getTagByID(id) {
+ try {
+ const result = await getTagsByIdDb(id);
+
+ if (result.length < 1) throw new ErrorHandler(404, 'Tags not found');
+
+ return result;
+ } catch (error) {
+ throw new ErrorHandler(error.statusCode, error.message);
+ }
+ }
+
+ // Create device
+ static async createTags(data) {
+ try {
+ if (!data || typeof data !== 'object') data = {};
+
+ const result = await createTagsDb(data);
+
+ return result;
+ } catch (error) {
+ throw new ErrorHandler(error.statusCode, error.message);
+ }
+ }
+
+ // Update device
+ static async updateTags(id, data) {
+ try {
+ if (!data || typeof data !== 'object') data = {};
+
+ const dataExist = await getTagsByIdDb(id);
+
+ if (dataExist.length < 1) {
+ throw new ErrorHandler(404, 'Tags not found');
+ }
+
+ const result = await updateTagsDb(id, data);
+
+ return result;
+ } catch (error) {
+ throw new ErrorHandler(error.statusCode, error.message);
+ }
+ }
+
+ // Soft delete device
+ static async deleteTags(id, userId) {
+ try {
+ const dataExist = await getTagsByIdDb(id);
+
+ if (dataExist.length < 1) {
+ throw new ErrorHandler(404, 'Tags not found');
+ }
+
+ const result = await deleteTagsDb(id, userId);
+
+ return result;
+ } catch (error) {
+ throw new ErrorHandler(error.statusCode, error.message);
+ }
+ }
+ }
+
+ module.exports = TagsService;
+
\ No newline at end of file
diff --git a/services/unit.service.js b/services/unit.service.js
new file mode 100644
index 0000000..c6d6624
--- /dev/null
+++ b/services/unit.service.js
@@ -0,0 +1,88 @@
+const {
+ getAllUnitsDb,
+ getUnitByIdDb,
+ createUnitDb,
+ updateUnitDb,
+ deleteUnitDb
+} = require('../db/unit.db');
+const { ErrorHandler } = require('../helpers/error');
+
+class UnitService {
+ // Get all units
+ static async getAllUnits(param) {
+ try {
+ const results = await getAllUnitsDb(param);
+
+ results.data.map(element => {
+ });
+
+ return results;
+ } catch (error) {
+ throw new ErrorHandler(error.statusCode, error.message);
+ }
+ }
+
+ // Get unit by ID
+ static async getUnitById(id) {
+ try {
+ const result = await getUnitByIdDb(id);
+
+ if (result.length < 1) throw new ErrorHandler(404, 'Unit not found');
+
+ return result;
+ } catch (error) {
+ throw new ErrorHandler(error.statusCode, error.message);
+ }
+ }
+
+ // Create unit
+ static async createUnit(data) {
+ try {
+ if (!data || typeof data !== 'object') data = {};
+
+ const result = await createUnitDb(data);
+
+ return result;
+ } catch (error) {
+ throw new ErrorHandler(error.statusCode, error.message);
+ }
+ }
+
+ // Update unit
+ static async updateUnit(id, data) {
+ try {
+ if (!data || typeof data !== 'object') data = {};
+
+ const dataExist = await getUnitByIdDb(id);
+
+ if (dataExist.length < 1) {
+ throw new ErrorHandler(404, 'Unit not found');
+ }
+
+ const result = await updateUnitDb(id, data);
+
+ return result;
+ } catch (error) {
+ throw new ErrorHandler(error.statusCode, error.message);
+ }
+ }
+
+ // Soft delete unit
+ static async deleteUnit(id, userId) {
+ try {
+ const dataExist = await getUnitByIdDb(id);
+
+ if (dataExist.length < 1) {
+ throw new ErrorHandler(404, 'Unit not found');
+ }
+
+ const result = await deleteUnitDb(id, userId);
+
+ return result;
+ } catch (error) {
+ throw new ErrorHandler(error.statusCode, error.message);
+ }
+ }
+}
+
+module.exports = UnitService;
\ No newline at end of file
diff --git a/services/user.service.js b/services/user.service.js
index 2e95fda..d2688b3 100644
--- a/services/user.service.js
+++ b/services/user.service.js
@@ -1,124 +1,188 @@
const {
- createUserDb,
- changeUserPasswordDb,
- getUserByIdDb,
- updateUserDb,
- deleteUserDb,
getAllUsersDb,
+ getUserByIdDb,
+ getUserByUserEmailDb,
getUserByUsernameDb,
- getAllRoleDb
-} = require("../db/user.db");
-const { ErrorHandler } = require("../helpers/error");
-const { convertId } = require("../helpers/utils");
-
-const statusName = [
- {
- status: true,
- status_name: "Aktif"
- }, {
- status: false,
- status_name: "NonAktif"
- }
-];
+ createUserDb,
+ updateUserDb,
+ approveUserDb,
+ rejectUserDb,
+ deleteUserDb,
+ changeUserPasswordDb
+} = require('../db/user.db');
+const { hashPassword } = require('../helpers/hashPassword');
+const { ErrorHandler } = require('../helpers/error');
class UserService {
- getAllStatusUsers = async () => {
- try {
- return statusName;
- } catch (error) {
- throw new ErrorHandler(error.statusCode, error.message);
- }
- };
-
- getAllUsers = async (param) => {
+ // Get all users
+ static async getAllUsers(param) {
try {
const results = await getAllUsersDb(param);
-
- results.data.map(element => {
- element.is_active = element.is_active == 1 ? true : false
- element.is_active_name = convertId(statusName, element.is_active, 'status', 'status_name')
- });
-
- return results
+ return results;
} catch (error) {
throw new ErrorHandler(error.statusCode, error.message);
}
- };
+ }
- createUser = async (param) => {
+ // Get user by ID
+ static async getUserById(id) {
try {
- const userByUsername = await getUserByUsernameDb(param.userName, param.tenantID);
+ const result = await getUserByIdDb(id);
- if (userByUsername) {
- throw new ErrorHandler(401, "username taken already");
+ if (!result) throw new ErrorHandler(404, 'User not found');
+
+ return result;
+ } catch (error) {
+ throw new ErrorHandler(error.statusCode, error.message);
+ }
+ }
+
+ // Create user
+ static async createUser(data) {
+ try {
+ if (!data || typeof data !== 'object') data = {};
+
+ const creatorId = data.userId || null;
+
+ // cek duplikasi username & email
+ const [existingUsername, existingEmail] = await Promise.all([
+ getUserByUsernameDb(data.user_name),
+ getUserByUserEmailDb(data.user_email)
+ ]);
+
+ if (existingUsername) throw new ErrorHandler(400, 'Username is already taken');
+ if (existingEmail) throw new ErrorHandler(400, 'Email is already taken');
+
+ // hash password
+ const hashedPassword = await hashPassword(data.user_password);
+
+ // siapkan data untuk insert
+ const userData = {
+ ...data,
+ user_password: hashedPassword,
+ is_approve: 2,
+ approved_by: creatorId,
+ created_by: creatorId,
+ updated_by: creatorId,
+ is_sa: 0,
+ is_active: 1
+ };
+
+ delete userData.userId;
+
+ const result = await createUserDb(userData);
+ return result;
+ } catch (error) {
+ throw new ErrorHandler(error.statusCode || 500, error.message);
+ }
+ }
+
+ // Update user
+ static async updateUser(id, data) {
+ try {
+ if (!data || typeof data !== 'object') data = {};
+
+ const existingEmail = await getUserByUserEmailDb(data.user_email);
+ const existingUsername = await getUserByUsernameDb(data.user_name);
+
+ if (existingUsername) {
+ throw new ErrorHandler(400, 'Username is already taken');
+ }
+ if (existingEmail) {
+ throw new ErrorHandler(400, 'Email is already taken')
}
- return await createUserDb(param);
+ const userExist = await getUserByIdDb(id);
+ if (!userExist) throw new ErrorHandler(404, 'User not found');
+
+ const result = await updateUserDb(id, data);
+ return result;
} catch (error) {
throw new ErrorHandler(error.statusCode, error.message);
}
- };
+ }
- getUserById = async (id) => {
+ // Approve user
+ static async approveUser(userId, approverId) {
try {
- const user = await getUserByIdDb(id);
- // user.password = undefined;
- user.is_active = user.is_active == 1 ? true : false
- return user;
- } catch (error) {
- throw new ErrorHandler(error.statusCode, error.message);
- }
- };
-
- changeUserPassword = async (password, email, tenantID) => {
- try {
- return await changeUserPasswordDb(password, email, tenantID);
- } catch (error) {
- throw new ErrorHandler(error.statusCode, error.message);
- }
- };
-
- updateUser = async (param) => {
- const { userName, id } = param;
- const errors = {};
- try {
-
- const user = await getUserByIdDb(id);
-
- const findUserByUsername = await getUserByUsernameDb(userName, param.tenantID);
-
- const usernameChanged = userName && user.user_name.toLowerCase() !== userName.toLowerCase();
-
- if (usernameChanged && typeof findUserByUsername === "object") {
- errors["username"] = "Username is already taken";
+ if (!userId) {
+ throw new ErrorHandler(400, 'User ID is required');
}
- if (Object.keys(errors).length > 0) {
- throw new ErrorHandler(403, errors);
+ const existingUser = await getUserByIdDb(userId);
+ if (!existingUser) {
+ throw new ErrorHandler(404, 'User not found');
}
- return await updateUserDb(param);
- } catch (error) {
- throw new ErrorHandler(error.statusCode, error.message);
- }
- };
+ if (existingUser.is_approve === 2) {
+ throw new ErrorHandler(400, 'User is already approved');
+ }
- deleteUser = async (id, userID) => {
- try {
- return await deleteUserDb(id, userID);
- } catch (error) {
- throw new ErrorHandler(error.statusCode, error.message);
- }
- };
+ if (existingUser.is_approve === 0) {
+ throw new ErrorHandler(400, 'User is already rejected');
+ }
- getAllRoles = async (tenantID) => {
+ const updatedUser = await approveUserDb(userId, approverId);
+ return updatedUser;
+ } catch (error) {
+ throw new ErrorHandler(error.statusCode || 500, error.message);
+ }
+ }
+
+ // Reject user
+ static async rejectUser(userId, approverId) {
try {
- return await getAllRoleDb(tenantID);
+ if (!userId) {
+ throw new ErrorHandler(400, 'User ID is required');
+ }
+
+ const existingUser = await getUserByIdDb(userId);
+ if (!existingUser) {
+ throw new ErrorHandler(404, 'User not found');
+ }
+
+ if (existingUser.is_approve === 2) {
+ throw new ErrorHandler(400, 'User is already approved');
+ }
+
+ if (existingUser.is_approve === 0) {
+ throw new ErrorHandler(400, 'User is already rejected');
+ }
+
+ const updatedUser = await rejectUserDb(userId, approverId);
+ return updatedUser;
+ } catch (error) {
+ throw new ErrorHandler(error.statusCode || 500, error.message);
+ }
+ }
+
+ // Soft delete user
+ static async deleteUser(id, userId) {
+ try {
+ const userExist = await getUserByIdDb(id);
+ if (!userExist) throw new ErrorHandler(404, 'User not found');
+
+ const result = await deleteUserDb(id, userId);
+ return result;
} catch (error) {
throw new ErrorHandler(error.statusCode, error.message);
}
- };
+ }
+
+ // Change password
+ static async changeUserPassword(id, newPassword) {
+ try {
+ const userExist = await getUserByIdDb(id);
+ if (!userExist) throw new ErrorHandler(404, 'User not found');
+
+ const hashedPassword = await hashPassword(newPassword);
+ const result = await changeUserPasswordDb(id, hashedPassword);
+ return result;
+ } catch (error) {
+ throw new ErrorHandler(error.statusCode, error.message);
+ }
+ }
}
-module.exports = new UserService();
+module.exports = UserService;
diff --git a/utils/captcha.js b/utils/captcha.js
new file mode 100644
index 0000000..d14d31c
--- /dev/null
+++ b/utils/captcha.js
@@ -0,0 +1,8 @@
+const svgCaptcha = require('svg-captcha');
+
+function createCaptcha() {
+ const captcha = svgCaptcha.create({ size: 5, noise: 7, color: true });
+ return { svg: captcha.data, text: captcha.text };
+}
+
+module.exports = { createCaptcha };
\ No newline at end of file
diff --git a/utils/date.js b/utils/date.js
new file mode 100644
index 0000000..400d9ce
--- /dev/null
+++ b/utils/date.js
@@ -0,0 +1,8 @@
+module.exports = {
+ formattedDate: (timestamp) => {
+ let date = new Date(timestamp);
+ let options = { day: "numeric", month: "long", year: "numeric" };
+ let formattedDate = date.toISOString("id-ID", options);
+ return formattedDate;
+ },
+ };
\ No newline at end of file
diff --git a/utils/jwt.js b/utils/jwt.js
new file mode 100644
index 0000000..4d5aaa4
--- /dev/null
+++ b/utils/jwt.js
@@ -0,0 +1,73 @@
+const jwt = require('jsonwebtoken');
+const crypto = require('crypto');
+
+const tokenSettings = {
+ access: {
+ expiresIn: '15m',
+ type: 'access',
+ secret: process.env.SECRET
+ },
+ refresh: {
+ expiresIn: '7d',
+ type: 'refresh',
+ secret: process.env.REFRESH_SECRET
+ }
+};
+
+function generateTokenId() {
+ return crypto.randomBytes(32).toString('hex');
+}
+
+function generateToken(payload, type) {
+ const settings = tokenSettings[type];
+ if (!settings) throw new Error(`Invalid token type: ${type}`);
+
+ const tokenPayload = { ...payload, type: settings.type };
+
+ return jwt.sign(tokenPayload, settings.secret, {
+ expiresIn: settings.expiresIn,
+ jwtid: generateTokenId()
+ });
+}
+
+function verifyTokenType(token, type) {
+ const settings = tokenSettings[type];
+ const decoded = jwt.verify(token, settings.secret);
+ if (decoded.type !== type) throw new Error('Invalid token type');
+ return decoded;
+}
+
+function generateAccessToken(payload) {
+ return generateToken(payload, 'access');
+}
+
+function generateRefreshToken(payload) {
+ return generateToken(payload, 'refresh');
+}
+
+function verifyToken(token) {
+ return verifyTokenType(token, 'access');
+}
+
+function verifyRefreshToken(token) {
+ return verifyTokenType(token, 'refresh');
+}
+
+function generateTokenPair(payload) {
+ const accessToken = generateAccessToken(payload);
+ const refreshToken = generateRefreshToken(payload);
+
+ return {
+ accessToken,
+ refreshToken,
+ tokenType: 'Bearer',
+ };
+}
+
+module.exports = {
+ generateAccessToken,
+ generateRefreshToken,
+ verifyToken,
+ verifyRefreshToken,
+ generateTokenPair,
+};
diff --git a/validate/auth.schema.js b/validate/auth.schema.js
new file mode 100644
index 0000000..bba7312
--- /dev/null
+++ b/validate/auth.schema.js
@@ -0,0 +1,40 @@
+const Joi = require("joi");
+
+// ========================
+// Auth Validation
+// ========================
+const registerSchema = Joi.object({
+ user_fullname: Joi.string().min(3).max(100).required(),
+ user_name: Joi.string().alphanum().min(3).max(50).required(),
+ user_email: Joi.string().email().required(),
+ user_phone: Joi.string()
+ .pattern(/^(?:\+62|0)8\d{7,10}$/)
+ .required()
+ .messages({
+ 'string.pattern.base':
+ 'Phone number must be a valid Indonesian number in format +628XXXXXXXXX'
+ }),
+ user_password: Joi.string()
+ .min(8)
+ .pattern(/[A-Z]/, 'uppercase letter')
+ .pattern(/[a-z]/, 'lowercase letter')
+ .pattern(/\d/, 'number')
+ .pattern(/[!@#$%^&*(),.?":{}|<>]/, 'special character')
+ .required()
+ .messages({
+ 'string.min': 'Password must be at least 8 characters long',
+ 'string.pattern.name': 'Password must contain at least one {#name}'
+ })
+});
+
+const loginSchema = Joi.object({
+ identifier: Joi.string().required(),
+ password: Joi.string().required(),
+ captcha: Joi.string().required(),
+ captchaText: Joi.string().required()
+});
+
+module.exports = {
+ registerSchema,
+ loginSchema,
+};
\ No newline at end of file
diff --git a/validate/device.schema.js b/validate/device.schema.js
new file mode 100644
index 0000000..4d1f65e
--- /dev/null
+++ b/validate/device.schema.js
@@ -0,0 +1,36 @@
+// ========================
+// Device Validation
+
+const Joi = require("joi");
+
+// ========================
+const insertDeviceSchema = Joi.object({
+ device_name: Joi.string().max(100).required(),
+ is_active: Joi.boolean().required(),
+ device_location: Joi.string().max(100).required(),
+ device_description: Joi.string().required(),
+ ip_address: Joi.string()
+ .ip({ version: ['ipv4', 'ipv6'] })
+ .required()
+ .messages({
+ 'string.ip': 'IP address must be a valid IPv4 or IPv6 address'
+ })
+});
+
+const updateDeviceSchema = Joi.object({
+ device_name: Joi.string().max(100),
+ is_active: Joi.boolean(),
+ device_location: Joi.string().max(100),
+ device_description: Joi.string(),
+ ip_address: Joi.string()
+ .ip({ version: ['ipv4', 'ipv6'] })
+ .messages({
+ 'string.ip': 'IP address must be a valid IPv4 or IPv6 address'
+ })
+}).min(1);
+
+
+// ✅ Export dengan CommonJS
+module.exports = {
+ insertDeviceSchema, updateDeviceSchema
+};
\ No newline at end of file
diff --git a/validate/roles.schema.js b/validate/roles.schema.js
new file mode 100644
index 0000000..d23436a
--- /dev/null
+++ b/validate/roles.schema.js
@@ -0,0 +1,25 @@
+// ========================
+// Device Validation
+
+const Joi = require("joi");
+
+// ========================
+const insertRolesSchema = Joi.object({
+ role_name: Joi.string().max(100).required(),
+ role_level: Joi.number().required(),
+ role_description: Joi.string().max(100).required(),
+ is_active: Joi.boolean().required(),
+});
+
+const updateRolesSchema = Joi.object({
+ role_name: Joi.string().max(100).optional(),
+ role_level: Joi.number().optional(),
+ role_description: Joi.string().max(100).optional(),
+ is_active: Joi.boolean().optional()
+}).min(1);
+
+
+// ✅ Export dengan CommonJS
+module.exports = {
+ insertRolesSchema, updateRolesSchema
+};
\ No newline at end of file
diff --git a/validate/schedule.schema.js b/validate/schedule.schema.js
new file mode 100644
index 0000000..ee0e37d
--- /dev/null
+++ b/validate/schedule.schema.js
@@ -0,0 +1,29 @@
+// ========================
+// Schedule Validation
+
+const Joi = require("joi");
+
+const datePattern = /^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$/;
+
+const insertScheduleSchema = Joi.object({
+ schedule_date: Joi.string().pattern(datePattern).required().messages({
+ "string.pattern.base": "schedule_date harus dalam format YYYY-MM-DD",
+ "any.required": "schedule_date wajib diisi",
+ }),
+ is_active: Joi.boolean().required(),
+ shift_id: Joi.number(),
+ next_day: Joi.number().required(),
+});
+
+const updateScheduleSchema = Joi.object({
+ schedule_date: Joi.string().pattern(datePattern).messages({
+ "string.pattern.base": "schedule_date harus dalam format YYYY-MM-DD",
+ }),
+ is_active: Joi.boolean(),
+ shift_id: Joi.number(),
+}).min(1);
+
+module.exports = {
+ insertScheduleSchema,
+ updateScheduleSchema,
+};
diff --git a/validate/shift.schema.js b/validate/shift.schema.js
new file mode 100644
index 0000000..4751778
--- /dev/null
+++ b/validate/shift.schema.js
@@ -0,0 +1,44 @@
+// ========================
+// Device Validation
+
+const Joi = require("joi");
+
+// ========================
+const timePattern = /^([01]\d|2[0-3]):([0-5]\d)(:[0-5]\d)?$/;
+
+const insertShiftSchema = Joi.object({
+ shift_name: Joi.string().max(100).required(),
+ is_active:Joi.boolean().required(),
+ start_time: Joi.string()
+ .pattern(timePattern)
+ .required()
+ .messages({
+ "string.pattern.base": "start_time harus dalam format HH:mm atau HH:mm:ss",
+ }),
+ end_time: Joi.string()
+ .pattern(timePattern)
+ .required()
+ .messages({
+ "string.pattern.base": "end_time harus dalam format HH:mm atau HH:mm:ss",
+ }),
+});
+
+const updateShiftSchema = Joi.object({
+ shift_name: Joi.string().max(100).optional(),
+ is_active:Joi.boolean().optional(),
+ start_time: Joi.string()
+ .pattern(timePattern)
+ .messages({
+ "string.pattern.base": "start_time harus dalam format HH:mm atau HH:mm:ss",
+ }).optional(),
+ end_time: Joi.string()
+ .pattern(timePattern)
+ .messages({
+ "string.pattern.base": "end_time harus dalam format HH:mm atau HH:mm:ss",
+ }),
+}).min(1);
+
+module.exports = {
+ insertShiftSchema,
+ updateShiftSchema,
+};
diff --git a/validate/status.schema.js b/validate/status.schema.js
new file mode 100644
index 0000000..9b79b65
--- /dev/null
+++ b/validate/status.schema.js
@@ -0,0 +1,26 @@
+const Joi = require("joi");
+
+// ========================
+// Status Validation
+// ========================
+const insertStatusSchema = Joi.object({
+ status_number: Joi.number().integer().required(),
+ status_name: Joi.string().max(200).required(),
+ status_color: Joi.string().max(200).required(),
+ status_description: Joi.string().allow('', null),
+ is_active: Joi.boolean().required(),
+});
+
+const updateStatusSchema = Joi.object({
+ status_number: Joi.number().integer().optional(),
+ status_name: Joi.string().max(200).optional(),
+ status_color: Joi.string().max(200).optional(),
+ status_description: Joi.string().allow('', null).optional(),
+ is_active: Joi.boolean().optional()
+}).min(1);
+
+// ✅ Export dengan CommonJS
+module.exports = {
+ insertStatusSchema,
+ updateStatusSchema
+};
diff --git a/validate/sub_section.schema.js b/validate/sub_section.schema.js
new file mode 100644
index 0000000..fe1e570
--- /dev/null
+++ b/validate/sub_section.schema.js
@@ -0,0 +1,33 @@
+const Joi = require("joi");
+
+// ========================
+// Plant Sub Section Validation
+// ========================
+const insertSubSectionSchema = Joi.object({
+ sub_section_name: Joi.string()
+ .max(200)
+ .required()
+ .messages({
+ "string.base": "Sub section name must be a string",
+ "string.max": "Sub section name cannot exceed 200 characters",
+ "any.required": "Sub section name is required"
+ }).required(),
+ is_active: Joi.boolean().required(),
+});
+
+const updateSubSectionSchema = Joi.object({
+ sub_section_name: Joi.string()
+ .max(200)
+ .messages({
+ "string.base": "Sub section name must be a string",
+ "string.max": "Sub section name cannot exceed 200 characters",
+ }).optional(),
+ is_active: Joi.boolean().optional(),
+}).min(1).messages({
+ "object.min": "At least one field must be provided to update",
+});
+
+module.exports = {
+ insertSubSectionSchema,
+ updateSubSectionSchema
+};
diff --git a/validate/tags.schema.js b/validate/tags.schema.js
new file mode 100644
index 0000000..29563f1
--- /dev/null
+++ b/validate/tags.schema.js
@@ -0,0 +1,33 @@
+// ========================
+// Device Validation
+
+const Joi = require("joi");
+
+// ========================
+const insertTagsSchema = Joi.object({
+ device_id: Joi.number().optional(),
+ tag_name: Joi.string().max(200).required(),
+ tag_number: Joi.number().required(),
+ is_active: Joi.boolean().required(),
+ data_type: Joi.string().max(50).required(),
+ unit: Joi.string().max(50).required(),
+ sub_section_id: Joi.number().optional(),
+ is_alarm: Joi.boolean().required()
+});
+
+const updateTagsSchema = Joi.object({
+ device_id: Joi.number(),
+ tag_name: Joi.string().max(200),
+ tag_number: Joi.number(),
+ is_active: Joi.boolean(),
+ data_type: Joi.string().max(50),
+ unit: Joi.string().max(50),
+ is_alarm: Joi.boolean().optional(),
+ sub_section_id: Joi.number().optional(),
+}).min(1);
+
+// ✅ Export dengan CommonJS
+module.exports = {
+ insertTagsSchema,
+ updateTagsSchema,
+};
diff --git a/validate/unit.schema.js b/validate/unit.schema.js
new file mode 100644
index 0000000..058e91c
--- /dev/null
+++ b/validate/unit.schema.js
@@ -0,0 +1,21 @@
+const Joi = require("joi");
+
+// ========================
+// Unit Validation
+// ========================
+const insertUnitSchema = Joi.object({
+ unit_name: Joi.string().max(100).required(),
+ tag_id: Joi.number().integer().optional(),
+ is_active: Joi.boolean().required(),
+});
+
+const updateUnitSchema = Joi.object({
+ unit_name: Joi.string().max(100).optional(),
+ tag_id: Joi.number().integer().optional(),
+ is_active: Joi.boolean().optional()
+}).min(1);
+
+module.exports = {
+ insertUnitSchema,
+ updateUnitSchema
+};
diff --git a/validate/user.schema.js b/validate/user.schema.js
new file mode 100644
index 0000000..19b4be9
--- /dev/null
+++ b/validate/user.schema.js
@@ -0,0 +1,61 @@
+const Joi = require("joi");
+
+// ========================
+// Users Validation
+// ========================
+const userSchema = Joi.object({
+ user_fullname: Joi.string().min(3).max(100).required(),
+ user_name: Joi.string().alphanum().min(3).max(50).required(),
+ user_email: Joi.string().email().required(),
+ user_phone: Joi.string()
+ .pattern(/^(?:\+62|0)8\d{7,10}$/)
+ .required()
+ .messages({
+ 'string.pattern.base':
+ 'Phone number must be a valid Indonesian number in format +628XXXXXXXXX'
+ }),
+ user_password: Joi.string()
+ .min(8)
+ .pattern(/[A-Z]/, 'uppercase letter')
+ .pattern(/[a-z]/, 'lowercase letter')
+ .pattern(/\d/, 'number')
+ .pattern(/[!@#$%^&*(),.?":{}|<>]/, 'special character')
+ .required()
+ .messages({
+ 'string.min': 'Password must be at least 8 characters long',
+ 'string.pattern.name': 'Password must contain at least one {#name}'
+ }),
+ role_id: Joi.number().integer().min(1)
+});
+
+const updateUserSchema = Joi.object({
+ user_fullname: Joi.string().min(3).max(100).optional(),
+ user_name: Joi.string().alphanum().min(3).max(50).optional(),
+ user_email: Joi.string().email().optional(),
+ user_phone: Joi.string()
+ .pattern(/^(?:\+62|0)8\d{7,10}$/)
+ .message('Phone number must be a valid Indonesian number in format +628XXXXXXXXX')
+ .optional(),
+ role_id: Joi.number().integer().min(1).optional(),
+ is_active: Joi.boolean().optional()
+}).min(1);
+
+const newPasswordSchema = Joi.object({
+ new_password: Joi.string()
+ .min(8)
+ .pattern(/[A-Z]/, 'uppercase letter')
+ .pattern(/[a-z]/, 'lowercase letter')
+ .pattern(/\d/, 'number')
+ .pattern(/[!@#$%^&*(),.?":{}|<>]/, 'special character')
+ .required()
+ .messages({
+ 'string.min': 'Password must be at least 8 characters long',
+ 'string.pattern.name': 'Password must contain at least one {#name}'
+ })
+});
+
+module.exports = {
+ userSchema,
+ newPasswordSchema,
+ updateUserSchema
+};
\ No newline at end of file