first commit
This commit is contained in:
@@ -0,0 +1,9 @@
|
|||||||
|
node_modules
|
||||||
|
npm-debug.log
|
||||||
|
Dockerfile
|
||||||
|
docker-compose.yml
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
.env
|
||||||
|
.env.prod
|
||||||
|
.env.sample
|
||||||
+53
@@ -0,0 +1,53 @@
|
|||||||
|
# App Details
|
||||||
|
APP_NAME=
|
||||||
|
|
||||||
|
# Database configuration variables
|
||||||
|
DB_HOST =
|
||||||
|
DB_USER =
|
||||||
|
DB_PASSWORD =
|
||||||
|
DB_NAME =
|
||||||
|
DB_PORT =
|
||||||
|
|
||||||
|
# Redis configuration variables
|
||||||
|
REDIS_HOST=localhost
|
||||||
|
REDIS_PORT=6379
|
||||||
|
|
||||||
|
# JWT secret key
|
||||||
|
JWT_SECRET = your_jwt_secret_key_here
|
||||||
|
JWT_EXPIRES_IN =1d
|
||||||
|
|
||||||
|
# AWS S3 configuration
|
||||||
|
AWS_ACCESS_KEY_ID=your_aws_access_key_id_here
|
||||||
|
AWS_SECRET_ACCESS_KEY=your_aws_secret_access_key_here
|
||||||
|
AWS_REGION=your_aws_region_here
|
||||||
|
AWS_S3_BUCKET_NAME=your_aws_bucket_name_here
|
||||||
|
|
||||||
|
# Mail configuration
|
||||||
|
MAIL_HOST=
|
||||||
|
MAIL_PORT=587
|
||||||
|
MAIL_USER=
|
||||||
|
MAIL_PASS=
|
||||||
|
MAIL_SECURE=false
|
||||||
|
MAIL_FROM=
|
||||||
|
|
||||||
|
# Documentation access credentials
|
||||||
|
DOCS_USER=
|
||||||
|
DOCS_PASS=
|
||||||
|
|
||||||
|
# Admin email for receiving notifications
|
||||||
|
MANAGER_EMAIL=
|
||||||
|
|
||||||
|
# Caching configuration
|
||||||
|
CACHE=true
|
||||||
|
|
||||||
|
# Application environment
|
||||||
|
NODE_ENV=development
|
||||||
|
|
||||||
|
# User default password
|
||||||
|
DEFAULT_PASSWORD=
|
||||||
|
|
||||||
|
# Frontend URL for CORS
|
||||||
|
FRONTEND_URL=http://localhost:3000
|
||||||
|
|
||||||
|
# Puppeteer executable path (if needed, otherwise Puppeteer will use the bundled Chromium)
|
||||||
|
PUPPETEER_EXECUTABLE_PATH = C:\Users\User\.cache\puppeteer\chrome-headless-shell\win64-142.0.7444.162\chrome-headless-shell-win64\chrome-headless-shell.exe
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
.env
|
||||||
|
.env.prod
|
||||||
|
/node_modules/
|
||||||
|
/.github/workflows/deploy-dev.yml
|
||||||
+44
@@ -0,0 +1,44 @@
|
|||||||
|
FROM node:20-slim
|
||||||
|
|
||||||
|
# Install Chromium + dependencies
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
chromium \
|
||||||
|
fonts-liberation \
|
||||||
|
libatk-bridge2.0-0 \
|
||||||
|
libatk1.0-0 \
|
||||||
|
libcups2 \
|
||||||
|
libxcomposite1 \
|
||||||
|
libxrandr2 \
|
||||||
|
libxdamage1 \
|
||||||
|
libgbm1 \
|
||||||
|
libasound2 \
|
||||||
|
libpangocairo-1.0-0 \
|
||||||
|
libpango-1.0-0 \
|
||||||
|
libnss3 \
|
||||||
|
libxss1 \
|
||||||
|
libgtk-3-0 \
|
||||||
|
libdrm2 \
|
||||||
|
libxshmfence1 \
|
||||||
|
ca-certificates \
|
||||||
|
--no-install-recommends \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Tell Puppeteer to use system Chromium
|
||||||
|
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium
|
||||||
|
|
||||||
|
# Prevent Puppeteer from downloading its own Chromium
|
||||||
|
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
|
||||||
|
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm ci --omit=dev
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
|
EXPOSE 3070
|
||||||
|
|
||||||
|
CMD ["node", "server.js"]
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
# Auth API
|
||||||
|
|
||||||
|
#### Request OTP
|
||||||
|
|
||||||
|
**Endpoint**
|
||||||
|
|
||||||
|
```
|
||||||
|
POST: http://localhost:3070/api/auth/req-otp
|
||||||
|
```
|
||||||
|
|
||||||
|
**Request Body**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"email": "sathira@niolla.lk",
|
||||||
|
"password": "Niolla@123"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Respond**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "OTP Sent Successfully"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Login
|
||||||
|
|
||||||
|
**Endpoint**
|
||||||
|
|
||||||
|
```
|
||||||
|
POST: http://localhost:3070/api/auth/login
|
||||||
|
```
|
||||||
|
|
||||||
|
**Request Body**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"email": "sathira@niolla.lk",
|
||||||
|
"otp": "922304"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Respond**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Login Successful",
|
||||||
|
"data": {
|
||||||
|
"id": "usr_5ff8afec-5ddc-47d9-a63f-b43df0d8c3b4",
|
||||||
|
"email": "sathira@niolla.lk",
|
||||||
|
"firstName": "Jhon",
|
||||||
|
"lastName": "Doe",
|
||||||
|
"role": "System Developer",
|
||||||
|
"accountType": "admin"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Get Current User
|
||||||
|
|
||||||
|
**Endpoint**
|
||||||
|
|
||||||
|
```
|
||||||
|
GET: http://localhost:3070/api/auth/me
|
||||||
|
```
|
||||||
|
|
||||||
|
**Authorization**: Required (Bearer token)
|
||||||
|
|
||||||
|
**Request Body**
|
||||||
|
|
||||||
|
```
|
||||||
|
No Body
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response (200)**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"authenticated": true,
|
||||||
|
"user": {
|
||||||
|
"id": "usr_5ff8afec-5ddc-47d9-a63f-b43df0d8c3b4",
|
||||||
|
"firstName": "Sathira",
|
||||||
|
"lastName": "Sri Sathsara",
|
||||||
|
"email": "sathira@niolla.lk",
|
||||||
|
"role": "System Developer",
|
||||||
|
"accountType": "admin",
|
||||||
|
"iat": 1778865265,
|
||||||
|
"exp": 1778868865,
|
||||||
|
"permissions": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Logout
|
||||||
|
|
||||||
|
**Endpoint**
|
||||||
|
|
||||||
|
```
|
||||||
|
POST: http://localhost:3070/api/auth/logout
|
||||||
|
```
|
||||||
|
|
||||||
|
**Request Body**
|
||||||
|
|
||||||
|
```
|
||||||
|
No Body
|
||||||
|
```
|
||||||
|
|
||||||
|
**Respond**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Logged out successfully"
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,740 @@
|
|||||||
|
# Document API Documentation
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The Document API allows authenticated users to generate documents asynchronously (PDF, Excel, etc.) from provided data. The system uses a queue-based architecture for reliable, scalable document generation.
|
||||||
|
|
||||||
|
**Key Features:**
|
||||||
|
- ✅ Non-blocking requests (returns immediately with jobId)
|
||||||
|
- ✅ Job status polling to track progress
|
||||||
|
- ✅ Automatic retries with exponential backoff
|
||||||
|
- ✅ Support for multiple document types and formats
|
||||||
|
- ✅ Concurrent document generation
|
||||||
|
- ✅ Case-insensitive document names with whitespace and punctuation normalization
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
Client Request → Queue Job → Return jobId (202)
|
||||||
|
↓
|
||||||
|
Worker processes
|
||||||
|
↓
|
||||||
|
Client polls status
|
||||||
|
↓
|
||||||
|
Job complete → Download file
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Endpoints
|
||||||
|
|
||||||
|
### 1. Generate Document
|
||||||
|
|
||||||
|
**Endpoint:** `POST /api/document/generate`
|
||||||
|
|
||||||
|
**URL:** `http://localhost:3070/api/document/generate`
|
||||||
|
|
||||||
|
**Authentication:** Required ✓
|
||||||
|
|
||||||
|
**Authorization:** Required - User must have one of the following roles:
|
||||||
|
- `admin`
|
||||||
|
- `management`
|
||||||
|
- `team_head`
|
||||||
|
- `user`
|
||||||
|
|
||||||
|
**HTTP Status:** `202 Accepted`
|
||||||
|
|
||||||
|
#### Request Headers
|
||||||
|
```
|
||||||
|
Content-Type: application/json
|
||||||
|
Authorization: Bearer <JWT_TOKEN>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Request Body
|
||||||
|
|
||||||
|
| Field | Type | Required | Description |
|
||||||
|
|-------|------|----------|-------------|
|
||||||
|
| `document` | string | Yes | Type of document (e.g., "INVOICE", "DISPATCHNOTE", "DELIVERYNOTE") |
|
||||||
|
| `documentType` | string | Yes | Output format (e.g., "pdf", "excel") |
|
||||||
|
| `documentData` | object | Yes | Data object for the document |
|
||||||
|
|
||||||
|
#### Response (202 Accepted)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Document generation started",
|
||||||
|
"jobId": "1"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Error Responses
|
||||||
|
|
||||||
|
**400 Bad Request:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"message": "document, documentType, and documentData are required"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**500 Server Error:**
|
||||||
|
```json
|
||||||
|
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"message": "Error message describing the issue"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Supported Document Values
|
||||||
|
|
||||||
|
You can send the document name in any case. Spaces, underscores, and hyphens are ignored by the generator.
|
||||||
|
|
||||||
|
- `PRECOST`
|
||||||
|
- `INVOICE`
|
||||||
|
- `DISPATCHNOTE`
|
||||||
|
- `DELIVERYNOTE`
|
||||||
|
- `CUSTOMDOCUMENT`
|
||||||
|
- `CREDITNOTE`
|
||||||
|
- `PO`
|
||||||
|
|
||||||
|
|
||||||
|
## Sample Request Bodies
|
||||||
|
|
||||||
|
All examples below are for `POST /api/document/generate` with `documentType: "pdf"`.
|
||||||
|
|
||||||
|
### PreCost
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"document": "PRECOST",
|
||||||
|
"documentType": "pdf",
|
||||||
|
"documentData": {
|
||||||
|
"vessel_name": "MV OCEAN STAR",
|
||||||
|
"supplyPort": "DUBAI",
|
||||||
|
"clientRfqNum": "RFQ-2026-001",
|
||||||
|
"omsRfqNum": "OMS-RFQ-0001",
|
||||||
|
"date": "08/07/2026",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"name": "Marine Rope",
|
||||||
|
"description": "High strength rope",
|
||||||
|
"qty": 10,
|
||||||
|
"unitPrice": 12.5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Safety Gloves",
|
||||||
|
"description": "Blue gloves",
|
||||||
|
"qty": 5,
|
||||||
|
"unitPrice": 8
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"sub_total": 125,
|
||||||
|
"discount": 0,
|
||||||
|
"additionalCharges": 0,
|
||||||
|
"total_cost": 125
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Invoice
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"document": "INVOICE",
|
||||||
|
"documentType": "pdf",
|
||||||
|
"documentData": {
|
||||||
|
"jobReference": "JOB-2026-001",
|
||||||
|
"poNumber": "PO-20254161",
|
||||||
|
"date": "08/07/2026",
|
||||||
|
"pageLabel": "Page 01 of 02",
|
||||||
|
"billToName": "SUNRICH SHIP CHANDLERS L.L.C",
|
||||||
|
"billToAddress": "BUSINESS BAY - ASPECT TOWER, 22ND FLOOR, OFFICE NO. 2201, P.O. BOX NO 39976, DUBAI",
|
||||||
|
"vesselName": "MV OCEAN STAR",
|
||||||
|
"purposeOfCall": "SUPPLY OF PROVISIONS",
|
||||||
|
"supplyDate": "08/07/2026",
|
||||||
|
"grt": "12345",
|
||||||
|
"imoNumber": "9876543",
|
||||||
|
"portCountry": "DUBAI / UAE",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"description": "Marine rope",
|
||||||
|
"remarks": "Delivered as requested",
|
||||||
|
"quantity": 10,
|
||||||
|
"unit_price": 12.5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Safety gloves",
|
||||||
|
"remarks": "Blue color",
|
||||||
|
"quantity": 5,
|
||||||
|
"unit_price": 8
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"subtotal": 165,
|
||||||
|
"tax": 0,
|
||||||
|
"discount": 0,
|
||||||
|
"total": 165,
|
||||||
|
"paymentTerms": "Payment due within 30 days",
|
||||||
|
"notes": "Please verify quantities upon delivery."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dispatch Note
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"document": "DISPATCHNOTE",
|
||||||
|
"documentType": "pdf",
|
||||||
|
"documentData": {
|
||||||
|
"referenceNumber": "DN/2026/001",
|
||||||
|
"date": "08/07/2026",
|
||||||
|
"vessel": "MV OCEAN STAR",
|
||||||
|
"captain": "CAPT. JOHN DOE",
|
||||||
|
"cook": "SAMUEL",
|
||||||
|
"agent": "OCEANIC AGENT LTD",
|
||||||
|
"details": "Delivery of provisions and stores",
|
||||||
|
"placeOfDelivery": "DUBAI PORT",
|
||||||
|
"eta": "08/07/2026 08:00",
|
||||||
|
"etd": "08/07/2026 18:00",
|
||||||
|
"numberOfCrew": 18,
|
||||||
|
"nextMainOrderIn": "24 HRS",
|
||||||
|
"port": "DUBAI",
|
||||||
|
"company": "OCEANIC SHIP CHANDLERS (PVT) LTD",
|
||||||
|
"fileNo": "FILE-2026-04",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"product": "Rice",
|
||||||
|
"qty": 10,
|
||||||
|
"unit": "BAGS",
|
||||||
|
"supp": "OMS",
|
||||||
|
"poNo": "PO-001",
|
||||||
|
"receivedQty": 10,
|
||||||
|
"issuedQty": 10,
|
||||||
|
"expDate": "12/07/2026",
|
||||||
|
"receivedTime": "09:15",
|
||||||
|
"date": "08/07/2026",
|
||||||
|
"rejects": "",
|
||||||
|
"fatCon": "",
|
||||||
|
"remark": "Delivered"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"showSignatures": true,
|
||||||
|
"preparedBy": "Admin User",
|
||||||
|
"approvedBy": "Manager User"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Delivery Note
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"document": "DELIVERYNOTE",
|
||||||
|
"documentType": "pdf",
|
||||||
|
"documentData": {
|
||||||
|
"referenceNumber": "DN/OSC/2602/01",
|
||||||
|
"date": "08/07/2026",
|
||||||
|
"billToName": "SUNRICH SHIP CHANDLERS L.L.C",
|
||||||
|
"billToAddress": "BUSINESS BAY - ASPECT TOWER, 22ND FLOOR, OFFICE NO. 2201, P.O. BOX NO 39976, DUBAI",
|
||||||
|
"supplyDate": "08/07/2026",
|
||||||
|
"poNumber": "PO-20254161",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"description": "Marine rope",
|
||||||
|
"remarks": "Delivered as requested",
|
||||||
|
"unit": "PCS",
|
||||||
|
"quantity": 10
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Safety gloves",
|
||||||
|
"remarks": "Blue color",
|
||||||
|
"unit": "BOX",
|
||||||
|
"quantity": 5
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Document
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"document": "CUSTOMDOCUMENT",
|
||||||
|
"documentType": "pdf",
|
||||||
|
"documentData": {
|
||||||
|
"title": "CUSTOM DOCUMENT",
|
||||||
|
"date": "08/07/2026",
|
||||||
|
"telePhone": "+94 112 083 206",
|
||||||
|
"emailAddress": "shipsupply@oceanicmsol.com",
|
||||||
|
"officeAddressLine1": "Level 5D, Valiant Towers, Nawala Mawatha",
|
||||||
|
"officeAddressLine2": "Colombo 02",
|
||||||
|
"officeAddressLine3": "SRI LANKA",
|
||||||
|
"directorOfCustoms": "The Director of Customs",
|
||||||
|
"vehicleNo": "WPLO 9767",
|
||||||
|
"permitNo": "MV20231214006001 / Permit No. MP20240717017006",
|
||||||
|
"chiefSecurityOfficer": "Chief Security Officer",
|
||||||
|
"exportGate": "Export Gate",
|
||||||
|
"slpa": "SLPA",
|
||||||
|
"gateNo": "Gate No. 03",
|
||||||
|
"permissionText": "Please grant permission to pass these Customs entry papers to supply goods to the vessel",
|
||||||
|
"companyName": "Oceanic Maritime Solutions (Pvt) Ltd",
|
||||||
|
"subtitle": "LICENSED SHIPCHANDLERS",
|
||||||
|
"sectionCode": "SC/FORM/06",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"quantity": 10,
|
||||||
|
"unit": "PCS",
|
||||||
|
"description": "Marine rope",
|
||||||
|
"rate": 12.5,
|
||||||
|
"total": 125,
|
||||||
|
"confirmOrderRate": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"quantity": 5,
|
||||||
|
"unit": "PCS",
|
||||||
|
"description": "Safety gloves",
|
||||||
|
"rate": 8,
|
||||||
|
"total": 40,
|
||||||
|
"confirmOrderRate": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Credit Note
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"document": "CREDITNOTE",
|
||||||
|
"documentType": "pdf",
|
||||||
|
"documentData": {
|
||||||
|
"companyName": "GREEK-LANKA MARITIME SERVICES (PVT) LTD",
|
||||||
|
"companyAddressLine1": "No. 56/2, Dharmapala Mw, Kotte",
|
||||||
|
"companyAddressLine2": "Sri Jayewardenepura",
|
||||||
|
"companyAddressLine3": "Sri Lanka",
|
||||||
|
"companyPostal": "Postal Code : 10100",
|
||||||
|
"companyContact": "Tel:+94 11 2083206 / Mobile (24/7) : +94 777 232 271",
|
||||||
|
"companyBR": "BR No : PV 0022630",
|
||||||
|
"companyLicense": "License No : SA00317-2024",
|
||||||
|
"crnNo": "GLMS/COLOMBO/474/CRN01",
|
||||||
|
"date": "26/07/2024",
|
||||||
|
"billToName": "Master & Owner of MV TEAM VENTURES",
|
||||||
|
"billToDetails": "VRIDHI MARITIME SHIP MANAGEMENT & OPERATION LLC\nOffice No: 2004, The Prism Tower, Dubai\n20th Floor, Business Bay,\nDubai - PO Box 29583.",
|
||||||
|
"vesselName": "MV \"TEAM VENTURES\"",
|
||||||
|
"grt": "25,543 MT",
|
||||||
|
"port": "REPAIRS AT COLOMBO DOCK YARD",
|
||||||
|
"imoNumber": "9339765",
|
||||||
|
"nameOfAgent": "29/04/2024 AT 21:00 HRS.LT",
|
||||||
|
"portCountry": "COLOMBO / SRI LANKA",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"description": "SIGN OFF C/E MR. DHANSINGH BHAURYAL AND M/S J/E MR. SENTHIL",
|
||||||
|
"amount": 50
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "VISA + Handling Charges - M/S M/S MR. SENTHIL",
|
||||||
|
"amount": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Transport from Airport to Dockyard",
|
||||||
|
"amount": 70
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"totalAmount": 220,
|
||||||
|
"approvedBy": "GLMS Finance Dept.",
|
||||||
|
"approvedByLabel": "Approved by GLMS Finance Dept.",
|
||||||
|
"departmentLabel": "GLMS - Accounts Department"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Get Job Status
|
||||||
|
|
||||||
|
**Endpoint:** `GET /api/document/job/:jobId/status`
|
||||||
|
|
||||||
|
**URL:** `http://localhost:3070/api/document/job/1/status`
|
||||||
|
|
||||||
|
**Authentication:** Required ✓
|
||||||
|
|
||||||
|
**Authorization:** Required
|
||||||
|
|
||||||
|
**HTTP Status:** `200 OK`
|
||||||
|
|
||||||
|
#### Request Headers
|
||||||
|
```
|
||||||
|
Authorization: Bearer <JWT_TOKEN>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Response (Waiting/Active)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"jobId": "1",
|
||||||
|
"state": "waiting",
|
||||||
|
"result": null,
|
||||||
|
"error": null,
|
||||||
|
"attempts": 0,
|
||||||
|
"stacktrace": []
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Response (Completed)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"jobId": "83",
|
||||||
|
"state": "completed",
|
||||||
|
"result": {
|
||||||
|
"fileName": "precost-1781614132061.pdf",
|
||||||
|
"mimeType": "application/pdf",
|
||||||
|
"size": 220008,
|
||||||
|
"s3Key": "uploads/16adf29b-c0c2-45f9-8808-d6e15cd3d755.pdf",
|
||||||
|
"documentId": "16adf29b-c0c2-45f9-8808-d6e15cd3d755"
|
||||||
|
},
|
||||||
|
"error": null,
|
||||||
|
"attempts": 1,
|
||||||
|
"stacktrace": []
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Response (Failed)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"jobId": "1",
|
||||||
|
"state": "failed",
|
||||||
|
"result": null,
|
||||||
|
"error": "Error message explaining the failure",
|
||||||
|
"attempts": 3,
|
||||||
|
"stacktrace": ["stack trace line 1", "stack trace line 2"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Job States
|
||||||
|
|
||||||
|
| State | Meaning | Next State |
|
||||||
|
|-------|---------|-----------|
|
||||||
|
| `waiting` | Job queued, waiting for worker | `active` |
|
||||||
|
| `active` | Worker currently processing | `completed` or `failed` |
|
||||||
|
| `completed` | Job done, result available | (final) |
|
||||||
|
| `failed` | Job failed after 3 retries | (final) |
|
||||||
|
| `delayed` | Scheduled for later processing | `waiting` |
|
||||||
|
|
||||||
|
#### Error Responses
|
||||||
|
|
||||||
|
**404 Not Found:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"message": "Job not found"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Download Document
|
||||||
|
|
||||||
|
**Endpoint:** `GET /api/document/download/:uuid`
|
||||||
|
|
||||||
|
**URL:** `http://localhost:3070/api/document/download/16adf29b-c0c2-45f9-8808-d6e15cd3d755`
|
||||||
|
|
||||||
|
**Authentication:** Required ✓
|
||||||
|
|
||||||
|
**Authorization:** Required
|
||||||
|
|
||||||
|
**HTTP Status:** `200 OK`
|
||||||
|
|
||||||
|
#### Request Headers
|
||||||
|
```
|
||||||
|
Authorization: Bearer <JWT_TOKEN>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Response (Success)
|
||||||
|
|
||||||
|
Returns the binary file with appropriate headers:
|
||||||
|
|
||||||
|
| Header | Value |
|
||||||
|
|--------|-------|
|
||||||
|
| `Content-Type` | `application/pdf` or `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet` |
|
||||||
|
| `Content-Disposition` | `attachment; filename="[fileName]"` |
|
||||||
|
|
||||||
|
**Response Body:** Binary file content
|
||||||
|
|
||||||
|
#### Error Responses
|
||||||
|
|
||||||
|
**400 Bad Request:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"message": "fileName is required"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**404 Not Found:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"message": "Document not found"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. Cancel Job
|
||||||
|
|
||||||
|
**Endpoint:** `DELETE /api/document/job/:jobId`
|
||||||
|
|
||||||
|
**URL:** `http://localhost:3070/api/document/job/1`
|
||||||
|
|
||||||
|
**Authentication:** Required ✓
|
||||||
|
|
||||||
|
**Authorization:** Required
|
||||||
|
|
||||||
|
**HTTP Status:** `200 OK`
|
||||||
|
|
||||||
|
#### Request Headers
|
||||||
|
```
|
||||||
|
Authorization: Bearer <JWT_TOKEN>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Response (Success)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Job cancelled successfully",
|
||||||
|
"jobId": "1"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Error Responses
|
||||||
|
|
||||||
|
**404 Not Found:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"message": "Job not found"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Document Data Notes
|
||||||
|
|
||||||
|
The generator is tolerant of different request shapes and common field aliases. For example, it accepts both `documentData` and nested `data`, and it normalizes document names by removing spaces, underscores, and hyphens.
|
||||||
|
|
||||||
|
If a document does not use every field in a sample payload, you can omit the unused keys.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Complete Workflow Example
|
||||||
|
|
||||||
|
### 1. Generate Document
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:3070/api/document/generate \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
|
||||||
|
-d '{
|
||||||
|
"document": "precost",
|
||||||
|
"documentType": "pdf",
|
||||||
|
"documentData": {
|
||||||
|
"customerName": "Acme Corp",
|
||||||
|
"items": [
|
||||||
|
{"name": "Item 1", "description": "Test", "qty": 2, "unitPrice": 100}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Document generation started",
|
||||||
|
"jobId": "1"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Check Job Status
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:3070/api/document/job/1/status \
|
||||||
|
-H "Authorization: Bearer YOUR_JWT_TOKEN"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Polling Response (Still Processing):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"jobId": "1",
|
||||||
|
"state": "active"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Polling Response (Completed):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"jobId": "1",
|
||||||
|
"state": "completed",
|
||||||
|
"result": {
|
||||||
|
"fileName": "precost-1715339340000.pdf"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Download File
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:3070/api/document/download/16adf29b-c0c2-45f9-8808-d6e15cd3d755 \
|
||||||
|
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
|
||||||
|
-o my-document.pdf
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## JavaScript/Fetch Example
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
async function generateAndDownloadDocument(token) {
|
||||||
|
// Step 1: Generate document
|
||||||
|
const generateRes = await fetch("/api/document/generate", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Authorization": `Bearer ${token}`,
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
document: "precost",
|
||||||
|
documentType: "pdf",
|
||||||
|
documentData: {
|
||||||
|
customerName: "Acme Corp",
|
||||||
|
items: [
|
||||||
|
{ name: "Item 1", description: "Test", qty: 2, unitPrice: 100 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const { jobId } = await generateRes.json();
|
||||||
|
console.log("Job queued:", jobId);
|
||||||
|
|
||||||
|
// Step 2: Poll for completion
|
||||||
|
let isComplete = false;
|
||||||
|
let result = null;
|
||||||
|
|
||||||
|
while (!isComplete) {
|
||||||
|
const statusRes = await fetch(`/api/document/job/${jobId}/status`, {
|
||||||
|
headers: { "Authorization": `Bearer ${token}` }
|
||||||
|
});
|
||||||
|
|
||||||
|
const status = await statusRes.json();
|
||||||
|
console.log("Job state:", status.state);
|
||||||
|
|
||||||
|
if (status.state === "completed") {
|
||||||
|
result = status.result;
|
||||||
|
isComplete = true;
|
||||||
|
} else if (status.state === "failed") {
|
||||||
|
throw new Error(`Job failed: ${status.error}`);
|
||||||
|
} else {
|
||||||
|
// Wait 2 seconds before polling again
|
||||||
|
await new Promise(r => setTimeout(r, 2000));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 3: Download the file
|
||||||
|
const downloadRes = await fetch(`/api/document/download/${result.fileName}`, {
|
||||||
|
headers: { "Authorization": `Bearer ${token}` }
|
||||||
|
});
|
||||||
|
|
||||||
|
const blob = await downloadRes.blob();
|
||||||
|
|
||||||
|
// Trigger browser download
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = url;
|
||||||
|
a.download = result.fileName;
|
||||||
|
a.click();
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
|
||||||
|
console.log("Download complete!");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Important Notes
|
||||||
|
|
||||||
|
### Authentication & Authorization
|
||||||
|
- All endpoints require JWT token in `Authorization: Bearer <token>` header
|
||||||
|
- User must have one of these roles: `admin`, `management`, `team_head`, `user`
|
||||||
|
|
||||||
|
### Polling Strategy
|
||||||
|
- Poll status every 2-5 seconds
|
||||||
|
- Max recommended: 60 attempts (5 minutes timeout)
|
||||||
|
- Move to download immediately when state is `completed`
|
||||||
|
|
||||||
|
### Error Handling
|
||||||
|
- Jobs automatically retry up to 3 times with exponential backoff
|
||||||
|
- Failed jobs remain in queue for debugging
|
||||||
|
- Check `error` and `stacktrace` fields for failure details
|
||||||
|
|
||||||
|
### File Management
|
||||||
|
- Generated files stored in `/app/documents/`
|
||||||
|
- File names auto-generated: `{document}-{timestamp}.{ext}`
|
||||||
|
- Always sanitize fileName in download requests
|
||||||
|
|
||||||
|
### Supported Formats
|
||||||
|
- **PDF:** `documentType: "pdf"`
|
||||||
|
- **Excel:** `documentType: "excel"`
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
- Worker concurrency: 2 concurrent jobs
|
||||||
|
- Timeout per job: 5 minutes
|
||||||
|
- Retry attempts: 3 with exponential backoff
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Common Response Codes
|
||||||
|
|
||||||
|
| Code | Meaning |
|
||||||
|
|------|---------|
|
||||||
|
| `202` | Accepted - Job queued successfully |
|
||||||
|
| `200` | OK - Successful operation |
|
||||||
|
| `400` | Bad Request - Invalid input |
|
||||||
|
| `401` | Unauthorized - Missing/invalid token |
|
||||||
|
| `403` | Forbidden - Insufficient permissions |
|
||||||
|
| `404` | Not Found - Job or file not found |
|
||||||
|
| `500` | Server Error - Internal error |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Migration Notes
|
||||||
|
|
||||||
|
**If upgrading from synchronous API:**
|
||||||
|
|
||||||
|
**Old (Synchronous):**
|
||||||
|
- Request returned file immediately
|
||||||
|
- Long request timeout
|
||||||
|
- Blocking operation
|
||||||
|
|
||||||
|
**New (Asynchronous):**
|
||||||
|
- Request returns jobId immediately (202)
|
||||||
|
- Poll `/api/document/job/{jobId}/status` for progress
|
||||||
|
- Non-blocking, scalable architecture
|
||||||
|
- Automatic retries built-in
|
||||||
|
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
|
||||||
|
# Notification API
|
||||||
|
|
||||||
|
## Create New Notification
|
||||||
|
|
||||||
|
**Endpoint**
|
||||||
|
|
||||||
|
```
|
||||||
|
POST: http://localhost:3070/api/notification
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
**Request Body**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"notificationHeadline": "System Maintenance",
|
||||||
|
"notificationDescription": "The system will be unavailable from 10 PM to 12 AM.",
|
||||||
|
"notificationType": "ANNOUNCEMENT"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Respond**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Notification created successfully",
|
||||||
|
"notification": {
|
||||||
|
"notification_id": "notif_1782553200000",
|
||||||
|
"notificationHeadline": "System Maintenance",
|
||||||
|
"notificationDescription": "The system will be unavailable from 10 PM to 12 AM.",
|
||||||
|
"notificationType": "ANNOUNCEMENT",
|
||||||
|
"dateCreated": "2026-06-27T05:30:00.000Z",
|
||||||
|
"isActive": true,
|
||||||
|
"updatedAt": "2026-06-27T05:30:00.000Z",
|
||||||
|
"createdAt": "2026-06-27T05:30:00.000Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Get Announcements
|
||||||
|
|
||||||
|
**Endpoint**
|
||||||
|
|
||||||
|
```
|
||||||
|
GET: http://localhost:3070/api/notification/announcements
|
||||||
|
```
|
||||||
|
|
||||||
|
**Respond**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"announcements": [
|
||||||
|
{
|
||||||
|
"notification_id": "notif_1782553200000",
|
||||||
|
"notificationHeadline": "System Maintenance",
|
||||||
|
"notificationDescription": "The system will be unavailable from 10 PM to 12 AM.",
|
||||||
|
"notificationType": "ANNOUNCEMENT",
|
||||||
|
"isActive": true,
|
||||||
|
"dateCreated": "2026-06-27T05:30:00.000Z",
|
||||||
|
"createdAt": "2026-06-27T05:30:00.000Z",
|
||||||
|
"updatedAt": "2026-06-27T05:30:00.000Z"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Get User Notifications
|
||||||
|
|
||||||
|
**Endpoint**
|
||||||
|
|
||||||
|
```
|
||||||
|
GET: http://localhost:3070/api/notification/user/:userId
|
||||||
|
```
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```
|
||||||
|
GET: http://localhost:3070/api/notification/user/usr_12345
|
||||||
|
```
|
||||||
|
|
||||||
|
**Respond**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"notifications": [
|
||||||
|
{
|
||||||
|
"id": "userNotif_001",
|
||||||
|
"user_id": "usr_12345",
|
||||||
|
"notification_id": "notif_1782553200000",
|
||||||
|
"isRead": false,
|
||||||
|
"createdAt": "2026-06-27T05:30:00.000Z",
|
||||||
|
"updatedAt": "2026-06-27T05:30:00.000Z",
|
||||||
|
"notification": {
|
||||||
|
"notification_id": "notif_1782553200000",
|
||||||
|
"notificationHeadline": "System Maintenance",
|
||||||
|
"notificationDescription": "The system will be unavailable from 10 PM to 12 AM.",
|
||||||
|
"notificationType": "ANNOUNCEMENT",
|
||||||
|
"isActive": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Mark Notification As Read
|
||||||
|
|
||||||
|
**Endpoint**
|
||||||
|
|
||||||
|
```
|
||||||
|
PATCH: http://localhost:3070/api/notification/user/:userId/notification/:notificationId/read
|
||||||
|
```
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```
|
||||||
|
PATCH: http://localhost:3070/api/notification/user/usr_12345/notification/notif_1782553200000/read
|
||||||
|
```
|
||||||
|
|
||||||
|
**Respond**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Notification marked as read"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
@@ -0,0 +1,755 @@
|
|||||||
|
# Permission API
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
All endpoints require authentication token. Include in header:
|
||||||
|
```
|
||||||
|
Authorization: Bearer <token>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Permission Endpoints
|
||||||
|
|
||||||
|
### Create Permission
|
||||||
|
|
||||||
|
**Endpoint**
|
||||||
|
```
|
||||||
|
POST: http://localhost:3070/api/permission
|
||||||
|
```
|
||||||
|
|
||||||
|
**Authorization**: Admin only
|
||||||
|
|
||||||
|
**Request Body**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"permissionName": "Create Inquiry",
|
||||||
|
"permissionDescription": "Permission to create new inquiries",
|
||||||
|
"page": "inquiry",
|
||||||
|
"module": "inquiry_management",
|
||||||
|
"action": "create"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response (201)**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"permission_id": "inquiry_management.inquiry.create",
|
||||||
|
"permissionName": "Create Inquiry",
|
||||||
|
"permissionDescription": "Permission to create new inquiries",
|
||||||
|
"page": "inquiry",
|
||||||
|
"module": "inquiry_management",
|
||||||
|
"action": "create",
|
||||||
|
"createdAt": "2026-05-15T10:30:00Z",
|
||||||
|
"updatedAt": "2026-05-15T10:30:00Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Bulk Create Permissions
|
||||||
|
|
||||||
|
**Endpoint**
|
||||||
|
```
|
||||||
|
POST: http://localhost:3070/api/permission/bulk
|
||||||
|
```
|
||||||
|
|
||||||
|
**Authorization**: Admin only
|
||||||
|
|
||||||
|
**Request Body**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"permissions": [
|
||||||
|
{
|
||||||
|
"permissionName": "Create Inquiry",
|
||||||
|
"permissionDescription": "Permission to create new inquiries",
|
||||||
|
"page": "inquiry",
|
||||||
|
"module": "inquiry_management",
|
||||||
|
"action": "create"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"permissionName": "View Inquiry",
|
||||||
|
"permissionDescription": "Permission to view inquiries",
|
||||||
|
"page": "inquiry",
|
||||||
|
"module": "inquiry_management",
|
||||||
|
"action": "view"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"permissionName": "Edit Inquiry",
|
||||||
|
"permissionDescription": "Permission to edit inquiries",
|
||||||
|
"page": "inquiry",
|
||||||
|
"module": "inquiry_management",
|
||||||
|
"action": "edit"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response (201)**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"created": [
|
||||||
|
{
|
||||||
|
"permission_id": "inquiry_management.inquiry.create",
|
||||||
|
"permissionName": "Create Inquiry",
|
||||||
|
"permissionDescription": "Permission to create new inquiries",
|
||||||
|
"page": "inquiry",
|
||||||
|
"module": "inquiry_management",
|
||||||
|
"action": "create"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"permission_id": "inquiry_management.inquiry.view",
|
||||||
|
"permissionName": "View Inquiry",
|
||||||
|
"permissionDescription": "Permission to view inquiries",
|
||||||
|
"page": "inquiry",
|
||||||
|
"module": "inquiry_management",
|
||||||
|
"action": "view"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"permission_id": "inquiry_management.inquiry.edit",
|
||||||
|
"permissionName": "Edit Inquiry",
|
||||||
|
"permissionDescription": "Permission to edit inquiries",
|
||||||
|
"page": "inquiry",
|
||||||
|
"module": "inquiry_management",
|
||||||
|
"action": "edit"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"failed": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Get All Permissions
|
||||||
|
|
||||||
|
**Endpoint**
|
||||||
|
```
|
||||||
|
GET: http://localhost:3070/api/permission
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response (200)**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"permission_id": "inquiry_management.inquiry.create",
|
||||||
|
"permissionName": "Create Inquiry",
|
||||||
|
"permissionDescription": "Permission to create new inquiries",
|
||||||
|
"page": "inquiry",
|
||||||
|
"module": "inquiry_management",
|
||||||
|
"action": "create"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"permission_id": "inquiry_management.inquiry.view",
|
||||||
|
"permissionName": "View Inquiry",
|
||||||
|
"permissionDescription": "Permission to view inquiries",
|
||||||
|
"page": "inquiry",
|
||||||
|
"module": "inquiry_management",
|
||||||
|
"action": "view"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Get Permission by ID
|
||||||
|
|
||||||
|
**Endpoint**
|
||||||
|
```
|
||||||
|
GET: http://localhost:3070/api/permission/:permissionId
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
```
|
||||||
|
GET: http://localhost:3070/api/permission/inquiry_management.inquiry.create
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response (200)**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"permission_id": "inquiry_management.inquiry.create",
|
||||||
|
"permissionName": "Create Inquiry",
|
||||||
|
"permissionDescription": "Permission to create new inquiries",
|
||||||
|
"page": "inquiry",
|
||||||
|
"module": "inquiry_management",
|
||||||
|
"action": "create"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Update Permission
|
||||||
|
|
||||||
|
**Endpoint**
|
||||||
|
```
|
||||||
|
PUT: http://localhost:3070/api/permission/:permissionId
|
||||||
|
```
|
||||||
|
|
||||||
|
**Authorization**: Admin only
|
||||||
|
|
||||||
|
**Request Body**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"permissionName": "Create New Inquiry",
|
||||||
|
"permissionDescription": "Updated permission to create new inquiries"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response (200)**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"permission_id": "inquiry_management.inquiry.create",
|
||||||
|
"permissionName": "Create New Inquiry",
|
||||||
|
"permissionDescription": "Updated permission to create new inquiries",
|
||||||
|
"page": "inquiry",
|
||||||
|
"module": "inquiry_management",
|
||||||
|
"action": "create"
|
||||||
|
},
|
||||||
|
"message": "Permission updated successfully"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Delete Permission
|
||||||
|
|
||||||
|
**Endpoint**
|
||||||
|
```
|
||||||
|
DELETE: http://localhost:3070/api/permission/:permissionId
|
||||||
|
```
|
||||||
|
|
||||||
|
**Authorization**: Admin only
|
||||||
|
|
||||||
|
**Response (200)**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Permission deleted successfully"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Role Endpoints
|
||||||
|
|
||||||
|
### Create Role
|
||||||
|
|
||||||
|
**Endpoint**
|
||||||
|
```
|
||||||
|
POST: http://localhost:3070/api/permission/roles
|
||||||
|
```
|
||||||
|
|
||||||
|
**Authorization**: Admin only
|
||||||
|
|
||||||
|
**Request Body**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"roleName": "Inquiry Manager",
|
||||||
|
"roleDescription": "Role for managing inquiries"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response (201)**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"role_id": "role_1715843400000",
|
||||||
|
"roleName": "Inquiry Manager",
|
||||||
|
"roleDescription": "Role for managing inquiries",
|
||||||
|
"createdAt": "2026-05-15T10:30:00Z",
|
||||||
|
"updatedAt": "2026-05-15T10:30:00Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Get All Roles
|
||||||
|
|
||||||
|
**Endpoint**
|
||||||
|
```
|
||||||
|
GET: http://localhost:3070/api/permission/roles
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response (200)**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"role_id": "role_1715843400000",
|
||||||
|
"roleName": "Inquiry Manager",
|
||||||
|
"roleDescription": "Role for managing inquiries",
|
||||||
|
"rolePermissions": [
|
||||||
|
{
|
||||||
|
"rp_id": "rp_1715843500000",
|
||||||
|
"role_id": "role_1715843400000",
|
||||||
|
"permission_id": "inquiry_management.inquiry.create",
|
||||||
|
"permission": {
|
||||||
|
"permission_id": "inquiry_management.inquiry.create",
|
||||||
|
"permissionName": "Create Inquiry",
|
||||||
|
"page": "inquiry",
|
||||||
|
"module": "inquiry_management",
|
||||||
|
"action": "create"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Get Role by ID
|
||||||
|
|
||||||
|
**Endpoint**
|
||||||
|
```
|
||||||
|
GET: http://localhost:3070/api/permission/roles/:roleId
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
```
|
||||||
|
GET: http://localhost:3070/api/permission/roles/role_1715843400000
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response (200)**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"role_id": "role_1715843400000",
|
||||||
|
"roleName": "Inquiry Manager",
|
||||||
|
"roleDescription": "Role for managing inquiries",
|
||||||
|
"rolePermissions": [
|
||||||
|
{
|
||||||
|
"rp_id": "rp_1715843500000",
|
||||||
|
"role_id": "role_1715843400000",
|
||||||
|
"permission_id": "inquiry_management.inquiry.create",
|
||||||
|
"permission": {
|
||||||
|
"permission_id": "inquiry_management.inquiry.create",
|
||||||
|
"permissionName": "Create Inquiry",
|
||||||
|
"page": "inquiry",
|
||||||
|
"module": "inquiry_management",
|
||||||
|
"action": "create"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Update Role
|
||||||
|
|
||||||
|
**Endpoint**
|
||||||
|
```
|
||||||
|
PUT: http://localhost:3070/api/permission/roles/:roleId
|
||||||
|
```
|
||||||
|
|
||||||
|
**Authorization**: Admin only
|
||||||
|
|
||||||
|
**Request Body**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"roleName": "Senior Inquiry Manager",
|
||||||
|
"roleDescription": "Updated role for managing inquiries"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response (200)**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"role_id": "role_1715843400000",
|
||||||
|
"roleName": "Senior Inquiry Manager",
|
||||||
|
"roleDescription": "Updated role for managing inquiries"
|
||||||
|
},
|
||||||
|
"message": "Role updated successfully"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Delete Role
|
||||||
|
|
||||||
|
**Endpoint**
|
||||||
|
```
|
||||||
|
DELETE: http://localhost:3070/api/permission/roles/:roleId
|
||||||
|
```
|
||||||
|
|
||||||
|
**Authorization**: Admin only
|
||||||
|
|
||||||
|
**Response (200)**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Role deleted successfully"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Role-Permission Assignment Endpoints
|
||||||
|
|
||||||
|
### Assign Permission to Role
|
||||||
|
|
||||||
|
**Endpoint**
|
||||||
|
```
|
||||||
|
POST: http://localhost:3070/api/permission/roles/:roleId/permissions
|
||||||
|
```
|
||||||
|
|
||||||
|
**Authorization**: Admin only
|
||||||
|
|
||||||
|
**Request Body**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"role_id": "role_1715843400000",
|
||||||
|
"permission_id": "inquiry_management.inquiry.create"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response (201)**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"rp_id": "rp_1715843500000",
|
||||||
|
"role_id": "role_1715843400000",
|
||||||
|
"permission_id": "inquiry_management.inquiry.create",
|
||||||
|
"createdAt": "2026-05-15T10:30:00Z",
|
||||||
|
"updatedAt": "2026-05-15T10:30:00Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Bulk Assign Permissions to Role
|
||||||
|
|
||||||
|
**Endpoint**
|
||||||
|
```
|
||||||
|
POST: http://localhost:3070/api/permission/roles/permissions/bulk
|
||||||
|
```
|
||||||
|
|
||||||
|
**Authorization**: Admin only
|
||||||
|
|
||||||
|
**Request Body**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"role_id": "role_1715843400000",
|
||||||
|
"permission_ids": [
|
||||||
|
"inquiry_management.inquiry.create",
|
||||||
|
"inquiry_management.inquiry.view",
|
||||||
|
"inquiry_management.inquiry.edit",
|
||||||
|
"vendor_management.vendor.view"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response (201)**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"created": [
|
||||||
|
{
|
||||||
|
"rp_id": "rp_1715843500001",
|
||||||
|
"role_id": "role_1715843400000",
|
||||||
|
"permission_id": "inquiry_management.inquiry.create"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rp_id": "rp_1715843500002",
|
||||||
|
"role_id": "role_1715843400000",
|
||||||
|
"permission_id": "inquiry_management.inquiry.view"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rp_id": "rp_1715843500003",
|
||||||
|
"role_id": "role_1715843400000",
|
||||||
|
"permission_id": "inquiry_management.inquiry.edit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rp_id": "rp_1715843500004",
|
||||||
|
"role_id": "role_1715843400000",
|
||||||
|
"permission_id": "vendor_management.vendor.view"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"failed": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response with Failures (201)**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"data": {
|
||||||
|
"created": [
|
||||||
|
{
|
||||||
|
"rp_id": "rp_1715843500001",
|
||||||
|
"role_id": "role_1715843400000",
|
||||||
|
"permission_id": "inquiry_management.inquiry.create"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"failed": [
|
||||||
|
{
|
||||||
|
"role_id": "role_1715843400000",
|
||||||
|
"permission_id": "inquiry_management.inquiry.view",
|
||||||
|
"error": "Role permission already exists"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role_id": "role_1715843400000",
|
||||||
|
"permission_id": "nonexistent_permission",
|
||||||
|
"error": "Permission not found"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## User Permission Endpoints
|
||||||
|
|
||||||
|
### Get User Permissions
|
||||||
|
|
||||||
|
**Endpoint**
|
||||||
|
```
|
||||||
|
GET: http://localhost:3070/api/permission/users/:userId/permissions
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
```
|
||||||
|
GET: http://localhost:3070/api/permission/users/usr_5ff8afec-5ddc-47d9-a63f-b43df0d8c3b4/permissions
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response (200)**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"up_id": 1,
|
||||||
|
"user_id": "usr_5ff8afec-5ddc-47d9-a63f-b43df0d8c3b4",
|
||||||
|
"permission_id": "inquiry_management.inquiry.create",
|
||||||
|
"permission": {
|
||||||
|
"permission_id": "inquiry_management.inquiry.create",
|
||||||
|
"permissionName": "Create Inquiry",
|
||||||
|
"page": "inquiry",
|
||||||
|
"module": "inquiry_management",
|
||||||
|
"action": "create"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Create User Permission
|
||||||
|
|
||||||
|
**Endpoint**
|
||||||
|
```
|
||||||
|
POST: http://localhost:3070/api/permission/users/permissions
|
||||||
|
```
|
||||||
|
|
||||||
|
**Authorization**: Admin only
|
||||||
|
|
||||||
|
**Request Body**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"user_id": "usr_5ff8afec-5ddc-47d9-a63f-b43df0d8c3b4",
|
||||||
|
"permission_id": "inquiry_management.inquiry.create"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response (201)**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"up_id": 1,
|
||||||
|
"user_id": "usr_5ff8afec-5ddc-47d9-a63f-b43df0d8c3b4",
|
||||||
|
"permission_id": "inquiry_management.inquiry.create",
|
||||||
|
"createdAt": "2026-05-15T10:30:00Z",
|
||||||
|
"updatedAt": "2026-05-15T10:30:00Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Bulk Create User Permissions
|
||||||
|
|
||||||
|
**Endpoint**
|
||||||
|
```
|
||||||
|
POST: http://localhost:3070/api/permission/users/permissions/bulk
|
||||||
|
```
|
||||||
|
|
||||||
|
**Authorization**: Admin only
|
||||||
|
|
||||||
|
**Request Body**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"user_id": "usr_5ff8afec-5ddc-47d9-a63f-b43df0d8c3b4",
|
||||||
|
"permission_ids": [
|
||||||
|
"inquiry_management.inquiry.create",
|
||||||
|
"inquiry_management.inquiry.view",
|
||||||
|
"inquiry_management.inquiry.edit",
|
||||||
|
"vendor_management.vendor.view"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response (201)**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"created": [
|
||||||
|
{
|
||||||
|
"up_id": 1,
|
||||||
|
"user_id": "usr_5ff8afec-5ddc-47d9-a63f-b43df0d8c3b4",
|
||||||
|
"permission_id": "inquiry_management.inquiry.create"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"up_id": 2,
|
||||||
|
"user_id": "usr_5ff8afec-5ddc-47d9-a63f-b43df0d8c3b4",
|
||||||
|
"permission_id": "inquiry_management.inquiry.view"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"up_id": 3,
|
||||||
|
"user_id": "usr_5ff8afec-5ddc-47d9-a63f-b43df0d8c3b4",
|
||||||
|
"permission_id": "inquiry_management.inquiry.edit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"up_id": 4,
|
||||||
|
"user_id": "usr_5ff8afec-5ddc-47d9-a63f-b43df0d8c3b4",
|
||||||
|
"permission_id": "vendor_management.vendor.view"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"failed": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response with Failures (201)**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"data": {
|
||||||
|
"created": [
|
||||||
|
{
|
||||||
|
"up_id": 1,
|
||||||
|
"user_id": "usr_5ff8afec-5ddc-47d9-a63f-b43df0d8c3b4",
|
||||||
|
"permission_id": "inquiry_management.inquiry.create"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"failed": [
|
||||||
|
{
|
||||||
|
"user_id": "usr_5ff8afec-5ddc-47d9-a63f-b43df0d8c3b4",
|
||||||
|
"permission_id": "inquiry_management.inquiry.view",
|
||||||
|
"error": "User permission already exists"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"user_id": "usr_5ff8afec-5ddc-47d9-a63f-b43df0d8c3b4",
|
||||||
|
"permission_id": "nonexistent_permission",
|
||||||
|
"error": "Permission not found"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Update User Permission
|
||||||
|
|
||||||
|
**Endpoint**
|
||||||
|
```
|
||||||
|
PUT: http://localhost:3070/api/permission/users/permissions/:upId
|
||||||
|
```
|
||||||
|
|
||||||
|
**Authorization**: Admin only
|
||||||
|
|
||||||
|
**Request Body**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"permission_id": "inquiry_management.inquiry.edit"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response (200)**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"up_id": 1,
|
||||||
|
"user_id": "usr_5ff8afec-5ddc-47d9-a63f-b43df0d8c3b4",
|
||||||
|
"permission_id": "inquiry_management.inquiry.edit"
|
||||||
|
},
|
||||||
|
"message": "User permission updated successfully"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Delete User Permission
|
||||||
|
|
||||||
|
**Endpoint**
|
||||||
|
```
|
||||||
|
DELETE: http://localhost:3070/api/permission/users/permissions/:upId
|
||||||
|
```
|
||||||
|
|
||||||
|
**Authorization**: Admin only
|
||||||
|
|
||||||
|
**Response (200)**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "User permission deleted successfully"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Error Responses
|
||||||
|
|
||||||
|
### Bad Request (400)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"message": "Permission name, page, module, and action are required"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Not Found (404)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"message": "Permission not found"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Internal Server Error (500)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"message": "An error occurred while fetching permissions."
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,253 @@
|
|||||||
|
# Profile API
|
||||||
|
|
||||||
|
#### Get Profile Avatar
|
||||||
|
|
||||||
|
**Endpoint**
|
||||||
|
|
||||||
|
```
|
||||||
|
GET: http://localhost:3070/api/profile/avatar/:userId
|
||||||
|
```
|
||||||
|
|
||||||
|
**Path Parameters**
|
||||||
|
|
||||||
|
| Parameter | Type | Required | Description |
|
||||||
|
|-----------|--------|----------|-----------------|
|
||||||
|
| userId | string | Yes | The user ID |
|
||||||
|
|
||||||
|
**Headers**
|
||||||
|
|
||||||
|
```
|
||||||
|
Authorization: Bearer <token>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"userId": "usr_763107d9-b56f-4b81-9d86-1889f98a6e6c",
|
||||||
|
"profilePictureUrl": "https://signed-s3-url.com/profile-picture.jpg"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Error Responses**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"message": "Profile not found",
|
||||||
|
"error": "Profile not found"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"message": "Internal server error",
|
||||||
|
"error": "error message"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Get Profile Background Image
|
||||||
|
|
||||||
|
**Endpoint**
|
||||||
|
|
||||||
|
```
|
||||||
|
GET: http://localhost:3070/api/profile/background/:userId
|
||||||
|
```
|
||||||
|
|
||||||
|
**Path Parameters**
|
||||||
|
|
||||||
|
| Parameter | Type | Required | Description |
|
||||||
|
|-----------|--------|----------|-----------------|
|
||||||
|
| userId | string | Yes | The user ID |
|
||||||
|
|
||||||
|
**Headers**
|
||||||
|
|
||||||
|
```
|
||||||
|
Authorization: Bearer <token>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"userId": "usr_763107d9-b56f-4b81-9d86-1889f98a6e6c",
|
||||||
|
"backgroundImageUrl": "https://signed-s3-url.com/background-image.jpg"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Error Responses**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"message": "Profile not found",
|
||||||
|
"error": "Profile not found"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"message": "Internal server error",
|
||||||
|
"error": "error message"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Profile Object Structure
|
||||||
|
|
||||||
|
The Profile object contains the following fields:
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|--------------------|---------|----------------------------------|
|
||||||
|
| profile_id | string | Unique profile identifier |
|
||||||
|
| user_id | string | Associated user ID |
|
||||||
|
| theme | string | User theme preference (light/dark) |
|
||||||
|
| notificationsEnabled | boolean | Notification settings |
|
||||||
|
| profilePicture_id | string | Upload ID for profile picture |
|
||||||
|
| backgroundImage_id | string | Upload ID for background image |
|
||||||
|
| dob | date | Date of birth |
|
||||||
|
| phone_number | string | Phone number |
|
||||||
|
| createdAt | datetime| Profile creation timestamp |
|
||||||
|
| updatedAt | datetime| Profile last update timestamp |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Request Password Reset
|
||||||
|
|
||||||
|
**Endpoint**
|
||||||
|
|
||||||
|
```
|
||||||
|
POST: http://localhost:3070/api/profile/req-reset-password
|
||||||
|
```
|
||||||
|
|
||||||
|
**Request Body**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"email": "sathira.nirmal@gmail.com"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "If an account exists with this email address, a password reset email has been sent."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Description**
|
||||||
|
|
||||||
|
This endpoint generates a password reset token and sends a reset link to the user's email. The reset link will be sent in email format:
|
||||||
|
|
||||||
|
```
|
||||||
|
http://localhost:3000/reset-password?token=TOKEN_HERE&email=EMAIL_HERE
|
||||||
|
```
|
||||||
|
|
||||||
|
Example reset link:
|
||||||
|
|
||||||
|
```
|
||||||
|
http://localhost:3000/reset-password?token=aa27def4222adedcf72a417dfc40b69cd01f8bdb07f37b324a8501f3bdb37e44&email=sathira.nirmal%40gmail.com
|
||||||
|
```
|
||||||
|
|
||||||
|
**Notes**
|
||||||
|
|
||||||
|
- This endpoint does not require authentication
|
||||||
|
- No error is returned if email doesn't exist (for security reasons)
|
||||||
|
- Token expires after the time specified in `PASSWORD_RESET_EXPIRY_TIME` environment variable (default: 10 minutes)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Reset Password
|
||||||
|
|
||||||
|
**Endpoint**
|
||||||
|
|
||||||
|
```
|
||||||
|
POST: http://localhost:3070/api/profile/reset-password
|
||||||
|
```
|
||||||
|
|
||||||
|
**Request Body**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"email": "sathira.nirmal@gmail.com",
|
||||||
|
"token": "168e5d9d4dfb124571f412c5521874e8440a0f9e4c151fa24518ae57ca1c4f8f",
|
||||||
|
"newPassword": "niolla"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Password reset successfully."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Error Responses**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"message": "Invalid email address."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"message": "Invalid or expired token."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Description**
|
||||||
|
|
||||||
|
This endpoint resets the user's password using a valid reset token. The token must be obtained from the password reset email.
|
||||||
|
|
||||||
|
**Notes**
|
||||||
|
|
||||||
|
- This endpoint does not require authentication
|
||||||
|
- Token must be valid and not expired
|
||||||
|
- New password will replace the old password immediately
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Authentication
|
||||||
|
|
||||||
|
All endpoints require authentication token unless otherwise noted. Include the Bearer token in the Authorization header:
|
||||||
|
|
||||||
|
```
|
||||||
|
Authorization: Bearer <your_jwt_token>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Authentication Required:**
|
||||||
|
- GET /avatar/:userId
|
||||||
|
- GET /background/:userId
|
||||||
|
- POST /change-password
|
||||||
|
|
||||||
|
**No Authentication Required:**
|
||||||
|
- POST /req-reset-password
|
||||||
|
- POST /reset-password
|
||||||
|
|
||||||
|
#### Authorization
|
||||||
|
|
||||||
|
Access levels for profile endpoints:
|
||||||
|
|
||||||
|
- **GET /avatar/:userId** - Accessible by: admin, management, team_head, user
|
||||||
|
- **GET /background/:userId** - Accessible by: admin, management, team_head, user
|
||||||
|
- **POST /change-password** - Accessible by: admin, management, team_head, user (authenticated users)
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
# Upload API
|
||||||
|
|
||||||
|
#### Upload A File
|
||||||
|
|
||||||
|
**Endpoint**
|
||||||
|
|
||||||
|
```
|
||||||
|
POST: http://localhost:3070/api/upload
|
||||||
|
```
|
||||||
|
|
||||||
|
**Request Body**
|
||||||
|
|
||||||
|
```
|
||||||
|
Key: file
|
||||||
|
Value: uploadfile.pdf / avatar.png
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
**Respond**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "File uploaded successfully",
|
||||||
|
"data": {
|
||||||
|
"id": 4,
|
||||||
|
"file_path": "uploads/1f642d1d-dc79-423f-aad0-36df9938c1ff.pdf",
|
||||||
|
"file_type": "application/pdf",
|
||||||
|
"file_size": 15640,
|
||||||
|
"original_name": "SamplePDF.pdf",
|
||||||
|
"use_for": "vendor_documents",
|
||||||
|
"uploaded_by": "usr_5ff8afec-5ddc-47d9-a63f-b43df0d8c3b4",
|
||||||
|
"updatedAt": "2026-04-04T19:44:03.252Z",
|
||||||
|
"createdAt": "2026-04-04T19:44:03.252Z",
|
||||||
|
"file_url": "https://oceanic-bucket.s3.ap-southeast-1.amazonaws.com/uploads/1f642d1d-dc79-423f-aad0-36df9938c1ff.pdf?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=AKIASP2AP6EU76EJ63PD%2F20260404%2Fap-southeast-1%2Fs3%2Faws4_request&X-Amz-Date=20260404T194403Z&X-Amz-Expires=3600&X-Amz-Signature=f9ecb283dfe206e87fa9bbd9f59194dfca4cdb11d8bb02f0ae5abd89db7ddc04&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Get Uploaded File
|
||||||
|
|
||||||
|
**Endpoint**
|
||||||
|
|
||||||
|
```
|
||||||
|
POST: http://localhost:3070/api/upload/signed-url/:id
|
||||||
|
```
|
||||||
|
|
||||||
|
Example: http://localhost:3070/api/upload/signed-url/3
|
||||||
|
|
||||||
|
**Respond**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "File URL retrieved successfully",
|
||||||
|
"data": {
|
||||||
|
"id": 3,
|
||||||
|
"file_url": "https://oceanic-bucket.s3.ap-southeast-1.amazonaws.com/uploads/9c441266-c1ee-4813-9b81-cf13276535c9.pdf?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=AKIASP2AP6EU76EJ63PD%2F20260404%2Fap-southeast-1%2Fs3%2Faws4_request&X-Amz-Date=20260404T195205Z&X-Amz-Expires=3600&X-Amz-Signature=de8f9a4658b2802af4230f0ebba25511fb57e75cce4be75aa2e34d40a771bffb&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,223 @@
|
|||||||
|
# User API
|
||||||
|
|
||||||
|
#### Create New User
|
||||||
|
|
||||||
|
**Endpoint**
|
||||||
|
|
||||||
|
```
|
||||||
|
POST: http://localhost:3070/api/user
|
||||||
|
```
|
||||||
|
|
||||||
|
**Request Body**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"firstName": "Jhon",
|
||||||
|
"lastName": "Doe",
|
||||||
|
"email": "kalanajayasekara@niolla.lk",
|
||||||
|
"role": "System Developer",
|
||||||
|
"accountType": "admin",
|
||||||
|
"department": "IT Department"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Respond**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "User Created Successfully",
|
||||||
|
"data": {
|
||||||
|
"id": "usr_763107d9-b56f-4b81-9d86-1889f98a6e6c",
|
||||||
|
"firstName": "Jhon",
|
||||||
|
"lastName": "Doe",
|
||||||
|
"email": "kalanajayasekara@niolla.lk",
|
||||||
|
"accountType": "admin",
|
||||||
|
"role": "System Developer",
|
||||||
|
"department": "IT Department"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Get All Users
|
||||||
|
|
||||||
|
**Endpoint**
|
||||||
|
|
||||||
|
```
|
||||||
|
GET: http://localhost:3070/api/user
|
||||||
|
```
|
||||||
|
|
||||||
|
**Respond**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"id": "usr_b00045f7-aafb-482a-8587-d28beb5195ec",
|
||||||
|
"firstName": "Kalana",
|
||||||
|
"lastName": "Jayasekara",
|
||||||
|
"email": "kalanamanupiya32@gmail.com",
|
||||||
|
"accountType": "admin",
|
||||||
|
"role": "Manager",
|
||||||
|
"department": "Operations",
|
||||||
|
"createdAt": "2026-02-14T19:14:25.000Z",
|
||||||
|
"updatedAt": "2026-02-14T19:14:25.000Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "usr_763107d9-b56f-4b81-9d86-1889f98a6e6c",
|
||||||
|
"firstName": "Kalana",
|
||||||
|
"lastName": "Manupiya",
|
||||||
|
"email": "kalanajayasekara@niolla.lk",
|
||||||
|
"accountType": "admin",
|
||||||
|
"role": "System Developer",
|
||||||
|
"department": "IT Department",
|
||||||
|
"createdAt": "2026-02-12T19:32:15.000Z",
|
||||||
|
"updatedAt": "2026-02-12T19:32:15.000Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "usr_5ff8afec-5ddc-47d9-a63f-b43df0d8c3b4",
|
||||||
|
"firstName": "Jhon",
|
||||||
|
"lastName": "Doe",
|
||||||
|
"email": "sathira@niolla.lk",
|
||||||
|
"accountType": "admin",
|
||||||
|
"role": "System Developer",
|
||||||
|
"department": "IT Department",
|
||||||
|
"createdAt": "2026-02-12T19:24:04.000Z",
|
||||||
|
"updatedAt": "2026-02-12T19:24:04.000Z"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"pagination": {
|
||||||
|
"totalUsers": 3,
|
||||||
|
"totalPages": 1,
|
||||||
|
"currentPage": 1,
|
||||||
|
"pageSize": 20
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This API uses Pagination:
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /user?page=1
|
||||||
|
GET /user?page=2
|
||||||
|
GET /user?page=3
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Get User by ID
|
||||||
|
|
||||||
|
**Endpoint**
|
||||||
|
|
||||||
|
```
|
||||||
|
GET: http://localhost:3070/api/user/:id
|
||||||
|
```
|
||||||
|
|
||||||
|
Ex: http://localhost:3070/api/user/usr_5ff8afec-5ddc-47d9-a63f-b43df0d8c3b4
|
||||||
|
|
||||||
|
**Response**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"id": "usr_5ff8afec-5ddc-47d9-a63f-b43df0d8c3b4",
|
||||||
|
"firstName": "Sathira",
|
||||||
|
"lastName": "Sri Sathsara",
|
||||||
|
"email": "sathira@niolla.lk",
|
||||||
|
"accountType": "admin",
|
||||||
|
"role": "System Developer",
|
||||||
|
"department": "IT Department",
|
||||||
|
"createdAt": "2026-02-12T19:24:04.000Z",
|
||||||
|
"updatedAt": "2026-02-14T21:33:19.751Z",
|
||||||
|
"profile": {
|
||||||
|
"profile_id": "prof_a1b2c3d4-e5f6-7890-1234-567890abcdef",
|
||||||
|
"user_id": "usr_5ff8afec-5ddc-47d9-a63f-b43df0d8c3b4",
|
||||||
|
"theme": "light",
|
||||||
|
"notificationsEnabled": true,
|
||||||
|
"profilePicture_id": "up_xyz789",
|
||||||
|
"backgroundImage_id": "up_abc123",
|
||||||
|
"dob": "1995-03-15T00:00:00.000Z",
|
||||||
|
"phone_number": "+1 234-567-8900",
|
||||||
|
"createdAt": "2026-02-12T19:24:04.000Z",
|
||||||
|
"updatedAt": "2026-02-12T19:24:04.000Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Update User
|
||||||
|
|
||||||
|
**Endpoint**
|
||||||
|
|
||||||
|
```
|
||||||
|
PATCH: http://localhost:3070/api/user/:id
|
||||||
|
```
|
||||||
|
|
||||||
|
Ex: http://localhost:3070/api/user/usr_5ff8afec-5ddc-47d9-a63f-b43df0d8c3b4
|
||||||
|
|
||||||
|
**Request Body**
|
||||||
|
|
||||||
|
Can send single field or multiple fields. The following fields can be updated:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"firstName": "Sathira",
|
||||||
|
"lastName": "Sri Sathsara",
|
||||||
|
"email": "sathira@niolla.lk",
|
||||||
|
"role": "System Developer",
|
||||||
|
"roleID": "role_123",
|
||||||
|
"accountType": "admin",
|
||||||
|
"department": "IT Department",
|
||||||
|
"theme": "dark",
|
||||||
|
"notificationsEnabled": false,
|
||||||
|
"profilePicture_id": "up_profile_123",
|
||||||
|
"backgroundImage_id": "up_background_456",
|
||||||
|
"dob": "1995-03-15",
|
||||||
|
"phone_number": "+1 234-567-8900"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "User updated successfully",
|
||||||
|
"data": {
|
||||||
|
"id": "usr_5ff8afec-5ddc-47d9-a63f-b43df0d8c3b4",
|
||||||
|
"firstName": "Sathira",
|
||||||
|
"lastName": "Sri Sathsara",
|
||||||
|
"email": "sathira@niolla.lk",
|
||||||
|
"accountType": "admin",
|
||||||
|
"role": "System Developer",
|
||||||
|
"department": "IT Department",
|
||||||
|
"profile": {
|
||||||
|
"theme": "dark",
|
||||||
|
"notificationsEnabled": false,
|
||||||
|
"profilePicture_id": "up_profile_123",
|
||||||
|
"backgroundImage_id": "up_background_456",
|
||||||
|
"dob": "1995-03-15T00:00:00.000Z",
|
||||||
|
"phone_number": "+1 234-567-8900"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Delete User
|
||||||
|
|
||||||
|
**Endpoint**
|
||||||
|
|
||||||
|
```
|
||||||
|
DELETE: http://localhost:3070/api/user/:id
|
||||||
|
```
|
||||||
|
|
||||||
|
Ex: http://localhost:3070/api/user/usr_b00045f7-aafb-482a-8587-d28beb5195ec
|
||||||
|
|
||||||
|
**Respond**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "User deleted successfully"
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
# User Activity API
|
||||||
|
|
||||||
|
#### Get All Users Activies
|
||||||
|
|
||||||
|
**Endpoint**
|
||||||
|
|
||||||
|
```
|
||||||
|
POST: http://localhost:3070/api/activity
|
||||||
|
```
|
||||||
|
|
||||||
|
**Respond**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "User activities retrieved successfully",
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"user_id": "usr_5ff8afec-5ddc-47d9-a63f-b43df0d8c3b4",
|
||||||
|
"username": "Sathira",
|
||||||
|
"activity_description": "Created vendor with ID: vnd_009270f9-e8ef-42d9-a3e4-5398c9466972",
|
||||||
|
"activity_type": "CREATE_VENDOR",
|
||||||
|
"module": "Vendor Management",
|
||||||
|
"activity_time": "2026-03-30T19:39:45.000Z",
|
||||||
|
"activity_date": "2026-03-30",
|
||||||
|
"createdAt": "2026-03-30T19:39:45.000Z",
|
||||||
|
"updatedAt": "2026-03-30T19:39:45.000Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"user_id": "usr_5ff8afec-5ddc-47d9-a63f-b43df0d8c3b4",
|
||||||
|
"username": "Sathira",
|
||||||
|
"activity_description": "Created vendor with ID: vnd_95018101-b539-45a4-b9a6-4551cecda990",
|
||||||
|
"activity_type": "CREATE_VENDOR",
|
||||||
|
"module": "Vendor Management",
|
||||||
|
"activity_time": "2026-03-30T19:17:58.000Z",
|
||||||
|
"activity_date": "2026-03-30",
|
||||||
|
"createdAt": "2026-03-30T19:17:58.000Z",
|
||||||
|
"updatedAt": "2026-03-30T19:17:58.000Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"user_id": "usr_5ff8afec-5ddc-47d9-a63f-b43df0d8c3b4",
|
||||||
|
"username": "Sathira",
|
||||||
|
"activity_description": "Created vendor with ID: vnd_c666b2cd-36fe-46db-aecd-18ec7677ccff",
|
||||||
|
"activity_type": "CREATE_VENDOR",
|
||||||
|
"module": null,
|
||||||
|
"activity_time": "2026-03-30T19:16:41.000Z",
|
||||||
|
"activity_date": "2026-03-30",
|
||||||
|
"createdAt": "2026-03-30T19:16:41.000Z",
|
||||||
|
"updatedAt": "2026-03-30T19:16:41.000Z"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Get Activies by User ID
|
||||||
|
|
||||||
|
**Endpoint**
|
||||||
|
|
||||||
|
```
|
||||||
|
POST: http://localhost:3070/api/activity/user/:userID
|
||||||
|
```
|
||||||
|
|
||||||
|
Example: http://localhost:3070/api/activity/user/usr_5ff8afec-5ddc-47d9-a63f-b43df0d8c3b4
|
||||||
|
|
||||||
|
**Respond**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "User activities retrieved successfully",
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"user_id": "usr_5ff8afec-5ddc-47d9-a63f-b43df0d8c3b4",
|
||||||
|
"username": "Sathira",
|
||||||
|
"activity_description": "Created vendor with ID: vnd_009270f9-e8ef-42d9-a3e4-5398c9466972",
|
||||||
|
"activity_type": "CREATE_VENDOR",
|
||||||
|
"module": "Vendor Management",
|
||||||
|
"activity_time": "2026-03-30T19:39:45.000Z",
|
||||||
|
"activity_date": "2026-03-30",
|
||||||
|
"createdAt": "2026-03-30T19:39:45.000Z",
|
||||||
|
"updatedAt": "2026-03-30T19:39:45.000Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"user_id": "usr_5ff8afec-5ddc-47d9-a63f-b43df0d8c3b4",
|
||||||
|
"username": "Sathira",
|
||||||
|
"activity_description": "Created vendor with ID: vnd_95018101-b539-45a4-b9a6-4551cecda990",
|
||||||
|
"activity_type": "CREATE_VENDOR",
|
||||||
|
"module": "Vendor Management",
|
||||||
|
"activity_time": "2026-03-30T19:17:58.000Z",
|
||||||
|
"activity_date": "2026-03-30",
|
||||||
|
"createdAt": "2026-03-30T19:17:58.000Z",
|
||||||
|
"updatedAt": "2026-03-30T19:17:58.000Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"user_id": "usr_5ff8afec-5ddc-47d9-a63f-b43df0d8c3b4",
|
||||||
|
"username": "Sathira",
|
||||||
|
"activity_description": "Created vendor with ID: vnd_c666b2cd-36fe-46db-aecd-18ec7677ccff",
|
||||||
|
"activity_type": "CREATE_VENDOR",
|
||||||
|
"module": null,
|
||||||
|
"activity_time": "2026-03-30T19:16:41.000Z",
|
||||||
|
"activity_date": "2026-03-30",
|
||||||
|
"createdAt": "2026-03-30T19:16:41.000Z",
|
||||||
|
"updatedAt": "2026-03-30T19:16:41.000Z"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,119 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Docs Home</title>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
background: #0d1117;
|
||||||
|
color: #c9d1d9;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-container {
|
||||||
|
display: flex;
|
||||||
|
gap: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: #161b22;
|
||||||
|
border: 1px solid #30363d;
|
||||||
|
padding: 30px;
|
||||||
|
border-radius: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
width: 220px;
|
||||||
|
transition: 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:hover {
|
||||||
|
background: #21262d;
|
||||||
|
transform: translateY(-5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card h2 {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card p {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #8b949e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logout {
|
||||||
|
position: absolute;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #f85149;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="logout" onclick="logout()">Logout</div>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<h1>Documentation Portal</h1>
|
||||||
|
|
||||||
|
<div class="card-container">
|
||||||
|
<div class="card" onclick="goDocs()">
|
||||||
|
<h2>📘 API Documentation</h2>
|
||||||
|
<p>View markdown API docs</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card" onclick="goCollection()">
|
||||||
|
<h2>📦 API Collections</h2>
|
||||||
|
<p>Interactive API testing</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// 🔐 check if user is logged in
|
||||||
|
async function checkAuth() {
|
||||||
|
const res = await fetch('/api/docs/markdown-files', {
|
||||||
|
credentials: "include"
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
window.location.href = "/Documentation/index.html";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function goDocs() {
|
||||||
|
window.location.href = "/Documentation/markdown.html";
|
||||||
|
}
|
||||||
|
|
||||||
|
function goCollection() {
|
||||||
|
window.location.href = "/Documentation/api-collection.html";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function logout() {
|
||||||
|
await fetch('/api/docs/logout', {
|
||||||
|
method: "POST",
|
||||||
|
credentials: "include"
|
||||||
|
});
|
||||||
|
|
||||||
|
window.location.href = "/Documentation/index.html";
|
||||||
|
}
|
||||||
|
|
||||||
|
checkAuth();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Docs Login</title>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
background: #0d1117;
|
||||||
|
color: #c9d1d9;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-container {
|
||||||
|
background: #161b22;
|
||||||
|
padding: 40px;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid #30363d;
|
||||||
|
width: 320px;
|
||||||
|
box-shadow: 0 0 20px rgba(0,0,0,0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
width: 90%;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid #30363d;
|
||||||
|
background: #0d1117;
|
||||||
|
color: #c9d1d9;
|
||||||
|
outline: none;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus {
|
||||||
|
border-color: #58a6ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
background: #238636;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: white;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background: #2ea043;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
margin-top: 10px;
|
||||||
|
color: #f85149;
|
||||||
|
font-size: 13px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
margin-top: 20px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #8b949e;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="login-container">
|
||||||
|
<h2>Docs Login</h2>
|
||||||
|
|
||||||
|
<div class="input-group">
|
||||||
|
<input id="username" placeholder="Username">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input-group">
|
||||||
|
<input id="password" type="password" placeholder="Password">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button onclick="login()">Login</button>
|
||||||
|
|
||||||
|
<div id="error" class="error"></div>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
Internal Documentation Access
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
async function login() {
|
||||||
|
const errorDiv = document.getElementById("error");
|
||||||
|
errorDiv.textContent = "";
|
||||||
|
|
||||||
|
const res = await fetch('/api/docs/login', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
username: document.getElementById('username').value,
|
||||||
|
password: document.getElementById('password').value
|
||||||
|
}),
|
||||||
|
credentials: "include"
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
window.location.href = "/Documentation/home.html";
|
||||||
|
} else {
|
||||||
|
errorDiv.textContent = "Invalid username or password";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,209 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Markdown Documentation</title>
|
||||||
|
|
||||||
|
<!-- Markdown parser -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||||
|
|
||||||
|
<!-- GitHub style -->
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/github-markdown-css/github-markdown-dark.min.css">
|
||||||
|
|
||||||
|
<!-- Highlight.js -->
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/highlight.js/styles/github-dark.min.css">
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/highlight.js/lib/common.min.js"></script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
background: #0d1117;
|
||||||
|
color: #c9d1d9;
|
||||||
|
display: flex;
|
||||||
|
height: 100vh;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
width: 280px;
|
||||||
|
background: #161b22;
|
||||||
|
border-right: 1px solid #30363d;
|
||||||
|
padding: 20px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.viewer-wrapper {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body {
|
||||||
|
max-width: 900px;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
width: 80%;
|
||||||
|
padding: 8px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
background: #0d1117;
|
||||||
|
border: 1px solid #30363d;
|
||||||
|
color: white;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
padding: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
li:hover {
|
||||||
|
background: #21262d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.highlight {
|
||||||
|
background: yellow;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 5px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 5px 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
background: #21262d;
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="sidebar">
|
||||||
|
<h3>Docs</h3>
|
||||||
|
|
||||||
|
<!-- File search -->
|
||||||
|
<input type="text" id="fileSearch" placeholder="Search files..." onkeyup="filterFiles()" />
|
||||||
|
|
||||||
|
<!-- Content search -->
|
||||||
|
<input type="text" id="contentSearch" placeholder="Search in document..." onkeyup="searchInContent()" />
|
||||||
|
|
||||||
|
<div class="search-controls">
|
||||||
|
<button onclick="prevMatch()">⬆</button>
|
||||||
|
<button onclick="nextMatch()">⬇</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul id="fileList"></ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="viewer-wrapper">
|
||||||
|
<div id="viewer" class="markdown-body">
|
||||||
|
<h2>Select a file</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let allFiles = [];
|
||||||
|
let matches = [];
|
||||||
|
let currentMatchIndex = 0;
|
||||||
|
|
||||||
|
async function loadFiles() {
|
||||||
|
const res = await fetch('/api/docs/markdown-files', {
|
||||||
|
credentials: "include"
|
||||||
|
});
|
||||||
|
const result = await res.json();
|
||||||
|
allFiles = result.data;
|
||||||
|
renderList(allFiles);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderList(files) {
|
||||||
|
const list = document.getElementById('fileList');
|
||||||
|
list.innerHTML = "";
|
||||||
|
|
||||||
|
files.forEach(file => {
|
||||||
|
const li = document.createElement('li');
|
||||||
|
li.textContent = file;
|
||||||
|
li.onclick = () => loadMarkdown(file);
|
||||||
|
list.appendChild(li);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterFiles() {
|
||||||
|
const q = document.getElementById('fileSearch').value.toLowerCase();
|
||||||
|
renderList(allFiles.filter(f => f.toLowerCase().includes(q)));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadMarkdown(file) {
|
||||||
|
const res = await fetch(`/api/docs/view/${file}`, {
|
||||||
|
credentials: "include"
|
||||||
|
});
|
||||||
|
const text = await res.text();
|
||||||
|
|
||||||
|
const html = marked.parse(text);
|
||||||
|
const viewer = document.getElementById('viewer');
|
||||||
|
viewer.innerHTML = html;
|
||||||
|
|
||||||
|
document.querySelectorAll('pre code').forEach(block => {
|
||||||
|
hljs.highlightElement(block);
|
||||||
|
});
|
||||||
|
|
||||||
|
matches = [];
|
||||||
|
currentMatchIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function searchInContent() {
|
||||||
|
const query = document.getElementById('contentSearch').value;
|
||||||
|
const viewer = document.getElementById('viewer');
|
||||||
|
|
||||||
|
if (!query) {
|
||||||
|
// reset
|
||||||
|
viewer.innerHTML = viewer.textContent;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const regex = new RegExp(`(${query})`, 'gi');
|
||||||
|
|
||||||
|
viewer.innerHTML = viewer.innerHTML.replace(/<mark class="highlight">(.*?)<\/mark>/g, '$1');
|
||||||
|
|
||||||
|
viewer.innerHTML = viewer.innerHTML.replace(regex, '<mark class="highlight">$1</mark>');
|
||||||
|
|
||||||
|
matches = Array.from(document.querySelectorAll('.highlight'));
|
||||||
|
currentMatchIndex = 0;
|
||||||
|
|
||||||
|
if (matches.length) scrollToMatch(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function scrollToMatch(index) {
|
||||||
|
matches[index].scrollIntoView({
|
||||||
|
behavior: "smooth",
|
||||||
|
block: "center"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function nextMatch() {
|
||||||
|
if (!matches.length) return;
|
||||||
|
currentMatchIndex = (currentMatchIndex + 1) % matches.length;
|
||||||
|
scrollToMatch(currentMatchIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
function prevMatch() {
|
||||||
|
if (!matches.length) return;
|
||||||
|
currentMatchIndex = (currentMatchIndex - 1 + matches.length) % matches.length;
|
||||||
|
scrollToMatch(currentMatchIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
loadFiles();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
/**
|
||||||
|
* 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.js
|
||||||
|
|
||||||
|
const express = require("express");
|
||||||
|
const cors = require("cors");
|
||||||
|
const morgan = require("morgan");
|
||||||
|
const routes = require("./app/routes");
|
||||||
|
const cookieParser = require("cookie-parser");
|
||||||
|
const { bullBoardRouter } = require("./app/config/bullBoard.config");
|
||||||
|
const path = require("path");
|
||||||
|
const startAllCrons = require("./cron");
|
||||||
|
|
||||||
|
const db = require("./app/models");
|
||||||
|
|
||||||
|
// Test DB connection and sync models
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
await db.sequelize.authenticate();
|
||||||
|
console.log("Database connected.");
|
||||||
|
|
||||||
|
await db.sequelize.sync();
|
||||||
|
console.log("Tables synced.");
|
||||||
|
|
||||||
|
// Start all cron jobs
|
||||||
|
startAllCrons();
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error("DB error:", error);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
app.use(cookieParser());
|
||||||
|
|
||||||
|
// CORS configuration
|
||||||
|
const corsOptions = {
|
||||||
|
origin: process.env.FRONTEND_URL || "https://oceanic-demo.vercel.app",
|
||||||
|
methods: ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"],
|
||||||
|
allowedHeaders: ["Content-Type", "Authorization"],
|
||||||
|
credentials: true,
|
||||||
|
};
|
||||||
|
app.use(cors(corsOptions));
|
||||||
|
app.options(/.*/, cors(corsOptions));
|
||||||
|
|
||||||
|
app.use(express.json());
|
||||||
|
app.use(morgan("dev"));
|
||||||
|
|
||||||
|
app.get("/health", (req, res) => {
|
||||||
|
let dbStatus = "N/A";
|
||||||
|
let emailStatus = "N/A";
|
||||||
|
let redisStatus = "N/A";
|
||||||
|
|
||||||
|
db.sequelize
|
||||||
|
.authenticate()
|
||||||
|
.then(() => {
|
||||||
|
console.log("DB connection successful.");
|
||||||
|
dbStatus = "OK";
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error("DB connection error:", err);
|
||||||
|
res.status(500).send("Internal Server Error");
|
||||||
|
});
|
||||||
|
|
||||||
|
emailStatus = "OK";
|
||||||
|
|
||||||
|
redisStatus = "OK";
|
||||||
|
|
||||||
|
res.send({
|
||||||
|
status: "Online ✅",
|
||||||
|
database: dbStatus,
|
||||||
|
emailService: emailStatus,
|
||||||
|
redis: redisStatus,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.use("/api", routes);
|
||||||
|
app.use(
|
||||||
|
"/Documentation",
|
||||||
|
(req, res, next) => {
|
||||||
|
if (req.path.endsWith(".md")) {
|
||||||
|
return res.status(403).send("Forbidden");
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
},
|
||||||
|
express.static(path.join(__dirname, "Documentation")),
|
||||||
|
);
|
||||||
|
app.use("/admin/queues", bullBoardRouter);
|
||||||
|
|
||||||
|
module.exports = app;
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user