first commit
This commit is contained in:
@@ -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(),
|
||||
};
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
@@ -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}>`,
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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",
|
||||
};
|
||||
@@ -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.",
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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."
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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.",
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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) {}
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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 };
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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 };
|
||||
@@ -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",
|
||||
});
|
||||
};
|
||||
@@ -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};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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();
|
||||
@@ -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;
|
||||
@@ -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 };
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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;"> </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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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 };
|
||||
@@ -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
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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 };
|
||||
};
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user