first commit

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

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