first commit
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
node_modules
|
||||
npm-debug.log
|
||||
Dockerfile
|
||||
docker-compose.yml
|
||||
.git
|
||||
.gitignore
|
||||
.env
|
||||
.env.prod
|
||||
.env.sample
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
# App Details
|
||||
APP_NAME=
|
||||
|
||||
# Database configuration variables
|
||||
DB_HOST =
|
||||
DB_USER =
|
||||
DB_PASSWORD =
|
||||
DB_NAME =
|
||||
DB_PORT =
|
||||
|
||||
# Redis configuration variables
|
||||
REDIS_HOST=localhost
|
||||
REDIS_PORT=6379
|
||||
|
||||
# JWT secret key
|
||||
JWT_SECRET = your_jwt_secret_key_here
|
||||
JWT_EXPIRES_IN =1d
|
||||
|
||||
# AWS S3 configuration
|
||||
AWS_ACCESS_KEY_ID=your_aws_access_key_id_here
|
||||
AWS_SECRET_ACCESS_KEY=your_aws_secret_access_key_here
|
||||
AWS_REGION=your_aws_region_here
|
||||
AWS_S3_BUCKET_NAME=your_aws_bucket_name_here
|
||||
|
||||
# Mail configuration
|
||||
MAIL_HOST=
|
||||
MAIL_PORT=587
|
||||
MAIL_USER=
|
||||
MAIL_PASS=
|
||||
MAIL_SECURE=false
|
||||
MAIL_FROM=
|
||||
|
||||
# Documentation access credentials
|
||||
DOCS_USER=
|
||||
DOCS_PASS=
|
||||
|
||||
# Admin email for receiving notifications
|
||||
MANAGER_EMAIL=
|
||||
|
||||
# Caching configuration
|
||||
CACHE=true
|
||||
|
||||
# Application environment
|
||||
NODE_ENV=development
|
||||
|
||||
# User default password
|
||||
DEFAULT_PASSWORD=
|
||||
|
||||
# Frontend URL for CORS
|
||||
FRONTEND_URL=http://localhost:3000
|
||||
|
||||
# Puppeteer executable path (if needed, otherwise Puppeteer will use the bundled Chromium)
|
||||
PUPPETEER_EXECUTABLE_PATH = C:\Users\User\.cache\puppeteer\chrome-headless-shell\win64-142.0.7444.162\chrome-headless-shell-win64\chrome-headless-shell.exe
|
||||
@@ -0,0 +1,4 @@
|
||||
.env
|
||||
.env.prod
|
||||
/node_modules/
|
||||
/.github/workflows/deploy-dev.yml
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
FROM node:20-slim
|
||||
|
||||
# Install Chromium + dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
chromium \
|
||||
fonts-liberation \
|
||||
libatk-bridge2.0-0 \
|
||||
libatk1.0-0 \
|
||||
libcups2 \
|
||||
libxcomposite1 \
|
||||
libxrandr2 \
|
||||
libxdamage1 \
|
||||
libgbm1 \
|
||||
libasound2 \
|
||||
libpangocairo-1.0-0 \
|
||||
libpango-1.0-0 \
|
||||
libnss3 \
|
||||
libxss1 \
|
||||
libgtk-3-0 \
|
||||
libdrm2 \
|
||||
libxshmfence1 \
|
||||
ca-certificates \
|
||||
--no-install-recommends \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Tell Puppeteer to use system Chromium
|
||||
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium
|
||||
|
||||
# Prevent Puppeteer from downloading its own Chromium
|
||||
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
|
||||
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package*.json ./
|
||||
RUN npm ci --omit=dev
|
||||
|
||||
COPY . .
|
||||
|
||||
ENV NODE_ENV=production
|
||||
|
||||
EXPOSE 3070
|
||||
|
||||
CMD ["node", "server.js"]
|
||||
@@ -0,0 +1,125 @@
|
||||
# Auth API
|
||||
|
||||
#### Request OTP
|
||||
|
||||
**Endpoint**
|
||||
|
||||
```
|
||||
POST: http://localhost:3070/api/auth/req-otp
|
||||
```
|
||||
|
||||
**Request Body**
|
||||
|
||||
```json
|
||||
{
|
||||
"email": "sathira@niolla.lk",
|
||||
"password": "Niolla@123"
|
||||
}
|
||||
```
|
||||
|
||||
**Respond**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "OTP Sent Successfully"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### Login
|
||||
|
||||
**Endpoint**
|
||||
|
||||
```
|
||||
POST: http://localhost:3070/api/auth/login
|
||||
```
|
||||
|
||||
**Request Body**
|
||||
|
||||
```json
|
||||
{
|
||||
"email": "sathira@niolla.lk",
|
||||
"otp": "922304"
|
||||
}
|
||||
```
|
||||
|
||||
**Respond**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Login Successful",
|
||||
"data": {
|
||||
"id": "usr_5ff8afec-5ddc-47d9-a63f-b43df0d8c3b4",
|
||||
"email": "sathira@niolla.lk",
|
||||
"firstName": "Jhon",
|
||||
"lastName": "Doe",
|
||||
"role": "System Developer",
|
||||
"accountType": "admin"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### Get Current User
|
||||
|
||||
**Endpoint**
|
||||
|
||||
```
|
||||
GET: http://localhost:3070/api/auth/me
|
||||
```
|
||||
|
||||
**Authorization**: Required (Bearer token)
|
||||
|
||||
**Request Body**
|
||||
|
||||
```
|
||||
No Body
|
||||
```
|
||||
|
||||
**Response (200)**
|
||||
|
||||
```json
|
||||
{
|
||||
"authenticated": true,
|
||||
"user": {
|
||||
"id": "usr_5ff8afec-5ddc-47d9-a63f-b43df0d8c3b4",
|
||||
"firstName": "Sathira",
|
||||
"lastName": "Sri Sathsara",
|
||||
"email": "sathira@niolla.lk",
|
||||
"role": "System Developer",
|
||||
"accountType": "admin",
|
||||
"iat": 1778865265,
|
||||
"exp": 1778868865,
|
||||
"permissions": []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Logout
|
||||
|
||||
**Endpoint**
|
||||
|
||||
```
|
||||
POST: http://localhost:3070/api/auth/logout
|
||||
```
|
||||
|
||||
**Request Body**
|
||||
|
||||
```
|
||||
No Body
|
||||
```
|
||||
|
||||
**Respond**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Logged out successfully"
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,740 @@
|
||||
# Document API Documentation
|
||||
|
||||
## Overview
|
||||
|
||||
The Document API allows authenticated users to generate documents asynchronously (PDF, Excel, etc.) from provided data. The system uses a queue-based architecture for reliable, scalable document generation.
|
||||
|
||||
**Key Features:**
|
||||
- ✅ Non-blocking requests (returns immediately with jobId)
|
||||
- ✅ Job status polling to track progress
|
||||
- ✅ Automatic retries with exponential backoff
|
||||
- ✅ Support for multiple document types and formats
|
||||
- ✅ Concurrent document generation
|
||||
- ✅ Case-insensitive document names with whitespace and punctuation normalization
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Client Request → Queue Job → Return jobId (202)
|
||||
↓
|
||||
Worker processes
|
||||
↓
|
||||
Client polls status
|
||||
↓
|
||||
Job complete → Download file
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Endpoints
|
||||
|
||||
### 1. Generate Document
|
||||
|
||||
**Endpoint:** `POST /api/document/generate`
|
||||
|
||||
**URL:** `http://localhost:3070/api/document/generate`
|
||||
|
||||
**Authentication:** Required ✓
|
||||
|
||||
**Authorization:** Required - User must have one of the following roles:
|
||||
- `admin`
|
||||
- `management`
|
||||
- `team_head`
|
||||
- `user`
|
||||
|
||||
**HTTP Status:** `202 Accepted`
|
||||
|
||||
#### Request Headers
|
||||
```
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer <JWT_TOKEN>
|
||||
```
|
||||
|
||||
#### Request Body
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `document` | string | Yes | Type of document (e.g., "INVOICE", "DISPATCHNOTE", "DELIVERYNOTE") |
|
||||
| `documentType` | string | Yes | Output format (e.g., "pdf", "excel") |
|
||||
| `documentData` | object | Yes | Data object for the document |
|
||||
|
||||
#### Response (202 Accepted)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Document generation started",
|
||||
"jobId": "1"
|
||||
}
|
||||
```
|
||||
|
||||
#### Error Responses
|
||||
|
||||
**400 Bad Request:**
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "document, documentType, and documentData are required"
|
||||
}
|
||||
```
|
||||
|
||||
**500 Server Error:**
|
||||
```json
|
||||
|
||||
{
|
||||
"success": false,
|
||||
"message": "Error message describing the issue"
|
||||
}
|
||||
```
|
||||
|
||||
### Supported Document Values
|
||||
|
||||
You can send the document name in any case. Spaces, underscores, and hyphens are ignored by the generator.
|
||||
|
||||
- `PRECOST`
|
||||
- `INVOICE`
|
||||
- `DISPATCHNOTE`
|
||||
- `DELIVERYNOTE`
|
||||
- `CUSTOMDOCUMENT`
|
||||
- `CREDITNOTE`
|
||||
- `PO`
|
||||
|
||||
|
||||
## Sample Request Bodies
|
||||
|
||||
All examples below are for `POST /api/document/generate` with `documentType: "pdf"`.
|
||||
|
||||
### PreCost
|
||||
|
||||
```json
|
||||
{
|
||||
"document": "PRECOST",
|
||||
"documentType": "pdf",
|
||||
"documentData": {
|
||||
"vessel_name": "MV OCEAN STAR",
|
||||
"supplyPort": "DUBAI",
|
||||
"clientRfqNum": "RFQ-2026-001",
|
||||
"omsRfqNum": "OMS-RFQ-0001",
|
||||
"date": "08/07/2026",
|
||||
"items": [
|
||||
{
|
||||
"name": "Marine Rope",
|
||||
"description": "High strength rope",
|
||||
"qty": 10,
|
||||
"unitPrice": 12.5
|
||||
},
|
||||
{
|
||||
"name": "Safety Gloves",
|
||||
"description": "Blue gloves",
|
||||
"qty": 5,
|
||||
"unitPrice": 8
|
||||
}
|
||||
],
|
||||
"sub_total": 125,
|
||||
"discount": 0,
|
||||
"additionalCharges": 0,
|
||||
"total_cost": 125
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Invoice
|
||||
|
||||
```json
|
||||
{
|
||||
"document": "INVOICE",
|
||||
"documentType": "pdf",
|
||||
"documentData": {
|
||||
"jobReference": "JOB-2026-001",
|
||||
"poNumber": "PO-20254161",
|
||||
"date": "08/07/2026",
|
||||
"pageLabel": "Page 01 of 02",
|
||||
"billToName": "SUNRICH SHIP CHANDLERS L.L.C",
|
||||
"billToAddress": "BUSINESS BAY - ASPECT TOWER, 22ND FLOOR, OFFICE NO. 2201, P.O. BOX NO 39976, DUBAI",
|
||||
"vesselName": "MV OCEAN STAR",
|
||||
"purposeOfCall": "SUPPLY OF PROVISIONS",
|
||||
"supplyDate": "08/07/2026",
|
||||
"grt": "12345",
|
||||
"imoNumber": "9876543",
|
||||
"portCountry": "DUBAI / UAE",
|
||||
"items": [
|
||||
{
|
||||
"description": "Marine rope",
|
||||
"remarks": "Delivered as requested",
|
||||
"quantity": 10,
|
||||
"unit_price": 12.5
|
||||
},
|
||||
{
|
||||
"description": "Safety gloves",
|
||||
"remarks": "Blue color",
|
||||
"quantity": 5,
|
||||
"unit_price": 8
|
||||
}
|
||||
],
|
||||
"subtotal": 165,
|
||||
"tax": 0,
|
||||
"discount": 0,
|
||||
"total": 165,
|
||||
"paymentTerms": "Payment due within 30 days",
|
||||
"notes": "Please verify quantities upon delivery."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Dispatch Note
|
||||
|
||||
```json
|
||||
{
|
||||
"document": "DISPATCHNOTE",
|
||||
"documentType": "pdf",
|
||||
"documentData": {
|
||||
"referenceNumber": "DN/2026/001",
|
||||
"date": "08/07/2026",
|
||||
"vessel": "MV OCEAN STAR",
|
||||
"captain": "CAPT. JOHN DOE",
|
||||
"cook": "SAMUEL",
|
||||
"agent": "OCEANIC AGENT LTD",
|
||||
"details": "Delivery of provisions and stores",
|
||||
"placeOfDelivery": "DUBAI PORT",
|
||||
"eta": "08/07/2026 08:00",
|
||||
"etd": "08/07/2026 18:00",
|
||||
"numberOfCrew": 18,
|
||||
"nextMainOrderIn": "24 HRS",
|
||||
"port": "DUBAI",
|
||||
"company": "OCEANIC SHIP CHANDLERS (PVT) LTD",
|
||||
"fileNo": "FILE-2026-04",
|
||||
"items": [
|
||||
{
|
||||
"product": "Rice",
|
||||
"qty": 10,
|
||||
"unit": "BAGS",
|
||||
"supp": "OMS",
|
||||
"poNo": "PO-001",
|
||||
"receivedQty": 10,
|
||||
"issuedQty": 10,
|
||||
"expDate": "12/07/2026",
|
||||
"receivedTime": "09:15",
|
||||
"date": "08/07/2026",
|
||||
"rejects": "",
|
||||
"fatCon": "",
|
||||
"remark": "Delivered"
|
||||
}
|
||||
],
|
||||
"showSignatures": true,
|
||||
"preparedBy": "Admin User",
|
||||
"approvedBy": "Manager User"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Delivery Note
|
||||
|
||||
```json
|
||||
{
|
||||
"document": "DELIVERYNOTE",
|
||||
"documentType": "pdf",
|
||||
"documentData": {
|
||||
"referenceNumber": "DN/OSC/2602/01",
|
||||
"date": "08/07/2026",
|
||||
"billToName": "SUNRICH SHIP CHANDLERS L.L.C",
|
||||
"billToAddress": "BUSINESS BAY - ASPECT TOWER, 22ND FLOOR, OFFICE NO. 2201, P.O. BOX NO 39976, DUBAI",
|
||||
"supplyDate": "08/07/2026",
|
||||
"poNumber": "PO-20254161",
|
||||
"items": [
|
||||
{
|
||||
"description": "Marine rope",
|
||||
"remarks": "Delivered as requested",
|
||||
"unit": "PCS",
|
||||
"quantity": 10
|
||||
},
|
||||
{
|
||||
"description": "Safety gloves",
|
||||
"remarks": "Blue color",
|
||||
"unit": "BOX",
|
||||
"quantity": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Document
|
||||
|
||||
```json
|
||||
{
|
||||
"document": "CUSTOMDOCUMENT",
|
||||
"documentType": "pdf",
|
||||
"documentData": {
|
||||
"title": "CUSTOM DOCUMENT",
|
||||
"date": "08/07/2026",
|
||||
"telePhone": "+94 112 083 206",
|
||||
"emailAddress": "shipsupply@oceanicmsol.com",
|
||||
"officeAddressLine1": "Level 5D, Valiant Towers, Nawala Mawatha",
|
||||
"officeAddressLine2": "Colombo 02",
|
||||
"officeAddressLine3": "SRI LANKA",
|
||||
"directorOfCustoms": "The Director of Customs",
|
||||
"vehicleNo": "WPLO 9767",
|
||||
"permitNo": "MV20231214006001 / Permit No. MP20240717017006",
|
||||
"chiefSecurityOfficer": "Chief Security Officer",
|
||||
"exportGate": "Export Gate",
|
||||
"slpa": "SLPA",
|
||||
"gateNo": "Gate No. 03",
|
||||
"permissionText": "Please grant permission to pass these Customs entry papers to supply goods to the vessel",
|
||||
"companyName": "Oceanic Maritime Solutions (Pvt) Ltd",
|
||||
"subtitle": "LICENSED SHIPCHANDLERS",
|
||||
"sectionCode": "SC/FORM/06",
|
||||
"items": [
|
||||
{
|
||||
"quantity": 10,
|
||||
"unit": "PCS",
|
||||
"description": "Marine rope",
|
||||
"rate": 12.5,
|
||||
"total": 125,
|
||||
"confirmOrderRate": ""
|
||||
},
|
||||
{
|
||||
"quantity": 5,
|
||||
"unit": "PCS",
|
||||
"description": "Safety gloves",
|
||||
"rate": 8,
|
||||
"total": 40,
|
||||
"confirmOrderRate": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Credit Note
|
||||
|
||||
```json
|
||||
{
|
||||
"document": "CREDITNOTE",
|
||||
"documentType": "pdf",
|
||||
"documentData": {
|
||||
"companyName": "GREEK-LANKA MARITIME SERVICES (PVT) LTD",
|
||||
"companyAddressLine1": "No. 56/2, Dharmapala Mw, Kotte",
|
||||
"companyAddressLine2": "Sri Jayewardenepura",
|
||||
"companyAddressLine3": "Sri Lanka",
|
||||
"companyPostal": "Postal Code : 10100",
|
||||
"companyContact": "Tel:+94 11 2083206 / Mobile (24/7) : +94 777 232 271",
|
||||
"companyBR": "BR No : PV 0022630",
|
||||
"companyLicense": "License No : SA00317-2024",
|
||||
"crnNo": "GLMS/COLOMBO/474/CRN01",
|
||||
"date": "26/07/2024",
|
||||
"billToName": "Master & Owner of MV TEAM VENTURES",
|
||||
"billToDetails": "VRIDHI MARITIME SHIP MANAGEMENT & OPERATION LLC\nOffice No: 2004, The Prism Tower, Dubai\n20th Floor, Business Bay,\nDubai - PO Box 29583.",
|
||||
"vesselName": "MV \"TEAM VENTURES\"",
|
||||
"grt": "25,543 MT",
|
||||
"port": "REPAIRS AT COLOMBO DOCK YARD",
|
||||
"imoNumber": "9339765",
|
||||
"nameOfAgent": "29/04/2024 AT 21:00 HRS.LT",
|
||||
"portCountry": "COLOMBO / SRI LANKA",
|
||||
"items": [
|
||||
{
|
||||
"description": "SIGN OFF C/E MR. DHANSINGH BHAURYAL AND M/S J/E MR. SENTHIL",
|
||||
"amount": 50
|
||||
},
|
||||
{
|
||||
"description": "VISA + Handling Charges - M/S M/S MR. SENTHIL",
|
||||
"amount": 100
|
||||
},
|
||||
{
|
||||
"description": "Transport from Airport to Dockyard",
|
||||
"amount": 70
|
||||
}
|
||||
],
|
||||
"totalAmount": 220,
|
||||
"approvedBy": "GLMS Finance Dept.",
|
||||
"approvedByLabel": "Approved by GLMS Finance Dept.",
|
||||
"departmentLabel": "GLMS - Accounts Department"
|
||||
}
|
||||
}
|
||||
```
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. Get Job Status
|
||||
|
||||
**Endpoint:** `GET /api/document/job/:jobId/status`
|
||||
|
||||
**URL:** `http://localhost:3070/api/document/job/1/status`
|
||||
|
||||
**Authentication:** Required ✓
|
||||
|
||||
**Authorization:** Required
|
||||
|
||||
**HTTP Status:** `200 OK`
|
||||
|
||||
#### Request Headers
|
||||
```
|
||||
Authorization: Bearer <JWT_TOKEN>
|
||||
```
|
||||
|
||||
#### Response (Waiting/Active)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"jobId": "1",
|
||||
"state": "waiting",
|
||||
"result": null,
|
||||
"error": null,
|
||||
"attempts": 0,
|
||||
"stacktrace": []
|
||||
}
|
||||
```
|
||||
|
||||
#### Response (Completed)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"jobId": "83",
|
||||
"state": "completed",
|
||||
"result": {
|
||||
"fileName": "precost-1781614132061.pdf",
|
||||
"mimeType": "application/pdf",
|
||||
"size": 220008,
|
||||
"s3Key": "uploads/16adf29b-c0c2-45f9-8808-d6e15cd3d755.pdf",
|
||||
"documentId": "16adf29b-c0c2-45f9-8808-d6e15cd3d755"
|
||||
},
|
||||
"error": null,
|
||||
"attempts": 1,
|
||||
"stacktrace": []
|
||||
}
|
||||
```
|
||||
|
||||
#### Response (Failed)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"jobId": "1",
|
||||
"state": "failed",
|
||||
"result": null,
|
||||
"error": "Error message explaining the failure",
|
||||
"attempts": 3,
|
||||
"stacktrace": ["stack trace line 1", "stack trace line 2"]
|
||||
}
|
||||
```
|
||||
|
||||
#### Job States
|
||||
|
||||
| State | Meaning | Next State |
|
||||
|-------|---------|-----------|
|
||||
| `waiting` | Job queued, waiting for worker | `active` |
|
||||
| `active` | Worker currently processing | `completed` or `failed` |
|
||||
| `completed` | Job done, result available | (final) |
|
||||
| `failed` | Job failed after 3 retries | (final) |
|
||||
| `delayed` | Scheduled for later processing | `waiting` |
|
||||
|
||||
#### Error Responses
|
||||
|
||||
**404 Not Found:**
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "Job not found"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. Download Document
|
||||
|
||||
**Endpoint:** `GET /api/document/download/:uuid`
|
||||
|
||||
**URL:** `http://localhost:3070/api/document/download/16adf29b-c0c2-45f9-8808-d6e15cd3d755`
|
||||
|
||||
**Authentication:** Required ✓
|
||||
|
||||
**Authorization:** Required
|
||||
|
||||
**HTTP Status:** `200 OK`
|
||||
|
||||
#### Request Headers
|
||||
```
|
||||
Authorization: Bearer <JWT_TOKEN>
|
||||
```
|
||||
|
||||
#### Response (Success)
|
||||
|
||||
Returns the binary file with appropriate headers:
|
||||
|
||||
| Header | Value |
|
||||
|--------|-------|
|
||||
| `Content-Type` | `application/pdf` or `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet` |
|
||||
| `Content-Disposition` | `attachment; filename="[fileName]"` |
|
||||
|
||||
**Response Body:** Binary file content
|
||||
|
||||
#### Error Responses
|
||||
|
||||
**400 Bad Request:**
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "fileName is required"
|
||||
}
|
||||
```
|
||||
|
||||
**404 Not Found:**
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "Document not found"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. Cancel Job
|
||||
|
||||
**Endpoint:** `DELETE /api/document/job/:jobId`
|
||||
|
||||
**URL:** `http://localhost:3070/api/document/job/1`
|
||||
|
||||
**Authentication:** Required ✓
|
||||
|
||||
**Authorization:** Required
|
||||
|
||||
**HTTP Status:** `200 OK`
|
||||
|
||||
#### Request Headers
|
||||
```
|
||||
Authorization: Bearer <JWT_TOKEN>
|
||||
```
|
||||
|
||||
#### Response (Success)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Job cancelled successfully",
|
||||
"jobId": "1"
|
||||
}
|
||||
```
|
||||
|
||||
#### Error Responses
|
||||
|
||||
**404 Not Found:**
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "Job not found"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Document Data Notes
|
||||
|
||||
The generator is tolerant of different request shapes and common field aliases. For example, it accepts both `documentData` and nested `data`, and it normalizes document names by removing spaces, underscores, and hyphens.
|
||||
|
||||
If a document does not use every field in a sample payload, you can omit the unused keys.
|
||||
|
||||
---
|
||||
|
||||
## Complete Workflow Example
|
||||
|
||||
### 1. Generate Document
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:3070/api/document/generate \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
|
||||
-d '{
|
||||
"document": "precost",
|
||||
"documentType": "pdf",
|
||||
"documentData": {
|
||||
"customerName": "Acme Corp",
|
||||
"items": [
|
||||
{"name": "Item 1", "description": "Test", "qty": 2, "unitPrice": 100}
|
||||
]
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Document generation started",
|
||||
"jobId": "1"
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Check Job Status
|
||||
|
||||
```bash
|
||||
curl http://localhost:3070/api/document/job/1/status \
|
||||
-H "Authorization: Bearer YOUR_JWT_TOKEN"
|
||||
```
|
||||
|
||||
**Polling Response (Still Processing):**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"jobId": "1",
|
||||
"state": "active"
|
||||
}
|
||||
```
|
||||
|
||||
**Polling Response (Completed):**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"jobId": "1",
|
||||
"state": "completed",
|
||||
"result": {
|
||||
"fileName": "precost-1715339340000.pdf"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Download File
|
||||
|
||||
```bash
|
||||
curl http://localhost:3070/api/document/download/16adf29b-c0c2-45f9-8808-d6e15cd3d755 \
|
||||
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
|
||||
-o my-document.pdf
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## JavaScript/Fetch Example
|
||||
|
||||
```javascript
|
||||
async function generateAndDownloadDocument(token) {
|
||||
// Step 1: Generate document
|
||||
const generateRes = await fetch("/api/document/generate", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Authorization": `Bearer ${token}`,
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
document: "precost",
|
||||
documentType: "pdf",
|
||||
documentData: {
|
||||
customerName: "Acme Corp",
|
||||
items: [
|
||||
{ name: "Item 1", description: "Test", qty: 2, unitPrice: 100 }
|
||||
]
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
const { jobId } = await generateRes.json();
|
||||
console.log("Job queued:", jobId);
|
||||
|
||||
// Step 2: Poll for completion
|
||||
let isComplete = false;
|
||||
let result = null;
|
||||
|
||||
while (!isComplete) {
|
||||
const statusRes = await fetch(`/api/document/job/${jobId}/status`, {
|
||||
headers: { "Authorization": `Bearer ${token}` }
|
||||
});
|
||||
|
||||
const status = await statusRes.json();
|
||||
console.log("Job state:", status.state);
|
||||
|
||||
if (status.state === "completed") {
|
||||
result = status.result;
|
||||
isComplete = true;
|
||||
} else if (status.state === "failed") {
|
||||
throw new Error(`Job failed: ${status.error}`);
|
||||
} else {
|
||||
// Wait 2 seconds before polling again
|
||||
await new Promise(r => setTimeout(r, 2000));
|
||||
}
|
||||
}
|
||||
|
||||
// Step 3: Download the file
|
||||
const downloadRes = await fetch(`/api/document/download/${result.fileName}`, {
|
||||
headers: { "Authorization": `Bearer ${token}` }
|
||||
});
|
||||
|
||||
const blob = await downloadRes.blob();
|
||||
|
||||
// Trigger browser download
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = result.fileName;
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
|
||||
console.log("Download complete!");
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Important Notes
|
||||
|
||||
### Authentication & Authorization
|
||||
- All endpoints require JWT token in `Authorization: Bearer <token>` header
|
||||
- User must have one of these roles: `admin`, `management`, `team_head`, `user`
|
||||
|
||||
### Polling Strategy
|
||||
- Poll status every 2-5 seconds
|
||||
- Max recommended: 60 attempts (5 minutes timeout)
|
||||
- Move to download immediately when state is `completed`
|
||||
|
||||
### Error Handling
|
||||
- Jobs automatically retry up to 3 times with exponential backoff
|
||||
- Failed jobs remain in queue for debugging
|
||||
- Check `error` and `stacktrace` fields for failure details
|
||||
|
||||
### File Management
|
||||
- Generated files stored in `/app/documents/`
|
||||
- File names auto-generated: `{document}-{timestamp}.{ext}`
|
||||
- Always sanitize fileName in download requests
|
||||
|
||||
### Supported Formats
|
||||
- **PDF:** `documentType: "pdf"`
|
||||
- **Excel:** `documentType: "excel"`
|
||||
|
||||
### Performance
|
||||
- Worker concurrency: 2 concurrent jobs
|
||||
- Timeout per job: 5 minutes
|
||||
- Retry attempts: 3 with exponential backoff
|
||||
|
||||
---
|
||||
|
||||
## Common Response Codes
|
||||
|
||||
| Code | Meaning |
|
||||
|------|---------|
|
||||
| `202` | Accepted - Job queued successfully |
|
||||
| `200` | OK - Successful operation |
|
||||
| `400` | Bad Request - Invalid input |
|
||||
| `401` | Unauthorized - Missing/invalid token |
|
||||
| `403` | Forbidden - Insufficient permissions |
|
||||
| `404` | Not Found - Job or file not found |
|
||||
| `500` | Server Error - Internal error |
|
||||
|
||||
---
|
||||
|
||||
## Migration Notes
|
||||
|
||||
**If upgrading from synchronous API:**
|
||||
|
||||
**Old (Synchronous):**
|
||||
- Request returned file immediately
|
||||
- Long request timeout
|
||||
- Blocking operation
|
||||
|
||||
**New (Asynchronous):**
|
||||
- Request returns jobId immediately (202)
|
||||
- Poll `/api/document/job/{jobId}/status` for progress
|
||||
- Non-blocking, scalable architecture
|
||||
- Automatic retries built-in
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
|
||||
# Notification API
|
||||
|
||||
## Create New Notification
|
||||
|
||||
**Endpoint**
|
||||
|
||||
```
|
||||
POST: http://localhost:3070/api/notification
|
||||
```
|
||||
|
||||
|
||||
**Request Body**
|
||||
|
||||
```json
|
||||
{
|
||||
"notificationHeadline": "System Maintenance",
|
||||
"notificationDescription": "The system will be unavailable from 10 PM to 12 AM.",
|
||||
"notificationType": "ANNOUNCEMENT"
|
||||
}
|
||||
```
|
||||
|
||||
**Respond**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Notification created successfully",
|
||||
"notification": {
|
||||
"notification_id": "notif_1782553200000",
|
||||
"notificationHeadline": "System Maintenance",
|
||||
"notificationDescription": "The system will be unavailable from 10 PM to 12 AM.",
|
||||
"notificationType": "ANNOUNCEMENT",
|
||||
"dateCreated": "2026-06-27T05:30:00.000Z",
|
||||
"isActive": true,
|
||||
"updatedAt": "2026-06-27T05:30:00.000Z",
|
||||
"createdAt": "2026-06-27T05:30:00.000Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
## Get Announcements
|
||||
|
||||
**Endpoint**
|
||||
|
||||
```
|
||||
GET: http://localhost:3070/api/notification/announcements
|
||||
```
|
||||
|
||||
**Respond**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"announcements": [
|
||||
{
|
||||
"notification_id": "notif_1782553200000",
|
||||
"notificationHeadline": "System Maintenance",
|
||||
"notificationDescription": "The system will be unavailable from 10 PM to 12 AM.",
|
||||
"notificationType": "ANNOUNCEMENT",
|
||||
"isActive": true,
|
||||
"dateCreated": "2026-06-27T05:30:00.000Z",
|
||||
"createdAt": "2026-06-27T05:30:00.000Z",
|
||||
"updatedAt": "2026-06-27T05:30:00.000Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Get User Notifications
|
||||
|
||||
**Endpoint**
|
||||
|
||||
```
|
||||
GET: http://localhost:3070/api/notification/user/:userId
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
GET: http://localhost:3070/api/notification/user/usr_12345
|
||||
```
|
||||
|
||||
**Respond**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"notifications": [
|
||||
{
|
||||
"id": "userNotif_001",
|
||||
"user_id": "usr_12345",
|
||||
"notification_id": "notif_1782553200000",
|
||||
"isRead": false,
|
||||
"createdAt": "2026-06-27T05:30:00.000Z",
|
||||
"updatedAt": "2026-06-27T05:30:00.000Z",
|
||||
"notification": {
|
||||
"notification_id": "notif_1782553200000",
|
||||
"notificationHeadline": "System Maintenance",
|
||||
"notificationDescription": "The system will be unavailable from 10 PM to 12 AM.",
|
||||
"notificationType": "ANNOUNCEMENT",
|
||||
"isActive": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Mark Notification As Read
|
||||
|
||||
**Endpoint**
|
||||
|
||||
```
|
||||
PATCH: http://localhost:3070/api/notification/user/:userId/notification/:notificationId/read
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
PATCH: http://localhost:3070/api/notification/user/usr_12345/notification/notif_1782553200000/read
|
||||
```
|
||||
|
||||
**Respond**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Notification marked as read"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -0,0 +1,755 @@
|
||||
# Permission API
|
||||
|
||||
## Authentication
|
||||
All endpoints require authentication token. Include in header:
|
||||
```
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Permission Endpoints
|
||||
|
||||
### Create Permission
|
||||
|
||||
**Endpoint**
|
||||
```
|
||||
POST: http://localhost:3070/api/permission
|
||||
```
|
||||
|
||||
**Authorization**: Admin only
|
||||
|
||||
**Request Body**
|
||||
```json
|
||||
{
|
||||
"permissionName": "Create Inquiry",
|
||||
"permissionDescription": "Permission to create new inquiries",
|
||||
"page": "inquiry",
|
||||
"module": "inquiry_management",
|
||||
"action": "create"
|
||||
}
|
||||
```
|
||||
|
||||
**Response (201)**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"permission_id": "inquiry_management.inquiry.create",
|
||||
"permissionName": "Create Inquiry",
|
||||
"permissionDescription": "Permission to create new inquiries",
|
||||
"page": "inquiry",
|
||||
"module": "inquiry_management",
|
||||
"action": "create",
|
||||
"createdAt": "2026-05-15T10:30:00Z",
|
||||
"updatedAt": "2026-05-15T10:30:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Bulk Create Permissions
|
||||
|
||||
**Endpoint**
|
||||
```
|
||||
POST: http://localhost:3070/api/permission/bulk
|
||||
```
|
||||
|
||||
**Authorization**: Admin only
|
||||
|
||||
**Request Body**
|
||||
```json
|
||||
{
|
||||
"permissions": [
|
||||
{
|
||||
"permissionName": "Create Inquiry",
|
||||
"permissionDescription": "Permission to create new inquiries",
|
||||
"page": "inquiry",
|
||||
"module": "inquiry_management",
|
||||
"action": "create"
|
||||
},
|
||||
{
|
||||
"permissionName": "View Inquiry",
|
||||
"permissionDescription": "Permission to view inquiries",
|
||||
"page": "inquiry",
|
||||
"module": "inquiry_management",
|
||||
"action": "view"
|
||||
},
|
||||
{
|
||||
"permissionName": "Edit Inquiry",
|
||||
"permissionDescription": "Permission to edit inquiries",
|
||||
"page": "inquiry",
|
||||
"module": "inquiry_management",
|
||||
"action": "edit"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Response (201)**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"created": [
|
||||
{
|
||||
"permission_id": "inquiry_management.inquiry.create",
|
||||
"permissionName": "Create Inquiry",
|
||||
"permissionDescription": "Permission to create new inquiries",
|
||||
"page": "inquiry",
|
||||
"module": "inquiry_management",
|
||||
"action": "create"
|
||||
},
|
||||
{
|
||||
"permission_id": "inquiry_management.inquiry.view",
|
||||
"permissionName": "View Inquiry",
|
||||
"permissionDescription": "Permission to view inquiries",
|
||||
"page": "inquiry",
|
||||
"module": "inquiry_management",
|
||||
"action": "view"
|
||||
},
|
||||
{
|
||||
"permission_id": "inquiry_management.inquiry.edit",
|
||||
"permissionName": "Edit Inquiry",
|
||||
"permissionDescription": "Permission to edit inquiries",
|
||||
"page": "inquiry",
|
||||
"module": "inquiry_management",
|
||||
"action": "edit"
|
||||
}
|
||||
],
|
||||
"failed": []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Get All Permissions
|
||||
|
||||
**Endpoint**
|
||||
```
|
||||
GET: http://localhost:3070/api/permission
|
||||
```
|
||||
|
||||
**Response (200)**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": [
|
||||
{
|
||||
"permission_id": "inquiry_management.inquiry.create",
|
||||
"permissionName": "Create Inquiry",
|
||||
"permissionDescription": "Permission to create new inquiries",
|
||||
"page": "inquiry",
|
||||
"module": "inquiry_management",
|
||||
"action": "create"
|
||||
},
|
||||
{
|
||||
"permission_id": "inquiry_management.inquiry.view",
|
||||
"permissionName": "View Inquiry",
|
||||
"permissionDescription": "Permission to view inquiries",
|
||||
"page": "inquiry",
|
||||
"module": "inquiry_management",
|
||||
"action": "view"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Get Permission by ID
|
||||
|
||||
**Endpoint**
|
||||
```
|
||||
GET: http://localhost:3070/api/permission/:permissionId
|
||||
```
|
||||
|
||||
**Example**
|
||||
```
|
||||
GET: http://localhost:3070/api/permission/inquiry_management.inquiry.create
|
||||
```
|
||||
|
||||
**Response (200)**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"permission_id": "inquiry_management.inquiry.create",
|
||||
"permissionName": "Create Inquiry",
|
||||
"permissionDescription": "Permission to create new inquiries",
|
||||
"page": "inquiry",
|
||||
"module": "inquiry_management",
|
||||
"action": "create"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Update Permission
|
||||
|
||||
**Endpoint**
|
||||
```
|
||||
PUT: http://localhost:3070/api/permission/:permissionId
|
||||
```
|
||||
|
||||
**Authorization**: Admin only
|
||||
|
||||
**Request Body**
|
||||
```json
|
||||
{
|
||||
"permissionName": "Create New Inquiry",
|
||||
"permissionDescription": "Updated permission to create new inquiries"
|
||||
}
|
||||
```
|
||||
|
||||
**Response (200)**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"permission_id": "inquiry_management.inquiry.create",
|
||||
"permissionName": "Create New Inquiry",
|
||||
"permissionDescription": "Updated permission to create new inquiries",
|
||||
"page": "inquiry",
|
||||
"module": "inquiry_management",
|
||||
"action": "create"
|
||||
},
|
||||
"message": "Permission updated successfully"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Delete Permission
|
||||
|
||||
**Endpoint**
|
||||
```
|
||||
DELETE: http://localhost:3070/api/permission/:permissionId
|
||||
```
|
||||
|
||||
**Authorization**: Admin only
|
||||
|
||||
**Response (200)**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Permission deleted successfully"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Role Endpoints
|
||||
|
||||
### Create Role
|
||||
|
||||
**Endpoint**
|
||||
```
|
||||
POST: http://localhost:3070/api/permission/roles
|
||||
```
|
||||
|
||||
**Authorization**: Admin only
|
||||
|
||||
**Request Body**
|
||||
```json
|
||||
{
|
||||
"roleName": "Inquiry Manager",
|
||||
"roleDescription": "Role for managing inquiries"
|
||||
}
|
||||
```
|
||||
|
||||
**Response (201)**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"role_id": "role_1715843400000",
|
||||
"roleName": "Inquiry Manager",
|
||||
"roleDescription": "Role for managing inquiries",
|
||||
"createdAt": "2026-05-15T10:30:00Z",
|
||||
"updatedAt": "2026-05-15T10:30:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Get All Roles
|
||||
|
||||
**Endpoint**
|
||||
```
|
||||
GET: http://localhost:3070/api/permission/roles
|
||||
```
|
||||
|
||||
**Response (200)**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": [
|
||||
{
|
||||
"role_id": "role_1715843400000",
|
||||
"roleName": "Inquiry Manager",
|
||||
"roleDescription": "Role for managing inquiries",
|
||||
"rolePermissions": [
|
||||
{
|
||||
"rp_id": "rp_1715843500000",
|
||||
"role_id": "role_1715843400000",
|
||||
"permission_id": "inquiry_management.inquiry.create",
|
||||
"permission": {
|
||||
"permission_id": "inquiry_management.inquiry.create",
|
||||
"permissionName": "Create Inquiry",
|
||||
"page": "inquiry",
|
||||
"module": "inquiry_management",
|
||||
"action": "create"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Get Role by ID
|
||||
|
||||
**Endpoint**
|
||||
```
|
||||
GET: http://localhost:3070/api/permission/roles/:roleId
|
||||
```
|
||||
|
||||
**Example**
|
||||
```
|
||||
GET: http://localhost:3070/api/permission/roles/role_1715843400000
|
||||
```
|
||||
|
||||
**Response (200)**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"role_id": "role_1715843400000",
|
||||
"roleName": "Inquiry Manager",
|
||||
"roleDescription": "Role for managing inquiries",
|
||||
"rolePermissions": [
|
||||
{
|
||||
"rp_id": "rp_1715843500000",
|
||||
"role_id": "role_1715843400000",
|
||||
"permission_id": "inquiry_management.inquiry.create",
|
||||
"permission": {
|
||||
"permission_id": "inquiry_management.inquiry.create",
|
||||
"permissionName": "Create Inquiry",
|
||||
"page": "inquiry",
|
||||
"module": "inquiry_management",
|
||||
"action": "create"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Update Role
|
||||
|
||||
**Endpoint**
|
||||
```
|
||||
PUT: http://localhost:3070/api/permission/roles/:roleId
|
||||
```
|
||||
|
||||
**Authorization**: Admin only
|
||||
|
||||
**Request Body**
|
||||
```json
|
||||
{
|
||||
"roleName": "Senior Inquiry Manager",
|
||||
"roleDescription": "Updated role for managing inquiries"
|
||||
}
|
||||
```
|
||||
|
||||
**Response (200)**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"role_id": "role_1715843400000",
|
||||
"roleName": "Senior Inquiry Manager",
|
||||
"roleDescription": "Updated role for managing inquiries"
|
||||
},
|
||||
"message": "Role updated successfully"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Delete Role
|
||||
|
||||
**Endpoint**
|
||||
```
|
||||
DELETE: http://localhost:3070/api/permission/roles/:roleId
|
||||
```
|
||||
|
||||
**Authorization**: Admin only
|
||||
|
||||
**Response (200)**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Role deleted successfully"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Role-Permission Assignment Endpoints
|
||||
|
||||
### Assign Permission to Role
|
||||
|
||||
**Endpoint**
|
||||
```
|
||||
POST: http://localhost:3070/api/permission/roles/:roleId/permissions
|
||||
```
|
||||
|
||||
**Authorization**: Admin only
|
||||
|
||||
**Request Body**
|
||||
```json
|
||||
{
|
||||
"role_id": "role_1715843400000",
|
||||
"permission_id": "inquiry_management.inquiry.create"
|
||||
}
|
||||
```
|
||||
|
||||
**Response (201)**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"rp_id": "rp_1715843500000",
|
||||
"role_id": "role_1715843400000",
|
||||
"permission_id": "inquiry_management.inquiry.create",
|
||||
"createdAt": "2026-05-15T10:30:00Z",
|
||||
"updatedAt": "2026-05-15T10:30:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Bulk Assign Permissions to Role
|
||||
|
||||
**Endpoint**
|
||||
```
|
||||
POST: http://localhost:3070/api/permission/roles/permissions/bulk
|
||||
```
|
||||
|
||||
**Authorization**: Admin only
|
||||
|
||||
**Request Body**
|
||||
```json
|
||||
{
|
||||
"role_id": "role_1715843400000",
|
||||
"permission_ids": [
|
||||
"inquiry_management.inquiry.create",
|
||||
"inquiry_management.inquiry.view",
|
||||
"inquiry_management.inquiry.edit",
|
||||
"vendor_management.vendor.view"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Response (201)**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"created": [
|
||||
{
|
||||
"rp_id": "rp_1715843500001",
|
||||
"role_id": "role_1715843400000",
|
||||
"permission_id": "inquiry_management.inquiry.create"
|
||||
},
|
||||
{
|
||||
"rp_id": "rp_1715843500002",
|
||||
"role_id": "role_1715843400000",
|
||||
"permission_id": "inquiry_management.inquiry.view"
|
||||
},
|
||||
{
|
||||
"rp_id": "rp_1715843500003",
|
||||
"role_id": "role_1715843400000",
|
||||
"permission_id": "inquiry_management.inquiry.edit"
|
||||
},
|
||||
{
|
||||
"rp_id": "rp_1715843500004",
|
||||
"role_id": "role_1715843400000",
|
||||
"permission_id": "vendor_management.vendor.view"
|
||||
}
|
||||
],
|
||||
"failed": []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Response with Failures (201)**
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"data": {
|
||||
"created": [
|
||||
{
|
||||
"rp_id": "rp_1715843500001",
|
||||
"role_id": "role_1715843400000",
|
||||
"permission_id": "inquiry_management.inquiry.create"
|
||||
}
|
||||
],
|
||||
"failed": [
|
||||
{
|
||||
"role_id": "role_1715843400000",
|
||||
"permission_id": "inquiry_management.inquiry.view",
|
||||
"error": "Role permission already exists"
|
||||
},
|
||||
{
|
||||
"role_id": "role_1715843400000",
|
||||
"permission_id": "nonexistent_permission",
|
||||
"error": "Permission not found"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## User Permission Endpoints
|
||||
|
||||
### Get User Permissions
|
||||
|
||||
**Endpoint**
|
||||
```
|
||||
GET: http://localhost:3070/api/permission/users/:userId/permissions
|
||||
```
|
||||
|
||||
**Example**
|
||||
```
|
||||
GET: http://localhost:3070/api/permission/users/usr_5ff8afec-5ddc-47d9-a63f-b43df0d8c3b4/permissions
|
||||
```
|
||||
|
||||
**Response (200)**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": [
|
||||
{
|
||||
"up_id": 1,
|
||||
"user_id": "usr_5ff8afec-5ddc-47d9-a63f-b43df0d8c3b4",
|
||||
"permission_id": "inquiry_management.inquiry.create",
|
||||
"permission": {
|
||||
"permission_id": "inquiry_management.inquiry.create",
|
||||
"permissionName": "Create Inquiry",
|
||||
"page": "inquiry",
|
||||
"module": "inquiry_management",
|
||||
"action": "create"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Create User Permission
|
||||
|
||||
**Endpoint**
|
||||
```
|
||||
POST: http://localhost:3070/api/permission/users/permissions
|
||||
```
|
||||
|
||||
**Authorization**: Admin only
|
||||
|
||||
**Request Body**
|
||||
```json
|
||||
{
|
||||
"user_id": "usr_5ff8afec-5ddc-47d9-a63f-b43df0d8c3b4",
|
||||
"permission_id": "inquiry_management.inquiry.create"
|
||||
}
|
||||
```
|
||||
|
||||
**Response (201)**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"up_id": 1,
|
||||
"user_id": "usr_5ff8afec-5ddc-47d9-a63f-b43df0d8c3b4",
|
||||
"permission_id": "inquiry_management.inquiry.create",
|
||||
"createdAt": "2026-05-15T10:30:00Z",
|
||||
"updatedAt": "2026-05-15T10:30:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Bulk Create User Permissions
|
||||
|
||||
**Endpoint**
|
||||
```
|
||||
POST: http://localhost:3070/api/permission/users/permissions/bulk
|
||||
```
|
||||
|
||||
**Authorization**: Admin only
|
||||
|
||||
**Request Body**
|
||||
```json
|
||||
{
|
||||
"user_id": "usr_5ff8afec-5ddc-47d9-a63f-b43df0d8c3b4",
|
||||
"permission_ids": [
|
||||
"inquiry_management.inquiry.create",
|
||||
"inquiry_management.inquiry.view",
|
||||
"inquiry_management.inquiry.edit",
|
||||
"vendor_management.vendor.view"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Response (201)**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"created": [
|
||||
{
|
||||
"up_id": 1,
|
||||
"user_id": "usr_5ff8afec-5ddc-47d9-a63f-b43df0d8c3b4",
|
||||
"permission_id": "inquiry_management.inquiry.create"
|
||||
},
|
||||
{
|
||||
"up_id": 2,
|
||||
"user_id": "usr_5ff8afec-5ddc-47d9-a63f-b43df0d8c3b4",
|
||||
"permission_id": "inquiry_management.inquiry.view"
|
||||
},
|
||||
{
|
||||
"up_id": 3,
|
||||
"user_id": "usr_5ff8afec-5ddc-47d9-a63f-b43df0d8c3b4",
|
||||
"permission_id": "inquiry_management.inquiry.edit"
|
||||
},
|
||||
{
|
||||
"up_id": 4,
|
||||
"user_id": "usr_5ff8afec-5ddc-47d9-a63f-b43df0d8c3b4",
|
||||
"permission_id": "vendor_management.vendor.view"
|
||||
}
|
||||
],
|
||||
"failed": []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Response with Failures (201)**
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"data": {
|
||||
"created": [
|
||||
{
|
||||
"up_id": 1,
|
||||
"user_id": "usr_5ff8afec-5ddc-47d9-a63f-b43df0d8c3b4",
|
||||
"permission_id": "inquiry_management.inquiry.create"
|
||||
}
|
||||
],
|
||||
"failed": [
|
||||
{
|
||||
"user_id": "usr_5ff8afec-5ddc-47d9-a63f-b43df0d8c3b4",
|
||||
"permission_id": "inquiry_management.inquiry.view",
|
||||
"error": "User permission already exists"
|
||||
},
|
||||
{
|
||||
"user_id": "usr_5ff8afec-5ddc-47d9-a63f-b43df0d8c3b4",
|
||||
"permission_id": "nonexistent_permission",
|
||||
"error": "Permission not found"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Update User Permission
|
||||
|
||||
**Endpoint**
|
||||
```
|
||||
PUT: http://localhost:3070/api/permission/users/permissions/:upId
|
||||
```
|
||||
|
||||
**Authorization**: Admin only
|
||||
|
||||
**Request Body**
|
||||
```json
|
||||
{
|
||||
"permission_id": "inquiry_management.inquiry.edit"
|
||||
}
|
||||
```
|
||||
|
||||
**Response (200)**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"up_id": 1,
|
||||
"user_id": "usr_5ff8afec-5ddc-47d9-a63f-b43df0d8c3b4",
|
||||
"permission_id": "inquiry_management.inquiry.edit"
|
||||
},
|
||||
"message": "User permission updated successfully"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Delete User Permission
|
||||
|
||||
**Endpoint**
|
||||
```
|
||||
DELETE: http://localhost:3070/api/permission/users/permissions/:upId
|
||||
```
|
||||
|
||||
**Authorization**: Admin only
|
||||
|
||||
**Response (200)**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "User permission deleted successfully"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Responses
|
||||
|
||||
### Bad Request (400)
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "Permission name, page, module, and action are required"
|
||||
}
|
||||
```
|
||||
|
||||
### Not Found (404)
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "Permission not found"
|
||||
}
|
||||
```
|
||||
|
||||
### Internal Server Error (500)
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "An error occurred while fetching permissions."
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,253 @@
|
||||
# Profile API
|
||||
|
||||
#### Get Profile Avatar
|
||||
|
||||
**Endpoint**
|
||||
|
||||
```
|
||||
GET: http://localhost:3070/api/profile/avatar/:userId
|
||||
```
|
||||
|
||||
**Path Parameters**
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|--------|----------|-----------------|
|
||||
| userId | string | Yes | The user ID |
|
||||
|
||||
**Headers**
|
||||
|
||||
```
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
**Response**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"userId": "usr_763107d9-b56f-4b81-9d86-1889f98a6e6c",
|
||||
"profilePictureUrl": "https://signed-s3-url.com/profile-picture.jpg"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Error Responses**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "Profile not found",
|
||||
"error": "Profile not found"
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "Internal server error",
|
||||
"error": "error message"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### Get Profile Background Image
|
||||
|
||||
**Endpoint**
|
||||
|
||||
```
|
||||
GET: http://localhost:3070/api/profile/background/:userId
|
||||
```
|
||||
|
||||
**Path Parameters**
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|--------|----------|-----------------|
|
||||
| userId | string | Yes | The user ID |
|
||||
|
||||
**Headers**
|
||||
|
||||
```
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
**Response**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"userId": "usr_763107d9-b56f-4b81-9d86-1889f98a6e6c",
|
||||
"backgroundImageUrl": "https://signed-s3-url.com/background-image.jpg"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Error Responses**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "Profile not found",
|
||||
"error": "Profile not found"
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "Internal server error",
|
||||
"error": "error message"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### Profile Object Structure
|
||||
|
||||
The Profile object contains the following fields:
|
||||
|
||||
| Field | Type | Description |
|
||||
|--------------------|---------|----------------------------------|
|
||||
| profile_id | string | Unique profile identifier |
|
||||
| user_id | string | Associated user ID |
|
||||
| theme | string | User theme preference (light/dark) |
|
||||
| notificationsEnabled | boolean | Notification settings |
|
||||
| profilePicture_id | string | Upload ID for profile picture |
|
||||
| backgroundImage_id | string | Upload ID for background image |
|
||||
| dob | date | Date of birth |
|
||||
| phone_number | string | Phone number |
|
||||
| createdAt | datetime| Profile creation timestamp |
|
||||
| updatedAt | datetime| Profile last update timestamp |
|
||||
|
||||
---
|
||||
|
||||
#### Request Password Reset
|
||||
|
||||
**Endpoint**
|
||||
|
||||
```
|
||||
POST: http://localhost:3070/api/profile/req-reset-password
|
||||
```
|
||||
|
||||
**Request Body**
|
||||
|
||||
```json
|
||||
{
|
||||
"email": "sathira.nirmal@gmail.com"
|
||||
}
|
||||
```
|
||||
|
||||
**Response**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "If an account exists with this email address, a password reset email has been sent."
|
||||
}
|
||||
```
|
||||
|
||||
**Description**
|
||||
|
||||
This endpoint generates a password reset token and sends a reset link to the user's email. The reset link will be sent in email format:
|
||||
|
||||
```
|
||||
http://localhost:3000/reset-password?token=TOKEN_HERE&email=EMAIL_HERE
|
||||
```
|
||||
|
||||
Example reset link:
|
||||
|
||||
```
|
||||
http://localhost:3000/reset-password?token=aa27def4222adedcf72a417dfc40b69cd01f8bdb07f37b324a8501f3bdb37e44&email=sathira.nirmal%40gmail.com
|
||||
```
|
||||
|
||||
**Notes**
|
||||
|
||||
- This endpoint does not require authentication
|
||||
- No error is returned if email doesn't exist (for security reasons)
|
||||
- Token expires after the time specified in `PASSWORD_RESET_EXPIRY_TIME` environment variable (default: 10 minutes)
|
||||
|
||||
---
|
||||
|
||||
#### Reset Password
|
||||
|
||||
**Endpoint**
|
||||
|
||||
```
|
||||
POST: http://localhost:3070/api/profile/reset-password
|
||||
```
|
||||
|
||||
**Request Body**
|
||||
|
||||
```json
|
||||
{
|
||||
"email": "sathira.nirmal@gmail.com",
|
||||
"token": "168e5d9d4dfb124571f412c5521874e8440a0f9e4c151fa24518ae57ca1c4f8f",
|
||||
"newPassword": "niolla"
|
||||
}
|
||||
```
|
||||
|
||||
**Response**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Password reset successfully."
|
||||
}
|
||||
```
|
||||
|
||||
**Error Responses**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "Invalid email address."
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "Invalid or expired token."
|
||||
}
|
||||
```
|
||||
|
||||
**Description**
|
||||
|
||||
This endpoint resets the user's password using a valid reset token. The token must be obtained from the password reset email.
|
||||
|
||||
**Notes**
|
||||
|
||||
- This endpoint does not require authentication
|
||||
- Token must be valid and not expired
|
||||
- New password will replace the old password immediately
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
#### Authentication
|
||||
|
||||
All endpoints require authentication token unless otherwise noted. Include the Bearer token in the Authorization header:
|
||||
|
||||
```
|
||||
Authorization: Bearer <your_jwt_token>
|
||||
```
|
||||
|
||||
**Authentication Required:**
|
||||
- GET /avatar/:userId
|
||||
- GET /background/:userId
|
||||
- POST /change-password
|
||||
|
||||
**No Authentication Required:**
|
||||
- POST /req-reset-password
|
||||
- POST /reset-password
|
||||
|
||||
#### Authorization
|
||||
|
||||
Access levels for profile endpoints:
|
||||
|
||||
- **GET /avatar/:userId** - Accessible by: admin, management, team_head, user
|
||||
- **GET /background/:userId** - Accessible by: admin, management, team_head, user
|
||||
- **POST /change-password** - Accessible by: admin, management, team_head, user (authenticated users)
|
||||
@@ -0,0 +1,61 @@
|
||||
# Upload API
|
||||
|
||||
#### Upload A File
|
||||
|
||||
**Endpoint**
|
||||
|
||||
```
|
||||
POST: http://localhost:3070/api/upload
|
||||
```
|
||||
|
||||
**Request Body**
|
||||
|
||||
```
|
||||
Key: file
|
||||
Value: uploadfile.pdf / avatar.png
|
||||
```
|
||||
|
||||
|
||||
**Respond**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "File uploaded successfully",
|
||||
"data": {
|
||||
"id": 4,
|
||||
"file_path": "uploads/1f642d1d-dc79-423f-aad0-36df9938c1ff.pdf",
|
||||
"file_type": "application/pdf",
|
||||
"file_size": 15640,
|
||||
"original_name": "SamplePDF.pdf",
|
||||
"use_for": "vendor_documents",
|
||||
"uploaded_by": "usr_5ff8afec-5ddc-47d9-a63f-b43df0d8c3b4",
|
||||
"updatedAt": "2026-04-04T19:44:03.252Z",
|
||||
"createdAt": "2026-04-04T19:44:03.252Z",
|
||||
"file_url": "https://oceanic-bucket.s3.ap-southeast-1.amazonaws.com/uploads/1f642d1d-dc79-423f-aad0-36df9938c1ff.pdf?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=AKIASP2AP6EU76EJ63PD%2F20260404%2Fap-southeast-1%2Fs3%2Faws4_request&X-Amz-Date=20260404T194403Z&X-Amz-Expires=3600&X-Amz-Signature=f9ecb283dfe206e87fa9bbd9f59194dfca4cdb11d8bb02f0ae5abd89db7ddc04&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Get Uploaded File
|
||||
|
||||
**Endpoint**
|
||||
|
||||
```
|
||||
POST: http://localhost:3070/api/upload/signed-url/:id
|
||||
```
|
||||
|
||||
Example: http://localhost:3070/api/upload/signed-url/3
|
||||
|
||||
**Respond**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "File URL retrieved successfully",
|
||||
"data": {
|
||||
"id": 3,
|
||||
"file_url": "https://oceanic-bucket.s3.ap-southeast-1.amazonaws.com/uploads/9c441266-c1ee-4813-9b81-cf13276535c9.pdf?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=AKIASP2AP6EU76EJ63PD%2F20260404%2Fap-southeast-1%2Fs3%2Faws4_request&X-Amz-Date=20260404T195205Z&X-Amz-Expires=3600&X-Amz-Signature=de8f9a4658b2802af4230f0ebba25511fb57e75cce4be75aa2e34d40a771bffb&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject"
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,223 @@
|
||||
# User API
|
||||
|
||||
#### Create New User
|
||||
|
||||
**Endpoint**
|
||||
|
||||
```
|
||||
POST: http://localhost:3070/api/user
|
||||
```
|
||||
|
||||
**Request Body**
|
||||
|
||||
```json
|
||||
{
|
||||
"firstName": "Jhon",
|
||||
"lastName": "Doe",
|
||||
"email": "kalanajayasekara@niolla.lk",
|
||||
"role": "System Developer",
|
||||
"accountType": "admin",
|
||||
"department": "IT Department"
|
||||
}
|
||||
```
|
||||
|
||||
**Respond**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "User Created Successfully",
|
||||
"data": {
|
||||
"id": "usr_763107d9-b56f-4b81-9d86-1889f98a6e6c",
|
||||
"firstName": "Jhon",
|
||||
"lastName": "Doe",
|
||||
"email": "kalanajayasekara@niolla.lk",
|
||||
"accountType": "admin",
|
||||
"role": "System Developer",
|
||||
"department": "IT Department"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Get All Users
|
||||
|
||||
**Endpoint**
|
||||
|
||||
```
|
||||
GET: http://localhost:3070/api/user
|
||||
```
|
||||
|
||||
**Respond**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": [
|
||||
{
|
||||
"id": "usr_b00045f7-aafb-482a-8587-d28beb5195ec",
|
||||
"firstName": "Kalana",
|
||||
"lastName": "Jayasekara",
|
||||
"email": "kalanamanupiya32@gmail.com",
|
||||
"accountType": "admin",
|
||||
"role": "Manager",
|
||||
"department": "Operations",
|
||||
"createdAt": "2026-02-14T19:14:25.000Z",
|
||||
"updatedAt": "2026-02-14T19:14:25.000Z"
|
||||
},
|
||||
{
|
||||
"id": "usr_763107d9-b56f-4b81-9d86-1889f98a6e6c",
|
||||
"firstName": "Kalana",
|
||||
"lastName": "Manupiya",
|
||||
"email": "kalanajayasekara@niolla.lk",
|
||||
"accountType": "admin",
|
||||
"role": "System Developer",
|
||||
"department": "IT Department",
|
||||
"createdAt": "2026-02-12T19:32:15.000Z",
|
||||
"updatedAt": "2026-02-12T19:32:15.000Z"
|
||||
},
|
||||
{
|
||||
"id": "usr_5ff8afec-5ddc-47d9-a63f-b43df0d8c3b4",
|
||||
"firstName": "Jhon",
|
||||
"lastName": "Doe",
|
||||
"email": "sathira@niolla.lk",
|
||||
"accountType": "admin",
|
||||
"role": "System Developer",
|
||||
"department": "IT Department",
|
||||
"createdAt": "2026-02-12T19:24:04.000Z",
|
||||
"updatedAt": "2026-02-12T19:24:04.000Z"
|
||||
}
|
||||
],
|
||||
"pagination": {
|
||||
"totalUsers": 3,
|
||||
"totalPages": 1,
|
||||
"currentPage": 1,
|
||||
"pageSize": 20
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This API uses Pagination:
|
||||
|
||||
```
|
||||
GET /user?page=1
|
||||
GET /user?page=2
|
||||
GET /user?page=3
|
||||
```
|
||||
|
||||
#### Get User by ID
|
||||
|
||||
**Endpoint**
|
||||
|
||||
```
|
||||
GET: http://localhost:3070/api/user/:id
|
||||
```
|
||||
|
||||
Ex: http://localhost:3070/api/user/usr_5ff8afec-5ddc-47d9-a63f-b43df0d8c3b4
|
||||
|
||||
**Response**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"id": "usr_5ff8afec-5ddc-47d9-a63f-b43df0d8c3b4",
|
||||
"firstName": "Sathira",
|
||||
"lastName": "Sri Sathsara",
|
||||
"email": "sathira@niolla.lk",
|
||||
"accountType": "admin",
|
||||
"role": "System Developer",
|
||||
"department": "IT Department",
|
||||
"createdAt": "2026-02-12T19:24:04.000Z",
|
||||
"updatedAt": "2026-02-14T21:33:19.751Z",
|
||||
"profile": {
|
||||
"profile_id": "prof_a1b2c3d4-e5f6-7890-1234-567890abcdef",
|
||||
"user_id": "usr_5ff8afec-5ddc-47d9-a63f-b43df0d8c3b4",
|
||||
"theme": "light",
|
||||
"notificationsEnabled": true,
|
||||
"profilePicture_id": "up_xyz789",
|
||||
"backgroundImage_id": "up_abc123",
|
||||
"dob": "1995-03-15T00:00:00.000Z",
|
||||
"phone_number": "+1 234-567-8900",
|
||||
"createdAt": "2026-02-12T19:24:04.000Z",
|
||||
"updatedAt": "2026-02-12T19:24:04.000Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Update User
|
||||
|
||||
**Endpoint**
|
||||
|
||||
```
|
||||
PATCH: http://localhost:3070/api/user/:id
|
||||
```
|
||||
|
||||
Ex: http://localhost:3070/api/user/usr_5ff8afec-5ddc-47d9-a63f-b43df0d8c3b4
|
||||
|
||||
**Request Body**
|
||||
|
||||
Can send single field or multiple fields. The following fields can be updated:
|
||||
|
||||
```json
|
||||
{
|
||||
"firstName": "Sathira",
|
||||
"lastName": "Sri Sathsara",
|
||||
"email": "sathira@niolla.lk",
|
||||
"role": "System Developer",
|
||||
"roleID": "role_123",
|
||||
"accountType": "admin",
|
||||
"department": "IT Department",
|
||||
"theme": "dark",
|
||||
"notificationsEnabled": false,
|
||||
"profilePicture_id": "up_profile_123",
|
||||
"backgroundImage_id": "up_background_456",
|
||||
"dob": "1995-03-15",
|
||||
"phone_number": "+1 234-567-8900"
|
||||
}
|
||||
```
|
||||
|
||||
**Response**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "User updated successfully",
|
||||
"data": {
|
||||
"id": "usr_5ff8afec-5ddc-47d9-a63f-b43df0d8c3b4",
|
||||
"firstName": "Sathira",
|
||||
"lastName": "Sri Sathsara",
|
||||
"email": "sathira@niolla.lk",
|
||||
"accountType": "admin",
|
||||
"role": "System Developer",
|
||||
"department": "IT Department",
|
||||
"profile": {
|
||||
"theme": "dark",
|
||||
"notificationsEnabled": false,
|
||||
"profilePicture_id": "up_profile_123",
|
||||
"backgroundImage_id": "up_background_456",
|
||||
"dob": "1995-03-15T00:00:00.000Z",
|
||||
"phone_number": "+1 234-567-8900"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Delete User
|
||||
|
||||
**Endpoint**
|
||||
|
||||
```
|
||||
DELETE: http://localhost:3070/api/user/:id
|
||||
```
|
||||
|
||||
Ex: http://localhost:3070/api/user/usr_b00045f7-aafb-482a-8587-d28beb5195ec
|
||||
|
||||
**Respond**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "User deleted successfully"
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,113 @@
|
||||
# User Activity API
|
||||
|
||||
#### Get All Users Activies
|
||||
|
||||
**Endpoint**
|
||||
|
||||
```
|
||||
POST: http://localhost:3070/api/activity
|
||||
```
|
||||
|
||||
**Respond**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "User activities retrieved successfully",
|
||||
"data": [
|
||||
{
|
||||
"id": 3,
|
||||
"user_id": "usr_5ff8afec-5ddc-47d9-a63f-b43df0d8c3b4",
|
||||
"username": "Sathira",
|
||||
"activity_description": "Created vendor with ID: vnd_009270f9-e8ef-42d9-a3e4-5398c9466972",
|
||||
"activity_type": "CREATE_VENDOR",
|
||||
"module": "Vendor Management",
|
||||
"activity_time": "2026-03-30T19:39:45.000Z",
|
||||
"activity_date": "2026-03-30",
|
||||
"createdAt": "2026-03-30T19:39:45.000Z",
|
||||
"updatedAt": "2026-03-30T19:39:45.000Z"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"user_id": "usr_5ff8afec-5ddc-47d9-a63f-b43df0d8c3b4",
|
||||
"username": "Sathira",
|
||||
"activity_description": "Created vendor with ID: vnd_95018101-b539-45a4-b9a6-4551cecda990",
|
||||
"activity_type": "CREATE_VENDOR",
|
||||
"module": "Vendor Management",
|
||||
"activity_time": "2026-03-30T19:17:58.000Z",
|
||||
"activity_date": "2026-03-30",
|
||||
"createdAt": "2026-03-30T19:17:58.000Z",
|
||||
"updatedAt": "2026-03-30T19:17:58.000Z"
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"user_id": "usr_5ff8afec-5ddc-47d9-a63f-b43df0d8c3b4",
|
||||
"username": "Sathira",
|
||||
"activity_description": "Created vendor with ID: vnd_c666b2cd-36fe-46db-aecd-18ec7677ccff",
|
||||
"activity_type": "CREATE_VENDOR",
|
||||
"module": null,
|
||||
"activity_time": "2026-03-30T19:16:41.000Z",
|
||||
"activity_date": "2026-03-30",
|
||||
"createdAt": "2026-03-30T19:16:41.000Z",
|
||||
"updatedAt": "2026-03-30T19:16:41.000Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### Get Activies by User ID
|
||||
|
||||
**Endpoint**
|
||||
|
||||
```
|
||||
POST: http://localhost:3070/api/activity/user/:userID
|
||||
```
|
||||
|
||||
Example: http://localhost:3070/api/activity/user/usr_5ff8afec-5ddc-47d9-a63f-b43df0d8c3b4
|
||||
|
||||
**Respond**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "User activities retrieved successfully",
|
||||
"data": [
|
||||
{
|
||||
"id": 3,
|
||||
"user_id": "usr_5ff8afec-5ddc-47d9-a63f-b43df0d8c3b4",
|
||||
"username": "Sathira",
|
||||
"activity_description": "Created vendor with ID: vnd_009270f9-e8ef-42d9-a3e4-5398c9466972",
|
||||
"activity_type": "CREATE_VENDOR",
|
||||
"module": "Vendor Management",
|
||||
"activity_time": "2026-03-30T19:39:45.000Z",
|
||||
"activity_date": "2026-03-30",
|
||||
"createdAt": "2026-03-30T19:39:45.000Z",
|
||||
"updatedAt": "2026-03-30T19:39:45.000Z"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"user_id": "usr_5ff8afec-5ddc-47d9-a63f-b43df0d8c3b4",
|
||||
"username": "Sathira",
|
||||
"activity_description": "Created vendor with ID: vnd_95018101-b539-45a4-b9a6-4551cecda990",
|
||||
"activity_type": "CREATE_VENDOR",
|
||||
"module": "Vendor Management",
|
||||
"activity_time": "2026-03-30T19:17:58.000Z",
|
||||
"activity_date": "2026-03-30",
|
||||
"createdAt": "2026-03-30T19:17:58.000Z",
|
||||
"updatedAt": "2026-03-30T19:17:58.000Z"
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"user_id": "usr_5ff8afec-5ddc-47d9-a63f-b43df0d8c3b4",
|
||||
"username": "Sathira",
|
||||
"activity_description": "Created vendor with ID: vnd_c666b2cd-36fe-46db-aecd-18ec7677ccff",
|
||||
"activity_type": "CREATE_VENDOR",
|
||||
"module": null,
|
||||
"activity_time": "2026-03-30T19:16:41.000Z",
|
||||
"activity_date": "2026-03-30",
|
||||
"createdAt": "2026-03-30T19:16:41.000Z",
|
||||
"updatedAt": "2026-03-30T19:16:41.000Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,119 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Docs Home</title>
|
||||
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
background: #0d1117;
|
||||
color: #c9d1d9;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.container {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.card-container {
|
||||
display: flex;
|
||||
gap: 30px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #161b22;
|
||||
border: 1px solid #30363d;
|
||||
padding: 30px;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
width: 220px;
|
||||
transition: 0.2s;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
background: #21262d;
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.card p {
|
||||
font-size: 14px;
|
||||
color: #8b949e;
|
||||
}
|
||||
|
||||
.logout {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
cursor: pointer;
|
||||
color: #f85149;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div class="logout" onclick="logout()">Logout</div>
|
||||
|
||||
<div class="container">
|
||||
<h1>Documentation Portal</h1>
|
||||
|
||||
<div class="card-container">
|
||||
<div class="card" onclick="goDocs()">
|
||||
<h2>📘 API Documentation</h2>
|
||||
<p>View markdown API docs</p>
|
||||
</div>
|
||||
|
||||
<div class="card" onclick="goCollection()">
|
||||
<h2>📦 API Collections</h2>
|
||||
<p>Interactive API testing</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 🔐 check if user is logged in
|
||||
async function checkAuth() {
|
||||
const res = await fetch('/api/docs/markdown-files', {
|
||||
credentials: "include"
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
window.location.href = "/Documentation/index.html";
|
||||
}
|
||||
}
|
||||
|
||||
function goDocs() {
|
||||
window.location.href = "/Documentation/markdown.html";
|
||||
}
|
||||
|
||||
function goCollection() {
|
||||
window.location.href = "/Documentation/api-collection.html";
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
await fetch('/api/docs/logout', {
|
||||
method: "POST",
|
||||
credentials: "include"
|
||||
});
|
||||
|
||||
window.location.href = "/Documentation/index.html";
|
||||
}
|
||||
|
||||
checkAuth();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,134 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Docs Login</title>
|
||||
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
background: #0d1117;
|
||||
color: #c9d1d9;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.login-container {
|
||||
background: #161b22;
|
||||
padding: 40px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #30363d;
|
||||
width: 320px;
|
||||
box-shadow: 0 0 20px rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
h2 {
|
||||
text-align: center;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 90%;
|
||||
padding: 10px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #30363d;
|
||||
background: #0d1117;
|
||||
color: #c9d1d9;
|
||||
outline: none;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
input:focus {
|
||||
border-color: #58a6ff;
|
||||
}
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
background: #238636;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: 0.2s;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: #2ea043;
|
||||
}
|
||||
|
||||
.error {
|
||||
margin-top: 10px;
|
||||
color: #f85149;
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: 20px;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
color: #8b949e;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div class="login-container">
|
||||
<h2>Docs Login</h2>
|
||||
|
||||
<div class="input-group">
|
||||
<input id="username" placeholder="Username">
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<input id="password" type="password" placeholder="Password">
|
||||
</div>
|
||||
|
||||
<button onclick="login()">Login</button>
|
||||
|
||||
<div id="error" class="error"></div>
|
||||
|
||||
<div class="footer">
|
||||
Internal Documentation Access
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
async function login() {
|
||||
const errorDiv = document.getElementById("error");
|
||||
errorDiv.textContent = "";
|
||||
|
||||
const res = await fetch('/api/docs/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
username: document.getElementById('username').value,
|
||||
password: document.getElementById('password').value
|
||||
}),
|
||||
credentials: "include"
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
window.location.href = "/Documentation/home.html";
|
||||
} else {
|
||||
errorDiv.textContent = "Invalid username or password";
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,209 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Markdown Documentation</title>
|
||||
|
||||
<!-- Markdown parser -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||
|
||||
<!-- GitHub style -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/github-markdown-css/github-markdown-dark.min.css">
|
||||
|
||||
<!-- Highlight.js -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/highlight.js/styles/github-dark.min.css">
|
||||
<script src="https://cdn.jsdelivr.net/npm/highlight.js/lib/common.min.js"></script>
|
||||
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
background: #0d1117;
|
||||
color: #c9d1d9;
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 280px;
|
||||
background: #161b22;
|
||||
border-right: 1px solid #30363d;
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.viewer-wrapper {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.markdown-body {
|
||||
max-width: 900px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 80%;
|
||||
padding: 8px;
|
||||
margin-bottom: 10px;
|
||||
background: #0d1117;
|
||||
border: 1px solid #30363d;
|
||||
color: white;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
li {
|
||||
padding: 8px;
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
li:hover {
|
||||
background: #21262d;
|
||||
}
|
||||
|
||||
.highlight {
|
||||
background: yellow;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.search-controls {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 5px 10px;
|
||||
cursor: pointer;
|
||||
background: #21262d;
|
||||
border: none;
|
||||
color: white;
|
||||
border-radius: 5px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div class="sidebar">
|
||||
<h3>Docs</h3>
|
||||
|
||||
<!-- File search -->
|
||||
<input type="text" id="fileSearch" placeholder="Search files..." onkeyup="filterFiles()" />
|
||||
|
||||
<!-- Content search -->
|
||||
<input type="text" id="contentSearch" placeholder="Search in document..." onkeyup="searchInContent()" />
|
||||
|
||||
<div class="search-controls">
|
||||
<button onclick="prevMatch()">⬆</button>
|
||||
<button onclick="nextMatch()">⬇</button>
|
||||
</div>
|
||||
|
||||
<ul id="fileList"></ul>
|
||||
</div>
|
||||
|
||||
<div class="viewer-wrapper">
|
||||
<div id="viewer" class="markdown-body">
|
||||
<h2>Select a file</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let allFiles = [];
|
||||
let matches = [];
|
||||
let currentMatchIndex = 0;
|
||||
|
||||
async function loadFiles() {
|
||||
const res = await fetch('/api/docs/markdown-files', {
|
||||
credentials: "include"
|
||||
});
|
||||
const result = await res.json();
|
||||
allFiles = result.data;
|
||||
renderList(allFiles);
|
||||
}
|
||||
|
||||
function renderList(files) {
|
||||
const list = document.getElementById('fileList');
|
||||
list.innerHTML = "";
|
||||
|
||||
files.forEach(file => {
|
||||
const li = document.createElement('li');
|
||||
li.textContent = file;
|
||||
li.onclick = () => loadMarkdown(file);
|
||||
list.appendChild(li);
|
||||
});
|
||||
}
|
||||
|
||||
function filterFiles() {
|
||||
const q = document.getElementById('fileSearch').value.toLowerCase();
|
||||
renderList(allFiles.filter(f => f.toLowerCase().includes(q)));
|
||||
}
|
||||
|
||||
async function loadMarkdown(file) {
|
||||
const res = await fetch(`/api/docs/view/${file}`, {
|
||||
credentials: "include"
|
||||
});
|
||||
const text = await res.text();
|
||||
|
||||
const html = marked.parse(text);
|
||||
const viewer = document.getElementById('viewer');
|
||||
viewer.innerHTML = html;
|
||||
|
||||
document.querySelectorAll('pre code').forEach(block => {
|
||||
hljs.highlightElement(block);
|
||||
});
|
||||
|
||||
matches = [];
|
||||
currentMatchIndex = 0;
|
||||
}
|
||||
|
||||
function searchInContent() {
|
||||
const query = document.getElementById('contentSearch').value;
|
||||
const viewer = document.getElementById('viewer');
|
||||
|
||||
if (!query) {
|
||||
// reset
|
||||
viewer.innerHTML = viewer.textContent;
|
||||
return;
|
||||
}
|
||||
|
||||
const regex = new RegExp(`(${query})`, 'gi');
|
||||
|
||||
viewer.innerHTML = viewer.innerHTML.replace(/<mark class="highlight">(.*?)<\/mark>/g, '$1');
|
||||
|
||||
viewer.innerHTML = viewer.innerHTML.replace(regex, '<mark class="highlight">$1</mark>');
|
||||
|
||||
matches = Array.from(document.querySelectorAll('.highlight'));
|
||||
currentMatchIndex = 0;
|
||||
|
||||
if (matches.length) scrollToMatch(0);
|
||||
}
|
||||
|
||||
function scrollToMatch(index) {
|
||||
matches[index].scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "center"
|
||||
});
|
||||
}
|
||||
|
||||
function nextMatch() {
|
||||
if (!matches.length) return;
|
||||
currentMatchIndex = (currentMatchIndex + 1) % matches.length;
|
||||
scrollToMatch(currentMatchIndex);
|
||||
}
|
||||
|
||||
function prevMatch() {
|
||||
if (!matches.length) return;
|
||||
currentMatchIndex = (currentMatchIndex - 1 + matches.length) % matches.length;
|
||||
scrollToMatch(currentMatchIndex);
|
||||
}
|
||||
|
||||
loadFiles();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,99 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Niolla
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is proprietary and confidential.
|
||||
* Unauthorized copying, modification, distribution, or use
|
||||
* of this file, via any medium, is strictly prohibited.
|
||||
*/
|
||||
|
||||
// app.js
|
||||
|
||||
const express = require("express");
|
||||
const cors = require("cors");
|
||||
const morgan = require("morgan");
|
||||
const routes = require("./app/routes");
|
||||
const cookieParser = require("cookie-parser");
|
||||
const { bullBoardRouter } = require("./app/config/bullBoard.config");
|
||||
const path = require("path");
|
||||
const startAllCrons = require("./cron");
|
||||
|
||||
const db = require("./app/models");
|
||||
|
||||
// Test DB connection and sync models
|
||||
(async () => {
|
||||
try {
|
||||
await db.sequelize.authenticate();
|
||||
console.log("Database connected.");
|
||||
|
||||
await db.sequelize.sync();
|
||||
console.log("Tables synced.");
|
||||
|
||||
// Start all cron jobs
|
||||
startAllCrons();
|
||||
|
||||
} catch (error) {
|
||||
console.error("DB error:", error);
|
||||
}
|
||||
})();
|
||||
|
||||
const app = express();
|
||||
|
||||
app.use(cookieParser());
|
||||
|
||||
// CORS configuration
|
||||
const corsOptions = {
|
||||
origin: process.env.FRONTEND_URL || "https://oceanic-demo.vercel.app",
|
||||
methods: ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"],
|
||||
allowedHeaders: ["Content-Type", "Authorization"],
|
||||
credentials: true,
|
||||
};
|
||||
app.use(cors(corsOptions));
|
||||
app.options(/.*/, cors(corsOptions));
|
||||
|
||||
app.use(express.json());
|
||||
app.use(morgan("dev"));
|
||||
|
||||
app.get("/health", (req, res) => {
|
||||
let dbStatus = "N/A";
|
||||
let emailStatus = "N/A";
|
||||
let redisStatus = "N/A";
|
||||
|
||||
db.sequelize
|
||||
.authenticate()
|
||||
.then(() => {
|
||||
console.log("DB connection successful.");
|
||||
dbStatus = "OK";
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("DB connection error:", err);
|
||||
res.status(500).send("Internal Server Error");
|
||||
});
|
||||
|
||||
emailStatus = "OK";
|
||||
|
||||
redisStatus = "OK";
|
||||
|
||||
res.send({
|
||||
status: "Online ✅",
|
||||
database: dbStatus,
|
||||
emailService: emailStatus,
|
||||
redis: redisStatus,
|
||||
});
|
||||
});
|
||||
|
||||
app.use("/api", routes);
|
||||
app.use(
|
||||
"/Documentation",
|
||||
(req, res, next) => {
|
||||
if (req.path.endsWith(".md")) {
|
||||
return res.status(403).send("Forbidden");
|
||||
}
|
||||
|
||||
next();
|
||||
},
|
||||
express.static(path.join(__dirname, "Documentation")),
|
||||
);
|
||||
app.use("/admin/queues", bullBoardRouter);
|
||||
|
||||
module.exports = app;
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Niolla
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is proprietary and confidential.
|
||||
* Unauthorized copying, modification, distribution, or use
|
||||
* of this file, via any medium, is strictly prohibited.
|
||||
*/
|
||||
|
||||
// app/config/bullBoard.config.js
|
||||
|
||||
const { createBullBoard } = require("@bull-board/api");
|
||||
const { ExpressAdapter } = require("@bull-board/express");
|
||||
const { BullMQAdapter } = require("@bull-board/api/bullMQAdapter");
|
||||
|
||||
const activityQueue = require("../queues/activity.queue");
|
||||
|
||||
const serverAdapter = new ExpressAdapter();
|
||||
serverAdapter.setBasePath("/admin/queues");
|
||||
|
||||
const { addQueue, removeQueue, setQueues, replaceQueues } =
|
||||
createBullBoard({
|
||||
queues: [new BullMQAdapter(activityQueue)],
|
||||
serverAdapter,
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
bullBoardRouter: serverAdapter.getRouter(),
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Niolla
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is proprietary and confidential.
|
||||
* Unauthorized copying, modification, distribution, or use
|
||||
* of this file, via any medium, is strictly prohibited.
|
||||
*/
|
||||
|
||||
// app/config/db.config.js
|
||||
|
||||
require("dotenv").config();
|
||||
|
||||
module.exports = {
|
||||
HOST: process.env.DB_HOST || "localhost",
|
||||
USER: process.env.DB_USER || "root",
|
||||
PASSWORD: process.env.DB_PASSWORD || "",
|
||||
DB: process.env.DB_NAME || "oceanic-db",
|
||||
PORT: process.env.DB_PORT || 3306,
|
||||
DIALECT: "mysql",
|
||||
|
||||
pool: {
|
||||
max: 5,
|
||||
min: 0,
|
||||
acquire: 30000,
|
||||
idle: 10000
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Niolla
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is proprietary and confidential.
|
||||
* Unauthorized copying, modification, distribution, or use
|
||||
* of this file, via any medium, is strictly prohibited.
|
||||
*/
|
||||
|
||||
// app/config/mail.config.js
|
||||
|
||||
|
||||
require("dotenv").config();
|
||||
|
||||
module.exports = {
|
||||
host: process.env.MAIL_HOST,
|
||||
port: process.env.MAIL_PORT,
|
||||
secure: process.env.MAIL_SECURE === "true", // true for 465, false for 587
|
||||
auth: {
|
||||
user: process.env.MAIL_USER,
|
||||
pass: process.env.MAIL_PASS,
|
||||
},
|
||||
from: `Oceanic Maritime Solutions <${process.env.MAIL_FROM}>`,
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Niolla
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is proprietary and confidential.
|
||||
* Unauthorized copying, modification, distribution, or use
|
||||
* of this file, via any medium, is strictly prohibited.
|
||||
*/
|
||||
|
||||
// app/config/redis.config.js
|
||||
|
||||
const { Redis } = require("ioredis");
|
||||
|
||||
const createRedisConnection = () => {
|
||||
const redis = new Redis({
|
||||
host: process.env.REDIS_HOST || "redis",
|
||||
port: process.env.REDIS_PORT || 6379,
|
||||
password: process.env.REDIS_PASSWORD,
|
||||
maxRetriesPerRequest: null,
|
||||
enableReadyCheck: false,
|
||||
});
|
||||
|
||||
// Connection events
|
||||
redis.on("connect", () => {
|
||||
console.log("Redis connecting...");
|
||||
});
|
||||
|
||||
redis.on("ready", () => {
|
||||
console.log("Redis connected and ready!");
|
||||
});
|
||||
|
||||
redis.on("error", (err) => {
|
||||
console.error("Redis error:", err.message);
|
||||
});
|
||||
|
||||
redis.on("close", () => {
|
||||
console.warn("Redis connection closed");
|
||||
});
|
||||
|
||||
redis.on("reconnecting", () => {
|
||||
console.log("Redis reconnecting...");
|
||||
});
|
||||
|
||||
return redis;
|
||||
};
|
||||
|
||||
module.exports = createRedisConnection;
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Niolla
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is proprietary and confidential.
|
||||
* Unauthorized copying, modification, distribution, or use
|
||||
* of this file, via any medium, is strictly prohibited.
|
||||
*/
|
||||
|
||||
|
||||
// app/config/redisClient.js
|
||||
|
||||
const createRedisConnection = require("./redis.config");
|
||||
|
||||
const redis = createRedisConnection();
|
||||
|
||||
module.exports = redis;
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Niolla
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is proprietary and confidential.
|
||||
* Unauthorized copying, modification, distribution, or use
|
||||
* of this file, via any medium, is strictly prohibited.
|
||||
*/
|
||||
|
||||
// app/config/s3.config.js
|
||||
|
||||
const { S3Client } = require("@aws-sdk/client-s3");
|
||||
|
||||
const s3 = new S3Client({
|
||||
region: process.env.AWS_REGION,
|
||||
credentials: {
|
||||
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
|
||||
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
|
||||
},
|
||||
});
|
||||
|
||||
module.exports = s3;
|
||||
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Niolla
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is proprietary and confidential.
|
||||
* Unauthorized copying, modification, distribution, or use
|
||||
* of this file, via any medium, is strictly prohibited.
|
||||
*/
|
||||
|
||||
|
||||
// app/constants/permissions.js
|
||||
|
||||
module.exports = {
|
||||
FINANCE_BASIS: "finance.basis",
|
||||
PRECOST: "precost.precost",
|
||||
};
|
||||
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Niolla
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is proprietary and confidential.
|
||||
* Unauthorized copying, modification, distribution, or use
|
||||
* of this file, via any medium, is strictly prohibited.
|
||||
*/
|
||||
|
||||
// app/controllers/activity.controller.js
|
||||
|
||||
const db = require("../models");
|
||||
const Activity = db.UserActivity;
|
||||
|
||||
// Get all user activities
|
||||
exports.getUserActivities = async (req, res) => {
|
||||
try {
|
||||
const avt = await Activity.findAll({
|
||||
order: [["createdAt", "DESC"]],
|
||||
});
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
message: "User activities retrieved successfully",
|
||||
data: avt,
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "An error occurred while fetching user activities.",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Get activity by User ID
|
||||
exports.getActivitiesByUserId = async (req, res) => {
|
||||
const userId = req.params.userId;
|
||||
|
||||
try {
|
||||
const avt = await Activity.findAll({
|
||||
where: { user_id: userId },
|
||||
order: [["createdAt", "DESC"]],
|
||||
});
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
message: "User activities retrieved successfully",
|
||||
data: avt,
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "An error occurred while fetching user activities.",
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,132 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Niolla
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is proprietary and confidential.
|
||||
* Unauthorized copying, modification, distribution, or use
|
||||
* of this file, via any medium, is strictly prohibited.
|
||||
*/
|
||||
|
||||
// app/controllers/auth.controller.js
|
||||
|
||||
const { checkPassword } = require("../utils/hashPassword.util");
|
||||
const { sendMail } = require("../utils/mail.util");
|
||||
const { generateOTP, validateOTP } = require("../utils/otp.util");
|
||||
const {getCachedUser,clearUserCache} = require("../utils/cache.util");
|
||||
const { generateToken } = require("../utils/jwt.util");
|
||||
const { log } = require("../utils/consoleLog.utill");
|
||||
|
||||
const appName = process.env.APP_NAME || "Niolla";
|
||||
|
||||
// Login Step 1: Request OTP
|
||||
exports.loginReq = async (req, res) => {
|
||||
try {
|
||||
const { email, password } = req.body;
|
||||
|
||||
const user = await getCachedUser(email);
|
||||
if (!user) {
|
||||
return res
|
||||
.status(404)
|
||||
.send({ success: false, message: "User Not Found" });
|
||||
}
|
||||
|
||||
const passwordIsValid = await checkPassword(password, user.password);
|
||||
if (!passwordIsValid) {
|
||||
return res
|
||||
.status(401)
|
||||
.send({ success: false, message: "Invalid Password" });
|
||||
}
|
||||
|
||||
const otp = generateOTP(email);
|
||||
|
||||
await sendMail({
|
||||
to: email,
|
||||
subject: `OTP for Your ${appName} Account`,
|
||||
templateName: "otp",
|
||||
templateVars: {
|
||||
firstName: user.firstName,
|
||||
otp: otp,
|
||||
},
|
||||
text: `Hello ${user.firstName}, your otp is ${otp}`,
|
||||
});
|
||||
|
||||
log(`OTP for ${email}: ${otp}`);
|
||||
log(`OTP sent to ${email} successfully.`);
|
||||
|
||||
res.status(201).send({ success: true, message: "OTP Sent Successfully" });
|
||||
} catch (error) {
|
||||
log("Error occurred while sending OTP:", error);
|
||||
res.status(500).send({ success: false, message: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
// Login Step 2: Verify OTP and issue JWT
|
||||
exports.login = async (req, res) => {
|
||||
try {
|
||||
const { email, otp } = req.body;
|
||||
|
||||
const user = await getCachedUser(email);
|
||||
|
||||
if (!user) {
|
||||
return res
|
||||
.status(404)
|
||||
.send({ success: false, message: "User Not Found" });
|
||||
}
|
||||
|
||||
const isValidOTP = validateOTP(email, otp);
|
||||
|
||||
if (!isValidOTP) {
|
||||
return res
|
||||
.status(401)
|
||||
.send({ success: false, message: "Invalid or Expired OTP" });
|
||||
}
|
||||
|
||||
// Generate JWT token
|
||||
const token = generateToken({
|
||||
id: user.id,
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
accountType: user.accountType,
|
||||
});
|
||||
|
||||
// 3. Set JWT as HttpOnly cookie
|
||||
res.cookie("access_token", token, {
|
||||
httpOnly: true, // JS cannot access
|
||||
secure: process.env.NODE_ENV === "production", // HTTPS only in prod
|
||||
sameSite: process.env.NODE_ENV === "production" ? "None" : "Lax",
|
||||
maxAge: 24 * 60 * 60 * 1000, // 1 day
|
||||
});
|
||||
|
||||
log(`JWT issued for ${email}`);
|
||||
clearUserCache(email);
|
||||
|
||||
res.status(200).send({
|
||||
success: true,
|
||||
message: "Login Successful",
|
||||
data: {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
role: user.role,
|
||||
accountType: user.accountType,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
log("Error occurred during login:", error);
|
||||
res.status(500).send({ success: false, message: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
// Logout: Clear the JWT cookie
|
||||
exports.logout = (req, res) => {
|
||||
res.clearCookie("access_token", {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
sameSite: process.env.NODE_ENV === "production" ? "None" : "Lax",
|
||||
});
|
||||
|
||||
res.json({ success: true, message: "Logged out successfully" });
|
||||
};
|
||||
@@ -0,0 +1,417 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Niolla
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is proprietary and confidential.
|
||||
* Unauthorized copying, modification, distribution, or use
|
||||
* of this file, via any medium, is strictly prohibited.
|
||||
*/
|
||||
|
||||
// app/controllers/generateDocument.controller.js
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const documentQueue = require("../queues/document.queue");
|
||||
const { createDocumentData } = require("../utils/document.utill");
|
||||
const {
|
||||
generateId,
|
||||
generateDocumentReferenceNo,
|
||||
} = require("../utils/idGen.util");
|
||||
|
||||
const { GetObjectCommand, DeleteObjectCommand } = require("@aws-sdk/client-s3");
|
||||
|
||||
const {log} = require("../utils/consoleLog.utill");
|
||||
|
||||
const s3 = require("../config/s3.config");
|
||||
const db = require("../models");
|
||||
const Document = db.Document;
|
||||
const DocumentType = db.DocumentType;
|
||||
|
||||
const normalizeDocumentData = (value) => {
|
||||
if (!value) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (typeof value === "string") {
|
||||
try {
|
||||
return JSON.parse(value);
|
||||
} catch (error) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get available document types
|
||||
*/
|
||||
exports.getAvailableDocumentTypes = async (req, res) => {
|
||||
try {
|
||||
|
||||
const doc_types = await DocumentType.findAll({
|
||||
attributes: ["doc_type_name"],
|
||||
});
|
||||
|
||||
const types = doc_types.map((t) => t.doc_type_name);
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
availableDocumentTypes: types,
|
||||
note: "Use these document type names in your requests (case-insensitive)",
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
log("Error fetching document types:", error.message);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Get Saved documents
|
||||
exports.getSavedDocuments = async (req, res) => {
|
||||
try {
|
||||
const { documentType } = req.body;
|
||||
|
||||
if (!documentType) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "documentType query parameter is required",
|
||||
});
|
||||
}
|
||||
|
||||
// Fetch saved documents based on documentType
|
||||
const savedDocuments = await Document.findAll({
|
||||
where: { doc_type: documentType.toUpperCase() },
|
||||
order: [["createdAt", "DESC"]],
|
||||
exclude: ["data"],
|
||||
});
|
||||
|
||||
// extract only necessary fields to return
|
||||
const formattedDocuments = savedDocuments.map((doc) => ({
|
||||
doc_id: doc.doc_id,
|
||||
reference_no: doc.reference_no,
|
||||
doc_type: doc.doc_type,
|
||||
status: doc.status,
|
||||
createdAt: doc.createdAt,
|
||||
updatedAt: doc.updatedAt,
|
||||
}));
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
savedDocuments: formattedDocuments,
|
||||
});
|
||||
} catch (error) {
|
||||
log("Error fetching saved documents:", error.message);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Get specific document data by doc_id
|
||||
exports.getDocumentData = async (req, res) => {
|
||||
try {
|
||||
const { docId } = req.params;
|
||||
|
||||
if (!docId) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "docId parameter is required",
|
||||
});
|
||||
}
|
||||
|
||||
const document = await Document.findOne({
|
||||
where: { doc_id: docId },
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "Document not found",
|
||||
});
|
||||
}
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
document: {
|
||||
doc_id: document.doc_id,
|
||||
reference_no: document.reference_no,
|
||||
doc_type: document.doc_type,
|
||||
data: document.data,
|
||||
status: document.status,
|
||||
createdAt: document.createdAt,
|
||||
updatedAt: document.updatedAt,
|
||||
},
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
log("Error fetching document data:", error.message);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
exports.generateReferenceNo = async (req, res) => {
|
||||
try {
|
||||
const { documentType } = req.params;
|
||||
|
||||
if (!documentType) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "documentType is required",
|
||||
});
|
||||
}
|
||||
|
||||
// Generate reference number
|
||||
const reference_no = await generateDocumentReferenceNo(documentType);
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
reference_no,
|
||||
});
|
||||
} catch (error) {
|
||||
log("Error generating reference number:", error.message);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
exports.generateDraftDocument = async (req, res) => {
|
||||
try {
|
||||
const { document } = req.body;
|
||||
const documentData = normalizeDocumentData(req.body.documentData || req.body.data || req.body);
|
||||
|
||||
// Validation
|
||||
if (!document || !documentData) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "document and documentData are required",
|
||||
});
|
||||
}
|
||||
|
||||
let documentDetails;
|
||||
|
||||
if (document !== "PRECOST") {
|
||||
// Save document details before generating
|
||||
documentDetails = await createDocumentData(
|
||||
document,
|
||||
documentData,
|
||||
"DRAFT",
|
||||
);
|
||||
} else {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message:
|
||||
"Invalid document type, This document type is not allowed to be generated as draft",
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(201).json({
|
||||
success: true,
|
||||
message: "Draft document created successfully",
|
||||
documentDetails,
|
||||
});
|
||||
} catch (error) {
|
||||
log("Error generating draft document:", error.message);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate document asynchronously
|
||||
* Returns jobId immediately
|
||||
*/
|
||||
exports.generateDocument = async (req, res) => {
|
||||
try {
|
||||
const { document, documentType } = req.body;
|
||||
const documentData = normalizeDocumentData(req.body.documentData || req.body.data || req.body);
|
||||
|
||||
// Validation
|
||||
if (!document || !documentType || !documentData) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "document, documentType, and documentData are required",
|
||||
});
|
||||
}
|
||||
|
||||
console.log(
|
||||
`📨 generateDocument request: document="${document}", documentType="${documentType}"`,
|
||||
);
|
||||
|
||||
if (document !== "PRECOST") {
|
||||
// Save document details before generating
|
||||
// If doc_id exists, finalize (update existing); otherwise create as DRAFT
|
||||
const status = documentData.doc_id ? "FINAL" : "DRAFT";
|
||||
await createDocumentData(document, documentData, status);
|
||||
}
|
||||
|
||||
// Add job to queue
|
||||
const job = await documentQueue.add("generate-document", {
|
||||
document,
|
||||
documentType,
|
||||
data: documentData,
|
||||
});
|
||||
|
||||
log(
|
||||
`📋 Document generation job queued: ${job.id} (document="${document}", type="${documentType}")`,
|
||||
);
|
||||
|
||||
return res.status(202).json({
|
||||
success: true,
|
||||
message: "Document generation started",
|
||||
jobId: job.id,
|
||||
});
|
||||
} catch (error) {
|
||||
log("Error queuing document generation:", error.message);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get job status and result
|
||||
*/
|
||||
exports.getJobStatus = async (req, res) => {
|
||||
try {
|
||||
const { jobId } = req.params;
|
||||
|
||||
if (!jobId) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "jobId is required",
|
||||
});
|
||||
}
|
||||
|
||||
// Get job from queue
|
||||
const job = await documentQueue.getJob(jobId);
|
||||
|
||||
if (!job) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "Job not found",
|
||||
});
|
||||
}
|
||||
|
||||
// Get job state
|
||||
const state = await job.getState();
|
||||
const result = job.returnvalue;
|
||||
const failedReason = job.failedReason;
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
jobId: job.id,
|
||||
state, // "waiting" | "active" | "completed" | "failed" | "delayed"
|
||||
result: state === "completed" ? result : null,
|
||||
error: state === "failed" ? failedReason : null,
|
||||
attempts: job.attemptsMade,
|
||||
stacktrace: job.stacktrace,
|
||||
});
|
||||
} catch (error) {
|
||||
log("Error fetching job status:", error.message);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Download generated document
|
||||
*/
|
||||
exports.downloadDocument = async (req, res) => {
|
||||
try {
|
||||
const { uuid } = req.params;
|
||||
|
||||
const key = `uploads/${uuid}.pdf`;
|
||||
|
||||
const command = new GetObjectCommand({
|
||||
Bucket: process.env.AWS_S3_BUCKET_NAME,
|
||||
Key: key,
|
||||
});
|
||||
|
||||
const response = await s3.send(command);
|
||||
|
||||
res.setHeader(
|
||||
"Content-Type",
|
||||
response.ContentType || "application/octet-stream",
|
||||
);
|
||||
|
||||
res.setHeader("Content-Disposition", `attachment; filename="${uuid}.pdf"`);
|
||||
|
||||
response.Body.pipe(res);
|
||||
|
||||
res.on("finish", async () => {
|
||||
try {
|
||||
await s3.send(
|
||||
new DeleteObjectCommand({
|
||||
Bucket: process.env.AWS_S3_BUCKET_NAME,
|
||||
Key: key,
|
||||
}),
|
||||
);
|
||||
|
||||
log(`Deleted from S3: ${key}`);
|
||||
} catch (err) {
|
||||
log(`Failed to delete ${key}:`, err.message);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
log("Download error:", error.message);
|
||||
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "Document not found",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Cancel/delete a job
|
||||
*/
|
||||
exports.cancelJob = async (req, res) => {
|
||||
try {
|
||||
const { jobId } = req.params;
|
||||
|
||||
if (!jobId) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "jobId is required",
|
||||
});
|
||||
}
|
||||
|
||||
const job = await documentQueue.getJob(jobId);
|
||||
|
||||
if (!job) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "Job not found",
|
||||
});
|
||||
}
|
||||
|
||||
await job.remove();
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
message: "Job cancelled successfully",
|
||||
jobId,
|
||||
});
|
||||
} catch (error) {
|
||||
log("Error cancelling job:", error.message);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,138 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Niolla
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is proprietary and confidential.
|
||||
* Unauthorized copying, modification, distribution, or use
|
||||
* of this file, via any medium, is strictly prohibited.
|
||||
*/
|
||||
|
||||
// app/controllers/notification.controller.js
|
||||
|
||||
const db = require("../models");
|
||||
const Notification = db.notification;
|
||||
const UserNotification = db.userNotification;
|
||||
|
||||
const log = require("../utils/consoleLog.utill").log;
|
||||
|
||||
// Controller for managing notifications
|
||||
exports.createNotification = async (req, res) => {
|
||||
try {
|
||||
const { notificationHeadline, notificationDescription, notificationType } =
|
||||
req.body;
|
||||
|
||||
const id = Date.now().toString(); // Generate a unique ID based on the current timestamp
|
||||
const notification_id = `notif_${id}`; // Prefix the ID with "notif_"
|
||||
|
||||
// Create a new notification
|
||||
const newNotification = await Notification.create({
|
||||
notification_id,
|
||||
notificationHeadline,
|
||||
notificationDescription,
|
||||
notificationType,
|
||||
dateCreated: new Date(),
|
||||
});
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: "Notification created successfully",
|
||||
notification: newNotification,
|
||||
});
|
||||
} catch (error) {
|
||||
log("Error creating notification:", error.message);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "Internal server error",
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Mark a notification as read for a user
|
||||
exports.markAsRead = async (req, res) => {
|
||||
try {
|
||||
const { userId, notificationId } = req.params;
|
||||
|
||||
// Find the user notification entry
|
||||
const userNotification = await UserNotification.findOne({
|
||||
where: { user_id: userId, notification_id: notificationId },
|
||||
});
|
||||
|
||||
if (!userNotification) {
|
||||
return res.status(404).json({ success: false, error: "User notification not found" });
|
||||
}
|
||||
|
||||
// Mark the notification as read
|
||||
userNotification.isRead = true;
|
||||
await userNotification.save();
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
message: "Notification marked as read",
|
||||
});
|
||||
}
|
||||
|
||||
catch (error) {
|
||||
log("Error marking notification as read:", error.message);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "Internal server error",
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Get announcements for all users
|
||||
exports.getAnnouncements = async (req, res) => {
|
||||
try {
|
||||
|
||||
// Fetch all announcements
|
||||
const announcements = await Notification.findAll({
|
||||
where: { notificationType: "ANNOUNCEMENT", isActive: true },
|
||||
});
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
announcements,
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
log("Error fetching announcements:", error.message);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "Internal server error",
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Get all notifications for a user
|
||||
exports.getUserNotifications = async (req, res) => {
|
||||
try {
|
||||
const { userId } = req.params;
|
||||
|
||||
// Fetch notifications for the user
|
||||
const notifications = await UserNotification.findAll({
|
||||
where: { user_id: userId, isRead: false },
|
||||
include: [
|
||||
{
|
||||
model: Notification,
|
||||
as: "notification",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
notifications,
|
||||
});
|
||||
}
|
||||
|
||||
catch (error) {
|
||||
log("Error fetching user notifications:", error.message);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "Internal server error",
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,868 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Niolla
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is proprietary and confidential.
|
||||
* Unauthorized copying, modification, distribution, or use
|
||||
* of this file, via any medium, is strictly prohibited.
|
||||
*/
|
||||
|
||||
// app/controllers/permission.controller.js
|
||||
|
||||
|
||||
// PERMISSIONS:
|
||||
// 1. createPermission --> Done
|
||||
// 2. getAllPermissions --> Done
|
||||
// 3. getPermissionById --> Done
|
||||
// 4. updatePermission --> Done
|
||||
// 5. deletePermission --> Done
|
||||
|
||||
// ROLES:
|
||||
// 1. createRole --> Done
|
||||
// 2. createRolePermission --> Done
|
||||
// 3. getAllRoles --> Done
|
||||
// 4. getRoleById --> Done
|
||||
// 5. updateRole --> Done
|
||||
// 6. deleteRole --> Done
|
||||
|
||||
// USER PERMISSIONS:
|
||||
// 1. getUserPermissions --> Done
|
||||
|
||||
const db = require("../models");
|
||||
const User = db.User;
|
||||
const Role = db.roles;
|
||||
const Permission = db.permission;
|
||||
const RolePermission = db.rolePermission;
|
||||
const UserPermission = db.userPermission;
|
||||
|
||||
// Create a new permission
|
||||
exports.createPermission = async (req, res) => {
|
||||
try {
|
||||
const { permissionName, permissionDescription, page, module, action } = req.body;
|
||||
if (!permissionName || !page || !module || !action) {
|
||||
return res.status(400).json({ message: "Permission name, page, module, and action are required" });
|
||||
}
|
||||
|
||||
const permissionId = `${module}.${page}.${action}`;
|
||||
|
||||
// Check if permission already exists
|
||||
const existingPermission = await Permission.findOne({ where: { permission_id: permissionId } });
|
||||
if (existingPermission) {
|
||||
return res.status(400).json({ message: "Permission already exists" });
|
||||
}
|
||||
|
||||
// Create the new permission
|
||||
const newPermission = await Permission.create({
|
||||
permission_id: permissionId,
|
||||
permissionName,
|
||||
permissionDescription,
|
||||
page,
|
||||
module,
|
||||
action
|
||||
});
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: newPermission
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error creating permission:", error);
|
||||
res.status(500).json({ message: "Internal server error" });
|
||||
}
|
||||
};
|
||||
|
||||
// Bulk create permissions
|
||||
exports.bulkCreatePermissions = async (req, res) => {
|
||||
try {
|
||||
const { permissions } = req.body;
|
||||
|
||||
if (!permissions || !Array.isArray(permissions) || permissions.length === 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "Permissions array is required and must not be empty"
|
||||
});
|
||||
}
|
||||
|
||||
const results = {
|
||||
created: [],
|
||||
failed: []
|
||||
};
|
||||
|
||||
for (const item of permissions) {
|
||||
try {
|
||||
const { permissionName, permissionDescription, page, module, action } = item;
|
||||
|
||||
if (!permissionName || !page || !module || !action) {
|
||||
results.failed.push({
|
||||
...item,
|
||||
error: "Permission name, page, module, and action are required"
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const permissionId = `${module}.${page}.${action}`;
|
||||
|
||||
// Check if permission already exists
|
||||
const existingPermission = await Permission.findOne({ where: { permission_id: permissionId } });
|
||||
if (existingPermission) {
|
||||
results.failed.push({
|
||||
...item,
|
||||
error: "Permission already exists"
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const newPermission = await Permission.create({
|
||||
permission_id: permissionId,
|
||||
permissionName,
|
||||
permissionDescription,
|
||||
page,
|
||||
module,
|
||||
action
|
||||
});
|
||||
|
||||
results.created.push(newPermission);
|
||||
|
||||
} catch (itemError) {
|
||||
results.failed.push({
|
||||
...item,
|
||||
error: itemError.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
res.status(201).json({
|
||||
success: results.failed.length === 0,
|
||||
data: results
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error bulk creating permissions:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "An error occurred while bulk creating permissions."
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Get all permissions
|
||||
exports.getAllPermissions = async (req, res) => {
|
||||
try {
|
||||
const permissions = await Permission.findAll();
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: permissions
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error fetching permissions:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "An error occurred while fetching permissions."
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Get permission by ID
|
||||
exports.getPermissionById = async (req, res) => {
|
||||
try {
|
||||
const { permissionId } = req.params;
|
||||
|
||||
const permission = await Permission.findByPk(permissionId);
|
||||
if (!permission) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "Permission not found"
|
||||
});
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: permission
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error fetching permission:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "An error occurred while fetching permission."
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Update permission
|
||||
exports.updatePermission = async (req, res) => {
|
||||
try {
|
||||
const { permissionId } = req.params;
|
||||
const { permissionName, permissionDescription, page, module, action } = req.body;
|
||||
|
||||
const permission = await Permission.findByPk(permissionId);
|
||||
if (!permission) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "Permission not found"
|
||||
});
|
||||
}
|
||||
|
||||
// Update permission fields
|
||||
await permission.update({
|
||||
permissionName: permissionName || permission.permissionName,
|
||||
permissionDescription: permissionDescription || permission.permissionDescription,
|
||||
page: page || permission.page,
|
||||
module: module || permission.module,
|
||||
action: action || permission.action
|
||||
});
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: permission,
|
||||
message: "Permission updated successfully"
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error updating permission:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "An error occurred while updating permission."
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Delete permission
|
||||
exports.deletePermission = async (req, res) => {
|
||||
try {
|
||||
const { permissionId } = req.params;
|
||||
|
||||
const permission = await Permission.findByPk(permissionId);
|
||||
if (!permission) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "Permission not found"
|
||||
});
|
||||
}
|
||||
|
||||
// Delete associated user and role permissions first
|
||||
await UserPermission.destroy({ where: { permission_id: permissionId } });
|
||||
await RolePermission.destroy({ where: { permission_id: permissionId } });
|
||||
|
||||
// Delete the permission
|
||||
await permission.destroy();
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
message: "Permission deleted successfully"
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error deleting permission:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "An error occurred while deleting permission."
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Create a new role
|
||||
exports.createRole = async (req, res) => {
|
||||
try {
|
||||
const { roleName, roleDescription } = req.body;
|
||||
|
||||
if (!roleName) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "Role name is required"
|
||||
});
|
||||
}
|
||||
|
||||
// Generate role ID
|
||||
const roleId = `role_${Date.now()}`;
|
||||
|
||||
const newRole = await Role.create({
|
||||
role_id: roleId,
|
||||
roleName,
|
||||
roleDescription
|
||||
});
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: newRole
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error creating role:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "An error occurred while creating role."
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Create role-permission assignment
|
||||
exports.createRolePermission = async (req, res) => {
|
||||
try {
|
||||
const { role_id, permission_id } = req.body;
|
||||
|
||||
if (!role_id || !permission_id) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "Role ID and Permission ID are required"
|
||||
});
|
||||
}
|
||||
|
||||
// Verify role and permission exist
|
||||
const role = await Role.findByPk(role_id);
|
||||
if (!role) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "Role not found"
|
||||
});
|
||||
}
|
||||
|
||||
const permission = await Permission.findByPk(permission_id);
|
||||
if (!permission) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "Permission not found"
|
||||
});
|
||||
}
|
||||
|
||||
// Check if assignment already exists
|
||||
const existingAssignment = await RolePermission.findOne({
|
||||
where: { role_id, permission_id }
|
||||
});
|
||||
if (existingAssignment) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "Role permission already exists"
|
||||
});
|
||||
}
|
||||
|
||||
const rpId = `rp_${Date.now()}`;
|
||||
const newRolePermission = await RolePermission.create({
|
||||
rp_id: rpId,
|
||||
role_id,
|
||||
permission_id
|
||||
});
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: newRolePermission
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error creating role permission:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "An error occurred while creating role permission."
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Bulk create role permissions
|
||||
exports.bulkCreateRolePermissions = async (req, res) => {
|
||||
try {
|
||||
const { role_id, permission_ids } = req.body;
|
||||
|
||||
if (!role_id || !permission_ids || !Array.isArray(permission_ids) || permission_ids.length === 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "Role ID and permission_ids array are required and must not be empty"
|
||||
});
|
||||
}
|
||||
|
||||
// Verify role exists
|
||||
const role = await Role.findByPk(role_id);
|
||||
if (!role) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "Role not found"
|
||||
});
|
||||
}
|
||||
|
||||
const results = {
|
||||
created: [],
|
||||
failed: []
|
||||
};
|
||||
|
||||
for (const permission_id of permission_ids) {
|
||||
try {
|
||||
if (!permission_id) {
|
||||
results.failed.push({
|
||||
role_id,
|
||||
permission_id,
|
||||
error: "Permission ID is required"
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// Verify permission exists
|
||||
const permission = await Permission.findByPk(permission_id);
|
||||
if (!permission) {
|
||||
results.failed.push({
|
||||
role_id,
|
||||
permission_id,
|
||||
error: "Permission not found"
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if assignment already exists
|
||||
const existingAssignment = await RolePermission.findOne({
|
||||
where: { role_id, permission_id }
|
||||
});
|
||||
if (existingAssignment) {
|
||||
results.failed.push({
|
||||
role_id,
|
||||
permission_id,
|
||||
error: "Role permission already exists"
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const rpId = `rp_${Date.now()}`;
|
||||
const newRolePermission = await RolePermission.create({
|
||||
rp_id: rpId,
|
||||
role_id,
|
||||
permission_id
|
||||
});
|
||||
|
||||
results.created.push(newRolePermission);
|
||||
|
||||
} catch (itemError) {
|
||||
results.failed.push({
|
||||
role_id,
|
||||
permission_id,
|
||||
error: itemError.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
res.status(201).json({
|
||||
success: results.failed.length === 0,
|
||||
data: results
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error bulk creating role permissions:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "An error occurred while bulk creating role permissions."
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Get all roles
|
||||
exports.getAllRoles = async (req, res) => {
|
||||
try {
|
||||
const roles = await Role.findAll({
|
||||
include: [
|
||||
{
|
||||
model: RolePermission,
|
||||
as: "rolePermissions",
|
||||
include: [
|
||||
{
|
||||
model: Permission,
|
||||
as: "permission",
|
||||
attributes: ["permission_id", "permissionName", "page", "module", "action"]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: roles
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error fetching roles:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "An error occurred while fetching roles."
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Get role by ID
|
||||
exports.getRoleById = async (req, res) => {
|
||||
try {
|
||||
const { roleId } = req.params;
|
||||
|
||||
const role = await Role.findByPk(roleId, {
|
||||
include: [
|
||||
{
|
||||
model: RolePermission,
|
||||
as: "rolePermissions",
|
||||
include: [
|
||||
{
|
||||
model: Permission,
|
||||
as: "permission",
|
||||
attributes: ["permission_id", "permissionName", "page", "module", "action"]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
if (!role) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "Role not found"
|
||||
});
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: role
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error fetching role:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "An error occurred while fetching role."
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Update role
|
||||
exports.updateRole = async (req, res) => {
|
||||
try {
|
||||
const { roleId } = req.params;
|
||||
const { roleName, roleDescription } = req.body;
|
||||
|
||||
const role = await Role.findByPk(roleId);
|
||||
if (!role) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "Role not found"
|
||||
});
|
||||
}
|
||||
|
||||
// Update role fields
|
||||
await role.update({
|
||||
roleName: roleName || role.roleName,
|
||||
roleDescription: roleDescription || role.roleDescription
|
||||
});
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: role,
|
||||
message: "Role updated successfully"
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error updating role:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "An error occurred while updating role."
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Delete role
|
||||
exports.deleteRole = async (req, res) => {
|
||||
try {
|
||||
const { roleId } = req.params;
|
||||
|
||||
const role = await Role.findByPk(roleId);
|
||||
if (!role) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "Role not found"
|
||||
});
|
||||
}
|
||||
|
||||
// Delete associated role permissions first
|
||||
await RolePermission.destroy({ where: { role_id: roleId } });
|
||||
|
||||
// Delete the role
|
||||
await role.destroy();
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
message: "Role deleted successfully"
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error deleting role:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "An error occurred while deleting role."
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Get user permissions
|
||||
exports.getUserPermissions = async (req, res) => {
|
||||
try {
|
||||
const userId = req.params.userId;
|
||||
|
||||
// Fetch user permissions
|
||||
const userPermissions = await UserPermission.findAll({
|
||||
where: { user_id: userId },
|
||||
include: [
|
||||
{
|
||||
model: Permission,
|
||||
as: "permission",
|
||||
attributes: ["permission_id", "permissionName", "page", "module", "action"]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: userPermissions
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error fetching user permissions:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "An error occurred while fetching user permissions."
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Create user permission
|
||||
exports.createUserPermission = async (req, res) => {
|
||||
try {
|
||||
const { user_id, permission_id } = req.body;
|
||||
|
||||
if (!user_id || !permission_id) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "User ID and Permission ID are required"
|
||||
});
|
||||
}
|
||||
|
||||
// Verify user and permission exist
|
||||
const user = await User.findByPk(user_id);
|
||||
if (!user) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "User not found"
|
||||
});
|
||||
}
|
||||
|
||||
const permission = await Permission.findByPk(permission_id);
|
||||
if (!permission) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "Permission not found"
|
||||
});
|
||||
}
|
||||
|
||||
// Check if assignment already exists
|
||||
const existingAssignment = await UserPermission.findOne({
|
||||
where: { user_id, permission_id }
|
||||
});
|
||||
if (existingAssignment) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "User permission already exists"
|
||||
});
|
||||
}
|
||||
|
||||
const newUserPermission = await UserPermission.create({
|
||||
user_id,
|
||||
permission_id
|
||||
});
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: newUserPermission
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error creating user permission:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "An error occurred while creating user permission."
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Bulk create user permissions
|
||||
exports.bulkCreateUserPermissions = async (req, res) => {
|
||||
try {
|
||||
const { user_id, permission_ids } = req.body;
|
||||
|
||||
if (!user_id || !permission_ids || !Array.isArray(permission_ids) || permission_ids.length === 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "User ID and permission_ids array are required and must not be empty"
|
||||
});
|
||||
}
|
||||
|
||||
// Verify user exists
|
||||
const user = await User.findByPk(user_id);
|
||||
if (!user) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "User not found"
|
||||
});
|
||||
}
|
||||
|
||||
const results = {
|
||||
created: [],
|
||||
failed: []
|
||||
};
|
||||
|
||||
for (const permission_id of permission_ids) {
|
||||
try {
|
||||
if (!permission_id) {
|
||||
results.failed.push({
|
||||
user_id,
|
||||
permission_id,
|
||||
error: "Permission ID is required"
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// Verify permission exists
|
||||
const permission = await Permission.findByPk(permission_id);
|
||||
if (!permission) {
|
||||
results.failed.push({
|
||||
user_id,
|
||||
permission_id,
|
||||
error: "Permission not found"
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if assignment already exists
|
||||
const existingAssignment = await UserPermission.findOne({
|
||||
where: { user_id, permission_id }
|
||||
});
|
||||
if (existingAssignment) {
|
||||
results.failed.push({
|
||||
user_id,
|
||||
permission_id,
|
||||
error: "User permission already exists"
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const newUserPermission = await UserPermission.create({
|
||||
user_id,
|
||||
permission_id
|
||||
});
|
||||
|
||||
results.created.push(newUserPermission);
|
||||
|
||||
} catch (itemError) {
|
||||
results.failed.push({
|
||||
user_id,
|
||||
permission_id,
|
||||
error: itemError.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
res.status(201).json({
|
||||
success: results.failed.length === 0,
|
||||
data: results
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error bulk creating user permissions:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "An error occurred while bulk creating user permissions."
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Update user permission
|
||||
exports.updateUserPermission = async (req, res) => {
|
||||
try {
|
||||
const { upId } = req.params;
|
||||
const { permission_id } = req.body;
|
||||
|
||||
if (!permission_id) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "Permission ID is required"
|
||||
});
|
||||
}
|
||||
|
||||
const userPermission = await UserPermission.findByPk(upId);
|
||||
if (!userPermission) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "User permission not found"
|
||||
});
|
||||
}
|
||||
|
||||
// Verify permission exists
|
||||
const permission = await Permission.findByPk(permission_id);
|
||||
if (!permission) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "Permission not found"
|
||||
});
|
||||
}
|
||||
|
||||
// Check if new permission assignment already exists for this user
|
||||
const existingAssignment = await UserPermission.findOne({
|
||||
where: {
|
||||
user_id: userPermission.user_id,
|
||||
permission_id
|
||||
}
|
||||
});
|
||||
if (existingAssignment && existingAssignment.up_id !== upId) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "This permission is already assigned to this user"
|
||||
});
|
||||
}
|
||||
|
||||
await userPermission.update({ permission_id });
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: userPermission,
|
||||
message: "User permission updated successfully"
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error updating user permission:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "An error occurred while updating user permission."
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Delete user permission
|
||||
exports.deleteUserPermission = async (req, res) => {
|
||||
try {
|
||||
const { upId } = req.params;
|
||||
|
||||
const userPermission = await UserPermission.findByPk(upId);
|
||||
if (!userPermission) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "User permission not found"
|
||||
});
|
||||
}
|
||||
|
||||
await userPermission.destroy();
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
message: "User permission deleted successfully"
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error deleting user permission:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "An error occurred while deleting user permission."
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,219 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Niolla
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is proprietary and confidential.
|
||||
* Unauthorized copying, modification, distribution, or use
|
||||
* of this file, via any medium, is strictly prohibited.
|
||||
*/
|
||||
|
||||
// app/controllers/profile.controller.js
|
||||
|
||||
const db = require("../models");
|
||||
const Profile = db.Profile;
|
||||
const Upload = db.Upload;
|
||||
const User = db.User;
|
||||
|
||||
const { generateTodoId } = require("../utils/idGen.util");
|
||||
const { getSignedFileUrl } = require("../utils/s3Upload.utill");
|
||||
const {
|
||||
generateResetToken,
|
||||
resetPassword,
|
||||
} = require("../utils/passwordReset.utill");
|
||||
const { sendMail } = require("../utils/mail.util");
|
||||
const { logActivity } = require("../services/activity.service");
|
||||
const { hashPassword, checkPassword } = require("../utils/hashPassword.util");
|
||||
const { log } = require("../utils/consoleLog.utill");
|
||||
|
||||
// Get Profile avatar by user ID
|
||||
exports.getProfileAvatar = async (req, res) => {
|
||||
try {
|
||||
const userId = req.params.userId;
|
||||
const profile = await Profile.findOne({ where: { user_id: userId } });
|
||||
if (!profile) {
|
||||
return res.status(404).json({ error: "Profile not found" });
|
||||
}
|
||||
|
||||
const uploadRecord = await Upload.findByPk(profile.profilePicture_id);
|
||||
|
||||
const fileUrl = await getSignedFileUrl(uploadRecord.file_path);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
userId: profile.userId,
|
||||
profilePictureUrl: fileUrl,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error fetching profile:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "Internal server error",
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Get profile background image by user ID
|
||||
exports.getProfileBackgroundImage = async (req, res) => {
|
||||
try {
|
||||
const userId = req.params.userId;
|
||||
const profile = await Profile.findOne({ where: { user_id: userId } });
|
||||
if (!profile) {
|
||||
return res
|
||||
.status(404)
|
||||
.json({ success: false, message: "Profile not found" });
|
||||
}
|
||||
|
||||
const uploadRecord = await Upload.findByPk(profile.backgroundImage_id);
|
||||
|
||||
const fileUrl = await getSignedFileUrl(uploadRecord.file_path);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
userId: profile.userId,
|
||||
backgroundImageUrl: fileUrl,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error fetching profile background image:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "Internal server error",
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Request password reset token
|
||||
exports.requestPasswordReset = async (req, res) => {
|
||||
try {
|
||||
const { email } = req.body;
|
||||
|
||||
const user = await User.findOne({
|
||||
where: { email },
|
||||
});
|
||||
|
||||
if (user) {
|
||||
const token = await generateResetToken(user.id);
|
||||
const resetLink = `${process.env.FRONTEND_URL}/reset-password?token=${token}&email=${encodeURIComponent(email)}`;
|
||||
|
||||
await sendMail({
|
||||
to: user.email,
|
||||
subject: "Password Reset",
|
||||
templateName: "passwordReset",
|
||||
templateVars: {
|
||||
firstName: user.firstName,
|
||||
resetLink,
|
||||
expiryTime: process.env.PASSWORD_RESET_EXPIRY_TIME || "10 minutes",
|
||||
},
|
||||
});
|
||||
|
||||
const activityUser = {
|
||||
id: 1,
|
||||
firstName: "System",
|
||||
};
|
||||
|
||||
await logActivity({
|
||||
user: activityUser,
|
||||
description: "Requested password reset for email: " + email,
|
||||
type: "PASSWORD_RESET_REQUEST",
|
||||
module: "Authentication",
|
||||
});
|
||||
}
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
message:
|
||||
"If an account exists with this email address, a password reset email has been sent.",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "Internal server error",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Reset password using token
|
||||
exports.resetPassword = async (req, res) => {
|
||||
try {
|
||||
const { email, token, newPassword } = req.body;
|
||||
|
||||
// Validate email
|
||||
const user = await User.findOne({
|
||||
where: { email },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "Invalid email address.",
|
||||
});
|
||||
}
|
||||
|
||||
await resetPassword(user.id, token, newPassword);
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
message: "Password reset successfully.",
|
||||
});
|
||||
} catch (error) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: error.message || "Invalid or expired token.",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Change Password
|
||||
exports.changePassword = async (req, res) => {
|
||||
try {
|
||||
const { currentPassword, newPassword } = req.body;
|
||||
|
||||
const user = req.user;
|
||||
|
||||
log(`User:`, user);
|
||||
|
||||
const foundUser = await User.findOne({ where: { id: user.id } });
|
||||
|
||||
if (!foundUser) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "User not found.",
|
||||
});
|
||||
}
|
||||
|
||||
const isMatch = await checkPassword(currentPassword, foundUser.password);
|
||||
|
||||
if (!isMatch) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "Current password is incorrect.",
|
||||
});
|
||||
}
|
||||
|
||||
foundUser.password = await hashPassword(newPassword);
|
||||
await foundUser.save();
|
||||
await logActivity({
|
||||
user: req.user,
|
||||
description: "Changed password",
|
||||
type: "PASSWORD_CHANGE",
|
||||
module: "Authentication",
|
||||
});
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
message: "Password changed successfully.",
|
||||
});
|
||||
} catch (error) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: error.message || "Failed to change password.",
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Niolla
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is proprietary and confidential.
|
||||
* Unauthorized copying, modification, distribution, or use
|
||||
* of this file, via any medium, is strictly prohibited.
|
||||
*/
|
||||
|
||||
// app/controllers/activity.controller.js
|
||||
|
||||
const { uploadToS3, getSignedFileUrl } = require("../utils/s3Upload.utill");
|
||||
const { log } = require("../utils/consoleLog.utill");
|
||||
|
||||
const db = require("../models");
|
||||
const Upload = db.Upload;
|
||||
const Assets = db.Assets;
|
||||
|
||||
// Constants
|
||||
const MAX_IMAGE_SIZE = 3 * 1024 * 1024; // 3MB
|
||||
const MAX_PDF_SIZE = 5 * 1024 * 1024; // 5MB
|
||||
|
||||
const isValidFileType = (mimetype) => {
|
||||
return mimetype.startsWith("image/") || mimetype === "application/pdf";
|
||||
};
|
||||
|
||||
const isValidFileSize = (mimetype, size) => {
|
||||
if (mimetype.startsWith("image/")) return size <= MAX_IMAGE_SIZE;
|
||||
if (mimetype === "application/pdf") return size <= MAX_PDF_SIZE;
|
||||
return false;
|
||||
};
|
||||
|
||||
exports.uploadFile = async (req, res) => {
|
||||
try {
|
||||
// 1. Check file exists
|
||||
if (!req.file) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "No file uploaded",
|
||||
});
|
||||
}
|
||||
|
||||
const { mimetype, size, originalname } = req.file;
|
||||
|
||||
// 2. Validate file type
|
||||
if (!isValidFileType(mimetype)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "Only images and PDFs are allowed",
|
||||
});
|
||||
}
|
||||
|
||||
// 3. Validate file size
|
||||
if (!isValidFileSize(mimetype, size)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: mimetype.startsWith("image/")
|
||||
? "Image too large (max 1MB)"
|
||||
: "PDF too large (max 5MB)",
|
||||
});
|
||||
}
|
||||
|
||||
log("File validation passed:", {
|
||||
mimetype,
|
||||
size,
|
||||
originalname,
|
||||
use_for: req.body.use_for,
|
||||
});
|
||||
|
||||
// 4. Upload to S3
|
||||
const fileKey = await uploadToS3(req.file, req.body.use_for);
|
||||
|
||||
const fileUrl = await getSignedFileUrl(fileKey);
|
||||
|
||||
// 5. Save to DB
|
||||
const uploadData = {
|
||||
file_path: fileKey,
|
||||
file_type: mimetype,
|
||||
file_size: size,
|
||||
original_name: originalname,
|
||||
use_for: req.body.use_for || null,
|
||||
uploaded_by: req.user?.id || null,
|
||||
};
|
||||
|
||||
const newUpload = await Upload.create(uploadData);
|
||||
|
||||
newUpload.dataValues.file_url = fileUrl; // Add URL to response
|
||||
|
||||
// 6. Response
|
||||
return res.status(201).json({
|
||||
success: true,
|
||||
message: "File uploaded successfully",
|
||||
data: newUpload,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Upload Controller Error:", error);
|
||||
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "Failed to upload file",
|
||||
error: process.env.NODE_ENV === "development" ? error.message : undefined,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Get Uploaded File URL
|
||||
exports.getFileUrl = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const uploadRecord = await Upload.findByPk(id);
|
||||
|
||||
if (!uploadRecord) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "File not found",
|
||||
});
|
||||
}
|
||||
|
||||
const fileUrl = await getSignedFileUrl(uploadRecord.file_path);
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
message: "File URL retrieved successfully",
|
||||
data: {
|
||||
id: uploadRecord.id,
|
||||
file_url: fileUrl,
|
||||
},
|
||||
});
|
||||
} catch (error) {}
|
||||
};
|
||||
@@ -0,0 +1,323 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Niolla
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is proprietary and confidential.
|
||||
* Unauthorized copying, modification, distribution, or use
|
||||
* of this file, via any medium, is strictly prohibited.
|
||||
*/
|
||||
|
||||
// app/controllers/user.controller.js
|
||||
|
||||
const db = require("../models");
|
||||
const { hashPassword } = require("../utils/hashPassword.util");
|
||||
const { generateUserId, generateId } = require("../utils/idGen.util");
|
||||
const { logActivity } = require("../services/activity.service");
|
||||
const { sendMail } = require("../utils/mail.util");
|
||||
const User = db.User;
|
||||
const Profile = db.Profile;
|
||||
|
||||
// Create a new user
|
||||
exports.createNewUser = async (req, res) => {
|
||||
const transaction = await db.sequelize.transaction();
|
||||
|
||||
try {
|
||||
const {
|
||||
firstName,
|
||||
lastName,
|
||||
email,
|
||||
role,
|
||||
roleID,
|
||||
accountType,
|
||||
department,
|
||||
} = req.body;
|
||||
|
||||
const hashedPassword = await hashPassword(process.env.DEFAULT_PASSWORD);
|
||||
const userID = generateUserId();
|
||||
|
||||
const userExists = await User.findOne({ where: { email } });
|
||||
if (userExists) {
|
||||
await transaction.rollback();
|
||||
|
||||
return res.status(400).send({
|
||||
success: false,
|
||||
message: "User with this email already exists",
|
||||
});
|
||||
}
|
||||
|
||||
const newUser = await User.create(
|
||||
{
|
||||
id: userID,
|
||||
firstName,
|
||||
lastName,
|
||||
email,
|
||||
password: hashedPassword,
|
||||
accountType,
|
||||
role,
|
||||
roleID: roleID || "N/A",
|
||||
department: department || null,
|
||||
},
|
||||
{ transaction },
|
||||
);
|
||||
|
||||
const newProfile = await Profile.create(
|
||||
{
|
||||
profile_id: generateId(),
|
||||
user_id: newUser.id,
|
||||
theme: "light",
|
||||
notificationsEnabled: true,
|
||||
profilePicture_id: "N/A",
|
||||
backgroundImage_id: "N/A",
|
||||
dob: null,
|
||||
phone_number: null,
|
||||
},
|
||||
{ transaction },
|
||||
);
|
||||
|
||||
await sendMail({
|
||||
to: email,
|
||||
subject: "Welcome - Ocenic Titan",
|
||||
templateName: "welcome",
|
||||
templateVars: {
|
||||
firstName: firstName,
|
||||
email: email,
|
||||
password: process.env.DEFAULT_PASSWORD,
|
||||
},
|
||||
text: `Hello ${firstName}, your account has been created successfully.`,
|
||||
});
|
||||
|
||||
await transaction.commit();
|
||||
|
||||
await logActivity({
|
||||
user: req.user,
|
||||
description: `Created New User with ID: ${newUser.id}`,
|
||||
type: "CREATE_USER",
|
||||
module: "User Management",
|
||||
});
|
||||
|
||||
res.status(201).send({
|
||||
success: true,
|
||||
message: "User Created Successfully",
|
||||
data: {
|
||||
id: newUser.id,
|
||||
firstName: newUser.firstName,
|
||||
lastName: newUser.lastName,
|
||||
email: newUser.email,
|
||||
accountType: newUser.accountType,
|
||||
role: newUser.role,
|
||||
department: newUser.department,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
|
||||
res.status(500).send({
|
||||
success: false,
|
||||
message: "Failed to create user",
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Get users with pagination (20 per page)
|
||||
exports.getAllUsers = async (req, res) => {
|
||||
try {
|
||||
const page = parseInt(req.query.page) || 1; // default page = 1
|
||||
const limit = 20;
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
const { count, rows: users } = await User.findAndCountAll({
|
||||
attributes: { exclude: ["password"] },
|
||||
limit,
|
||||
offset,
|
||||
order: [["createdAt", "DESC"]], // optional sorting
|
||||
});
|
||||
|
||||
res.status(200).send({
|
||||
success: true,
|
||||
data: users,
|
||||
pagination: {
|
||||
totalUsers: count,
|
||||
totalPages: Math.ceil(count / limit),
|
||||
currentPage: page,
|
||||
pageSize: limit,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).send({
|
||||
success: false,
|
||||
message: "Failed to retrieve users",
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Get user details by ID
|
||||
exports.getUserById = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const user = await User.findOne({
|
||||
where: { id },
|
||||
attributes: { exclude: ["password"] },
|
||||
include: [{ model: Profile, as: "profile" }],
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return res.status(404).send({
|
||||
success: false,
|
||||
message: "User not found",
|
||||
});
|
||||
}
|
||||
|
||||
res.status(200).send({
|
||||
success: true,
|
||||
data: user,
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).send({
|
||||
success: false,
|
||||
message: "Failed to retrieve user",
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Update user details
|
||||
exports.updateUser = async (req, res) => {
|
||||
const transaction = await db.sequelize.transaction();
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const {
|
||||
firstName,
|
||||
lastName,
|
||||
email,
|
||||
role,
|
||||
roleID,
|
||||
accountType,
|
||||
department,
|
||||
theme,
|
||||
notificationsEnabled,
|
||||
profilePicture_id,
|
||||
backgroundImage_id,
|
||||
dob,
|
||||
phone_number,
|
||||
} = req.body;
|
||||
const user = await User.findOne({
|
||||
where: { id },
|
||||
});
|
||||
const UserProfile = await Profile.findOne({
|
||||
where: { user_id: id },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return res.status(404).send({
|
||||
success: false,
|
||||
message: "User not found",
|
||||
});
|
||||
}
|
||||
|
||||
if (!UserProfile) {
|
||||
return res.status(404).send({
|
||||
success: false,
|
||||
message: "User profile not found",
|
||||
});
|
||||
}
|
||||
|
||||
user.firstName = firstName || user.firstName;
|
||||
user.lastName = lastName || user.lastName;
|
||||
user.role = role || user.role;
|
||||
user.roleID = roleID || user.roleID || "N/A";
|
||||
user.accountType = accountType || user.accountType;
|
||||
user.department = department || user.department;
|
||||
|
||||
UserProfile.theme = theme || UserProfile.theme;
|
||||
UserProfile.notificationsEnabled =
|
||||
notificationsEnabled !== undefined
|
||||
? notificationsEnabled
|
||||
: UserProfile.notificationsEnabled;
|
||||
UserProfile.profilePicture_id =
|
||||
profilePicture_id || UserProfile.profilePicture_id || "N/A";
|
||||
UserProfile.backgroundImage_id =
|
||||
backgroundImage_id || UserProfile.backgroundImage_id || "N/A";
|
||||
UserProfile.dob = dob || UserProfile.dob;
|
||||
UserProfile.phone_number = phone_number || UserProfile.phone_number;
|
||||
|
||||
await UserProfile.save({ transaction });
|
||||
await user.save({ transaction });
|
||||
await transaction.commit();
|
||||
|
||||
await logActivity({
|
||||
user: req.user,
|
||||
description: `Updated User with ID: ${user.id}`,
|
||||
type: "UPDATE_USER",
|
||||
module: "User Management",
|
||||
});
|
||||
|
||||
const data = {
|
||||
id: user.id,
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
email: user.email,
|
||||
accountType: user.accountType,
|
||||
role: user.role,
|
||||
department: user.department,
|
||||
profile: {
|
||||
theme: UserProfile.theme,
|
||||
notificationsEnabled: UserProfile.notificationsEnabled,
|
||||
profilePicture_id: UserProfile.profilePicture_id,
|
||||
backgroundImage_id: UserProfile.backgroundImage_id,
|
||||
dob: UserProfile.dob,
|
||||
phone_number: UserProfile.phone_number,
|
||||
},
|
||||
};
|
||||
|
||||
res.status(200).send({
|
||||
success: true,
|
||||
message: "User updated successfully",
|
||||
data,
|
||||
});
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
res.status(500).send({
|
||||
success: false,
|
||||
message: `Error: ${error.message}`,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Delete user
|
||||
exports.deleteUser = async (req, res) => {
|
||||
const transaction = await db.sequelize.transaction();
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const user = await User.findOne({
|
||||
where: { id },
|
||||
});
|
||||
if (!user) {
|
||||
return res.status(404).send({
|
||||
success: false,
|
||||
message: "User not found",
|
||||
});
|
||||
}
|
||||
await user.destroy({ transaction });
|
||||
await transaction.commit();
|
||||
|
||||
await logActivity({
|
||||
user: req.user,
|
||||
description: `Deleted User with ID: ${user.id}`,
|
||||
type: "DELETE_USER",
|
||||
module: "User Management",
|
||||
});
|
||||
|
||||
res.status(200).send({
|
||||
success: true,
|
||||
message: "User deleted successfully",
|
||||
});
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
res.status(500).send({
|
||||
success: false,
|
||||
message: `Error: ${error.message}`,
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Niolla
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is proprietary and confidential.
|
||||
* Unauthorized copying, modification, distribution, or use
|
||||
* of this file, via any medium, is strictly prohibited.
|
||||
*/
|
||||
|
||||
// app/logic/documents/builders/invoice.builder.js
|
||||
|
||||
module.exports = {
|
||||
sheetName: "Invoice",
|
||||
|
||||
build: async (worksheet, data) => {
|
||||
worksheet.addRow(["INVOICE"]).font = {
|
||||
bold: true,
|
||||
size: 18,
|
||||
};
|
||||
|
||||
worksheet.addRow([]);
|
||||
|
||||
worksheet.addRow(["Invoice No", data.invoiceNo]);
|
||||
worksheet.addRow(["Customer", data.customerName]);
|
||||
worksheet.addRow(["Date", data.date]);
|
||||
|
||||
worksheet.addRow([]);
|
||||
|
||||
const header = worksheet.addRow([
|
||||
"Description",
|
||||
"Qty",
|
||||
"Rate",
|
||||
"Amount",
|
||||
]);
|
||||
|
||||
header.font = { bold: true };
|
||||
|
||||
let total = 0;
|
||||
|
||||
(data.items || []).forEach((item) => {
|
||||
const amount = item.qty * item.rate;
|
||||
|
||||
total += amount;
|
||||
|
||||
worksheet.addRow([
|
||||
item.description,
|
||||
item.qty,
|
||||
item.rate,
|
||||
amount,
|
||||
]);
|
||||
});
|
||||
|
||||
worksheet.addRow([]);
|
||||
|
||||
worksheet.addRow([
|
||||
"",
|
||||
"",
|
||||
"TOTAL",
|
||||
total,
|
||||
]).font = {
|
||||
bold: true,
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Niolla
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is proprietary and confidential.
|
||||
* Unauthorized copying, modification, distribution, or use
|
||||
* of this file, via any medium, is strictly prohibited.
|
||||
*/
|
||||
|
||||
// app/logic/documents/builders/precost.builder.js
|
||||
|
||||
module.exports = {
|
||||
sheetName: "PreCost Estimate",
|
||||
|
||||
build: async (worksheet, data) => {
|
||||
worksheet.addRow(["PreCost Estimate"]).font = {
|
||||
bold: true,
|
||||
size: 16,
|
||||
};
|
||||
|
||||
worksheet.addRow([]);
|
||||
|
||||
worksheet.addRow([
|
||||
"Customer",
|
||||
data.customerName || "",
|
||||
]);
|
||||
|
||||
worksheet.addRow([]);
|
||||
|
||||
const header = worksheet.addRow([
|
||||
"Item",
|
||||
"Description",
|
||||
"Qty",
|
||||
"Unit Price",
|
||||
"Total",
|
||||
]);
|
||||
|
||||
header.font = { bold: true };
|
||||
|
||||
let total = 0;
|
||||
|
||||
(data.items || []).forEach((item) => {
|
||||
const rowTotal = item.quantity * item.unit_price;
|
||||
|
||||
total += rowTotal;
|
||||
|
||||
worksheet.addRow([
|
||||
item.item_name,
|
||||
item.customer_remark ,
|
||||
item.quantity,
|
||||
item.unit_price,
|
||||
rowTotal,
|
||||
]);
|
||||
});
|
||||
|
||||
worksheet.addRow([]);
|
||||
|
||||
worksheet.addRow([
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"Grand Total",
|
||||
total,
|
||||
]).font = {
|
||||
bold: true,
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Niolla
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is proprietary and confidential.
|
||||
* Unauthorized copying, modification, distribution, or use
|
||||
* of this file, via any medium, is strictly prohibited.
|
||||
*/
|
||||
|
||||
// app/logic/documents/engine/excel.engine.js
|
||||
|
||||
const ExcelJS = require("exceljs");
|
||||
|
||||
const generateExcel = async (builder, data) => {
|
||||
const workbook = new ExcelJS.Workbook();
|
||||
const worksheet = workbook.addWorksheet(builder.sheetName);
|
||||
|
||||
await builder.build(worksheet, data);
|
||||
|
||||
return await workbook.xlsx.writeBuffer();
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
generateExcel,
|
||||
};
|
||||
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Niolla
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is proprietary and confidential.
|
||||
* Unauthorized copying, modification, distribution, or use
|
||||
* of this file, via any medium, is strictly prohibited.
|
||||
*/
|
||||
|
||||
// app/logic/documents/engine/pdf.engine.js
|
||||
|
||||
const puppeteer = require("puppeteer");
|
||||
|
||||
const generatePDF = async (html) => {
|
||||
const launchConfig = {
|
||||
args: ["--no-sandbox", "--disable-setuid-sandbox"],
|
||||
};
|
||||
|
||||
if (process.env.PUPPETEER_EXECUTABLE_PATH) {
|
||||
launchConfig.executablePath = process.env.PUPPETEER_EXECUTABLE_PATH;
|
||||
} else {
|
||||
launchConfig.headless = true;
|
||||
}
|
||||
|
||||
const browser = await puppeteer.launch(launchConfig);
|
||||
|
||||
try {
|
||||
const page = await browser.newPage();
|
||||
|
||||
await page.setContent(html, {
|
||||
waitUntil: "load",
|
||||
});
|
||||
|
||||
// Wait for fonts
|
||||
await page.evaluate(() => document.fonts?.ready);
|
||||
|
||||
// Wait for images to fully decode
|
||||
await page.evaluate(async () => {
|
||||
const images = Array.from(document.images);
|
||||
|
||||
await Promise.all(
|
||||
images.map((img) => {
|
||||
if (img.complete && img.naturalHeight !== 0) return;
|
||||
|
||||
return new Promise((resolve) => {
|
||||
img.onload = resolve;
|
||||
img.onerror = resolve;
|
||||
});
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
// Force layout + paint
|
||||
await page.evaluate(() => document.body.getBoundingClientRect());
|
||||
|
||||
// Extra stability delay (important on servers)
|
||||
await new Promise((r) => setTimeout(r, 1000));
|
||||
|
||||
const buffer = await page.pdf({
|
||||
format: "A4",
|
||||
printBackground: true,
|
||||
});
|
||||
|
||||
return buffer;
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = { generatePDF };
|
||||
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Niolla
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is proprietary and confidential.
|
||||
* Unauthorized copying, modification, distribution, or use
|
||||
* of this file, via any medium, is strictly prohibited.
|
||||
*/
|
||||
|
||||
// app/logic/documents/index.js
|
||||
|
||||
const registry = require("./registry");
|
||||
|
||||
const { generateExcel } = require("./engine/excel.engine");
|
||||
const {
|
||||
generatePdfFromPreCost,
|
||||
generatePdfFromInvoice,
|
||||
generatePdfFromDispatchNote,
|
||||
generatePdfFromDeliveryNote,
|
||||
generatePdfFromPO,
|
||||
generatePdfFromCustomDocument,
|
||||
generatePdfFromCreditNote,
|
||||
} = require("../../utils/pdfGenerator");
|
||||
|
||||
const formatMeta = {
|
||||
pdf: {
|
||||
ext: "pdf",
|
||||
mime: "application/pdf",
|
||||
},
|
||||
|
||||
excel: {
|
||||
ext: "xlsx",
|
||||
mime:
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
},
|
||||
};
|
||||
|
||||
const pdfGenerators = {
|
||||
precost: generatePdfFromPreCost,
|
||||
invoice: generatePdfFromInvoice,
|
||||
dispatchnote: generatePdfFromDispatchNote,
|
||||
deliverynote: generatePdfFromDeliveryNote,
|
||||
po: generatePdfFromPO,
|
||||
customdocument: generatePdfFromCustomDocument,
|
||||
creditnote: generatePdfFromCreditNote,
|
||||
};
|
||||
|
||||
const generateDocument = async (
|
||||
document,
|
||||
format,
|
||||
data
|
||||
) => {
|
||||
document = document.toLowerCase().replace(/[\s_-]+/g, "");
|
||||
format = format.toLowerCase();
|
||||
|
||||
const doc = registry[document];
|
||||
|
||||
if (!doc) {
|
||||
const availableTypes = Object.keys(registry).join(", ");
|
||||
throw new Error(`Unsupported document type: "${document}". Available types: ${availableTypes}`);
|
||||
}
|
||||
|
||||
let fileBuffer;
|
||||
|
||||
if (format === "pdf") {
|
||||
// Format the data using the template
|
||||
const formattedData = await doc.pdfTemplate(data);
|
||||
|
||||
// Get the correct PDF generator for this document type
|
||||
const pdfGenerator = pdfGenerators[document];
|
||||
|
||||
if (pdfGenerator) {
|
||||
fileBuffer = await pdfGenerator(formattedData);
|
||||
} else {
|
||||
throw new Error(`No PDF generator found for document type: ${document}`);
|
||||
}
|
||||
}
|
||||
|
||||
else if (format === "excel") {
|
||||
if (doc.excelBuilder) {
|
||||
fileBuffer = await generateExcel(
|
||||
doc.excelBuilder,
|
||||
data
|
||||
);
|
||||
} else {
|
||||
throw new Error(`Excel generation not supported for document type: ${document}`);
|
||||
}
|
||||
}
|
||||
|
||||
else {
|
||||
throw new Error("Unsupported format");
|
||||
}
|
||||
|
||||
const meta = formatMeta[format];
|
||||
|
||||
return {
|
||||
fileBuffer,
|
||||
fileName: `${document}-${Date.now()}.${meta.ext}`,
|
||||
mimeType: meta.mime,
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
generateDocument,
|
||||
};
|
||||
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Niolla
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is proprietary and confidential.
|
||||
* Unauthorized copying, modification, distribution, or use
|
||||
* of this file, via any medium, is strictly prohibited.
|
||||
*/
|
||||
|
||||
// app/logic/documents/registry.js
|
||||
|
||||
let buildPreCostTemplate, buildInvoiceTemplate, buildDispatchNoteTemplate, buildDeliveryNoteTemplate, buildPOTemplate, buildCustomDocumentTemplate, buildCreditNoteTemplate;
|
||||
let precostBuilder, invoiceBuilder;
|
||||
|
||||
try {
|
||||
buildPreCostTemplate = require("../../templates/documents/precost/preCost.template");
|
||||
buildInvoiceTemplate = require("../../templates/documents/invoice/invoice.template");
|
||||
buildDispatchNoteTemplate = require("../../templates/documents/dispatchNote/dispatchNote.template");
|
||||
buildDeliveryNoteTemplate = require("../../templates/documents/deliveryNote/deliveryNote.template");
|
||||
buildPOTemplate = require("../../templates/documents/po/po.template");
|
||||
buildCustomDocumentTemplate = require("../../templates/documents/customDocument/customDocument.template");
|
||||
buildCreditNoteTemplate = require("../../templates/documents/creditNote/creditNote.template");
|
||||
|
||||
precostBuilder = require("./builders/precost.builder");
|
||||
invoiceBuilder = require("./builders/invoice.builder");
|
||||
|
||||
console.log("✅ All document templates loaded successfully");
|
||||
} catch (error) {
|
||||
console.error("❌ Error loading document templates:", error.message);
|
||||
console.error(error.stack);
|
||||
}
|
||||
|
||||
const registry = {
|
||||
precost: {
|
||||
pdfTemplate: buildPreCostTemplate,
|
||||
excelBuilder: precostBuilder,
|
||||
},
|
||||
|
||||
invoice: {
|
||||
pdfTemplate: buildInvoiceTemplate,
|
||||
excelBuilder: invoiceBuilder,
|
||||
},
|
||||
|
||||
dispatchnote: {
|
||||
pdfTemplate: buildDispatchNoteTemplate,
|
||||
excelBuilder: null,
|
||||
},
|
||||
|
||||
deliverynote: {
|
||||
pdfTemplate: buildDeliveryNoteTemplate,
|
||||
excelBuilder: null,
|
||||
},
|
||||
|
||||
po: {
|
||||
pdfTemplate: buildPOTemplate,
|
||||
excelBuilder: null,
|
||||
},
|
||||
|
||||
customdocument: {
|
||||
pdfTemplate: buildCustomDocumentTemplate,
|
||||
excelBuilder: null,
|
||||
},
|
||||
|
||||
creditnote: {
|
||||
pdfTemplate: buildCreditNoteTemplate,
|
||||
excelBuilder: null,
|
||||
},
|
||||
};
|
||||
|
||||
console.log("📋 Registry initialized with keys:", Object.keys(registry));
|
||||
|
||||
module.exports = registry;
|
||||
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Niolla
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is proprietary and confidential.
|
||||
* Unauthorized copying, modification, distribution, or use
|
||||
* of this file, via any medium, is strictly prohibited.
|
||||
*/
|
||||
|
||||
// app/middleware/auth.middleware.js
|
||||
|
||||
const { verifyToken } = require("../utils/jwt.util");
|
||||
const { getEffectivePermissions } = require("../services/permission.service");
|
||||
|
||||
const authenticate = async (req, res, next) => {
|
||||
try {
|
||||
let token = null;
|
||||
|
||||
// Get token from cookie
|
||||
if (req.cookies?.access_token) {
|
||||
token = req.cookies.access_token;
|
||||
}
|
||||
|
||||
// Fallback to Bearer token
|
||||
if (!token && req.headers.authorization?.startsWith("Bearer ")) {
|
||||
token = req.headers.authorization.split(" ")[1];
|
||||
}
|
||||
|
||||
if (!token) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: "Unauthorized",
|
||||
});
|
||||
}
|
||||
|
||||
// Verify token
|
||||
const decoded = verifyToken(token);
|
||||
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
console.log("DECODED:", decoded);
|
||||
}
|
||||
|
||||
const userId = decoded.sub || decoded.id;
|
||||
|
||||
if (!userId) {
|
||||
throw new Error("User ID missing in token");
|
||||
}
|
||||
|
||||
// Load permissions
|
||||
const permissions = await getEffectivePermissions(userId);
|
||||
|
||||
req.user = {
|
||||
...decoded,
|
||||
id: userId,
|
||||
permissions,
|
||||
};
|
||||
|
||||
next();
|
||||
} catch (err) {
|
||||
console.error("AUTH ERROR:", err);
|
||||
|
||||
// Clear invalid/expired cookie
|
||||
res.clearCookie("access_token", {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
sameSite: process.env.NODE_ENV === "production" ? "None" : "Lax",
|
||||
});
|
||||
|
||||
// Token expired
|
||||
if (err.name === "TokenExpiredError") {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: "Session expired. Please login again.",
|
||||
});
|
||||
}
|
||||
|
||||
// Invalid token
|
||||
if (err.name === "JsonWebTokenError") {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: "Invalid token",
|
||||
});
|
||||
}
|
||||
|
||||
// Default
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: "Authentication failed",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = { authenticate };
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Niolla
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is proprietary and confidential.
|
||||
* Unauthorized copying, modification, distribution, or use
|
||||
* of this file, via any medium, is strictly prohibited.
|
||||
*/
|
||||
|
||||
// app/middleware/docsSession.middleware.js
|
||||
|
||||
module.exports = (req, res, next) => {
|
||||
if (req.cookies && req.cookies.docsAuth === "true") {
|
||||
return next();
|
||||
}
|
||||
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: "Unauthorized: Please login to access docs",
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Niolla
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is proprietary and confidential.
|
||||
* Unauthorized copying, modification, distribution, or use
|
||||
* of this file, via any medium, is strictly prohibited.
|
||||
*/
|
||||
|
||||
// app/middleware/permission.middleware.js
|
||||
|
||||
const hasPermission = (userPermissions, requiredPermission) => {
|
||||
return userPermissions.some(p => {
|
||||
if (p === requiredPermission) return true;
|
||||
|
||||
// wildcard support
|
||||
if (p.endsWith(".*")) {
|
||||
const prefix = p.slice(0, -2);
|
||||
return requiredPermission.startsWith(prefix);
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
};
|
||||
|
||||
const methodToAction = {
|
||||
GET: "view",
|
||||
POST: "create",
|
||||
PUT: "update",
|
||||
PATCH: "update",
|
||||
DELETE: "delete"
|
||||
};
|
||||
|
||||
const checkPermission = (baseOrFull, options = {}) => {
|
||||
return (req, res, next) => {
|
||||
if (!req.user) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: "Unauthorized"
|
||||
});
|
||||
}
|
||||
|
||||
// admin bypass
|
||||
if (req.user.accountType === "admin") {
|
||||
return next();
|
||||
}
|
||||
|
||||
if (typeof baseOrFull !== "string") {
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "Permission must be a string"
|
||||
});
|
||||
}
|
||||
|
||||
let requiredPermission;
|
||||
|
||||
if (options.custom) {
|
||||
requiredPermission = baseOrFull;
|
||||
} else {
|
||||
const action = methodToAction[req.method];
|
||||
|
||||
if (!action) {
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "Unknown HTTP method"
|
||||
});
|
||||
}
|
||||
|
||||
requiredPermission = `${baseOrFull}.${action}`;
|
||||
}
|
||||
|
||||
const userPermissions = req.user.permissions || [];
|
||||
|
||||
const hasPermission =
|
||||
userPermissions.includes(requiredPermission) ||
|
||||
userPermissions.includes(`${baseOrFull}.*`);
|
||||
|
||||
if (!hasPermission) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: `Forbidden - Missing ${requiredPermission}`
|
||||
});
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
|
||||
const authorizedAccountType = (allowedTypes) => {
|
||||
return (req, res, next) => {
|
||||
if (!req.user || !allowedTypes.includes(req.user.accountType)) {
|
||||
return res
|
||||
.status(403)
|
||||
.json({ success: false, message: "Forbidden" });
|
||||
}
|
||||
next();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {authorizedAccountType, checkPermission};
|
||||
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Niolla
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is proprietary and confidential.
|
||||
* Unauthorized copying, modification, distribution, or use
|
||||
* of this file, via any medium, is strictly prohibited.
|
||||
*/
|
||||
|
||||
// app/middlewares/upload.middleware.js
|
||||
|
||||
const multer = require("multer");
|
||||
|
||||
// Use memory storage (required for S3 upload)
|
||||
const storage = multer.memoryStorage();
|
||||
|
||||
// File filter (only images + PDFs)
|
||||
const fileFilter = (req, file, cb) => {
|
||||
const allowedTypes = [
|
||||
"image/",
|
||||
"application/pdf",
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", // .xlsx
|
||||
"application/vnd.ms-excel" // .xls
|
||||
];
|
||||
|
||||
const isValid =
|
||||
file.mimetype.startsWith("image/") ||
|
||||
file.mimetype === "application/pdf" ||
|
||||
file.mimetype === "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" ||
|
||||
file.mimetype === "application/vnd.ms-excel";
|
||||
|
||||
if (isValid) {
|
||||
cb(null, true);
|
||||
} else {
|
||||
cb(new Error("Only images, PDFs, and Excel files are allowed"), false);
|
||||
}
|
||||
};
|
||||
|
||||
// Multer instance
|
||||
const upload = multer({
|
||||
storage,
|
||||
fileFilter,
|
||||
limits: {
|
||||
fileSize: 5 * 1024 * 1024, // max 5MB (global limit)
|
||||
},
|
||||
});
|
||||
|
||||
// Middleware wrappers
|
||||
|
||||
// Single file upload
|
||||
const uploadSingle = (fieldName) => upload.single(fieldName);
|
||||
|
||||
// Multiple files (same field)
|
||||
const uploadMultiple = (fieldName, maxCount = 5) =>
|
||||
upload.array(fieldName, maxCount);
|
||||
|
||||
// Multiple fields (advanced)
|
||||
const uploadFields = (fields) => upload.fields(fields);
|
||||
|
||||
module.exports = {
|
||||
uploadSingle,
|
||||
uploadMultiple,
|
||||
uploadFields,
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Niolla
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is proprietary and confidential.
|
||||
* Unauthorized copying, modification, distribution, or use
|
||||
* of this file, via any medium, is strictly prohibited.
|
||||
*/
|
||||
|
||||
// app/models/userActivities.model.js
|
||||
|
||||
module.exports = (sequelize, DataTypes) => {
|
||||
const UserActivity = sequelize.define(
|
||||
"UserActivity",
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
autoIncrement: true,
|
||||
primaryKey: true,
|
||||
},
|
||||
user_id: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
},
|
||||
username: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
},
|
||||
activity_description: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
},
|
||||
activity_type: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
},
|
||||
module:{
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
},
|
||||
activity_time: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
},
|
||||
activity_date:{
|
||||
type: DataTypes.DATEONLY,
|
||||
allowNull: false,
|
||||
}
|
||||
},
|
||||
{
|
||||
tableName: "UserActivity",
|
||||
timestamps: true,
|
||||
},
|
||||
);
|
||||
|
||||
return UserActivity;
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Niolla
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is proprietary and confidential.
|
||||
* Unauthorized copying, modification, distribution, or use
|
||||
* of this file, via any medium, is strictly prohibited.
|
||||
*/
|
||||
|
||||
// app/models/document/document.model.js
|
||||
|
||||
module.exports = (sequelize, DataTypes) => {
|
||||
const document = sequelize.define(
|
||||
"Document",
|
||||
{
|
||||
doc_id:{
|
||||
type: DataTypes.STRING,
|
||||
primaryKey: true,
|
||||
},
|
||||
reference_no: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
},
|
||||
doc_type: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
defaultValue: "N/A"
|
||||
},
|
||||
data: {
|
||||
type: DataTypes.JSON,
|
||||
allowNull: false,
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
defaultValue: "DRAFT",
|
||||
}
|
||||
},
|
||||
{
|
||||
tableName: "Document",
|
||||
timestamps: true,
|
||||
},
|
||||
);
|
||||
|
||||
return document;
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Niolla
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is proprietary and confidential.
|
||||
* Unauthorized copying, modification, distribution, or use
|
||||
* of this file, via any medium, is strictly prohibited.
|
||||
*/
|
||||
|
||||
// app/models/document/documentType.model.js
|
||||
|
||||
module.exports = (sequelize, DataTypes) => {
|
||||
const documentType = sequelize.define(
|
||||
"DocumentType",
|
||||
{
|
||||
id:{
|
||||
type: DataTypes.INTEGER,
|
||||
autoIncrement: true,
|
||||
primaryKey: true,
|
||||
},
|
||||
doc_type_name: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
defaultValue: "N/A"
|
||||
},
|
||||
description: {
|
||||
type: DataTypes.JSON,
|
||||
allowNull: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
tableName: "DocumentType",
|
||||
timestamps: true,
|
||||
},
|
||||
);
|
||||
|
||||
return documentType;
|
||||
};
|
||||
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Niolla
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is proprietary and confidential.
|
||||
* Unauthorized copying, modification, distribution, or use
|
||||
* of this file, via any medium, is strictly prohibited.
|
||||
*/
|
||||
|
||||
// app/models/index.js
|
||||
|
||||
|
||||
const { Sequelize, DataTypes } = require("sequelize");
|
||||
const dbConfig = require("../config/db.config");
|
||||
|
||||
let loggingOption = false;
|
||||
|
||||
if(process.env.NODE_ENV === 'production') {
|
||||
loggingOption = true;
|
||||
}
|
||||
|
||||
const sequelize = new Sequelize(
|
||||
dbConfig.DB,
|
||||
dbConfig.USER,
|
||||
dbConfig.PASSWORD,
|
||||
{
|
||||
host: dbConfig.HOST,
|
||||
port: dbConfig.PORT,
|
||||
dialect: dbConfig.DIALECT,
|
||||
pool: dbConfig.pool,
|
||||
logging: loggingOption
|
||||
}
|
||||
);
|
||||
|
||||
const db = {};
|
||||
|
||||
db.Sequelize = Sequelize;
|
||||
db.sequelize = sequelize;
|
||||
|
||||
/* Register db models */
|
||||
|
||||
// User and Authentication
|
||||
db.User = require("./user/user.model")(sequelize, DataTypes);
|
||||
db.UserActivity = require("./activities/userActivities.model")(sequelize, DataTypes);
|
||||
db.Profile = require("./user/profile.model")(sequelize, DataTypes);
|
||||
|
||||
// Uploads
|
||||
db.Upload = require("./upload/upload.model")(sequelize, DataTypes);
|
||||
|
||||
// Roles and Permissions
|
||||
db.roles = require("./permission/role.model")(sequelize, DataTypes);
|
||||
db.permission = require("./permission/permission.model")(sequelize, DataTypes);
|
||||
db.rolePermission = require("./permission/rolePermission.model")(sequelize, DataTypes);
|
||||
db.userPermission = require("./permission/userPermission.model")(sequelize, DataTypes);
|
||||
|
||||
// Document Management
|
||||
db.Document = require("./document/document.model")(sequelize, DataTypes);
|
||||
db.DocumentType = require("./document/documentType.model")(sequelize, DataTypes);
|
||||
|
||||
// Reference Numbers
|
||||
db.referenceNumber = require("./referenceNumbers/referenceNumber.model")(sequelize, DataTypes);
|
||||
|
||||
// Notifications
|
||||
db.notification = require("./notification/notification.model")(sequelize, DataTypes);
|
||||
db.userNotification = require("./notification/userNotification.model")(sequelize, DataTypes);
|
||||
|
||||
/* Associations */
|
||||
Object.keys(db).forEach(model => {
|
||||
if (db[model].associate) {
|
||||
db[model].associate(db);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
module.exports = db;
|
||||
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Niolla
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is proprietary and confidential.
|
||||
* Unauthorized copying, modification, distribution, or use
|
||||
* of this file, via any medium, is strictly prohibited.
|
||||
*/
|
||||
|
||||
// app/models/notification/notification.model.js
|
||||
|
||||
module.exports = (sequelize, DataTypes) => {
|
||||
const notification = sequelize.define(
|
||||
"notification",
|
||||
{
|
||||
notification_id: {
|
||||
type: DataTypes.STRING,
|
||||
primaryKey: true,
|
||||
},
|
||||
notificationHeadline: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
},
|
||||
notificationDescription: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
},
|
||||
notificationType: {
|
||||
type: DataTypes.ENUM("USER", "ANNOUNCEMENT"),
|
||||
allowNull: false,
|
||||
},
|
||||
isActive: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: true,
|
||||
},
|
||||
dateCreated: {
|
||||
type: DataTypes.DATE,
|
||||
defaultValue: DataTypes.NOW,
|
||||
},
|
||||
},
|
||||
{
|
||||
tableName: "notification",
|
||||
timestamps: true,
|
||||
},
|
||||
);
|
||||
|
||||
notification.associate = (models) => {
|
||||
notification.hasMany(models.userNotification, {
|
||||
foreignKey: "notification_id",
|
||||
as: "users",
|
||||
});
|
||||
};
|
||||
|
||||
return notification;
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Niolla
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is proprietary and confidential.
|
||||
* Unauthorized copying, modification, distribution, or use
|
||||
* of this file, via any medium, is strictly prohibited.
|
||||
*/
|
||||
|
||||
// app/models/notification/userNotification.model.js
|
||||
|
||||
module.exports = (sequelize, DataTypes) => {
|
||||
const userNotification = sequelize.define(
|
||||
"userNotification",
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
autoIncrement: true,
|
||||
primaryKey: true,
|
||||
},
|
||||
user_id: {
|
||||
type: DataTypes.STRING(255),
|
||||
allowNull: false,
|
||||
collate: "utf8mb4_general_ci",
|
||||
},
|
||||
notification_id: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
},
|
||||
isRead: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
tableName: "user_notification",
|
||||
timestamps: true,
|
||||
},
|
||||
);
|
||||
|
||||
userNotification.associate = (models) => {
|
||||
userNotification.belongsTo(models.User, {
|
||||
foreignKey: "user_id",
|
||||
constraints: false,
|
||||
});
|
||||
|
||||
userNotification.belongsTo(models.notification, {
|
||||
foreignKey: "notification_id",
|
||||
constraints: false,
|
||||
});
|
||||
};
|
||||
|
||||
return userNotification;
|
||||
};
|
||||
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Niolla
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is proprietary and confidential.
|
||||
* Unauthorized copying, modification, distribution, or use
|
||||
* of this file, via any medium, is strictly prohibited.
|
||||
*/
|
||||
|
||||
// app/models/permission/permission.model.js
|
||||
|
||||
module.exports = (sequelize, DataTypes) => {
|
||||
const permission = sequelize.define(
|
||||
"permission",
|
||||
{
|
||||
permission_id: {
|
||||
type: DataTypes.STRING,
|
||||
primaryKey: true,
|
||||
},
|
||||
permissionName: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
},
|
||||
permissionDescription: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
},
|
||||
page:{
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
},
|
||||
module:{
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
},
|
||||
action: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
}
|
||||
},
|
||||
{
|
||||
tableName: "permission",
|
||||
timestamps: true,
|
||||
},
|
||||
);
|
||||
|
||||
permission.associate = (db) => {
|
||||
permission.hasMany(db.userPermission, {
|
||||
foreignKey: "permission_id",
|
||||
as: "userPermissions"
|
||||
});
|
||||
permission.hasMany(db.rolePermission, {
|
||||
foreignKey: "permission_id",
|
||||
as: "rolePermissions"
|
||||
});
|
||||
};
|
||||
|
||||
return permission;
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Niolla
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is proprietary and confidential.
|
||||
* Unauthorized copying, modification, distribution, or use
|
||||
* of this file, via any medium, is strictly prohibited.
|
||||
*/
|
||||
|
||||
// app/models/permission/role.model.js
|
||||
|
||||
module.exports = (sequelize, DataTypes) => {
|
||||
const roles = sequelize.define(
|
||||
"roles",
|
||||
{
|
||||
role_id: {
|
||||
type: DataTypes.STRING,
|
||||
primaryKey: true
|
||||
},
|
||||
roleName: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
roleDescription: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true
|
||||
}
|
||||
},
|
||||
{
|
||||
tableName: "roles",
|
||||
timestamps: true
|
||||
}
|
||||
);
|
||||
|
||||
roles.associate = (db) => {
|
||||
roles.hasMany(db.rolePermission, {
|
||||
foreignKey: "role_id",
|
||||
as: "rolePermissions"
|
||||
});
|
||||
};
|
||||
|
||||
return roles;
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Niolla
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is proprietary and confidential.
|
||||
* Unauthorized copying, modification, distribution, or use
|
||||
* of this file, via any medium, is strictly prohibited.
|
||||
*/
|
||||
|
||||
// app/models/permission/rolePermission.model.js
|
||||
|
||||
module.exports = (sequelize, DataTypes) => {
|
||||
const rolePermission = sequelize.define(
|
||||
"rolePermission",
|
||||
{
|
||||
rp_id: {
|
||||
type: DataTypes.STRING,
|
||||
primaryKey: true
|
||||
},
|
||||
role_id: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
permission_id: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
}
|
||||
},
|
||||
{
|
||||
tableName: "rolePermission",
|
||||
timestamps: true
|
||||
}
|
||||
);
|
||||
|
||||
rolePermission.associate = (db) => {
|
||||
rolePermission.belongsTo(db.roles, {
|
||||
foreignKey: "role_id",
|
||||
as: "role"
|
||||
});
|
||||
rolePermission.belongsTo(db.permission, {
|
||||
foreignKey: "permission_id",
|
||||
as: "permission"
|
||||
});
|
||||
};
|
||||
|
||||
return rolePermission;
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Niolla
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is proprietary and confidential.
|
||||
* Unauthorized copying, modification, distribution, or use
|
||||
* of this file, via any medium, is strictly prohibited.
|
||||
*/
|
||||
|
||||
// app/models/permission/userPermission.model.js
|
||||
|
||||
module.exports = (sequelize, DataTypes) => {
|
||||
const userPermission = sequelize.define(
|
||||
"userPermission",
|
||||
{
|
||||
up_id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
},
|
||||
user_id: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
permission_id: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
}
|
||||
},
|
||||
{
|
||||
tableName: "userPermission",
|
||||
timestamps: true
|
||||
}
|
||||
);
|
||||
|
||||
userPermission.associate = (db) => {
|
||||
userPermission.belongsTo(db.permission, {
|
||||
foreignKey: "permission_id",
|
||||
as: "permission"
|
||||
});
|
||||
userPermission.belongsTo(db.User, {
|
||||
foreignKey: "user_id",
|
||||
as: "user"
|
||||
});
|
||||
};
|
||||
|
||||
return userPermission;
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Niolla
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is proprietary and confidential.
|
||||
* Unauthorized copying, modification, distribution, or use
|
||||
* of this file, via any medium, is strictly prohibited.
|
||||
*/
|
||||
|
||||
// app/models/referenceNumbers/referenceNumber.model.js
|
||||
|
||||
module.exports = (sequelize, DataTypes) => {
|
||||
const ReferenceNumber = sequelize.define(
|
||||
"ReferenceNumber",
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.STRING,
|
||||
primaryKey: true,
|
||||
},
|
||||
|
||||
// Unique sequence scope
|
||||
sequence_key: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
},
|
||||
|
||||
current_number: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
},
|
||||
|
||||
last_ref_number: {
|
||||
type: DataTypes.STRING,
|
||||
},
|
||||
},
|
||||
{
|
||||
tableName: "referenceNumbers",
|
||||
timestamps: true,
|
||||
}
|
||||
);
|
||||
|
||||
return ReferenceNumber;
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* © 2026 Niolla. All rights reserved.
|
||||
*
|
||||
* This file is part of the Niolla software project and is intended for internal use only.
|
||||
* Unauthorized copying, modification, distribution, or disclosure of this file,
|
||||
* via any medium, is strictly prohibited without written permission from Niolla.
|
||||
*
|
||||
* For inquiries, contact: support@niolla.lk
|
||||
*/
|
||||
|
||||
// app/models/upload.model.js
|
||||
|
||||
module.exports = (sequelize, DataTypes) => {
|
||||
const upload = sequelize.define(
|
||||
"upload",
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
autoIncrement: true,
|
||||
primaryKey: true,
|
||||
},
|
||||
file_path: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
},
|
||||
file_type: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
},
|
||||
file_size: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
},
|
||||
original_name: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
},
|
||||
use_for: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
},
|
||||
uploaded_by: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
tableName: "uploads",
|
||||
timestamps: true,
|
||||
},
|
||||
);
|
||||
|
||||
return upload;
|
||||
};
|
||||
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Niolla
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is proprietary and confidential.
|
||||
* Unauthorized copying, modification, distribution, or use
|
||||
* of this file, via any medium, is strictly prohibited.
|
||||
*/
|
||||
|
||||
// app/models/profile.model.js
|
||||
|
||||
module.exports = (sequelize, DataTypes) => {
|
||||
const Profile = sequelize.define(
|
||||
"Profile",
|
||||
{
|
||||
profile_id: {
|
||||
type: DataTypes.STRING,
|
||||
primaryKey: true,
|
||||
},
|
||||
user_id: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
collate: 'utf8mb4_general_ci'
|
||||
},
|
||||
theme: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
defaultValue: "light",
|
||||
},
|
||||
notificationsEnabled: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: true,
|
||||
},
|
||||
profilePicture_id: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
},
|
||||
backgroundImage_id: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
},
|
||||
dob: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true,
|
||||
},
|
||||
phone_number:{
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
}
|
||||
},
|
||||
{
|
||||
tableName: "profiles",
|
||||
timestamps: true,
|
||||
},
|
||||
);
|
||||
|
||||
Profile.associate = (db) => {
|
||||
Profile.belongsTo(db.User, {
|
||||
foreignKey: "user_id",
|
||||
as: "user",
|
||||
});
|
||||
};
|
||||
|
||||
return Profile;
|
||||
};
|
||||
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Niolla
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is proprietary and confidential.
|
||||
* Unauthorized copying, modification, distribution, or use
|
||||
* of this file, via any medium, is strictly prohibited.
|
||||
*/
|
||||
|
||||
// app/models/User.model.js
|
||||
|
||||
module.exports = (sequelize, DataTypes) => {
|
||||
const User = sequelize.define(
|
||||
"User",
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.STRING,
|
||||
primaryKey: true,
|
||||
collate: 'utf8mb4_general_ci'
|
||||
},
|
||||
firstName: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
lastName: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
email: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
unique: true
|
||||
},
|
||||
password: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
accountType: {
|
||||
type: DataTypes.ENUM("admin", "management", "team_head", "user"),
|
||||
defaultValue: "user"
|
||||
},
|
||||
role: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
roleID:{
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
department: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true
|
||||
}
|
||||
},
|
||||
{
|
||||
tableName: "users",
|
||||
timestamps: true
|
||||
}
|
||||
);
|
||||
|
||||
User.associate = (db) => {
|
||||
User.hasMany(db.userPermission, {
|
||||
foreignKey: "user_id",
|
||||
as: "userPermissions"
|
||||
});
|
||||
User.hasOne(db.Profile, {
|
||||
foreignKey: "user_id",
|
||||
as: "profile",
|
||||
});
|
||||
};
|
||||
|
||||
return User;
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Niolla
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is proprietary and confidential.
|
||||
* Unauthorized copying, modification, distribution, or use
|
||||
* of this file, via any medium, is strictly prohibited.
|
||||
*/
|
||||
|
||||
// app/queues/activity.queue.js
|
||||
|
||||
const { Queue } = require("bullmq");
|
||||
const connection = require("../config/redisClient");
|
||||
|
||||
|
||||
const activityQueue = new Queue("activity-queue", {
|
||||
connection,
|
||||
});
|
||||
|
||||
module.exports = activityQueue;
|
||||
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Niolla
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is proprietary and confidential.
|
||||
* Unauthorized copying, modification, distribution, or use
|
||||
* of this file, via any medium, is strictly prohibited.
|
||||
*/
|
||||
|
||||
// app/queues/document.queue.js
|
||||
|
||||
const { Queue } = require("bullmq");
|
||||
const redis = require("../config/redisClient");
|
||||
|
||||
const documentQueue = new Queue("document-generation", {
|
||||
connection: redis,
|
||||
defaultJobOptions: {
|
||||
attempts: 3,
|
||||
|
||||
backoff: {
|
||||
type: "exponential",
|
||||
delay: 2000,
|
||||
},
|
||||
|
||||
removeOnComplete: {
|
||||
age: 3600,
|
||||
count: 1000,
|
||||
},
|
||||
|
||||
removeOnFail: false,
|
||||
},
|
||||
});
|
||||
|
||||
// Event listeners
|
||||
documentQueue.on("error", (err) => {
|
||||
console.error("Document Queue Error:", err);
|
||||
});
|
||||
|
||||
documentQueue.on("waiting", (job) => {
|
||||
console.log(`Document Job ${job.id} waiting...`);
|
||||
});
|
||||
|
||||
documentQueue.on("active", (job) => {
|
||||
console.log(`Document Job ${job.id} started...`);
|
||||
});
|
||||
|
||||
documentQueue.on("completed", (job) => {
|
||||
console.log(`Document Job ${job.id} completed`);
|
||||
});
|
||||
|
||||
documentQueue.on("failed", (job, err) => {
|
||||
console.error(
|
||||
`Document Job ${job.id} failed:`,
|
||||
err.message
|
||||
);
|
||||
});
|
||||
|
||||
module.exports = documentQueue;
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Niolla
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is proprietary and confidential.
|
||||
* Unauthorized copying, modification, distribution, or use
|
||||
* of this file, via any medium, is strictly prohibited.
|
||||
*/
|
||||
|
||||
// app/queues/log.queue.js
|
||||
|
||||
const { Queue } = require("bullmq");
|
||||
const connection = require("../config/redisClient");
|
||||
|
||||
const logQueue = new Queue("logQueue", { connection });
|
||||
|
||||
module.exports = logQueue;
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Niolla
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is proprietary and confidential.
|
||||
* Unauthorized copying, modification, distribution, or use
|
||||
* of this file, via any medium, is strictly prohibited.
|
||||
*/
|
||||
|
||||
// app/routes/activity.routes.js
|
||||
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
const activityController = require("../controllers/activity.controller");
|
||||
const {
|
||||
authenticate,
|
||||
} = require("../middleware/auth.middleware");
|
||||
|
||||
const { authorizedAccountType, checkPermission } = require("../middleware/permission.middleware");
|
||||
const PERMISSIONS = require("../constants/permissions");
|
||||
|
||||
|
||||
// Get all user activities
|
||||
router.get(
|
||||
"/",
|
||||
authenticate,
|
||||
authorizedAccountType(["admin"]),
|
||||
activityController.getUserActivities,
|
||||
);
|
||||
|
||||
// Get activity by User ID
|
||||
router.get(
|
||||
"/user/:userId",
|
||||
authenticate,
|
||||
authorizedAccountType(["admin"]),
|
||||
activityController.getActivitiesByUserId,
|
||||
);
|
||||
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Niolla
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is proprietary and confidential.
|
||||
* Unauthorized copying, modification, distribution, or use
|
||||
* of this file, via any medium, is strictly prohibited.
|
||||
*/
|
||||
|
||||
// app/routes/auth.routes.js
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const authController = require('../controllers/auth.controller');
|
||||
const {authenticate} = require('../middleware/auth.middleware');
|
||||
|
||||
// GET /api/auth/me
|
||||
router.get("/me", authenticate, (req, res) => {
|
||||
res.json({
|
||||
authenticated: true,
|
||||
user: req.user
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/req-otp', authController.loginReq);
|
||||
router.post('/login', authController.login);
|
||||
router.post('/logout', authController.logout);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* Markdown Docs Route
|
||||
*/
|
||||
|
||||
const express = require("express");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const docsSession = require("../middleware/docsSession.middleware");
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
|
||||
const DOCS_DIR = path.join(__dirname, "../../Documentation");
|
||||
|
||||
router.get("/markdown-files", docsSession, (req, res) => {
|
||||
fs.readdir(DOCS_DIR, (err, files) => {
|
||||
if (err) {
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "Failed to read documentation folder",
|
||||
error: err.message,
|
||||
});
|
||||
}
|
||||
|
||||
const mdFiles = files.filter((file) => file.endsWith(".md"));
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: mdFiles,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.get("/view/:file", docsSession, (req, res) => {
|
||||
const filePath = path.join(DOCS_DIR, req.params.file);
|
||||
|
||||
if (!req.params.file.endsWith(".md")) {
|
||||
return res.status(400).send("Only markdown files allowed");
|
||||
}
|
||||
|
||||
// basic protection against path traversal
|
||||
if (!filePath.startsWith(DOCS_DIR)) {
|
||||
return res.status(400).send("Invalid path");
|
||||
}
|
||||
|
||||
res.sendFile(filePath);
|
||||
});
|
||||
|
||||
// Docs login (simple)
|
||||
router.post("/login", (req, res) => {
|
||||
const { username, password } = req.body;
|
||||
|
||||
if (
|
||||
username === process.env.DOCS_USER &&
|
||||
password === process.env.DOCS_PASS
|
||||
) {
|
||||
res.cookie("docsAuth", "true", {
|
||||
httpOnly: true,
|
||||
sameSite: "lax",
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
});
|
||||
|
||||
return res.json({ success: true });
|
||||
}
|
||||
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: "Invalid credentials",
|
||||
});
|
||||
});
|
||||
|
||||
router.post("/logout", (req, res) => {
|
||||
res.clearCookie("docsAuth");
|
||||
res.json({ success: true });
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Niolla
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is proprietary and confidential.
|
||||
* Unauthorized copying, modification, distribution, or use
|
||||
* of this file, via any medium, is strictly prohibited.
|
||||
*/
|
||||
|
||||
// app/routes/document.routes.js
|
||||
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
const generateDocumentController = require("../controllers/generateDocument.controller");
|
||||
const {
|
||||
authenticate,
|
||||
} = require("../middleware/auth.middleware");
|
||||
|
||||
const { authorizedAccountType, checkPermission } = require("../middleware/permission.middleware");
|
||||
const PERMISSIONS = require("../constants/permissions");
|
||||
|
||||
|
||||
// Get available document types
|
||||
router.get(
|
||||
"/types",
|
||||
authenticate,
|
||||
authorizedAccountType(["admin", "management", "team_head", "user"]),
|
||||
generateDocumentController.getAvailableDocumentTypes
|
||||
);
|
||||
|
||||
// Get saved documents
|
||||
router.post(
|
||||
"/saved",
|
||||
authenticate,
|
||||
authorizedAccountType(["admin", "management", "team_head", "user"]),
|
||||
generateDocumentController.getSavedDocuments
|
||||
);
|
||||
|
||||
// Generate Document (async - returns jobId immediately)
|
||||
router.post(
|
||||
"/generate",
|
||||
authenticate,
|
||||
authorizedAccountType(["admin", "management", "team_head", "user"]),
|
||||
generateDocumentController.generateDocument
|
||||
);
|
||||
|
||||
router.post(
|
||||
"/draft",
|
||||
authenticate,
|
||||
authorizedAccountType(["admin", "management", "team_head", "user"]),
|
||||
generateDocumentController.generateDraftDocument
|
||||
);
|
||||
|
||||
// Generate Reference Number
|
||||
router.get(
|
||||
"/reference-number/:documentType",
|
||||
authenticate,
|
||||
authorizedAccountType(["admin", "management", "team_head", "user"]),
|
||||
generateDocumentController.generateReferenceNo
|
||||
);
|
||||
|
||||
// Get Job Status
|
||||
router.get(
|
||||
"/job/:jobId/status",
|
||||
authenticate,
|
||||
authorizedAccountType(["admin", "management", "team_head", "user"]),
|
||||
generateDocumentController.getJobStatus
|
||||
);
|
||||
|
||||
// Download Document
|
||||
router.get(
|
||||
"/download/:uuid",
|
||||
authenticate,
|
||||
authorizedAccountType(["admin", "management", "team_head", "user"]),
|
||||
generateDocumentController.downloadDocument
|
||||
);
|
||||
|
||||
// Cancel Job
|
||||
router.delete(
|
||||
"/job/:jobId",
|
||||
authenticate,
|
||||
authorizedAccountType(["admin", "management", "team_head", "user"]),
|
||||
generateDocumentController.cancelJob
|
||||
);
|
||||
|
||||
// Get document details
|
||||
router.get(
|
||||
"/:docId",
|
||||
authenticate,
|
||||
authorizedAccountType(["admin", "management", "team_head", "user"]),
|
||||
generateDocumentController.getDocumentData
|
||||
);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Niolla
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is proprietary and confidential.
|
||||
* Unauthorized copying, modification, distribution, or use
|
||||
* of this file, via any medium, is strictly prohibited.
|
||||
*/
|
||||
|
||||
// app/routes/index.js
|
||||
|
||||
const express = require("express");
|
||||
|
||||
const authRoutes = require("./auth.routes");
|
||||
const userRoutes = require("./user.routes");
|
||||
const activityRoutes = require("./activity.routes");
|
||||
const uploadRoutes = require("./upload.routes");
|
||||
const documentRoutes = require("./document.routes");
|
||||
const docsRoutes = require("./docs.routes");
|
||||
const permissionRoutes = require("./permission.routes");
|
||||
const profileRoutes = require("./profile.routes");
|
||||
const notificationRoutes = require("./notification.routes");
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.use("/auth", authRoutes);
|
||||
router.use("/user", userRoutes);
|
||||
router.use("/activity", activityRoutes);
|
||||
router.use("/upload", uploadRoutes);
|
||||
router.use("/document", documentRoutes);
|
||||
router.use("/docs", docsRoutes);
|
||||
router.use("/profile", profileRoutes);
|
||||
router.use("/notification", notificationRoutes);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Niolla
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is proprietary and confidential.
|
||||
* Unauthorized copying, modification, distribution, or use
|
||||
* of this file, via any medium, is strictly prohibited.
|
||||
*/
|
||||
|
||||
// app/routes/notification.routes.js
|
||||
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
|
||||
const notificationController = require("../controllers/notification.controller");
|
||||
|
||||
const {
|
||||
authenticate,
|
||||
} = require("../middleware/auth.middleware");
|
||||
|
||||
const {
|
||||
authorizedAccountType,
|
||||
checkPermission,
|
||||
} = require("../middleware/permission.middleware");
|
||||
|
||||
const PERMISSIONS = require("../constants/permissions");
|
||||
|
||||
|
||||
// Create notification
|
||||
router.post(
|
||||
"/",
|
||||
authenticate,
|
||||
authorizedAccountType(["admin", "management"]),
|
||||
// checkPermission(PERMISSIONS.NOTIFICATION_CREATE),
|
||||
notificationController.createNotification,
|
||||
);
|
||||
|
||||
|
||||
// Get all announcements
|
||||
router.get(
|
||||
"/announcements",
|
||||
authenticate,
|
||||
authorizedAccountType(["admin", "management", "team_head", "user"]),
|
||||
// checkPermission(PERMISSIONS.NOTIFICATION_VIEW),
|
||||
notificationController.getAnnouncements,
|
||||
);
|
||||
|
||||
|
||||
// Get notifications for a user
|
||||
router.get(
|
||||
"/user/:userId",
|
||||
authenticate,
|
||||
authorizedAccountType(["admin", "management", "team_head", "user"]),
|
||||
// checkPermission(PERMISSIONS.NOTIFICATION_VIEW),
|
||||
notificationController.getUserNotifications,
|
||||
);
|
||||
|
||||
|
||||
// Mark notification as read
|
||||
router.patch(
|
||||
"/user/:userId/notification/:notificationId/read",
|
||||
authenticate,
|
||||
authorizedAccountType(["admin", "management", "team_head", "user"]),
|
||||
// checkPermission(PERMISSIONS.NOTIFICATION_READ),
|
||||
notificationController.markAsRead,
|
||||
);
|
||||
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,145 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Niolla
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is proprietary and confidential.
|
||||
* Unauthorized copying, modification, distribution, or use
|
||||
* of this file, via any medium, is strictly prohibited.
|
||||
*/
|
||||
|
||||
// app/routes/permission.routes.js
|
||||
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
const permissionController = require("../controllers/permission.controller");
|
||||
const { authenticate } = require("../middleware/auth.middleware");
|
||||
const { authorizedAccountType } = require("../middleware/permission.middleware");
|
||||
|
||||
// Permission routes
|
||||
router.post(
|
||||
"/",
|
||||
authenticate,
|
||||
authorizedAccountType(["admin"]),
|
||||
permissionController.createPermission
|
||||
);
|
||||
|
||||
router.post(
|
||||
"/bulk",
|
||||
authenticate,
|
||||
authorizedAccountType(["admin"]),
|
||||
permissionController.bulkCreatePermissions
|
||||
);
|
||||
|
||||
router.get(
|
||||
"/",
|
||||
authenticate,
|
||||
permissionController.getAllPermissions
|
||||
);
|
||||
|
||||
router.get(
|
||||
"/:permissionId",
|
||||
authenticate,
|
||||
permissionController.getPermissionById
|
||||
);
|
||||
|
||||
router.put(
|
||||
"/:permissionId",
|
||||
authenticate,
|
||||
authorizedAccountType(["admin"]),
|
||||
permissionController.updatePermission
|
||||
);
|
||||
|
||||
router.delete(
|
||||
"/:permissionId",
|
||||
authenticate,
|
||||
authorizedAccountType(["admin"]),
|
||||
permissionController.deletePermission
|
||||
);
|
||||
|
||||
// Role routes
|
||||
router.post(
|
||||
"/roles",
|
||||
authenticate,
|
||||
authorizedAccountType(["admin"]),
|
||||
permissionController.createRole
|
||||
);
|
||||
|
||||
router.get(
|
||||
"/roles",
|
||||
authenticate,
|
||||
permissionController.getAllRoles
|
||||
);
|
||||
|
||||
router.get(
|
||||
"/roles/:roleId",
|
||||
authenticate,
|
||||
permissionController.getRoleById
|
||||
);
|
||||
|
||||
router.put(
|
||||
"/roles/:roleId",
|
||||
authenticate,
|
||||
authorizedAccountType(["admin"]),
|
||||
permissionController.updateRole
|
||||
);
|
||||
|
||||
router.delete(
|
||||
"/roles/:roleId",
|
||||
authenticate,
|
||||
authorizedAccountType(["admin"]),
|
||||
permissionController.deleteRole
|
||||
);
|
||||
|
||||
// Role-Permission assignment route
|
||||
router.post(
|
||||
"/roles/:roleId/permissions",
|
||||
authenticate,
|
||||
authorizedAccountType(["admin"]),
|
||||
permissionController.createRolePermission
|
||||
);
|
||||
|
||||
// Bulk Role-Permission assignment
|
||||
router.post(
|
||||
"/roles/permissions/bulk",
|
||||
authenticate,
|
||||
authorizedAccountType(["admin"]),
|
||||
permissionController.bulkCreateRolePermissions
|
||||
);
|
||||
|
||||
// User permissions route
|
||||
router.get(
|
||||
"/users/:userId/permissions",
|
||||
authenticate,
|
||||
permissionController.getUserPermissions
|
||||
);
|
||||
|
||||
router.post(
|
||||
"/users/permissions",
|
||||
authenticate,
|
||||
authorizedAccountType(["admin"]),
|
||||
permissionController.createUserPermission
|
||||
);
|
||||
|
||||
// Bulk User Permissions
|
||||
router.post(
|
||||
"/users/permissions/bulk",
|
||||
authenticate,
|
||||
authorizedAccountType(["admin"]),
|
||||
permissionController.bulkCreateUserPermissions
|
||||
);
|
||||
|
||||
router.put(
|
||||
"/users/permissions/:upId",
|
||||
authenticate,
|
||||
authorizedAccountType(["admin"]),
|
||||
permissionController.updateUserPermission
|
||||
);
|
||||
|
||||
router.delete(
|
||||
"/users/permissions/:upId",
|
||||
authenticate,
|
||||
authorizedAccountType(["admin"]),
|
||||
permissionController.deleteUserPermission
|
||||
);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Niolla
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is proprietary and confidential.
|
||||
* Unauthorized copying, modification, distribution, or use
|
||||
* of this file, via any medium, is strictly prohibited.
|
||||
*/
|
||||
|
||||
// app/routes/profile.routes.js
|
||||
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
const profileController = require("../controllers/profile.controller.js");
|
||||
const { authenticate } = require("../middleware/auth.middleware");
|
||||
|
||||
const {
|
||||
authorizedAccountType,
|
||||
checkPermission,
|
||||
} = require("../middleware/permission.middleware");
|
||||
const PERMISSIONS = require("../constants/permissions");
|
||||
|
||||
router.post("/req-reset-password", profileController.requestPasswordReset);
|
||||
|
||||
router.post("/reset-password", profileController.resetPassword);
|
||||
|
||||
router.post(
|
||||
"/change-password",
|
||||
authenticate,
|
||||
authorizedAccountType(["admin", "management", "team_head", "user"]),
|
||||
profileController.changePassword,
|
||||
);
|
||||
|
||||
router.get(
|
||||
"/avatar/:userId",
|
||||
authenticate,
|
||||
authorizedAccountType(["admin", "management", "team_head", "user"]),
|
||||
profileController.getProfileAvatar,
|
||||
);
|
||||
|
||||
router.get(
|
||||
"/background/:userId",
|
||||
authenticate,
|
||||
authorizedAccountType(["admin", "management", "team_head", "user"]),
|
||||
profileController.getProfileBackgroundImage,
|
||||
);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Niolla
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is proprietary and confidential.
|
||||
* Unauthorized copying, modification, distribution, or use
|
||||
* of this file, via any medium, is strictly prohibited.
|
||||
*/
|
||||
|
||||
// app/routes/upload.routes.js
|
||||
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
const uploadController = require("../controllers/upload.controller");
|
||||
const { uploadSingle } = require("../middleware/upload.middleware");
|
||||
const {
|
||||
authenticate,
|
||||
} = require("../middleware/auth.middleware");
|
||||
|
||||
const { authorizedAccountType, checkPermission } = require("../middleware/permission.middleware");
|
||||
const PERMISSIONS = require("../constants/permissions");
|
||||
|
||||
// Upload file
|
||||
router.post(
|
||||
"/",
|
||||
authenticate,
|
||||
authorizedAccountType(["admin", "staff"]),
|
||||
uploadSingle("file"),
|
||||
uploadController.uploadFile,
|
||||
);
|
||||
|
||||
// Get signed URL for a file
|
||||
router.get(
|
||||
"/signed-url/:id",
|
||||
authenticate,
|
||||
authorizedAccountType(["admin", "staff"]),
|
||||
uploadController.getFileUrl,
|
||||
);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Niolla
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is proprietary and confidential.
|
||||
* Unauthorized copying, modification, distribution, or use
|
||||
* of this file, via any medium, is strictly prohibited.
|
||||
*/
|
||||
|
||||
// app/routes/user.routes.js
|
||||
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
const userController = require("../controllers/user.controller");
|
||||
const {
|
||||
authenticate,
|
||||
} = require("../middleware/auth.middleware");
|
||||
|
||||
const { authorizedAccountType, checkPermission } = require("../middleware/permission.middleware");
|
||||
const PERMISSIONS = require("../constants/permissions");
|
||||
|
||||
|
||||
router.post(
|
||||
"/",
|
||||
authenticate,
|
||||
authorizedAccountType(["admin"]),
|
||||
userController.createNewUser
|
||||
);
|
||||
|
||||
router.get(
|
||||
"/",
|
||||
authenticate,
|
||||
authorizedAccountType(["admin"]),
|
||||
userController.getAllUsers
|
||||
);
|
||||
|
||||
router.get(
|
||||
"/:id",
|
||||
authenticate,
|
||||
authorizedAccountType(["admin", "management", "team_head", "user"]),
|
||||
userController.getUserById
|
||||
);
|
||||
|
||||
router.patch(
|
||||
"/:id",
|
||||
authenticate,
|
||||
authorizedAccountType(["admin", "management", "team_head", "user"]),
|
||||
userController.updateUser
|
||||
);
|
||||
|
||||
router.delete(
|
||||
"/:id",
|
||||
authenticate,
|
||||
authorizedAccountType(["admin"]),
|
||||
userController.deleteUser
|
||||
);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Niolla
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is proprietary and confidential.
|
||||
* Unauthorized copying, modification, distribution, or use
|
||||
* of this file, via any medium, is strictly prohibited.
|
||||
*/
|
||||
|
||||
// app/seeders/documentType.seeder.js
|
||||
|
||||
const db = require("../models");
|
||||
|
||||
const seedDocumentTypes = async () => {
|
||||
try {
|
||||
const types = [
|
||||
{ doc_type_name: "DOC_TYPE_HERE_01", description: "N/A" },
|
||||
{ doc_type_name: "DOC_TYPE_HERE_02", description: "N/A" },
|
||||
];
|
||||
|
||||
const existingDocuments = await db.DocumentType.findAll({
|
||||
attributes: ["doc_type_name"],
|
||||
});
|
||||
|
||||
const existingNames = existingDocuments.map((doc) => doc.doc_type_name);
|
||||
|
||||
const newDocuments = types.filter(
|
||||
(dt) => !existingNames.includes(dt.doc_type_name)
|
||||
);
|
||||
|
||||
if (newDocuments.length > 0) {
|
||||
await db.DocumentType.bulkCreate(newDocuments);
|
||||
|
||||
newDocuments.forEach((dt) =>
|
||||
console.log(`Document type inserted: ${dt.doc_type_name}`)
|
||||
);
|
||||
} else {
|
||||
console.log("No new document types to insert");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Document type seeding failed:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = seedDocumentTypes;
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Niolla
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is proprietary and confidential.
|
||||
* Unauthorized copying, modification, distribution, or use
|
||||
* of this file, via any medium, is strictly prohibited.
|
||||
*/
|
||||
|
||||
// app/seeders/index.js
|
||||
|
||||
const db = require("../models");
|
||||
|
||||
// Import all seeders
|
||||
const seedUsers = require("./user.seeder");
|
||||
const seedDocumentTypes = require("./documentType.seeder");
|
||||
const {log} = require("../utils/consoleLog.utill");
|
||||
|
||||
|
||||
const runSeeders = async () => {
|
||||
try {
|
||||
log("Starting database seeding...");
|
||||
|
||||
await seedUsers();
|
||||
await seedDocumentTypes();
|
||||
log("All seeders executed successfully");
|
||||
} catch (error) {
|
||||
log("Seeding process failed:", error.message);
|
||||
} finally {
|
||||
await db.sequelize.close();
|
||||
process.exit();
|
||||
}
|
||||
};
|
||||
|
||||
runSeeders();
|
||||
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Niolla
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is proprietary and confidential.
|
||||
* Unauthorized copying, modification, distribution, or use
|
||||
* of this file, via any medium, is strictly prohibited.
|
||||
*/
|
||||
|
||||
// app/seeders/user.seeder.js
|
||||
|
||||
const bcrypt = require("bcrypt");
|
||||
const { v4: uuidv4 } = require("uuid");
|
||||
|
||||
const db = require("../models");
|
||||
|
||||
const seedUsers = async () => {
|
||||
const transaction = await db.sequelize.transaction();
|
||||
|
||||
try {
|
||||
const users = [
|
||||
{
|
||||
firstName: "System",
|
||||
lastName: "Administrator",
|
||||
email: "team@niolla.lk",
|
||||
password: "Admin@123",
|
||||
accountType: "admin",
|
||||
role: "Administrator",
|
||||
roleID: "admin",
|
||||
department: "Administration",
|
||||
profile: {
|
||||
theme: "light",
|
||||
notificationsEnabled: true,
|
||||
dob: null,
|
||||
phone_number: null,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
for (const userData of users) {
|
||||
const existingUser = await db.User.findOne({
|
||||
where: {
|
||||
email: userData.email,
|
||||
},
|
||||
transaction,
|
||||
});
|
||||
|
||||
if (existingUser) {
|
||||
console.log(`User already exists: ${userData.email}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const hashedPassword = await bcrypt.hash(userData.password, 12);
|
||||
|
||||
const userId = `usr_${uuidv4()}`;
|
||||
const profileId = `prf_${uuidv4()}`;
|
||||
|
||||
const newUser = await db.User.create(
|
||||
{
|
||||
id: userId,
|
||||
firstName: userData.firstName,
|
||||
lastName: userData.lastName,
|
||||
email: userData.email,
|
||||
password: hashedPassword,
|
||||
accountType: userData.accountType,
|
||||
role: userData.role,
|
||||
roleID: userData.roleID,
|
||||
department: userData.department,
|
||||
},
|
||||
{
|
||||
transaction,
|
||||
},
|
||||
);
|
||||
|
||||
await db.Profile.create(
|
||||
{
|
||||
profile_id: profileId,
|
||||
user_id: newUser.id,
|
||||
theme: userData.profile.theme,
|
||||
notificationsEnabled: userData.profile.notificationsEnabled,
|
||||
dob: userData.profile.dob,
|
||||
phone_number: userData.profile.phone_number,
|
||||
},
|
||||
{
|
||||
transaction,
|
||||
},
|
||||
);
|
||||
|
||||
console.log(
|
||||
`User inserted: ${newUser.firstName} ${newUser.lastName} (${newUser.email})`,
|
||||
);
|
||||
}
|
||||
|
||||
await transaction.commit();
|
||||
|
||||
console.log("User seeding completed successfully");
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
|
||||
console.error("User seeding failed:", error);
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = seedUsers;
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Niolla
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is proprietary and confidential.
|
||||
* Unauthorized copying, modification, distribution, or use
|
||||
* of this file, via any medium, is strictly prohibited.
|
||||
*/
|
||||
|
||||
// app/services/activity.service.js
|
||||
|
||||
const activityQueue = require("../queues/activity.queue");
|
||||
|
||||
const logActivity = async ({
|
||||
user,
|
||||
description,
|
||||
type = "ACTION",
|
||||
module
|
||||
}) => {
|
||||
try {
|
||||
await activityQueue.add("log-activity", {
|
||||
user_id: user.id || user.user_id,
|
||||
username: user.firstName,
|
||||
activity_description: description,
|
||||
module: module,
|
||||
activity_type: type,
|
||||
activity_time: new Date(),
|
||||
activity_date: new Date().toISOString().split("T")[0]
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Queue push error:", err.message);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = { logActivity };
|
||||
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Niolla
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is proprietary and confidential.
|
||||
* Unauthorized copying, modification, distribution, or use
|
||||
* of this file, via any medium, is strictly prohibited.
|
||||
*/
|
||||
|
||||
// app/services/permission.service.js
|
||||
|
||||
const db = require("../models");
|
||||
const User = db.User;
|
||||
const RolePermission = db.rolePermission;
|
||||
const UserPermission = db.userPermission;
|
||||
|
||||
const {
|
||||
getCachedPermissions,
|
||||
setCachedPermissions
|
||||
} = require("../utils/cache.util");
|
||||
|
||||
const getEffectivePermissions = async (userId) => {
|
||||
try {
|
||||
const cached = await getCachedPermissions(userId);
|
||||
if (cached) {
|
||||
console.log("⚡ PERMISSION CACHE HIT");
|
||||
return cached;
|
||||
}
|
||||
|
||||
console.log("🐢 PERMISSION CACHE MISS");
|
||||
|
||||
const user = await User.findByPk(userId);
|
||||
if (!user) return [];
|
||||
|
||||
const rolePermissions = await RolePermission.findAll({
|
||||
where: { role_id: user.roleID },
|
||||
attributes: ["permission_id"]
|
||||
});
|
||||
|
||||
const userPermissions = await UserPermission.findAll({
|
||||
where: { user_id: userId },
|
||||
attributes: ["permission_id"]
|
||||
});
|
||||
|
||||
const map = {};
|
||||
|
||||
// role permissions
|
||||
rolePermissions.forEach((rp) => {
|
||||
map[rp.permission_id] = true;
|
||||
});
|
||||
|
||||
// user overrides (just add)
|
||||
userPermissions.forEach((up) => {
|
||||
map[up.permission_id] = true;
|
||||
});
|
||||
|
||||
const finalPermissions = Object.keys(map);
|
||||
|
||||
console.log("FINAL PERMS:", finalPermissions);
|
||||
|
||||
await setCachedPermissions(userId, finalPermissions);
|
||||
|
||||
return finalPermissions;
|
||||
|
||||
} catch (err) {
|
||||
console.error("PERMISSION SERVICE ERROR:", err);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = { getEffectivePermissions };
|
||||
@@ -0,0 +1,372 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@page {
|
||||
size: A4;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
background: white;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.sheet {
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border: 1px solid #222;
|
||||
}
|
||||
|
||||
.company-band {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1.35fr 0.95fr;
|
||||
min-height: 112px;
|
||||
border-bottom: 1px solid #444;
|
||||
}
|
||||
|
||||
.company-left,
|
||||
.company-middle,
|
||||
.company-right {
|
||||
padding: 4px 6px;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.company-left {
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.company-middle {
|
||||
text-align: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.logo-placeholder {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 6px;
|
||||
color: #1c5c9d;
|
||||
}
|
||||
|
||||
.company-middle .tagline {
|
||||
margin-top: 4px;
|
||||
font-size: 10px;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.company-right {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-end;
|
||||
text-align: right;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.company-right .stack {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.title-row {
|
||||
border-bottom: 1px solid #444;
|
||||
text-align: center;
|
||||
padding: 5px 0 4px;
|
||||
color: #7da3de;
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.bill-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1.55fr 1fr;
|
||||
border-bottom: 1px solid #444;
|
||||
min-height: 72px;
|
||||
}
|
||||
|
||||
.bill-left,
|
||||
.bill-right {
|
||||
padding: 4px 6px;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.bill-left {
|
||||
border-right: 1px solid #444;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.bill-right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 2px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.bill-line {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.meta-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
border-bottom: 1px solid #444;
|
||||
}
|
||||
|
||||
.meta-col {
|
||||
padding: 4px 6px;
|
||||
font-size: 10px;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.meta-col:first-child {
|
||||
border-right: 1px solid #444;
|
||||
}
|
||||
|
||||
.meta-line {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.meta-label {
|
||||
font-weight: 700;
|
||||
min-width: 112px;
|
||||
}
|
||||
|
||||
.items-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
.items-table th,
|
||||
.items-table td {
|
||||
border: 1px solid #444;
|
||||
font-size: 10px;
|
||||
padding: 2px 3px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.items-table th {
|
||||
text-align: center;
|
||||
font-weight: 700;
|
||||
background: #b7d2ea;
|
||||
}
|
||||
|
||||
.no-col { width: 5%; }
|
||||
.desc-col { width: 37%; }
|
||||
.remarks-col { width: 33%; }
|
||||
.amount-col { width: 12%; }
|
||||
.confirm-col { width: 13%; }
|
||||
|
||||
.center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.spacer-row td {
|
||||
height: 26px;
|
||||
}
|
||||
|
||||
.total-row td {
|
||||
font-weight: 700;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.approval-area {
|
||||
height: 94px;
|
||||
border-top: 1px solid #444;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: flex-end;
|
||||
padding: 10px 12px 14px;
|
||||
}
|
||||
|
||||
.approval-box {
|
||||
text-align: center;
|
||||
min-width: 230px;
|
||||
}
|
||||
|
||||
.signature-line {
|
||||
margin-top: 18px;
|
||||
border-top: 1px solid #222;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.footer-row {
|
||||
border-top: 1px solid #444;
|
||||
text-align: center;
|
||||
padding: 4px 0 5px;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.watermark-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.line-dots {
|
||||
letter-spacing: 3px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.company-logo-text {
|
||||
text-align: center;
|
||||
color: #1c5c9d;
|
||||
font-size: 14px;
|
||||
line-height: 1.1;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.company-logo-small {
|
||||
font-size: 10px;
|
||||
font-weight: 400;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.header-stack {
|
||||
line-height: 1.15;
|
||||
}
|
||||
|
||||
.small-note {
|
||||
font-size: 9px;
|
||||
color: #444;
|
||||
}
|
||||
|
||||
.highlight {
|
||||
background: #ffeb3b;
|
||||
padding: 0 2px;
|
||||
}
|
||||
|
||||
.multi-line {
|
||||
white-space: pre-line;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<%
|
||||
const creditCompany = typeof company !== 'undefined' && company ? company : {};
|
||||
const creditHeader = typeof header !== 'undefined' && header ? header : {};
|
||||
const creditBillTo = typeof billTo !== 'undefined' && billTo ? billTo : {};
|
||||
const creditMeta = typeof meta !== 'undefined' && meta ? meta : {};
|
||||
const creditVessel = typeof vessel !== 'undefined' && vessel ? vessel : {};
|
||||
const creditOrderMeta = typeof orderMeta !== 'undefined' && orderMeta ? orderMeta : {};
|
||||
const creditItems = typeof items !== 'undefined' && Array.isArray(items) ? items : [];
|
||||
const creditApprovedByLabel = typeof approvedByLabel !== 'undefined' && approvedByLabel ? approvedByLabel : 'Approved by GLMS Finance Dept.';
|
||||
const creditDepartmentLabel = typeof departmentLabel !== 'undefined' && departmentLabel ? departmentLabel : 'GLMS - Accounts Department';
|
||||
%>
|
||||
<div class="sheet">
|
||||
<div class="company-band">
|
||||
<div class="company-left">
|
||||
<div><strong><%= creditCompany.name || 'GREEK-LANKA MARITIME SERVICES (PVT) LTD' %></strong></div>
|
||||
<div><%= creditCompany.addressLine1 || '' %></div>
|
||||
<div><%= creditCompany.addressLine2 || '' %></div>
|
||||
<div><%= creditCompany.addressLine3 || '' %></div>
|
||||
<div><%= creditCompany.postal || '' %></div>
|
||||
<div><%= creditCompany.contact || '' %></div>
|
||||
<div><%= creditCompany.br || '' %></div>
|
||||
<div><%= creditCompany.license || '' %></div>
|
||||
</div>
|
||||
<div class="company-middle">
|
||||
<div>
|
||||
<div class="company-logo-text">GREEK<br/>LANKA</div>
|
||||
<div class="company-logo-small">We Serve the Ocean</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="company-right">
|
||||
<div class="stack">
|
||||
<div class="header-stack">
|
||||
<div style="font-size: 14px; font-weight:700; color:#7da3de; letter-spacing:0.5px;">CREDIT NOTE</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="title-row"><%= creditHeader.title || 'CREDIT NOTE' %></div>
|
||||
|
||||
<div class="bill-row">
|
||||
<div class="bill-left">
|
||||
<div><strong>Bill To :</strong> <%= creditBillTo.name || 'Master & Owner of MV TEAM VENTURES' %></div>
|
||||
<div class="multi-line"><%- (creditBillTo.details || '').replace(/\n/g, '<br/>') || 'VRIDHI MARITIME SHIP MANAGEMENT & OPERATION LLC<br/>Office No: 2004, The Prism Tower, Dubai' %></div>
|
||||
</div>
|
||||
<div class="bill-right">
|
||||
<div class="bill-line"><span><strong>CRN NO :</strong></span><span><%= creditMeta.crnNo || 'N/A' %></span></div>
|
||||
<div class="bill-line"><span><strong>DATE :</strong></span><span><%= creditMeta.date || 'N/A' %></span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="meta-row">
|
||||
<div class="meta-col">
|
||||
<div class="meta-line"><span class="meta-label">Vessel Name :</span> <span><%= creditVessel.name || 'N/A' %></span></div>
|
||||
<div class="meta-line"><span class="meta-label">Port :</span> <span><%= creditVessel.port || 'N/A' %></span></div>
|
||||
<div class="meta-line"><span class="meta-label">Name of Agent :</span> <span><%= creditVessel.agent || 'N/A' %></span></div>
|
||||
</div>
|
||||
<div class="meta-col">
|
||||
<div class="meta-line"><span class="meta-label">GRT :</span> <span><%= creditVessel.grt || 'N/A' %></span></div>
|
||||
<div class="meta-line"><span class="meta-label">IMO Number :</span> <span><%= creditVessel.imo || 'N/A' %></span></div>
|
||||
<div class="meta-line"><span class="meta-label">Port/Country :</span> <span><%= creditVessel.portCountry || 'N/A' %></span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="overflow-x:auto;">
|
||||
<table class="items-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="no-col">DESCRIPTION</th>
|
||||
<th class="amount-col">AMOUNT</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% if (creditItems.length > 0) { %>
|
||||
<% creditItems.forEach((item, index) => { %>
|
||||
<tr class="spacer-row">
|
||||
<td>
|
||||
<%= item.description || item.desc || '' %>
|
||||
<% if (item.note) { %>
|
||||
<div class="small-note"><%= item.note %></div>
|
||||
<% } %>
|
||||
</td>
|
||||
<td class="right"><%= (typeof item.amount !== 'undefined') ? item.amount : '' %></td>
|
||||
</tr>
|
||||
<% }); %>
|
||||
<% } %>
|
||||
<tr class="total-row">
|
||||
<td class="right">Total Amount</td>
|
||||
<td class="right"><%= total || totalAmount || 0 %></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="approval-area">
|
||||
<div class="approval-box">
|
||||
<div style="height: 42px;"></div>
|
||||
<div class="signature-line"></div>
|
||||
<div style="margin-top: 4px;"><%= creditApprovedByLabel %></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer-row">
|
||||
<%= creditDepartmentLabel %>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Niolla
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is proprietary and confidential.
|
||||
* Unauthorized copying, modification, distribution, or use
|
||||
* of this file, via any medium, is strictly prohibited.
|
||||
*/
|
||||
|
||||
// app/templates/documents/creditNote/creditNote.template.js
|
||||
|
||||
const { log } = require("../../../utils/consoleLog.utill");
|
||||
|
||||
module.exports = async (data) => {
|
||||
log("📋 Preparing Credit Note data for PDF...");
|
||||
|
||||
let payload = data;
|
||||
|
||||
if (typeof payload === "string") {
|
||||
try {
|
||||
payload = JSON.parse(payload);
|
||||
} catch (error) {
|
||||
payload = {};
|
||||
}
|
||||
}
|
||||
|
||||
const source = payload.documentData || payload.document_data || payload.data || payload;
|
||||
|
||||
const pick = (...keys) => {
|
||||
for (const key of keys) {
|
||||
const value = source?.[key];
|
||||
if (value !== undefined && value !== null && value !== "") {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
};
|
||||
|
||||
// Normalize incoming fields and provide defaults matching the screenshot
|
||||
const company = {
|
||||
name: pick("companyName", "company_name") || "GREEK-LANKA MARITIME SERVICES (PVT) LTD",
|
||||
addressLine1: pick("companyAddressLine1", "company_address_line1") || "No. 56/2, Dharmapala Mw, Kotte",
|
||||
addressLine2: pick("companyAddressLine2", "company_address_line2") || "Sri Jayewardenepura",
|
||||
addressLine3: pick("companyAddressLine3", "company_address_line3") || "Sri Lanka",
|
||||
postal: pick("companyPostal", "company_postal") || "Postal Code : 10100",
|
||||
contact: pick("companyContact", "company_contact") || "Tel:+94 11 2083206 / Mobile (24/7) : +94 777 232 271",
|
||||
br: pick("companyBR", "company_br") || "BR No : PV 0022630",
|
||||
license: pick("companyLicense", "company_license") || "License No : SA00317-2024",
|
||||
logoUrl: pick("logoUrl", "logo_url") ||
|
||||
"https://res.cloudinary.com/dtthuvvir/image/upload/v1780748661/company_logo_transperent_bg_uullzw.png",
|
||||
};
|
||||
|
||||
const header = {
|
||||
title: pick("title") || "CREDIT NOTE",
|
||||
};
|
||||
|
||||
const billTo = {
|
||||
name: pick("billToName", "bill_to_name") || "Master & Owner of MV TEAM VENTURES",
|
||||
details: pick("billToDetails", "bill_to_details") || "VRIDHI MARITIME SHIP MANAGEMENT & OPERATION LLC\nOffice No: 2004, The Prism Tower, Dubai",
|
||||
};
|
||||
|
||||
const meta = {
|
||||
crnNo: pick("crnNo", "crn_no", "creditNoteNumber", "credit_note_number", "reference_no") || "GLMS/COLOMBO/474/CRN01",
|
||||
date: pick("date") || new Date().toLocaleDateString(),
|
||||
};
|
||||
|
||||
const vessel = {
|
||||
name: pick("vesselName", "vessel_name") || 'MV "TEAM VENTURES"',
|
||||
grt: pick("grt", "GRT") || "25,543 MT",
|
||||
port: pick("port", "port_name") || "REPAIRS AT COLOMBO DOCK YARD",
|
||||
imo: pick("imo", "imo_number", "imoNumber") || "9339765",
|
||||
agent: pick("nameOfAgent", "name_of_agent", "agent") || "",
|
||||
portCountry: pick("portCountry", "port_country") || "COLOMBO / SRI LANKA",
|
||||
};
|
||||
|
||||
const orderMeta = {
|
||||
orderDate: pick("orderDate", "order_date") || meta.date,
|
||||
orderNo: pick("orderNo", "order_no") || "",
|
||||
portReference: pick("portReference", "port_reference") || "",
|
||||
};
|
||||
|
||||
// Items: array of { description, amount }
|
||||
const items = (Array.isArray(source.items) ? source.items : []).map((it) => ({
|
||||
description: it.description || it.desc || it.item_name || '',
|
||||
amount: Number(it.amount || it.value || it.total || it.totalAmount || 0),
|
||||
quantity: it.quantity ?? it.qty ?? '',
|
||||
note: it.note || it.notes || '',
|
||||
}));
|
||||
|
||||
const computedTotal = items.reduce((s, i) => s + (i.amount || 0), 0);
|
||||
|
||||
const templateData = {
|
||||
company,
|
||||
header,
|
||||
billTo,
|
||||
meta,
|
||||
vessel,
|
||||
orderMeta,
|
||||
items,
|
||||
totalAmount: Number(pick("totalAmount", "total") || computedTotal),
|
||||
approvedBy: pick("approvedBy", "approved_by") || 'GLMS Finance Dept.',
|
||||
approvedByLabel: pick("approvedByLabel", "approved_by_label") || 'Approved by GLMS Finance Dept.',
|
||||
departmentLabel: pick("departmentLabel", "department_label") || 'GLMS - Accounts Department',
|
||||
};
|
||||
|
||||
// Top-level conveniences for templates and pdfRenderer
|
||||
templateData.logoUrl = company.logoUrl;
|
||||
templateData.creditNoteNumber = templateData.meta.crnNo;
|
||||
templateData.date = templateData.meta.date;
|
||||
templateData.billToName = templateData.billTo.name;
|
||||
templateData.billToDetails = templateData.billTo.details;
|
||||
templateData.vessel = templateData.vessel;
|
||||
// Provide invoiceNumber if present
|
||||
templateData.invoiceNumber = pick("invoiceNumber", "invoice_number", "invoice_no") || '';
|
||||
|
||||
// Provide subtotal/tax/discount/total for EJS summary table compatibility
|
||||
templateData.subtotal = Number(pick("subtotal", "sub_total") || computedTotal);
|
||||
templateData.tax = Number(pick("tax") || 0);
|
||||
templateData.discount = Number(pick("discount") || 0);
|
||||
templateData.total = Number(pick("totalAmount", "total") || templateData.subtotal + templateData.tax - templateData.discount);
|
||||
// Provide reason / notes
|
||||
templateData.reason = pick("reason", "remarks", "notes") || '';
|
||||
// Provide paymentTerms and notes for EJS
|
||||
templateData.paymentTerms = pick("paymentTerms", "payment_terms") || '';
|
||||
templateData.notes = pick("notes", "note") || '';
|
||||
|
||||
log("✅ Credit Note data prepared", JSON.stringify(templateData));
|
||||
|
||||
return templateData;
|
||||
};
|
||||
@@ -0,0 +1,266 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@page {
|
||||
size: A4;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
background: #fff;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.sheet {
|
||||
width: 100%;
|
||||
border: 1px solid #222;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.top-band {
|
||||
display: grid;
|
||||
grid-template-columns: 1.45fr 1fr;
|
||||
border-bottom: 1px solid #444;
|
||||
min-height: 76px;
|
||||
}
|
||||
|
||||
.top-left,
|
||||
.top-right {
|
||||
padding: 4px 6px;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.top-right {
|
||||
text-align: right;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.top-line {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.highlight-box {
|
||||
display: grid;
|
||||
grid-template-columns: 1.6fr 1fr;
|
||||
border-bottom: 1px solid #444;
|
||||
}
|
||||
|
||||
.highlight-left,
|
||||
.highlight-right {
|
||||
padding: 4px 6px;
|
||||
min-height: 58px;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.highlight-left {
|
||||
border-right: 1px solid #444;
|
||||
}
|
||||
|
||||
.highlight-row {
|
||||
display: grid;
|
||||
grid-template-columns: 155px 1fr;
|
||||
gap: 6px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.permission-line {
|
||||
text-align: center;
|
||||
padding: 10px 8px 4px;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.dots {
|
||||
text-align: center;
|
||||
letter-spacing: 3px;
|
||||
color: #555;
|
||||
padding: 2px 0 8px;
|
||||
}
|
||||
|
||||
.company-name {
|
||||
text-align: center;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
padding: 6px 0 10px;
|
||||
}
|
||||
|
||||
.company-subtitle {
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.date-row {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
border-bottom: 1px solid #444;
|
||||
padding: 0 6px 4px;
|
||||
}
|
||||
|
||||
.date-box {
|
||||
min-width: 138px;
|
||||
background: #ffeb3b;
|
||||
font-weight: 700;
|
||||
padding: 2px 8px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.form-code {
|
||||
font-size: 9px;
|
||||
padding: 2px 4px 3px;
|
||||
}
|
||||
|
||||
.items-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
.items-table th,
|
||||
.items-table td {
|
||||
border: 1px solid #444;
|
||||
font-size: 10px;
|
||||
padding: 2px 3px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.items-table th {
|
||||
text-align: center;
|
||||
font-weight: 700;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.items-table thead th {
|
||||
height: 33px;
|
||||
}
|
||||
|
||||
.no-col { width: 8%; }
|
||||
.qty-col { width: 7%; }
|
||||
.unit-col { width: 7%; }
|
||||
.description-col { width: 40%; }
|
||||
.rate-col { width: 13%; }
|
||||
.total-col { width: 12%; }
|
||||
.confirm-col { width: 13%; }
|
||||
|
||||
.center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.spacer-row td {
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.muted {
|
||||
color: #666;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<%
|
||||
const customTitle = typeof title !== 'undefined' && title ? title : 'CUSTOM DOCUMENT';
|
||||
const customDate = typeof date !== 'undefined' && date ? date : 'N/A';
|
||||
const customTelePhone = typeof telePhone !== 'undefined' && telePhone ? telePhone : '';
|
||||
const customEmail = typeof emailAddress !== 'undefined' && emailAddress ? emailAddress : '';
|
||||
const customOfficeAddressLine1 = typeof officeAddressLine1 !== 'undefined' && officeAddressLine1 ? officeAddressLine1 : '';
|
||||
const customOfficeAddressLine2 = typeof officeAddressLine2 !== 'undefined' && officeAddressLine2 ? officeAddressLine2 : '';
|
||||
const customOfficeAddressLine3 = typeof officeAddressLine3 !== 'undefined' && officeAddressLine3 ? officeAddressLine3 : '';
|
||||
const customDirectorOfCustoms = typeof directorOfCustoms !== 'undefined' && directorOfCustoms ? directorOfCustoms : '';
|
||||
const customVehicleNo = typeof vehicleNo !== 'undefined' && vehicleNo ? vehicleNo : '';
|
||||
const customPermitNo = typeof permitNo !== 'undefined' && permitNo ? permitNo : '';
|
||||
const customChiefSecurityOfficer = typeof chiefSecurityOfficer !== 'undefined' && chiefSecurityOfficer ? chiefSecurityOfficer : '';
|
||||
const customExportGate = typeof exportGate !== 'undefined' && exportGate ? exportGate : '';
|
||||
const customSlpa = typeof slpa !== 'undefined' && slpa ? slpa : '';
|
||||
const customGateNo = typeof gateNo !== 'undefined' && gateNo ? gateNo : '';
|
||||
const customPermissionText = typeof permissionText !== 'undefined' && permissionText ? permissionText : '';
|
||||
const customCompanyName = typeof companyName !== 'undefined' && companyName ? companyName : 'Oceanic Maritime Solutions (Pvt) Ltd';
|
||||
const customSubtitle = typeof subtitle !== 'undefined' && subtitle ? subtitle : 'LICENSED SHIPCHANDLERS';
|
||||
const customSectionCode = typeof sectionCode !== 'undefined' && sectionCode ? sectionCode : 'SC/FORM/06';
|
||||
const customDateLabel = typeof dateLabel !== 'undefined' && dateLabel ? dateLabel : 'DATE :';
|
||||
const customItems = typeof items !== 'undefined' && Array.isArray(items) ? items : [];
|
||||
const rows = Math.max(7, customItems.length);
|
||||
%>
|
||||
<div class="sheet">
|
||||
<div class="top-band">
|
||||
<div class="top-left">
|
||||
<div class="top-line"><span class="label">Tele :</span> <%= customTelePhone || '' %></div>
|
||||
<div class="top-line"><span class="label">E-mail Address :</span> <%= customEmail || '' %></div>
|
||||
</div>
|
||||
<div class="top-right">
|
||||
<div><%= customOfficeAddressLine1 %></div>
|
||||
<div><%= customOfficeAddressLine2 %></div>
|
||||
<div><%= customOfficeAddressLine3 %></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="highlight-box">
|
||||
<div class="highlight-left">
|
||||
<div class="highlight-row"><div class="label">The Director of Customs</div><div><%= customDirectorOfCustoms %></div></div>
|
||||
<div class="highlight-row"><div class="label">Export Gate</div><div><span style="color:#e53935;">Colombo 1.</span></div></div>
|
||||
<div class="highlight-row"><div class="label">Vehicle No. / Permit No.</div><div><%= customVehicleNo %> / <%= customPermitNo %></div></div>
|
||||
</div>
|
||||
<div class="highlight-right">
|
||||
<div class="highlight-row"><div class="label">Chief Security Officer</div><div><%= customChiefSecurityOfficer %></div></div>
|
||||
<div class="highlight-row"><div class="label">SLPA</div><div><%= customSlpa %></div></div>
|
||||
<div class="highlight-row"><div class="label">Gate No.</div><div><%= customGateNo %></div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="permission-line"><%= customPermissionText %></div>
|
||||
<div class="dots">............................................................</div>
|
||||
|
||||
<div class="company-name"><%= customCompanyName %></div>
|
||||
<div class="company-subtitle"><%= customSubtitle %></div>
|
||||
|
||||
<div class="date-row">
|
||||
<div class="date-box"><%= customDateLabel %> <%= customDate %></div>
|
||||
</div>
|
||||
|
||||
<div class="form-code"><%= customSectionCode %></div>
|
||||
|
||||
<table class="items-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="no-col">NO</th>
|
||||
<th class="qty-col">QTY</th>
|
||||
<th class="unit-col">UNIT</th>
|
||||
<th class="description-col">DESCRIPTION</th>
|
||||
<th class="rate-col">RATE</th>
|
||||
<th class="total-col">TOTAL</th>
|
||||
<th class="confirm-col">Confirm<br/>Order Rate</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% for (let index = 0; index < rows; index++) { %>
|
||||
<% const item = customItems[index] || {}; %>
|
||||
<tr class="spacer-row">
|
||||
<td class="center"><%= index + 1 %></td>
|
||||
<td class="center"><%= item.quantity || item.qty || '' %></td>
|
||||
<td class="center"><%= item.unit || '' %></td>
|
||||
<td><%= item.description || item.item_name || item.name || '' %></td>
|
||||
<td class="right"><%= item.rate || item.unit_price || '-' %></td>
|
||||
<td class="right"><%= item.total || item.amount || '-' %></td>
|
||||
<td><%= item.confirmOrderRate || item.confirm_order_rate || '' %></td>
|
||||
</tr>
|
||||
<% } %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Niolla
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is proprietary and confidential.
|
||||
* Unauthorized copying, modification, distribution, or use
|
||||
* of this file, via any medium, is strictly prohibited.
|
||||
*/
|
||||
|
||||
// app/templates/documents/customDocument/customDocument.template.js
|
||||
|
||||
const { log } = require("../../../utils/consoleLog.utill");
|
||||
|
||||
module.exports = async (data) => {
|
||||
log("📋 Preparing Custom Document data for PDF...");
|
||||
|
||||
let payload = data;
|
||||
|
||||
if (typeof payload === "string") {
|
||||
try {
|
||||
payload = JSON.parse(payload);
|
||||
} catch (error) {
|
||||
payload = {};
|
||||
}
|
||||
}
|
||||
|
||||
const source = payload.documentData || payload.document_data || payload.data || payload;
|
||||
|
||||
const pick = (...keys) => {
|
||||
for (const key of keys) {
|
||||
const value = source?.[key];
|
||||
if (value !== undefined && value !== null && value !== "") {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
};
|
||||
|
||||
const templateData = {
|
||||
logoUrl:
|
||||
pick("logoUrl", "logo_url") ||
|
||||
"https://res.cloudinary.com/dtthuvvir/image/upload/v1780748661/company_logo_transperent_bg_uullzw.png",
|
||||
|
||||
title: pick("title") || "CUSTOM DOCUMENT",
|
||||
documentNumber: pick("documentNumber", "document_number") || "",
|
||||
date: pick("date") || new Date().toLocaleDateString(),
|
||||
|
||||
telePhone: pick("telePhone", "telephone", "phone", "tel") || "",
|
||||
emailAddress: pick("emailAddress", "email", "e_mail") || "",
|
||||
|
||||
officeAddressLine1: pick("officeAddressLine1", "office_address_line1") || "",
|
||||
officeAddressLine2: pick("officeAddressLine2", "office_address_line2") || "",
|
||||
officeAddressLine3: pick("officeAddressLine3", "office_address_line3") || "",
|
||||
|
||||
directorOfCustoms: pick("directorOfCustoms", "director_of_customs") || "",
|
||||
vehicleNo: pick("vehicleNo", "vehicle_no") || "",
|
||||
permitNo: pick("permitNo", "permit_no") || "",
|
||||
chiefSecurityOfficer: pick("chiefSecurityOfficer", "chief_security_officer") || "",
|
||||
exportGate: pick("exportGate", "export_gate") || "",
|
||||
slpa: pick("slpa") || "",
|
||||
gateNo: pick("gateNo", "gate_no") || "",
|
||||
permissionText: pick("permissionText", "permission_text") || "Please grant permission to pass these Customs entry papers to supply goods to the vessel",
|
||||
|
||||
companyName: pick("companyName", "company_name") || "Oceanic Maritime Solutions (Pvt) Ltd",
|
||||
subtitle: pick("subtitle") || "LICENSED SHIPCHANDLERS",
|
||||
sectionCode: pick("sectionCode", "section_code") || "SC/FORM/06",
|
||||
dateLabel: pick("dateLabel", "date_label") || "DATE :",
|
||||
|
||||
items: Array.isArray(source.items) ? source.items : [],
|
||||
itemHeaders:
|
||||
pick("itemHeaders", "item_headers") ||
|
||||
["NO", "QTY", "UNIT", "DESCRIPTION", "RATE", "TOTAL", "Confirm Order Rate"],
|
||||
|
||||
footerText: pick("footerText", "footer_text") || "",
|
||||
};
|
||||
|
||||
log("✅ Custom Document data prepared");
|
||||
|
||||
return templateData;
|
||||
};
|
||||
@@ -0,0 +1,189 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@page {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 14px;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.sheet {
|
||||
width: 100%;
|
||||
border: 2px solid #2a5aa8;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.logo-row {
|
||||
height: 82px;
|
||||
border-bottom: 1px solid #4c4c4c;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 6px 12px 2px;
|
||||
}
|
||||
|
||||
.logo-row img {
|
||||
max-height: 64px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.title-row {
|
||||
border-bottom: 1px solid #4c4c4c;
|
||||
text-align: center;
|
||||
padding: 6px 0 7px;
|
||||
color: #2f5fa6;
|
||||
font-size: 26px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.details {
|
||||
display: grid;
|
||||
grid-template-columns: 1.6fr 1fr;
|
||||
gap: 0;
|
||||
min-height: 84px;
|
||||
border-bottom: 1px solid #bdbdbd;
|
||||
}
|
||||
|
||||
.left-pane,
|
||||
.right-pane {
|
||||
padding: 10px 8px 8px;
|
||||
}
|
||||
|
||||
.left-pane {
|
||||
border-right: 1px solid #d4d4d4;
|
||||
}
|
||||
|
||||
.label-line {
|
||||
font-size: 11px;
|
||||
line-height: 1.35;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
color: #111;
|
||||
}
|
||||
|
||||
.label-line + .label-line {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.right-pane {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.right-row {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.right-row .value {
|
||||
font-weight: 400;
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
.items-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
.items-table th,
|
||||
.items-table td {
|
||||
border: 1px solid #5d5d5d;
|
||||
font-size: 11px;
|
||||
padding: 5px 6px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.items-table th {
|
||||
background: #a7d8df;
|
||||
color: #111;
|
||||
text-transform: uppercase;
|
||||
text-align: center;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.no-col { width: 8%; }
|
||||
.desc-col { width: 36%; }
|
||||
.remarks-col { width: 25%; }
|
||||
.unit-col { width: 14%; }
|
||||
.qty-col { width: 17%; }
|
||||
|
||||
.items-table tbody td {
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.right {
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="sheet">
|
||||
<div class="logo-row">
|
||||
<% if (logoBase64) { %>
|
||||
<img src="<%= logoBase64 %>" alt="Company Logo" />
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
<div class="title-row"><%= title || 'DELIVERY NOTE' %></div>
|
||||
|
||||
<div class="details">
|
||||
<div class="left-pane">
|
||||
<div class="label-line">Bill To: <span style="font-weight:700;"><%= billToName || 'N/A' %></span></div>
|
||||
<div class="label-line"><%= billToAddress || 'N/A' %></div>
|
||||
</div>
|
||||
<div class="right-pane">
|
||||
<div class="right-row"><span>Supply Date:</span> <span class="value"><%= supplyDate || 'N/A' %></span></div>
|
||||
<div class="right-row"><span>PO Number:</span> <span class="value"><%= poNumber || 'N/A' %></span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="items-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="no-col">No.</th>
|
||||
<th class="desc-col">Description</th>
|
||||
<th class="remarks-col">Remarks</th>
|
||||
<th class="unit-col">Unit</th>
|
||||
<th class="qty-col">Quantity</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% const rows = Math.max(10, (items && items.length) || 0); %>
|
||||
<% for (let index = 0; index < rows; index++) { %>
|
||||
<% const item = items && items[index] ? items[index] : {}; %>
|
||||
<tr>
|
||||
<td class="center"><%= index + 1 %></td>
|
||||
<td><%= item.description || item.item_name || item.name || '' %></td>
|
||||
<td><%= item.remarks || '' %></td>
|
||||
<td class="center"><%= item.unit || '' %></td>
|
||||
<td class="right"><%= item.quantity || item.qty || '' %></td>
|
||||
</tr>
|
||||
<% } %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Niolla
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is proprietary and confidential.
|
||||
* Unauthorized copying, modification, distribution, or use
|
||||
* of this file, via any medium, is strictly prohibited.
|
||||
*/
|
||||
|
||||
// app/templates/documents/deliveryNote/deliveryNote.template.js
|
||||
|
||||
const { log } = require("../../../utils/consoleLog.utill");
|
||||
|
||||
module.exports = async (data) => {
|
||||
log("📋 Preparing Delivery Note data for PDF...");
|
||||
|
||||
const templateData = {
|
||||
logoUrl:
|
||||
data.logoUrl ||
|
||||
"https://res.cloudinary.com/dtthuvvir/image/upload/v1780748661/company_logo_transperent_bg_uullzw.png",
|
||||
title: data.title || "DELIVERY NOTE",
|
||||
referenceNumber:
|
||||
data.referenceNumber || data.reference_no || data.deliveryNoteNumber || data.delivery_note_number || "N/A",
|
||||
date: data.date || new Date().toLocaleDateString(),
|
||||
billToName: data.billToName || data.bill_to_name || "N/A",
|
||||
billToAddress: data.billToAddress || data.bill_to_address || "N/A",
|
||||
supplyDate: data.supplyDate || data.supply_date || data.date || new Date().toLocaleDateString(),
|
||||
poNumber: data.poNumber || data.po_number || "N/A",
|
||||
items: Array.isArray(data.items) ? data.items : [],
|
||||
};
|
||||
|
||||
log("✅ Delivery Note data prepared");
|
||||
|
||||
return templateData;
|
||||
};
|
||||
@@ -0,0 +1,345 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@page {
|
||||
size: A4;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
background: white;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.sheet {
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border: 1px solid #222;
|
||||
}
|
||||
|
||||
.title-bar {
|
||||
background: #173d64;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 1px;
|
||||
padding: 4px 0 5px;
|
||||
border-bottom: 1px solid #222;
|
||||
}
|
||||
|
||||
.top-area {
|
||||
display: grid;
|
||||
grid-template-columns: 1.4fr 1fr 0.65fr;
|
||||
min-height: 95px;
|
||||
border-bottom: 1px solid #444;
|
||||
}
|
||||
|
||||
.top-col {
|
||||
padding: 5px 6px;
|
||||
border-right: 1px solid #444;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.top-col:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.field-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1px;
|
||||
}
|
||||
|
||||
.field-row {
|
||||
display: grid;
|
||||
grid-template-columns: 110px 1fr;
|
||||
align-items: center;
|
||||
min-height: 17px;
|
||||
border: 1px solid #444;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.field-row:last-child {
|
||||
border-bottom: 1px solid #444;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-weight: 700;
|
||||
padding: 1px 4px;
|
||||
}
|
||||
|
||||
.value {
|
||||
padding: 1px 4px;
|
||||
}
|
||||
|
||||
.top-left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.top-middle {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.top-right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.meta-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1.4fr 1fr 0.65fr;
|
||||
border-bottom: 1px solid #444;
|
||||
}
|
||||
|
||||
.meta-box {
|
||||
min-height: 82px;
|
||||
padding: 4px 6px;
|
||||
border-right: 1px solid #444;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.meta-box:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.meta-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.meta-table td {
|
||||
border: 1px solid #444;
|
||||
padding: 2px 4px;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.meta-table td.label-cell {
|
||||
width: 105px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.details-note {
|
||||
font-size: 9px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.items-title {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
padding: 2px 0 3px;
|
||||
border-bottom: 1px solid #444;
|
||||
background: #efefef;
|
||||
}
|
||||
|
||||
.items-wrap {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.items-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
.items-table th,
|
||||
.items-table td {
|
||||
border: 1px solid #666;
|
||||
font-size: 10px;
|
||||
padding: 2px 3px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.items-table th {
|
||||
background: #d9d9d9;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.no-col { width: 5%; }
|
||||
.product-col { width: 24%; }
|
||||
.qty-col { width: 5%; }
|
||||
.unit-col { width: 5%; }
|
||||
.supp-col { width: 6%; }
|
||||
.pono-col { width: 6%; }
|
||||
.rcvdqty-col { width: 7%; }
|
||||
.issuedqty-col { width: 7%; }
|
||||
.expdate-col { width: 8%; }
|
||||
.rcvdtime-col { width: 7%; }
|
||||
.date-col { width: 7%; }
|
||||
.rejects-col { width: 6%; }
|
||||
.fatcol-col { width: 6%; }
|
||||
.remark-col { width: 7%; }
|
||||
|
||||
.center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.spacer-row td {
|
||||
height: 21px;
|
||||
}
|
||||
|
||||
.signature-section {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.signature-block {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
border-top: 1px solid #333;
|
||||
padding-top: 34px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.footer-note {
|
||||
text-align: center;
|
||||
margin-top: 12px;
|
||||
border-top: 1px solid #ddd;
|
||||
padding-top: 5px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<%
|
||||
const dispatchTitle = typeof title !== 'undefined' && title ? title : 'DISPATCH NOTE';
|
||||
const dispatchReferenceNumber = typeof referenceNumber !== 'undefined' && referenceNumber ? referenceNumber : 'N/A';
|
||||
const dispatchDate = typeof date !== 'undefined' && date ? date : 'N/A';
|
||||
const dispatchVessel = typeof vessel !== 'undefined' && vessel ? vessel : 'N/A';
|
||||
const dispatchCaptain = typeof captain !== 'undefined' && captain ? captain : 'N/A';
|
||||
const dispatchCook = typeof cook !== 'undefined' && cook ? cook : 'N/A';
|
||||
const dispatchAgent = typeof agent !== 'undefined' && agent ? agent : 'N/A';
|
||||
const dispatchDetails = typeof details !== 'undefined' && details ? details : '';
|
||||
const dispatchPlaceOfDelivery = typeof placeOfDelivery !== 'undefined' && placeOfDelivery ? placeOfDelivery : 'N/A';
|
||||
const dispatchEta = typeof eta !== 'undefined' && eta ? eta : 'N/A';
|
||||
const dispatchEtd = typeof etd !== 'undefined' && etd ? etd : 'N/A';
|
||||
const dispatchNumberOfCrew = typeof numberOfCrew !== 'undefined' && numberOfCrew ? numberOfCrew : 'N/A';
|
||||
const dispatchNextMainOrderIn = typeof nextMainOrderIn !== 'undefined' && nextMainOrderIn ? nextMainOrderIn : 'N/A';
|
||||
const dispatchPort = typeof port !== 'undefined' && port ? port : 'N/A';
|
||||
const dispatchCompany = typeof company !== 'undefined' && company ? company : 'N/A';
|
||||
const dispatchFileNo = typeof fileNo !== 'undefined' && fileNo ? fileNo : 'N/A';
|
||||
const dispatchItems = typeof items !== 'undefined' && Array.isArray(items) ? items : [];
|
||||
%>
|
||||
<div class="sheet">
|
||||
<div class="title-bar"><%= dispatchTitle %></div>
|
||||
|
||||
<div class="top-area">
|
||||
<div class="top-col">
|
||||
<div class="field-grid">
|
||||
<div class="field-row"><div class="label">Vessel:</div><div class="value"><%= dispatchVessel %></div></div>
|
||||
<div class="field-row"><div class="label">Captain:</div><div class="value"><%= dispatchCaptain %></div></div>
|
||||
<div class="field-row"><div class="label">Cook:</div><div class="value"><%= dispatchCook %></div></div>
|
||||
<div class="field-row"><div class="label">Agent:</div><div class="value"><%= dispatchAgent %></div></div>
|
||||
<div class="field-row"><div class="label">Details:</div><div class="value"><%= dispatchDetails %></div></div>
|
||||
</div>
|
||||
<div class="details-note">SC/FORM/04</div>
|
||||
</div>
|
||||
<div class="top-col">
|
||||
<div class="field-grid">
|
||||
<div class="field-row"><div class="label">Place of delivery:</div><div class="value"><%= dispatchPlaceOfDelivery %></div></div>
|
||||
<div class="field-row"><div class="label">ETA:</div><div class="value"><%= dispatchEta %></div></div>
|
||||
<div class="field-row"><div class="label">ETD:</div><div class="value"><%= dispatchEtd %></div></div>
|
||||
<div class="field-row"><div class="label">Number of crew:</div><div class="value"><%= dispatchNumberOfCrew %></div></div>
|
||||
<div class="field-row"><div class="label">Next main order in:</div><div class="value"><%= dispatchNextMainOrderIn %></div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="top-col">
|
||||
<div class="field-grid">
|
||||
<div class="field-row"><div class="label">Port</div><div class="value"><%= dispatchPort %></div></div>
|
||||
<div class="field-row"><div class="label">Date</div><div class="value"><%= dispatchDate %></div></div>
|
||||
<div class="field-row"><div class="label">Company</div><div class="value"><%= dispatchCompany %></div></div>
|
||||
<div class="field-row"><div class="label">File No</div><div class="value"><%= dispatchFileNo %></div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="items-title">Item Receiving Authorization and Vessel Delivery Oder details</div>
|
||||
|
||||
<div class="items-wrap">
|
||||
<table class="items-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="no-col">No</th>
|
||||
<th class="product-col">Product</th>
|
||||
<th class="qty-col">Qty</th>
|
||||
<th class="unit-col">Unit</th>
|
||||
<th class="supp-col">SUPP</th>
|
||||
<th class="pono-col">PO NO</th>
|
||||
<th class="rcvdqty-col">Rcvd Qty</th>
|
||||
<th class="issuedqty-col">Issued Qty</th>
|
||||
<th class="expdate-col">Exp Date</th>
|
||||
<th class="rcvdtime-col">Rcvd Time</th>
|
||||
<th class="date-col">Date</th>
|
||||
<th class="rejects-col">Rejects</th>
|
||||
<th class="fatcol-col">Fat Con.</th>
|
||||
<th class="remark-col">Remark</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% const rows = Math.max(5, dispatchItems.length); %>
|
||||
<% for (let index = 0; index < rows; index++) { %>
|
||||
<% const item = dispatchItems[index] || {}; %>
|
||||
<tr class="spacer-row">
|
||||
<td class="center"><%= index + 1 %></td>
|
||||
<td><%= item.product || item.description || item.item_name || item.name || '' %></td>
|
||||
<td class="center"><%= item.qty || item.quantity || '' %></td>
|
||||
<td class="center"><%= item.unit || '' %></td>
|
||||
<td class="center"><%= item.supp || item.supplier || '' %></td>
|
||||
<td class="center"><%= item.poNo || item.po_number || item.poNumber || '' %></td>
|
||||
<td class="center"><%= item.receivedQty || item.rcvdQty || '' %></td>
|
||||
<td class="center"><%= item.issuedQty || '' %></td>
|
||||
<td class="center"><%= item.expDate || item.exp_date || '' %></td>
|
||||
<td class="center"><%= item.receivedTime || item.rcvdTime || '' %></td>
|
||||
<td class="center"><%= item.date || '' %></td>
|
||||
<td class="center"><%= item.rejects || '' %></td>
|
||||
<td class="center"><%= item.fatCon || item.fat_con || '' %></td>
|
||||
<td><%= item.remark || item.remarks || '' %></td>
|
||||
</tr>
|
||||
<% } %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<% if (showSignatures) { %>
|
||||
<div class="signature-section">
|
||||
<div class="signature-block">
|
||||
<strong>Prepared By</strong>
|
||||
<div style="margin-top: 28px;">___________________</div>
|
||||
<div style="margin-top: 5px;"><%= preparedBy || 'Name' %></div>
|
||||
</div>
|
||||
<div class="signature-block">
|
||||
<strong>Approved By</strong>
|
||||
<div style="margin-top: 28px;">___________________</div>
|
||||
<div style="margin-top: 5px;"><%= approvedBy || 'Name' %></div>
|
||||
</div>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<div class="footer-note">
|
||||
<p style="font-size: 10px; color: #999;">This is an official dispatch note</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Niolla
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is proprietary and confidential.
|
||||
* Unauthorized copying, modification, distribution, or use
|
||||
* of this file, via any medium, is strictly prohibited.
|
||||
*/
|
||||
|
||||
// app/templates/documents/dispatchNote/dispatchNote.template.js
|
||||
|
||||
const { log } = require("../../../utils/consoleLog.utill");
|
||||
|
||||
module.exports = async (data) => {
|
||||
log("📋 Preparing Dispatch Note data for PDF...");
|
||||
|
||||
let payload = data;
|
||||
|
||||
if (typeof payload === "string") {
|
||||
try {
|
||||
payload = JSON.parse(payload);
|
||||
} catch (error) {
|
||||
payload = {};
|
||||
}
|
||||
}
|
||||
|
||||
const source = payload.documentData || payload.document_data || payload.data || payload;
|
||||
|
||||
const pick = (...keys) => {
|
||||
for (const key of keys) {
|
||||
const value = source?.[key];
|
||||
if (value !== undefined && value !== null && value !== "") {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
};
|
||||
|
||||
const templateData = {
|
||||
logoUrl:
|
||||
pick("logoUrl", "logo_url") ||
|
||||
"https://res.cloudinary.com/dtthuvvir/image/upload/v1780748661/company_logo_transperent_bg_uullzw.png",
|
||||
|
||||
title: pick("title") || "DISPATCH NOTE",
|
||||
referenceNumber: pick("referenceNumber", "reference_number", "dnNo", "dn_no") || "N/A",
|
||||
date: pick("date") || new Date().toLocaleDateString(),
|
||||
|
||||
vessel: pick("vessel", "vesselName", "vessel_name") || "",
|
||||
captain: pick("captain", "captainName", "captain_name") || "",
|
||||
cook: pick("cook", "cookName", "cook_name") || "",
|
||||
agent: pick("agent", "agentName", "agent_name") || "",
|
||||
details: pick("details", "remarks", "note") || "",
|
||||
|
||||
placeOfDelivery: pick("placeOfDelivery", "place_of_delivery") || "",
|
||||
eta: pick("eta") || "",
|
||||
etd: pick("etd") || "",
|
||||
numberOfCrew: pick("numberOfCrew", "number_of_crew", "crewCount", "crew_count") || "",
|
||||
nextMainOrderIn: pick("nextMainOrderIn", "next_main_order_in") || "",
|
||||
port: pick("port", "portName", "port_name") || "",
|
||||
company: pick("company", "companyName", "company_name") || "",
|
||||
fileNo: pick("fileNo", "file_no") || "",
|
||||
|
||||
items: Array.isArray(source.items) ? source.items : [],
|
||||
|
||||
showSignatures: pick("showSignatures") !== false,
|
||||
preparedBy: pick("preparedBy", "prepared_by") || "",
|
||||
approvedBy: pick("approvedBy", "approved_by") || "",
|
||||
};
|
||||
|
||||
log("✅ Dispatch Note data prepared");
|
||||
|
||||
return templateData;
|
||||
};
|
||||
@@ -0,0 +1,308 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@page {
|
||||
size: A4;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
background: white;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.page {
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border: 1px solid #333;
|
||||
}
|
||||
|
||||
.logo-section {
|
||||
height: 72px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-bottom: 1px solid #c9c9c9;
|
||||
}
|
||||
|
||||
.logo-section img {
|
||||
max-height: 52px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.title-section {
|
||||
text-align: center;
|
||||
padding: 5px 0 4px;
|
||||
border-bottom: 1px solid #333;
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
color: #2d5c96;
|
||||
letter-spacing: 0.4px;
|
||||
}
|
||||
|
||||
.top-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1.6fr 1fr;
|
||||
min-height: 70px;
|
||||
border-bottom: 1px solid #c9c9c9;
|
||||
}
|
||||
|
||||
.top-left,
|
||||
.top-right,
|
||||
.mid-left,
|
||||
.mid-right {
|
||||
padding: 4px 6px;
|
||||
font-size: 10px;
|
||||
color: #111;
|
||||
}
|
||||
|
||||
.top-left {
|
||||
border-right: 1px solid #e2e2e2;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.bill-to-label {
|
||||
font-weight: 700;
|
||||
min-width: 48px;
|
||||
}
|
||||
|
||||
.bill-to-name {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.bill-to-address {
|
||||
margin-top: 2px;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.top-right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
gap: 2px;
|
||||
text-align: right;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.top-right-row {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.top-right-row .value {
|
||||
font-weight: 400;
|
||||
min-width: 110px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1.4fr 1fr;
|
||||
border-bottom: 1px solid #c9c9c9;
|
||||
}
|
||||
|
||||
.mid-left {
|
||||
border-right: 1px solid #e2e2e2;
|
||||
}
|
||||
|
||||
.mid-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
text-align: right;
|
||||
min-width: 90px;
|
||||
}
|
||||
|
||||
.items-wrap {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.items-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.items-table thead th {
|
||||
background: #a8d9de;
|
||||
color: #111;
|
||||
font-weight: bold;
|
||||
font-size: 10px;
|
||||
text-align: center;
|
||||
padding: 5px 4px;
|
||||
border: 1px solid #4b4b4b;
|
||||
}
|
||||
|
||||
.items-table td {
|
||||
border: 1px solid #5d5d5d;
|
||||
font-size: 10px;
|
||||
height: 15px;
|
||||
padding: 2px 4px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.col-no { width: 8%; }
|
||||
.col-desc { width: 30%; }
|
||||
.col-remarks { width: 24%; }
|
||||
.col-qty { width: 9%; }
|
||||
.col-unit { width: 9%; }
|
||||
.col-price { width: 10%; }
|
||||
.col-total { width: 10%; }
|
||||
|
||||
.spacer-row td {
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.bottom-table {
|
||||
display: grid;
|
||||
grid-template-columns: 1.4fr 1fr;
|
||||
border-top: 1px solid #c9c9c9;
|
||||
}
|
||||
|
||||
.bottom-left,
|
||||
.bottom-right {
|
||||
padding: 4px 6px;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.bottom-left {
|
||||
border-right: 1px solid #e2e2e2;
|
||||
}
|
||||
|
||||
.bottom-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.bottom-line {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.footer-row {
|
||||
border-top: 1px solid #333;
|
||||
text-align: right;
|
||||
padding: 4px 6px;
|
||||
font-size: 9px;
|
||||
color: #555;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<%
|
||||
const invoiceTitle = typeof title !== 'undefined' && title ? title : 'INVOICE';
|
||||
const invoiceJobReference = typeof jobReference !== 'undefined' && jobReference ? jobReference : 'N/A';
|
||||
const invoicePoNumber = typeof poNumber !== 'undefined' && poNumber ? poNumber : 'N/A';
|
||||
const invoiceDate = typeof date !== 'undefined' && date ? date : 'N/A';
|
||||
const invoicePageLabel = typeof pageLabel !== 'undefined' && pageLabel ? pageLabel : 'Page 01 of 02';
|
||||
const invoiceBillToName = typeof billToName !== 'undefined' && billToName ? billToName : 'Client Name';
|
||||
const invoiceBillToAddress = typeof billToAddress !== 'undefined' && billToAddress ? billToAddress : 'Address';
|
||||
const invoiceVesselName = typeof vesselName !== 'undefined' && vesselName ? vesselName : 'N/A';
|
||||
const invoicePurposeOfCall = typeof purposeOfCall !== 'undefined' && purposeOfCall ? purposeOfCall : 'N/A';
|
||||
const invoiceSupplyDate = typeof supplyDate !== 'undefined' && supplyDate ? supplyDate : 'N/A';
|
||||
const invoiceGrt = typeof grt !== 'undefined' && grt ? grt : 'N/A';
|
||||
const invoiceImoNumber = typeof imoNumber !== 'undefined' && imoNumber ? imoNumber : 'N/A';
|
||||
const invoicePortCountry = typeof portCountry !== 'undefined' && portCountry ? portCountry : 'N/A';
|
||||
const invoiceItems = typeof items !== 'undefined' && Array.isArray(items) ? items : [];
|
||||
%>
|
||||
<div class="page">
|
||||
<div class="logo-section">
|
||||
<% if (logoBase64) { %>
|
||||
<img src="<%= logoBase64 %>" alt="Company Logo" />
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
<div class="title-section"><%= invoiceTitle %></div>
|
||||
|
||||
<div class="top-grid">
|
||||
<div class="top-left">
|
||||
<div>
|
||||
<div><span class="bill-to-label">Bill To:</span> <span class="bill-to-name"><%= invoiceBillToName %></span></div>
|
||||
<div class="bill-to-address"><%= invoiceBillToAddress %></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="top-right">
|
||||
<div class="top-right-row"><span>Job Reference :</span> <span class="value"><%= invoiceJobReference %></span></div>
|
||||
<div class="top-right-row"><span>PO Number :</span> <span class="value"><%= invoicePoNumber %></span></div>
|
||||
<div class="top-right-row"><span>Date :</span> <span class="value"><%= invoiceDate %></span></div>
|
||||
<div class="top-right-row"><span></span> <span class="value"><%= invoicePageLabel %></span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-grid">
|
||||
<div class="mid-left">
|
||||
<div class="mid-row"><span class="info-label">Vessel Name :</span> <span class="info-value"><%= invoiceVesselName %></span></div>
|
||||
<div class="mid-row"><span class="info-label">Purpose of Call :</span> <span class="info-value"><%= invoicePurposeOfCall %></span></div>
|
||||
<div class="mid-row"><span class="info-label">Supply Date :</span> <span class="info-value"><%= invoiceSupplyDate %></span></div>
|
||||
</div>
|
||||
<div class="mid-right">
|
||||
<div class="mid-row"><span class="info-label">GRT :</span> <span class="info-value"><%= invoiceGrt %></span></div>
|
||||
<div class="mid-row"><span class="info-label">IMO Number :</span> <span class="info-value"><%= invoiceImoNumber %></span></div>
|
||||
<div class="mid-row"><span class="info-label">Port/Country :</span> <span class="info-value"><%= invoicePortCountry %></span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="items-wrap">
|
||||
<table class="items-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="col-no">No.</th>
|
||||
<th class="col-desc">Description</th>
|
||||
<th class="col-remarks">Remarks</th>
|
||||
<th class="col-qty">Qty</th>
|
||||
<th class="col-unit">Unit</th>
|
||||
<th class="col-price">Unit Price (USD)</th>
|
||||
<th class="col-total">Final Amount (USD)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% const rows = Math.max(10, invoiceItems.length); %>
|
||||
<% for (let index = 0; index < rows; index++) { %>
|
||||
<% const item = invoiceItems[index] || {}; %>
|
||||
<% const quantity = Number(item.quantity || item.qty || 0); %>
|
||||
<% const unitPrice = Number(item.unit_price || item.unitPrice || 0); %>
|
||||
<tr class="spacer-row">
|
||||
<td class="center"><%= index + 1 %></td>
|
||||
<td><%= item.description || item.item_name || item.name || '' %></td>
|
||||
<td><%= item.remarks || '' %></td>
|
||||
<td class="center"><%= item.quantity || item.qty || '' %></td>
|
||||
<td class="center"><%= item.unit || '' %></td>
|
||||
<td class="right"><%= item.unit_price || item.unitPrice || '' %></td>
|
||||
<td class="right"><%= quantity && unitPrice ? quantity * unitPrice : (item.total || item.amount || '') %></td>
|
||||
</tr>
|
||||
<% } %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,111 @@
|
||||
|
||||
/**
|
||||
* Copyright (c) 2026 Niolla
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is proprietary and confidential.
|
||||
* Unauthorized copying, modification, distribution, or use
|
||||
* of this file, via any medium, is strictly prohibited.
|
||||
*/
|
||||
|
||||
// app/templates/documents/invoice/invoice.template.js
|
||||
|
||||
const { log } = require("../../../utils/consoleLog.utill");
|
||||
|
||||
module.exports = async (data) => {
|
||||
log("📋 Preparing Invoice data for PDF...");
|
||||
|
||||
let payload = data;
|
||||
|
||||
if (typeof payload === "string") {
|
||||
try {
|
||||
payload = JSON.parse(payload);
|
||||
} catch (error) {
|
||||
payload = {};
|
||||
}
|
||||
}
|
||||
|
||||
const source =
|
||||
payload.documentData ||
|
||||
payload.document_data ||
|
||||
payload.data ||
|
||||
payload;
|
||||
|
||||
const deepFind = (node, keys, visited = new Set()) => {
|
||||
if (!node || typeof node !== "object" || visited.has(node)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
visited.add(node);
|
||||
|
||||
for (const key of keys) {
|
||||
const value = node[key];
|
||||
if (value !== undefined && value !== null && value !== "") {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
for (const child of Object.values(node)) {
|
||||
const found = deepFind(child, keys, visited);
|
||||
if (found !== "") {
|
||||
return found;
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
};
|
||||
|
||||
const pick = (...keys) => {
|
||||
const direct = deepFind(source, keys);
|
||||
|
||||
if (direct !== "") {
|
||||
return direct;
|
||||
}
|
||||
|
||||
return deepFind(payload, keys);
|
||||
};
|
||||
|
||||
const templateData = {
|
||||
logoUrl:
|
||||
pick("logoUrl", "logo_url") ||
|
||||
"https://res.cloudinary.com/dtthuvvir/image/upload/v1780748661/company_logo_transperent_bg_uullzw.png",
|
||||
|
||||
title: pick("title") || "INVOICE",
|
||||
jobReference:
|
||||
pick(
|
||||
"jobReference",
|
||||
"job_reference",
|
||||
"referenceNumber",
|
||||
"reference_no",
|
||||
"jobRef",
|
||||
"job_ref",
|
||||
) || "",
|
||||
poNumber: pick("poNumber", "po_number", "poNo", "po_no") || "",
|
||||
date: pick("date") || new Date().toLocaleDateString(),
|
||||
pageLabel: pick("pageLabel", "page_label") || "Page 01 of 02",
|
||||
|
||||
billToName: pick("billToName", "bill_to_name") || "Client Name",
|
||||
billToAddress: pick("billToAddress", "bill_to_address") || "Address",
|
||||
|
||||
vesselName: pick("vesselName", "vessel_name") || "",
|
||||
purposeOfCall: pick("purposeOfCall", "purpose_of_call") || "",
|
||||
supplyDate: pick("supplyDate", "supply_date") || "",
|
||||
grt: pick("grt", "GRT") || "",
|
||||
imoNumber: pick("imoNumber", "imo_number", "imo") || "",
|
||||
portCountry: pick("portCountry", "port_country") || "",
|
||||
|
||||
items: Array.isArray(source.items) ? source.items : [],
|
||||
|
||||
subtotal: Number(pick("subtotal", "sub_total") || 0),
|
||||
tax: Number(pick("tax") || 0),
|
||||
discount: Number(pick("discount") || 0),
|
||||
total: Number(pick("total") || 0),
|
||||
|
||||
paymentTerms: pick("paymentTerms", "payment_terms") || "",
|
||||
notes: pick("notes") || "",
|
||||
};
|
||||
|
||||
log("✅ Invoice data prepared");
|
||||
|
||||
return templateData;
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
body {
|
||||
font-family: Arial;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
td,
|
||||
th {
|
||||
border: 1px solid #ddd;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
th {
|
||||
background: #f4f4f4;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
{{styles}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>PreCost Estimate</h1>
|
||||
|
||||
<p>Customer: {{customerName}}</p>
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<th>Item</th>
|
||||
<th>Description</th>
|
||||
<th>Qty</th>
|
||||
<th>Unit Price</th>
|
||||
<th>Total</th>
|
||||
</tr>
|
||||
|
||||
{{items}}
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,285 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
background: white;
|
||||
font-family: 'Segoe UI', 'Roboto', 'Helvetica Neue', sans-serif;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.header-section {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 30px;
|
||||
border-bottom: 2px solid #1a7a4d;
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
|
||||
.header-section img {
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
.doc-title {
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
color: #1a7a4d;
|
||||
text-align: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.po-number {
|
||||
text-align: right;
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
margin-bottom: 30px;
|
||||
background: #e8f5e9;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.info-left, .info-right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-weight: bold;
|
||||
color: #2e7d32;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.items-table thead {
|
||||
background-color: #2e7d32;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.items-table th {
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.items-table td {
|
||||
padding: 12px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.items-table tbody tr:nth-child(even) {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.summary-area {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 30px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.summary-table {
|
||||
width: 400px;
|
||||
}
|
||||
|
||||
.summary-table td {
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.summary-table td:first-child {
|
||||
font-weight: bold;
|
||||
background-color: #f5f5f5;
|
||||
width: 60%;
|
||||
}
|
||||
|
||||
.summary-table td:last-child {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.total-row {
|
||||
background-color: #2e7d32;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.terms-section {
|
||||
margin-top: 30px;
|
||||
border-top: 1px solid #ddd;
|
||||
padding-top: 15px;
|
||||
}
|
||||
|
||||
.terms-section h4 {
|
||||
color: #2e7d32;
|
||||
font-size: 12px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.terms-section p {
|
||||
font-size: 11px;
|
||||
color: #666;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.footer-note {
|
||||
text-align: center;
|
||||
margin-top: 40px;
|
||||
border-top: 2px solid #ddd;
|
||||
padding-top: 20px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<!-- HEADER SECTION -->
|
||||
<div class="header-section">
|
||||
<div>
|
||||
<% if (logoBase64) { %>
|
||||
<img src="<%= logoBase64 %>" alt="Company Logo" />
|
||||
<% } %>
|
||||
</div>
|
||||
<div style="flex: 1; text-align: center;">
|
||||
<div class="doc-title">PURCHASE ORDER</div>
|
||||
</div>
|
||||
<div class="po-number">
|
||||
<strong>PO #:</strong> <%= poNumber || 'N/A' %><br/>
|
||||
<strong>Date:</strong> <%= date || 'N/A' %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- INFO GRID -->
|
||||
<div class="info-grid">
|
||||
<div class="info-left">
|
||||
<div class="info-row">
|
||||
<span class="info-label">VENDOR:</span>
|
||||
</div>
|
||||
<div style="margin-left: 0;">
|
||||
<strong><%= vendorName || 'Vendor Name' %></strong><br/>
|
||||
<%= vendorAddress || 'Address' %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-right">
|
||||
<div class="info-row">
|
||||
<span class="info-label">DELIVERY DATE:</span>
|
||||
<span class="info-value"><%= deliveryDate || 'N/A' %></span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">PAYMENT TERMS:</span>
|
||||
<span class="info-value"><%= paymentTerms || 'Net 30' %></span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">VESSEL:</span>
|
||||
<span class="info-value"><%= vesselName || 'N/A' %></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ITEMS TABLE -->
|
||||
<div style="overflow-x: auto;">
|
||||
<table class="items-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ITEM NO</th>
|
||||
<th>DESCRIPTION</th>
|
||||
<th>QTY</th>
|
||||
<th>UNIT</th>
|
||||
<th>UNIT PRICE</th>
|
||||
<th>TOTAL</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% if (items && items.length > 0) { %>
|
||||
<% items.forEach((item, index) => { %>
|
||||
<tr>
|
||||
<td><%= index + 1 %></td>
|
||||
<td><%= item.description || item.item_name || '' %></td>
|
||||
<td><%= item.quantity || 0 %></td>
|
||||
<td><%= item.unit || '' %></td>
|
||||
<td><%= item.unit_price || 0 %></td>
|
||||
<td><%= (item.quantity || 0) * (item.unit_price || 0) %></td>
|
||||
</tr>
|
||||
<% }); %>
|
||||
<% } %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- SUMMARY SECTION -->
|
||||
<div class="summary-area">
|
||||
<table class="summary-table">
|
||||
<tr>
|
||||
<td>SUB TOTAL</td>
|
||||
<td><%= subtotal || 0 %></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>TAX (%)</td>
|
||||
<td><%= tax || 0 %></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>SHIPPING</td>
|
||||
<td><%= shipping || 0 %></td>
|
||||
</tr>
|
||||
<tr class="total-row">
|
||||
<td>TOTAL</td>
|
||||
<td><%= total || 0 %></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- TERMS SECTION -->
|
||||
<% if (paymentTermsDetails || notes) { %>
|
||||
<div class="terms-section">
|
||||
<% if (paymentTermsDetails) { %>
|
||||
<h4>TERMS & CONDITIONS</h4>
|
||||
<p><%= paymentTermsDetails %></p>
|
||||
<% } %>
|
||||
<% if (notes) { %>
|
||||
<h4>SPECIAL INSTRUCTIONS</h4>
|
||||
<p><%= notes %></p>
|
||||
<% } %>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<!-- FOOTER -->
|
||||
<div class="footer-note">
|
||||
<p style="font-size: 10px; color: #999;">Please confirm receipt and delivery dates</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Niolla
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is proprietary and confidential.
|
||||
* Unauthorized copying, modification, distribution, or use
|
||||
* of this file, via any medium, is strictly prohibited.
|
||||
*/
|
||||
|
||||
// app/templates/documents/po/po.template.js
|
||||
|
||||
const { log } = require("../../../utils/consoleLog.utill");
|
||||
|
||||
module.exports = async (data) => {
|
||||
log("📋 Preparing Purchase Order data for PDF...");
|
||||
|
||||
const templateData = {
|
||||
logoUrl: "https://res.cloudinary.com/dtthuvvir/image/upload/v1780748661/company_logo_transperent_bg_uullzw.png",
|
||||
|
||||
poNumber: data.poNumber || data.po_number || "PO-001",
|
||||
date: data.date || new Date().toLocaleDateString(),
|
||||
|
||||
vendorName: data.vendorName || data.vendor_name || "Vendor Name",
|
||||
vendorAddress: data.vendorAddress || data.vendor_address || "Address",
|
||||
|
||||
deliveryDate: data.deliveryDate || data.delivery_date || "N/A",
|
||||
paymentTerms: data.paymentTerms || data.payment_terms || "Net 30",
|
||||
vesselName: data.vesselName || data.vessel_name || "N/A",
|
||||
|
||||
items: data.items || [],
|
||||
|
||||
subtotal: data.subtotal || data.sub_total || 0,
|
||||
tax: data.tax || 0,
|
||||
shipping: data.shipping || 0,
|
||||
total: data.total || 0,
|
||||
|
||||
paymentTermsDetails: data.paymentTermsDetails || data.payment_terms_details || "",
|
||||
notes: data.notes || "",
|
||||
};
|
||||
|
||||
log("✅ Purchase Order data prepared");
|
||||
|
||||
return templateData;
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Niolla
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is proprietary and confidential.
|
||||
* Unauthorized copying, modification, distribution, or use
|
||||
* of this file, via any medium, is strictly prohibited.
|
||||
*/
|
||||
|
||||
// app/templates/documents/precost/preCost.template.js
|
||||
|
||||
const { log } = require("../../../utils/consoleLog.utill");
|
||||
|
||||
module.exports = async (data) => {
|
||||
log("📋 Preparing PreCost data for PDF...");
|
||||
log("📥 Incoming data:", JSON.stringify(data, null, 2));
|
||||
|
||||
const templateData = {
|
||||
logoUrl:
|
||||
"https://res.cloudinary.com/dtthuvvir/image/upload/v1780748661/company_logo_transperent_bg_uullzw.png",
|
||||
|
||||
vesselName: data.vessel_name || "MV Vessel",
|
||||
|
||||
supplierName: data.supplyPort || "N/A",
|
||||
clientRfqNum: data.clientRfqNum || "N/A",
|
||||
omsRfqNum: data.omsRfqNum || "N/A",
|
||||
date: data.date || new Date().toLocaleDateString(),
|
||||
|
||||
items: data.items || [],
|
||||
|
||||
subtotal: data.sub_total || 0,
|
||||
discount: data.discount || 0,
|
||||
additionalCharges: data.additionalCharges || 0,
|
||||
fullTotal: data.total_cost || data.total_cost_usd || (data.sub_total - data.discount + data.additionalCharges) || 0,
|
||||
};
|
||||
|
||||
log("Data:", data);
|
||||
log("fullTotal calculation:", {
|
||||
total_cost: data.total_cost,
|
||||
total_cost_usd: data.total_cost_usd,
|
||||
calculated: data.sub_total - data.discount + data.additionalCharges,
|
||||
final: templateData.fullTotal,
|
||||
});
|
||||
|
||||
log("✅ PreCost data prepared:", JSON.stringify(templateData, null, 2));
|
||||
|
||||
return templateData;
|
||||
};
|
||||
@@ -0,0 +1,300 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
background: white;
|
||||
font-family: 'Segoe UI', 'Roboto', 'Helvetica Neue', sans-serif;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.header-section {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 30px;
|
||||
border-bottom: 2px solid #1a7a4d;
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
|
||||
.header-section img {
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
.po-title {
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
color: #1a7a4d;
|
||||
text-align: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
margin-bottom: 30px;
|
||||
background: #e8f5e9;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.info-left,
|
||||
.info-right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-weight: bold;
|
||||
color: #2e7d32;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.items-table thead {
|
||||
background-color: #2e7d32;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.items-table th {
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.items-table td {
|
||||
padding: 12px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.items-table tbody tr:nth-child(even) {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.summary-area {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 30px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.summary-table {
|
||||
width: 400px;
|
||||
}
|
||||
|
||||
.summary-table td {
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.summary-table td:first-child {
|
||||
font-weight: bold;
|
||||
background-color: #f5f5f5;
|
||||
width: 60%;
|
||||
}
|
||||
|
||||
.summary-table td:last-child {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.total-row {
|
||||
background-color: #2e7d32;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.total-row td {
|
||||
border-color: #1a7a4d;
|
||||
}
|
||||
|
||||
.footer-note {
|
||||
text-align: center;
|
||||
margin-top: 40px;
|
||||
border-top: 2px solid #ddd;
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.footer-note img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.text-black{
|
||||
color: black !important;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<!-- HEADER SECTION -->
|
||||
<div class="header-section">
|
||||
<div>
|
||||
<% if (logoBase64) { %>
|
||||
<img src="<%= logoBase64 %>" alt="Company Logo" />
|
||||
<% } %>
|
||||
</div>
|
||||
<div class="po-title">PRICE QUOTATION</div>
|
||||
</div>
|
||||
|
||||
<!-- INFO GRID -->
|
||||
<div class="info-grid">
|
||||
<div class="info-left">
|
||||
<div class="info-row">
|
||||
<span class="info-label">VESSEL NAME :</span>
|
||||
<span class="info-value">
|
||||
<%= vesselName %>
|
||||
</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">SUPPLY PORT :</span>
|
||||
<span class="info-value">
|
||||
<%= supplierName || 'Supplier Name' %>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-right">
|
||||
<div class="info-row">
|
||||
<span class="info-label">CLIENT RFQ :</span>
|
||||
<span class="info-value">
|
||||
<%= clientRfqNum || 'RFQ-001' %>
|
||||
</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">OMS RFQ :</span>
|
||||
<span class="info-value">
|
||||
<%= omsRfqNum || 'RFQ-001' %>
|
||||
</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">DATE :</span>
|
||||
<span class="info-value">
|
||||
<%= date || 'Current Date' %>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ITEMS TABLE -->
|
||||
<div style="overflow-x: auto;">
|
||||
<table class="items-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ITEM NO</th>
|
||||
<th>DESCRIPTION</th>
|
||||
<th>REMARKS</th>
|
||||
<th>QTY</th>
|
||||
<th>UNIT</th>
|
||||
<th>UNIT PRICE</th>
|
||||
<th>TOTAL PRICE</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% if (items && items.length> 0) { %>
|
||||
<% items.forEach((item, index)=> { %>
|
||||
<tr>
|
||||
<td>
|
||||
<%= index + 1 %>
|
||||
</td>
|
||||
<td>
|
||||
<%= item.item_name || '' %>
|
||||
</td>
|
||||
<td>
|
||||
<%= item.customer_remark || '' %>
|
||||
</td>
|
||||
<td>
|
||||
<%= item.quantity || 0 %>
|
||||
</td>
|
||||
<td>
|
||||
<%= item.unit || '' %>
|
||||
</td>
|
||||
<td>
|
||||
<%= item.unit_price || 0 %>
|
||||
</td>
|
||||
<td>
|
||||
<%= (item.quantity || 0) * (item.unit_price || 0) %>
|
||||
</td>
|
||||
</tr>
|
||||
<% }); %>
|
||||
<% } %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- SUMMARY SECTION -->
|
||||
<div class="summary-area">
|
||||
<table class="summary-table">
|
||||
<tr>
|
||||
<td>SUB TOTAL</td>
|
||||
<td>
|
||||
<%= subtotal || 0 %>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<% if (discount && Number(discount) !==0) { %>
|
||||
<tr>
|
||||
<td>DISCOUNT (%)</td>
|
||||
<td>
|
||||
<%= discount %>
|
||||
</td>
|
||||
</tr>
|
||||
<% } %>
|
||||
|
||||
<% if (additionalCharges && Number(additionalCharges) !==0) { %>
|
||||
<tr>
|
||||
<td>ADDITIONAL CHARGES<br />(CUSTOM/HANDLING/TRANSPORT)</td>
|
||||
<td>
|
||||
<%= additionalCharges %>
|
||||
</td>
|
||||
</tr>
|
||||
<% } %>
|
||||
|
||||
<tr class="total-row">
|
||||
<td class="text-black" >GRAND TOTAL</td>
|
||||
<td>
|
||||
<%= fullTotal || 0 %>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- FOOTER -->
|
||||
<div class="footer-note">
|
||||
<img src="https://res.cloudinary.com/dtthuvvir/image/upload/v1780748661/footer_ax2jyz.png" alt="Footer">
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,527 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
background: white;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 2rem 1rem;
|
||||
font-family: 'Segoe UI', 'Roboto', 'Helvetica Neue', sans-serif;
|
||||
}
|
||||
|
||||
|
||||
/* actual table style — clean, border-collapse */
|
||||
.po-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.85rem;
|
||||
font-family: 'Segoe UI', system-ui, 'Calibri', sans-serif;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.02);
|
||||
}
|
||||
|
||||
/* header sections & company info area (using table-like but not inside main table) */
|
||||
.header-section {
|
||||
margin-bottom: 1.8rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.po-title {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 1px;
|
||||
color: #257339;
|
||||
border-top: 2px solid #257339;
|
||||
border-bottom: 2px solid #257339;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
margin-top: 1.5rem;
|
||||
font-family: 'Inter', 'Segoe UI', sans-serif;
|
||||
}
|
||||
|
||||
/* two column info grid (company, supplier, dates etc) */
|
||||
.info-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
gap: 1.2rem;
|
||||
margin-bottom: 2rem;
|
||||
background: #e1f3db;
|
||||
padding: 1rem 1.2rem;
|
||||
border: 1px solid #e2edf2;
|
||||
}
|
||||
|
||||
.info-left {
|
||||
flex: 2;
|
||||
min-width: 240px;
|
||||
}
|
||||
|
||||
.info-right {
|
||||
flex: 1.2;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 12px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-weight: 700;
|
||||
width: 140px;
|
||||
color: #257339;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
font-weight: 500;
|
||||
color: #1f2d3d;
|
||||
border-bottom: 1px dashed #bdd3e6;
|
||||
min-width: 200px;
|
||||
padding: 0 0.2rem;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.inline-placeholder {
|
||||
color: #2c7da0;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
hr {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
/* main items table */
|
||||
.items-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 1.2rem 0 1rem;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.items-table th,
|
||||
.items-table td {
|
||||
border: 1px solid #cddfe7;
|
||||
padding: 10px 6px;
|
||||
vertical-align: top;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.items-table th {
|
||||
background-color: #257339;
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
font-size: 0.8rem;
|
||||
letter-spacing: 0.5px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.items-table td {
|
||||
background-color: #fff;
|
||||
color: #1e2a3a;
|
||||
}
|
||||
|
||||
.items-table input,
|
||||
.items-table .pseudo-input {
|
||||
width: 100%;
|
||||
border: none;
|
||||
background: transparent;
|
||||
font-family: inherit;
|
||||
font-size: 0.8rem;
|
||||
padding: 4px 2px;
|
||||
outline: none;
|
||||
border-bottom: 1px dotted #acc8dd;
|
||||
}
|
||||
|
||||
.items-table input:focus {
|
||||
border-bottom: 1px solid #1f6e8c;
|
||||
background: #fefce8;
|
||||
}
|
||||
|
||||
/* numeric cells alignment */
|
||||
.numeric-cell {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.qty-cell input,
|
||||
.price-cell input {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.total-cell {
|
||||
font-weight: 600;
|
||||
background-color: #f8fafc;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* summary section (subtotal, discount, transport) */
|
||||
.summary-area {
|
||||
margin-top: 1.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
border-top: 2px solid #cbdde6;
|
||||
padding-top: 1.2rem;
|
||||
}
|
||||
|
||||
.summary-table {
|
||||
width: 320px;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.summary-table td {
|
||||
padding: 6px 8px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.summary-table td:first-child {
|
||||
font-weight: 700;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.summary-table td:last-child {
|
||||
text-align: right;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.total-row {
|
||||
border-top: 2px solid #2c5a74;
|
||||
font-weight: 800;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.discount-input,
|
||||
.transport-input {
|
||||
border: 1px solid #b9d0df;
|
||||
border-radius: 8px;
|
||||
padding: 4px 10px;
|
||||
width: 110px;
|
||||
text-align: right;
|
||||
font-weight: 500;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.inline-action {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
button.recalc {
|
||||
background: #eef2f5;
|
||||
border: 1px solid #b9cfdf;
|
||||
border-radius: 30px;
|
||||
padding: 4px 12px;
|
||||
font-size: 0.7rem;
|
||||
cursor: pointer;
|
||||
transition: 0.1s;
|
||||
margin-left: 10px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
button.recalc:hover {
|
||||
background: #cbdde6;
|
||||
}
|
||||
|
||||
.footer-note {
|
||||
margin-top: 2rem;
|
||||
font-size: 0.7rem;
|
||||
text-align: center;
|
||||
color: #547b97;
|
||||
border-top: 1px solid #d4e2ec;
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 750px) {
|
||||
.po-container {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.summary-table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.items-table th,
|
||||
.items-table td {
|
||||
padding: 6px 3px;
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
}
|
||||
|
||||
.editable-highlight {
|
||||
background-color: #fef9e3;
|
||||
} * {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
background: white;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 2rem 1rem;
|
||||
font-family: 'Segoe UI', 'Roboto', 'Helvetica Neue', sans-serif;
|
||||
}
|
||||
|
||||
|
||||
/* actual table style — clean, border-collapse */
|
||||
.po-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.85rem;
|
||||
font-family: 'Segoe UI', system-ui, 'Calibri', sans-serif;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.02);
|
||||
}
|
||||
|
||||
/* header sections & company info area (using table-like but not inside main table) */
|
||||
.header-section {
|
||||
margin-bottom: 1.8rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.po-title {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 1px;
|
||||
color: #257339;
|
||||
border-top: 2px solid #257339;
|
||||
border-bottom: 2px solid #257339;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
margin-top: 1.5rem;
|
||||
font-family: 'Inter', 'Segoe UI', sans-serif;
|
||||
}
|
||||
|
||||
/* two column info grid (company, supplier, dates etc) */
|
||||
.info-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
gap: 1.2rem;
|
||||
margin-bottom: 2rem;
|
||||
background: #e1f3db;
|
||||
padding: 1rem 1.2rem;
|
||||
border: 1px solid #e2edf2;
|
||||
}
|
||||
|
||||
.info-left {
|
||||
flex: 2;
|
||||
min-width: 240px;
|
||||
}
|
||||
|
||||
.info-right {
|
||||
flex: 1.2;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 12px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-weight: 700;
|
||||
width: 140px;
|
||||
color: #257339;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
font-weight: 500;
|
||||
color: #1f2d3d;
|
||||
border-bottom: 1px dashed #bdd3e6;
|
||||
min-width: 200px;
|
||||
padding: 0 0.2rem;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.inline-placeholder {
|
||||
color: #2c7da0;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
hr {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
/* main items table */
|
||||
.items-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 1.2rem 0 1rem;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.items-table th,
|
||||
.items-table td {
|
||||
border: 1px solid #cddfe7;
|
||||
padding: 10px 6px;
|
||||
vertical-align: top;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.items-table th {
|
||||
background-color: #257339;
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
font-size: 0.8rem;
|
||||
letter-spacing: 0.5px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.items-table td {
|
||||
background-color: #fff;
|
||||
color: #1e2a3a;
|
||||
}
|
||||
|
||||
.items-table input,
|
||||
.items-table .pseudo-input {
|
||||
width: 100%;
|
||||
border: none;
|
||||
background: transparent;
|
||||
font-family: inherit;
|
||||
font-size: 0.8rem;
|
||||
padding: 4px 2px;
|
||||
outline: none;
|
||||
border-bottom: 1px dotted #acc8dd;
|
||||
}
|
||||
|
||||
.items-table input:focus {
|
||||
border-bottom: 1px solid #1f6e8c;
|
||||
background: #fefce8;
|
||||
}
|
||||
|
||||
/* numeric cells alignment */
|
||||
.numeric-cell {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.qty-cell input,
|
||||
.price-cell input {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.total-cell {
|
||||
font-weight: 600;
|
||||
background-color: #f8fafc;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* summary section (subtotal, discount, transport) */
|
||||
.summary-area {
|
||||
margin-top: 1.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
border-top: 2px solid #cbdde6;
|
||||
padding-top: 1.2rem;
|
||||
}
|
||||
|
||||
.summary-table {
|
||||
width: 320px;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.summary-table td {
|
||||
padding: 6px 8px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.summary-table td:first-child {
|
||||
font-weight: 700;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.summary-table td:last-child {
|
||||
text-align: right;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.total-row {
|
||||
border-top: 2px solid #2c5a74;
|
||||
font-weight: 800;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.discount-input,
|
||||
.transport-input {
|
||||
border: 1px solid #b9d0df;
|
||||
border-radius: 8px;
|
||||
padding: 4px 10px;
|
||||
width: 110px;
|
||||
text-align: right;
|
||||
font-weight: 500;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.inline-action {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
button.recalc {
|
||||
background: #eef2f5;
|
||||
border: 1px solid #b9cfdf;
|
||||
border-radius: 30px;
|
||||
padding: 4px 12px;
|
||||
font-size: 0.7rem;
|
||||
cursor: pointer;
|
||||
transition: 0.1s;
|
||||
margin-left: 10px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
button.recalc:hover {
|
||||
background: #cbdde6;
|
||||
}
|
||||
|
||||
.footer-note {
|
||||
margin-top: 2rem;
|
||||
font-size: 0.7rem;
|
||||
text-align: center;
|
||||
color: #547b97;
|
||||
border-top: 1px solid #d4e2ec;
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 750px) {
|
||||
.po-container {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.summary-table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.items-table th,
|
||||
.items-table td {
|
||||
padding: 6px 3px;
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
}
|
||||
|
||||
.editable-highlight {
|
||||
background-color: #fef9e3;
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<style>
|
||||
{{styles}}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div>
|
||||
<!-- PURCHASE ORDER title -->
|
||||
<div class="header-section">
|
||||
<div>
|
||||
<img src="{{logoBase64}}" alt="Company Logo" style="height:80px; margin-bottom:-8px;" />
|
||||
</div>
|
||||
<div class="po-title">PRICE QUOTATION</div>
|
||||
</div>
|
||||
|
||||
<!-- Company & supplier info (dynamic / editable) -->
|
||||
<div class="info-grid">
|
||||
<div class="info-left">
|
||||
<div class="info-row">
|
||||
<span class="info-label">VESSEL NAME :</span>
|
||||
<span class="info-value" id="companyName">Oceanic Maritime Solutions (Pvt) Ltd</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">SUPPLY PORT :</span>
|
||||
<span class="info-value" id="supplierName">Supplier Name</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-right">
|
||||
<div class="info-row">
|
||||
<span class="info-label">CLIENT RFQ :</span>
|
||||
<span class="info-value" id="rfqNum">RFQ-001</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">OMS RFQ :</span>
|
||||
<span class="info-value" id="rfqNum">RFQ-001</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">DATE :</span>
|
||||
<span class="info-value" id="orderDate">Current Date</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- items table: ITEM NO, DESCRIPTION, REMARKS, UNIT, QTY, UNIT PRICE, TOTAL PRICE -->
|
||||
<div style="overflow-x: auto;">
|
||||
<table class="items-table" id="poItemsTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ITEM NO</th>
|
||||
<th>DESCRIPTION</th>
|
||||
<th>REMARKS</th>
|
||||
<th>QTY</th>
|
||||
<th>UNIT</th>
|
||||
<th>UNIT PRICE</th>
|
||||
<th>TOTAL PRICE</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="itemsTableBody">
|
||||
{{items}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- summary: subtotal, discount, transportation, full total -->
|
||||
<div class="summary-area">
|
||||
<table class="summary-table" id="summaryTable">
|
||||
<tr>
|
||||
<td>SUB TOTAL</td>
|
||||
<td id="subTotalVal">{{subtotal}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>DISCOUNT <span style="font-weight:normal;">(%)</span>
|
||||
</td>
|
||||
<td id="discountAmount">{{discount}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>ADDITIONAL CHARGES (CUSTOM/HANDLING/TRANSPORT)
|
||||
</td>
|
||||
<td id="transportDisplay">{{transport}}</td>
|
||||
</tr>
|
||||
<tr class="total-row">
|
||||
<td>GRAND TOTAL</td>
|
||||
<td id="fullTotalVal">{{fullTotal}}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="footer-note">
|
||||
<img src="https://res.cloudinary.com/dtthuvvir/image/upload/v1780748661/footer_ax2jyz.png" alt="Footer">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,157 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<title>Your Verification Code</title>
|
||||
<style>
|
||||
/* Reset styles */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
/* Mobile responsive */
|
||||
@media screen and (max-width: 600px) {
|
||||
.container {
|
||||
width: 100% !important;
|
||||
padding: 20px !important;
|
||||
}
|
||||
|
||||
.otp-code {
|
||||
font-size: 36px !important;
|
||||
letter-spacing: 8px !important;
|
||||
}
|
||||
|
||||
.button {
|
||||
width: 100% !important;
|
||||
text-align: center !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body style="background-color: #f4f6f9; margin: 0; padding: 0;">
|
||||
|
||||
<!-- Main Container -->
|
||||
<table width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color: #f4f6f9; padding: 40px 0;">
|
||||
<tr>
|
||||
<td align="center">
|
||||
|
||||
<!-- Email Content -->
|
||||
<table width="600" cellpadding="0" cellspacing="0" border="0" class="container"
|
||||
style="background-color: #ffffff; border-radius: 16px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05); max-width: 600px; width: 100%;">
|
||||
|
||||
<!-- Header -->
|
||||
<tr>
|
||||
<td style="padding: 40px 40px 20px 40px;" align="center">
|
||||
<img src="https://res.cloudinary.com/dhqcnszvn/image/upload/v1771106508/OCEANIC_MARITIME_SOLUTIONS_LOGO-01-removebg-preview-3_vweopp.png"
|
||||
alt="Logo" style="max-width: 150px; height: auto;">
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Main Content -->
|
||||
<tr>
|
||||
<td style="padding: 0 40px;">
|
||||
|
||||
<p
|
||||
style="color: #475569; font-size: 16px; line-height: 24px; margin-bottom: 20px; text-align: center;">
|
||||
A new temporary vendor has been added for precost No: <strong
|
||||
style="color: #3b82f6;">{{precostNo}}</strong> Please review this and proceed with
|
||||
the approval.
|
||||
</p>
|
||||
|
||||
<!-- OTP Container -->
|
||||
<table width="100%" cellpadding="0" cellspacing="0" border="0"
|
||||
style="background-color: #f8fafc; border-radius: 12px; padding: 30px; margin: 30px 0;">
|
||||
<tr>
|
||||
<td align="center">
|
||||
<span
|
||||
style="color: #64748b; font-size: 14px; text-transform: uppercase; letter-spacing: 2px; display: block; margin-bottom: 10px;">
|
||||
Vendor Name
|
||||
</span>
|
||||
<span class="otp-code"
|
||||
style="color: #0f172a; font-size: 48px; font-weight: 700; letter-spacing: 12px; font-family: 'Courier New', monospace; display: block;">
|
||||
{{vendor_name}}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<span
|
||||
style="color: #64748b; font-size: 14px; text-transform: uppercase; letter-spacing: 2px; display: block; margin-bottom: 10px;">
|
||||
Vendor Contact Person
|
||||
</span>
|
||||
<span class="otp-code"
|
||||
style="color: #0f172a; font-size: 48px; font-weight: 700; letter-spacing: 12px; font-family: 'Courier New', monospace; display: block;">
|
||||
{{contact_person}}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<span
|
||||
style="color: #64748b; font-size: 14px; text-transform: uppercase; letter-spacing: 2px; display: block; margin-bottom: 10px;">
|
||||
Vendor Email
|
||||
</span>
|
||||
<span class="otp-code"
|
||||
style="color: #0f172a; font-size: 48px; font-weight: 700; letter-spacing: 12px; font-family: 'Courier New', monospace; display: block;">
|
||||
{{email}}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<span
|
||||
style="color: #64748b; font-size: 14px; text-transform: uppercase; letter-spacing: 2px; display: block; margin-bottom: 10px;">
|
||||
Vendor Contact Number
|
||||
</span>
|
||||
<span class="otp-code"
|
||||
style="color: #0f172a; font-size: 48px; font-weight: 700; letter-spacing: 12px; font-family: 'Courier New', monospace; display: block;">
|
||||
{{phone}}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Footer -->
|
||||
<tr>
|
||||
<td style="background-color: #f8fafc; border-radius: 0 0 16px 16px; padding: 30px 40px;">
|
||||
<table width="100%" cellpadding="0" cellspacing="0" border="0">
|
||||
<tr>
|
||||
<td align="center" style="color: #94a3b8; font-size: 12px; line-height: 18px;">
|
||||
<p style="margin: 5px 0;">
|
||||
© {{currentYear}} Oceanic Maritime Solutions. All rights reserved.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<!-- End Email Content -->
|
||||
|
||||
<!-- Footer Note -->
|
||||
<table width="600" cellpadding="0" cellspacing="0" border="0"
|
||||
style="max-width: 600px; margin-top: 20px;">
|
||||
<tr>
|
||||
<td align="center" style="color: #94a3b8; font-size: 11px; padding: 0 20px;">
|
||||
This is an automated message, please do not reply to this email.
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,37 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>GLAURA 2FA Code</title>
|
||||
</head>
|
||||
<body style="font-family: Arial, Helvetica, sans-serif; font-size: 14px; color: #000; line-height: 1.6; margin: 0; padding: 20px;">
|
||||
<p>Dear {{firstName}},</p>
|
||||
|
||||
<p>Greetings from <b>Oceanic Titan</b>!</p>
|
||||
|
||||
<p>Your One Time Password (OTP) to log in to your Oceanic Titan account is mentioned below.</p>
|
||||
|
||||
<p>Please enter this OTP in the required field to proceed further:</p>
|
||||
|
||||
<table cellpadding="8" cellspacing="0" style="border-collapse: collapse; width: 400px; border: 1px solid #000;">
|
||||
<tr style="background-color: #053534; color: #ffffff; text-align: left;">
|
||||
<th style="border: 1px solid #000;">Details</th>
|
||||
<th style="border: 1px solid #000;"> </th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="border: 1px solid #000;">One Time Password (OTP)</td>
|
||||
<td style="border: 1px solid #000; text-align: center; font-weight: bold;">{{otp}}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<p>The above-mentioned OTP is valid for <b>5 minutes</b>.</p>
|
||||
|
||||
<br>
|
||||
|
||||
<p>Regards,<br>
|
||||
<b>Oceanic Titan Team</b>
|
||||
</p>
|
||||
|
||||
<p style="font-size: 12px; color: #555;">{{currentYear}} Oceanic Titan. This is an auto-generated email, please do not reply to this email.</p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,53 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Document</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
font-size: 14px;
|
||||
color: #000;
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
}
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
width: 400px;
|
||||
border: 1px solid #000;
|
||||
}
|
||||
th {
|
||||
background-color: #053534;
|
||||
color: #ffffff;
|
||||
text-align: left;
|
||||
}
|
||||
td {
|
||||
border: 1px solid #000;
|
||||
}
|
||||
a {
|
||||
color: #1a73e8;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<p>Dear {{firstName}},</p>
|
||||
|
||||
<p>Greetings from <b>Oceanic Titan</b>!</p>
|
||||
|
||||
<p>You have requested to reset your password for your Oceanic Titan account. Please click the link below to reset your password:</p>
|
||||
|
||||
<p><a href="{{resetLink}}" target="_blank">Reset Password</a></p>
|
||||
|
||||
<p>If you did not request a password reset, please ignore this email. The above link will expire in <b>{{expiryTime}}</b>.</p>
|
||||
|
||||
<br>
|
||||
|
||||
<p>Regards,<br>
|
||||
<b>Oceanic Titan Team</b>
|
||||
</p>
|
||||
|
||||
<p style="font-size: 12px; color: #555;">{{currentYear}} Oceanic Titan. This is an auto-generated email, please do not reply to this email.</p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,167 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<title>Your Verification Code</title>
|
||||
<style>
|
||||
/* Reset styles */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
/* Mobile responsive */
|
||||
@media screen and (max-width: 600px) {
|
||||
.container {
|
||||
width: 100% !important;
|
||||
padding: 20px !important;
|
||||
}
|
||||
.otp-code {
|
||||
font-size: 36px !important;
|
||||
letter-spacing: 8px !important;
|
||||
}
|
||||
.button {
|
||||
width: 100% !important;
|
||||
text-align: center !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body style="background-color: #f4f6f9; margin: 0; padding: 0;">
|
||||
|
||||
<!-- Main Container -->
|
||||
<table width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color: #f4f6f9; padding: 40px 0;">
|
||||
<tr>
|
||||
<td align="center">
|
||||
|
||||
<!-- Email Content -->
|
||||
<table width="600" cellpadding="0" cellspacing="0" border="0" class="container" style="background-color: #ffffff; border-radius: 16px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05); max-width: 600px; width: 100%;">
|
||||
|
||||
<!-- Header -->
|
||||
<tr>
|
||||
<td style="padding: 40px 40px 20px 40px;" align="center">
|
||||
<img src="https://res.cloudinary.com/dhqcnszvn/image/upload/v1771106508/OCEANIC_MARITIME_SOLUTIONS_LOGO-01-removebg-preview-3_vweopp.png" alt="Logo" style="max-width: 150px; height: auto;">
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Main Content -->
|
||||
<!-- Main Content (Updated with modern styling) -->
|
||||
<tr>
|
||||
<td style="padding: 40px;" class="inner-padding">
|
||||
<!-- Welcome Message -->
|
||||
<p style="margin: 0 0 24px 0; font-size: 16px; line-height: 1.6; color: #334155;">
|
||||
Dear {{firstName}},<br><br>
|
||||
Welcome to <strong style="color: #1e293b;">Oceanic Maritime Solutions</strong> Inquiry management system.<br>
|
||||
Please find your account details below:
|
||||
</p>
|
||||
|
||||
<!-- Credentials Card (Styled like OTP container) -->
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="background-color: #f8fafc; border-radius: 12px; padding: 30px; margin: 30px 0;">
|
||||
<tr>
|
||||
<td>
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" class="credentials-table">
|
||||
<tr>
|
||||
<td style="padding: 0 16px 0 0; width: 50%;">
|
||||
<span style="color: #64748b; font-size: 12px; text-transform: uppercase; letter-spacing: 0.05em; font-weight: 600; display: block; margin-bottom: 8px;">
|
||||
Email / Username
|
||||
</span>
|
||||
<div style="background-color: #ffffff; border: 1px solid #e2e8f0; border-radius: 8px; padding: 12px 16px; font-family: 'Courier New', monospace; font-size: 14px; color: #0f172a; font-weight: 500;">
|
||||
{{email}}
|
||||
</div>
|
||||
</td>
|
||||
<td style="padding: 0 0 0 16px; width: 50%;">
|
||||
<span style="color: #64748b; font-size: 12px; text-transform: uppercase; letter-spacing: 0.05em; font-weight: 600; display: block; margin-bottom: 8px;">
|
||||
Password
|
||||
</span>
|
||||
<div style="background-color: #ffffff; border: 1px solid #e2e8f0; border-radius: 8px; padding: 12px 16px; font-family: 'Courier New', monospace; font-size: 14px; color: #0f172a; font-weight: 500;">
|
||||
{{password}}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- Security Warning (Styled like security message) -->
|
||||
<div style="background-color: #fff7ed; border-radius: 8px; padding: 16px; margin-bottom: 30px; border-left: 4px solid #b4651a;">
|
||||
<p style="color: #9a3412; font-size: 14px; line-height: 20px; margin: 0;">
|
||||
⚡ <strong>Important:</strong> Do not share this information with anyone. Change your password immediately after logging in.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Getting Started Section (New modern card) -->
|
||||
<div style="background-color: #f1f5f9; border-radius: 12px; padding: 24px; margin: 30px 0;">
|
||||
<h3 style="color: #0f172a; font-size: 16px; font-weight: 600; margin-bottom: 16px;">
|
||||
🚀 Get Started with System:
|
||||
</h3>
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
|
||||
<tr>
|
||||
<td style="padding: 0 0 12px 0; color: #334155; font-size: 15px;">
|
||||
<span style="color: #1e293b; font-weight: 500; margin-right: 10px;">•</span> Explore your dashboard and features
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 0 0 12px 0; color: #334155; font-size: 15px;">
|
||||
<span style="color: #1e293b; font-weight: 500; margin-right: 10px;">•</span> Connect with our support team
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 0 0 0 0; color: #334155; font-size: 15px;">
|
||||
<span style="color: #1e293b; font-weight: 500; margin-right: 10px;">•</span> Stay updated with latest features
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Support Information -->
|
||||
<p style="margin: 0 0 30px 0; font-size: 15px; line-height: 1.6; color: #475569;">
|
||||
If you need any assistance, feel free to reach out to us.
|
||||
</p>
|
||||
|
||||
<!-- Welcome Message -->
|
||||
<div style="background-color: #f0f9ff; border-radius: 8px; padding: 20px; margin: 30px 0;">
|
||||
<p style="margin: 0; font-size: 16px; line-height: 1.6; color: #0c4a6e;">
|
||||
<span style="font-size: 20px; display: block; margin-bottom: 8px;">👋</span>
|
||||
Once again, welcome aboard! We're thrilled to have you with us.
|
||||
</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Footer -->
|
||||
<tr>
|
||||
<td style="background-color: #f8fafc; border-radius: 0 0 16px 16px; padding: 30px 40px;">
|
||||
<table width="100%" cellpadding="0" cellspacing="0" border="0">
|
||||
<tr>
|
||||
<td align="center" style="color: #94a3b8; font-size: 12px; line-height: 18px;">
|
||||
<p style="margin: 5px 0;">
|
||||
© {{currentYear}} Oceanic Maritime Solutions. All rights reserved.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<!-- End Email Content -->
|
||||
|
||||
<!-- Footer Note -->
|
||||
<table width="600" cellpadding="0" cellspacing="0" border="0" style="max-width: 600px; margin-top: 20px;">
|
||||
<tr>
|
||||
<td align="center" style="color: #94a3b8; font-size: 11px; padding: 0 20px;">
|
||||
This is an automated message, please do not reply to this email.
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Niolla
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is proprietary and confidential.
|
||||
* Unauthorized copying, modification, distribution, or use
|
||||
* of this file, via any medium, is strictly prohibited.
|
||||
*/
|
||||
|
||||
// app/utils/basis.util.js
|
||||
|
||||
const calculateBasis = (marginPercent, usdRate) => {
|
||||
if (!usdRate || usdRate <= 0) {
|
||||
throw new Error("Invalid USD rate");
|
||||
}
|
||||
|
||||
const multiplier = (100 + marginPercent) / 100; // e.g. 25% -> 1.25
|
||||
const basis = multiplier / usdRate;
|
||||
|
||||
return Number(basis).toFixed(10);
|
||||
};
|
||||
|
||||
module.exports = { calculateBasis };
|
||||
@@ -0,0 +1,129 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Niolla
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is proprietary and confidential.
|
||||
* Unauthorized copying, modification, distribution, or use
|
||||
* of this file, via any medium, is strictly prohibited.
|
||||
*/
|
||||
|
||||
// app/utils/cache.util.js
|
||||
|
||||
const redis = require("../config/redisClient");
|
||||
const { log } = require("./consoleLog.utill");
|
||||
const db = require("../models");
|
||||
const User = db.User;
|
||||
|
||||
|
||||
///////////////////////////////
|
||||
// User Cache //
|
||||
/////////////////////////////
|
||||
|
||||
const CACHE_TTL = 20 * 60; // 20 minutes (in seconds)
|
||||
|
||||
const getCacheKey = (email) => `user:${email}`;
|
||||
|
||||
async function getCachedUser(email) {
|
||||
const key = getCacheKey(email);
|
||||
|
||||
try {
|
||||
// 1. Check Redis first
|
||||
if (process.env.CACHE === "true") {
|
||||
const cached = await redis.get(key);
|
||||
|
||||
if (cached) {
|
||||
log("⚡ Cache HIT - returning user from Redis");
|
||||
return JSON.parse(cached);
|
||||
}
|
||||
}
|
||||
|
||||
log("Cache MISS - fetching user from DB");
|
||||
// 2. Fetch from DB
|
||||
const user = await User.findOne({ where: { email } });
|
||||
if (!user) return null;
|
||||
|
||||
const plainUser = user.get({ plain: true });
|
||||
|
||||
// 3. Store in Redis
|
||||
if (process.env.CACHE === "true") {
|
||||
await redis.set(
|
||||
key,
|
||||
JSON.stringify(plainUser),
|
||||
"EX",
|
||||
CACHE_TTL
|
||||
);
|
||||
}
|
||||
|
||||
return plainUser;
|
||||
|
||||
} catch (err) {
|
||||
log("Error fetching user:", err);
|
||||
throw new Error("Failed to fetch user");
|
||||
}
|
||||
}
|
||||
|
||||
// Invalidate cache
|
||||
async function clearUserCache(email) {
|
||||
const key = getCacheKey(email);
|
||||
|
||||
try {
|
||||
await redis.del(key);
|
||||
log(`Cache cleared for user: ${email}`);
|
||||
} catch (err) {
|
||||
log("Error clearing cache:", err);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////////
|
||||
// Permission Cache //
|
||||
/////////////////////////////
|
||||
|
||||
const PERM_TTL = 10 * 60;
|
||||
|
||||
const getPermKey = (userId) => `perm:user:${userId}`;
|
||||
|
||||
async function getCachedPermissions(userId) {
|
||||
if (process.env.CACHE !== "true") return null;
|
||||
|
||||
try {
|
||||
const cached = await redis.get(getPermKey(userId));
|
||||
return cached ? JSON.parse(cached) : null;
|
||||
} catch (err) {
|
||||
console.log("Cache read error:", err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function setCachedPermissions(userId, permissions) {
|
||||
if (process.env.CACHE !== "true") return;
|
||||
|
||||
try {
|
||||
await redis.set(
|
||||
getPermKey(userId),
|
||||
JSON.stringify(permissions),
|
||||
"EX",
|
||||
PERM_TTL
|
||||
);
|
||||
} catch (err) {
|
||||
console.log("Cache write error:", err);
|
||||
}
|
||||
}
|
||||
|
||||
async function clearPermissionCache(userId) {
|
||||
try {
|
||||
await redis.del(getPermKey(userId));
|
||||
} catch (err) {
|
||||
console.log("Cache delete error:", err);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
module.exports = {
|
||||
getCachedUser,
|
||||
clearUserCache,
|
||||
getCachedPermissions,
|
||||
setCachedPermissions,
|
||||
clearPermissionCache
|
||||
};
|
||||
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Niolla
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is proprietary and confidential.
|
||||
* Unauthorized copying, modification, distribution, or use
|
||||
* of this file, via any medium, is strictly prohibited.
|
||||
*/
|
||||
|
||||
// app/utils/calendar.util.js
|
||||
|
||||
const db = require("../models");
|
||||
const { log } = require("./consoleLog.utill");
|
||||
|
||||
const Calendar = db.calender;
|
||||
const CalenderHoliday = db.calenderHolidays;
|
||||
|
||||
|
||||
const calculateDeadline = async () => {
|
||||
let deadlineDate = new Date();
|
||||
|
||||
const currentYear = deadlineDate.getFullYear();
|
||||
|
||||
// Get calendar for current year
|
||||
const calendar = await Calendar.findOne({
|
||||
where: {
|
||||
year: currentYear
|
||||
}
|
||||
});
|
||||
|
||||
if (!calendar) {
|
||||
throw new Error(`Calendar for year ${currentYear} not found`);
|
||||
}
|
||||
|
||||
|
||||
deadlineDate.setDate(deadlineDate.getDate() + 1);
|
||||
|
||||
|
||||
while (true) {
|
||||
|
||||
const month = deadlineDate.getMonth() + 1;
|
||||
const day = deadlineDate.getDate();
|
||||
|
||||
|
||||
// Get holidays for this month
|
||||
const holidays = await CalenderHoliday.findAll({
|
||||
where: {
|
||||
calender_id: calendar.calender_id,
|
||||
month: month,
|
||||
shouldConsiderAsHoliday: true
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
const isWeekend =
|
||||
deadlineDate.getDay() === 0 ||
|
||||
deadlineDate.getDay() === 6;
|
||||
|
||||
|
||||
const holiday = holidays.find(
|
||||
h => h.day === day
|
||||
);
|
||||
|
||||
|
||||
const isHoliday =
|
||||
isWeekend || !!holiday;
|
||||
|
||||
|
||||
log(
|
||||
`${deadlineDate.toISOString().split("T")[0]} => Holiday: ${isHoliday}`
|
||||
);
|
||||
|
||||
|
||||
if (!isHoliday) {
|
||||
return deadlineDate;
|
||||
}
|
||||
|
||||
|
||||
// Move one more day
|
||||
deadlineDate.setDate(
|
||||
deadlineDate.getDate() + 1
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
calculateDeadline
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Niolla
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is proprietary and confidential.
|
||||
* Unauthorized copying, modification, distribution, or use
|
||||
* of this file, via any medium, is strictly prohibited.
|
||||
*/
|
||||
|
||||
// app/utils/consoleLog.util.js
|
||||
|
||||
const isDevelopment = process.env.NODE_ENV === "development";
|
||||
|
||||
const logQueue = require("../queues/log.queue");
|
||||
|
||||
const log = async (...args) => {
|
||||
if (isDevelopment) {
|
||||
console.log(...args);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const message = args
|
||||
.map((arg) => {
|
||||
if (arg instanceof Error) {
|
||||
return `${arg.message}\n${arg.stack}`;
|
||||
}
|
||||
|
||||
if (typeof arg === "object") {
|
||||
return JSON.stringify(arg);
|
||||
}
|
||||
|
||||
return String(arg);
|
||||
})
|
||||
.join(" ");
|
||||
|
||||
await logQueue.add("log", {
|
||||
message,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Log queue failed:", err);
|
||||
}
|
||||
};
|
||||
|
||||
function runningEnvironment() {
|
||||
return !isDevelopment;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
log,
|
||||
runningEnvironment,
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user