first commit
This commit is contained in:
@@ -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 };
|
||||
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* 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/passwordReset.util.js
|
||||
|
||||
const crypto = require("crypto");
|
||||
|
||||
const createRedisConnection = require("../config/redis.config");
|
||||
const redis = createRedisConnection();
|
||||
|
||||
const db = require("../models");
|
||||
|
||||
const User = db.User;
|
||||
|
||||
const { log } = require("./consoleLog.utill");
|
||||
const { hashPassword } = require("./hashPassword.util");
|
||||
|
||||
const RESET_TOKEN_TTL = process.env.RESET_TOKEN_TTL || 10 * 60; // 10 minutes
|
||||
|
||||
/**
|
||||
* Generate password reset token
|
||||
*
|
||||
* @param {string} userId
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
async function generateResetToken(userId) {
|
||||
try {
|
||||
const token = crypto.randomBytes(32).toString("hex");
|
||||
|
||||
const tokenHash = crypto
|
||||
.createHash("sha256")
|
||||
.update(token)
|
||||
.digest("hex");
|
||||
|
||||
await redis.set(
|
||||
`passwordReset:user:${userId}`,
|
||||
tokenHash,
|
||||
"EX",
|
||||
RESET_TOKEN_TTL
|
||||
);
|
||||
log(`Generated password reset token for user ${userId} with TTL of ${RESET_TOKEN_TTL} seconds, Generated token:`, token);
|
||||
return token;
|
||||
} catch (error) {
|
||||
log("Password reset token generation failed", error);
|
||||
throw new Error("Failed to generate password reset token");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset password using token
|
||||
*
|
||||
* @param {string} userId
|
||||
* @param {string} token
|
||||
* @param {string} newPassword
|
||||
*/
|
||||
async function resetPassword(userId, token, newPassword) {
|
||||
try {
|
||||
const storedHash = await redis.get(
|
||||
`passwordReset:user:${userId}`
|
||||
);
|
||||
|
||||
if (!storedHash) {
|
||||
throw new Error("Invalid or expired reset token");
|
||||
}
|
||||
|
||||
const tokenHash = crypto
|
||||
.createHash("sha256")
|
||||
.update(token)
|
||||
.digest("hex");
|
||||
|
||||
const isValid =
|
||||
crypto.timingSafeEqual(
|
||||
Buffer.from(storedHash),
|
||||
Buffer.from(tokenHash)
|
||||
);
|
||||
|
||||
if (!isValid) {
|
||||
throw new Error("Invalid or expired reset token");
|
||||
}
|
||||
|
||||
const user = await User.findByPk(userId);
|
||||
|
||||
if (!user) {
|
||||
throw new Error("User not found");
|
||||
}
|
||||
|
||||
user.password = await hashPassword(newPassword);
|
||||
|
||||
await user.save();
|
||||
|
||||
await redis.del(`passwordReset:user:${userId}`);
|
||||
|
||||
log(
|
||||
`Password reset completed successfully for user ${userId}`
|
||||
);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
log("Password reset failed", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
generateResetToken,
|
||||
resetPassword
|
||||
};
|
||||
@@ -0,0 +1,197 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Niolla
|
||||
* All rights reserved.
|
||||
*/
|
||||
|
||||
// app/utils/pdfGenerator.js
|
||||
|
||||
const path = require("path");
|
||||
const ejs = require("ejs");
|
||||
const puppeteer = require("puppeteer");
|
||||
const fs = require("fs");
|
||||
const { imageToBase64 } = require("./imageToBase64.utill");
|
||||
const { log, runningEnvironment } = require("./consoleLog.utill");
|
||||
|
||||
let browser;
|
||||
|
||||
let executablePath = null;
|
||||
|
||||
const isProd = runningEnvironment();
|
||||
|
||||
if (isProd) {
|
||||
executablePath = "/usr/bin/chromium";
|
||||
} else {
|
||||
executablePath = puppeteer.executablePath();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Launch or reuse Puppeteer browser safely
|
||||
*/
|
||||
async function getBrowser() {
|
||||
if (!browser || !browser.isConnected?.()) {
|
||||
console.log("🚀 Launching Chromium from Docker system path...");
|
||||
|
||||
browser = await puppeteer.launch({
|
||||
headless: "new",
|
||||
executablePath: executablePath,
|
||||
args: [
|
||||
"--no-sandbox",
|
||||
"--disable-setuid-sandbox",
|
||||
"--disable-dev-shm-usage",
|
||||
"--disable-gpu",
|
||||
"--no-zygote"
|
||||
],
|
||||
});
|
||||
|
||||
console.log("✅ Browser launched");
|
||||
}
|
||||
|
||||
return browser;
|
||||
}
|
||||
|
||||
/**
|
||||
* Close browser safely
|
||||
*/
|
||||
async function closeBrowser() {
|
||||
if (browser) {
|
||||
try {
|
||||
await browser.close();
|
||||
} catch (e) {}
|
||||
browser = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Core PDF renderer
|
||||
*/
|
||||
async function renderPdf(templateFile, data = {}, opts = {}) {
|
||||
const templatePath = path.join(__dirname, "../templates", templateFile);
|
||||
|
||||
console.log("📝 Rendering:", templateFile);
|
||||
|
||||
// Convert image to base64 if needed
|
||||
if (data.logoUrl && !data.logoBase64) {
|
||||
try {
|
||||
data.logoBase64 = await imageToBase64(data.logoUrl);
|
||||
} catch (err) {
|
||||
console.error("❌ Logo conversion failed:", err.message);
|
||||
data.logoBase64 = null;
|
||||
}
|
||||
}
|
||||
|
||||
const html = await ejs.renderFile(templatePath, data);
|
||||
|
||||
const browserInstance = await getBrowser();
|
||||
const page = await browserInstance.newPage();
|
||||
|
||||
try {
|
||||
// Safety timeouts
|
||||
await page.setDefaultTimeout(60000);
|
||||
await page.setDefaultNavigationTimeout(60000);
|
||||
|
||||
await page.setViewport({ width: 1200, height: 800 });
|
||||
|
||||
console.log("📄 Loading HTML...");
|
||||
await page.setContent(html, { waitUntil: "networkidle0" });
|
||||
|
||||
await page.emulateMediaType("print");
|
||||
|
||||
// Wait for fonts
|
||||
await page.evaluate(() => document.fonts?.ready || Promise.resolve());
|
||||
|
||||
const margin = {
|
||||
top: 20,
|
||||
bottom: 20,
|
||||
left: 10,
|
||||
right: 10,
|
||||
...opts.margin,
|
||||
};
|
||||
|
||||
// Optional: measure content (kept for logging only)
|
||||
const contentHeightPx = await page.evaluate(() => {
|
||||
const body = document.body;
|
||||
const htmlEl = document.documentElement;
|
||||
|
||||
return Math.max(
|
||||
body.scrollHeight,
|
||||
body.offsetHeight,
|
||||
htmlEl.scrollHeight,
|
||||
htmlEl.offsetHeight
|
||||
);
|
||||
});
|
||||
|
||||
console.log("📏 Content height:", contentHeightPx, "px");
|
||||
|
||||
// ❗ REMOVED scaling logic (causes PDF distortion in production)
|
||||
|
||||
const showPageNumbers = opts.showPageNumbers === true;
|
||||
|
||||
console.log("⚙️ Generating PDF...");
|
||||
|
||||
const pdfBuffer = await page.pdf({
|
||||
format: "A4",
|
||||
printBackground: true,
|
||||
preferCSSPageSize: true,
|
||||
margin: {
|
||||
top: `${margin.top}mm`,
|
||||
bottom: `${margin.bottom}mm`,
|
||||
left: `${margin.left}mm`,
|
||||
right: `${margin.right}mm`,
|
||||
},
|
||||
displayHeaderFooter: showPageNumbers,
|
||||
headerTemplate: `<div></div>`,
|
||||
footerTemplate: showPageNumbers
|
||||
? `
|
||||
<div style="font-size:10px;width:100%;text-align:center;">
|
||||
Page <span class="pageNumber"></span> / <span class="totalPages"></span>
|
||||
</div>`
|
||||
: `<div></div>`,
|
||||
});
|
||||
|
||||
console.log("✅ PDF generated");
|
||||
|
||||
return pdfBuffer;
|
||||
} catch (err) {
|
||||
console.error("❌ PDF generation error:", err.message);
|
||||
throw err;
|
||||
} finally {
|
||||
await page.close().catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helpers
|
||||
*/
|
||||
const generatePdfFromPreCost = (data) =>
|
||||
renderPdf("documents/precost/precost.ejs", data);
|
||||
|
||||
const generatePdfFromInvoice = (data) =>
|
||||
renderPdf("documents/invoice/invoice.ejs", data);
|
||||
|
||||
const generatePdfFromDispatchNote = (data) =>
|
||||
renderPdf("documents/dispatchNote/dispatchNote.ejs", data);
|
||||
|
||||
const generatePdfFromDeliveryNote = (data) =>
|
||||
renderPdf("documents/deliveryNote/deliveryNote.ejs", data);
|
||||
|
||||
const generatePdfFromPO = (data) =>
|
||||
renderPdf("documents/po/po.ejs", data);
|
||||
|
||||
const generatePdfFromCustomDocument = (data) =>
|
||||
renderPdf("documents/customDocument/customDocument.ejs", data);
|
||||
|
||||
const generatePdfFromCreditNote = (data) =>
|
||||
renderPdf("documents/creditNote/creditNote.ejs", data);
|
||||
|
||||
module.exports = {
|
||||
generatePdfFromPreCost,
|
||||
generatePdfFromInvoice,
|
||||
generatePdfFromDispatchNote,
|
||||
generatePdfFromDeliveryNote,
|
||||
generatePdfFromPO,
|
||||
generatePdfFromCustomDocument,
|
||||
generatePdfFromCreditNote,
|
||||
renderPdf,
|
||||
closeBrowser,
|
||||
};
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
// utils/referenceNumber.util.js
|
||||
|
||||
const { referenceNumber } = require("../models");
|
||||
const { sequelize } = require("../models");
|
||||
const crypto = require("crypto");
|
||||
|
||||
const pad = (num, size = 4) => {
|
||||
return String(num).padStart(size, "0");
|
||||
};
|
||||
|
||||
const nextSequence = async (sequenceKey, transaction) => {
|
||||
const options = {
|
||||
where: { sequence_key: sequenceKey },
|
||||
};
|
||||
|
||||
// Only add transaction and lock if transaction is provided
|
||||
if (transaction) {
|
||||
options.transaction = transaction;
|
||||
options.lock = transaction.LOCK.UPDATE;
|
||||
}
|
||||
|
||||
let sequence = await referenceNumber.findOne(options);
|
||||
|
||||
if (!sequence) {
|
||||
const createOptions = transaction ? { transaction } : {};
|
||||
|
||||
sequence = await referenceNumber.create(
|
||||
{
|
||||
id: crypto.randomUUID(),
|
||||
sequence_key: sequenceKey,
|
||||
current_number: 1,
|
||||
},
|
||||
createOptions
|
||||
);
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
sequence.current_number += 1;
|
||||
|
||||
const saveOptions = transaction ? { transaction } : {};
|
||||
await sequence.save(saveOptions);
|
||||
|
||||
return sequence.current_number;
|
||||
};
|
||||
|
||||
module.exports = { nextSequence, pad };
|
||||
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* 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/s3Upload.util.js
|
||||
|
||||
const { PutObjectCommand, GetObjectCommand } = require("@aws-sdk/client-s3");
|
||||
|
||||
const { getSignedUrl } = require("@aws-sdk/s3-request-presigner");
|
||||
const s3 = require("../config/s3.config");
|
||||
const { v4: uuidv4 } = require("uuid");
|
||||
const path = require("path");
|
||||
const createRedisConnection = require("../config/redis.config");
|
||||
const redis = createRedisConnection();
|
||||
const { log } = require("./consoleLog.utill");
|
||||
|
||||
// Upload + return key (BEST PRACTICE)
|
||||
const uploadToS3 = async (file, usedFor) => {
|
||||
try {
|
||||
const fileExtension = path.extname(file.originalname);
|
||||
const fileName = `${uuidv4()}${fileExtension}`;
|
||||
let key = `uploads/${fileName}`;
|
||||
|
||||
if (usedFor === "asset") {
|
||||
key = `assets/${fileName}`;
|
||||
}
|
||||
|
||||
log("Uploading file to S3 with key:", key);
|
||||
|
||||
const params = {
|
||||
Bucket: process.env.AWS_S3_BUCKET_NAME,
|
||||
Key: key,
|
||||
Body: file.buffer,
|
||||
ContentType: file.mimetype,
|
||||
};
|
||||
|
||||
await s3.send(new PutObjectCommand(params));
|
||||
|
||||
// Return KEY (not URL)
|
||||
return key;
|
||||
} catch (error) {
|
||||
log("S3 Upload Error:", error);
|
||||
throw new Error("File upload failed");
|
||||
}
|
||||
};
|
||||
|
||||
// Generate signed URL with Redis caching
|
||||
const getSignedFileUrl = async (key) => {
|
||||
try {
|
||||
const cacheKey = `s3:url:${key}`;
|
||||
|
||||
// 1. Check cache
|
||||
const cachedUrl = await redis.get(cacheKey);
|
||||
|
||||
if (cachedUrl) {
|
||||
log("⚡ Cache HIT - returning signed URL from Redis");
|
||||
return cachedUrl;
|
||||
}
|
||||
|
||||
log("🐢 Cache MISS - generating new signed URL");
|
||||
|
||||
// 2. Generate signed URL
|
||||
const command = new GetObjectCommand({
|
||||
Bucket: process.env.AWS_S3_BUCKET_NAME,
|
||||
Key: key,
|
||||
});
|
||||
|
||||
const signedUrl = await getSignedUrl(s3, command, {
|
||||
expiresIn: 3600, // 1 hour
|
||||
});
|
||||
|
||||
// 3. Store in Redis (TTL: 55 minutes)
|
||||
await redis.set(cacheKey, signedUrl, "EX", 3300);
|
||||
|
||||
return signedUrl;
|
||||
} catch (error) {
|
||||
log("Signed URL Error:", error);
|
||||
throw new Error("Failed to generate file URL");
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
uploadToS3,
|
||||
getSignedFileUrl,
|
||||
};
|
||||
@@ -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/utils/validation/validateSupplier.util.js
|
||||
|
||||
|
||||
const db = require("../../models");
|
||||
const Supplier = db.Vendor;
|
||||
|
||||
const extractCellValue = (value) => {
|
||||
if (value && typeof value === "object") {
|
||||
return value.text || value.result || "";
|
||||
}
|
||||
return value?.toString().trim() || "";
|
||||
};
|
||||
|
||||
const validateSupplierColumn = async (worksheet, columns) => {
|
||||
const supplierIdCol =
|
||||
columns.findIndex((col) => col.column_name === "supplier_id") + 1;
|
||||
|
||||
if (!supplierIdCol) return;
|
||||
|
||||
const supplierIds = new Set();
|
||||
const errors = [];
|
||||
|
||||
// 🔹 Extract values (ONLY collect non-empty)
|
||||
for (let i = 2; i <= worksheet.rowCount; i++) {
|
||||
let value = extractCellValue(
|
||||
worksheet.getRow(i).getCell(supplierIdCol).value
|
||||
);
|
||||
|
||||
if (value) {
|
||||
supplierIds.add(value);
|
||||
}
|
||||
}
|
||||
|
||||
if (supplierIds.size === 0) return [];
|
||||
|
||||
// 🔹 Batch DB validation
|
||||
const suppliers = await Supplier.findAll({
|
||||
where: {
|
||||
vendor_id: [...supplierIds],
|
||||
},
|
||||
});
|
||||
|
||||
const validIds = new Set(suppliers.map((s) => s.vendor_id));
|
||||
|
||||
// 🔹 Validate ONLY non-empty values
|
||||
for (let i = 2; i <= worksheet.rowCount; i++) {
|
||||
let value = extractCellValue(
|
||||
worksheet.getRow(i).getCell(supplierIdCol).value
|
||||
);
|
||||
|
||||
if (value && !validIds.has(value)) {
|
||||
errors.push({
|
||||
row: i,
|
||||
column: "supplier_id",
|
||||
message: `Invalid supplier ID: ${value}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
};
|
||||
|
||||
module.exports = { validateSupplierColumn };
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* 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/venStat.utill.js
|
||||
|
||||
// Function to calculate vendor status based on approval flags
|
||||
const calVendorStatus = (is_md_approved, is_manager_approved) => {
|
||||
// 1. Nothing decided yet
|
||||
if (is_manager_approved === null && is_md_approved === null) {
|
||||
return "Pending Approval";
|
||||
}
|
||||
|
||||
// 2. Manager decided, MD pending
|
||||
if (is_md_approved === null) {
|
||||
if (is_manager_approved === true) return "Manager Approved";
|
||||
if (is_manager_approved === false) return "Manager Rejected";
|
||||
}
|
||||
|
||||
// 3. MD decision is FINAL (always overrides)
|
||||
if (is_md_approved === true) {
|
||||
return "Approved";
|
||||
}
|
||||
|
||||
if (is_md_approved === false) {
|
||||
return "MD Rejected";
|
||||
}
|
||||
|
||||
return "Pending Approval";
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
calVendorStatus,
|
||||
}
|
||||
Reference in New Issue
Block a user