first commit

This commit is contained in:
Isuru Bimsara
2026-08-14 22:46:02 +05:30
commit 81d0e65686
132 changed files with 24398 additions and 0 deletions
+29
View File
@@ -0,0 +1,29 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/config/bullBoard.config.js
const { createBullBoard } = require("@bull-board/api");
const { ExpressAdapter } = require("@bull-board/express");
const { BullMQAdapter } = require("@bull-board/api/bullMQAdapter");
const activityQueue = require("../queues/activity.queue");
const serverAdapter = new ExpressAdapter();
serverAdapter.setBasePath("/admin/queues");
const { addQueue, removeQueue, setQueues, replaceQueues } =
createBullBoard({
queues: [new BullMQAdapter(activityQueue)],
serverAdapter,
});
module.exports = {
bullBoardRouter: serverAdapter.getRouter(),
};
+28
View File
@@ -0,0 +1,28 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/config/db.config.js
require("dotenv").config();
module.exports = {
HOST: process.env.DB_HOST || "localhost",
USER: process.env.DB_USER || "root",
PASSWORD: process.env.DB_PASSWORD || "",
DB: process.env.DB_NAME || "oceanic-db",
PORT: process.env.DB_PORT || 3306,
DIALECT: "mysql",
pool: {
max: 5,
min: 0,
acquire: 30000,
idle: 10000
}
};
+24
View File
@@ -0,0 +1,24 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/config/mail.config.js
require("dotenv").config();
module.exports = {
host: process.env.MAIL_HOST,
port: process.env.MAIL_PORT,
secure: process.env.MAIL_SECURE === "true", // true for 465, false for 587
auth: {
user: process.env.MAIL_USER,
pass: process.env.MAIL_PASS,
},
from: `Oceanic Maritime Solutions <${process.env.MAIL_FROM}>`,
};
+47
View File
@@ -0,0 +1,47 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/config/redis.config.js
const { Redis } = require("ioredis");
const createRedisConnection = () => {
const redis = new Redis({
host: process.env.REDIS_HOST || "redis",
port: process.env.REDIS_PORT || 6379,
password: process.env.REDIS_PASSWORD,
maxRetriesPerRequest: null,
enableReadyCheck: false,
});
// Connection events
redis.on("connect", () => {
console.log("Redis connecting...");
});
redis.on("ready", () => {
console.log("Redis connected and ready!");
});
redis.on("error", (err) => {
console.error("Redis error:", err.message);
});
redis.on("close", () => {
console.warn("Redis connection closed");
});
redis.on("reconnecting", () => {
console.log("Redis reconnecting...");
});
return redis;
};
module.exports = createRedisConnection;
+17
View File
@@ -0,0 +1,17 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/config/redisClient.js
const createRedisConnection = require("./redis.config");
const redis = createRedisConnection();
module.exports = redis;
+22
View File
@@ -0,0 +1,22 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/config/s3.config.js
const { S3Client } = require("@aws-sdk/client-s3");
const s3 = new S3Client({
region: process.env.AWS_REGION,
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
},
});
module.exports = s3;
+16
View File
@@ -0,0 +1,16 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/constants/permissions.js
module.exports = {
FINANCE_BASIS: "finance.basis",
PRECOST: "precost.precost",
};
+56
View File
@@ -0,0 +1,56 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/controllers/activity.controller.js
const db = require("../models");
const Activity = db.UserActivity;
// Get all user activities
exports.getUserActivities = async (req, res) => {
try {
const avt = await Activity.findAll({
order: [["createdAt", "DESC"]],
});
res.status(200).json({
success: true,
message: "User activities retrieved successfully",
data: avt,
});
} catch (error) {
res.status(500).json({
success: false,
message: "An error occurred while fetching user activities.",
});
}
};
// Get activity by User ID
exports.getActivitiesByUserId = async (req, res) => {
const userId = req.params.userId;
try {
const avt = await Activity.findAll({
where: { user_id: userId },
order: [["createdAt", "DESC"]],
});
res.status(200).json({
success: true,
message: "User activities retrieved successfully",
data: avt,
});
} catch (error) {
res.status(500).json({
success: false,
message: "An error occurred while fetching user activities.",
});
}
};
+132
View File
@@ -0,0 +1,132 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/controllers/auth.controller.js
const { checkPassword } = require("../utils/hashPassword.util");
const { sendMail } = require("../utils/mail.util");
const { generateOTP, validateOTP } = require("../utils/otp.util");
const {getCachedUser,clearUserCache} = require("../utils/cache.util");
const { generateToken } = require("../utils/jwt.util");
const { log } = require("../utils/consoleLog.utill");
const appName = process.env.APP_NAME || "Niolla";
// Login Step 1: Request OTP
exports.loginReq = async (req, res) => {
try {
const { email, password } = req.body;
const user = await getCachedUser(email);
if (!user) {
return res
.status(404)
.send({ success: false, message: "User Not Found" });
}
const passwordIsValid = await checkPassword(password, user.password);
if (!passwordIsValid) {
return res
.status(401)
.send({ success: false, message: "Invalid Password" });
}
const otp = generateOTP(email);
await sendMail({
to: email,
subject: `OTP for Your ${appName} Account`,
templateName: "otp",
templateVars: {
firstName: user.firstName,
otp: otp,
},
text: `Hello ${user.firstName}, your otp is ${otp}`,
});
log(`OTP for ${email}: ${otp}`);
log(`OTP sent to ${email} successfully.`);
res.status(201).send({ success: true, message: "OTP Sent Successfully" });
} catch (error) {
log("Error occurred while sending OTP:", error);
res.status(500).send({ success: false, message: error.message });
}
};
// Login Step 2: Verify OTP and issue JWT
exports.login = async (req, res) => {
try {
const { email, otp } = req.body;
const user = await getCachedUser(email);
if (!user) {
return res
.status(404)
.send({ success: false, message: "User Not Found" });
}
const isValidOTP = validateOTP(email, otp);
if (!isValidOTP) {
return res
.status(401)
.send({ success: false, message: "Invalid or Expired OTP" });
}
// Generate JWT token
const token = generateToken({
id: user.id,
firstName: user.firstName,
lastName: user.lastName,
email: user.email,
role: user.role,
accountType: user.accountType,
});
// 3. Set JWT as HttpOnly cookie
res.cookie("access_token", token, {
httpOnly: true, // JS cannot access
secure: process.env.NODE_ENV === "production", // HTTPS only in prod
sameSite: process.env.NODE_ENV === "production" ? "None" : "Lax",
maxAge: 24 * 60 * 60 * 1000, // 1 day
});
log(`JWT issued for ${email}`);
clearUserCache(email);
res.status(200).send({
success: true,
message: "Login Successful",
data: {
id: user.id,
email: user.email,
firstName: user.firstName,
lastName: user.lastName,
role: user.role,
accountType: user.accountType,
},
});
} catch (error) {
log("Error occurred during login:", error);
res.status(500).send({ success: false, message: error.message });
}
};
// Logout: Clear the JWT cookie
exports.logout = (req, res) => {
res.clearCookie("access_token", {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: process.env.NODE_ENV === "production" ? "None" : "Lax",
});
res.json({ success: true, message: "Logged out successfully" });
};
@@ -0,0 +1,417 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/controllers/generateDocument.controller.js
const fs = require("fs");
const path = require("path");
const documentQueue = require("../queues/document.queue");
const { createDocumentData } = require("../utils/document.utill");
const {
generateId,
generateDocumentReferenceNo,
} = require("../utils/idGen.util");
const { GetObjectCommand, DeleteObjectCommand } = require("@aws-sdk/client-s3");
const {log} = require("../utils/consoleLog.utill");
const s3 = require("../config/s3.config");
const db = require("../models");
const Document = db.Document;
const DocumentType = db.DocumentType;
const normalizeDocumentData = (value) => {
if (!value) {
return {};
}
if (typeof value === "string") {
try {
return JSON.parse(value);
} catch (error) {
return {};
}
}
return value;
};
/**
* Get available document types
*/
exports.getAvailableDocumentTypes = async (req, res) => {
try {
const doc_types = await DocumentType.findAll({
attributes: ["doc_type_name"],
});
const types = doc_types.map((t) => t.doc_type_name);
return res.json({
success: true,
availableDocumentTypes: types,
note: "Use these document type names in your requests (case-insensitive)",
});
} catch (error) {
log("Error fetching document types:", error.message);
return res.status(500).json({
success: false,
message: error.message,
});
}
};
// Get Saved documents
exports.getSavedDocuments = async (req, res) => {
try {
const { documentType } = req.body;
if (!documentType) {
return res.status(400).json({
success: false,
message: "documentType query parameter is required",
});
}
// Fetch saved documents based on documentType
const savedDocuments = await Document.findAll({
where: { doc_type: documentType.toUpperCase() },
order: [["createdAt", "DESC"]],
exclude: ["data"],
});
// extract only necessary fields to return
const formattedDocuments = savedDocuments.map((doc) => ({
doc_id: doc.doc_id,
reference_no: doc.reference_no,
doc_type: doc.doc_type,
status: doc.status,
createdAt: doc.createdAt,
updatedAt: doc.updatedAt,
}));
return res.json({
success: true,
savedDocuments: formattedDocuments,
});
} catch (error) {
log("Error fetching saved documents:", error.message);
return res.status(500).json({
success: false,
message: error.message,
});
}
};
// Get specific document data by doc_id
exports.getDocumentData = async (req, res) => {
try {
const { docId } = req.params;
if (!docId) {
return res.status(400).json({
success: false,
message: "docId parameter is required",
});
}
const document = await Document.findOne({
where: { doc_id: docId },
});
if (!document) {
return res.status(404).json({
success: false,
message: "Document not found",
});
}
return res.json({
success: true,
document: {
doc_id: document.doc_id,
reference_no: document.reference_no,
doc_type: document.doc_type,
data: document.data,
status: document.status,
createdAt: document.createdAt,
updatedAt: document.updatedAt,
},
});
} catch (error) {
log("Error fetching document data:", error.message);
return res.status(500).json({
success: false,
message: error.message,
});
}
}
exports.generateReferenceNo = async (req, res) => {
try {
const { documentType } = req.params;
if (!documentType) {
return res.status(400).json({
success: false,
message: "documentType is required",
});
}
// Generate reference number
const reference_no = await generateDocumentReferenceNo(documentType);
return res.json({
success: true,
reference_no,
});
} catch (error) {
log("Error generating reference number:", error.message);
return res.status(500).json({
success: false,
message: error.message,
});
}
};
exports.generateDraftDocument = async (req, res) => {
try {
const { document } = req.body;
const documentData = normalizeDocumentData(req.body.documentData || req.body.data || req.body);
// Validation
if (!document || !documentData) {
return res.status(400).json({
success: false,
message: "document and documentData are required",
});
}
let documentDetails;
if (document !== "PRECOST") {
// Save document details before generating
documentDetails = await createDocumentData(
document,
documentData,
"DRAFT",
);
} else {
return res.status(400).json({
success: false,
message:
"Invalid document type, This document type is not allowed to be generated as draft",
});
}
return res.status(201).json({
success: true,
message: "Draft document created successfully",
documentDetails,
});
} catch (error) {
log("Error generating draft document:", error.message);
return res.status(500).json({
success: false,
message: error.message,
});
}
};
/**
* Generate document asynchronously
* Returns jobId immediately
*/
exports.generateDocument = async (req, res) => {
try {
const { document, documentType } = req.body;
const documentData = normalizeDocumentData(req.body.documentData || req.body.data || req.body);
// Validation
if (!document || !documentType || !documentData) {
return res.status(400).json({
success: false,
message: "document, documentType, and documentData are required",
});
}
console.log(
`📨 generateDocument request: document="${document}", documentType="${documentType}"`,
);
if (document !== "PRECOST") {
// Save document details before generating
// If doc_id exists, finalize (update existing); otherwise create as DRAFT
const status = documentData.doc_id ? "FINAL" : "DRAFT";
await createDocumentData(document, documentData, status);
}
// Add job to queue
const job = await documentQueue.add("generate-document", {
document,
documentType,
data: documentData,
});
log(
`📋 Document generation job queued: ${job.id} (document="${document}", type="${documentType}")`,
);
return res.status(202).json({
success: true,
message: "Document generation started",
jobId: job.id,
});
} catch (error) {
log("Error queuing document generation:", error.message);
return res.status(500).json({
success: false,
message: error.message,
});
}
};
/**
* Get job status and result
*/
exports.getJobStatus = async (req, res) => {
try {
const { jobId } = req.params;
if (!jobId) {
return res.status(400).json({
success: false,
message: "jobId is required",
});
}
// Get job from queue
const job = await documentQueue.getJob(jobId);
if (!job) {
return res.status(404).json({
success: false,
message: "Job not found",
});
}
// Get job state
const state = await job.getState();
const result = job.returnvalue;
const failedReason = job.failedReason;
return res.json({
success: true,
jobId: job.id,
state, // "waiting" | "active" | "completed" | "failed" | "delayed"
result: state === "completed" ? result : null,
error: state === "failed" ? failedReason : null,
attempts: job.attemptsMade,
stacktrace: job.stacktrace,
});
} catch (error) {
log("Error fetching job status:", error.message);
return res.status(500).json({
success: false,
message: error.message,
});
}
};
/**
* Download generated document
*/
exports.downloadDocument = async (req, res) => {
try {
const { uuid } = req.params;
const key = `uploads/${uuid}.pdf`;
const command = new GetObjectCommand({
Bucket: process.env.AWS_S3_BUCKET_NAME,
Key: key,
});
const response = await s3.send(command);
res.setHeader(
"Content-Type",
response.ContentType || "application/octet-stream",
);
res.setHeader("Content-Disposition", `attachment; filename="${uuid}.pdf"`);
response.Body.pipe(res);
res.on("finish", async () => {
try {
await s3.send(
new DeleteObjectCommand({
Bucket: process.env.AWS_S3_BUCKET_NAME,
Key: key,
}),
);
log(`Deleted from S3: ${key}`);
} catch (err) {
log(`Failed to delete ${key}:`, err.message);
}
});
} catch (error) {
log("Download error:", error.message);
return res.status(404).json({
success: false,
message: "Document not found",
});
}
};
/**
* Cancel/delete a job
*/
exports.cancelJob = async (req, res) => {
try {
const { jobId } = req.params;
if (!jobId) {
return res.status(400).json({
success: false,
message: "jobId is required",
});
}
const job = await documentQueue.getJob(jobId);
if (!job) {
return res.status(404).json({
success: false,
message: "Job not found",
});
}
await job.remove();
return res.json({
success: true,
message: "Job cancelled successfully",
jobId,
});
} catch (error) {
log("Error cancelling job:", error.message);
return res.status(500).json({
success: false,
message: error.message,
});
}
};
+138
View File
@@ -0,0 +1,138 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/controllers/notification.controller.js
const db = require("../models");
const Notification = db.notification;
const UserNotification = db.userNotification;
const log = require("../utils/consoleLog.utill").log;
// Controller for managing notifications
exports.createNotification = async (req, res) => {
try {
const { notificationHeadline, notificationDescription, notificationType } =
req.body;
const id = Date.now().toString(); // Generate a unique ID based on the current timestamp
const notification_id = `notif_${id}`; // Prefix the ID with "notif_"
// Create a new notification
const newNotification = await Notification.create({
notification_id,
notificationHeadline,
notificationDescription,
notificationType,
dateCreated: new Date(),
});
res.status(201).json({
success: true,
message: "Notification created successfully",
notification: newNotification,
});
} catch (error) {
log("Error creating notification:", error.message);
res.status(500).json({
success: false,
message: "Internal server error",
error: error.message
});
}
};
// Mark a notification as read for a user
exports.markAsRead = async (req, res) => {
try {
const { userId, notificationId } = req.params;
// Find the user notification entry
const userNotification = await UserNotification.findOne({
where: { user_id: userId, notification_id: notificationId },
});
if (!userNotification) {
return res.status(404).json({ success: false, error: "User notification not found" });
}
// Mark the notification as read
userNotification.isRead = true;
await userNotification.save();
res.status(200).json({
success: true,
message: "Notification marked as read",
});
}
catch (error) {
log("Error marking notification as read:", error.message);
res.status(500).json({
success: false,
message: "Internal server error",
error: error.message
});
}
};
// Get announcements for all users
exports.getAnnouncements = async (req, res) => {
try {
// Fetch all announcements
const announcements = await Notification.findAll({
where: { notificationType: "ANNOUNCEMENT", isActive: true },
});
res.status(200).json({
success: true,
announcements,
});
} catch (error) {
log("Error fetching announcements:", error.message);
res.status(500).json({
success: false,
message: "Internal server error",
error: error.message
});
}
}
// Get all notifications for a user
exports.getUserNotifications = async (req, res) => {
try {
const { userId } = req.params;
// Fetch notifications for the user
const notifications = await UserNotification.findAll({
where: { user_id: userId, isRead: false },
include: [
{
model: Notification,
as: "notification",
},
],
});
res.status(200).json({
success: true,
notifications,
});
}
catch (error) {
log("Error fetching user notifications:", error.message);
res.status(500).json({
success: false,
message: "Internal server error",
error: error.message
});
}
};
+868
View File
@@ -0,0 +1,868 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/controllers/permission.controller.js
// PERMISSIONS:
// 1. createPermission --> Done
// 2. getAllPermissions --> Done
// 3. getPermissionById --> Done
// 4. updatePermission --> Done
// 5. deletePermission --> Done
// ROLES:
// 1. createRole --> Done
// 2. createRolePermission --> Done
// 3. getAllRoles --> Done
// 4. getRoleById --> Done
// 5. updateRole --> Done
// 6. deleteRole --> Done
// USER PERMISSIONS:
// 1. getUserPermissions --> Done
const db = require("../models");
const User = db.User;
const Role = db.roles;
const Permission = db.permission;
const RolePermission = db.rolePermission;
const UserPermission = db.userPermission;
// Create a new permission
exports.createPermission = async (req, res) => {
try {
const { permissionName, permissionDescription, page, module, action } = req.body;
if (!permissionName || !page || !module || !action) {
return res.status(400).json({ message: "Permission name, page, module, and action are required" });
}
const permissionId = `${module}.${page}.${action}`;
// Check if permission already exists
const existingPermission = await Permission.findOne({ where: { permission_id: permissionId } });
if (existingPermission) {
return res.status(400).json({ message: "Permission already exists" });
}
// Create the new permission
const newPermission = await Permission.create({
permission_id: permissionId,
permissionName,
permissionDescription,
page,
module,
action
});
res.status(201).json({
success: true,
data: newPermission
});
} catch (error) {
console.error("Error creating permission:", error);
res.status(500).json({ message: "Internal server error" });
}
};
// Bulk create permissions
exports.bulkCreatePermissions = async (req, res) => {
try {
const { permissions } = req.body;
if (!permissions || !Array.isArray(permissions) || permissions.length === 0) {
return res.status(400).json({
success: false,
message: "Permissions array is required and must not be empty"
});
}
const results = {
created: [],
failed: []
};
for (const item of permissions) {
try {
const { permissionName, permissionDescription, page, module, action } = item;
if (!permissionName || !page || !module || !action) {
results.failed.push({
...item,
error: "Permission name, page, module, and action are required"
});
continue;
}
const permissionId = `${module}.${page}.${action}`;
// Check if permission already exists
const existingPermission = await Permission.findOne({ where: { permission_id: permissionId } });
if (existingPermission) {
results.failed.push({
...item,
error: "Permission already exists"
});
continue;
}
const newPermission = await Permission.create({
permission_id: permissionId,
permissionName,
permissionDescription,
page,
module,
action
});
results.created.push(newPermission);
} catch (itemError) {
results.failed.push({
...item,
error: itemError.message
});
}
}
res.status(201).json({
success: results.failed.length === 0,
data: results
});
} catch (error) {
console.error("Error bulk creating permissions:", error);
res.status(500).json({
success: false,
message: "An error occurred while bulk creating permissions."
});
}
};
// Get all permissions
exports.getAllPermissions = async (req, res) => {
try {
const permissions = await Permission.findAll();
res.status(200).json({
success: true,
data: permissions
});
} catch (error) {
console.error("Error fetching permissions:", error);
res.status(500).json({
success: false,
message: "An error occurred while fetching permissions."
});
}
};
// Get permission by ID
exports.getPermissionById = async (req, res) => {
try {
const { permissionId } = req.params;
const permission = await Permission.findByPk(permissionId);
if (!permission) {
return res.status(404).json({
success: false,
message: "Permission not found"
});
}
res.status(200).json({
success: true,
data: permission
});
} catch (error) {
console.error("Error fetching permission:", error);
res.status(500).json({
success: false,
message: "An error occurred while fetching permission."
});
}
};
// Update permission
exports.updatePermission = async (req, res) => {
try {
const { permissionId } = req.params;
const { permissionName, permissionDescription, page, module, action } = req.body;
const permission = await Permission.findByPk(permissionId);
if (!permission) {
return res.status(404).json({
success: false,
message: "Permission not found"
});
}
// Update permission fields
await permission.update({
permissionName: permissionName || permission.permissionName,
permissionDescription: permissionDescription || permission.permissionDescription,
page: page || permission.page,
module: module || permission.module,
action: action || permission.action
});
res.status(200).json({
success: true,
data: permission,
message: "Permission updated successfully"
});
} catch (error) {
console.error("Error updating permission:", error);
res.status(500).json({
success: false,
message: "An error occurred while updating permission."
});
}
};
// Delete permission
exports.deletePermission = async (req, res) => {
try {
const { permissionId } = req.params;
const permission = await Permission.findByPk(permissionId);
if (!permission) {
return res.status(404).json({
success: false,
message: "Permission not found"
});
}
// Delete associated user and role permissions first
await UserPermission.destroy({ where: { permission_id: permissionId } });
await RolePermission.destroy({ where: { permission_id: permissionId } });
// Delete the permission
await permission.destroy();
res.status(200).json({
success: true,
message: "Permission deleted successfully"
});
} catch (error) {
console.error("Error deleting permission:", error);
res.status(500).json({
success: false,
message: "An error occurred while deleting permission."
});
}
};
// Create a new role
exports.createRole = async (req, res) => {
try {
const { roleName, roleDescription } = req.body;
if (!roleName) {
return res.status(400).json({
success: false,
message: "Role name is required"
});
}
// Generate role ID
const roleId = `role_${Date.now()}`;
const newRole = await Role.create({
role_id: roleId,
roleName,
roleDescription
});
res.status(201).json({
success: true,
data: newRole
});
} catch (error) {
console.error("Error creating role:", error);
res.status(500).json({
success: false,
message: "An error occurred while creating role."
});
}
};
// Create role-permission assignment
exports.createRolePermission = async (req, res) => {
try {
const { role_id, permission_id } = req.body;
if (!role_id || !permission_id) {
return res.status(400).json({
success: false,
message: "Role ID and Permission ID are required"
});
}
// Verify role and permission exist
const role = await Role.findByPk(role_id);
if (!role) {
return res.status(404).json({
success: false,
message: "Role not found"
});
}
const permission = await Permission.findByPk(permission_id);
if (!permission) {
return res.status(404).json({
success: false,
message: "Permission not found"
});
}
// Check if assignment already exists
const existingAssignment = await RolePermission.findOne({
where: { role_id, permission_id }
});
if (existingAssignment) {
return res.status(400).json({
success: false,
message: "Role permission already exists"
});
}
const rpId = `rp_${Date.now()}`;
const newRolePermission = await RolePermission.create({
rp_id: rpId,
role_id,
permission_id
});
res.status(201).json({
success: true,
data: newRolePermission
});
} catch (error) {
console.error("Error creating role permission:", error);
res.status(500).json({
success: false,
message: "An error occurred while creating role permission."
});
}
};
// Bulk create role permissions
exports.bulkCreateRolePermissions = async (req, res) => {
try {
const { role_id, permission_ids } = req.body;
if (!role_id || !permission_ids || !Array.isArray(permission_ids) || permission_ids.length === 0) {
return res.status(400).json({
success: false,
message: "Role ID and permission_ids array are required and must not be empty"
});
}
// Verify role exists
const role = await Role.findByPk(role_id);
if (!role) {
return res.status(404).json({
success: false,
message: "Role not found"
});
}
const results = {
created: [],
failed: []
};
for (const permission_id of permission_ids) {
try {
if (!permission_id) {
results.failed.push({
role_id,
permission_id,
error: "Permission ID is required"
});
continue;
}
// Verify permission exists
const permission = await Permission.findByPk(permission_id);
if (!permission) {
results.failed.push({
role_id,
permission_id,
error: "Permission not found"
});
continue;
}
// Check if assignment already exists
const existingAssignment = await RolePermission.findOne({
where: { role_id, permission_id }
});
if (existingAssignment) {
results.failed.push({
role_id,
permission_id,
error: "Role permission already exists"
});
continue;
}
const rpId = `rp_${Date.now()}`;
const newRolePermission = await RolePermission.create({
rp_id: rpId,
role_id,
permission_id
});
results.created.push(newRolePermission);
} catch (itemError) {
results.failed.push({
role_id,
permission_id,
error: itemError.message
});
}
}
res.status(201).json({
success: results.failed.length === 0,
data: results
});
} catch (error) {
console.error("Error bulk creating role permissions:", error);
res.status(500).json({
success: false,
message: "An error occurred while bulk creating role permissions."
});
}
};
// Get all roles
exports.getAllRoles = async (req, res) => {
try {
const roles = await Role.findAll({
include: [
{
model: RolePermission,
as: "rolePermissions",
include: [
{
model: Permission,
as: "permission",
attributes: ["permission_id", "permissionName", "page", "module", "action"]
}
]
}
]
});
res.status(200).json({
success: true,
data: roles
});
} catch (error) {
console.error("Error fetching roles:", error);
res.status(500).json({
success: false,
message: "An error occurred while fetching roles."
});
}
};
// Get role by ID
exports.getRoleById = async (req, res) => {
try {
const { roleId } = req.params;
const role = await Role.findByPk(roleId, {
include: [
{
model: RolePermission,
as: "rolePermissions",
include: [
{
model: Permission,
as: "permission",
attributes: ["permission_id", "permissionName", "page", "module", "action"]
}
]
}
]
});
if (!role) {
return res.status(404).json({
success: false,
message: "Role not found"
});
}
res.status(200).json({
success: true,
data: role
});
} catch (error) {
console.error("Error fetching role:", error);
res.status(500).json({
success: false,
message: "An error occurred while fetching role."
});
}
};
// Update role
exports.updateRole = async (req, res) => {
try {
const { roleId } = req.params;
const { roleName, roleDescription } = req.body;
const role = await Role.findByPk(roleId);
if (!role) {
return res.status(404).json({
success: false,
message: "Role not found"
});
}
// Update role fields
await role.update({
roleName: roleName || role.roleName,
roleDescription: roleDescription || role.roleDescription
});
res.status(200).json({
success: true,
data: role,
message: "Role updated successfully"
});
} catch (error) {
console.error("Error updating role:", error);
res.status(500).json({
success: false,
message: "An error occurred while updating role."
});
}
};
// Delete role
exports.deleteRole = async (req, res) => {
try {
const { roleId } = req.params;
const role = await Role.findByPk(roleId);
if (!role) {
return res.status(404).json({
success: false,
message: "Role not found"
});
}
// Delete associated role permissions first
await RolePermission.destroy({ where: { role_id: roleId } });
// Delete the role
await role.destroy();
res.status(200).json({
success: true,
message: "Role deleted successfully"
});
} catch (error) {
console.error("Error deleting role:", error);
res.status(500).json({
success: false,
message: "An error occurred while deleting role."
});
}
};
// Get user permissions
exports.getUserPermissions = async (req, res) => {
try {
const userId = req.params.userId;
// Fetch user permissions
const userPermissions = await UserPermission.findAll({
where: { user_id: userId },
include: [
{
model: Permission,
as: "permission",
attributes: ["permission_id", "permissionName", "page", "module", "action"]
}
]
});
res.status(200).json({
success: true,
data: userPermissions
});
} catch (error) {
console.error("Error fetching user permissions:", error);
res.status(500).json({
success: false,
message: "An error occurred while fetching user permissions."
});
}
};
// Create user permission
exports.createUserPermission = async (req, res) => {
try {
const { user_id, permission_id } = req.body;
if (!user_id || !permission_id) {
return res.status(400).json({
success: false,
message: "User ID and Permission ID are required"
});
}
// Verify user and permission exist
const user = await User.findByPk(user_id);
if (!user) {
return res.status(404).json({
success: false,
message: "User not found"
});
}
const permission = await Permission.findByPk(permission_id);
if (!permission) {
return res.status(404).json({
success: false,
message: "Permission not found"
});
}
// Check if assignment already exists
const existingAssignment = await UserPermission.findOne({
where: { user_id, permission_id }
});
if (existingAssignment) {
return res.status(400).json({
success: false,
message: "User permission already exists"
});
}
const newUserPermission = await UserPermission.create({
user_id,
permission_id
});
res.status(201).json({
success: true,
data: newUserPermission
});
} catch (error) {
console.error("Error creating user permission:", error);
res.status(500).json({
success: false,
message: "An error occurred while creating user permission."
});
}
};
// Bulk create user permissions
exports.bulkCreateUserPermissions = async (req, res) => {
try {
const { user_id, permission_ids } = req.body;
if (!user_id || !permission_ids || !Array.isArray(permission_ids) || permission_ids.length === 0) {
return res.status(400).json({
success: false,
message: "User ID and permission_ids array are required and must not be empty"
});
}
// Verify user exists
const user = await User.findByPk(user_id);
if (!user) {
return res.status(404).json({
success: false,
message: "User not found"
});
}
const results = {
created: [],
failed: []
};
for (const permission_id of permission_ids) {
try {
if (!permission_id) {
results.failed.push({
user_id,
permission_id,
error: "Permission ID is required"
});
continue;
}
// Verify permission exists
const permission = await Permission.findByPk(permission_id);
if (!permission) {
results.failed.push({
user_id,
permission_id,
error: "Permission not found"
});
continue;
}
// Check if assignment already exists
const existingAssignment = await UserPermission.findOne({
where: { user_id, permission_id }
});
if (existingAssignment) {
results.failed.push({
user_id,
permission_id,
error: "User permission already exists"
});
continue;
}
const newUserPermission = await UserPermission.create({
user_id,
permission_id
});
results.created.push(newUserPermission);
} catch (itemError) {
results.failed.push({
user_id,
permission_id,
error: itemError.message
});
}
}
res.status(201).json({
success: results.failed.length === 0,
data: results
});
} catch (error) {
console.error("Error bulk creating user permissions:", error);
res.status(500).json({
success: false,
message: "An error occurred while bulk creating user permissions."
});
}
};
// Update user permission
exports.updateUserPermission = async (req, res) => {
try {
const { upId } = req.params;
const { permission_id } = req.body;
if (!permission_id) {
return res.status(400).json({
success: false,
message: "Permission ID is required"
});
}
const userPermission = await UserPermission.findByPk(upId);
if (!userPermission) {
return res.status(404).json({
success: false,
message: "User permission not found"
});
}
// Verify permission exists
const permission = await Permission.findByPk(permission_id);
if (!permission) {
return res.status(404).json({
success: false,
message: "Permission not found"
});
}
// Check if new permission assignment already exists for this user
const existingAssignment = await UserPermission.findOne({
where: {
user_id: userPermission.user_id,
permission_id
}
});
if (existingAssignment && existingAssignment.up_id !== upId) {
return res.status(400).json({
success: false,
message: "This permission is already assigned to this user"
});
}
await userPermission.update({ permission_id });
res.status(200).json({
success: true,
data: userPermission,
message: "User permission updated successfully"
});
} catch (error) {
console.error("Error updating user permission:", error);
res.status(500).json({
success: false,
message: "An error occurred while updating user permission."
});
}
};
// Delete user permission
exports.deleteUserPermission = async (req, res) => {
try {
const { upId } = req.params;
const userPermission = await UserPermission.findByPk(upId);
if (!userPermission) {
return res.status(404).json({
success: false,
message: "User permission not found"
});
}
await userPermission.destroy();
res.status(200).json({
success: true,
message: "User permission deleted successfully"
});
} catch (error) {
console.error("Error deleting user permission:", error);
res.status(500).json({
success: false,
message: "An error occurred while deleting user permission."
});
}
};
+219
View File
@@ -0,0 +1,219 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/controllers/profile.controller.js
const db = require("../models");
const Profile = db.Profile;
const Upload = db.Upload;
const User = db.User;
const { generateTodoId } = require("../utils/idGen.util");
const { getSignedFileUrl } = require("../utils/s3Upload.utill");
const {
generateResetToken,
resetPassword,
} = require("../utils/passwordReset.utill");
const { sendMail } = require("../utils/mail.util");
const { logActivity } = require("../services/activity.service");
const { hashPassword, checkPassword } = require("../utils/hashPassword.util");
const { log } = require("../utils/consoleLog.utill");
// Get Profile avatar by user ID
exports.getProfileAvatar = async (req, res) => {
try {
const userId = req.params.userId;
const profile = await Profile.findOne({ where: { user_id: userId } });
if (!profile) {
return res.status(404).json({ error: "Profile not found" });
}
const uploadRecord = await Upload.findByPk(profile.profilePicture_id);
const fileUrl = await getSignedFileUrl(uploadRecord.file_path);
res.json({
success: true,
data: {
userId: profile.userId,
profilePictureUrl: fileUrl,
},
});
} catch (error) {
console.error("Error fetching profile:", error);
res.status(500).json({
success: false,
message: "Internal server error",
error: error.message,
});
}
};
// Get profile background image by user ID
exports.getProfileBackgroundImage = async (req, res) => {
try {
const userId = req.params.userId;
const profile = await Profile.findOne({ where: { user_id: userId } });
if (!profile) {
return res
.status(404)
.json({ success: false, message: "Profile not found" });
}
const uploadRecord = await Upload.findByPk(profile.backgroundImage_id);
const fileUrl = await getSignedFileUrl(uploadRecord.file_path);
res.json({
success: true,
data: {
userId: profile.userId,
backgroundImageUrl: fileUrl,
},
});
} catch (error) {
console.error("Error fetching profile background image:", error);
res.status(500).json({
success: false,
message: "Internal server error",
error: error.message,
});
}
};
// Request password reset token
exports.requestPasswordReset = async (req, res) => {
try {
const { email } = req.body;
const user = await User.findOne({
where: { email },
});
if (user) {
const token = await generateResetToken(user.id);
const resetLink = `${process.env.FRONTEND_URL}/reset-password?token=${token}&email=${encodeURIComponent(email)}`;
await sendMail({
to: user.email,
subject: "Password Reset",
templateName: "passwordReset",
templateVars: {
firstName: user.firstName,
resetLink,
expiryTime: process.env.PASSWORD_RESET_EXPIRY_TIME || "10 minutes",
},
});
const activityUser = {
id: 1,
firstName: "System",
};
await logActivity({
user: activityUser,
description: "Requested password reset for email: " + email,
type: "PASSWORD_RESET_REQUEST",
module: "Authentication",
});
}
return res.json({
success: true,
message:
"If an account exists with this email address, a password reset email has been sent.",
});
} catch (error) {
console.error(error);
return res.status(500).json({
success: false,
message: "Internal server error",
});
}
};
// Reset password using token
exports.resetPassword = async (req, res) => {
try {
const { email, token, newPassword } = req.body;
// Validate email
const user = await User.findOne({
where: { email },
});
if (!user) {
return res.status(400).json({
success: false,
message: "Invalid email address.",
});
}
await resetPassword(user.id, token, newPassword);
return res.json({
success: true,
message: "Password reset successfully.",
});
} catch (error) {
return res.status(400).json({
success: false,
message: error.message || "Invalid or expired token.",
});
}
};
// Change Password
exports.changePassword = async (req, res) => {
try {
const { currentPassword, newPassword } = req.body;
const user = req.user;
log(`User:`, user);
const foundUser = await User.findOne({ where: { id: user.id } });
if (!foundUser) {
return res.status(404).json({
success: false,
message: "User not found.",
});
}
const isMatch = await checkPassword(currentPassword, foundUser.password);
if (!isMatch) {
return res.status(400).json({
success: false,
message: "Current password is incorrect.",
});
}
foundUser.password = await hashPassword(newPassword);
await foundUser.save();
await logActivity({
user: req.user,
description: "Changed password",
type: "PASSWORD_CHANGE",
module: "Authentication",
});
return res.json({
success: true,
message: "Password changed successfully.",
});
} catch (error) {
return res.status(400).json({
success: false,
message: error.message || "Failed to change password.",
});
}
};
+131
View File
@@ -0,0 +1,131 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/controllers/activity.controller.js
const { uploadToS3, getSignedFileUrl } = require("../utils/s3Upload.utill");
const { log } = require("../utils/consoleLog.utill");
const db = require("../models");
const Upload = db.Upload;
const Assets = db.Assets;
// Constants
const MAX_IMAGE_SIZE = 3 * 1024 * 1024; // 3MB
const MAX_PDF_SIZE = 5 * 1024 * 1024; // 5MB
const isValidFileType = (mimetype) => {
return mimetype.startsWith("image/") || mimetype === "application/pdf";
};
const isValidFileSize = (mimetype, size) => {
if (mimetype.startsWith("image/")) return size <= MAX_IMAGE_SIZE;
if (mimetype === "application/pdf") return size <= MAX_PDF_SIZE;
return false;
};
exports.uploadFile = async (req, res) => {
try {
// 1. Check file exists
if (!req.file) {
return res.status(400).json({
success: false,
message: "No file uploaded",
});
}
const { mimetype, size, originalname } = req.file;
// 2. Validate file type
if (!isValidFileType(mimetype)) {
return res.status(400).json({
success: false,
message: "Only images and PDFs are allowed",
});
}
// 3. Validate file size
if (!isValidFileSize(mimetype, size)) {
return res.status(400).json({
success: false,
message: mimetype.startsWith("image/")
? "Image too large (max 1MB)"
: "PDF too large (max 5MB)",
});
}
log("File validation passed:", {
mimetype,
size,
originalname,
use_for: req.body.use_for,
});
// 4. Upload to S3
const fileKey = await uploadToS3(req.file, req.body.use_for);
const fileUrl = await getSignedFileUrl(fileKey);
// 5. Save to DB
const uploadData = {
file_path: fileKey,
file_type: mimetype,
file_size: size,
original_name: originalname,
use_for: req.body.use_for || null,
uploaded_by: req.user?.id || null,
};
const newUpload = await Upload.create(uploadData);
newUpload.dataValues.file_url = fileUrl; // Add URL to response
// 6. Response
return res.status(201).json({
success: true,
message: "File uploaded successfully",
data: newUpload,
});
} catch (error) {
console.error("Upload Controller Error:", error);
return res.status(500).json({
success: false,
message: "Failed to upload file",
error: process.env.NODE_ENV === "development" ? error.message : undefined,
});
}
};
// Get Uploaded File URL
exports.getFileUrl = async (req, res) => {
try {
const { id } = req.params;
const uploadRecord = await Upload.findByPk(id);
if (!uploadRecord) {
return res.status(404).json({
success: false,
message: "File not found",
});
}
const fileUrl = await getSignedFileUrl(uploadRecord.file_path);
return res.status(200).json({
success: true,
message: "File URL retrieved successfully",
data: {
id: uploadRecord.id,
file_url: fileUrl,
},
});
} catch (error) {}
};
+323
View File
@@ -0,0 +1,323 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/controllers/user.controller.js
const db = require("../models");
const { hashPassword } = require("../utils/hashPassword.util");
const { generateUserId, generateId } = require("../utils/idGen.util");
const { logActivity } = require("../services/activity.service");
const { sendMail } = require("../utils/mail.util");
const User = db.User;
const Profile = db.Profile;
// Create a new user
exports.createNewUser = async (req, res) => {
const transaction = await db.sequelize.transaction();
try {
const {
firstName,
lastName,
email,
role,
roleID,
accountType,
department,
} = req.body;
const hashedPassword = await hashPassword(process.env.DEFAULT_PASSWORD);
const userID = generateUserId();
const userExists = await User.findOne({ where: { email } });
if (userExists) {
await transaction.rollback();
return res.status(400).send({
success: false,
message: "User with this email already exists",
});
}
const newUser = await User.create(
{
id: userID,
firstName,
lastName,
email,
password: hashedPassword,
accountType,
role,
roleID: roleID || "N/A",
department: department || null,
},
{ transaction },
);
const newProfile = await Profile.create(
{
profile_id: generateId(),
user_id: newUser.id,
theme: "light",
notificationsEnabled: true,
profilePicture_id: "N/A",
backgroundImage_id: "N/A",
dob: null,
phone_number: null,
},
{ transaction },
);
await sendMail({
to: email,
subject: "Welcome - Ocenic Titan",
templateName: "welcome",
templateVars: {
firstName: firstName,
email: email,
password: process.env.DEFAULT_PASSWORD,
},
text: `Hello ${firstName}, your account has been created successfully.`,
});
await transaction.commit();
await logActivity({
user: req.user,
description: `Created New User with ID: ${newUser.id}`,
type: "CREATE_USER",
module: "User Management",
});
res.status(201).send({
success: true,
message: "User Created Successfully",
data: {
id: newUser.id,
firstName: newUser.firstName,
lastName: newUser.lastName,
email: newUser.email,
accountType: newUser.accountType,
role: newUser.role,
department: newUser.department,
},
});
} catch (error) {
await transaction.rollback();
res.status(500).send({
success: false,
message: "Failed to create user",
error: error.message,
});
}
};
// Get users with pagination (20 per page)
exports.getAllUsers = async (req, res) => {
try {
const page = parseInt(req.query.page) || 1; // default page = 1
const limit = 20;
const offset = (page - 1) * limit;
const { count, rows: users } = await User.findAndCountAll({
attributes: { exclude: ["password"] },
limit,
offset,
order: [["createdAt", "DESC"]], // optional sorting
});
res.status(200).send({
success: true,
data: users,
pagination: {
totalUsers: count,
totalPages: Math.ceil(count / limit),
currentPage: page,
pageSize: limit,
},
});
} catch (error) {
res.status(500).send({
success: false,
message: "Failed to retrieve users",
error: error.message,
});
}
};
// Get user details by ID
exports.getUserById = async (req, res) => {
try {
const { id } = req.params;
const user = await User.findOne({
where: { id },
attributes: { exclude: ["password"] },
include: [{ model: Profile, as: "profile" }],
});
if (!user) {
return res.status(404).send({
success: false,
message: "User not found",
});
}
res.status(200).send({
success: true,
data: user,
});
} catch (error) {
res.status(500).send({
success: false,
message: "Failed to retrieve user",
error: error.message,
});
}
};
// Update user details
exports.updateUser = async (req, res) => {
const transaction = await db.sequelize.transaction();
try {
const { id } = req.params;
const {
firstName,
lastName,
email,
role,
roleID,
accountType,
department,
theme,
notificationsEnabled,
profilePicture_id,
backgroundImage_id,
dob,
phone_number,
} = req.body;
const user = await User.findOne({
where: { id },
});
const UserProfile = await Profile.findOne({
where: { user_id: id },
});
if (!user) {
return res.status(404).send({
success: false,
message: "User not found",
});
}
if (!UserProfile) {
return res.status(404).send({
success: false,
message: "User profile not found",
});
}
user.firstName = firstName || user.firstName;
user.lastName = lastName || user.lastName;
user.role = role || user.role;
user.roleID = roleID || user.roleID || "N/A";
user.accountType = accountType || user.accountType;
user.department = department || user.department;
UserProfile.theme = theme || UserProfile.theme;
UserProfile.notificationsEnabled =
notificationsEnabled !== undefined
? notificationsEnabled
: UserProfile.notificationsEnabled;
UserProfile.profilePicture_id =
profilePicture_id || UserProfile.profilePicture_id || "N/A";
UserProfile.backgroundImage_id =
backgroundImage_id || UserProfile.backgroundImage_id || "N/A";
UserProfile.dob = dob || UserProfile.dob;
UserProfile.phone_number = phone_number || UserProfile.phone_number;
await UserProfile.save({ transaction });
await user.save({ transaction });
await transaction.commit();
await logActivity({
user: req.user,
description: `Updated User with ID: ${user.id}`,
type: "UPDATE_USER",
module: "User Management",
});
const data = {
id: user.id,
firstName: user.firstName,
lastName: user.lastName,
email: user.email,
accountType: user.accountType,
role: user.role,
department: user.department,
profile: {
theme: UserProfile.theme,
notificationsEnabled: UserProfile.notificationsEnabled,
profilePicture_id: UserProfile.profilePicture_id,
backgroundImage_id: UserProfile.backgroundImage_id,
dob: UserProfile.dob,
phone_number: UserProfile.phone_number,
},
};
res.status(200).send({
success: true,
message: "User updated successfully",
data,
});
} catch (error) {
await transaction.rollback();
res.status(500).send({
success: false,
message: `Error: ${error.message}`,
});
}
};
// Delete user
exports.deleteUser = async (req, res) => {
const transaction = await db.sequelize.transaction();
try {
const { id } = req.params;
const user = await User.findOne({
where: { id },
});
if (!user) {
return res.status(404).send({
success: false,
message: "User not found",
});
}
await user.destroy({ transaction });
await transaction.commit();
await logActivity({
user: req.user,
description: `Deleted User with ID: ${user.id}`,
type: "DELETE_USER",
module: "User Management",
});
res.status(200).send({
success: true,
message: "User deleted successfully",
});
} catch (error) {
await transaction.rollback();
res.status(500).send({
success: false,
message: `Error: ${error.message}`,
});
}
};
@@ -0,0 +1,64 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/logic/documents/builders/invoice.builder.js
module.exports = {
sheetName: "Invoice",
build: async (worksheet, data) => {
worksheet.addRow(["INVOICE"]).font = {
bold: true,
size: 18,
};
worksheet.addRow([]);
worksheet.addRow(["Invoice No", data.invoiceNo]);
worksheet.addRow(["Customer", data.customerName]);
worksheet.addRow(["Date", data.date]);
worksheet.addRow([]);
const header = worksheet.addRow([
"Description",
"Qty",
"Rate",
"Amount",
]);
header.font = { bold: true };
let total = 0;
(data.items || []).forEach((item) => {
const amount = item.qty * item.rate;
total += amount;
worksheet.addRow([
item.description,
item.qty,
item.rate,
amount,
]);
});
worksheet.addRow([]);
worksheet.addRow([
"",
"",
"TOTAL",
total,
]).font = {
bold: true,
};
},
};
@@ -0,0 +1,68 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/logic/documents/builders/precost.builder.js
module.exports = {
sheetName: "PreCost Estimate",
build: async (worksheet, data) => {
worksheet.addRow(["PreCost Estimate"]).font = {
bold: true,
size: 16,
};
worksheet.addRow([]);
worksheet.addRow([
"Customer",
data.customerName || "",
]);
worksheet.addRow([]);
const header = worksheet.addRow([
"Item",
"Description",
"Qty",
"Unit Price",
"Total",
]);
header.font = { bold: true };
let total = 0;
(data.items || []).forEach((item) => {
const rowTotal = item.quantity * item.unit_price;
total += rowTotal;
worksheet.addRow([
item.item_name,
item.customer_remark ,
item.quantity,
item.unit_price,
rowTotal,
]);
});
worksheet.addRow([]);
worksheet.addRow([
"",
"",
"",
"Grand Total",
total,
]).font = {
bold: true,
};
},
};
@@ -0,0 +1,25 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/logic/documents/engine/excel.engine.js
const ExcelJS = require("exceljs");
const generateExcel = async (builder, data) => {
const workbook = new ExcelJS.Workbook();
const worksheet = workbook.addWorksheet(builder.sheetName);
await builder.build(worksheet, data);
return await workbook.xlsx.writeBuffer();
};
module.exports = {
generateExcel,
};
+70
View File
@@ -0,0 +1,70 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/logic/documents/engine/pdf.engine.js
const puppeteer = require("puppeteer");
const generatePDF = async (html) => {
const launchConfig = {
args: ["--no-sandbox", "--disable-setuid-sandbox"],
};
if (process.env.PUPPETEER_EXECUTABLE_PATH) {
launchConfig.executablePath = process.env.PUPPETEER_EXECUTABLE_PATH;
} else {
launchConfig.headless = true;
}
const browser = await puppeteer.launch(launchConfig);
try {
const page = await browser.newPage();
await page.setContent(html, {
waitUntil: "load",
});
// Wait for fonts
await page.evaluate(() => document.fonts?.ready);
// Wait for images to fully decode
await page.evaluate(async () => {
const images = Array.from(document.images);
await Promise.all(
images.map((img) => {
if (img.complete && img.naturalHeight !== 0) return;
return new Promise((resolve) => {
img.onload = resolve;
img.onerror = resolve;
});
}),
);
});
// Force layout + paint
await page.evaluate(() => document.body.getBoundingClientRect());
// Extra stability delay (important on servers)
await new Promise((r) => setTimeout(r, 1000));
const buffer = await page.pdf({
format: "A4",
printBackground: true,
});
return buffer;
} finally {
await browser.close();
}
};
module.exports = { generatePDF };
+105
View File
@@ -0,0 +1,105 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/logic/documents/index.js
const registry = require("./registry");
const { generateExcel } = require("./engine/excel.engine");
const {
generatePdfFromPreCost,
generatePdfFromInvoice,
generatePdfFromDispatchNote,
generatePdfFromDeliveryNote,
generatePdfFromPO,
generatePdfFromCustomDocument,
generatePdfFromCreditNote,
} = require("../../utils/pdfGenerator");
const formatMeta = {
pdf: {
ext: "pdf",
mime: "application/pdf",
},
excel: {
ext: "xlsx",
mime:
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
},
};
const pdfGenerators = {
precost: generatePdfFromPreCost,
invoice: generatePdfFromInvoice,
dispatchnote: generatePdfFromDispatchNote,
deliverynote: generatePdfFromDeliveryNote,
po: generatePdfFromPO,
customdocument: generatePdfFromCustomDocument,
creditnote: generatePdfFromCreditNote,
};
const generateDocument = async (
document,
format,
data
) => {
document = document.toLowerCase().replace(/[\s_-]+/g, "");
format = format.toLowerCase();
const doc = registry[document];
if (!doc) {
const availableTypes = Object.keys(registry).join(", ");
throw new Error(`Unsupported document type: "${document}". Available types: ${availableTypes}`);
}
let fileBuffer;
if (format === "pdf") {
// Format the data using the template
const formattedData = await doc.pdfTemplate(data);
// Get the correct PDF generator for this document type
const pdfGenerator = pdfGenerators[document];
if (pdfGenerator) {
fileBuffer = await pdfGenerator(formattedData);
} else {
throw new Error(`No PDF generator found for document type: ${document}`);
}
}
else if (format === "excel") {
if (doc.excelBuilder) {
fileBuffer = await generateExcel(
doc.excelBuilder,
data
);
} else {
throw new Error(`Excel generation not supported for document type: ${document}`);
}
}
else {
throw new Error("Unsupported format");
}
const meta = formatMeta[format];
return {
fileBuffer,
fileName: `${document}-${Date.now()}.${meta.ext}`,
mimeType: meta.mime,
};
};
module.exports = {
generateDocument,
};
+72
View File
@@ -0,0 +1,72 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/logic/documents/registry.js
let buildPreCostTemplate, buildInvoiceTemplate, buildDispatchNoteTemplate, buildDeliveryNoteTemplate, buildPOTemplate, buildCustomDocumentTemplate, buildCreditNoteTemplate;
let precostBuilder, invoiceBuilder;
try {
buildPreCostTemplate = require("../../templates/documents/precost/preCost.template");
buildInvoiceTemplate = require("../../templates/documents/invoice/invoice.template");
buildDispatchNoteTemplate = require("../../templates/documents/dispatchNote/dispatchNote.template");
buildDeliveryNoteTemplate = require("../../templates/documents/deliveryNote/deliveryNote.template");
buildPOTemplate = require("../../templates/documents/po/po.template");
buildCustomDocumentTemplate = require("../../templates/documents/customDocument/customDocument.template");
buildCreditNoteTemplate = require("../../templates/documents/creditNote/creditNote.template");
precostBuilder = require("./builders/precost.builder");
invoiceBuilder = require("./builders/invoice.builder");
console.log("✅ All document templates loaded successfully");
} catch (error) {
console.error("❌ Error loading document templates:", error.message);
console.error(error.stack);
}
const registry = {
precost: {
pdfTemplate: buildPreCostTemplate,
excelBuilder: precostBuilder,
},
invoice: {
pdfTemplate: buildInvoiceTemplate,
excelBuilder: invoiceBuilder,
},
dispatchnote: {
pdfTemplate: buildDispatchNoteTemplate,
excelBuilder: null,
},
deliverynote: {
pdfTemplate: buildDeliveryNoteTemplate,
excelBuilder: null,
},
po: {
pdfTemplate: buildPOTemplate,
excelBuilder: null,
},
customdocument: {
pdfTemplate: buildCustomDocumentTemplate,
excelBuilder: null,
},
creditnote: {
pdfTemplate: buildCreditNoteTemplate,
excelBuilder: null,
},
};
console.log("📋 Registry initialized with keys:", Object.keys(registry));
module.exports = registry;
+93
View File
@@ -0,0 +1,93 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/middleware/auth.middleware.js
const { verifyToken } = require("../utils/jwt.util");
const { getEffectivePermissions } = require("../services/permission.service");
const authenticate = async (req, res, next) => {
try {
let token = null;
// Get token from cookie
if (req.cookies?.access_token) {
token = req.cookies.access_token;
}
// Fallback to Bearer token
if (!token && req.headers.authorization?.startsWith("Bearer ")) {
token = req.headers.authorization.split(" ")[1];
}
if (!token) {
return res.status(401).json({
success: false,
message: "Unauthorized",
});
}
// Verify token
const decoded = verifyToken(token);
if (process.env.NODE_ENV === "development") {
console.log("DECODED:", decoded);
}
const userId = decoded.sub || decoded.id;
if (!userId) {
throw new Error("User ID missing in token");
}
// Load permissions
const permissions = await getEffectivePermissions(userId);
req.user = {
...decoded,
id: userId,
permissions,
};
next();
} catch (err) {
console.error("AUTH ERROR:", err);
// Clear invalid/expired cookie
res.clearCookie("access_token", {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: process.env.NODE_ENV === "production" ? "None" : "Lax",
});
// Token expired
if (err.name === "TokenExpiredError") {
return res.status(401).json({
success: false,
message: "Session expired. Please login again.",
});
}
// Invalid token
if (err.name === "JsonWebTokenError") {
return res.status(401).json({
success: false,
message: "Invalid token",
});
}
// Default
return res.status(401).json({
success: false,
message: "Authentication failed",
});
}
};
module.exports = { authenticate };
+21
View File
@@ -0,0 +1,21 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/middleware/docsSession.middleware.js
module.exports = (req, res, next) => {
if (req.cookies && req.cookies.docsAuth === "true") {
return next();
}
return res.status(401).json({
success: false,
message: "Unauthorized: Please login to access docs",
});
};
+100
View File
@@ -0,0 +1,100 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/middleware/permission.middleware.js
const hasPermission = (userPermissions, requiredPermission) => {
return userPermissions.some(p => {
if (p === requiredPermission) return true;
// wildcard support
if (p.endsWith(".*")) {
const prefix = p.slice(0, -2);
return requiredPermission.startsWith(prefix);
}
return false;
});
};
const methodToAction = {
GET: "view",
POST: "create",
PUT: "update",
PATCH: "update",
DELETE: "delete"
};
const checkPermission = (baseOrFull, options = {}) => {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({
success: false,
message: "Unauthorized"
});
}
// admin bypass
if (req.user.accountType === "admin") {
return next();
}
if (typeof baseOrFull !== "string") {
return res.status(500).json({
success: false,
message: "Permission must be a string"
});
}
let requiredPermission;
if (options.custom) {
requiredPermission = baseOrFull;
} else {
const action = methodToAction[req.method];
if (!action) {
return res.status(500).json({
success: false,
message: "Unknown HTTP method"
});
}
requiredPermission = `${baseOrFull}.${action}`;
}
const userPermissions = req.user.permissions || [];
const hasPermission =
userPermissions.includes(requiredPermission) ||
userPermissions.includes(`${baseOrFull}.*`);
if (!hasPermission) {
return res.status(403).json({
success: false,
message: `Forbidden - Missing ${requiredPermission}`
});
}
next();
};
};
const authorizedAccountType = (allowedTypes) => {
return (req, res, next) => {
if (!req.user || !allowedTypes.includes(req.user.accountType)) {
return res
.status(403)
.json({ success: false, message: "Forbidden" });
}
next();
}
}
module.exports = {authorizedAccountType, checkPermission};
+64
View File
@@ -0,0 +1,64 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/middlewares/upload.middleware.js
const multer = require("multer");
// Use memory storage (required for S3 upload)
const storage = multer.memoryStorage();
// File filter (only images + PDFs)
const fileFilter = (req, file, cb) => {
const allowedTypes = [
"image/",
"application/pdf",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", // .xlsx
"application/vnd.ms-excel" // .xls
];
const isValid =
file.mimetype.startsWith("image/") ||
file.mimetype === "application/pdf" ||
file.mimetype === "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" ||
file.mimetype === "application/vnd.ms-excel";
if (isValid) {
cb(null, true);
} else {
cb(new Error("Only images, PDFs, and Excel files are allowed"), false);
}
};
// Multer instance
const upload = multer({
storage,
fileFilter,
limits: {
fileSize: 5 * 1024 * 1024, // max 5MB (global limit)
},
});
// Middleware wrappers
// Single file upload
const uploadSingle = (fieldName) => upload.single(fieldName);
// Multiple files (same field)
const uploadMultiple = (fieldName, maxCount = 5) =>
upload.array(fieldName, maxCount);
// Multiple fields (advanced)
const uploadFields = (fields) => upload.fields(fields);
module.exports = {
uploadSingle,
uploadMultiple,
uploadFields,
};
@@ -0,0 +1,57 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/models/userActivities.model.js
module.exports = (sequelize, DataTypes) => {
const UserActivity = sequelize.define(
"UserActivity",
{
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true,
},
user_id: {
type: DataTypes.STRING,
allowNull: false,
},
username: {
type: DataTypes.STRING,
allowNull: false,
},
activity_description: {
type: DataTypes.STRING,
allowNull: false,
},
activity_type: {
type: DataTypes.STRING,
allowNull: true,
},
module:{
type: DataTypes.STRING,
allowNull: true,
},
activity_time: {
type: DataTypes.DATE,
allowNull: false,
},
activity_date:{
type: DataTypes.DATEONLY,
allowNull: false,
}
},
{
tableName: "UserActivity",
timestamps: true,
},
);
return UserActivity;
};
+46
View File
@@ -0,0 +1,46 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/models/document/document.model.js
module.exports = (sequelize, DataTypes) => {
const document = sequelize.define(
"Document",
{
doc_id:{
type: DataTypes.STRING,
primaryKey: true,
},
reference_no: {
type: DataTypes.STRING,
allowNull: false,
},
doc_type: {
type: DataTypes.STRING,
allowNull: true,
defaultValue: "N/A"
},
data: {
type: DataTypes.JSON,
allowNull: false,
},
status: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: "DRAFT",
}
},
{
tableName: "Document",
timestamps: true,
},
);
return document;
};
+38
View File
@@ -0,0 +1,38 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/models/document/documentType.model.js
module.exports = (sequelize, DataTypes) => {
const documentType = sequelize.define(
"DocumentType",
{
id:{
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true,
},
doc_type_name: {
type: DataTypes.STRING,
allowNull: true,
defaultValue: "N/A"
},
description: {
type: DataTypes.JSON,
allowNull: false,
},
},
{
tableName: "DocumentType",
timestamps: true,
},
);
return documentType;
};
+77
View File
@@ -0,0 +1,77 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/models/index.js
const { Sequelize, DataTypes } = require("sequelize");
const dbConfig = require("../config/db.config");
let loggingOption = false;
if(process.env.NODE_ENV === 'production') {
loggingOption = true;
}
const sequelize = new Sequelize(
dbConfig.DB,
dbConfig.USER,
dbConfig.PASSWORD,
{
host: dbConfig.HOST,
port: dbConfig.PORT,
dialect: dbConfig.DIALECT,
pool: dbConfig.pool,
logging: loggingOption
}
);
const db = {};
db.Sequelize = Sequelize;
db.sequelize = sequelize;
/* Register db models */
// User and Authentication
db.User = require("./user/user.model")(sequelize, DataTypes);
db.UserActivity = require("./activities/userActivities.model")(sequelize, DataTypes);
db.Profile = require("./user/profile.model")(sequelize, DataTypes);
// Uploads
db.Upload = require("./upload/upload.model")(sequelize, DataTypes);
// Roles and Permissions
db.roles = require("./permission/role.model")(sequelize, DataTypes);
db.permission = require("./permission/permission.model")(sequelize, DataTypes);
db.rolePermission = require("./permission/rolePermission.model")(sequelize, DataTypes);
db.userPermission = require("./permission/userPermission.model")(sequelize, DataTypes);
// Document Management
db.Document = require("./document/document.model")(sequelize, DataTypes);
db.DocumentType = require("./document/documentType.model")(sequelize, DataTypes);
// Reference Numbers
db.referenceNumber = require("./referenceNumbers/referenceNumber.model")(sequelize, DataTypes);
// Notifications
db.notification = require("./notification/notification.model")(sequelize, DataTypes);
db.userNotification = require("./notification/userNotification.model")(sequelize, DataTypes);
/* Associations */
Object.keys(db).forEach(model => {
if (db[model].associate) {
db[model].associate(db);
}
});
module.exports = db;
@@ -0,0 +1,55 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/models/notification/notification.model.js
module.exports = (sequelize, DataTypes) => {
const notification = sequelize.define(
"notification",
{
notification_id: {
type: DataTypes.STRING,
primaryKey: true,
},
notificationHeadline: {
type: DataTypes.STRING,
allowNull: false,
},
notificationDescription: {
type: DataTypes.TEXT,
allowNull: true,
},
notificationType: {
type: DataTypes.ENUM("USER", "ANNOUNCEMENT"),
allowNull: false,
},
isActive: {
type: DataTypes.BOOLEAN,
defaultValue: true,
},
dateCreated: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW,
},
},
{
tableName: "notification",
timestamps: true,
},
);
notification.associate = (models) => {
notification.hasMany(models.userNotification, {
foreignKey: "notification_id",
as: "users",
});
};
return notification;
};
@@ -0,0 +1,54 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/models/notification/userNotification.model.js
module.exports = (sequelize, DataTypes) => {
const userNotification = sequelize.define(
"userNotification",
{
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true,
},
user_id: {
type: DataTypes.STRING(255),
allowNull: false,
collate: "utf8mb4_general_ci",
},
notification_id: {
type: DataTypes.STRING,
allowNull: false,
},
isRead: {
type: DataTypes.BOOLEAN,
defaultValue: false,
},
},
{
tableName: "user_notification",
timestamps: true,
},
);
userNotification.associate = (models) => {
userNotification.belongsTo(models.User, {
foreignKey: "user_id",
constraints: false,
});
userNotification.belongsTo(models.notification, {
foreignKey: "notification_id",
constraints: false,
});
};
return userNotification;
};
+59
View File
@@ -0,0 +1,59 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/models/permission/permission.model.js
module.exports = (sequelize, DataTypes) => {
const permission = sequelize.define(
"permission",
{
permission_id: {
type: DataTypes.STRING,
primaryKey: true,
},
permissionName: {
type: DataTypes.STRING,
allowNull: false,
},
permissionDescription: {
type: DataTypes.TEXT,
allowNull: true,
},
page:{
type: DataTypes.STRING,
allowNull: false,
},
module:{
type: DataTypes.STRING,
allowNull: false,
},
action: {
type: DataTypes.STRING,
allowNull: false,
}
},
{
tableName: "permission",
timestamps: true,
},
);
permission.associate = (db) => {
permission.hasMany(db.userPermission, {
foreignKey: "permission_id",
as: "userPermissions"
});
permission.hasMany(db.rolePermission, {
foreignKey: "permission_id",
as: "rolePermissions"
});
};
return permission;
};
+43
View File
@@ -0,0 +1,43 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/models/permission/role.model.js
module.exports = (sequelize, DataTypes) => {
const roles = sequelize.define(
"roles",
{
role_id: {
type: DataTypes.STRING,
primaryKey: true
},
roleName: {
type: DataTypes.STRING,
allowNull: false
},
roleDescription: {
type: DataTypes.TEXT,
allowNull: true
}
},
{
tableName: "roles",
timestamps: true
}
);
roles.associate = (db) => {
roles.hasMany(db.rolePermission, {
foreignKey: "role_id",
as: "rolePermissions"
});
};
return roles;
};
@@ -0,0 +1,47 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/models/permission/rolePermission.model.js
module.exports = (sequelize, DataTypes) => {
const rolePermission = sequelize.define(
"rolePermission",
{
rp_id: {
type: DataTypes.STRING,
primaryKey: true
},
role_id: {
type: DataTypes.STRING,
allowNull: false
},
permission_id: {
type: DataTypes.STRING,
allowNull: false
}
},
{
tableName: "rolePermission",
timestamps: true
}
);
rolePermission.associate = (db) => {
rolePermission.belongsTo(db.roles, {
foreignKey: "role_id",
as: "role"
});
rolePermission.belongsTo(db.permission, {
foreignKey: "permission_id",
as: "permission"
});
};
return rolePermission;
};
@@ -0,0 +1,48 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/models/permission/userPermission.model.js
module.exports = (sequelize, DataTypes) => {
const userPermission = sequelize.define(
"userPermission",
{
up_id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
user_id: {
type: DataTypes.STRING,
allowNull: false
},
permission_id: {
type: DataTypes.STRING,
allowNull: false
}
},
{
tableName: "userPermission",
timestamps: true
}
);
userPermission.associate = (db) => {
userPermission.belongsTo(db.permission, {
foreignKey: "permission_id",
as: "permission"
});
userPermission.belongsTo(db.User, {
foreignKey: "user_id",
as: "user"
});
};
return userPermission;
};
@@ -0,0 +1,45 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/models/referenceNumbers/referenceNumber.model.js
module.exports = (sequelize, DataTypes) => {
const ReferenceNumber = sequelize.define(
"ReferenceNumber",
{
id: {
type: DataTypes.STRING,
primaryKey: true,
},
// Unique sequence scope
sequence_key: {
type: DataTypes.STRING,
allowNull: false,
unique: true,
},
current_number: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0,
},
last_ref_number: {
type: DataTypes.STRING,
},
},
{
tableName: "referenceNumbers",
timestamps: true,
}
);
return ReferenceNumber;
};
+54
View File
@@ -0,0 +1,54 @@
/**
* © 2026 Niolla. All rights reserved.
*
* This file is part of the Niolla software project and is intended for internal use only.
* Unauthorized copying, modification, distribution, or disclosure of this file,
* via any medium, is strictly prohibited without written permission from Niolla.
*
* For inquiries, contact: support@niolla.lk
*/
// app/models/upload.model.js
module.exports = (sequelize, DataTypes) => {
const upload = sequelize.define(
"upload",
{
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true,
},
file_path: {
type: DataTypes.STRING,
allowNull: false,
},
file_type: {
type: DataTypes.STRING,
allowNull: false,
},
file_size: {
type: DataTypes.INTEGER,
allowNull: false,
},
original_name: {
type: DataTypes.STRING,
allowNull: false,
},
use_for: {
type: DataTypes.STRING,
allowNull: false,
},
uploaded_by: {
type: DataTypes.STRING,
allowNull: false,
},
},
{
tableName: "uploads",
timestamps: true,
},
);
return upload;
};
+66
View File
@@ -0,0 +1,66 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/models/profile.model.js
module.exports = (sequelize, DataTypes) => {
const Profile = sequelize.define(
"Profile",
{
profile_id: {
type: DataTypes.STRING,
primaryKey: true,
},
user_id: {
type: DataTypes.STRING,
allowNull: false,
unique: true,
collate: 'utf8mb4_general_ci'
},
theme: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: "light",
},
notificationsEnabled: {
type: DataTypes.BOOLEAN,
defaultValue: true,
},
profilePicture_id: {
type: DataTypes.STRING,
allowNull: true,
},
backgroundImage_id: {
type: DataTypes.STRING,
allowNull: true,
},
dob: {
type: DataTypes.DATE,
allowNull: true,
},
phone_number:{
type: DataTypes.STRING,
allowNull: true,
}
},
{
tableName: "profiles",
timestamps: true,
},
);
Profile.associate = (db) => {
Profile.belongsTo(db.User, {
foreignKey: "user_id",
as: "user",
});
};
return Profile;
};
+73
View File
@@ -0,0 +1,73 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/models/User.model.js
module.exports = (sequelize, DataTypes) => {
const User = sequelize.define(
"User",
{
id: {
type: DataTypes.STRING,
primaryKey: true,
collate: 'utf8mb4_general_ci'
},
firstName: {
type: DataTypes.STRING,
allowNull: false
},
lastName: {
type: DataTypes.STRING,
allowNull: false
},
email: {
type: DataTypes.STRING,
allowNull: false,
unique: true
},
password: {
type: DataTypes.STRING,
allowNull: false
},
accountType: {
type: DataTypes.ENUM("admin", "management", "team_head", "user"),
defaultValue: "user"
},
role: {
type: DataTypes.STRING,
allowNull: false
},
roleID:{
type: DataTypes.STRING,
allowNull: false
},
department: {
type: DataTypes.STRING,
allowNull: true
}
},
{
tableName: "users",
timestamps: true
}
);
User.associate = (db) => {
User.hasMany(db.userPermission, {
foreignKey: "user_id",
as: "userPermissions"
});
User.hasOne(db.Profile, {
foreignKey: "user_id",
as: "profile",
});
};
return User;
};
+20
View File
@@ -0,0 +1,20 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/queues/activity.queue.js
const { Queue } = require("bullmq");
const connection = require("../config/redisClient");
const activityQueue = new Queue("activity-queue", {
connection,
});
module.exports = activityQueue;
+58
View File
@@ -0,0 +1,58 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/queues/document.queue.js
const { Queue } = require("bullmq");
const redis = require("../config/redisClient");
const documentQueue = new Queue("document-generation", {
connection: redis,
defaultJobOptions: {
attempts: 3,
backoff: {
type: "exponential",
delay: 2000,
},
removeOnComplete: {
age: 3600,
count: 1000,
},
removeOnFail: false,
},
});
// Event listeners
documentQueue.on("error", (err) => {
console.error("Document Queue Error:", err);
});
documentQueue.on("waiting", (job) => {
console.log(`Document Job ${job.id} waiting...`);
});
documentQueue.on("active", (job) => {
console.log(`Document Job ${job.id} started...`);
});
documentQueue.on("completed", (job) => {
console.log(`Document Job ${job.id} completed`);
});
documentQueue.on("failed", (job, err) => {
console.error(
`Document Job ${job.id} failed:`,
err.message
);
});
module.exports = documentQueue;
+17
View File
@@ -0,0 +1,17 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/queues/log.queue.js
const { Queue } = require("bullmq");
const connection = require("../config/redisClient");
const logQueue = new Queue("logQueue", { connection });
module.exports = logQueue;
+40
View File
@@ -0,0 +1,40 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/routes/activity.routes.js
const express = require("express");
const router = express.Router();
const activityController = require("../controllers/activity.controller");
const {
authenticate,
} = require("../middleware/auth.middleware");
const { authorizedAccountType, checkPermission } = require("../middleware/permission.middleware");
const PERMISSIONS = require("../constants/permissions");
// Get all user activities
router.get(
"/",
authenticate,
authorizedAccountType(["admin"]),
activityController.getUserActivities,
);
// Get activity by User ID
router.get(
"/user/:userId",
authenticate,
authorizedAccountType(["admin"]),
activityController.getActivitiesByUserId,
);
module.exports = router;
+29
View File
@@ -0,0 +1,29 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/routes/auth.routes.js
const express = require('express');
const router = express.Router();
const authController = require('../controllers/auth.controller');
const {authenticate} = require('../middleware/auth.middleware');
// GET /api/auth/me
router.get("/me", authenticate, (req, res) => {
res.json({
authenticated: true,
user: req.user
});
});
router.post('/req-otp', authController.loginReq);
router.post('/login', authController.login);
router.post('/logout', authController.logout);
module.exports = router;
+77
View File
@@ -0,0 +1,77 @@
/**
* Markdown Docs Route
*/
const express = require("express");
const fs = require("fs");
const path = require("path");
const docsSession = require("../middleware/docsSession.middleware");
const router = express.Router();
const DOCS_DIR = path.join(__dirname, "../../Documentation");
router.get("/markdown-files", docsSession, (req, res) => {
fs.readdir(DOCS_DIR, (err, files) => {
if (err) {
return res.status(500).json({
success: false,
message: "Failed to read documentation folder",
error: err.message,
});
}
const mdFiles = files.filter((file) => file.endsWith(".md"));
res.json({
success: true,
data: mdFiles,
});
});
});
router.get("/view/:file", docsSession, (req, res) => {
const filePath = path.join(DOCS_DIR, req.params.file);
if (!req.params.file.endsWith(".md")) {
return res.status(400).send("Only markdown files allowed");
}
// basic protection against path traversal
if (!filePath.startsWith(DOCS_DIR)) {
return res.status(400).send("Invalid path");
}
res.sendFile(filePath);
});
// Docs login (simple)
router.post("/login", (req, res) => {
const { username, password } = req.body;
if (
username === process.env.DOCS_USER &&
password === process.env.DOCS_PASS
) {
res.cookie("docsAuth", "true", {
httpOnly: true,
sameSite: "lax",
secure: process.env.NODE_ENV === "production",
});
return res.json({ success: true });
}
return res.status(401).json({
success: false,
message: "Invalid credentials",
});
});
router.post("/logout", (req, res) => {
res.clearCookie("docsAuth");
res.json({ success: true });
});
module.exports = router;
+94
View File
@@ -0,0 +1,94 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/routes/document.routes.js
const express = require("express");
const router = express.Router();
const generateDocumentController = require("../controllers/generateDocument.controller");
const {
authenticate,
} = require("../middleware/auth.middleware");
const { authorizedAccountType, checkPermission } = require("../middleware/permission.middleware");
const PERMISSIONS = require("../constants/permissions");
// Get available document types
router.get(
"/types",
authenticate,
authorizedAccountType(["admin", "management", "team_head", "user"]),
generateDocumentController.getAvailableDocumentTypes
);
// Get saved documents
router.post(
"/saved",
authenticate,
authorizedAccountType(["admin", "management", "team_head", "user"]),
generateDocumentController.getSavedDocuments
);
// Generate Document (async - returns jobId immediately)
router.post(
"/generate",
authenticate,
authorizedAccountType(["admin", "management", "team_head", "user"]),
generateDocumentController.generateDocument
);
router.post(
"/draft",
authenticate,
authorizedAccountType(["admin", "management", "team_head", "user"]),
generateDocumentController.generateDraftDocument
);
// Generate Reference Number
router.get(
"/reference-number/:documentType",
authenticate,
authorizedAccountType(["admin", "management", "team_head", "user"]),
generateDocumentController.generateReferenceNo
);
// Get Job Status
router.get(
"/job/:jobId/status",
authenticate,
authorizedAccountType(["admin", "management", "team_head", "user"]),
generateDocumentController.getJobStatus
);
// Download Document
router.get(
"/download/:uuid",
authenticate,
authorizedAccountType(["admin", "management", "team_head", "user"]),
generateDocumentController.downloadDocument
);
// Cancel Job
router.delete(
"/job/:jobId",
authenticate,
authorizedAccountType(["admin", "management", "team_head", "user"]),
generateDocumentController.cancelJob
);
// Get document details
router.get(
"/:docId",
authenticate,
authorizedAccountType(["admin", "management", "team_head", "user"]),
generateDocumentController.getDocumentData
);
module.exports = router;
+35
View File
@@ -0,0 +1,35 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/routes/index.js
const express = require("express");
const authRoutes = require("./auth.routes");
const userRoutes = require("./user.routes");
const activityRoutes = require("./activity.routes");
const uploadRoutes = require("./upload.routes");
const documentRoutes = require("./document.routes");
const docsRoutes = require("./docs.routes");
const permissionRoutes = require("./permission.routes");
const profileRoutes = require("./profile.routes");
const notificationRoutes = require("./notification.routes");
const router = express.Router();
router.use("/auth", authRoutes);
router.use("/user", userRoutes);
router.use("/activity", activityRoutes);
router.use("/upload", uploadRoutes);
router.use("/document", documentRoutes);
router.use("/docs", docsRoutes);
router.use("/profile", profileRoutes);
router.use("/notification", notificationRoutes);
module.exports = router;
+69
View File
@@ -0,0 +1,69 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/routes/notification.routes.js
const express = require("express");
const router = express.Router();
const notificationController = require("../controllers/notification.controller");
const {
authenticate,
} = require("../middleware/auth.middleware");
const {
authorizedAccountType,
checkPermission,
} = require("../middleware/permission.middleware");
const PERMISSIONS = require("../constants/permissions");
// Create notification
router.post(
"/",
authenticate,
authorizedAccountType(["admin", "management"]),
// checkPermission(PERMISSIONS.NOTIFICATION_CREATE),
notificationController.createNotification,
);
// Get all announcements
router.get(
"/announcements",
authenticate,
authorizedAccountType(["admin", "management", "team_head", "user"]),
// checkPermission(PERMISSIONS.NOTIFICATION_VIEW),
notificationController.getAnnouncements,
);
// Get notifications for a user
router.get(
"/user/:userId",
authenticate,
authorizedAccountType(["admin", "management", "team_head", "user"]),
// checkPermission(PERMISSIONS.NOTIFICATION_VIEW),
notificationController.getUserNotifications,
);
// Mark notification as read
router.patch(
"/user/:userId/notification/:notificationId/read",
authenticate,
authorizedAccountType(["admin", "management", "team_head", "user"]),
// checkPermission(PERMISSIONS.NOTIFICATION_READ),
notificationController.markAsRead,
);
module.exports = router;
+145
View File
@@ -0,0 +1,145 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/routes/permission.routes.js
const express = require("express");
const router = express.Router();
const permissionController = require("../controllers/permission.controller");
const { authenticate } = require("../middleware/auth.middleware");
const { authorizedAccountType } = require("../middleware/permission.middleware");
// Permission routes
router.post(
"/",
authenticate,
authorizedAccountType(["admin"]),
permissionController.createPermission
);
router.post(
"/bulk",
authenticate,
authorizedAccountType(["admin"]),
permissionController.bulkCreatePermissions
);
router.get(
"/",
authenticate,
permissionController.getAllPermissions
);
router.get(
"/:permissionId",
authenticate,
permissionController.getPermissionById
);
router.put(
"/:permissionId",
authenticate,
authorizedAccountType(["admin"]),
permissionController.updatePermission
);
router.delete(
"/:permissionId",
authenticate,
authorizedAccountType(["admin"]),
permissionController.deletePermission
);
// Role routes
router.post(
"/roles",
authenticate,
authorizedAccountType(["admin"]),
permissionController.createRole
);
router.get(
"/roles",
authenticate,
permissionController.getAllRoles
);
router.get(
"/roles/:roleId",
authenticate,
permissionController.getRoleById
);
router.put(
"/roles/:roleId",
authenticate,
authorizedAccountType(["admin"]),
permissionController.updateRole
);
router.delete(
"/roles/:roleId",
authenticate,
authorizedAccountType(["admin"]),
permissionController.deleteRole
);
// Role-Permission assignment route
router.post(
"/roles/:roleId/permissions",
authenticate,
authorizedAccountType(["admin"]),
permissionController.createRolePermission
);
// Bulk Role-Permission assignment
router.post(
"/roles/permissions/bulk",
authenticate,
authorizedAccountType(["admin"]),
permissionController.bulkCreateRolePermissions
);
// User permissions route
router.get(
"/users/:userId/permissions",
authenticate,
permissionController.getUserPermissions
);
router.post(
"/users/permissions",
authenticate,
authorizedAccountType(["admin"]),
permissionController.createUserPermission
);
// Bulk User Permissions
router.post(
"/users/permissions/bulk",
authenticate,
authorizedAccountType(["admin"]),
permissionController.bulkCreateUserPermissions
);
router.put(
"/users/permissions/:upId",
authenticate,
authorizedAccountType(["admin"]),
permissionController.updateUserPermission
);
router.delete(
"/users/permissions/:upId",
authenticate,
authorizedAccountType(["admin"]),
permissionController.deleteUserPermission
);
module.exports = router;
+48
View File
@@ -0,0 +1,48 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/routes/profile.routes.js
const express = require("express");
const router = express.Router();
const profileController = require("../controllers/profile.controller.js");
const { authenticate } = require("../middleware/auth.middleware");
const {
authorizedAccountType,
checkPermission,
} = require("../middleware/permission.middleware");
const PERMISSIONS = require("../constants/permissions");
router.post("/req-reset-password", profileController.requestPasswordReset);
router.post("/reset-password", profileController.resetPassword);
router.post(
"/change-password",
authenticate,
authorizedAccountType(["admin", "management", "team_head", "user"]),
profileController.changePassword,
);
router.get(
"/avatar/:userId",
authenticate,
authorizedAccountType(["admin", "management", "team_head", "user"]),
profileController.getProfileAvatar,
);
router.get(
"/background/:userId",
authenticate,
authorizedAccountType(["admin", "management", "team_head", "user"]),
profileController.getProfileBackgroundImage,
);
module.exports = router;
+40
View File
@@ -0,0 +1,40 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/routes/upload.routes.js
const express = require("express");
const router = express.Router();
const uploadController = require("../controllers/upload.controller");
const { uploadSingle } = require("../middleware/upload.middleware");
const {
authenticate,
} = require("../middleware/auth.middleware");
const { authorizedAccountType, checkPermission } = require("../middleware/permission.middleware");
const PERMISSIONS = require("../constants/permissions");
// Upload file
router.post(
"/",
authenticate,
authorizedAccountType(["admin", "staff"]),
uploadSingle("file"),
uploadController.uploadFile,
);
// Get signed URL for a file
router.get(
"/signed-url/:id",
authenticate,
authorizedAccountType(["admin", "staff"]),
uploadController.getFileUrl,
);
module.exports = router;
+58
View File
@@ -0,0 +1,58 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/routes/user.routes.js
const express = require("express");
const router = express.Router();
const userController = require("../controllers/user.controller");
const {
authenticate,
} = require("../middleware/auth.middleware");
const { authorizedAccountType, checkPermission } = require("../middleware/permission.middleware");
const PERMISSIONS = require("../constants/permissions");
router.post(
"/",
authenticate,
authorizedAccountType(["admin"]),
userController.createNewUser
);
router.get(
"/",
authenticate,
authorizedAccountType(["admin"]),
userController.getAllUsers
);
router.get(
"/:id",
authenticate,
authorizedAccountType(["admin", "management", "team_head", "user"]),
userController.getUserById
);
router.patch(
"/:id",
authenticate,
authorizedAccountType(["admin", "management", "team_head", "user"]),
userController.updateUser
);
router.delete(
"/:id",
authenticate,
authorizedAccountType(["admin"]),
userController.deleteUser
);
module.exports = router;
+46
View File
@@ -0,0 +1,46 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/seeders/documentType.seeder.js
const db = require("../models");
const seedDocumentTypes = async () => {
try {
const types = [
{ doc_type_name: "DOC_TYPE_HERE_01", description: "N/A" },
{ doc_type_name: "DOC_TYPE_HERE_02", description: "N/A" },
];
const existingDocuments = await db.DocumentType.findAll({
attributes: ["doc_type_name"],
});
const existingNames = existingDocuments.map((doc) => doc.doc_type_name);
const newDocuments = types.filter(
(dt) => !existingNames.includes(dt.doc_type_name)
);
if (newDocuments.length > 0) {
await db.DocumentType.bulkCreate(newDocuments);
newDocuments.forEach((dt) =>
console.log(`Document type inserted: ${dt.doc_type_name}`)
);
} else {
console.log("No new document types to insert");
}
} catch (error) {
console.error("Document type seeding failed:", error);
throw error;
}
};
module.exports = seedDocumentTypes;
+35
View File
@@ -0,0 +1,35 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/seeders/index.js
const db = require("../models");
// Import all seeders
const seedUsers = require("./user.seeder");
const seedDocumentTypes = require("./documentType.seeder");
const {log} = require("../utils/consoleLog.utill");
const runSeeders = async () => {
try {
log("Starting database seeding...");
await seedUsers();
await seedDocumentTypes();
log("All seeders executed successfully");
} catch (error) {
log("Seeding process failed:", error.message);
} finally {
await db.sequelize.close();
process.exit();
}
};
runSeeders();
+106
View File
@@ -0,0 +1,106 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/seeders/user.seeder.js
const bcrypt = require("bcrypt");
const { v4: uuidv4 } = require("uuid");
const db = require("../models");
const seedUsers = async () => {
const transaction = await db.sequelize.transaction();
try {
const users = [
{
firstName: "System",
lastName: "Administrator",
email: "team@niolla.lk",
password: "Admin@123",
accountType: "admin",
role: "Administrator",
roleID: "admin",
department: "Administration",
profile: {
theme: "light",
notificationsEnabled: true,
dob: null,
phone_number: null,
},
},
];
for (const userData of users) {
const existingUser = await db.User.findOne({
where: {
email: userData.email,
},
transaction,
});
if (existingUser) {
console.log(`User already exists: ${userData.email}`);
continue;
}
const hashedPassword = await bcrypt.hash(userData.password, 12);
const userId = `usr_${uuidv4()}`;
const profileId = `prf_${uuidv4()}`;
const newUser = await db.User.create(
{
id: userId,
firstName: userData.firstName,
lastName: userData.lastName,
email: userData.email,
password: hashedPassword,
accountType: userData.accountType,
role: userData.role,
roleID: userData.roleID,
department: userData.department,
},
{
transaction,
},
);
await db.Profile.create(
{
profile_id: profileId,
user_id: newUser.id,
theme: userData.profile.theme,
notificationsEnabled: userData.profile.notificationsEnabled,
dob: userData.profile.dob,
phone_number: userData.profile.phone_number,
},
{
transaction,
},
);
console.log(
`User inserted: ${newUser.firstName} ${newUser.lastName} (${newUser.email})`,
);
}
await transaction.commit();
console.log("User seeding completed successfully");
} catch (error) {
await transaction.rollback();
console.error("User seeding failed:", error);
throw error;
}
};
module.exports = seedUsers;
+35
View File
@@ -0,0 +1,35 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/services/activity.service.js
const activityQueue = require("../queues/activity.queue");
const logActivity = async ({
user,
description,
type = "ACTION",
module
}) => {
try {
await activityQueue.add("log-activity", {
user_id: user.id || user.user_id,
username: user.firstName,
activity_description: description,
module: module,
activity_type: type,
activity_time: new Date(),
activity_date: new Date().toISOString().split("T")[0]
});
} catch (err) {
console.error("Queue push error:", err.message);
}
};
module.exports = { logActivity };
+71
View File
@@ -0,0 +1,71 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/services/permission.service.js
const db = require("../models");
const User = db.User;
const RolePermission = db.rolePermission;
const UserPermission = db.userPermission;
const {
getCachedPermissions,
setCachedPermissions
} = require("../utils/cache.util");
const getEffectivePermissions = async (userId) => {
try {
const cached = await getCachedPermissions(userId);
if (cached) {
console.log("⚡ PERMISSION CACHE HIT");
return cached;
}
console.log("🐢 PERMISSION CACHE MISS");
const user = await User.findByPk(userId);
if (!user) return [];
const rolePermissions = await RolePermission.findAll({
where: { role_id: user.roleID },
attributes: ["permission_id"]
});
const userPermissions = await UserPermission.findAll({
where: { user_id: userId },
attributes: ["permission_id"]
});
const map = {};
// role permissions
rolePermissions.forEach((rp) => {
map[rp.permission_id] = true;
});
// user overrides (just add)
userPermissions.forEach((up) => {
map[up.permission_id] = true;
});
const finalPermissions = Object.keys(map);
console.log("FINAL PERMS:", finalPermissions);
await setCachedPermissions(userId, finalPermissions);
return finalPermissions;
} catch (err) {
console.error("PERMISSION SERVICE ERROR:", err);
return [];
}
};
module.exports = { getEffectivePermissions };
@@ -0,0 +1,372 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
@page {
size: A4;
margin: 0;
}
body {
background: white;
font-family: Arial, Helvetica, sans-serif;
padding: 10px;
}
.sheet {
width: 100%;
margin: 0 auto;
background: white;
border: 1px solid #222;
}
.company-band {
display: grid;
grid-template-columns: 1fr 1.35fr 0.95fr;
min-height: 112px;
border-bottom: 1px solid #444;
}
.company-left,
.company-middle,
.company-right {
padding: 4px 6px;
font-size: 10px;
}
.company-left {
line-height: 1.35;
}
.company-middle {
text-align: center;
display: flex;
align-items: center;
justify-content: center;
}
.logo-placeholder {
font-size: 28px;
font-weight: 700;
letter-spacing: 6px;
color: #1c5c9d;
}
.company-middle .tagline {
margin-top: 4px;
font-size: 10px;
color: #555;
}
.company-right {
display: flex;
align-items: flex-start;
justify-content: flex-end;
text-align: right;
line-height: 1.25;
}
.company-right .stack {
max-width: 100%;
}
.title-row {
border-bottom: 1px solid #444;
text-align: center;
padding: 5px 0 4px;
color: #7da3de;
font-size: 22px;
font-weight: 700;
}
.bill-row {
display: grid;
grid-template-columns: 1.55fr 1fr;
border-bottom: 1px solid #444;
min-height: 72px;
}
.bill-left,
.bill-right {
padding: 4px 6px;
font-size: 10px;
}
.bill-left {
border-right: 1px solid #444;
line-height: 1.25;
}
.bill-right {
display: flex;
flex-direction: column;
justify-content: center;
gap: 2px;
line-height: 1.2;
}
.bill-line {
display: flex;
justify-content: space-between;
gap: 6px;
}
.meta-row {
display: grid;
grid-template-columns: 1fr 1fr;
border-bottom: 1px solid #444;
}
.meta-col {
padding: 4px 6px;
font-size: 10px;
line-height: 1.25;
}
.meta-col:first-child {
border-right: 1px solid #444;
}
.meta-line {
display: flex;
justify-content: space-between;
gap: 8px;
}
.meta-label {
font-weight: 700;
min-width: 112px;
}
.items-table {
width: 100%;
border-collapse: collapse;
table-layout: fixed;
}
.items-table th,
.items-table td {
border: 1px solid #444;
font-size: 10px;
padding: 2px 3px;
vertical-align: middle;
}
.items-table th {
text-align: center;
font-weight: 700;
background: #b7d2ea;
}
.no-col { width: 5%; }
.desc-col { width: 37%; }
.remarks-col { width: 33%; }
.amount-col { width: 12%; }
.confirm-col { width: 13%; }
.center {
text-align: center;
}
.right {
text-align: right;
}
.spacer-row td {
height: 26px;
}
.total-row td {
font-weight: 700;
background: #f5f5f5;
}
.approval-area {
height: 94px;
border-top: 1px solid #444;
display: flex;
align-items: flex-end;
justify-content: flex-end;
padding: 10px 12px 14px;
}
.approval-box {
text-align: center;
min-width: 230px;
}
.signature-line {
margin-top: 18px;
border-top: 1px solid #222;
width: 100%;
}
.footer-row {
border-top: 1px solid #444;
text-align: center;
padding: 4px 0 5px;
font-size: 10px;
}
.watermark-line {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
font-size: 10px;
}
.line-dots {
letter-spacing: 3px;
text-align: right;
}
.company-logo-text {
text-align: center;
color: #1c5c9d;
font-size: 14px;
line-height: 1.1;
font-weight: 700;
}
.company-logo-small {
font-size: 10px;
font-weight: 400;
color: #333;
}
.header-stack {
line-height: 1.15;
}
.small-note {
font-size: 9px;
color: #444;
}
.highlight {
background: #ffeb3b;
padding: 0 2px;
}
.multi-line {
white-space: pre-line;
}
</style>
</head>
<body>
<%
const creditCompany = typeof company !== 'undefined' && company ? company : {};
const creditHeader = typeof header !== 'undefined' && header ? header : {};
const creditBillTo = typeof billTo !== 'undefined' && billTo ? billTo : {};
const creditMeta = typeof meta !== 'undefined' && meta ? meta : {};
const creditVessel = typeof vessel !== 'undefined' && vessel ? vessel : {};
const creditOrderMeta = typeof orderMeta !== 'undefined' && orderMeta ? orderMeta : {};
const creditItems = typeof items !== 'undefined' && Array.isArray(items) ? items : [];
const creditApprovedByLabel = typeof approvedByLabel !== 'undefined' && approvedByLabel ? approvedByLabel : 'Approved by GLMS Finance Dept.';
const creditDepartmentLabel = typeof departmentLabel !== 'undefined' && departmentLabel ? departmentLabel : 'GLMS - Accounts Department';
%>
<div class="sheet">
<div class="company-band">
<div class="company-left">
<div><strong><%= creditCompany.name || 'GREEK-LANKA MARITIME SERVICES (PVT) LTD' %></strong></div>
<div><%= creditCompany.addressLine1 || '' %></div>
<div><%= creditCompany.addressLine2 || '' %></div>
<div><%= creditCompany.addressLine3 || '' %></div>
<div><%= creditCompany.postal || '' %></div>
<div><%= creditCompany.contact || '' %></div>
<div><%= creditCompany.br || '' %></div>
<div><%= creditCompany.license || '' %></div>
</div>
<div class="company-middle">
<div>
<div class="company-logo-text">GREEK<br/>LANKA</div>
<div class="company-logo-small">We Serve the Ocean</div>
</div>
</div>
<div class="company-right">
<div class="stack">
<div class="header-stack">
<div style="font-size: 14px; font-weight:700; color:#7da3de; letter-spacing:0.5px;">CREDIT NOTE</div>
</div>
</div>
</div>
</div>
<div class="title-row"><%= creditHeader.title || 'CREDIT NOTE' %></div>
<div class="bill-row">
<div class="bill-left">
<div><strong>Bill To :</strong> <%= creditBillTo.name || 'Master & Owner of MV TEAM VENTURES' %></div>
<div class="multi-line"><%- (creditBillTo.details || '').replace(/\n/g, '<br/>') || 'VRIDHI MARITIME SHIP MANAGEMENT & OPERATION LLC<br/>Office No: 2004, The Prism Tower, Dubai' %></div>
</div>
<div class="bill-right">
<div class="bill-line"><span><strong>CRN NO :</strong></span><span><%= creditMeta.crnNo || 'N/A' %></span></div>
<div class="bill-line"><span><strong>DATE :</strong></span><span><%= creditMeta.date || 'N/A' %></span></div>
</div>
</div>
<div class="meta-row">
<div class="meta-col">
<div class="meta-line"><span class="meta-label">Vessel Name :</span> <span><%= creditVessel.name || 'N/A' %></span></div>
<div class="meta-line"><span class="meta-label">Port :</span> <span><%= creditVessel.port || 'N/A' %></span></div>
<div class="meta-line"><span class="meta-label">Name of Agent :</span> <span><%= creditVessel.agent || 'N/A' %></span></div>
</div>
<div class="meta-col">
<div class="meta-line"><span class="meta-label">GRT :</span> <span><%= creditVessel.grt || 'N/A' %></span></div>
<div class="meta-line"><span class="meta-label">IMO Number :</span> <span><%= creditVessel.imo || 'N/A' %></span></div>
<div class="meta-line"><span class="meta-label">Port/Country :</span> <span><%= creditVessel.portCountry || 'N/A' %></span></div>
</div>
</div>
<div style="overflow-x:auto;">
<table class="items-table">
<thead>
<tr>
<th class="no-col">DESCRIPTION</th>
<th class="amount-col">AMOUNT</th>
</tr>
</thead>
<tbody>
<% if (creditItems.length > 0) { %>
<% creditItems.forEach((item, index) => { %>
<tr class="spacer-row">
<td>
<%= item.description || item.desc || '' %>
<% if (item.note) { %>
<div class="small-note"><%= item.note %></div>
<% } %>
</td>
<td class="right"><%= (typeof item.amount !== 'undefined') ? item.amount : '' %></td>
</tr>
<% }); %>
<% } %>
<tr class="total-row">
<td class="right">Total Amount</td>
<td class="right"><%= total || totalAmount || 0 %></td>
</tr>
</tbody>
</table>
</div>
<div class="approval-area">
<div class="approval-box">
<div style="height: 42px;"></div>
<div class="signature-line"></div>
<div style="margin-top: 4px;"><%= creditApprovedByLabel %></div>
</div>
</div>
<div class="footer-row">
<%= creditDepartmentLabel %>
</div>
</div>
</body>
</html>
@@ -0,0 +1,131 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/templates/documents/creditNote/creditNote.template.js
const { log } = require("../../../utils/consoleLog.utill");
module.exports = async (data) => {
log("📋 Preparing Credit Note data for PDF...");
let payload = data;
if (typeof payload === "string") {
try {
payload = JSON.parse(payload);
} catch (error) {
payload = {};
}
}
const source = payload.documentData || payload.document_data || payload.data || payload;
const pick = (...keys) => {
for (const key of keys) {
const value = source?.[key];
if (value !== undefined && value !== null && value !== "") {
return value;
}
}
return "";
};
// Normalize incoming fields and provide defaults matching the screenshot
const company = {
name: pick("companyName", "company_name") || "GREEK-LANKA MARITIME SERVICES (PVT) LTD",
addressLine1: pick("companyAddressLine1", "company_address_line1") || "No. 56/2, Dharmapala Mw, Kotte",
addressLine2: pick("companyAddressLine2", "company_address_line2") || "Sri Jayewardenepura",
addressLine3: pick("companyAddressLine3", "company_address_line3") || "Sri Lanka",
postal: pick("companyPostal", "company_postal") || "Postal Code : 10100",
contact: pick("companyContact", "company_contact") || "Tel:+94 11 2083206 / Mobile (24/7) : +94 777 232 271",
br: pick("companyBR", "company_br") || "BR No : PV 0022630",
license: pick("companyLicense", "company_license") || "License No : SA00317-2024",
logoUrl: pick("logoUrl", "logo_url") ||
"https://res.cloudinary.com/dtthuvvir/image/upload/v1780748661/company_logo_transperent_bg_uullzw.png",
};
const header = {
title: pick("title") || "CREDIT NOTE",
};
const billTo = {
name: pick("billToName", "bill_to_name") || "Master & Owner of MV TEAM VENTURES",
details: pick("billToDetails", "bill_to_details") || "VRIDHI MARITIME SHIP MANAGEMENT & OPERATION LLC\nOffice No: 2004, The Prism Tower, Dubai",
};
const meta = {
crnNo: pick("crnNo", "crn_no", "creditNoteNumber", "credit_note_number", "reference_no") || "GLMS/COLOMBO/474/CRN01",
date: pick("date") || new Date().toLocaleDateString(),
};
const vessel = {
name: pick("vesselName", "vessel_name") || 'MV "TEAM VENTURES"',
grt: pick("grt", "GRT") || "25,543 MT",
port: pick("port", "port_name") || "REPAIRS AT COLOMBO DOCK YARD",
imo: pick("imo", "imo_number", "imoNumber") || "9339765",
agent: pick("nameOfAgent", "name_of_agent", "agent") || "",
portCountry: pick("portCountry", "port_country") || "COLOMBO / SRI LANKA",
};
const orderMeta = {
orderDate: pick("orderDate", "order_date") || meta.date,
orderNo: pick("orderNo", "order_no") || "",
portReference: pick("portReference", "port_reference") || "",
};
// Items: array of { description, amount }
const items = (Array.isArray(source.items) ? source.items : []).map((it) => ({
description: it.description || it.desc || it.item_name || '',
amount: Number(it.amount || it.value || it.total || it.totalAmount || 0),
quantity: it.quantity ?? it.qty ?? '',
note: it.note || it.notes || '',
}));
const computedTotal = items.reduce((s, i) => s + (i.amount || 0), 0);
const templateData = {
company,
header,
billTo,
meta,
vessel,
orderMeta,
items,
totalAmount: Number(pick("totalAmount", "total") || computedTotal),
approvedBy: pick("approvedBy", "approved_by") || 'GLMS Finance Dept.',
approvedByLabel: pick("approvedByLabel", "approved_by_label") || 'Approved by GLMS Finance Dept.',
departmentLabel: pick("departmentLabel", "department_label") || 'GLMS - Accounts Department',
};
// Top-level conveniences for templates and pdfRenderer
templateData.logoUrl = company.logoUrl;
templateData.creditNoteNumber = templateData.meta.crnNo;
templateData.date = templateData.meta.date;
templateData.billToName = templateData.billTo.name;
templateData.billToDetails = templateData.billTo.details;
templateData.vessel = templateData.vessel;
// Provide invoiceNumber if present
templateData.invoiceNumber = pick("invoiceNumber", "invoice_number", "invoice_no") || '';
// Provide subtotal/tax/discount/total for EJS summary table compatibility
templateData.subtotal = Number(pick("subtotal", "sub_total") || computedTotal);
templateData.tax = Number(pick("tax") || 0);
templateData.discount = Number(pick("discount") || 0);
templateData.total = Number(pick("totalAmount", "total") || templateData.subtotal + templateData.tax - templateData.discount);
// Provide reason / notes
templateData.reason = pick("reason", "remarks", "notes") || '';
// Provide paymentTerms and notes for EJS
templateData.paymentTerms = pick("paymentTerms", "payment_terms") || '';
templateData.notes = pick("notes", "note") || '';
log("✅ Credit Note data prepared", JSON.stringify(templateData));
return templateData;
};
@@ -0,0 +1,266 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
@page {
size: A4;
margin: 0;
}
body {
background: #fff;
font-family: Arial, Helvetica, sans-serif;
padding: 8px;
}
.sheet {
width: 100%;
border: 1px solid #222;
background: #fff;
}
.top-band {
display: grid;
grid-template-columns: 1.45fr 1fr;
border-bottom: 1px solid #444;
min-height: 76px;
}
.top-left,
.top-right {
padding: 4px 6px;
font-size: 10px;
}
.top-right {
text-align: right;
line-height: 1.2;
}
.top-line {
margin-bottom: 2px;
}
.highlight-box {
display: grid;
grid-template-columns: 1.6fr 1fr;
border-bottom: 1px solid #444;
}
.highlight-left,
.highlight-right {
padding: 4px 6px;
min-height: 58px;
font-size: 10px;
}
.highlight-left {
border-right: 1px solid #444;
}
.highlight-row {
display: grid;
grid-template-columns: 155px 1fr;
gap: 6px;
margin-bottom: 2px;
}
.label {
font-weight: 700;
}
.permission-line {
text-align: center;
padding: 10px 8px 4px;
font-size: 10px;
}
.dots {
text-align: center;
letter-spacing: 3px;
color: #555;
padding: 2px 0 8px;
}
.company-name {
text-align: center;
font-size: 18px;
font-weight: 700;
padding: 6px 0 10px;
}
.company-subtitle {
text-align: center;
font-size: 13px;
font-weight: 700;
padding-bottom: 10px;
}
.date-row {
display: flex;
justify-content: flex-end;
border-bottom: 1px solid #444;
padding: 0 6px 4px;
}
.date-box {
min-width: 138px;
background: #ffeb3b;
font-weight: 700;
padding: 2px 8px;
text-align: right;
}
.form-code {
font-size: 9px;
padding: 2px 4px 3px;
}
.items-table {
width: 100%;
border-collapse: collapse;
table-layout: fixed;
}
.items-table th,
.items-table td {
border: 1px solid #444;
font-size: 10px;
padding: 2px 3px;
vertical-align: middle;
}
.items-table th {
text-align: center;
font-weight: 700;
background: #fff;
}
.items-table thead th {
height: 33px;
}
.no-col { width: 8%; }
.qty-col { width: 7%; }
.unit-col { width: 7%; }
.description-col { width: 40%; }
.rate-col { width: 13%; }
.total-col { width: 12%; }
.confirm-col { width: 13%; }
.center {
text-align: center;
}
.right {
text-align: right;
}
.spacer-row td {
height: 24px;
}
.muted {
color: #666;
}
</style>
</head>
<body>
<%
const customTitle = typeof title !== 'undefined' && title ? title : 'CUSTOM DOCUMENT';
const customDate = typeof date !== 'undefined' && date ? date : 'N/A';
const customTelePhone = typeof telePhone !== 'undefined' && telePhone ? telePhone : '';
const customEmail = typeof emailAddress !== 'undefined' && emailAddress ? emailAddress : '';
const customOfficeAddressLine1 = typeof officeAddressLine1 !== 'undefined' && officeAddressLine1 ? officeAddressLine1 : '';
const customOfficeAddressLine2 = typeof officeAddressLine2 !== 'undefined' && officeAddressLine2 ? officeAddressLine2 : '';
const customOfficeAddressLine3 = typeof officeAddressLine3 !== 'undefined' && officeAddressLine3 ? officeAddressLine3 : '';
const customDirectorOfCustoms = typeof directorOfCustoms !== 'undefined' && directorOfCustoms ? directorOfCustoms : '';
const customVehicleNo = typeof vehicleNo !== 'undefined' && vehicleNo ? vehicleNo : '';
const customPermitNo = typeof permitNo !== 'undefined' && permitNo ? permitNo : '';
const customChiefSecurityOfficer = typeof chiefSecurityOfficer !== 'undefined' && chiefSecurityOfficer ? chiefSecurityOfficer : '';
const customExportGate = typeof exportGate !== 'undefined' && exportGate ? exportGate : '';
const customSlpa = typeof slpa !== 'undefined' && slpa ? slpa : '';
const customGateNo = typeof gateNo !== 'undefined' && gateNo ? gateNo : '';
const customPermissionText = typeof permissionText !== 'undefined' && permissionText ? permissionText : '';
const customCompanyName = typeof companyName !== 'undefined' && companyName ? companyName : 'Oceanic Maritime Solutions (Pvt) Ltd';
const customSubtitle = typeof subtitle !== 'undefined' && subtitle ? subtitle : 'LICENSED SHIPCHANDLERS';
const customSectionCode = typeof sectionCode !== 'undefined' && sectionCode ? sectionCode : 'SC/FORM/06';
const customDateLabel = typeof dateLabel !== 'undefined' && dateLabel ? dateLabel : 'DATE :';
const customItems = typeof items !== 'undefined' && Array.isArray(items) ? items : [];
const rows = Math.max(7, customItems.length);
%>
<div class="sheet">
<div class="top-band">
<div class="top-left">
<div class="top-line"><span class="label">Tele :</span> <%= customTelePhone || '' %></div>
<div class="top-line"><span class="label">E-mail Address :</span> <%= customEmail || '' %></div>
</div>
<div class="top-right">
<div><%= customOfficeAddressLine1 %></div>
<div><%= customOfficeAddressLine2 %></div>
<div><%= customOfficeAddressLine3 %></div>
</div>
</div>
<div class="highlight-box">
<div class="highlight-left">
<div class="highlight-row"><div class="label">The Director of Customs</div><div><%= customDirectorOfCustoms %></div></div>
<div class="highlight-row"><div class="label">Export Gate</div><div><span style="color:#e53935;">Colombo 1.</span></div></div>
<div class="highlight-row"><div class="label">Vehicle No. / Permit No.</div><div><%= customVehicleNo %> / <%= customPermitNo %></div></div>
</div>
<div class="highlight-right">
<div class="highlight-row"><div class="label">Chief Security Officer</div><div><%= customChiefSecurityOfficer %></div></div>
<div class="highlight-row"><div class="label">SLPA</div><div><%= customSlpa %></div></div>
<div class="highlight-row"><div class="label">Gate No.</div><div><%= customGateNo %></div></div>
</div>
</div>
<div class="permission-line"><%= customPermissionText %></div>
<div class="dots">............................................................</div>
<div class="company-name"><%= customCompanyName %></div>
<div class="company-subtitle"><%= customSubtitle %></div>
<div class="date-row">
<div class="date-box"><%= customDateLabel %> <%= customDate %></div>
</div>
<div class="form-code"><%= customSectionCode %></div>
<table class="items-table">
<thead>
<tr>
<th class="no-col">NO</th>
<th class="qty-col">QTY</th>
<th class="unit-col">UNIT</th>
<th class="description-col">DESCRIPTION</th>
<th class="rate-col">RATE</th>
<th class="total-col">TOTAL</th>
<th class="confirm-col">Confirm<br/>Order Rate</th>
</tr>
</thead>
<tbody>
<% for (let index = 0; index < rows; index++) { %>
<% const item = customItems[index] || {}; %>
<tr class="spacer-row">
<td class="center"><%= index + 1 %></td>
<td class="center"><%= item.quantity || item.qty || '' %></td>
<td class="center"><%= item.unit || '' %></td>
<td><%= item.description || item.item_name || item.name || '' %></td>
<td class="right"><%= item.rate || item.unit_price || '-' %></td>
<td class="right"><%= item.total || item.amount || '-' %></td>
<td><%= item.confirmOrderRate || item.confirm_order_rate || '' %></td>
</tr>
<% } %>
</tbody>
</table>
</div>
</body>
</html>
@@ -0,0 +1,81 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/templates/documents/customDocument/customDocument.template.js
const { log } = require("../../../utils/consoleLog.utill");
module.exports = async (data) => {
log("📋 Preparing Custom Document data for PDF...");
let payload = data;
if (typeof payload === "string") {
try {
payload = JSON.parse(payload);
} catch (error) {
payload = {};
}
}
const source = payload.documentData || payload.document_data || payload.data || payload;
const pick = (...keys) => {
for (const key of keys) {
const value = source?.[key];
if (value !== undefined && value !== null && value !== "") {
return value;
}
}
return "";
};
const templateData = {
logoUrl:
pick("logoUrl", "logo_url") ||
"https://res.cloudinary.com/dtthuvvir/image/upload/v1780748661/company_logo_transperent_bg_uullzw.png",
title: pick("title") || "CUSTOM DOCUMENT",
documentNumber: pick("documentNumber", "document_number") || "",
date: pick("date") || new Date().toLocaleDateString(),
telePhone: pick("telePhone", "telephone", "phone", "tel") || "",
emailAddress: pick("emailAddress", "email", "e_mail") || "",
officeAddressLine1: pick("officeAddressLine1", "office_address_line1") || "",
officeAddressLine2: pick("officeAddressLine2", "office_address_line2") || "",
officeAddressLine3: pick("officeAddressLine3", "office_address_line3") || "",
directorOfCustoms: pick("directorOfCustoms", "director_of_customs") || "",
vehicleNo: pick("vehicleNo", "vehicle_no") || "",
permitNo: pick("permitNo", "permit_no") || "",
chiefSecurityOfficer: pick("chiefSecurityOfficer", "chief_security_officer") || "",
exportGate: pick("exportGate", "export_gate") || "",
slpa: pick("slpa") || "",
gateNo: pick("gateNo", "gate_no") || "",
permissionText: pick("permissionText", "permission_text") || "Please grant permission to pass these Customs entry papers to supply goods to the vessel",
companyName: pick("companyName", "company_name") || "Oceanic Maritime Solutions (Pvt) Ltd",
subtitle: pick("subtitle") || "LICENSED SHIPCHANDLERS",
sectionCode: pick("sectionCode", "section_code") || "SC/FORM/06",
dateLabel: pick("dateLabel", "date_label") || "DATE :",
items: Array.isArray(source.items) ? source.items : [],
itemHeaders:
pick("itemHeaders", "item_headers") ||
["NO", "QTY", "UNIT", "DESCRIPTION", "RATE", "TOTAL", "Confirm Order Rate"],
footerText: pick("footerText", "footer_text") || "",
};
log("✅ Custom Document data prepared");
return templateData;
};
@@ -0,0 +1,189 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
* {
box-sizing: border-box;
}
@page {
margin: 0;
}
body {
margin: 0;
padding: 14px;
font-family: Arial, Helvetica, sans-serif;
background: #fff;
}
.sheet {
width: 100%;
border: 2px solid #2a5aa8;
background: #fff;
}
.logo-row {
height: 82px;
border-bottom: 1px solid #4c4c4c;
display: flex;
align-items: center;
justify-content: center;
padding: 6px 12px 2px;
}
.logo-row img {
max-height: 64px;
object-fit: contain;
}
.title-row {
border-bottom: 1px solid #4c4c4c;
text-align: center;
padding: 6px 0 7px;
color: #2f5fa6;
font-size: 26px;
font-weight: 700;
letter-spacing: 0.5px;
}
.details {
display: grid;
grid-template-columns: 1.6fr 1fr;
gap: 0;
min-height: 84px;
border-bottom: 1px solid #bdbdbd;
}
.left-pane,
.right-pane {
padding: 10px 8px 8px;
}
.left-pane {
border-right: 1px solid #d4d4d4;
}
.label-line {
font-size: 11px;
line-height: 1.35;
font-weight: 700;
text-transform: uppercase;
color: #111;
}
.label-line + .label-line {
margin-top: 4px;
}
.right-pane {
display: flex;
flex-direction: column;
justify-content: center;
gap: 8px;
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
}
.right-row {
display: flex;
justify-content: flex-start;
gap: 6px;
}
.right-row .value {
font-weight: 400;
text-transform: none;
}
.items-table {
width: 100%;
border-collapse: collapse;
table-layout: fixed;
}
.items-table th,
.items-table td {
border: 1px solid #5d5d5d;
font-size: 11px;
padding: 5px 6px;
vertical-align: top;
}
.items-table th {
background: #a7d8df;
color: #111;
text-transform: uppercase;
text-align: center;
font-weight: 700;
}
.no-col { width: 8%; }
.desc-col { width: 36%; }
.remarks-col { width: 25%; }
.unit-col { width: 14%; }
.qty-col { width: 17%; }
.items-table tbody td {
height: 16px;
}
.center {
text-align: center;
}
.right {
text-align: right;
}
</style>
</head>
<body>
<div class="sheet">
<div class="logo-row">
<% if (logoBase64) { %>
<img src="<%= logoBase64 %>" alt="Company Logo" />
<% } %>
</div>
<div class="title-row"><%= title || 'DELIVERY NOTE' %></div>
<div class="details">
<div class="left-pane">
<div class="label-line">Bill To: <span style="font-weight:700;"><%= billToName || 'N/A' %></span></div>
<div class="label-line"><%= billToAddress || 'N/A' %></div>
</div>
<div class="right-pane">
<div class="right-row"><span>Supply Date:</span> <span class="value"><%= supplyDate || 'N/A' %></span></div>
<div class="right-row"><span>PO Number:</span> <span class="value"><%= poNumber || 'N/A' %></span></div>
</div>
</div>
<table class="items-table">
<thead>
<tr>
<th class="no-col">No.</th>
<th class="desc-col">Description</th>
<th class="remarks-col">Remarks</th>
<th class="unit-col">Unit</th>
<th class="qty-col">Quantity</th>
</tr>
</thead>
<tbody>
<% const rows = Math.max(10, (items && items.length) || 0); %>
<% for (let index = 0; index < rows; index++) { %>
<% const item = items && items[index] ? items[index] : {}; %>
<tr>
<td class="center"><%= index + 1 %></td>
<td><%= item.description || item.item_name || item.name || '' %></td>
<td><%= item.remarks || '' %></td>
<td class="center"><%= item.unit || '' %></td>
<td class="right"><%= item.quantity || item.qty || '' %></td>
</tr>
<% } %>
</tbody>
</table>
</div>
</body>
</html>
@@ -0,0 +1,35 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/templates/documents/deliveryNote/deliveryNote.template.js
const { log } = require("../../../utils/consoleLog.utill");
module.exports = async (data) => {
log("📋 Preparing Delivery Note data for PDF...");
const templateData = {
logoUrl:
data.logoUrl ||
"https://res.cloudinary.com/dtthuvvir/image/upload/v1780748661/company_logo_transperent_bg_uullzw.png",
title: data.title || "DELIVERY NOTE",
referenceNumber:
data.referenceNumber || data.reference_no || data.deliveryNoteNumber || data.delivery_note_number || "N/A",
date: data.date || new Date().toLocaleDateString(),
billToName: data.billToName || data.bill_to_name || "N/A",
billToAddress: data.billToAddress || data.bill_to_address || "N/A",
supplyDate: data.supplyDate || data.supply_date || data.date || new Date().toLocaleDateString(),
poNumber: data.poNumber || data.po_number || "N/A",
items: Array.isArray(data.items) ? data.items : [],
};
log("✅ Delivery Note data prepared");
return templateData;
};
@@ -0,0 +1,345 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
@page {
size: A4;
margin: 0;
}
body {
background: white;
font-family: Arial, Helvetica, sans-serif;
padding: 10px;
}
.sheet {
width: 100%;
margin: 0 auto;
background: white;
border: 1px solid #222;
}
.title-bar {
background: #173d64;
color: #fff;
text-align: center;
font-size: 22px;
font-weight: 700;
letter-spacing: 1px;
padding: 4px 0 5px;
border-bottom: 1px solid #222;
}
.top-area {
display: grid;
grid-template-columns: 1.4fr 1fr 0.65fr;
min-height: 95px;
border-bottom: 1px solid #444;
}
.top-col {
padding: 5px 6px;
border-right: 1px solid #444;
font-size: 10px;
}
.top-col:last-child {
border-right: none;
}
.field-grid {
display: flex;
flex-direction: column;
gap: 1px;
}
.field-row {
display: grid;
grid-template-columns: 110px 1fr;
align-items: center;
min-height: 17px;
border: 1px solid #444;
border-bottom: none;
}
.field-row:last-child {
border-bottom: 1px solid #444;
}
.label {
font-weight: 700;
padding: 1px 4px;
}
.value {
padding: 1px 4px;
}
.top-left {
display: flex;
flex-direction: column;
gap: 0;
}
.top-middle {
display: flex;
flex-direction: column;
gap: 0;
}
.top-right {
display: flex;
flex-direction: column;
gap: 0;
}
.meta-grid {
display: grid;
grid-template-columns: 1.4fr 1fr 0.65fr;
border-bottom: 1px solid #444;
}
.meta-box {
min-height: 82px;
padding: 4px 6px;
border-right: 1px solid #444;
font-size: 10px;
}
.meta-box:last-child {
border-right: none;
}
.meta-table {
width: 100%;
border-collapse: collapse;
}
.meta-table td {
border: 1px solid #444;
padding: 2px 4px;
font-size: 10px;
}
.meta-table td.label-cell {
width: 105px;
font-weight: 700;
}
.details-note {
font-size: 9px;
margin-top: 2px;
}
.items-title {
font-size: 12px;
font-weight: 700;
text-align: center;
padding: 2px 0 3px;
border-bottom: 1px solid #444;
background: #efefef;
}
.items-wrap {
overflow-x: auto;
}
.items-table {
width: 100%;
border-collapse: collapse;
table-layout: fixed;
}
.items-table th,
.items-table td {
border: 1px solid #666;
font-size: 10px;
padding: 2px 3px;
vertical-align: middle;
}
.items-table th {
background: #d9d9d9;
font-weight: 700;
text-align: center;
}
.no-col { width: 5%; }
.product-col { width: 24%; }
.qty-col { width: 5%; }
.unit-col { width: 5%; }
.supp-col { width: 6%; }
.pono-col { width: 6%; }
.rcvdqty-col { width: 7%; }
.issuedqty-col { width: 7%; }
.expdate-col { width: 8%; }
.rcvdtime-col { width: 7%; }
.date-col { width: 7%; }
.rejects-col { width: 6%; }
.fatcol-col { width: 6%; }
.remark-col { width: 7%; }
.center {
text-align: center;
}
.right {
text-align: right;
}
.spacer-row td {
height: 21px;
}
.signature-section {
margin-top: 20px;
display: flex;
justify-content: space-between;
gap: 20px;
}
.signature-block {
flex: 1;
text-align: center;
border-top: 1px solid #333;
padding-top: 34px;
font-size: 11px;
}
.footer-note {
text-align: center;
margin-top: 12px;
border-top: 1px solid #ddd;
padding-top: 5px;
}
</style>
</head>
<body>
<%
const dispatchTitle = typeof title !== 'undefined' && title ? title : 'DISPATCH NOTE';
const dispatchReferenceNumber = typeof referenceNumber !== 'undefined' && referenceNumber ? referenceNumber : 'N/A';
const dispatchDate = typeof date !== 'undefined' && date ? date : 'N/A';
const dispatchVessel = typeof vessel !== 'undefined' && vessel ? vessel : 'N/A';
const dispatchCaptain = typeof captain !== 'undefined' && captain ? captain : 'N/A';
const dispatchCook = typeof cook !== 'undefined' && cook ? cook : 'N/A';
const dispatchAgent = typeof agent !== 'undefined' && agent ? agent : 'N/A';
const dispatchDetails = typeof details !== 'undefined' && details ? details : '';
const dispatchPlaceOfDelivery = typeof placeOfDelivery !== 'undefined' && placeOfDelivery ? placeOfDelivery : 'N/A';
const dispatchEta = typeof eta !== 'undefined' && eta ? eta : 'N/A';
const dispatchEtd = typeof etd !== 'undefined' && etd ? etd : 'N/A';
const dispatchNumberOfCrew = typeof numberOfCrew !== 'undefined' && numberOfCrew ? numberOfCrew : 'N/A';
const dispatchNextMainOrderIn = typeof nextMainOrderIn !== 'undefined' && nextMainOrderIn ? nextMainOrderIn : 'N/A';
const dispatchPort = typeof port !== 'undefined' && port ? port : 'N/A';
const dispatchCompany = typeof company !== 'undefined' && company ? company : 'N/A';
const dispatchFileNo = typeof fileNo !== 'undefined' && fileNo ? fileNo : 'N/A';
const dispatchItems = typeof items !== 'undefined' && Array.isArray(items) ? items : [];
%>
<div class="sheet">
<div class="title-bar"><%= dispatchTitle %></div>
<div class="top-area">
<div class="top-col">
<div class="field-grid">
<div class="field-row"><div class="label">Vessel:</div><div class="value"><%= dispatchVessel %></div></div>
<div class="field-row"><div class="label">Captain:</div><div class="value"><%= dispatchCaptain %></div></div>
<div class="field-row"><div class="label">Cook:</div><div class="value"><%= dispatchCook %></div></div>
<div class="field-row"><div class="label">Agent:</div><div class="value"><%= dispatchAgent %></div></div>
<div class="field-row"><div class="label">Details:</div><div class="value"><%= dispatchDetails %></div></div>
</div>
<div class="details-note">SC/FORM/04</div>
</div>
<div class="top-col">
<div class="field-grid">
<div class="field-row"><div class="label">Place of delivery:</div><div class="value"><%= dispatchPlaceOfDelivery %></div></div>
<div class="field-row"><div class="label">ETA:</div><div class="value"><%= dispatchEta %></div></div>
<div class="field-row"><div class="label">ETD:</div><div class="value"><%= dispatchEtd %></div></div>
<div class="field-row"><div class="label">Number of crew:</div><div class="value"><%= dispatchNumberOfCrew %></div></div>
<div class="field-row"><div class="label">Next main order in:</div><div class="value"><%= dispatchNextMainOrderIn %></div></div>
</div>
</div>
<div class="top-col">
<div class="field-grid">
<div class="field-row"><div class="label">Port</div><div class="value"><%= dispatchPort %></div></div>
<div class="field-row"><div class="label">Date</div><div class="value"><%= dispatchDate %></div></div>
<div class="field-row"><div class="label">Company</div><div class="value"><%= dispatchCompany %></div></div>
<div class="field-row"><div class="label">File No</div><div class="value"><%= dispatchFileNo %></div></div>
</div>
</div>
</div>
<div class="items-title">Item Receiving Authorization and Vessel Delivery Oder details</div>
<div class="items-wrap">
<table class="items-table">
<thead>
<tr>
<th class="no-col">No</th>
<th class="product-col">Product</th>
<th class="qty-col">Qty</th>
<th class="unit-col">Unit</th>
<th class="supp-col">SUPP</th>
<th class="pono-col">PO NO</th>
<th class="rcvdqty-col">Rcvd Qty</th>
<th class="issuedqty-col">Issued Qty</th>
<th class="expdate-col">Exp Date</th>
<th class="rcvdtime-col">Rcvd Time</th>
<th class="date-col">Date</th>
<th class="rejects-col">Rejects</th>
<th class="fatcol-col">Fat Con.</th>
<th class="remark-col">Remark</th>
</tr>
</thead>
<tbody>
<% const rows = Math.max(5, dispatchItems.length); %>
<% for (let index = 0; index < rows; index++) { %>
<% const item = dispatchItems[index] || {}; %>
<tr class="spacer-row">
<td class="center"><%= index + 1 %></td>
<td><%= item.product || item.description || item.item_name || item.name || '' %></td>
<td class="center"><%= item.qty || item.quantity || '' %></td>
<td class="center"><%= item.unit || '' %></td>
<td class="center"><%= item.supp || item.supplier || '' %></td>
<td class="center"><%= item.poNo || item.po_number || item.poNumber || '' %></td>
<td class="center"><%= item.receivedQty || item.rcvdQty || '' %></td>
<td class="center"><%= item.issuedQty || '' %></td>
<td class="center"><%= item.expDate || item.exp_date || '' %></td>
<td class="center"><%= item.receivedTime || item.rcvdTime || '' %></td>
<td class="center"><%= item.date || '' %></td>
<td class="center"><%= item.rejects || '' %></td>
<td class="center"><%= item.fatCon || item.fat_con || '' %></td>
<td><%= item.remark || item.remarks || '' %></td>
</tr>
<% } %>
</tbody>
</table>
</div>
<% if (showSignatures) { %>
<div class="signature-section">
<div class="signature-block">
<strong>Prepared By</strong>
<div style="margin-top: 28px;">___________________</div>
<div style="margin-top: 5px;"><%= preparedBy || 'Name' %></div>
</div>
<div class="signature-block">
<strong>Approved By</strong>
<div style="margin-top: 28px;">___________________</div>
<div style="margin-top: 5px;"><%= approvedBy || 'Name' %></div>
</div>
</div>
<% } %>
<div class="footer-note">
<p style="font-size: 10px; color: #999;">This is an official dispatch note</p>
</div>
</div>
</body>
</html>
@@ -0,0 +1,74 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/templates/documents/dispatchNote/dispatchNote.template.js
const { log } = require("../../../utils/consoleLog.utill");
module.exports = async (data) => {
log("📋 Preparing Dispatch Note data for PDF...");
let payload = data;
if (typeof payload === "string") {
try {
payload = JSON.parse(payload);
} catch (error) {
payload = {};
}
}
const source = payload.documentData || payload.document_data || payload.data || payload;
const pick = (...keys) => {
for (const key of keys) {
const value = source?.[key];
if (value !== undefined && value !== null && value !== "") {
return value;
}
}
return "";
};
const templateData = {
logoUrl:
pick("logoUrl", "logo_url") ||
"https://res.cloudinary.com/dtthuvvir/image/upload/v1780748661/company_logo_transperent_bg_uullzw.png",
title: pick("title") || "DISPATCH NOTE",
referenceNumber: pick("referenceNumber", "reference_number", "dnNo", "dn_no") || "N/A",
date: pick("date") || new Date().toLocaleDateString(),
vessel: pick("vessel", "vesselName", "vessel_name") || "",
captain: pick("captain", "captainName", "captain_name") || "",
cook: pick("cook", "cookName", "cook_name") || "",
agent: pick("agent", "agentName", "agent_name") || "",
details: pick("details", "remarks", "note") || "",
placeOfDelivery: pick("placeOfDelivery", "place_of_delivery") || "",
eta: pick("eta") || "",
etd: pick("etd") || "",
numberOfCrew: pick("numberOfCrew", "number_of_crew", "crewCount", "crew_count") || "",
nextMainOrderIn: pick("nextMainOrderIn", "next_main_order_in") || "",
port: pick("port", "portName", "port_name") || "",
company: pick("company", "companyName", "company_name") || "",
fileNo: pick("fileNo", "file_no") || "",
items: Array.isArray(source.items) ? source.items : [],
showSignatures: pick("showSignatures") !== false,
preparedBy: pick("preparedBy", "prepared_by") || "",
approvedBy: pick("approvedBy", "approved_by") || "",
};
log("✅ Dispatch Note data prepared");
return templateData;
};
+308
View File
@@ -0,0 +1,308 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
@page {
size: A4;
margin: 0;
}
body {
background: white;
font-family: Arial, Helvetica, sans-serif;
padding: 12px;
}
.page {
width: 100%;
margin: 0 auto;
background: white;
border: 1px solid #333;
}
.logo-section {
height: 72px;
display: flex;
align-items: center;
justify-content: center;
border-bottom: 1px solid #c9c9c9;
}
.logo-section img {
max-height: 52px;
object-fit: contain;
}
.title-section {
text-align: center;
padding: 5px 0 4px;
border-bottom: 1px solid #333;
font-size: 22px;
font-weight: 700;
color: #2d5c96;
letter-spacing: 0.4px;
}
.top-grid {
display: grid;
grid-template-columns: 1.6fr 1fr;
min-height: 70px;
border-bottom: 1px solid #c9c9c9;
}
.top-left,
.top-right,
.mid-left,
.mid-right {
padding: 4px 6px;
font-size: 10px;
color: #111;
}
.top-left {
border-right: 1px solid #e2e2e2;
display: flex;
align-items: flex-start;
}
.bill-to-label {
font-weight: 700;
min-width: 48px;
}
.bill-to-name {
font-weight: 700;
}
.bill-to-address {
margin-top: 2px;
line-height: 1.25;
}
.top-right {
display: flex;
flex-direction: column;
justify-content: flex-start;
gap: 2px;
text-align: right;
font-weight: 700;
}
.top-right-row {
display: flex;
justify-content: flex-end;
gap: 6px;
}
.top-right-row .value {
font-weight: 400;
min-width: 110px;
text-align: left;
}
.info-grid {
display: grid;
grid-template-columns: 1.4fr 1fr;
border-bottom: 1px solid #c9c9c9;
}
.mid-left {
border-right: 1px solid #e2e2e2;
}
.mid-row {
display: flex;
justify-content: space-between;
gap: 10px;
margin-bottom: 2px;
}
.info-label {
font-weight: 700;
}
.info-value {
text-align: right;
min-width: 90px;
}
.items-wrap {
overflow-x: auto;
}
.items-table {
width: 100%;
border-collapse: collapse;
}
.items-table thead th {
background: #a8d9de;
color: #111;
font-weight: bold;
font-size: 10px;
text-align: center;
padding: 5px 4px;
border: 1px solid #4b4b4b;
}
.items-table td {
border: 1px solid #5d5d5d;
font-size: 10px;
height: 15px;
padding: 2px 4px;
vertical-align: middle;
}
.center {
text-align: center;
}
.right {
text-align: right;
}
.col-no { width: 8%; }
.col-desc { width: 30%; }
.col-remarks { width: 24%; }
.col-qty { width: 9%; }
.col-unit { width: 9%; }
.col-price { width: 10%; }
.col-total { width: 10%; }
.spacer-row td {
height: 18px;
}
.bottom-table {
display: grid;
grid-template-columns: 1.4fr 1fr;
border-top: 1px solid #c9c9c9;
}
.bottom-left,
.bottom-right {
padding: 4px 6px;
font-size: 10px;
}
.bottom-left {
border-right: 1px solid #e2e2e2;
}
.bottom-row {
display: flex;
flex-direction: column;
gap: 2px;
}
.bottom-line {
display: flex;
justify-content: space-between;
gap: 8px;
}
.footer-row {
border-top: 1px solid #333;
text-align: right;
padding: 4px 6px;
font-size: 9px;
color: #555;
}
</style>
</head>
<body>
<%
const invoiceTitle = typeof title !== 'undefined' && title ? title : 'INVOICE';
const invoiceJobReference = typeof jobReference !== 'undefined' && jobReference ? jobReference : 'N/A';
const invoicePoNumber = typeof poNumber !== 'undefined' && poNumber ? poNumber : 'N/A';
const invoiceDate = typeof date !== 'undefined' && date ? date : 'N/A';
const invoicePageLabel = typeof pageLabel !== 'undefined' && pageLabel ? pageLabel : 'Page 01 of 02';
const invoiceBillToName = typeof billToName !== 'undefined' && billToName ? billToName : 'Client Name';
const invoiceBillToAddress = typeof billToAddress !== 'undefined' && billToAddress ? billToAddress : 'Address';
const invoiceVesselName = typeof vesselName !== 'undefined' && vesselName ? vesselName : 'N/A';
const invoicePurposeOfCall = typeof purposeOfCall !== 'undefined' && purposeOfCall ? purposeOfCall : 'N/A';
const invoiceSupplyDate = typeof supplyDate !== 'undefined' && supplyDate ? supplyDate : 'N/A';
const invoiceGrt = typeof grt !== 'undefined' && grt ? grt : 'N/A';
const invoiceImoNumber = typeof imoNumber !== 'undefined' && imoNumber ? imoNumber : 'N/A';
const invoicePortCountry = typeof portCountry !== 'undefined' && portCountry ? portCountry : 'N/A';
const invoiceItems = typeof items !== 'undefined' && Array.isArray(items) ? items : [];
%>
<div class="page">
<div class="logo-section">
<% if (logoBase64) { %>
<img src="<%= logoBase64 %>" alt="Company Logo" />
<% } %>
</div>
<div class="title-section"><%= invoiceTitle %></div>
<div class="top-grid">
<div class="top-left">
<div>
<div><span class="bill-to-label">Bill To:</span> <span class="bill-to-name"><%= invoiceBillToName %></span></div>
<div class="bill-to-address"><%= invoiceBillToAddress %></div>
</div>
</div>
<div class="top-right">
<div class="top-right-row"><span>Job Reference :</span> <span class="value"><%= invoiceJobReference %></span></div>
<div class="top-right-row"><span>PO Number :</span> <span class="value"><%= invoicePoNumber %></span></div>
<div class="top-right-row"><span>Date :</span> <span class="value"><%= invoiceDate %></span></div>
<div class="top-right-row"><span></span> <span class="value"><%= invoicePageLabel %></span></div>
</div>
</div>
<div class="info-grid">
<div class="mid-left">
<div class="mid-row"><span class="info-label">Vessel Name :</span> <span class="info-value"><%= invoiceVesselName %></span></div>
<div class="mid-row"><span class="info-label">Purpose of Call :</span> <span class="info-value"><%= invoicePurposeOfCall %></span></div>
<div class="mid-row"><span class="info-label">Supply Date :</span> <span class="info-value"><%= invoiceSupplyDate %></span></div>
</div>
<div class="mid-right">
<div class="mid-row"><span class="info-label">GRT :</span> <span class="info-value"><%= invoiceGrt %></span></div>
<div class="mid-row"><span class="info-label">IMO Number :</span> <span class="info-value"><%= invoiceImoNumber %></span></div>
<div class="mid-row"><span class="info-label">Port/Country :</span> <span class="info-value"><%= invoicePortCountry %></span></div>
</div>
</div>
<div class="items-wrap">
<table class="items-table">
<thead>
<tr>
<th class="col-no">No.</th>
<th class="col-desc">Description</th>
<th class="col-remarks">Remarks</th>
<th class="col-qty">Qty</th>
<th class="col-unit">Unit</th>
<th class="col-price">Unit Price (USD)</th>
<th class="col-total">Final Amount (USD)</th>
</tr>
</thead>
<tbody>
<% const rows = Math.max(10, invoiceItems.length); %>
<% for (let index = 0; index < rows; index++) { %>
<% const item = invoiceItems[index] || {}; %>
<% const quantity = Number(item.quantity || item.qty || 0); %>
<% const unitPrice = Number(item.unit_price || item.unitPrice || 0); %>
<tr class="spacer-row">
<td class="center"><%= index + 1 %></td>
<td><%= item.description || item.item_name || item.name || '' %></td>
<td><%= item.remarks || '' %></td>
<td class="center"><%= item.quantity || item.qty || '' %></td>
<td class="center"><%= item.unit || '' %></td>
<td class="right"><%= item.unit_price || item.unitPrice || '' %></td>
<td class="right"><%= quantity && unitPrice ? quantity * unitPrice : (item.total || item.amount || '') %></td>
</tr>
<% } %>
</tbody>
</table>
</div>
</div>
</body>
</html>
@@ -0,0 +1,111 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/templates/documents/invoice/invoice.template.js
const { log } = require("../../../utils/consoleLog.utill");
module.exports = async (data) => {
log("📋 Preparing Invoice data for PDF...");
let payload = data;
if (typeof payload === "string") {
try {
payload = JSON.parse(payload);
} catch (error) {
payload = {};
}
}
const source =
payload.documentData ||
payload.document_data ||
payload.data ||
payload;
const deepFind = (node, keys, visited = new Set()) => {
if (!node || typeof node !== "object" || visited.has(node)) {
return "";
}
visited.add(node);
for (const key of keys) {
const value = node[key];
if (value !== undefined && value !== null && value !== "") {
return value;
}
}
for (const child of Object.values(node)) {
const found = deepFind(child, keys, visited);
if (found !== "") {
return found;
}
}
return "";
};
const pick = (...keys) => {
const direct = deepFind(source, keys);
if (direct !== "") {
return direct;
}
return deepFind(payload, keys);
};
const templateData = {
logoUrl:
pick("logoUrl", "logo_url") ||
"https://res.cloudinary.com/dtthuvvir/image/upload/v1780748661/company_logo_transperent_bg_uullzw.png",
title: pick("title") || "INVOICE",
jobReference:
pick(
"jobReference",
"job_reference",
"referenceNumber",
"reference_no",
"jobRef",
"job_ref",
) || "",
poNumber: pick("poNumber", "po_number", "poNo", "po_no") || "",
date: pick("date") || new Date().toLocaleDateString(),
pageLabel: pick("pageLabel", "page_label") || "Page 01 of 02",
billToName: pick("billToName", "bill_to_name") || "Client Name",
billToAddress: pick("billToAddress", "bill_to_address") || "Address",
vesselName: pick("vesselName", "vessel_name") || "",
purposeOfCall: pick("purposeOfCall", "purpose_of_call") || "",
supplyDate: pick("supplyDate", "supply_date") || "",
grt: pick("grt", "GRT") || "",
imoNumber: pick("imoNumber", "imo_number", "imo") || "",
portCountry: pick("portCountry", "port_country") || "",
items: Array.isArray(source.items) ? source.items : [],
subtotal: Number(pick("subtotal", "sub_total") || 0),
tax: Number(pick("tax") || 0),
discount: Number(pick("discount") || 0),
total: Number(pick("total") || 0),
paymentTerms: pick("paymentTerms", "payment_terms") || "",
notes: pick("notes") || "",
};
log("✅ Invoice data prepared");
return templateData;
};
+23
View File
@@ -0,0 +1,23 @@
body {
font-family: Arial;
padding: 20px;
}
h1 {
text-align: center;
}
table {
width: 100%;
border-collapse: collapse;
}
td,
th {
border: 1px solid #ddd;
padding: 8px;
}
th {
background: #f4f4f4;
}
@@ -0,0 +1,24 @@
<html>
<head>
<style>
{{styles}}
</style>
</head>
<body>
<h1>PreCost Estimate</h1>
<p>Customer: {{customerName}}</p>
<table>
<tr>
<th>Item</th>
<th>Description</th>
<th>Qty</th>
<th>Unit Price</th>
<th>Total</th>
</tr>
{{items}}
</table>
</body>
</html>
+285
View File
@@ -0,0 +1,285 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: white;
font-family: 'Segoe UI', 'Roboto', 'Helvetica Neue', sans-serif;
padding: 20px;
}
.container {
max-width: 1000px;
margin: 0 auto;
background: white;
}
.header-section {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
border-bottom: 2px solid #1a7a4d;
padding-bottom: 15px;
}
.header-section img {
height: 80px;
}
.doc-title {
font-size: 28px;
font-weight: bold;
color: #1a7a4d;
text-align: center;
flex: 1;
}
.po-number {
text-align: right;
color: #666;
font-size: 12px;
}
.info-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-bottom: 30px;
background: #e8f5e9;
padding: 15px;
border-radius: 4px;
}
.info-left, .info-right {
display: flex;
flex-direction: column;
gap: 8px;
}
.info-row {
display: flex;
justify-content: space-between;
}
.info-label {
font-weight: bold;
color: #2e7d32;
min-width: 150px;
}
.info-value {
color: #333;
}
table {
width: 100%;
border-collapse: collapse;
margin-bottom: 20px;
}
.items-table thead {
background-color: #2e7d32;
color: white;
}
.items-table th {
padding: 12px;
text-align: left;
font-weight: bold;
font-size: 12px;
}
.items-table td {
padding: 12px;
border-bottom: 1px solid #ddd;
}
.items-table tbody tr:nth-child(even) {
background-color: #f5f5f5;
}
.summary-area {
display: flex;
justify-content: flex-end;
margin-top: 30px;
margin-bottom: 30px;
}
.summary-table {
width: 400px;
}
.summary-table td {
padding: 10px;
border: 1px solid #ddd;
}
.summary-table td:first-child {
font-weight: bold;
background-color: #f5f5f5;
width: 60%;
}
.summary-table td:last-child {
text-align: right;
}
.total-row {
background-color: #2e7d32;
color: white;
font-weight: bold;
font-size: 14px;
}
.terms-section {
margin-top: 30px;
border-top: 1px solid #ddd;
padding-top: 15px;
}
.terms-section h4 {
color: #2e7d32;
font-size: 12px;
margin-bottom: 8px;
}
.terms-section p {
font-size: 11px;
color: #666;
line-height: 1.5;
}
.footer-note {
text-align: center;
margin-top: 40px;
border-top: 2px solid #ddd;
padding-top: 20px;
}
</style>
</head>
<body>
<div class="container">
<!-- HEADER SECTION -->
<div class="header-section">
<div>
<% if (logoBase64) { %>
<img src="<%= logoBase64 %>" alt="Company Logo" />
<% } %>
</div>
<div style="flex: 1; text-align: center;">
<div class="doc-title">PURCHASE ORDER</div>
</div>
<div class="po-number">
<strong>PO #:</strong> <%= poNumber || 'N/A' %><br/>
<strong>Date:</strong> <%= date || 'N/A' %>
</div>
</div>
<!-- INFO GRID -->
<div class="info-grid">
<div class="info-left">
<div class="info-row">
<span class="info-label">VENDOR:</span>
</div>
<div style="margin-left: 0;">
<strong><%= vendorName || 'Vendor Name' %></strong><br/>
<%= vendorAddress || 'Address' %>
</div>
</div>
<div class="info-right">
<div class="info-row">
<span class="info-label">DELIVERY DATE:</span>
<span class="info-value"><%= deliveryDate || 'N/A' %></span>
</div>
<div class="info-row">
<span class="info-label">PAYMENT TERMS:</span>
<span class="info-value"><%= paymentTerms || 'Net 30' %></span>
</div>
<div class="info-row">
<span class="info-label">VESSEL:</span>
<span class="info-value"><%= vesselName || 'N/A' %></span>
</div>
</div>
</div>
<!-- ITEMS TABLE -->
<div style="overflow-x: auto;">
<table class="items-table">
<thead>
<tr>
<th>ITEM NO</th>
<th>DESCRIPTION</th>
<th>QTY</th>
<th>UNIT</th>
<th>UNIT PRICE</th>
<th>TOTAL</th>
</tr>
</thead>
<tbody>
<% if (items && items.length > 0) { %>
<% items.forEach((item, index) => { %>
<tr>
<td><%= index + 1 %></td>
<td><%= item.description || item.item_name || '' %></td>
<td><%= item.quantity || 0 %></td>
<td><%= item.unit || '' %></td>
<td><%= item.unit_price || 0 %></td>
<td><%= (item.quantity || 0) * (item.unit_price || 0) %></td>
</tr>
<% }); %>
<% } %>
</tbody>
</table>
</div>
<!-- SUMMARY SECTION -->
<div class="summary-area">
<table class="summary-table">
<tr>
<td>SUB TOTAL</td>
<td><%= subtotal || 0 %></td>
</tr>
<tr>
<td>TAX (%)</td>
<td><%= tax || 0 %></td>
</tr>
<tr>
<td>SHIPPING</td>
<td><%= shipping || 0 %></td>
</tr>
<tr class="total-row">
<td>TOTAL</td>
<td><%= total || 0 %></td>
</tr>
</table>
</div>
<!-- TERMS SECTION -->
<% if (paymentTermsDetails || notes) { %>
<div class="terms-section">
<% if (paymentTermsDetails) { %>
<h4>TERMS & CONDITIONS</h4>
<p><%= paymentTermsDetails %></p>
<% } %>
<% if (notes) { %>
<h4>SPECIAL INSTRUCTIONS</h4>
<p><%= notes %></p>
<% } %>
</div>
<% } %>
<!-- FOOTER -->
<div class="footer-note">
<p style="font-size: 10px; color: #999;">Please confirm receipt and delivery dates</p>
</div>
</div>
</body>
</html>
+44
View File
@@ -0,0 +1,44 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/templates/documents/po/po.template.js
const { log } = require("../../../utils/consoleLog.utill");
module.exports = async (data) => {
log("📋 Preparing Purchase Order data for PDF...");
const templateData = {
logoUrl: "https://res.cloudinary.com/dtthuvvir/image/upload/v1780748661/company_logo_transperent_bg_uullzw.png",
poNumber: data.poNumber || data.po_number || "PO-001",
date: data.date || new Date().toLocaleDateString(),
vendorName: data.vendorName || data.vendor_name || "Vendor Name",
vendorAddress: data.vendorAddress || data.vendor_address || "Address",
deliveryDate: data.deliveryDate || data.delivery_date || "N/A",
paymentTerms: data.paymentTerms || data.payment_terms || "Net 30",
vesselName: data.vesselName || data.vessel_name || "N/A",
items: data.items || [],
subtotal: data.subtotal || data.sub_total || 0,
tax: data.tax || 0,
shipping: data.shipping || 0,
total: data.total || 0,
paymentTermsDetails: data.paymentTermsDetails || data.payment_terms_details || "",
notes: data.notes || "",
};
log("✅ Purchase Order data prepared");
return templateData;
};
@@ -0,0 +1,48 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/templates/documents/precost/preCost.template.js
const { log } = require("../../../utils/consoleLog.utill");
module.exports = async (data) => {
log("📋 Preparing PreCost data for PDF...");
log("📥 Incoming data:", JSON.stringify(data, null, 2));
const templateData = {
logoUrl:
"https://res.cloudinary.com/dtthuvvir/image/upload/v1780748661/company_logo_transperent_bg_uullzw.png",
vesselName: data.vessel_name || "MV Vessel",
supplierName: data.supplyPort || "N/A",
clientRfqNum: data.clientRfqNum || "N/A",
omsRfqNum: data.omsRfqNum || "N/A",
date: data.date || new Date().toLocaleDateString(),
items: data.items || [],
subtotal: data.sub_total || 0,
discount: data.discount || 0,
additionalCharges: data.additionalCharges || 0,
fullTotal: data.total_cost || data.total_cost_usd || (data.sub_total - data.discount + data.additionalCharges) || 0,
};
log("Data:", data);
log("fullTotal calculation:", {
total_cost: data.total_cost,
total_cost_usd: data.total_cost_usd,
calculated: data.sub_total - data.discount + data.additionalCharges,
final: templateData.fullTotal,
});
log("✅ PreCost data prepared:", JSON.stringify(templateData, null, 2));
return templateData;
};
+300
View File
@@ -0,0 +1,300 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: white;
font-family: 'Segoe UI', 'Roboto', 'Helvetica Neue', sans-serif;
padding: 20px;
}
.container {
max-width: 1000px;
margin: 0 auto;
background: white;
}
.header-section {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
border-bottom: 2px solid #1a7a4d;
padding-bottom: 15px;
}
.header-section img {
height: 80px;
}
.po-title {
font-size: 28px;
font-weight: bold;
color: #1a7a4d;
text-align: center;
flex: 1;
}
.info-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-bottom: 30px;
background: #e8f5e9;
padding: 15px;
border-radius: 4px;
}
.info-left,
.info-right {
display: flex;
flex-direction: column;
gap: 8px;
}
.info-row {
display: flex;
justify-content: space-between;
}
.info-label {
font-weight: bold;
color: #2e7d32;
min-width: 150px;
}
.info-value {
color: #333;
}
table {
width: 100%;
border-collapse: collapse;
margin-bottom: 20px;
}
.items-table thead {
background-color: #2e7d32;
color: white;
}
.items-table th {
padding: 12px;
text-align: left;
font-weight: bold;
font-size: 12px;
}
.items-table td {
padding: 12px;
border-bottom: 1px solid #ddd;
}
.items-table tbody tr:nth-child(even) {
background-color: #f5f5f5;
}
.summary-area {
display: flex;
justify-content: flex-end;
margin-top: 30px;
margin-bottom: 30px;
}
.summary-table {
width: 400px;
}
.summary-table td {
padding: 10px;
border: 1px solid #ddd;
}
.summary-table td:first-child {
font-weight: bold;
background-color: #f5f5f5;
width: 60%;
}
.summary-table td:last-child {
text-align: right;
}
.total-row {
background-color: #2e7d32;
color: white;
font-weight: bold;
font-size: 14px;
}
.total-row td {
border-color: #1a7a4d;
}
.footer-note {
text-align: center;
margin-top: 40px;
border-top: 2px solid #ddd;
padding-top: 20px;
}
.footer-note img {
max-width: 100%;
height: auto;
}
.text-black{
color: black !important;
}
</style>
</head>
<body>
<div class="container">
<!-- HEADER SECTION -->
<div class="header-section">
<div>
<% if (logoBase64) { %>
<img src="<%= logoBase64 %>" alt="Company Logo" />
<% } %>
</div>
<div class="po-title">PRICE QUOTATION</div>
</div>
<!-- INFO GRID -->
<div class="info-grid">
<div class="info-left">
<div class="info-row">
<span class="info-label">VESSEL NAME :</span>
<span class="info-value">
<%= vesselName %>
</span>
</div>
<div class="info-row">
<span class="info-label">SUPPLY PORT :</span>
<span class="info-value">
<%= supplierName || 'Supplier Name' %>
</span>
</div>
</div>
<div class="info-right">
<div class="info-row">
<span class="info-label">CLIENT RFQ :</span>
<span class="info-value">
<%= clientRfqNum || 'RFQ-001' %>
</span>
</div>
<div class="info-row">
<span class="info-label">OMS RFQ :</span>
<span class="info-value">
<%= omsRfqNum || 'RFQ-001' %>
</span>
</div>
<div class="info-row">
<span class="info-label">DATE :</span>
<span class="info-value">
<%= date || 'Current Date' %>
</span>
</div>
</div>
</div>
<!-- ITEMS TABLE -->
<div style="overflow-x: auto;">
<table class="items-table">
<thead>
<tr>
<th>ITEM NO</th>
<th>DESCRIPTION</th>
<th>REMARKS</th>
<th>QTY</th>
<th>UNIT</th>
<th>UNIT PRICE</th>
<th>TOTAL PRICE</th>
</tr>
</thead>
<tbody>
<% if (items && items.length> 0) { %>
<% items.forEach((item, index)=> { %>
<tr>
<td>
<%= index + 1 %>
</td>
<td>
<%= item.item_name || '' %>
</td>
<td>
<%= item.customer_remark || '' %>
</td>
<td>
<%= item.quantity || 0 %>
</td>
<td>
<%= item.unit || '' %>
</td>
<td>
<%= item.unit_price || 0 %>
</td>
<td>
<%= (item.quantity || 0) * (item.unit_price || 0) %>
</td>
</tr>
<% }); %>
<% } %>
</tbody>
</table>
</div>
<!-- SUMMARY SECTION -->
<div class="summary-area">
<table class="summary-table">
<tr>
<td>SUB TOTAL</td>
<td>
<%= subtotal || 0 %>
</td>
</tr>
<% if (discount && Number(discount) !==0) { %>
<tr>
<td>DISCOUNT (%)</td>
<td>
<%= discount %>
</td>
</tr>
<% } %>
<% if (additionalCharges && Number(additionalCharges) !==0) { %>
<tr>
<td>ADDITIONAL CHARGES<br />(CUSTOM/HANDLING/TRANSPORT)</td>
<td>
<%= additionalCharges %>
</td>
</tr>
<% } %>
<tr class="total-row">
<td class="text-black" >GRAND TOTAL</td>
<td>
<%= fullTotal || 0 %>
</td>
</tr>
</table>
</div>
<!-- FOOTER -->
<div class="footer-note">
<img src="https://res.cloudinary.com/dtthuvvir/image/upload/v1780748661/footer_ax2jyz.png" alt="Footer">
</div>
</div>
</body>
</html>
+527
View File
@@ -0,0 +1,527 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: white;
display: flex;
justify-content: center;
align-items: center;
padding: 2rem 1rem;
font-family: 'Segoe UI', 'Roboto', 'Helvetica Neue', sans-serif;
}
/* actual table style — clean, border-collapse */
.po-table {
width: 100%;
border-collapse: collapse;
font-size: 0.85rem;
font-family: 'Segoe UI', system-ui, 'Calibri', sans-serif;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.02);
}
/* header sections & company info area (using table-like but not inside main table) */
.header-section {
margin-bottom: 1.8rem;
text-align: center;
}
.po-title {
font-size: 2rem;
font-weight: 700;
letter-spacing: 1px;
color: #257339;
border-top: 2px solid #257339;
border-bottom: 2px solid #257339;
padding: 1rem;
margin-bottom: 1.5rem;
margin-top: 1.5rem;
font-family: 'Inter', 'Segoe UI', sans-serif;
}
/* two column info grid (company, supplier, dates etc) */
.info-grid {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
gap: 1.2rem;
margin-bottom: 2rem;
background: #e1f3db;
padding: 1rem 1.2rem;
border: 1px solid #e2edf2;
}
.info-left {
flex: 2;
min-width: 240px;
}
.info-right {
flex: 1.2;
min-width: 200px;
}
.info-row {
display: flex;
align-items: baseline;
flex-wrap: wrap;
margin-bottom: 12px;
font-size: 0.9rem;
}
.info-label {
font-weight: 700;
width: 140px;
color: #257339;
letter-spacing: 0.3px;
}
.info-value {
font-weight: 500;
color: #1f2d3d;
border-bottom: 1px dashed #bdd3e6;
min-width: 200px;
padding: 0 0.2rem;
background: transparent;
}
.inline-placeholder {
color: #2c7da0;
font-weight: 500;
}
hr {
margin: 1rem 0;
}
/* main items table */
.items-table {
width: 100%;
border-collapse: collapse;
margin: 1.2rem 0 1rem;
font-size: 0.8rem;
}
.items-table th,
.items-table td {
border: 1px solid #cddfe7;
padding: 10px 6px;
vertical-align: top;
text-align: left;
}
.items-table th {
background-color: #257339;
color: white;
font-weight: 600;
font-size: 0.8rem;
letter-spacing: 0.5px;
text-align: center;
}
.items-table td {
background-color: #fff;
color: #1e2a3a;
}
.items-table input,
.items-table .pseudo-input {
width: 100%;
border: none;
background: transparent;
font-family: inherit;
font-size: 0.8rem;
padding: 4px 2px;
outline: none;
border-bottom: 1px dotted #acc8dd;
}
.items-table input:focus {
border-bottom: 1px solid #1f6e8c;
background: #fefce8;
}
/* numeric cells alignment */
.numeric-cell {
text-align: right;
}
.qty-cell input,
.price-cell input {
text-align: right;
}
.total-cell {
font-weight: 600;
background-color: #f8fafc;
text-align: right;
}
/* summary section (subtotal, discount, transport) */
.summary-area {
margin-top: 1.5rem;
display: flex;
flex-direction: column;
align-items: flex-end;
border-top: 2px solid #cbdde6;
padding-top: 1.2rem;
}
.summary-table {
width: 320px;
border-collapse: collapse;
font-size: 0.85rem;
}
.summary-table td {
padding: 6px 8px;
border: none;
}
.summary-table td:first-child {
font-weight: 700;
text-align: left;
}
.summary-table td:last-child {
text-align: right;
font-weight: 600;
}
.total-row {
border-top: 2px solid #2c5a74;
font-weight: 800;
font-size: 1rem;
}
.discount-input,
.transport-input {
border: 1px solid #b9d0df;
border-radius: 8px;
padding: 4px 10px;
width: 110px;
text-align: right;
font-weight: 500;
background: #fff;
}
.inline-action {
display: inline-flex;
align-items: center;
gap: 5px;
}
button.recalc {
background: #eef2f5;
border: 1px solid #b9cfdf;
border-radius: 30px;
padding: 4px 12px;
font-size: 0.7rem;
cursor: pointer;
transition: 0.1s;
margin-left: 10px;
font-weight: 500;
}
button.recalc:hover {
background: #cbdde6;
}
.footer-note {
margin-top: 2rem;
font-size: 0.7rem;
text-align: center;
color: #547b97;
border-top: 1px solid #d4e2ec;
padding-top: 1rem;
}
@media (max-width: 750px) {
.po-container {
padding: 1rem;
}
.info-grid {
flex-direction: column;
}
.summary-table {
width: 100%;
}
.items-table th,
.items-table td {
padding: 6px 3px;
font-size: 0.7rem;
}
}
.editable-highlight {
background-color: #fef9e3;
} * {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: white;
display: flex;
justify-content: center;
align-items: center;
padding: 2rem 1rem;
font-family: 'Segoe UI', 'Roboto', 'Helvetica Neue', sans-serif;
}
/* actual table style — clean, border-collapse */
.po-table {
width: 100%;
border-collapse: collapse;
font-size: 0.85rem;
font-family: 'Segoe UI', system-ui, 'Calibri', sans-serif;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.02);
}
/* header sections & company info area (using table-like but not inside main table) */
.header-section {
margin-bottom: 1.8rem;
text-align: center;
}
.po-title {
font-size: 2rem;
font-weight: 700;
letter-spacing: 1px;
color: #257339;
border-top: 2px solid #257339;
border-bottom: 2px solid #257339;
padding: 1rem;
margin-bottom: 1.5rem;
margin-top: 1.5rem;
font-family: 'Inter', 'Segoe UI', sans-serif;
}
/* two column info grid (company, supplier, dates etc) */
.info-grid {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
gap: 1.2rem;
margin-bottom: 2rem;
background: #e1f3db;
padding: 1rem 1.2rem;
border: 1px solid #e2edf2;
}
.info-left {
flex: 2;
min-width: 240px;
}
.info-right {
flex: 1.2;
min-width: 200px;
}
.info-row {
display: flex;
align-items: baseline;
flex-wrap: wrap;
margin-bottom: 12px;
font-size: 0.9rem;
}
.info-label {
font-weight: 700;
width: 140px;
color: #257339;
letter-spacing: 0.3px;
}
.info-value {
font-weight: 500;
color: #1f2d3d;
border-bottom: 1px dashed #bdd3e6;
min-width: 200px;
padding: 0 0.2rem;
background: transparent;
}
.inline-placeholder {
color: #2c7da0;
font-weight: 500;
}
hr {
margin: 1rem 0;
}
/* main items table */
.items-table {
width: 100%;
border-collapse: collapse;
margin: 1.2rem 0 1rem;
font-size: 0.8rem;
}
.items-table th,
.items-table td {
border: 1px solid #cddfe7;
padding: 10px 6px;
vertical-align: top;
text-align: left;
}
.items-table th {
background-color: #257339;
color: white;
font-weight: 600;
font-size: 0.8rem;
letter-spacing: 0.5px;
text-align: center;
}
.items-table td {
background-color: #fff;
color: #1e2a3a;
}
.items-table input,
.items-table .pseudo-input {
width: 100%;
border: none;
background: transparent;
font-family: inherit;
font-size: 0.8rem;
padding: 4px 2px;
outline: none;
border-bottom: 1px dotted #acc8dd;
}
.items-table input:focus {
border-bottom: 1px solid #1f6e8c;
background: #fefce8;
}
/* numeric cells alignment */
.numeric-cell {
text-align: right;
}
.qty-cell input,
.price-cell input {
text-align: right;
}
.total-cell {
font-weight: 600;
background-color: #f8fafc;
text-align: right;
}
/* summary section (subtotal, discount, transport) */
.summary-area {
margin-top: 1.5rem;
display: flex;
flex-direction: column;
align-items: flex-end;
border-top: 2px solid #cbdde6;
padding-top: 1.2rem;
}
.summary-table {
width: 320px;
border-collapse: collapse;
font-size: 0.85rem;
}
.summary-table td {
padding: 6px 8px;
border: none;
}
.summary-table td:first-child {
font-weight: 700;
text-align: left;
}
.summary-table td:last-child {
text-align: right;
font-weight: 600;
}
.total-row {
border-top: 2px solid #2c5a74;
font-weight: 800;
font-size: 1rem;
}
.discount-input,
.transport-input {
border: 1px solid #b9d0df;
border-radius: 8px;
padding: 4px 10px;
width: 110px;
text-align: right;
font-weight: 500;
background: #fff;
}
.inline-action {
display: inline-flex;
align-items: center;
gap: 5px;
}
button.recalc {
background: #eef2f5;
border: 1px solid #b9cfdf;
border-radius: 30px;
padding: 4px 12px;
font-size: 0.7rem;
cursor: pointer;
transition: 0.1s;
margin-left: 10px;
font-weight: 500;
}
button.recalc:hover {
background: #cbdde6;
}
.footer-note {
margin-top: 2rem;
font-size: 0.7rem;
text-align: center;
color: #547b97;
border-top: 1px solid #d4e2ec;
padding-top: 1rem;
}
@media (max-width: 750px) {
.po-container {
padding: 1rem;
}
.info-grid {
flex-direction: column;
}
.summary-table {
width: 100%;
}
.items-table th,
.items-table td {
padding: 6px 3px;
font-size: 0.7rem;
}
}
.editable-highlight {
background-color: #fef9e3;
}
@@ -0,0 +1,97 @@
<html>
<head>
<style>
{{styles}}
</style>
</head>
<body>
<div>
<!-- PURCHASE ORDER title -->
<div class="header-section">
<div>
<img src="{{logoBase64}}" alt="Company Logo" style="height:80px; margin-bottom:-8px;" />
</div>
<div class="po-title">PRICE QUOTATION</div>
</div>
<!-- Company & supplier info (dynamic / editable) -->
<div class="info-grid">
<div class="info-left">
<div class="info-row">
<span class="info-label">VESSEL NAME :</span>
<span class="info-value" id="companyName">Oceanic Maritime Solutions (Pvt) Ltd</span>
</div>
<div class="info-row">
<span class="info-label">SUPPLY PORT :</span>
<span class="info-value" id="supplierName">Supplier Name</span>
</div>
</div>
<div class="info-right">
<div class="info-row">
<span class="info-label">CLIENT RFQ :</span>
<span class="info-value" id="rfqNum">RFQ-001</span>
</div>
<div class="info-row">
<span class="info-label">OMS RFQ :</span>
<span class="info-value" id="rfqNum">RFQ-001</span>
</div>
<div class="info-row">
<span class="info-label">DATE :</span>
<span class="info-value" id="orderDate">Current Date</span>
</div>
</div>
</div>
<!-- items table: ITEM NO, DESCRIPTION, REMARKS, UNIT, QTY, UNIT PRICE, TOTAL PRICE -->
<div style="overflow-x: auto;">
<table class="items-table" id="poItemsTable">
<thead>
<tr>
<th>ITEM NO</th>
<th>DESCRIPTION</th>
<th>REMARKS</th>
<th>QTY</th>
<th>UNIT</th>
<th>UNIT PRICE</th>
<th>TOTAL PRICE</th>
</tr>
</thead>
<tbody id="itemsTableBody">
{{items}}
</tbody>
</table>
</div>
<!-- summary: subtotal, discount, transportation, full total -->
<div class="summary-area">
<table class="summary-table" id="summaryTable">
<tr>
<td>SUB TOTAL</td>
<td id="subTotalVal">{{subtotal}}</td>
</tr>
<tr>
<td>DISCOUNT <span style="font-weight:normal;">(%)</span>
</td>
<td id="discountAmount">{{discount}}</td>
</tr>
<tr>
<td>ADDITIONAL CHARGES (CUSTOM/HANDLING/TRANSPORT)
</td>
<td id="transportDisplay">{{transport}}</td>
</tr>
<tr class="total-row">
<td>GRAND TOTAL</td>
<td id="fullTotalVal">{{fullTotal}}</td>
</tr>
</table>
</div>
<div class="footer-note">
<img src="https://res.cloudinary.com/dtthuvvir/image/upload/v1780748661/footer_ax2jyz.png" alt="Footer">
</div>
</div>
</body>
</html>
+157
View File
@@ -0,0 +1,157 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Your Verification Code</title>
<style>
/* Reset styles */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
}
/* Mobile responsive */
@media screen and (max-width: 600px) {
.container {
width: 100% !important;
padding: 20px !important;
}
.otp-code {
font-size: 36px !important;
letter-spacing: 8px !important;
}
.button {
width: 100% !important;
text-align: center !important;
}
}
</style>
</head>
<body style="background-color: #f4f6f9; margin: 0; padding: 0;">
<!-- Main Container -->
<table width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color: #f4f6f9; padding: 40px 0;">
<tr>
<td align="center">
<!-- Email Content -->
<table width="600" cellpadding="0" cellspacing="0" border="0" class="container"
style="background-color: #ffffff; border-radius: 16px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05); max-width: 600px; width: 100%;">
<!-- Header -->
<tr>
<td style="padding: 40px 40px 20px 40px;" align="center">
<img src="https://res.cloudinary.com/dhqcnszvn/image/upload/v1771106508/OCEANIC_MARITIME_SOLUTIONS_LOGO-01-removebg-preview-3_vweopp.png"
alt="Logo" style="max-width: 150px; height: auto;">
</td>
</tr>
<!-- Main Content -->
<tr>
<td style="padding: 0 40px;">
<p
style="color: #475569; font-size: 16px; line-height: 24px; margin-bottom: 20px; text-align: center;">
A new temporary vendor has been added for precost No: <strong
style="color: #3b82f6;">{{precostNo}}</strong> Please review this and proceed with
the approval.
</p>
<!-- OTP Container -->
<table width="100%" cellpadding="0" cellspacing="0" border="0"
style="background-color: #f8fafc; border-radius: 12px; padding: 30px; margin: 30px 0;">
<tr>
<td align="center">
<span
style="color: #64748b; font-size: 14px; text-transform: uppercase; letter-spacing: 2px; display: block; margin-bottom: 10px;">
Vendor Name
</span>
<span class="otp-code"
style="color: #0f172a; font-size: 48px; font-weight: 700; letter-spacing: 12px; font-family: 'Courier New', monospace; display: block;">
{{vendor_name}}
</span>
</td>
</tr>
<tr>
<td align="center">
<span
style="color: #64748b; font-size: 14px; text-transform: uppercase; letter-spacing: 2px; display: block; margin-bottom: 10px;">
Vendor Contact Person
</span>
<span class="otp-code"
style="color: #0f172a; font-size: 48px; font-weight: 700; letter-spacing: 12px; font-family: 'Courier New', monospace; display: block;">
{{contact_person}}
</span>
</td>
</tr>
<tr>
<td align="center">
<span
style="color: #64748b; font-size: 14px; text-transform: uppercase; letter-spacing: 2px; display: block; margin-bottom: 10px;">
Vendor Email
</span>
<span class="otp-code"
style="color: #0f172a; font-size: 48px; font-weight: 700; letter-spacing: 12px; font-family: 'Courier New', monospace; display: block;">
{{email}}
</span>
</td>
</tr>
<tr>
<td align="center">
<span
style="color: #64748b; font-size: 14px; text-transform: uppercase; letter-spacing: 2px; display: block; margin-bottom: 10px;">
Vendor Contact Number
</span>
<span class="otp-code"
style="color: #0f172a; font-size: 48px; font-weight: 700; letter-spacing: 12px; font-family: 'Courier New', monospace; display: block;">
{{phone}}
</span>
</td>
</tr>
</table>
</td>
</tr>
<!-- Footer -->
<tr>
<td style="background-color: #f8fafc; border-radius: 0 0 16px 16px; padding: 30px 40px;">
<table width="100%" cellpadding="0" cellspacing="0" border="0">
<tr>
<td align="center" style="color: #94a3b8; font-size: 12px; line-height: 18px;">
<p style="margin: 5px 0;">
© {{currentYear}} Oceanic Maritime Solutions. All rights reserved.
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
<!-- End Email Content -->
<!-- Footer Note -->
<table width="600" cellpadding="0" cellspacing="0" border="0"
style="max-width: 600px; margin-top: 20px;">
<tr>
<td align="center" style="color: #94a3b8; font-size: 11px; padding: 0 20px;">
This is an automated message, please do not reply to this email.
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
+37
View File
@@ -0,0 +1,37 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>GLAURA 2FA Code</title>
</head>
<body style="font-family: Arial, Helvetica, sans-serif; font-size: 14px; color: #000; line-height: 1.6; margin: 0; padding: 20px;">
<p>Dear {{firstName}},</p>
<p>Greetings from <b>Oceanic Titan</b>!</p>
<p>Your One Time Password (OTP) to log in to your Oceanic Titan account is mentioned below.</p>
<p>Please enter this OTP in the required field to proceed further:</p>
<table cellpadding="8" cellspacing="0" style="border-collapse: collapse; width: 400px; border: 1px solid #000;">
<tr style="background-color: #053534; color: #ffffff; text-align: left;">
<th style="border: 1px solid #000;">Details</th>
<th style="border: 1px solid #000;">&nbsp;</th>
</tr>
<tr>
<td style="border: 1px solid #000;">One Time Password (OTP)</td>
<td style="border: 1px solid #000; text-align: center; font-weight: bold;">{{otp}}</td>
</tr>
</table>
<p>The above-mentioned OTP is valid for <b>5 minutes</b>.</p>
<br>
<p>Regards,<br>
<b>Oceanic Titan Team</b>
</p>
<p style="font-size: 12px; color: #555;">{{currentYear}} Oceanic Titan. This is an auto-generated email, please do not reply to this email.</p>
</body>
</html>
+53
View File
@@ -0,0 +1,53 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
body {
font-family: Arial, Helvetica, sans-serif;
font-size: 14px;
color: #000;
line-height: 1.6;
margin: 0;
padding: 20px;
}
table {
border-collapse: collapse;
width: 400px;
border: 1px solid #000;
}
th {
background-color: #053534;
color: #ffffff;
text-align: left;
}
td {
border: 1px solid #000;
}
a {
color: #1a73e8;
}
</style>
</head>
<body>
<p>Dear {{firstName}},</p>
<p>Greetings from <b>Oceanic Titan</b>!</p>
<p>You have requested to reset your password for your Oceanic Titan account. Please click the link below to reset your password:</p>
<p><a href="{{resetLink}}" target="_blank">Reset Password</a></p>
<p>If you did not request a password reset, please ignore this email. The above link will expire in <b>{{expiryTime}}</b>.</p>
<br>
<p>Regards,<br>
<b>Oceanic Titan Team</b>
</p>
<p style="font-size: 12px; color: #555;">{{currentYear}} Oceanic Titan. This is an auto-generated email, please do not reply to this email.</p>
</body>
</html>
+167
View File
@@ -0,0 +1,167 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Your Verification Code</title>
<style>
/* Reset styles */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
}
/* Mobile responsive */
@media screen and (max-width: 600px) {
.container {
width: 100% !important;
padding: 20px !important;
}
.otp-code {
font-size: 36px !important;
letter-spacing: 8px !important;
}
.button {
width: 100% !important;
text-align: center !important;
}
}
</style>
</head>
<body style="background-color: #f4f6f9; margin: 0; padding: 0;">
<!-- Main Container -->
<table width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color: #f4f6f9; padding: 40px 0;">
<tr>
<td align="center">
<!-- Email Content -->
<table width="600" cellpadding="0" cellspacing="0" border="0" class="container" style="background-color: #ffffff; border-radius: 16px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05); max-width: 600px; width: 100%;">
<!-- Header -->
<tr>
<td style="padding: 40px 40px 20px 40px;" align="center">
<img src="https://res.cloudinary.com/dhqcnszvn/image/upload/v1771106508/OCEANIC_MARITIME_SOLUTIONS_LOGO-01-removebg-preview-3_vweopp.png" alt="Logo" style="max-width: 150px; height: auto;">
</td>
</tr>
<!-- Main Content -->
<!-- Main Content (Updated with modern styling) -->
<tr>
<td style="padding: 40px;" class="inner-padding">
<!-- Welcome Message -->
<p style="margin: 0 0 24px 0; font-size: 16px; line-height: 1.6; color: #334155;">
Dear {{firstName}},<br><br>
Welcome to <strong style="color: #1e293b;">Oceanic Maritime Solutions</strong> Inquiry management system.<br>
Please find your account details below:
</p>
<!-- Credentials Card (Styled like OTP container) -->
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="background-color: #f8fafc; border-radius: 12px; padding: 30px; margin: 30px 0;">
<tr>
<td>
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" class="credentials-table">
<tr>
<td style="padding: 0 16px 0 0; width: 50%;">
<span style="color: #64748b; font-size: 12px; text-transform: uppercase; letter-spacing: 0.05em; font-weight: 600; display: block; margin-bottom: 8px;">
Email / Username
</span>
<div style="background-color: #ffffff; border: 1px solid #e2e8f0; border-radius: 8px; padding: 12px 16px; font-family: 'Courier New', monospace; font-size: 14px; color: #0f172a; font-weight: 500;">
{{email}}
</div>
</td>
<td style="padding: 0 0 0 16px; width: 50%;">
<span style="color: #64748b; font-size: 12px; text-transform: uppercase; letter-spacing: 0.05em; font-weight: 600; display: block; margin-bottom: 8px;">
Password
</span>
<div style="background-color: #ffffff; border: 1px solid #e2e8f0; border-radius: 8px; padding: 12px 16px; font-family: 'Courier New', monospace; font-size: 14px; color: #0f172a; font-weight: 500;">
{{password}}
</div>
</td>
</tr>
</table>
</td>
</tr>
</table>
<!-- Security Warning (Styled like security message) -->
<div style="background-color: #fff7ed; border-radius: 8px; padding: 16px; margin-bottom: 30px; border-left: 4px solid #b4651a;">
<p style="color: #9a3412; font-size: 14px; line-height: 20px; margin: 0;">
⚡ <strong>Important:</strong> Do not share this information with anyone. Change your password immediately after logging in.
</p>
</div>
<!-- Getting Started Section (New modern card) -->
<div style="background-color: #f1f5f9; border-radius: 12px; padding: 24px; margin: 30px 0;">
<h3 style="color: #0f172a; font-size: 16px; font-weight: 600; margin-bottom: 16px;">
🚀 Get Started with System:
</h3>
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td style="padding: 0 0 12px 0; color: #334155; font-size: 15px;">
<span style="color: #1e293b; font-weight: 500; margin-right: 10px;">•</span> Explore your dashboard and features
</td>
</tr>
<tr>
<td style="padding: 0 0 12px 0; color: #334155; font-size: 15px;">
<span style="color: #1e293b; font-weight: 500; margin-right: 10px;">•</span> Connect with our support team
</td>
</tr>
<tr>
<td style="padding: 0 0 0 0; color: #334155; font-size: 15px;">
<span style="color: #1e293b; font-weight: 500; margin-right: 10px;">•</span> Stay updated with latest features
</td>
</tr>
</table>
</div>
<!-- Support Information -->
<p style="margin: 0 0 30px 0; font-size: 15px; line-height: 1.6; color: #475569;">
If you need any assistance, feel free to reach out to us.
</p>
<!-- Welcome Message -->
<div style="background-color: #f0f9ff; border-radius: 8px; padding: 20px; margin: 30px 0;">
<p style="margin: 0; font-size: 16px; line-height: 1.6; color: #0c4a6e;">
<span style="font-size: 20px; display: block; margin-bottom: 8px;">👋</span>
Once again, welcome aboard! We're thrilled to have you with us.
</p>
</div>
</td>
</tr>
<!-- Footer -->
<tr>
<td style="background-color: #f8fafc; border-radius: 0 0 16px 16px; padding: 30px 40px;">
<table width="100%" cellpadding="0" cellspacing="0" border="0">
<tr>
<td align="center" style="color: #94a3b8; font-size: 12px; line-height: 18px;">
<p style="margin: 5px 0;">
© {{currentYear}} Oceanic Maritime Solutions. All rights reserved.
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
<!-- End Email Content -->
<!-- Footer Note -->
<table width="600" cellpadding="0" cellspacing="0" border="0" style="max-width: 600px; margin-top: 20px;">
<tr>
<td align="center" style="color: #94a3b8; font-size: 11px; padding: 0 20px;">
This is an automated message, please do not reply to this email.
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
+23
View File
@@ -0,0 +1,23 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/utils/basis.util.js
const calculateBasis = (marginPercent, usdRate) => {
if (!usdRate || usdRate <= 0) {
throw new Error("Invalid USD rate");
}
const multiplier = (100 + marginPercent) / 100; // e.g. 25% -> 1.25
const basis = multiplier / usdRate;
return Number(basis).toFixed(10);
};
module.exports = { calculateBasis };
+129
View File
@@ -0,0 +1,129 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/utils/cache.util.js
const redis = require("../config/redisClient");
const { log } = require("./consoleLog.utill");
const db = require("../models");
const User = db.User;
///////////////////////////////
// User Cache //
/////////////////////////////
const CACHE_TTL = 20 * 60; // 20 minutes (in seconds)
const getCacheKey = (email) => `user:${email}`;
async function getCachedUser(email) {
const key = getCacheKey(email);
try {
// 1. Check Redis first
if (process.env.CACHE === "true") {
const cached = await redis.get(key);
if (cached) {
log("⚡ Cache HIT - returning user from Redis");
return JSON.parse(cached);
}
}
log("Cache MISS - fetching user from DB");
// 2. Fetch from DB
const user = await User.findOne({ where: { email } });
if (!user) return null;
const plainUser = user.get({ plain: true });
// 3. Store in Redis
if (process.env.CACHE === "true") {
await redis.set(
key,
JSON.stringify(plainUser),
"EX",
CACHE_TTL
);
}
return plainUser;
} catch (err) {
log("Error fetching user:", err);
throw new Error("Failed to fetch user");
}
}
// Invalidate cache
async function clearUserCache(email) {
const key = getCacheKey(email);
try {
await redis.del(key);
log(`Cache cleared for user: ${email}`);
} catch (err) {
log("Error clearing cache:", err);
}
}
///////////////////////////////
// Permission Cache //
/////////////////////////////
const PERM_TTL = 10 * 60;
const getPermKey = (userId) => `perm:user:${userId}`;
async function getCachedPermissions(userId) {
if (process.env.CACHE !== "true") return null;
try {
const cached = await redis.get(getPermKey(userId));
return cached ? JSON.parse(cached) : null;
} catch (err) {
console.log("Cache read error:", err);
return null;
}
}
async function setCachedPermissions(userId, permissions) {
if (process.env.CACHE !== "true") return;
try {
await redis.set(
getPermKey(userId),
JSON.stringify(permissions),
"EX",
PERM_TTL
);
} catch (err) {
console.log("Cache write error:", err);
}
}
async function clearPermissionCache(userId) {
try {
await redis.del(getPermKey(userId));
} catch (err) {
console.log("Cache delete error:", err);
}
}
module.exports = {
getCachedUser,
clearUserCache,
getCachedPermissions,
setCachedPermissions,
clearPermissionCache
};
+88
View File
@@ -0,0 +1,88 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/utils/calendar.util.js
const db = require("../models");
const { log } = require("./consoleLog.utill");
const Calendar = db.calender;
const CalenderHoliday = db.calenderHolidays;
const calculateDeadline = async () => {
let deadlineDate = new Date();
const currentYear = deadlineDate.getFullYear();
// Get calendar for current year
const calendar = await Calendar.findOne({
where: {
year: currentYear
}
});
if (!calendar) {
throw new Error(`Calendar for year ${currentYear} not found`);
}
deadlineDate.setDate(deadlineDate.getDate() + 1);
while (true) {
const month = deadlineDate.getMonth() + 1;
const day = deadlineDate.getDate();
// Get holidays for this month
const holidays = await CalenderHoliday.findAll({
where: {
calender_id: calendar.calender_id,
month: month,
shouldConsiderAsHoliday: true
}
});
const isWeekend =
deadlineDate.getDay() === 0 ||
deadlineDate.getDay() === 6;
const holiday = holidays.find(
h => h.day === day
);
const isHoliday =
isWeekend || !!holiday;
log(
`${deadlineDate.toISOString().split("T")[0]} => Holiday: ${isHoliday}`
);
if (!isHoliday) {
return deadlineDate;
}
// Move one more day
deadlineDate.setDate(
deadlineDate.getDate() + 1
);
}
};
module.exports = {
calculateDeadline
};
+53
View File
@@ -0,0 +1,53 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/utils/consoleLog.util.js
const isDevelopment = process.env.NODE_ENV === "development";
const logQueue = require("../queues/log.queue");
const log = async (...args) => {
if (isDevelopment) {
console.log(...args);
return;
}
try {
const message = args
.map((arg) => {
if (arg instanceof Error) {
return `${arg.message}\n${arg.stack}`;
}
if (typeof arg === "object") {
return JSON.stringify(arg);
}
return String(arg);
})
.join(" ");
await logQueue.add("log", {
message,
timestamp: new Date().toISOString(),
});
} catch (err) {
console.error("Log queue failed:", err);
}
};
function runningEnvironment() {
return !isDevelopment;
}
module.exports = {
log,
runningEnvironment,
};
+101
View File
@@ -0,0 +1,101 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/utils/document.util.js
const { Document } = require("../models");
const { generateId, generateDocumentReferenceNo } = require("./idGen.util");
const {log} = require("./consoleLog.utill");
const DOCUMENT_STATUS = {
DRAFT: "DRAFT",
FINAL: "FINAL",
};
const validateStatus = (status) => {
if (!Object.values(DOCUMENT_STATUS).includes(status)) {
throw new Error(
`Invalid status "${status}". Allowed values: ${Object.values(
DOCUMENT_STATUS,
).join(", ")}`,
);
}
};
const createDraftDocument = async (document, doc_data) => {
log("Creating draft document data...");
log("Incoming document data:", JSON.stringify(doc_data, null, 2));
const result = await Document.create({
doc_id: generateId(),
reference_no: doc_data.reference_no || "N/A",
doc_type: document,
data: doc_data,
status: DOCUMENT_STATUS.DRAFT,
});
log("Draft document created with ID:", result.doc_id);
return result;
};
const finalizeDocument = async (document, doc_data) => {
if (!doc_data.doc_id) {
throw new Error("Document data ID is required for FINAL status.");
}
const [updatedRows] = await Document.update(
{
reference_no: doc_data.reference_no || "N/A",
doc_type: document,
data: doc_data,
status: DOCUMENT_STATUS.FINAL,
},
{
where: {
doc_id: doc_data.doc_id,
},
},
);
if (updatedRows === 0) {
throw new Error("Document data not found or already updated.");
}
return await Document.findOne({
where: {
doc_id: doc_data.doc_id,
},
});
};
const createDocumentData = async (document, doc_data, status) => {
try {
validateStatus(status);
switch (status) {
case DOCUMENT_STATUS.DRAFT:
return await createDraftDocument(document, doc_data);
case DOCUMENT_STATUS.FINAL:
return await finalizeDocument(document, doc_data);
default:
throw new Error(`Unhandled document status: ${status}`);
}
} catch (error) {
console.error("❌ Document data operation failed:", error.message);
throw error;
}
};
module.exports = {
createDocumentData,
DOCUMENT_STATUS,
};
+168
View File
@@ -0,0 +1,168 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/utils/documentJob.util.js
const documentQueue = require("../queues/pdf.queue");
/**
* Queue a document generation job
* @param {string} document - Document type (e.g., 'precost')
* @param {string} documentType - Format (e.g., 'pdf', 'excel')
* @param {object} data - Data to pass to document generator
* @returns {Promise<object>} Job object with id
*/
const queueDocumentJob = async (document, documentType, data) => {
try {
const job = await documentQueue .add("generate-document", {
document,
documentType,
data,
});
return {
success: true,
jobId: job.id,
createdAt: new Date(),
};
} catch (error) {
throw new Error(`Failed to queue document job: ${error.message}`);
}
};
/**
* Get job status
* @param {string|number} jobId - Job ID
* @returns {Promise<object>} Job status and details
*/
const getDocumentJobStatus = async (jobId) => {
try {
const job = await documentQueue .getJob(jobId);
if (!job) {
return {
success: false,
message: "Job not found",
};
}
const state = await job.getState();
const result = job.returnvalue;
return {
success: true,
jobId: job.id,
state,
result: state === "completed" ? result : null,
error: state === "failed" ? job.failedReason : null,
createdAt: new Date(job.timestamp),
startedAt: job.processedOn ? new Date(job.processedOn) : null,
completedAt: job.finishedOn ? new Date(job.finishedOn) : null,
};
} catch (error) {
throw new Error(`Failed to get job status: ${error.message}`);
}
};
/**
* Wait for job completion with timeout
* @param {string|number} jobId - Job ID
* @param {number} timeout - Timeout in milliseconds (default: 300000 = 5 minutes)
* @returns {Promise<object>} Job result when completed
*/
const waitForDocumentJob = async (jobId, timeout = 300000) => {
try {
const job = await documentQueue .getJob(jobId);
if (!job) {
throw new Error("Job not found");
}
return await job.waitUntilFinished(
{ timeout },
documentQueue .events
);
} catch (error) {
throw new Error(`Failed to wait for job: ${error.message}`);
}
};
/**
* Remove a job from queue
* @param {string|number} jobId - Job ID
* @returns {Promise<boolean>} Success status
*/
const cancelDocumentJob = async (jobId) => {
try {
const job = await documentQueue .getJob(jobId);
if (!job) {
throw new Error("Job not found");
}
await job.remove();
return true;
} catch (error) {
throw new Error(`Failed to cancel job: ${error.message}`);
}
};
/**
* Get all active jobs
* @returns {Promise<array>} Array of active job details
*/
const getActiveJobs = async () => {
try {
const jobs = await documentQueue .getActiveCount();
const activeJobs = await documentQueue .getJobs(["active"], 0, -1);
return {
count: jobs,
jobs: activeJobs.map((job) => ({
id: job.id,
data: job.data,
state: "active",
progress: job.progress(),
})),
};
} catch (error) {
throw new Error(`Failed to get active jobs: ${error.message}`);
}
};
/**
* Get all pending jobs
* @returns {Promise<array>} Array of pending job details
*/
const getPendingJobs = async () => {
try {
const jobs = await documentQueue .getWaitingCount();
const pendingJobs = await documentQueue .getJobs(["waiting"], 0, -1);
return {
count: jobs,
jobs: pendingJobs.map((job) => ({
id: job.id,
data: job.data,
state: "waiting",
})),
};
} catch (error) {
throw new Error(`Failed to get pending jobs: ${error.message}`);
}
};
module.exports = {
queueDocumentJob,
getDocumentJobStatus,
waitForDocumentJob,
cancelDocumentJob,
getActiveJobs,
getPendingJobs,
};
+91
View File
@@ -0,0 +1,91 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/utills/excel/confirmOrder.excel.js
const ExcelJS = require("exceljs");
/**
* Generate Excel template buffer
*/
exports.generateTemplate = async (items) => {
const workbook = new ExcelJS.Workbook();
const sheet = workbook.addWorksheet("Confirm Order");
sheet.columns = [
{ header: "Item ID", key: "id", width: 25 },
{ header: "Item Name", key: "item_name", width: 30 },
{ header: "Original Quantity", key: "quantity", width: 20 },
{ header: "Confirmed Quantity", key: "confirmed_quantity", width: 20 },
];
const headerRow = sheet.getRow(1);
headerRow.eachCell((cell) => {
cell.fill = {
type: "pattern",
pattern: "solid",
fgColor: { argb: "1F4E78" },
};
cell.font = {
bold: true,
color: { argb: "FFFFFF" },
};
cell.alignment = {
vertical: "middle",
horizontal: "center",
};
cell.border = {
top: { style: "thin" },
left: { style: "thin" },
bottom: { style: "thin" },
right: { style: "thin" },
};
});
items.forEach((item) => {
sheet.addRow({
id: item.data_id,
item_name: item.item_name,
quantity: item.quantity,
confirmed_quantity: item.quantity,
});
});
return workbook.xlsx.writeBuffer();
};
/**
* Parse uploaded Excel buffer
*/
exports.parseExcel = async (buffer) => {
const workbook = new ExcelJS.Workbook();
await workbook.xlsx.load(buffer);
const sheet = workbook.getWorksheet("Confirm Order");
if (!sheet) throw new Error("Invalid sheet name");
const confirmedMap = new Map();
sheet.eachRow((row, rowNumber) => {
if (rowNumber === 1) return;
const itemId = row.getCell(1).value;
const confirmedQty = Number(row.getCell(4).value);
if (itemId) {
confirmedMap.set(itemId, confirmedQty);
}
});
return confirmedMap;
};
@@ -0,0 +1,116 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/utills/generateExcelTemplate.utill.js
const ExcelJS = require("exceljs");
exports.generateExcelTemplate = async (columns, vendors) => {
const workbook = new ExcelJS.Workbook();
// =========================
// Template Sheet
// =========================
const worksheet = workbook.addWorksheet("Template");
const headers = columns.map((col) =>
col.is_required ? `${col.column_name} *` : col.column_name,
);
const headerRow = worksheet.addRow(headers);
headerRow.eachCell((cell) => {
cell.font = { bold: true };
cell.alignment = { vertical: "middle", horizontal: "center" };
});
headers.forEach((header, i) => {
worksheet.getColumn(i + 1).width = Math.max(header.length + 5, 20);
});
columns.forEach((col, index) => {
const headerCell = worksheet.getCell(1, index + 1);
if (col.is_required) {
headerCell.fill = {
type: "pattern",
pattern: "solid",
fgColor: { argb: "FFF4CCCC" },
};
headerCell.font = {
bold: true,
color: { argb: "FF000000" },
};
} else {
headerCell.fill = {
type: "pattern",
pattern: "solid",
fgColor: { argb: "FFFFF2CC" },
};
headerCell.font = {
bold: true,
color: { argb: "FF000000" },
};
}
if (col.validation_type === "number") {
for (let i = 2; i <= 100; i++) {
worksheet.getCell(i, index + 1).dataValidation = {
type: "whole",
operator: "greaterThanOrEqual",
formulae: [0],
showErrorMessage: true,
error: "Please enter a valid number",
};
}
}
});
worksheet.views = [{ state: "frozen", ySplit: 1 }];
// =========================
// Vendors Sheet
// =========================
const vendorSheet = workbook.addWorksheet("Vendors");
const vendorHeader = vendorSheet.addRow(["Supplier ID", "Supplier Name"]);
vendorHeader.eachCell((cell) => {
cell.font = {
bold: true,
color: { argb: "FFFFFFFF" },
};
cell.alignment = {
vertical: "middle",
horizontal: "center",
};
cell.fill = {
type: "pattern",
pattern: "solid",
fgColor: { argb: "4472C4" },
};
});
// Add vendor rows
vendors.forEach((vendor) => {
vendorSheet.addRow([vendor.vendor_id, vendor.name]);
});
// Optional styling
vendorSheet.columns = [{ width: 25 }, { width: 35 }];
vendorSheet.views = [{ state: "frozen", ySplit: 1 }];
return workbook;
};
+101
View File
@@ -0,0 +1,101 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/utills/validateExcel.utill.js
const ExcelJS = require("exceljs");
exports.validateExcel = async (buffer, columns) => {
const workbook = new ExcelJS.Workbook();
await workbook.xlsx.load(buffer);
const worksheet = workbook.worksheets[0];
if (!worksheet) {
throw { status: 400, message: "Excel sheet is empty" };
}
const expectedHeaders = columns.map((col) =>
col.is_required ? `${col.column_name} *` : col.column_name
);
const headerRow = worksheet.getRow(1);
for (let i = 1; i <= expectedHeaders.length; i++) {
const cellValue = (headerRow.getCell(i).value || "").toString().trim();
if (cellValue !== expectedHeaders[i - 1]) {
throw {
status: 400,
message: `Invalid header in column ${i}`,
expected: expectedHeaders[i - 1],
received: cellValue,
};
}
}
const errors = [];
const validData = [];
worksheet.eachRow((row, rowNumber) => {
if (rowNumber === 1) return;
let rowData = {};
let rowHasError = false;
columns.forEach((col, index) => {
let value = row.getCell(index + 1).value;
if (value && typeof value === "object") {
value = value.text || value.result || "";
}
value = value?.toString().trim() || "";
if (col.is_required && !value) {
errors.push({
row: rowNumber,
column: col.column_name,
message: "Required field is missing",
});
rowHasError = true;
}
if (value) {
if (col.validation_type === "number" && isNaN(value)) {
errors.push({
row: rowNumber,
column: col.column_name,
message: "Must be a number",
});
rowHasError = true;
}
if (
col.validation_type === "boolean" &&
!["true", "false"].includes(value.toLowerCase())
) {
errors.push({
row: rowNumber,
column: col.column_name,
message: "Must be true or false",
});
rowHasError = true;
}
}
rowData[col.column_name] = value;
});
if (!rowHasError) {
validData.push(rowData);
}
});
return { errors, validData };
};
+24
View File
@@ -0,0 +1,24 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/utils/hashPassword.util.js
const bcrypt = require("bcrypt");
const hashPassword = async (password) => {
const saltRounds = 10;
const hashedPassword = await bcrypt.hash(password, saltRounds);
return hashedPassword;
}
const checkPassword = async (password, hashedPassword) => {
return await bcrypt.compare(password, hashedPassword);
}
module.exports = { hashPassword, checkPassword };
+33
View File
@@ -0,0 +1,33 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/utils/id/confirmOrder.util.js
const { nextSequence, pad } = require("../referenceNumber.util");
const { sequelize } = require("../../models");
const generateOrderNumber = async () => {
const now = new Date();
const yy = String(now.getFullYear()).slice(-2);
const mm = String(now.getMonth() + 1).padStart(2, "0");
const period = `${yy}${mm}`;
const sequenceKey = `ORD-${period}`;
return await sequelize.transaction(async (transaction) => {
const nextNo = await nextSequence(sequenceKey, transaction);
return `OSC/${period}/${pad(nextNo)}`;
});
};
module.exports = { generateOrderNumber };
+26
View File
@@ -0,0 +1,26 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/utils/id/dispatchNote.util.js
const { nextSequence, pad } = require("../referenceNumber.util");
const { sequelize } = require("../../models");
const generateDeliveryNote = async (orderNo) => {
const sequenceKey = `DN-${orderNo}`;
return await sequelize.transaction(async (transaction) => {
const nextNo = await nextSequence(sequenceKey, transaction);
return `DN/${orderNo}/${String(nextNo).padStart(2, "0")}`;
});
};
module.exports = { generateDeliveryNote };
+29
View File
@@ -0,0 +1,29 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/utils/id/inquiry.util.js
const { nextSequence, pad } = require("../referenceNumber.util");
const { sequelize } = require("../../models");
const {log} = require("../consoleLog.utill");
const generateInquiryNumber = async (port) => {
const year = new Date().getFullYear();
const sequenceKey = `INQ-${year}-${port}`;
return await sequelize.transaction(async (transaction) => {
const nextNo = await nextSequence(sequenceKey, transaction);
log(`Generated inquiry number: OSC/INQ/${year}/${port}/${pad(nextNo)}`);
return `OSC/INQ/${year}/${port}/${pad(nextNo)}`;
});
};
module.exports = { generateInquiryNumber };
+25
View File
@@ -0,0 +1,25 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/utils/id/po.util.js
const { nextSequence, pad } = require("../referenceNumber.util");
const { sequelize } = require("../../models");
const generatePONumber = async (orderNo) => {
const sequenceKey = `PO-${orderNo}`;
return await sequelize.transaction(async (transaction) => {
const nextVendor = await nextSequence(sequenceKey, transaction);
return `PO/${orderNo}/V${String(nextVendor).padStart(2, "0")}`;
});
};
module.exports = { generatePONumber };
+32
View File
@@ -0,0 +1,32 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/utils/id/quotation.util.js
const { nextSequence, pad } = require("../referenceNumber.util");
const { sequelize } = require("../../models");
const generateRFQNumber = async () => {
const now = new Date();
const yy = String(now.getFullYear()).slice(-2);
const mm = String(now.getMonth() + 1).padStart(2, "0");
const period = `${yy}${mm}`;
const sequenceKey = `RFQ-${period}`;
return await sequelize.transaction(async (transaction) => {
const nextNo = await nextSequence(sequenceKey, transaction);
return `RFQ/OSC/${period}/${pad(nextNo)}`;
});
};
module.exports = { generateRFQNumber };
+160
View File
@@ -0,0 +1,160 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/utils/idGen.util.js
const { v4: uuidv4 } = require("uuid");
const { nextSequence } = require("./referenceNumber.util");
const {log} = require("./consoleLog.utill");
const {generateInquiryNumber} = require("./id/inquiryID.utill");
const { generateRFQNumber } = require("./id/quotation.utill");
const {generatePONumber} = require("./id/po.utill");
const { generateDeliveryNote } = require("./id/dispatchNote.utill");
const generateUserId = () => {
return "usr_" + Math.random().toString(36).slice(2, 10);
};
const generateClientId = () => {
const prefix = "cli_";
return prefix + uuidv4();
};
const generateInquRefNo = async (port) => {
const reference_number = await generateInquiryNumber(port);
log("Generated Number", reference_number)
return reference_number;
};
const generateInquId = async (port) => {
const prefix = "inq_";
const inquID = prefix + uuidv4();
return inquID;
};
const generateCategoryId = () => {
const prefix = "cte_";
return prefix + uuidv4();
};
const generateRemarkId = () => {
const prefix = "rmk_";
return prefix + uuidv4();
};
const generateTodoId = () => {
const prefix = "tdo_";
return prefix + uuidv4();
};
const generateVendorStatusId = () => {
const prefix = "vst_";
return prefix + uuidv4();
}
const generateVendorId = () => {
const prefix = "vnd_";
return prefix + uuidv4();
}
const generateVendorPICId = () => {
const prefix = "vpic_";
return prefix + uuidv4();
}
const generateVendorDocumentId = () => {
const prefix = "vdoc_";
return prefix + uuidv4();
}
const generateColumnId = () => {
const prefix = "col_";
return prefix + uuidv4();
}
const generatePreCostId = () => {
const prefix = "pc_";
return prefix + uuidv4();
}
const generatePreCostPaymanentId = () => {
const prefix = "pcp_";
return prefix + uuidv4();
}
const generateCalenderId = () => {
const prefix = "cal_";
return prefix + uuidv4();
}
const generateId = () => {
return uuidv4();
};
const generateInvoiceNo = async () => {
const invoiceNumber = await nextSequence("invoice");
return `INV-${invoiceNumber.toString().padStart(6, "0")}`;
}
const generateDocumentReferenceNo = async (type) => {
const normalizedType = String(type).toUpperCase().replace(/[\s_-]+/g, "");
if (normalizedType === "QUOTATION"){
// Implement quotation number generation logic here
return 'N/A'
} else if (normalizedType === "PRECOST"){
// Implement precost number generation logic here
const precostRef = await generateRFQNumber()
return precostRef
} else if (normalizedType === "PO"){
// Implement contract number generation logic here
const Ref = await generatePONumber("OSC/2602")
return Ref
} else if (normalizedType === "BILL"){
return 'N/A'
// Implement bill number generation logic here
} else if (normalizedType === "PAYMENT"){
return 'N/A'
// Implement payment number generation logic here
} else if (normalizedType === "INVOICE"){
// Implement invoice number generation logic here
const invoiceNo = await generateInvoiceNo();
return invoiceNo;
} else if (normalizedType === "CREDITNOTE") {
return 'N/A';
} else if (normalizedType === "DELIVERYNOTE") {
const orderNo = "OSC/2602";
const deliveryNoteNo = await generateDeliveryNote(orderNo);
return deliveryNoteNo;
}else {
throw new Error("Unsupported document type for ID generation");
}
}
module.exports = {
generateUserId,
generateClientId,
generateInquId,
generateInquRefNo,
generateId,
generateCategoryId,
generateRemarkId,
generateTodoId,
generateVendorStatusId,
generateVendorId,
generateVendorPICId,
generateVendorDocumentId,
generateColumnId,
generatePreCostId,
generatePreCostPaymanentId,
generateDocumentReferenceNo,
generateCalenderId,
};
+26
View File
@@ -0,0 +1,26 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/utils/imageToBase64.utill.js
const axios = require("axios");
const imageToBase64 = async (url) => {
const response = await axios.get(url, {
responseType: "arraybuffer",
});
const base64 = Buffer.from(response.data, "binary").toString("base64");
const mimeType = response.headers["content-type"];
return `data:${mimeType};base64,${base64}`;
};
module.exports = { imageToBase64 };
+36
View File
@@ -0,0 +1,36 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/utils/jwt.util.js
const jwt = require("jsonwebtoken");
require("dotenv").config();
const JWT_SECRET = process.env.JWT_SECRET || "your_jwt_secret_key";
const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || "1d"; // token validity
/**
* Generate JWT token
* @param {Object} payload - usually { id, email, role }
* @returns string
*/
const generateToken = (payload) => {
return jwt.sign(payload, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN });
};
/**
* Verify JWT token
* @param {string} token
* @returns payload or throws error
*/
const verifyToken = (token) => {
return jwt.verify(token, JWT_SECRET);
};
module.exports = { generateToken, verifyToken };
+89
View File
@@ -0,0 +1,89 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/utils/mailer.util.js
const nodemailer = require("nodemailer");
const fs = require("fs");
const path = require("path");
const mailConfig = require("../config/mail.config");
const transporter = nodemailer.createTransport({
host: mailConfig.host,
port: mailConfig.port,
secure: mailConfig.secure,
auth: mailConfig.auth,
});
transporter.verify((err) => {
if (err) console.error("Mail server connection failed", err);
else console.log("Mail server ready");
});
// Utility to load template and replace placeholders
const loadTemplate = (templateName, variables = {}) => {
const templatePath = path.join(__dirname, "../templates/emails", `${templateName}.html`);
let template = fs.readFileSync(templatePath, "utf-8");
Object.keys(variables).forEach((key) => {
const regex = new RegExp(`{{${key}}}`, "g");
template = template.replace(regex, variables[key]);
});
return template;
};
const send = async ({ to, subject, templateName, templateVars = {}, text }) => {
const html = templateName ? loadTemplate(templateName, templateVars) : undefined;
const mailOptions = {
from: mailConfig.from,
to,
subject,
text,
html,
};
try {
const info = await transporter.sendMail(mailOptions);
console.log("Email sent:", info.messageId);
return info;
} catch (err) {
console.error("Error sending email:", err);
throw err;
}
};
const sendMail = async (options) => {
const today = new Date();
// Full formatted date: "January 1, 2026"
const formattedDate = today.toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
});
// Only year: "2026"
const currentYear = today.getFullYear();
const templateVars = {
...options.templateVars, // keep existing vars
currentDate: formattedDate,
currentYear: currentYear,
};
return send({ ...options, templateVars });
};
module.exports = { sendMail };
View File
+38
View File
@@ -0,0 +1,38 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/utils/otp.util.js
const otpCache = new Map();
function generateOTP(key){
const otp = Math.floor(100000 + Math.random() * 900000).toString();
saveOTP(key, otp);
return otp;
}
function saveOTP(key, otp, ttl = 5 * 60 * 1000) { // default TTL: 5 mins
const expiresAt = Date.now() + ttl;
otpCache.set(key, { otp, expiresAt });
}
// Validate OTP
function validateOTP(key, otp) {
const record = otpCache.get(key);
if (!record) return false;
if (Date.now() > record.expiresAt) {
otpCache.delete(key);
return false;
}
const isValid = record.otp === otp;
if (isValid) otpCache.delete(key); // OTP can be used only once
return isValid;
}
module.exports = { generateOTP, validateOTP };

Some files were not shown because too many files have changed in this diff Show More