removed postgress from dependency checked open request enhanced in appand mail trigger from reports user activity verified approvel flow ,and tat tracker verified
This commit is contained in:
parent
2dbfcd7a56
commit
c9a0305d44
159
docs/SYSTEM_ARCHITECTURE.md
Normal file
159
docs/SYSTEM_ARCHITECTURE.md
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
# Royal Enfield Workflow Management System - Technical Architecture Definition
|
||||||
|
|
||||||
|
## 1. Platform Overview
|
||||||
|
The Royal Enfield (RE) Workflow Management System is a resilient, horizontally scalable infrastructure designed to orchestrate complex internal business processes. It utilizes a decoupled, service-oriented architecture leveraging **Node.js (TypeScript)**, **MongoDB Atlas (v8)**, and **Google Cloud Storage (GCS)** to ensure high availability and performance across enterprise workflows.
|
||||||
|
|
||||||
|
This document focus exclusively on the core platform infrastructure and custom workflow engine, excluding legacy dealer claim modules.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Global Architecture & Ingress
|
||||||
|
|
||||||
|
### A. High-Level System Architecture
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
User((User / Client))
|
||||||
|
subgraph "Public Interface"
|
||||||
|
Nginx[Nginx Reverse Proxy]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "Application Layer (Node.js)"
|
||||||
|
Auth[Auth Middleware]
|
||||||
|
Core[Workflow Service]
|
||||||
|
Dynamic[Ad-hoc Logic]
|
||||||
|
AI[Vertex AI Service]
|
||||||
|
TAT[TAT Worker / BullMQ]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "Persistence & Infrastructure"
|
||||||
|
Atlas[(MongoDB Atlas v8)]
|
||||||
|
GCS_Bucket[GCS Bucket - Artifacts]
|
||||||
|
GSM[Google Secret Manager]
|
||||||
|
Redis[(Redis Cache)]
|
||||||
|
end
|
||||||
|
|
||||||
|
User --> Nginx
|
||||||
|
Nginx --> Auth
|
||||||
|
Auth --> Core
|
||||||
|
Core --> Dynamic
|
||||||
|
Core --> Atlas
|
||||||
|
Core --> GCS_Bucket
|
||||||
|
Core --> AI
|
||||||
|
TAT --> Redis
|
||||||
|
TAT --> Atlas
|
||||||
|
Core --> GSM
|
||||||
|
```
|
||||||
|
|
||||||
|
### B. Professional Entrance: Nginx Proxy
|
||||||
|
All incoming traffic is managed by **Nginx**, acting as the "Deployed Server" facade.
|
||||||
|
- **SSL Termination**: Encrypts traffic at the edge.
|
||||||
|
- **Micro-caching**: Caches static metadata to reduce load on Node.js.
|
||||||
|
- **Proxying**: Strategically routes `/api` to the backend and serves the production React bundle for root requests.
|
||||||
|
|
||||||
|
### C. Stateless Authentication (JWT + RBAC)
|
||||||
|
The platform follows a stateless security model:
|
||||||
|
1. **JWT Validation**: `auth.middleware.ts` verifies signatures using secrets managed by **Google Secret Manager (GSM)**.
|
||||||
|
2. **Context Enrichment**: User identity is synchronized from the `users` collection in MongoDB Atlas.
|
||||||
|
3. **Granular RBAC**: Access is governed by roles (`ADMIN`, `MANAGEMENT`, `USER`) and dynamic participant checks.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Background Processing & SLA Management (BullMQ)
|
||||||
|
|
||||||
|
At the heart of the platform's performance is the **Asynchronous Task Engine** powered by **BullMQ** and **Redis**.
|
||||||
|
|
||||||
|
### A. TAT (Turnaround Time) Tracking Logic
|
||||||
|
Turnaround time is monitored per-level using a highly accurate calculation engine that accounts for:
|
||||||
|
- **Business Days/Hours**: Weekend and holiday filtering via `tatTimeUtils.ts`.
|
||||||
|
- **Priority Multipliers**: Scaling TAT for `STANDARD` vs `EXPRESS` requests.
|
||||||
|
- **Pause Impact**: Snapshot-based SLA halting during business-case pauses.
|
||||||
|
|
||||||
|
### B. TAT Worker Flow (Redis Backed)
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
Trigger[Request Assignment] --> Queue[tatQueue - BullMQ]
|
||||||
|
Queue --> Redis[(Redis Cache)]
|
||||||
|
Redis --> Worker[tatWorker.ts]
|
||||||
|
Worker --> Processor[tatProcessor.mongo.ts]
|
||||||
|
Processor --> Check{Threshold Reached?}
|
||||||
|
Check -->|50/75%| Notify[Reminder Notification]
|
||||||
|
Check -->|100%| Breach[Breach Alert + Escalation]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Multi-Channel Notification Dispatch Engine
|
||||||
|
|
||||||
|
The system ensures critical workflow events (Approvals, Breaches, Comments) reach users through three distinct synchronous and asynchronous channels.
|
||||||
|
|
||||||
|
### A. Channel Orchestration
|
||||||
|
Managed by `notification.service.ts`, the engine handles:
|
||||||
|
1. **Real-time (Socket.io)**: Immediate UI updates via room-based events.
|
||||||
|
2. **Web Push (Vapid)**: Browser-level push notifications for offline users.
|
||||||
|
3. **Enterprise Email**: Specialized services like `emailNotification.service.ts` dispatch templated HTML emails.
|
||||||
|
|
||||||
|
### B. Notification Lifecycle
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant S as Service Layer
|
||||||
|
participant N as Notification Service
|
||||||
|
participant DB as MongoDB (NotificationModel)
|
||||||
|
participant SK as Socket.io
|
||||||
|
participant E as Email Service
|
||||||
|
|
||||||
|
S->>N: Trigger Event (e.g. "Assignment")
|
||||||
|
N->>DB: Persist Notification Record (Audit)
|
||||||
|
N->>SK: broadcast(user:id, "notification:new")
|
||||||
|
N->>E: dispatchAsync(EmailTemplate)
|
||||||
|
DB-->>S: Success
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Cloud-Native Storage & Assets (GCS)
|
||||||
|
|
||||||
|
The architecture treats **Google Cloud Storage (GCS)** as a first-class citizen for both operational and deployment data.
|
||||||
|
|
||||||
|
### A. Deployment Artifact Architecture
|
||||||
|
- **Static Site Hosting**: GCS stores the compiled frontend artifacts.
|
||||||
|
- **Production Secrets**: `Google Secret Manager` ensures that no production passwords or API keys reside in the codebase.
|
||||||
|
|
||||||
|
### B. Scalable Document Storage
|
||||||
|
- **Decoupling**: Binaries are never stored in the database. MongoDB only stores the URI.
|
||||||
|
- **Privacy Mode**: Documents are retrieved via **Signed URLs** with a configurable TTL.
|
||||||
|
- **Structure**: `requests/{requestNumber}/documents/`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Real-time Collaboration (Socket.io)
|
||||||
|
|
||||||
|
Collaborative features like "Who else is viewing this request?" and "Instant Alerts" are powered by a persistent WebSocket layer.
|
||||||
|
|
||||||
|
- **Presence Tracking**: A `Map<requestId, Set<userId>>` tracks online users per workflow request.
|
||||||
|
- **Room Logic**: Users join specific "Rooms" based on their current active request view.
|
||||||
|
- **Bi-directional Sync**: Frontend emits `presence:join` when entering a request page.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Intelligent Monitoring & Observability
|
||||||
|
|
||||||
|
The platform includes a dedicated monitoring stack for "Day 2" operations.
|
||||||
|
|
||||||
|
- **Metrics (Prometheus)**: Scrapes the `/metrics` endpoint provided by our Prometheus middleware.
|
||||||
|
- **Log Aggregation (Grafana Loki)**: `promtail` ships container logs to Loki for centralized debugging.
|
||||||
|
- **Alerting**: **Alertmanager** triggers PagerDuty/Email alerts for critical system failures.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
App[RE Backend] -->|Prometheus| P[Prometheus DB]
|
||||||
|
App -->|Logs| L[Loki]
|
||||||
|
P --> G[Grafana Dashboards]
|
||||||
|
L --> G
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Dynamic Workflow Flexibility
|
||||||
|
The "Custom Workflow" module provides logic for ad-hoc adjustments:
|
||||||
|
1. **Skip Approver**: Bypasses a level while maintaining a forced audit reason.
|
||||||
|
2. **Ad-hoc Insertion**: Inserts an approver level mid-flight, dynamically recalculating the downstream chain.
|
||||||
620
package-lock.json
generated
620
package-lock.json
generated
@ -34,10 +34,7 @@
|
|||||||
"openai": "^6.8.1",
|
"openai": "^6.8.1",
|
||||||
"passport": "^0.7.0",
|
"passport": "^0.7.0",
|
||||||
"passport-jwt": "^4.0.1",
|
"passport-jwt": "^4.0.1",
|
||||||
"pg": "^8.13.1",
|
|
||||||
"pg-hstore": "^2.3.4",
|
|
||||||
"prom-client": "^15.1.3",
|
"prom-client": "^15.1.3",
|
||||||
"sequelize": "^6.37.5",
|
|
||||||
"socket.io": "^4.8.1",
|
"socket.io": "^4.8.1",
|
||||||
"uuid": "^8.3.2",
|
"uuid": "^8.3.2",
|
||||||
"web-push": "^3.6.7",
|
"web-push": "^3.6.7",
|
||||||
@ -58,7 +55,6 @@
|
|||||||
"@types/node": "^22.19.1",
|
"@types/node": "^22.19.1",
|
||||||
"@types/passport": "^1.0.16",
|
"@types/passport": "^1.0.16",
|
||||||
"@types/passport-jwt": "^4.0.1",
|
"@types/passport-jwt": "^4.0.1",
|
||||||
"@types/pg": "^8.15.6",
|
|
||||||
"@types/supertest": "^6.0.2",
|
"@types/supertest": "^6.0.2",
|
||||||
"@types/web-push": "^3.6.4",
|
"@types/web-push": "^3.6.4",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.19.1",
|
"@typescript-eslint/eslint-plugin": "^8.19.1",
|
||||||
@ -67,7 +63,6 @@
|
|||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"nodemon": "^3.1.9",
|
"nodemon": "^3.1.9",
|
||||||
"prettier": "^3.4.2",
|
"prettier": "^3.4.2",
|
||||||
"sequelize-cli": "^6.6.2",
|
|
||||||
"supertest": "^7.0.0",
|
"supertest": "^7.0.0",
|
||||||
"ts-jest": "^29.2.5",
|
"ts-jest": "^29.2.5",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
@ -2811,13 +2806,6 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@one-ini/wasm": {
|
|
||||||
"version": "0.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz",
|
|
||||||
"integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@opentelemetry/api": {
|
"node_modules/@opentelemetry/api": {
|
||||||
"version": "1.9.0",
|
"version": "1.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
|
||||||
@ -3687,15 +3675,6 @@
|
|||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/debug": {
|
|
||||||
"version": "4.1.12",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
|
|
||||||
"integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/ms": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@types/estree": {
|
"node_modules/@types/estree": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||||
@ -3839,6 +3818,7 @@
|
|||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
|
||||||
"integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
|
"integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/multer": {
|
"node_modules/@types/multer": {
|
||||||
@ -3902,18 +3882,6 @@
|
|||||||
"@types/passport": "*"
|
"@types/passport": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/pg": {
|
|
||||||
"version": "8.15.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.15.6.tgz",
|
|
||||||
"integrity": "sha512-NoaMtzhxOrubeL/7UZuNTrejB4MPAJ0RpxZqXQf2qXuVlTPuG6Y8p4u9dKRaue4yjmC7ZhzVO2/Yyyn25znrPQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/node": "*",
|
|
||||||
"pg-protocol": "*",
|
|
||||||
"pg-types": "^2.2.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@types/qs": {
|
"node_modules/@types/qs": {
|
||||||
"version": "6.14.0",
|
"version": "6.14.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz",
|
||||||
@ -4053,12 +4021,6 @@
|
|||||||
"integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==",
|
"integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/validator": {
|
|
||||||
"version": "13.15.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.4.tgz",
|
|
||||||
"integrity": "sha512-LSFfpSnJJY9wbC0LQxgvfb+ynbHftFo0tMsFOl/J4wexLnYMmDSPaj2ZyDv3TkfL1UePxPrxOWJfbiRS8mQv7A==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@types/web-push": {
|
"node_modules/@types/web-push": {
|
||||||
"version": "3.6.4",
|
"version": "3.6.4",
|
||||||
"resolved": "https://registry.npmjs.org/@types/web-push/-/web-push-3.6.4.tgz",
|
"resolved": "https://registry.npmjs.org/@types/web-push/-/web-push-3.6.4.tgz",
|
||||||
@ -4336,16 +4298,6 @@
|
|||||||
"url": "https://opencollective.com/eslint"
|
"url": "https://opencollective.com/eslint"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/abbrev": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz",
|
|
||||||
"integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
|
||||||
"engines": {
|
|
||||||
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/abort-controller": {
|
"node_modules/abort-controller": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
|
||||||
@ -4581,16 +4533,6 @@
|
|||||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/at-least-node": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 4.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/axios": {
|
"node_modules/axios": {
|
||||||
"version": "1.13.0",
|
"version": "1.13.0",
|
||||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.0.tgz",
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.0.tgz",
|
||||||
@ -4825,13 +4767,6 @@
|
|||||||
"integrity": "sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==",
|
"integrity": "sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/bluebird": {
|
|
||||||
"version": "3.7.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
|
|
||||||
"integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/bn.js": {
|
"node_modules/bn.js": {
|
||||||
"version": "4.12.2",
|
"version": "4.12.2",
|
||||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz",
|
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz",
|
||||||
@ -5327,16 +5262,6 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/commander": {
|
|
||||||
"version": "10.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz",
|
|
||||||
"integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/component-emitter": {
|
"node_modules/component-emitter": {
|
||||||
"version": "1.3.1",
|
"version": "1.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz",
|
||||||
@ -5399,17 +5324,6 @@
|
|||||||
"safe-buffer": "~5.1.0"
|
"safe-buffer": "~5.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/config-chain": {
|
|
||||||
"version": "1.1.13",
|
|
||||||
"resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz",
|
|
||||||
"integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"ini": "^1.3.4",
|
|
||||||
"proto-list": "~1.2.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/content-disposition": {
|
"node_modules/content-disposition": {
|
||||||
"version": "0.5.4",
|
"version": "0.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
|
||||||
@ -5733,12 +5647,6 @@
|
|||||||
"url": "https://dotenvx.com"
|
"url": "https://dotenvx.com"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/dottie": {
|
|
||||||
"version": "2.0.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.6.tgz",
|
|
||||||
"integrity": "sha512-iGCHkfUc5kFekGiqhe8B/mdaurD+lakO9txNnTvKtA6PISrw86LgqHvRzWYPyoE2Ph5aMIrCw9/uko6XHTKCwA==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/dunder-proto": {
|
"node_modules/dunder-proto": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||||
@ -5790,41 +5698,6 @@
|
|||||||
"safe-buffer": "^5.0.1"
|
"safe-buffer": "^5.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/editorconfig": {
|
|
||||||
"version": "1.0.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.4.tgz",
|
|
||||||
"integrity": "sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@one-ini/wasm": "0.1.1",
|
|
||||||
"commander": "^10.0.0",
|
|
||||||
"minimatch": "9.0.1",
|
|
||||||
"semver": "^7.5.3"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"editorconfig": "bin/editorconfig"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/editorconfig/node_modules/minimatch": {
|
|
||||||
"version": "9.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.1.tgz",
|
|
||||||
"integrity": "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
|
||||||
"dependencies": {
|
|
||||||
"brace-expansion": "^2.0.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=16 || 14 >=14.17"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/isaacs"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/ee-first": {
|
"node_modules/ee-first": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||||
@ -6758,22 +6631,6 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/fs-extra": {
|
|
||||||
"version": "9.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
|
|
||||||
"integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"at-least-node": "^1.0.0",
|
|
||||||
"graceful-fs": "^4.2.0",
|
|
||||||
"jsonfile": "^6.0.1",
|
|
||||||
"universalify": "^2.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/fs.realpath": {
|
"node_modules/fs.realpath": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||||
@ -7558,15 +7415,6 @@
|
|||||||
"node": ">=0.8.19"
|
"node": ">=0.8.19"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/inflection": {
|
|
||||||
"version": "1.13.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/inflection/-/inflection-1.13.4.tgz",
|
|
||||||
"integrity": "sha512-6I/HUDeYFfuNCVS3td055BaXBwKYuzw7K3ExVMStBowKo9oOAMJIXIHvdyR3iboTCp1b+1i5DSkIZTcwIktuDw==",
|
|
||||||
"engines": [
|
|
||||||
"node >= 0.4.0"
|
|
||||||
],
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/inflight": {
|
"node_modules/inflight": {
|
||||||
"version": "1.0.6",
|
"version": "1.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
||||||
@ -7585,13 +7433,6 @@
|
|||||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/ini": {
|
|
||||||
"version": "1.3.8",
|
|
||||||
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
|
|
||||||
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "ISC"
|
|
||||||
},
|
|
||||||
"node_modules/ioredis": {
|
"node_modules/ioredis": {
|
||||||
"version": "5.8.2",
|
"version": "5.8.2",
|
||||||
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.8.2.tgz",
|
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.8.2.tgz",
|
||||||
@ -8406,59 +8247,6 @@
|
|||||||
"url": "https://github.com/chalk/supports-color?sponsor=1"
|
"url": "https://github.com/chalk/supports-color?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/js-beautify": {
|
|
||||||
"version": "1.15.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.4.tgz",
|
|
||||||
"integrity": "sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"config-chain": "^1.1.13",
|
|
||||||
"editorconfig": "^1.0.4",
|
|
||||||
"glob": "^10.4.2",
|
|
||||||
"js-cookie": "^3.0.5",
|
|
||||||
"nopt": "^7.2.1"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"css-beautify": "js/bin/css-beautify.js",
|
|
||||||
"html-beautify": "js/bin/html-beautify.js",
|
|
||||||
"js-beautify": "js/bin/js-beautify.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/js-beautify/node_modules/glob": {
|
|
||||||
"version": "10.4.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
|
|
||||||
"integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
|
||||||
"dependencies": {
|
|
||||||
"foreground-child": "^3.1.0",
|
|
||||||
"jackspeak": "^3.1.2",
|
|
||||||
"minimatch": "^9.0.4",
|
|
||||||
"minipass": "^7.1.2",
|
|
||||||
"package-json-from-dist": "^1.0.0",
|
|
||||||
"path-scurry": "^1.11.1"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"glob": "dist/esm/bin.mjs"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/isaacs"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/js-cookie": {
|
|
||||||
"version": "3.0.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
|
|
||||||
"integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/js-tokens": {
|
"node_modules/js-tokens": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||||
@ -8542,19 +8330,6 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/jsonfile": {
|
|
||||||
"version": "6.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
|
|
||||||
"integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"universalify": "^2.0.0"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"graceful-fs": "^4.1.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/jsonwebtoken": {
|
"node_modules/jsonwebtoken": {
|
||||||
"version": "9.0.2",
|
"version": "9.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
|
||||||
@ -8701,12 +8476,6 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lodash": {
|
|
||||||
"version": "4.17.21",
|
|
||||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
|
||||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/lodash.camelcase": {
|
"node_modules/lodash.camelcase": {
|
||||||
"version": "4.3.0",
|
"version": "4.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
|
||||||
@ -9023,27 +8792,6 @@
|
|||||||
"mkdirp": "bin/cmd.js"
|
"mkdirp": "bin/cmd.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/moment": {
|
|
||||||
"version": "2.30.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
|
|
||||||
"integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/moment-timezone": {
|
|
||||||
"version": "0.5.48",
|
|
||||||
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.48.tgz",
|
|
||||||
"integrity": "sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"moment": "^2.29.4"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/mongodb-connection-string-url": {
|
"node_modules/mongodb-connection-string-url": {
|
||||||
"version": "7.0.1",
|
"version": "7.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-7.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-7.0.1.tgz",
|
||||||
@ -9587,22 +9335,6 @@
|
|||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/nopt": {
|
|
||||||
"version": "7.2.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz",
|
|
||||||
"integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
|
||||||
"dependencies": {
|
|
||||||
"abbrev": "^2.0.0"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"nopt": "bin/nopt.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/normalize-path": {
|
"node_modules/normalize-path": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||||
@ -9953,107 +9685,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz",
|
||||||
"integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg=="
|
"integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg=="
|
||||||
},
|
},
|
||||||
"node_modules/pg": {
|
|
||||||
"version": "8.16.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz",
|
|
||||||
"integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"pg-connection-string": "^2.9.1",
|
|
||||||
"pg-pool": "^3.10.1",
|
|
||||||
"pg-protocol": "^1.10.3",
|
|
||||||
"pg-types": "2.2.0",
|
|
||||||
"pgpass": "1.0.5"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 16.0.0"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"pg-cloudflare": "^1.2.7"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"pg-native": ">=3.0.1"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"pg-native": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/pg-cloudflare": {
|
|
||||||
"version": "1.2.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.2.7.tgz",
|
|
||||||
"integrity": "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"node_modules/pg-connection-string": {
|
|
||||||
"version": "2.9.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.1.tgz",
|
|
||||||
"integrity": "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/pg-hstore": {
|
|
||||||
"version": "2.3.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/pg-hstore/-/pg-hstore-2.3.4.tgz",
|
|
||||||
"integrity": "sha512-N3SGs/Rf+xA1M2/n0JBiXFDVMzdekwLZLAO0g7mpDY9ouX+fDI7jS6kTq3JujmYbtNSJ53TJ0q4G98KVZSM4EA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"underscore": "^1.13.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.8.x"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/pg-int8": {
|
|
||||||
"version": "1.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
|
|
||||||
"integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
|
|
||||||
"license": "ISC",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=4.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/pg-pool": {
|
|
||||||
"version": "3.10.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.10.1.tgz",
|
|
||||||
"integrity": "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"peerDependencies": {
|
|
||||||
"pg": ">=8.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/pg-protocol": {
|
|
||||||
"version": "1.10.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.10.3.tgz",
|
|
||||||
"integrity": "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/pg-types": {
|
|
||||||
"version": "2.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
|
|
||||||
"integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"pg-int8": "1.0.1",
|
|
||||||
"postgres-array": "~2.0.0",
|
|
||||||
"postgres-bytea": "~1.0.0",
|
|
||||||
"postgres-date": "~1.0.4",
|
|
||||||
"postgres-interval": "^1.1.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/pgpass": {
|
|
||||||
"version": "1.0.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
|
|
||||||
"integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"split2": "^4.1.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/picocolors": {
|
"node_modules/picocolors": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||||
@ -10166,45 +9797,6 @@
|
|||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/postgres-array": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
|
|
||||||
"integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/postgres-bytea": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/postgres-date": {
|
|
||||||
"version": "1.0.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
|
|
||||||
"integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/postgres-interval": {
|
|
||||||
"version": "1.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
|
|
||||||
"integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"xtend": "^4.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/prelude-ls": {
|
"node_modules/prelude-ls": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
||||||
@ -10292,13 +9884,6 @@
|
|||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/proto-list": {
|
|
||||||
"version": "1.2.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz",
|
|
||||||
"integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "ISC"
|
|
||||||
},
|
|
||||||
"node_modules/proto3-json-serializer": {
|
"node_modules/proto3-json-serializer": {
|
||||||
"version": "3.0.4",
|
"version": "3.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-3.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-3.0.4.tgz",
|
||||||
@ -10604,12 +10189,6 @@
|
|||||||
"node": ">= 4"
|
"node": ">= 4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/retry-as-promised": {
|
|
||||||
"version": "7.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-7.1.1.tgz",
|
|
||||||
"integrity": "sha512-hMD7odLOt3LkTjcif8aRZqi/hybjpLNgSk5oF5FCowfCjok6LukpN2bDX7R5wDmbgBQFn7YoBxSagmtXHaJYJw==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/retry-request": {
|
"node_modules/retry-request": {
|
||||||
"version": "7.0.2",
|
"version": "7.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/retry-request/-/retry-request-7.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/retry-request/-/retry-request-7.0.2.tgz",
|
||||||
@ -10780,141 +10359,6 @@
|
|||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/sequelize": {
|
|
||||||
"version": "6.37.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.37.7.tgz",
|
|
||||||
"integrity": "sha512-mCnh83zuz7kQxxJirtFD7q6Huy6liPanI67BSlbzSYgVNl5eXVdE2CN1FuAeZwG1SNpGsNRCV+bJAVVnykZAFA==",
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/sequelize"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/debug": "^4.1.8",
|
|
||||||
"@types/validator": "^13.7.17",
|
|
||||||
"debug": "^4.3.4",
|
|
||||||
"dottie": "^2.0.6",
|
|
||||||
"inflection": "^1.13.4",
|
|
||||||
"lodash": "^4.17.21",
|
|
||||||
"moment": "^2.29.4",
|
|
||||||
"moment-timezone": "^0.5.43",
|
|
||||||
"pg-connection-string": "^2.6.1",
|
|
||||||
"retry-as-promised": "^7.0.4",
|
|
||||||
"semver": "^7.5.4",
|
|
||||||
"sequelize-pool": "^7.1.0",
|
|
||||||
"toposort-class": "^1.0.1",
|
|
||||||
"uuid": "^8.3.2",
|
|
||||||
"validator": "^13.9.0",
|
|
||||||
"wkx": "^0.5.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10.0.0"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"ibm_db": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"mariadb": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"mysql2": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"oracledb": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"pg": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"pg-hstore": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"snowflake-sdk": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"sqlite3": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"tedious": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/sequelize-cli": {
|
|
||||||
"version": "6.6.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/sequelize-cli/-/sequelize-cli-6.6.3.tgz",
|
|
||||||
"integrity": "sha512-1YYPrcSRt/bpMDDSKM5ubY1mnJ2TEwIaGZcqITw4hLtGtE64nIqaBnLtMvH8VKHg6FbWpXTiFNc2mS/BtQCXZw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"fs-extra": "^9.1.0",
|
|
||||||
"js-beautify": "1.15.4",
|
|
||||||
"lodash": "^4.17.21",
|
|
||||||
"picocolors": "^1.1.1",
|
|
||||||
"resolve": "^1.22.1",
|
|
||||||
"umzug": "^2.3.0",
|
|
||||||
"yargs": "^16.2.0"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"sequelize": "lib/sequelize",
|
|
||||||
"sequelize-cli": "lib/sequelize"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/sequelize-cli/node_modules/cliui": {
|
|
||||||
"version": "7.0.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
|
|
||||||
"integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
|
||||||
"dependencies": {
|
|
||||||
"string-width": "^4.2.0",
|
|
||||||
"strip-ansi": "^6.0.0",
|
|
||||||
"wrap-ansi": "^7.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/sequelize-cli/node_modules/yargs": {
|
|
||||||
"version": "16.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
|
|
||||||
"integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"cliui": "^7.0.2",
|
|
||||||
"escalade": "^3.1.1",
|
|
||||||
"get-caller-file": "^2.0.5",
|
|
||||||
"require-directory": "^2.1.1",
|
|
||||||
"string-width": "^4.2.0",
|
|
||||||
"y18n": "^5.0.5",
|
|
||||||
"yargs-parser": "^20.2.2"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/sequelize-cli/node_modules/yargs-parser": {
|
|
||||||
"version": "20.2.9",
|
|
||||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz",
|
|
||||||
"integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/sequelize-pool": {
|
|
||||||
"version": "7.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/sequelize-pool/-/sequelize-pool-7.1.0.tgz",
|
|
||||||
"integrity": "sha512-G9c0qlIWQSK29pR/5U2JF5dDQeqqHRragoyahj/Nx4KOOQ3CPPfzxnfqFPCSB7x5UgjOgnZ61nSxz+fjDpRlJg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 10.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/serve-static": {
|
"node_modules/serve-static": {
|
||||||
"version": "1.16.2",
|
"version": "1.16.2",
|
||||||
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
|
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
|
||||||
@ -11249,15 +10693,6 @@
|
|||||||
"memory-pager": "^1.0.2"
|
"memory-pager": "^1.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/split2": {
|
|
||||||
"version": "4.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
|
|
||||||
"integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
|
|
||||||
"license": "ISC",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 10.x"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/sprintf-js": {
|
"node_modules/sprintf-js": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
|
||||||
@ -11674,12 +11109,6 @@
|
|||||||
"node": ">=0.6"
|
"node": ">=0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/toposort-class": {
|
|
||||||
"version": "1.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/toposort-class/-/toposort-class-1.0.1.tgz",
|
|
||||||
"integrity": "sha512-OsLcGGbYF3rMjPUf8oKktyvCiUxSbqMMS39m33MAjLTC1DVIH6x3WSt63/M77ihI09+Sdfk1AXvfhCEeUmC7mg==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/touch": {
|
"node_modules/touch": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz",
|
||||||
@ -12065,19 +11494,6 @@
|
|||||||
"node": ">=0.8.0"
|
"node": ">=0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/umzug": {
|
|
||||||
"version": "2.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/umzug/-/umzug-2.3.0.tgz",
|
|
||||||
"integrity": "sha512-Z274K+e8goZK8QJxmbRPhl89HPO1K+ORFtm6rySPhFKfKc5GHhqdzD0SGhSWHkzoXasqJuItdhorSvY7/Cgflw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"bluebird": "^3.7.2"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/undefsafe": {
|
"node_modules/undefsafe": {
|
||||||
"version": "2.0.5",
|
"version": "2.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
|
||||||
@ -12085,28 +11501,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/underscore": {
|
|
||||||
"version": "1.13.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz",
|
|
||||||
"integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/undici-types": {
|
"node_modules/undici-types": {
|
||||||
"version": "6.21.0",
|
"version": "6.21.0",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/universalify": {
|
|
||||||
"version": "2.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
|
|
||||||
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 10.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/unpipe": {
|
"node_modules/unpipe": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
||||||
@ -12209,15 +11609,6 @@
|
|||||||
"node": ">=10.12.0"
|
"node": ">=10.12.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/validator": {
|
|
||||||
"version": "13.15.20",
|
|
||||||
"resolved": "https://registry.npmjs.org/validator/-/validator-13.15.20.tgz",
|
|
||||||
"integrity": "sha512-KxPOq3V2LmfQPP4eqf3Mq/zrT0Dqp2Vmx2Bn285LwVahLc+CsxOM0crBHczm8ijlcjZ0Q5Xd6LW3z3odTPnlrw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/vary": {
|
"node_modules/vary": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||||
@ -12348,15 +11739,6 @@
|
|||||||
"node": ">= 12.0.0"
|
"node": ">= 12.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/wkx": {
|
|
||||||
"version": "0.5.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/wkx/-/wkx-0.5.0.tgz",
|
|
||||||
"integrity": "sha512-Xng/d4Ichh8uN4l0FToV/258EjMGU9MGcA0HV2d9B/ZpZB3lqQm7nkOdZdm5GhKtLLhAE7PiVQwN4eN+2YJJUg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/node": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/word-wrap": {
|
"node_modules/word-wrap": {
|
||||||
"version": "1.2.5",
|
"version": "1.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
|
||||||
|
|||||||
13
package.json
13
package.json
@ -16,13 +16,9 @@
|
|||||||
"type-check": "tsc --noEmit",
|
"type-check": "tsc --noEmit",
|
||||||
"clean": "rm -rf dist",
|
"clean": "rm -rf dist",
|
||||||
"setup": "ts-node -r tsconfig-paths/register src/scripts/auto-setup.ts",
|
"setup": "ts-node -r tsconfig-paths/register src/scripts/auto-setup.ts",
|
||||||
"migrate": "ts-node -r tsconfig-paths/register src/scripts/migrate.ts",
|
"seed:config": "ts-node -r tsconfig-paths/register src/scripts/seed-admin-configs.ts",
|
||||||
"seed:config": "ts-node -r tsconfig-paths/register src/scripts/seed-admin-config.ts",
|
|
||||||
"seed:test-dealer": "ts-node -r tsconfig-paths/register src/scripts/seed-test-dealer.ts",
|
|
||||||
"cleanup:dealer-claims": "ts-node -r tsconfig-paths/register src/scripts/cleanup-dealer-claims.ts",
|
|
||||||
"reset:mongo": "ts-node -r tsconfig-paths/register src/scripts/reset-mongo-db.ts",
|
"reset:mongo": "ts-node -r tsconfig-paths/register src/scripts/reset-mongo-db.ts",
|
||||||
"seed:config:mongo": "ts-node -r tsconfig-paths/register src/scripts/seed-admin-config.mongo.ts",
|
"seed:test-dealer": "ts-node -r tsconfig-paths/register src/scripts/seed-test-dealer.mongo.ts"
|
||||||
"seed:test-dealer:mongo": "ts-node -r tsconfig-paths/register src/scripts/seed-test-dealer.mongo.ts"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@google-cloud/secret-manager": "^6.1.1",
|
"@google-cloud/secret-manager": "^6.1.1",
|
||||||
@ -51,10 +47,7 @@
|
|||||||
"openai": "^6.8.1",
|
"openai": "^6.8.1",
|
||||||
"passport": "^0.7.0",
|
"passport": "^0.7.0",
|
||||||
"passport-jwt": "^4.0.1",
|
"passport-jwt": "^4.0.1",
|
||||||
"pg": "^8.13.1",
|
|
||||||
"pg-hstore": "^2.3.4",
|
|
||||||
"prom-client": "^15.1.3",
|
"prom-client": "^15.1.3",
|
||||||
"sequelize": "^6.37.5",
|
|
||||||
"socket.io": "^4.8.1",
|
"socket.io": "^4.8.1",
|
||||||
"uuid": "^8.3.2",
|
"uuid": "^8.3.2",
|
||||||
"web-push": "^3.6.7",
|
"web-push": "^3.6.7",
|
||||||
@ -75,7 +68,6 @@
|
|||||||
"@types/node": "^22.19.1",
|
"@types/node": "^22.19.1",
|
||||||
"@types/passport": "^1.0.16",
|
"@types/passport": "^1.0.16",
|
||||||
"@types/passport-jwt": "^4.0.1",
|
"@types/passport-jwt": "^4.0.1",
|
||||||
"@types/pg": "^8.15.6",
|
|
||||||
"@types/supertest": "^6.0.2",
|
"@types/supertest": "^6.0.2",
|
||||||
"@types/web-push": "^3.6.4",
|
"@types/web-push": "^3.6.4",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.19.1",
|
"@typescript-eslint/eslint-plugin": "^8.19.1",
|
||||||
@ -84,7 +76,6 @@
|
|||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"nodemon": "^3.1.9",
|
"nodemon": "^3.1.9",
|
||||||
"prettier": "^3.4.2",
|
"prettier": "^3.4.2",
|
||||||
"sequelize-cli": "^6.6.2",
|
|
||||||
"supertest": "^7.0.0",
|
"supertest": "^7.0.0",
|
||||||
"ts-jest": "^29.2.5",
|
"ts-jest": "^29.2.5",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
|
|||||||
11
src/app.ts
11
src/app.ts
@ -5,7 +5,7 @@ import dotenv from 'dotenv';
|
|||||||
import cookieParser from 'cookie-parser';
|
import cookieParser from 'cookie-parser';
|
||||||
import { UserService } from './services/user.service';
|
import { UserService } from './services/user.service';
|
||||||
import { SSOUserData } from './types/auth.types';
|
import { SSOUserData } from './types/auth.types';
|
||||||
import { sequelize } from './config/database';
|
|
||||||
import { corsMiddleware } from './middlewares/cors.middleware';
|
import { corsMiddleware } from './middlewares/cors.middleware';
|
||||||
import { metricsMiddleware, createMetricsRouter } from './middlewares/metrics.middleware';
|
import { metricsMiddleware, createMetricsRouter } from './middlewares/metrics.middleware';
|
||||||
import routes from './routes/index';
|
import routes from './routes/index';
|
||||||
@ -21,13 +21,10 @@ dotenv.config();
|
|||||||
const app: express.Application = express();
|
const app: express.Application = express();
|
||||||
const userService = new UserService();
|
const userService = new UserService();
|
||||||
|
|
||||||
// Initialize database connection
|
// Database initialization
|
||||||
const initializeDatabase = async () => {
|
const initializeDatabase = async () => {
|
||||||
try {
|
// MongoDB is connected via server.ts or separate config
|
||||||
await sequelize.authenticate();
|
// No Sequelize initialization needed
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ Database connection failed:', error);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initialize database
|
// Initialize database
|
||||||
|
|||||||
@ -1,43 +1,56 @@
|
|||||||
import { Sequelize } from 'sequelize';
|
|
||||||
import mongoose from 'mongoose';
|
import mongoose from 'mongoose';
|
||||||
import dotenv from 'dotenv';
|
import dotenv from 'dotenv';
|
||||||
|
import logger from '../utils/logger';
|
||||||
|
import dns from 'dns';
|
||||||
|
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
const sequelize = new Sequelize({
|
|
||||||
host: process.env.DB_HOST || 'localhost',
|
|
||||||
port: parseInt(process.env.DB_PORT || '5432', 10),
|
|
||||||
database: process.env.DB_NAME || 're_workflow_db',
|
|
||||||
username: process.env.DB_USER || 'postgres',
|
|
||||||
password: process.env.DB_PASSWORD || 'postgres',
|
|
||||||
dialect: 'postgres',
|
|
||||||
logging: false, // Disable SQL query logging for cleaner console output
|
|
||||||
pool: {
|
|
||||||
min: parseInt(process.env.DB_POOL_MIN || '2', 10),
|
|
||||||
max: parseInt(process.env.DB_POOL_MAX || '10', 10),
|
|
||||||
acquire: 30000,
|
|
||||||
idle: 10000,
|
|
||||||
},
|
|
||||||
dialectOptions: {
|
|
||||||
ssl: process.env.DB_SSL === 'true' ? {
|
|
||||||
require: true,
|
|
||||||
rejectUnauthorized: false,
|
|
||||||
} : false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const connectMongoDB = async () => {
|
export const connectMongoDB = async () => {
|
||||||
try {
|
try {
|
||||||
const mongoUri = process.env.MONGO_URI || process.env.MONGODB_URL || 'mongodb://localhost:27017/re_workflow_db';
|
const mongoUri = process.env.MONGO_URI || process.env.MONGODB_URL || 'mongodb://localhost:27017/re_workflow_db';
|
||||||
await mongoose.connect(mongoUri);
|
|
||||||
console.log('MongoDB Connected Successfully');
|
// Workaround for querySrv ECONNREFUSED in specific network environments (e.g. some Windows setups/VPNs)
|
||||||
} catch (error) {
|
// Set DNS servers BEFORE any connection attempt to fix SRV resolution issues
|
||||||
console.error('MongoDB Connection Error:', error);
|
if (mongoUri.startsWith('mongodb+srv://')) {
|
||||||
// Don't exit process in development if Mongo is optional for now
|
logger.info('[Database] Detected Atlas SRV URI, configuring DNS resolution...');
|
||||||
if (process.env.NODE_ENV === 'production') {
|
try {
|
||||||
process.exit(1);
|
// Set public DNS servers globally to fix Windows DNS resolution issues
|
||||||
|
dns.setServers(['8.8.8.8', '8.8.4.4', '1.1.1.1', '1.0.0.1']);
|
||||||
|
logger.info('[Database] DNS servers configured: Google DNS (8.8.8.8, 8.8.4.4) and Cloudflare DNS (1.1.1.1, 1.0.0.1)');
|
||||||
|
|
||||||
|
// Add a small delay to ensure DNS settings take effect
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
|
} catch (dnsErr) {
|
||||||
|
logger.warn('[Database] Failed to set public DNS servers:', dnsErr);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.info('[Database] Connecting to MongoDB...');
|
||||||
|
await mongoose.connect(mongoUri, {
|
||||||
|
serverSelectionTimeoutMS: 10000, // Increase timeout to 10 seconds
|
||||||
|
socketTimeoutMS: 45000,
|
||||||
|
});
|
||||||
|
logger.info('✅ MongoDB Connected Successfully');
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error('❌ MongoDB Connection Error:', error.message);
|
||||||
|
if (error.stack) {
|
||||||
|
logger.error('Stack trace:', error.stack);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provide helpful error messages
|
||||||
|
if (error.message.includes('querySrv ECONNREFUSED') || error.message.includes('ENOTFOUND')) {
|
||||||
|
logger.error('');
|
||||||
|
logger.error('🔍 DNS Resolution Failed. Possible solutions:');
|
||||||
|
logger.error(' 1. Check your internet connection');
|
||||||
|
logger.error(' 2. Verify the MongoDB Atlas cluster is running');
|
||||||
|
logger.error(' 3. Try disabling VPN if you\'re using one');
|
||||||
|
logger.error(' 4. Check Windows Firewall settings');
|
||||||
|
logger.error(' 5. Verify your MongoDB Atlas connection string is correct');
|
||||||
|
logger.error('');
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error; // Re-throw to stop server startup
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export { sequelize, mongoose };
|
export { mongoose };
|
||||||
|
|||||||
@ -28,7 +28,7 @@ export const SYSTEM_CONFIG = {
|
|||||||
|
|
||||||
// Test mode for faster testing
|
// Test mode for faster testing
|
||||||
TEST_MODE: process.env.TAT_TEST_MODE === 'true',
|
TEST_MODE: process.env.TAT_TEST_MODE === 'true',
|
||||||
TEST_TIME_MULTIPLIER: process.env.TAT_TEST_MODE === 'true' ? 1/60 : 1, // 1 hour = 1 minute in test mode
|
TEST_TIME_MULTIPLIER: process.env.TAT_TEST_MODE === 'true' ? 1 / 60 : 1, // 1 hour = 1 minute in test mode
|
||||||
|
|
||||||
// Default TAT values by priority (in hours)
|
// Default TAT values by priority (in hours)
|
||||||
DEFAULT_EXPRESS_TAT: parseInt(process.env.DEFAULT_EXPRESS_TAT || '24', 10),
|
DEFAULT_EXPRESS_TAT: parseInt(process.env.DEFAULT_EXPRESS_TAT || '24', 10),
|
||||||
@ -149,8 +149,8 @@ export async function getPublicConfig() {
|
|||||||
const { getConfigValue } = require('../services/configReader.service');
|
const { getConfigValue } = require('../services/configReader.service');
|
||||||
|
|
||||||
// Get AI configuration from admin settings (database)
|
// Get AI configuration from admin settings (database)
|
||||||
const aiEnabled = (await getConfigValue('AI_ENABLED', 'true'))?.toLowerCase() === 'true';
|
const aiEnabled = String(await getConfigValue('AI_ENABLED', 'true')).toLowerCase() === 'true';
|
||||||
const remarkGenerationEnabled = (await getConfigValue('AI_REMARK_GENERATION_ENABLED', 'true'))?.toLowerCase() === 'true';
|
const remarkGenerationEnabled = String(await getConfigValue('AI_REMARK_GENERATION_ENABLED', 'true')).toLowerCase() === 'true';
|
||||||
const maxRemarkLength = parseInt(await getConfigValue('AI_MAX_REMARK_LENGTH', '2000') || '2000', 10);
|
const maxRemarkLength = parseInt(await getConfigValue('AI_MAX_REMARK_LENGTH', '2000') || '2000', 10);
|
||||||
|
|
||||||
// Try to get AI service status (gracefully handle if not available)
|
// Try to get AI service status (gracefully handle if not available)
|
||||||
|
|||||||
@ -1,13 +1,13 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { Holiday, HolidayType } from '@models/Holiday';
|
import { HolidayModel as Holiday, HolidayType } from '../models/mongoose/Holiday.schema';
|
||||||
import { holidayMongoService as holidayService } from '@services/holiday.service';
|
import { holidayMongoService as holidayService } from '../services/holiday.service';
|
||||||
import { activityTypeService } from '@services/activityType.service';
|
import { activityTypeService } from '../services/activityType.service';
|
||||||
import { sequelize } from '../config/database'; // Import sequelize instance
|
import { adminConfigMongoService } from '../services/adminConfig.service';
|
||||||
import { QueryTypes } from 'sequelize'; // Import QueryTypes
|
import logger from '../utils/logger';
|
||||||
import logger from '@utils/logger';
|
import dayjs from 'dayjs';
|
||||||
import { initializeHolidaysCache, clearWorkingHoursCache } from '@utils/tatTimeUtils';
|
import { initializeHolidaysCache, clearWorkingHoursCache } from '../utils/tatTimeUtils';
|
||||||
import { clearConfigCache } from '@services/configReader.service';
|
import { clearConfigCache } from '../services/configReader.service';
|
||||||
import { UserModel as User, IUser } from '@models/mongoose/User.schema';
|
import { UserModel as User, IUser } from '../models/mongoose/User.schema';
|
||||||
import { UserRole } from '../types/user.types';
|
import { UserRole } from '../types/user.types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -20,10 +20,13 @@ export const getAllHolidays = async (req: Request, res: Response): Promise<void>
|
|||||||
|
|
||||||
const holidays = await holidayService.getAllActiveHolidays(yearNum);
|
const holidays = await holidayService.getAllActiveHolidays(yearNum);
|
||||||
|
|
||||||
|
// Format response to match legacy structure
|
||||||
|
const formattedHolidays = holidays.map(mapToLegacyHoliday);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: holidays,
|
data: formattedHolidays,
|
||||||
count: holidays.length
|
count: formattedHolidays.length
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('[Admin] Error fetching holidays:', error);
|
logger.error('[Admin] Error fetching holidays:', error);
|
||||||
@ -50,13 +53,17 @@ export const getHolidayCalendar = async (req: Request, res: Response): Promise<v
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const calendar = await holidayService.getHolidayCalendar(yearNum);
|
// Use getAllActiveHolidays to get full docs, then filter by year in memory or update service
|
||||||
|
// Service has getHolidayCalendar(year) which returns partial objects.
|
||||||
|
// Better to use getAllActiveHolidays(year) and map ourselves.
|
||||||
|
const holidays = await holidayService.getAllActiveHolidays(yearNum);
|
||||||
|
const formattedHolidays = holidays.map(mapToLegacyHoliday);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
year: yearNum,
|
year: yearNum,
|
||||||
holidays: calendar,
|
holidays: formattedHolidays,
|
||||||
count: calendar.length
|
count: formattedHolidays.length
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('[Admin] Error fetching holiday calendar:', error);
|
logger.error('[Admin] Error fetching holiday calendar:', error);
|
||||||
@ -102,20 +109,28 @@ export const createHoliday = async (req: Request, res: Response): Promise<void>
|
|||||||
}
|
}
|
||||||
|
|
||||||
const holiday = await holidayService.createHoliday({
|
const holiday = await holidayService.createHoliday({
|
||||||
date: holidayDate,
|
holidayDate,
|
||||||
name: holidayName,
|
holidayName,
|
||||||
type: (holidayType as any) || HolidayType.ORGANIZATIONAL,
|
holidayType: (holidayType as any) || HolidayType.ORGANIZATIONAL,
|
||||||
// explanation property removed as it is not part of the service interface
|
|
||||||
year: new Date(holidayDate).getFullYear(),
|
year: new Date(holidayDate).getFullYear(),
|
||||||
|
appliesToDepartments,
|
||||||
|
appliesToLocations,
|
||||||
|
description,
|
||||||
|
isRecurring,
|
||||||
|
recurrenceRule,
|
||||||
|
createdBy: userId
|
||||||
});
|
});
|
||||||
|
|
||||||
// Reload holidays cache
|
// Reload holidays cache
|
||||||
await initializeHolidaysCache();
|
await initializeHolidaysCache();
|
||||||
|
|
||||||
|
// Format response to match legacy structure
|
||||||
|
const legacyResponse = mapToLegacyHoliday(holiday);
|
||||||
|
|
||||||
res.status(201).json({
|
res.status(201).json({
|
||||||
success: true,
|
success: true,
|
||||||
message: 'Holiday created successfully',
|
message: 'Holiday created successfully',
|
||||||
data: holiday
|
data: [legacyResponse] // Returning array as requested
|
||||||
});
|
});
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
logger.error('[Admin] Error creating holiday:', error);
|
logger.error('[Admin] Error creating holiday:', error);
|
||||||
@ -126,6 +141,28 @@ export const createHoliday = async (req: Request, res: Response): Promise<void>
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to map Mongoose document to Legacy JSON format
|
||||||
|
*/
|
||||||
|
const mapToLegacyHoliday = (holiday: any) => ({
|
||||||
|
holidayId: holiday._id,
|
||||||
|
holidayDate: dayjs(holiday.holidayDate).format('YYYY-MM-DD'),
|
||||||
|
holidayName: holiday.holidayName,
|
||||||
|
description: holiday.description || null,
|
||||||
|
isRecurring: holiday.isRecurring || false,
|
||||||
|
recurrenceRule: holiday.recurrenceRule || null,
|
||||||
|
holidayType: holiday.holidayType,
|
||||||
|
isActive: holiday.isActive !== undefined ? holiday.isActive : true,
|
||||||
|
appliesToDepartments: (holiday.appliesToDepartments && holiday.appliesToDepartments.length > 0) ? holiday.appliesToDepartments : null,
|
||||||
|
appliesToLocations: (holiday.appliesToLocations && holiday.appliesToLocations.length > 0) ? holiday.appliesToLocations : null,
|
||||||
|
createdBy: holiday.createdBy || null,
|
||||||
|
updatedBy: holiday.updatedBy || null,
|
||||||
|
createdAt: holiday.createdAt,
|
||||||
|
updatedAt: holiday.updatedAt,
|
||||||
|
created_at: holiday.createdAt,
|
||||||
|
updated_at: holiday.updatedAt
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update a holiday
|
* Update a holiday
|
||||||
*/
|
*/
|
||||||
@ -159,7 +196,7 @@ export const updateHoliday = async (req: Request, res: Response): Promise<void>
|
|||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
message: 'Holiday updated successfully',
|
message: 'Holiday updated successfully',
|
||||||
data: holiday
|
data: [mapToLegacyHoliday(holiday)] // Returning array for consistency
|
||||||
});
|
});
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
logger.error('[Admin] Error updating holiday:', error);
|
logger.error('[Admin] Error updating holiday:', error);
|
||||||
@ -256,35 +293,7 @@ export const getPublicConfigurations = async (req: Request, res: Response): Prom
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let whereClause = '';
|
const configurations = await adminConfigMongoService.getPublicConfigurations(category as string);
|
||||||
if (category) {
|
|
||||||
whereClause = `WHERE config_category = '${category}' AND is_sensitive = false`;
|
|
||||||
} else {
|
|
||||||
whereClause = `WHERE config_category IN ('DOCUMENT_POLICY', 'TAT_SETTINGS', 'WORKFLOW_SHARING', 'SYSTEM_SETTINGS') AND is_sensitive = false`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rawConfigurations = await sequelize.query(`
|
|
||||||
SELECT
|
|
||||||
config_key,
|
|
||||||
config_category,
|
|
||||||
config_value,
|
|
||||||
value_type,
|
|
||||||
display_name,
|
|
||||||
description
|
|
||||||
FROM admin_configurations
|
|
||||||
${whereClause}
|
|
||||||
ORDER BY config_category, sort_order
|
|
||||||
`, { type: QueryTypes.SELECT });
|
|
||||||
|
|
||||||
// Map snake_case to camelCase for frontend
|
|
||||||
const configurations = (rawConfigurations as any[]).map((config: any) => ({
|
|
||||||
configKey: config.config_key,
|
|
||||||
configCategory: config.config_category,
|
|
||||||
configValue: config.config_value,
|
|
||||||
valueType: config.value_type,
|
|
||||||
displayName: config.display_name,
|
|
||||||
description: config.description
|
|
||||||
}));
|
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
@ -307,55 +316,7 @@ export const getAllConfigurations = async (req: Request, res: Response): Promise
|
|||||||
try {
|
try {
|
||||||
const { category } = req.query;
|
const { category } = req.query;
|
||||||
|
|
||||||
let whereClause = '';
|
const configurations = await adminConfigMongoService.getAllConfigurations(category as string);
|
||||||
if (category) {
|
|
||||||
whereClause = `WHERE config_category = '${category}'`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rawConfigurations = await sequelize.query(`
|
|
||||||
SELECT
|
|
||||||
config_id,
|
|
||||||
config_key,
|
|
||||||
config_category,
|
|
||||||
config_value,
|
|
||||||
value_type,
|
|
||||||
display_name,
|
|
||||||
description,
|
|
||||||
default_value,
|
|
||||||
is_editable,
|
|
||||||
is_sensitive,
|
|
||||||
validation_rules,
|
|
||||||
ui_component,
|
|
||||||
options,
|
|
||||||
sort_order,
|
|
||||||
requires_restart,
|
|
||||||
last_modified_at,
|
|
||||||
last_modified_by
|
|
||||||
FROM admin_configurations
|
|
||||||
${whereClause}
|
|
||||||
ORDER BY config_category, sort_order
|
|
||||||
`, { type: QueryTypes.SELECT });
|
|
||||||
|
|
||||||
// Map snake_case to camelCase for frontend
|
|
||||||
const configurations = (rawConfigurations as any[]).map((config: any) => ({
|
|
||||||
configId: config.config_id,
|
|
||||||
configKey: config.config_key,
|
|
||||||
configCategory: config.config_category,
|
|
||||||
configValue: config.config_value,
|
|
||||||
valueType: config.value_type,
|
|
||||||
displayName: config.display_name,
|
|
||||||
description: config.description,
|
|
||||||
defaultValue: config.default_value,
|
|
||||||
isEditable: config.is_editable,
|
|
||||||
isSensitive: config.is_sensitive || false,
|
|
||||||
validationRules: config.validation_rules,
|
|
||||||
uiComponent: config.ui_component,
|
|
||||||
options: config.options,
|
|
||||||
sortOrder: config.sort_order,
|
|
||||||
requiresRestart: config.requires_restart || false,
|
|
||||||
lastModifiedAt: config.last_modified_at,
|
|
||||||
lastModifiedBy: config.last_modified_by
|
|
||||||
}));
|
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
@ -397,22 +358,9 @@ export const updateConfiguration = async (req: Request, res: Response): Promise<
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update configuration
|
// Update configuration
|
||||||
const result = await sequelize.query(`
|
const config = await adminConfigMongoService.updateConfig(configKey, configValue, userId);
|
||||||
UPDATE admin_configurations
|
|
||||||
SET
|
|
||||||
config_value = :configValue,
|
|
||||||
last_modified_by = :userId,
|
|
||||||
last_modified_at = NOW(),
|
|
||||||
updated_at = NOW()
|
|
||||||
WHERE config_key = :configKey
|
|
||||||
AND is_editable = true
|
|
||||||
RETURNING *
|
|
||||||
`, {
|
|
||||||
replacements: { configValue, userId, configKey },
|
|
||||||
type: QueryTypes.UPDATE
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!result || (result[1] as any) === 0) {
|
if (!config) {
|
||||||
res.status(404).json({
|
res.status(404).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Configuration not found or not editable'
|
error: 'Configuration not found or not editable'
|
||||||
@ -464,15 +412,15 @@ export const resetConfiguration = async (req: Request, res: Response): Promise<v
|
|||||||
try {
|
try {
|
||||||
const { configKey } = req.params;
|
const { configKey } = req.params;
|
||||||
|
|
||||||
await sequelize.query(`
|
const config = await adminConfigMongoService.resetConfig(configKey);
|
||||||
UPDATE admin_configurations
|
|
||||||
SET config_value = default_value,
|
if (!config) {
|
||||||
updated_at = NOW()
|
res.status(404).json({
|
||||||
WHERE config_key = :configKey
|
success: false,
|
||||||
`, {
|
error: 'Configuration not found'
|
||||||
replacements: { configKey },
|
});
|
||||||
type: QueryTypes.UPDATE
|
return;
|
||||||
});
|
}
|
||||||
|
|
||||||
// Clear config cache so reset values are used immediately
|
// Clear config cache so reset values are used immediately
|
||||||
clearConfigCache();
|
clearConfigCache();
|
||||||
@ -630,27 +578,16 @@ export const getUsersByRole = async (req: Request, res: Response): Promise<void>
|
|||||||
.limit(limitNum);
|
.limit(limitNum);
|
||||||
|
|
||||||
// Get role summary (across all users, not just current page)
|
// Get role summary (across all users, not just current page)
|
||||||
const roleStats = await sequelize.query(`
|
const roleStatsRaw = await User.aggregate([
|
||||||
SELECT
|
{ $match: { isActive: true } },
|
||||||
role,
|
{ $group: { _id: '$role', count: { $sum: 1 } } },
|
||||||
COUNT(*) as count
|
{ $sort: { _id: 1 } }
|
||||||
FROM users
|
]);
|
||||||
WHERE is_active = true
|
|
||||||
GROUP BY role
|
|
||||||
ORDER BY
|
|
||||||
CASE role
|
|
||||||
WHEN 'ADMIN' THEN 1
|
|
||||||
WHEN 'MANAGEMENT' THEN 2
|
|
||||||
WHEN 'USER' THEN 3
|
|
||||||
END
|
|
||||||
`, {
|
|
||||||
type: QueryTypes.SELECT
|
|
||||||
});
|
|
||||||
|
|
||||||
const summary = {
|
const summary = {
|
||||||
ADMIN: parseInt((roleStats.find((s: any) => s.role === 'ADMIN') as any)?.count || '0'),
|
ADMIN: roleStatsRaw.find((s: any) => s._id === 'ADMIN')?.count || 0,
|
||||||
MANAGEMENT: parseInt((roleStats.find((s: any) => s.role === 'MANAGEMENT') as any)?.count || '0'),
|
MANAGEMENT: roleStatsRaw.find((s: any) => s._id === 'MANAGEMENT')?.count || 0,
|
||||||
USER: parseInt((roleStats.find((s: any) => s.role === 'USER') as any)?.count || '0')
|
USER: roleStatsRaw.find((s: any) => s._id === 'USER')?.count || 0
|
||||||
};
|
};
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
@ -687,29 +624,31 @@ export const getUsersByRole = async (req: Request, res: Response): Promise<void>
|
|||||||
*/
|
*/
|
||||||
export const getRoleStatistics = async (req: Request, res: Response): Promise<void> => {
|
export const getRoleStatistics = async (req: Request, res: Response): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
const stats = await sequelize.query(`
|
const stats = await User.aggregate([
|
||||||
SELECT
|
{
|
||||||
role,
|
$group: {
|
||||||
COUNT(*) as count,
|
_id: '$role',
|
||||||
COUNT(CASE WHEN is_active = true THEN 1 END) as active_count,
|
count: { $sum: 1 },
|
||||||
COUNT(CASE WHEN is_active = false THEN 1 END) as inactive_count
|
activeCount: { $sum: { $cond: ['$isActive', 1, 0] } },
|
||||||
FROM users
|
inactiveCount: { $sum: { $cond: ['$isActive', 0, 1] } }
|
||||||
GROUP BY role
|
}
|
||||||
ORDER BY
|
},
|
||||||
CASE role
|
{ $sort: { _id: 1 } }
|
||||||
WHEN 'ADMIN' THEN 1
|
]);
|
||||||
WHEN 'MANAGEMENT' THEN 2
|
|
||||||
WHEN 'USER' THEN 3
|
// Format for frontend
|
||||||
END
|
const formattedStats = stats.map((stat: any) => ({
|
||||||
`, {
|
role: stat._id,
|
||||||
type: QueryTypes.SELECT
|
count: stat.count,
|
||||||
});
|
active_count: stat.activeCount,
|
||||||
|
inactive_count: stat.inactiveCount
|
||||||
|
}));
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: {
|
data: {
|
||||||
statistics: stats,
|
statistics: formattedStats,
|
||||||
total: stats.reduce((sum: number, stat: any) => sum + parseInt(stat.count), 0)
|
total: formattedStats.reduce((sum: number, stat: any) => sum + stat.count, 0)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@ -24,7 +24,7 @@ export class ApprovalController {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const workflow = await WorkflowRequest.findOne({ requestNumber: level.requestId });
|
const workflow = await WorkflowRequest.findOne({ requestId: level.requestId });
|
||||||
if (!workflow) {
|
if (!workflow) {
|
||||||
ResponseHandler.notFound(res, 'Workflow not found');
|
ResponseHandler.notFound(res, 'Workflow not found');
|
||||||
return;
|
return;
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { WorkflowRequest, ApprovalLevel, WorkNote, Document, Activity, ConclusionRemark } from '@models/index';
|
import { WorkflowRequest, ApprovalLevel, WorkNote, Document, Activity, ConclusionRemark, User } from '../models'; // Fixed imports
|
||||||
import { aiService } from '@services/ai.service';
|
import { aiService } from '../services/ai.service';
|
||||||
import { activityMongoService as activityService } from '@services/activity.service';
|
import { activityMongoService as activityService } from '../services/activity.service';
|
||||||
import logger from '@utils/logger';
|
import logger from '../utils/logger';
|
||||||
import { getRequestMetadata } from '@utils/requestUtils';
|
import { getRequestMetadata } from '../utils/requestUtils';
|
||||||
|
|
||||||
export class ConclusionController {
|
export class ConclusionController {
|
||||||
/**
|
/**
|
||||||
@ -15,19 +15,16 @@ export class ConclusionController {
|
|||||||
const { requestId } = req.params;
|
const { requestId } = req.params;
|
||||||
const userId = (req as any).user?.userId;
|
const userId = (req as any).user?.userId;
|
||||||
|
|
||||||
// Fetch request with all related data
|
// Fetch request
|
||||||
const request = await WorkflowRequest.findOne({
|
// Mongoose doesn't support 'include' directly like Sequelize.
|
||||||
where: { requestId },
|
// We'll fetch the request first.
|
||||||
include: [
|
const request = await WorkflowRequest.findOne({ requestId });
|
||||||
{ association: 'initiator', attributes: ['userId', 'displayName', 'email'] }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!request) {
|
if (!request) {
|
||||||
return res.status(404).json({ error: 'Request not found' });
|
return res.status(404).json({ error: 'Request not found' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if user is the initiator
|
// Check if user is the initiator (compare userId strings)
|
||||||
if ((request as any).initiatorId !== userId) {
|
if ((request as any).initiatorId !== userId) {
|
||||||
return res.status(403).json({ error: 'Only the initiator can generate conclusion remarks' });
|
return res.status(403).json({ error: 'Only the initiator can generate conclusion remarks' });
|
||||||
}
|
}
|
||||||
@ -71,27 +68,23 @@ export class ConclusionController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Gather context for AI generation
|
// Gather context for AI generation
|
||||||
const approvalLevels = await ApprovalLevel.findAll({
|
// Mongoose: find({ requestId }), sort by levelNumber
|
||||||
where: { requestId },
|
const approvalLevels = await ApprovalLevel.find({ requestId })
|
||||||
order: [['levelNumber', 'ASC']]
|
.sort({ levelNumber: 1 });
|
||||||
});
|
|
||||||
|
|
||||||
const workNotes = await WorkNote.findAll({
|
const workNotes = await WorkNote.find({ requestId })
|
||||||
where: { requestId },
|
.sort({ createdAt: 1 })
|
||||||
order: [['createdAt', 'ASC']],
|
.limit(20);
|
||||||
limit: 20 // Last 20 work notes - keep full context for better conclusions
|
|
||||||
});
|
|
||||||
|
|
||||||
const documents = await Document.findAll({
|
const documents = await Document.find({ requestId })
|
||||||
where: { requestId },
|
.sort({ uploadedAt: -1 });
|
||||||
order: [['uploadedAt', 'DESC']]
|
|
||||||
});
|
|
||||||
|
|
||||||
const activities = await Activity.findAll({
|
const activities = await Activity.find({ requestId })
|
||||||
where: { requestId },
|
.sort({ createdAt: 1 })
|
||||||
order: [['createdAt', 'ASC']],
|
.limit(50);
|
||||||
limit: 50 // Last 50 activities - keep full context for better conclusions
|
|
||||||
});
|
// Fetch initiator details manually since we can't 'include'
|
||||||
|
const initiator = await User.findOne({ userId: (request as any).initiatorId });
|
||||||
|
|
||||||
// Build context object
|
// Build context object
|
||||||
const context = {
|
const context = {
|
||||||
@ -138,7 +131,7 @@ export class ConclusionController {
|
|||||||
const aiResult = await aiService.generateConclusionRemark(context);
|
const aiResult = await aiService.generateConclusionRemark(context);
|
||||||
|
|
||||||
// Check if conclusion already exists
|
// Check if conclusion already exists
|
||||||
let conclusionInstance = await ConclusionRemark.findOne({ where: { requestId } });
|
let conclusionInstance = await ConclusionRemark.findOne({ requestId });
|
||||||
|
|
||||||
const conclusionData = {
|
const conclusionData = {
|
||||||
aiGeneratedRemark: aiResult.remark,
|
aiGeneratedRemark: aiResult.remark,
|
||||||
@ -160,19 +153,21 @@ export class ConclusionController {
|
|||||||
|
|
||||||
if (conclusionInstance) {
|
if (conclusionInstance) {
|
||||||
// Update existing conclusion (allow regeneration)
|
// Update existing conclusion (allow regeneration)
|
||||||
await conclusionInstance.update(conclusionData as any);
|
// Mongoose document update
|
||||||
|
Object.assign(conclusionInstance, conclusionData);
|
||||||
|
await conclusionInstance.save();
|
||||||
logger.info(`[Conclusion] ✅ AI conclusion regenerated for request ${requestId}`);
|
logger.info(`[Conclusion] ✅ AI conclusion regenerated for request ${requestId}`);
|
||||||
} else {
|
} else {
|
||||||
// Create new conclusion
|
// Create new conclusion
|
||||||
conclusionInstance = await ConclusionRemark.create({
|
conclusionInstance = await ConclusionRemark.create({
|
||||||
requestId,
|
requestId,
|
||||||
...conclusionData,
|
...conclusionData,
|
||||||
finalRemark: null,
|
finalRemark: undefined,
|
||||||
editedBy: null,
|
editedBy: undefined,
|
||||||
isEdited: false,
|
isEdited: false,
|
||||||
editCount: 0,
|
editCount: 0,
|
||||||
finalizedAt: null
|
finalizedAt: undefined
|
||||||
} as any);
|
});
|
||||||
logger.info(`[Conclusion] ✅ AI conclusion generated for request ${requestId}`);
|
logger.info(`[Conclusion] ✅ AI conclusion generated for request ${requestId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -181,7 +176,7 @@ export class ConclusionController {
|
|||||||
await activityService.log({
|
await activityService.log({
|
||||||
requestId,
|
requestId,
|
||||||
type: 'ai_conclusion_generated',
|
type: 'ai_conclusion_generated',
|
||||||
user: { userId, name: (request as any).initiator?.displayName || 'Initiator' },
|
user: { userId, name: initiator?.displayName || 'Initiator' },
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
action: 'AI Conclusion Generated',
|
action: 'AI Conclusion Generated',
|
||||||
details: 'AI-powered conclusion remark generated for review',
|
details: 'AI-powered conclusion remark generated for review',
|
||||||
@ -192,7 +187,7 @@ export class ConclusionController {
|
|||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
message: 'Conclusion generated successfully',
|
message: 'Conclusion generated successfully',
|
||||||
data: {
|
data: {
|
||||||
conclusionId: (conclusionInstance as any).conclusionId,
|
conclusionId: (conclusionInstance as any).conclusionId || (conclusionInstance as any)._id,
|
||||||
aiGeneratedRemark: aiResult.remark,
|
aiGeneratedRemark: aiResult.remark,
|
||||||
keyDiscussionPoints: aiResult.keyPoints,
|
keyDiscussionPoints: aiResult.keyPoints,
|
||||||
confidence: aiResult.confidence,
|
confidence: aiResult.confidence,
|
||||||
@ -231,7 +226,7 @@ export class ConclusionController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fetch request
|
// Fetch request
|
||||||
const request = await WorkflowRequest.findOne({ where: { requestId } });
|
const request = await WorkflowRequest.findOne({ requestId });
|
||||||
if (!request) {
|
if (!request) {
|
||||||
return res.status(404).json({ error: 'Request not found' });
|
return res.status(404).json({ error: 'Request not found' });
|
||||||
}
|
}
|
||||||
@ -242,7 +237,7 @@ export class ConclusionController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Find conclusion
|
// Find conclusion
|
||||||
const conclusion = await ConclusionRemark.findOne({ where: { requestId } });
|
const conclusion = await ConclusionRemark.findOne({ requestId });
|
||||||
if (!conclusion) {
|
if (!conclusion) {
|
||||||
return res.status(404).json({ error: 'Conclusion not found. Generate it first.' });
|
return res.status(404).json({ error: 'Conclusion not found. Generate it first.' });
|
||||||
}
|
}
|
||||||
@ -250,12 +245,13 @@ export class ConclusionController {
|
|||||||
// Update conclusion
|
// Update conclusion
|
||||||
const wasEdited = (conclusion as any).aiGeneratedRemark !== finalRemark;
|
const wasEdited = (conclusion as any).aiGeneratedRemark !== finalRemark;
|
||||||
|
|
||||||
await conclusion.update({
|
conclusion.finalRemark = finalRemark;
|
||||||
finalRemark: finalRemark,
|
conclusion.editedBy = userId;
|
||||||
editedBy: userId,
|
conclusion.isEdited = wasEdited;
|
||||||
isEdited: wasEdited,
|
if (wasEdited) {
|
||||||
editCount: wasEdited ? (conclusion as any).editCount + 1 : (conclusion as any).editCount
|
conclusion.editCount = ((conclusion as any).editCount || 0) + 1;
|
||||||
} as any);
|
}
|
||||||
|
await conclusion.save();
|
||||||
|
|
||||||
logger.info(`[Conclusion] Updated conclusion for request ${requestId} (edited: ${wasEdited})`);
|
logger.info(`[Conclusion] Updated conclusion for request ${requestId} (edited: ${wasEdited})`);
|
||||||
|
|
||||||
@ -284,17 +280,15 @@ export class ConclusionController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fetch request
|
// Fetch request
|
||||||
const request = await WorkflowRequest.findOne({
|
const request = await WorkflowRequest.findOne({ requestId });
|
||||||
where: { requestId },
|
|
||||||
include: [
|
|
||||||
{ association: 'initiator', attributes: ['userId', 'displayName', 'email'] }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!request) {
|
if (!request) {
|
||||||
return res.status(404).json({ error: 'Request not found' });
|
return res.status(404).json({ error: 'Request not found' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fetch initiator manually
|
||||||
|
const initiator = await User.findOne({ userId: (request as any).initiatorId });
|
||||||
|
|
||||||
// Check if user is the initiator
|
// Check if user is the initiator
|
||||||
if ((request as any).initiatorId !== userId) {
|
if ((request as any).initiatorId !== userId) {
|
||||||
return res.status(403).json({ error: 'Only the initiator can finalize conclusion remarks' });
|
return res.status(403).json({ error: 'Only the initiator can finalize conclusion remarks' });
|
||||||
@ -306,15 +300,15 @@ export class ConclusionController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Find or create conclusion
|
// Find or create conclusion
|
||||||
let conclusion = await ConclusionRemark.findOne({ where: { requestId } });
|
let conclusion = await ConclusionRemark.findOne({ requestId });
|
||||||
|
|
||||||
if (!conclusion) {
|
if (!conclusion) {
|
||||||
// Create if doesn't exist (manual conclusion without AI)
|
// Create if doesn't exist (manual conclusion without AI)
|
||||||
conclusion = await ConclusionRemark.create({
|
conclusion = await ConclusionRemark.create({
|
||||||
requestId,
|
requestId,
|
||||||
aiGeneratedRemark: null,
|
aiGeneratedRemark: undefined,
|
||||||
aiModelUsed: null,
|
aiModelUsed: undefined,
|
||||||
aiConfidenceScore: null,
|
aiConfidenceScore: undefined,
|
||||||
finalRemark: finalRemark,
|
finalRemark: finalRemark,
|
||||||
editedBy: userId,
|
editedBy: userId,
|
||||||
isEdited: false,
|
isEdited: false,
|
||||||
@ -322,28 +316,28 @@ export class ConclusionController {
|
|||||||
approvalSummary: {},
|
approvalSummary: {},
|
||||||
documentSummary: {},
|
documentSummary: {},
|
||||||
keyDiscussionPoints: [],
|
keyDiscussionPoints: [],
|
||||||
generatedAt: null,
|
generatedAt: undefined,
|
||||||
finalizedAt: new Date()
|
finalizedAt: new Date()
|
||||||
} as any);
|
});
|
||||||
} else {
|
} else {
|
||||||
// Update existing conclusion
|
// Update existing conclusion
|
||||||
const wasEdited = (conclusion as any).aiGeneratedRemark !== finalRemark;
|
const wasEdited = (conclusion as any).aiGeneratedRemark !== finalRemark;
|
||||||
|
|
||||||
await conclusion.update({
|
conclusion.finalRemark = finalRemark;
|
||||||
finalRemark: finalRemark,
|
conclusion.editedBy = userId;
|
||||||
editedBy: userId,
|
conclusion.isEdited = wasEdited;
|
||||||
isEdited: wasEdited,
|
if (wasEdited) {
|
||||||
editCount: wasEdited ? (conclusion as any).editCount + 1 : (conclusion as any).editCount,
|
conclusion.editCount = ((conclusion as any).editCount || 0) + 1;
|
||||||
finalizedAt: new Date()
|
}
|
||||||
} as any);
|
conclusion.finalizedAt = new Date();
|
||||||
|
await conclusion.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update request status to CLOSED
|
// Update request status to CLOSED
|
||||||
await request.update({
|
request.status = 'CLOSED';
|
||||||
status: 'CLOSED',
|
(request as any).conclusionRemark = finalRemark;
|
||||||
conclusionRemark: finalRemark,
|
(request as any).closureDate = new Date();
|
||||||
closureDate: new Date()
|
await request.save();
|
||||||
} as any);
|
|
||||||
|
|
||||||
logger.info(`[Conclusion] ✅ Request ${requestId} finalized and closed`);
|
logger.info(`[Conclusion] ✅ Request ${requestId} finalized and closed`);
|
||||||
|
|
||||||
@ -351,7 +345,7 @@ export class ConclusionController {
|
|||||||
// Since the initiator is finalizing, this should always succeed
|
// Since the initiator is finalizing, this should always succeed
|
||||||
let summaryId = null;
|
let summaryId = null;
|
||||||
try {
|
try {
|
||||||
const { summaryService } = await import('@services/summary.service');
|
const { summaryService } = await import('../services/summary.service');
|
||||||
const userRole = (req as any).user?.role || (req as any).auth?.role;
|
const userRole = (req as any).user?.role || (req as any).auth?.role;
|
||||||
const summary = await summaryService.createSummary(requestId, userId, { userRole });
|
const summary = await summaryService.createSummary(requestId, userId, { userRole });
|
||||||
summaryId = (summary as any).summaryId;
|
summaryId = (summary as any).summaryId;
|
||||||
@ -367,10 +361,10 @@ export class ConclusionController {
|
|||||||
await activityService.log({
|
await activityService.log({
|
||||||
requestId,
|
requestId,
|
||||||
type: 'closed',
|
type: 'closed',
|
||||||
user: { userId, name: (request as any).initiator?.displayName || 'Initiator' },
|
user: { userId, name: initiator?.displayName || 'Initiator' },
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
action: 'Request Closed',
|
action: 'Request Closed',
|
||||||
details: `Request closed with conclusion remark by ${(request as any).initiator?.displayName}`,
|
details: `Request closed with conclusion remark by ${initiator?.displayName}`,
|
||||||
ipAddress: requestMeta.ipAddress,
|
ipAddress: requestMeta.ipAddress,
|
||||||
userAgent: requestMeta.userAgent
|
userAgent: requestMeta.userAgent
|
||||||
});
|
});
|
||||||
@ -378,7 +372,7 @@ export class ConclusionController {
|
|||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
message: 'Request finalized and closed successfully',
|
message: 'Request finalized and closed successfully',
|
||||||
data: {
|
data: {
|
||||||
conclusionId: (conclusion as any).conclusionId,
|
conclusionId: (conclusion as any).conclusionId || (conclusion as any)._id,
|
||||||
requestNumber: (request as any).requestNumber,
|
requestNumber: (request as any).requestNumber,
|
||||||
status: 'CLOSED',
|
status: 'CLOSED',
|
||||||
finalRemark: finalRemark,
|
finalRemark: finalRemark,
|
||||||
@ -400,20 +394,31 @@ export class ConclusionController {
|
|||||||
try {
|
try {
|
||||||
const { requestId } = req.params;
|
const { requestId } = req.params;
|
||||||
|
|
||||||
const conclusion = await ConclusionRemark.findOne({
|
const conclusion = await ConclusionRemark.findOne({ requestId });
|
||||||
where: { requestId },
|
|
||||||
include: [
|
|
||||||
{ association: 'editor', attributes: ['userId', 'displayName', 'email'] }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!conclusion) {
|
if (!conclusion) {
|
||||||
return res.status(404).json({ error: 'Conclusion not found' });
|
return res.status(404).json({ error: 'Conclusion not found' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Manually fetch editor if needed
|
||||||
|
let editor = null;
|
||||||
|
if (conclusion.editedBy) {
|
||||||
|
editor = await User.findOne({ userId: conclusion.editedBy });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append editor info to result if needed, or just return conclusion
|
||||||
|
const result = (conclusion as any).toJSON ? (conclusion as any).toJSON() : conclusion;
|
||||||
|
if (editor) {
|
||||||
|
result.editor = {
|
||||||
|
userId: editor.userId,
|
||||||
|
displayName: editor.displayName,
|
||||||
|
email: editor.email
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
message: 'Conclusion retrieved successfully',
|
message: 'Conclusion retrieved successfully',
|
||||||
data: conclusion
|
data: result
|
||||||
});
|
});
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
logger.error('[Conclusion] Error getting conclusion:', error);
|
logger.error('[Conclusion] Error getting conclusion:', error);
|
||||||
|
|||||||
@ -4,8 +4,7 @@ import { DealerClaimMongoService } from '../services/dealerClaim.service';
|
|||||||
import { ResponseHandler } from '../utils/responseHandler';
|
import { ResponseHandler } from '../utils/responseHandler';
|
||||||
import logger from '../utils/logger';
|
import logger from '../utils/logger';
|
||||||
import { gcsStorageService } from '../services/gcsStorage.service';
|
import { gcsStorageService } from '../services/gcsStorage.service';
|
||||||
import { Document } from '../models/Document';
|
import { Document, InternalOrder, WorkflowRequest } from '../models'; // Fixed imports
|
||||||
import { InternalOrder } from '../models/InternalOrder';
|
|
||||||
import { constants } from '../config/constants';
|
import { constants } from '../config/constants';
|
||||||
import { sapIntegrationService } from '../services/sapIntegration.service';
|
import { sapIntegrationService } from '../services/sapIntegration.service';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
@ -121,11 +120,11 @@ export class DealerClaimController {
|
|||||||
return uuidRegex.test(id);
|
return uuidRegex.test(id);
|
||||||
};
|
};
|
||||||
|
|
||||||
const { WorkflowRequest } = await import('../models/WorkflowRequest');
|
// Use WorkflowRequest from imports (Mongoose model)
|
||||||
if (isUuid(identifier)) {
|
if (isUuid(identifier)) {
|
||||||
return await WorkflowRequest.findByPk(identifier);
|
return await WorkflowRequest.findOne({ requestId: identifier });
|
||||||
} else {
|
} else {
|
||||||
return await WorkflowRequest.findOne({ where: { requestNumber: identifier } });
|
return await WorkflowRequest.findOne({ requestNumber: identifier });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -312,8 +311,9 @@ export class DealerClaimController {
|
|||||||
|
|
||||||
const extension = path.extname(file.originalname).replace('.', '').toLowerCase();
|
const extension = path.extname(file.originalname).replace('.', '').toLowerCase();
|
||||||
|
|
||||||
// Save to documents table
|
// Save to documents table (Mongoose)
|
||||||
const doc = await Document.create({
|
const doc = await Document.create({
|
||||||
|
documentId: crypto.randomUUID(), // Generate UUID if model requires it and doesn't auto-gen
|
||||||
requestId,
|
requestId,
|
||||||
uploadedBy: userId,
|
uploadedBy: userId,
|
||||||
fileName: path.basename(file.filename || file.originalname),
|
fileName: path.basename(file.filename || file.originalname),
|
||||||
@ -332,10 +332,11 @@ export class DealerClaimController {
|
|||||||
parentDocumentId: null as any,
|
parentDocumentId: null as any,
|
||||||
isDeleted: false,
|
isDeleted: false,
|
||||||
downloadCount: 0,
|
downloadCount: 0,
|
||||||
} as any);
|
uploadedAt: new Date()
|
||||||
|
});
|
||||||
|
|
||||||
completionDocuments.push({
|
completionDocuments.push({
|
||||||
documentId: doc.documentId,
|
documentId: (doc as any).documentId,
|
||||||
name: file.originalname,
|
name: file.originalname,
|
||||||
url: uploadResult.storageUrl,
|
url: uploadResult.storageUrl,
|
||||||
size: file.size,
|
size: file.size,
|
||||||
@ -373,6 +374,7 @@ export class DealerClaimController {
|
|||||||
|
|
||||||
// Save to documents table
|
// Save to documents table
|
||||||
const doc = await Document.create({
|
const doc = await Document.create({
|
||||||
|
documentId: crypto.randomUUID(),
|
||||||
requestId,
|
requestId,
|
||||||
uploadedBy: userId,
|
uploadedBy: userId,
|
||||||
fileName: path.basename(file.filename || file.originalname),
|
fileName: path.basename(file.filename || file.originalname),
|
||||||
@ -391,10 +393,11 @@ export class DealerClaimController {
|
|||||||
parentDocumentId: null as any,
|
parentDocumentId: null as any,
|
||||||
isDeleted: false,
|
isDeleted: false,
|
||||||
downloadCount: 0,
|
downloadCount: 0,
|
||||||
} as any);
|
uploadedAt: new Date()
|
||||||
|
});
|
||||||
|
|
||||||
activityPhotos.push({
|
activityPhotos.push({
|
||||||
documentId: doc.documentId,
|
documentId: (doc as any).documentId,
|
||||||
name: file.originalname,
|
name: file.originalname,
|
||||||
url: uploadResult.storageUrl,
|
url: uploadResult.storageUrl,
|
||||||
size: file.size,
|
size: file.size,
|
||||||
@ -433,6 +436,7 @@ export class DealerClaimController {
|
|||||||
|
|
||||||
// Save to documents table
|
// Save to documents table
|
||||||
const doc = await Document.create({
|
const doc = await Document.create({
|
||||||
|
documentId: crypto.randomUUID(), // UUID gen
|
||||||
requestId,
|
requestId,
|
||||||
uploadedBy: userId,
|
uploadedBy: userId,
|
||||||
fileName: path.basename(file.filename || file.originalname),
|
fileName: path.basename(file.filename || file.originalname),
|
||||||
@ -451,10 +455,11 @@ export class DealerClaimController {
|
|||||||
parentDocumentId: null as any,
|
parentDocumentId: null as any,
|
||||||
isDeleted: false,
|
isDeleted: false,
|
||||||
downloadCount: 0,
|
downloadCount: 0,
|
||||||
} as any);
|
uploadedAt: new Date()
|
||||||
|
});
|
||||||
|
|
||||||
invoicesReceipts.push({
|
invoicesReceipts.push({
|
||||||
documentId: doc.documentId,
|
documentId: (doc as any).documentId,
|
||||||
name: file.originalname,
|
name: file.originalname,
|
||||||
url: uploadResult.storageUrl,
|
url: uploadResult.storageUrl,
|
||||||
size: file.size,
|
size: file.size,
|
||||||
@ -493,6 +498,7 @@ export class DealerClaimController {
|
|||||||
|
|
||||||
// Save to documents table
|
// Save to documents table
|
||||||
const doc = await Document.create({
|
const doc = await Document.create({
|
||||||
|
documentId: crypto.randomUUID(), // UUID gen
|
||||||
requestId,
|
requestId,
|
||||||
uploadedBy: userId,
|
uploadedBy: userId,
|
||||||
fileName: path.basename(attendanceSheetFile.filename || attendanceSheetFile.originalname),
|
fileName: path.basename(attendanceSheetFile.filename || attendanceSheetFile.originalname),
|
||||||
@ -511,10 +517,11 @@ export class DealerClaimController {
|
|||||||
parentDocumentId: null as any,
|
parentDocumentId: null as any,
|
||||||
isDeleted: false,
|
isDeleted: false,
|
||||||
downloadCount: 0,
|
downloadCount: 0,
|
||||||
} as any);
|
uploadedAt: new Date()
|
||||||
|
});
|
||||||
|
|
||||||
attendanceSheet = {
|
attendanceSheet = {
|
||||||
documentId: doc.documentId,
|
documentId: (doc as any).documentId,
|
||||||
name: attendanceSheetFile.originalname,
|
name: attendanceSheetFile.originalname,
|
||||||
url: uploadResult.storageUrl,
|
url: uploadResult.storageUrl,
|
||||||
size: attendanceSheetFile.size,
|
size: attendanceSheetFile.size,
|
||||||
@ -659,7 +666,7 @@ export class DealerClaimController {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Fetch and return the updated IO details from database
|
// Fetch and return the updated IO details from database
|
||||||
const updatedIO = await InternalOrder.findOne({ where: { requestId } });
|
const updatedIO = await InternalOrder.findOne({ requestId });
|
||||||
|
|
||||||
if (updatedIO) {
|
if (updatedIO) {
|
||||||
return ResponseHandler.success(res, {
|
return ResponseHandler.success(res, {
|
||||||
@ -803,125 +810,4 @@ export class DealerClaimController {
|
|||||||
return ResponseHandler.error(res, 'Failed to update credit note details', 500, errorMessage);
|
return ResponseHandler.error(res, 'Failed to update credit note details', 500, errorMessage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Send credit note to dealer and auto-approve Step 8
|
|
||||||
* POST /api/v1/dealer-claims/:requestId/credit-note/send
|
|
||||||
* Accepts either UUID or requestNumber
|
|
||||||
*/
|
|
||||||
async sendCreditNoteToDealer(
|
|
||||||
req: AuthenticatedRequest,
|
|
||||||
res: Response
|
|
||||||
): Promise<void> {
|
|
||||||
try {
|
|
||||||
const identifier = req.params.requestId; // Can be UUID or requestNumber
|
|
||||||
const userId = req.user?.userId;
|
|
||||||
if (!userId) {
|
|
||||||
return ResponseHandler.error(res, 'Unauthorized', 401);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find workflow to get actual UUID
|
|
||||||
const workflow = await this.findWorkflowByIdentifier(identifier);
|
|
||||||
if (!workflow) {
|
|
||||||
return ResponseHandler.error(res, 'Workflow request not found', 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
const requestId = (workflow as any).requestId || (workflow as any).request_id;
|
|
||||||
if (!requestId) {
|
|
||||||
return ResponseHandler.error(res, 'Invalid workflow request', 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.dealerClaimService.sendCreditNoteToDealer(requestId, userId);
|
|
||||||
|
|
||||||
return ResponseHandler.success(res, { message: 'Credit note sent to dealer and Step 8 approved successfully' }, 'Credit note sent');
|
|
||||||
} catch (error) {
|
|
||||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
||||||
logger.error('[DealerClaimController] Error sending credit note to dealer:', error);
|
|
||||||
return ResponseHandler.error(res, 'Failed to send credit note to dealer', 500, errorMessage);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test SAP Budget Blocking (for testing/debugging)
|
|
||||||
* POST /api/v1/dealer-claims/test/sap-block
|
|
||||||
*
|
|
||||||
* This endpoint allows direct testing of SAP budget blocking without creating a full request
|
|
||||||
*/
|
|
||||||
async testSapBudgetBlock(req: AuthenticatedRequest, res: Response): Promise<void> {
|
|
||||||
try {
|
|
||||||
const userId = req.user?.userId;
|
|
||||||
if (!userId) {
|
|
||||||
return ResponseHandler.error(res, 'Unauthorized', 401);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { ioNumber, amount, requestNumber } = req.body;
|
|
||||||
|
|
||||||
// Validation
|
|
||||||
if (!ioNumber || !amount) {
|
|
||||||
return ResponseHandler.error(res, 'Missing required fields: ioNumber and amount are required', 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
const blockAmount = parseFloat(amount);
|
|
||||||
if (isNaN(blockAmount) || blockAmount <= 0) {
|
|
||||||
return ResponseHandler.error(res, 'Amount must be a positive number', 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info(`[DealerClaimController] Testing SAP budget block:`, {
|
|
||||||
ioNumber,
|
|
||||||
amount: blockAmount,
|
|
||||||
requestNumber: requestNumber || 'TEST-REQUEST',
|
|
||||||
userId
|
|
||||||
});
|
|
||||||
|
|
||||||
// First validate IO number
|
|
||||||
const ioValidation = await sapIntegrationService.validateIONumber(ioNumber);
|
|
||||||
|
|
||||||
if (!ioValidation.isValid) {
|
|
||||||
return ResponseHandler.error(res, `Invalid IO number: ${ioValidation.error || 'IO number not found in SAP'}`, 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info(`[DealerClaimController] IO validation successful:`, {
|
|
||||||
ioNumber,
|
|
||||||
availableBalance: ioValidation.availableBalance
|
|
||||||
});
|
|
||||||
|
|
||||||
// Block budget in SAP
|
|
||||||
const testRequestNumber = requestNumber || `TEST-${Date.now()}`;
|
|
||||||
const blockResult = await sapIntegrationService.blockBudget(
|
|
||||||
ioNumber,
|
|
||||||
blockAmount,
|
|
||||||
testRequestNumber,
|
|
||||||
`Test budget block for ${testRequestNumber}`
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!blockResult.success) {
|
|
||||||
return ResponseHandler.error(res, `Failed to block budget in SAP: ${blockResult.error}`, 500);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return detailed response
|
|
||||||
return ResponseHandler.success(res, {
|
|
||||||
message: 'SAP budget block test successful',
|
|
||||||
ioNumber,
|
|
||||||
requestedAmount: blockAmount,
|
|
||||||
availableBalance: ioValidation.availableBalance,
|
|
||||||
sapResponse: {
|
|
||||||
success: blockResult.success,
|
|
||||||
blockedAmount: blockResult.blockedAmount,
|
|
||||||
remainingBalance: blockResult.remainingBalance,
|
|
||||||
sapDocumentNumber: blockResult.blockId || null,
|
|
||||||
error: blockResult.error || null
|
|
||||||
},
|
|
||||||
calculatedRemainingBalance: ioValidation.availableBalance - blockResult.blockedAmount,
|
|
||||||
validation: {
|
|
||||||
isValid: ioValidation.isValid,
|
|
||||||
availableBalance: ioValidation.availableBalance,
|
|
||||||
error: ioValidation.error || null
|
|
||||||
}
|
|
||||||
}, 'SAP budget block test completed');
|
|
||||||
} catch (error: any) {
|
|
||||||
logger.error('[DealerClaimController] Error testing SAP budget block:', error);
|
|
||||||
return ResponseHandler.error(res, error.message || 'Failed to test SAP budget block', 500);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
|
|
||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import { DocumentModel } from '@models/mongoose/Document.schema';
|
import { DocumentModel } from '../models/mongoose/Document.schema';
|
||||||
import { UserModel } from '../models/mongoose/User.schema';
|
import { UserModel } from '../models/mongoose/User.schema';
|
||||||
import { WorkflowRequestModel as WorkflowRequest } from '../models/mongoose/WorkflowRequest.schema';
|
import { WorkflowRequestModel as WorkflowRequest } from '../models/mongoose/WorkflowRequest.schema';
|
||||||
import { ParticipantModel as Participant } from '../models/mongoose/Participant.schema';
|
import { ParticipantModel as Participant } from '../models/mongoose/Participant.schema';
|
||||||
@ -124,7 +125,7 @@ export class DocumentController {
|
|||||||
if (file.size > maxFileSizeBytes) {
|
if (file.size > maxFileSizeBytes) {
|
||||||
ResponseHandler.error(
|
ResponseHandler.error(
|
||||||
res,
|
res,
|
||||||
`File size exceeds the maximum allowed size of ${maxFileSizeMB}MB. Current size: ${(file.size / (1024 * 1024)).toFixed(2)}MB`,
|
`File size exceeds the maximum allowed size of ${maxFileSizeMB} MB.Current size: ${(file.size / (1024 * 1024)).toFixed(2)} MB`,
|
||||||
400
|
400
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
@ -138,7 +139,7 @@ export class DocumentController {
|
|||||||
if (!allowedFileTypes.includes(fileExtension)) {
|
if (!allowedFileTypes.includes(fileExtension)) {
|
||||||
ResponseHandler.error(
|
ResponseHandler.error(
|
||||||
res,
|
res,
|
||||||
`File type "${fileExtension}" is not allowed. Allowed types: ${allowedFileTypes.join(', ')}`,
|
`File type "${fileExtension}" is not allowed.Allowed types: ${allowedFileTypes.join(', ')} `,
|
||||||
400
|
400
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
@ -214,7 +215,7 @@ export class DocumentController {
|
|||||||
user: { userId, name: uploaderName },
|
user: { userId, name: uploaderName },
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
action: 'Document Added',
|
action: 'Document Added',
|
||||||
details: `Added ${file.originalname} as supporting document by ${uploaderName}`,
|
details: `Added ${file.originalname} as supporting document by ${uploaderName} `,
|
||||||
metadata: {
|
metadata: {
|
||||||
fileName: file.originalname,
|
fileName: file.originalname,
|
||||||
fileSize: file.size,
|
fileSize: file.size,
|
||||||
@ -288,10 +289,10 @@ export class DocumentController {
|
|||||||
|
|
||||||
await notificationService.sendToUsers(recipientIds, {
|
await notificationService.sendToUsers(recipientIds, {
|
||||||
title: 'Additional Document Added',
|
title: 'Additional Document Added',
|
||||||
body: `${uploaderName} added "${file.originalname}" to ${requestNumber}`,
|
body: `${uploaderName} added "${file.originalname}" to ${requestNumber} `,
|
||||||
requestId,
|
requestId,
|
||||||
requestNumber,
|
requestNumber,
|
||||||
url: `/request/${requestNumber}`,
|
url: `/ request / ${requestNumber} `,
|
||||||
type: 'document_added',
|
type: 'document_added',
|
||||||
priority: 'MEDIUM',
|
priority: 'MEDIUM',
|
||||||
actionRequired: false,
|
actionRequired: false,
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
|
import mongoose from 'mongoose';
|
||||||
import { NotificationModel as Notification } from '../models/mongoose/Notification.schema';
|
import { NotificationModel as Notification } from '../models/mongoose/Notification.schema';
|
||||||
import { Op } from 'sequelize';
|
import logger from '../utils/logger';
|
||||||
import logger from '@utils/logger';
|
import { notificationMongoService as notificationService } from '../services/notification.service';
|
||||||
import { notificationMongoService as notificationService } from '@services/notification.service';
|
|
||||||
|
|
||||||
export class NotificationController {
|
export class NotificationController {
|
||||||
/**
|
/**
|
||||||
@ -90,6 +90,11 @@ export class NotificationController {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!mongoose.Types.ObjectId.isValid(notificationId)) {
|
||||||
|
res.status(400).json({ success: false, message: 'Invalid notification ID' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const notification = await Notification.findOne({
|
const notification = await Notification.findOne({
|
||||||
_id: notificationId, userId
|
_id: notificationId, userId
|
||||||
});
|
});
|
||||||
@ -155,6 +160,11 @@ export class NotificationController {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!mongoose.Types.ObjectId.isValid(notificationId)) {
|
||||||
|
res.status(400).json({ success: false, message: 'Invalid notification ID' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const result = await Notification.deleteOne({
|
const result = await Notification.deleteOne({
|
||||||
_id: notificationId, userId
|
_id: notificationId, userId
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,13 +1,11 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { TatAlert } from '@models/TatAlert';
|
import { TatAlertModel as TatAlert } from '../models/mongoose/TatAlert.schema';
|
||||||
import { ApprovalLevel } from '@models/ApprovalLevel';
|
import { ApprovalLevelModel as ApprovalLevel } from '../models/mongoose/ApprovalLevel.schema';
|
||||||
import { UserModel } from '../models/mongoose/User.schema';
|
import { UserModel } from '../models/mongoose/User.schema';
|
||||||
import { WorkflowRequest } from '@models/WorkflowRequest';
|
import { WorkflowRequestModel as WorkflowRequest } from '../models/mongoose/WorkflowRequest.schema';
|
||||||
import logger from '@utils/logger';
|
import logger from '../utils/logger';
|
||||||
import { sequelize } from '@config/database';
|
import { activityMongoService as activityService } from '../services/activity.service';
|
||||||
import { QueryTypes } from 'sequelize';
|
import { getRequestMetadata } from '../utils/requestUtils';
|
||||||
import { activityMongoService as activityService } from '@services/activity.service';
|
|
||||||
import { getRequestMetadata } from '@utils/requestUtils';
|
|
||||||
import type { AuthenticatedRequest } from '../types/express';
|
import type { AuthenticatedRequest } from '../types/express';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -17,23 +15,20 @@ export const getTatAlertsByRequest = async (req: Request, res: Response) => {
|
|||||||
try {
|
try {
|
||||||
const { requestId } = req.params;
|
const { requestId } = req.params;
|
||||||
|
|
||||||
const alerts = await TatAlert.findAll({
|
const alerts = await TatAlert.find({ requestId })
|
||||||
where: { requestId },
|
.sort({ alertSentAt: 1 })
|
||||||
include: [
|
.lean();
|
||||||
{
|
|
||||||
model: ApprovalLevel,
|
|
||||||
as: 'level',
|
|
||||||
attributes: ['levelNumber', 'levelName', 'approverName', 'status']
|
|
||||||
}
|
|
||||||
],
|
|
||||||
order: [['alertSentAt', 'ASC']]
|
|
||||||
});
|
|
||||||
|
|
||||||
// Manually enrich with approver data from MongoDB
|
// Enrich with level info manually since we can't easily populate across collections if not using ObjectIds strictly for references in Mongoose style (using strings here)
|
||||||
|
// Or we can query ApprovalLevel
|
||||||
const enrichedAlerts = await Promise.all(alerts.map(async (alert: any) => {
|
const enrichedAlerts = await Promise.all(alerts.map(async (alert: any) => {
|
||||||
const alertData = alert.toJSON();
|
// Fetch level info
|
||||||
if (alertData.approverId) {
|
const level = await ApprovalLevel.findOne({ levelId: alert.levelId }).select('levelNumber levelName approverName status').lean(); // Use findOne with levelId (string)
|
||||||
const approver = await UserModel.findOne({ userId: alertData.approverId }).select('userId displayName email department');
|
|
||||||
|
const alertData = { ...alert, level };
|
||||||
|
|
||||||
|
if (alert.approverId) {
|
||||||
|
const approver = await UserModel.findOne({ userId: alert.approverId }).select('userId displayName email department').lean();
|
||||||
if (approver) {
|
if (approver) {
|
||||||
alertData.approver = {
|
alertData.approver = {
|
||||||
userId: approver.userId,
|
userId: approver.userId,
|
||||||
@ -66,10 +61,8 @@ export const getTatAlertsByLevel = async (req: Request, res: Response) => {
|
|||||||
try {
|
try {
|
||||||
const { levelId } = req.params;
|
const { levelId } = req.params;
|
||||||
|
|
||||||
const alerts = await TatAlert.findAll({
|
const alerts = await TatAlert.find({ levelId })
|
||||||
where: { levelId },
|
.sort({ alertSentAt: 1 });
|
||||||
order: [['alertSentAt', 'ASC']]
|
|
||||||
});
|
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
@ -91,31 +84,61 @@ export const getTatComplianceSummary = async (req: Request, res: Response) => {
|
|||||||
try {
|
try {
|
||||||
const { startDate, endDate } = req.query;
|
const { startDate, endDate } = req.query;
|
||||||
|
|
||||||
let dateFilter = '';
|
const matchStage: any = {};
|
||||||
if (startDate && endDate) {
|
if (startDate && endDate) {
|
||||||
dateFilter = `AND alert_sent_at BETWEEN '${startDate}' AND '${endDate}'`;
|
matchStage.alertSentAt = {
|
||||||
|
$gte: new Date(startDate as string),
|
||||||
|
$lte: new Date(endDate as string)
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const summary = await sequelize.query(`
|
const summary = await TatAlert.aggregate([
|
||||||
SELECT
|
{ $match: matchStage },
|
||||||
COUNT(*) as total_alerts,
|
{
|
||||||
COUNT(CASE WHEN alert_type = 'TAT_50' THEN 1 END) as alerts_50,
|
$group: {
|
||||||
COUNT(CASE WHEN alert_type = 'TAT_75' THEN 1 END) as alerts_75,
|
_id: null,
|
||||||
COUNT(CASE WHEN alert_type = 'TAT_100' THEN 1 END) as breaches,
|
total_alerts: { $sum: 1 },
|
||||||
COUNT(CASE WHEN was_completed_on_time = true THEN 1 END) as completed_on_time,
|
alerts_50: { $sum: { $cond: [{ $eq: ['$alertType', 'TAT_50'] }, 1, 0] } },
|
||||||
COUNT(CASE WHEN was_completed_on_time = false THEN 1 END) as completed_late,
|
alerts_75: { $sum: { $cond: [{ $eq: ['$alertType', 'TAT_75'] }, 1, 0] } },
|
||||||
ROUND(
|
breaches: { $sum: { $cond: [{ $eq: ['$alertType', 'TAT_100'] }, 1, 0] } },
|
||||||
COUNT(CASE WHEN was_completed_on_time = true THEN 1 END) * 100.0 /
|
completed_on_time: { $sum: { $cond: [{ $eq: ['$wasCompletedOnTime', true] }, 1, 0] } },
|
||||||
NULLIF(COUNT(CASE WHEN was_completed_on_time IS NOT NULL THEN 1 END), 0),
|
completed_late: { $sum: { $cond: [{ $eq: ['$wasCompletedOnTime', false] }, 1, 0] } },
|
||||||
2
|
completed_total: {
|
||||||
) as compliance_percentage
|
$sum: { $cond: [{ $ne: ['$wasCompletedOnTime', null] }, 1, 0] }
|
||||||
FROM tat_alerts
|
}
|
||||||
WHERE 1=1 ${dateFilter}
|
}
|
||||||
`, { type: QueryTypes.SELECT });
|
},
|
||||||
|
{
|
||||||
|
$project: {
|
||||||
|
_id: 0,
|
||||||
|
total_alerts: 1,
|
||||||
|
alerts_50: 1,
|
||||||
|
alerts_75: 1,
|
||||||
|
breaches: 1,
|
||||||
|
completed_on_time: 1,
|
||||||
|
completed_late: 1,
|
||||||
|
compliance_percentage: {
|
||||||
|
$cond: [
|
||||||
|
{ $eq: ['$completed_total', 0] },
|
||||||
|
0,
|
||||||
|
{ $round: [{ $multiply: [{ $divide: ['$completed_on_time', '$completed_total'] }, 100] }, 2] }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: summary[0] || {}
|
data: summary[0] || {
|
||||||
|
total_alerts: 0,
|
||||||
|
alerts_50: 0,
|
||||||
|
alerts_75: 0,
|
||||||
|
breaches: 0,
|
||||||
|
completed_on_time: 0,
|
||||||
|
completed_late: 0,
|
||||||
|
compliance_percentage: 0
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('[TAT Controller] Error fetching TAT compliance summary:', error);
|
logger.error('[TAT Controller] Error fetching TAT compliance summary:', error);
|
||||||
@ -131,32 +154,56 @@ export const getTatComplianceSummary = async (req: Request, res: Response) => {
|
|||||||
*/
|
*/
|
||||||
export const getTatBreachReport = async (req: Request, res: Response) => {
|
export const getTatBreachReport = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const breaches = await sequelize.query(`
|
const breaches = await TatAlert.aggregate([
|
||||||
SELECT
|
{ $match: { isBreached: true } },
|
||||||
ta.alert_id,
|
{ $sort: { alertSentAt: -1 } },
|
||||||
ta.request_id,
|
{ $limit: 100 },
|
||||||
w.request_number,
|
// Lookup WorkflowRequest
|
||||||
w.title as request_title,
|
{
|
||||||
w.priority,
|
$lookup: {
|
||||||
al.level_number,
|
from: 'workflow_requests',
|
||||||
al.approver_name,
|
localField: 'requestId',
|
||||||
ta.tat_hours_allocated,
|
foreignField: 'requestId',
|
||||||
ta.tat_hours_elapsed,
|
as: 'request'
|
||||||
ta.alert_sent_at,
|
}
|
||||||
ta.completion_time,
|
},
|
||||||
ta.was_completed_on_time,
|
{ $unwind: { path: '$request', preserveNullAndEmptyArrays: true } },
|
||||||
CASE
|
// Lookup ApprovalLevel
|
||||||
WHEN ta.completion_time IS NULL THEN 'Still Pending'
|
{
|
||||||
WHEN ta.was_completed_on_time = false THEN 'Completed Late'
|
$lookup: {
|
||||||
ELSE 'Completed On Time'
|
from: 'approval_levels',
|
||||||
END as completion_status
|
localField: 'levelId',
|
||||||
FROM tat_alerts ta
|
foreignField: 'levelId',
|
||||||
JOIN workflow_requests w ON ta.request_id = w.request_id
|
as: 'level'
|
||||||
JOIN approval_levels al ON ta.level_id = al.level_id
|
}
|
||||||
WHERE ta.is_breached = true
|
},
|
||||||
ORDER BY ta.alert_sent_at DESC
|
{ $unwind: { path: '$level', preserveNullAndEmptyArrays: true } },
|
||||||
LIMIT 100
|
{
|
||||||
`, { type: QueryTypes.SELECT });
|
$project: {
|
||||||
|
alert_id: '$_id',
|
||||||
|
request_id: '$requestId',
|
||||||
|
request_number: '$request.requestNumber',
|
||||||
|
request_title: '$request.title',
|
||||||
|
priority: '$request.priority',
|
||||||
|
level_number: '$level.levelNumber',
|
||||||
|
approver_name: '$level.approverName',
|
||||||
|
tat_hours_allocated: '$tatHoursAllocated',
|
||||||
|
tat_hours_elapsed: '$tatHoursElapsed',
|
||||||
|
alert_sent_at: '$alertSentAt',
|
||||||
|
completion_time: '$completionTime',
|
||||||
|
was_completed_on_time: '$wasCompletedOnTime',
|
||||||
|
completion_status: {
|
||||||
|
$switch: {
|
||||||
|
branches: [
|
||||||
|
{ case: { $eq: ['$completionTime', null] }, then: 'Still Pending' },
|
||||||
|
{ case: { $eq: ['$wasCompletedOnTime', false] }, then: 'Completed Late' }
|
||||||
|
],
|
||||||
|
default: 'Completed On Time'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
@ -196,7 +243,9 @@ export const updateBreachReason = async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get the approval level to verify permissions
|
// Get the approval level to verify permissions
|
||||||
const level = await ApprovalLevel.findByPk(levelId);
|
// Note: levelId in params likely refers to the level document UUID
|
||||||
|
const level = await ApprovalLevel.findOne({ levelId }); // Use findOne with levelId custom ID
|
||||||
|
|
||||||
if (!level) {
|
if (!level) {
|
||||||
return res.status(404).json({
|
return res.status(404).json({
|
||||||
success: false,
|
success: false,
|
||||||
@ -214,7 +263,7 @@ export const updateBreachReason = async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const userRole = user.role;
|
const userRole = user.role;
|
||||||
const approverId = (level as any).approverId;
|
const approverId = (level as any).approverId || (level.approver ? level.approver.userId : null);
|
||||||
|
|
||||||
// Check permissions: ADMIN, MANAGEMENT, or the approver
|
// Check permissions: ADMIN, MANAGEMENT, or the approver
|
||||||
const hasPermission =
|
const hasPermission =
|
||||||
@ -233,15 +282,12 @@ export const updateBreachReason = async (req: Request, res: Response) => {
|
|||||||
const userDisplayName = user.displayName || user.email || 'Unknown User';
|
const userDisplayName = user.displayName || user.email || 'Unknown User';
|
||||||
const isUpdate = !!(level as any).breachReason; // Check if this is an update or first time
|
const isUpdate = !!(level as any).breachReason; // Check if this is an update or first time
|
||||||
const levelNumber = (level as any).levelNumber;
|
const levelNumber = (level as any).levelNumber;
|
||||||
const approverName = (level as any).approverName || 'Unknown Approver';
|
const approverName = (level as any).approverName || (level.approver ? level.approver.name : 'Unknown Approver');
|
||||||
|
|
||||||
// Update breach reason directly in approval_levels table
|
// Update breach reason directly in approval_levels
|
||||||
await level.update({
|
// Mongoose update
|
||||||
breachReason: breachReason.trim()
|
(level as any).breachReason = breachReason.trim();
|
||||||
});
|
await level.save();
|
||||||
|
|
||||||
// Reload to get updated data
|
|
||||||
await level.reload();
|
|
||||||
|
|
||||||
// Log activity for the request
|
// Log activity for the request
|
||||||
const userRoleLabel = userRole === 'ADMIN' ? 'Admin' : userRole === 'MANAGEMENT' ? 'Management' : 'Approver';
|
const userRoleLabel = userRole === 'ADMIN' ? 'Admin' : userRole === 'MANAGEMENT' ? 'Management' : 'Approver';
|
||||||
@ -293,28 +339,52 @@ export const getApproverTatPerformance = async (req: Request, res: Response) =>
|
|||||||
try {
|
try {
|
||||||
const { approverId } = req.params;
|
const { approverId } = req.params;
|
||||||
|
|
||||||
const performance = await sequelize.query(`
|
const performance = await TatAlert.aggregate([
|
||||||
SELECT
|
{ $match: { approverId: approverId } },
|
||||||
COUNT(DISTINCT ta.level_id) as total_approvals,
|
{
|
||||||
COUNT(CASE WHEN ta.alert_type = 'TAT_50' THEN 1 END) as alerts_50_received,
|
$group: {
|
||||||
COUNT(CASE WHEN ta.alert_type = 'TAT_75' THEN 1 END) as alerts_75_received,
|
_id: null,
|
||||||
COUNT(CASE WHEN ta.is_breached = true THEN 1 END) as breaches,
|
total_approvals: { $addToSet: '$levelId' }, // Count distinct levels? Or count alerts? Query said count distinct level_id.
|
||||||
AVG(ta.tat_hours_elapsed) as avg_hours_taken,
|
alerts_50_received: { $sum: { $cond: [{ $eq: ['$alertType', 'TAT_50'] }, 1, 0] } },
|
||||||
ROUND(
|
alerts_75_received: { $sum: { $cond: [{ $eq: ['$alertType', 'TAT_75'] }, 1, 0] } },
|
||||||
COUNT(CASE WHEN ta.was_completed_on_time = true THEN 1 END) * 100.0 /
|
breaches: { $sum: { $cond: [{ $eq: ['$isBreached', true] }, 1, 0] } },
|
||||||
NULLIF(COUNT(CASE WHEN ta.was_completed_on_time IS NOT NULL THEN 1 END), 0),
|
min_hours: { $min: '$tatHoursElapsed' }, // Helper to ensure avg works if field exists
|
||||||
2
|
tatHoursElapsedSum: { $sum: '$tatHoursElapsed' },
|
||||||
) as compliance_rate
|
tatHoursElapsedCount: { $sum: 1 },
|
||||||
FROM tat_alerts ta
|
|
||||||
WHERE ta.approver_id = :approverId
|
completed_on_time: { $sum: { $cond: [{ $eq: ['$wasCompletedOnTime', true] }, 1, 0] } },
|
||||||
`, {
|
completed_total: { $sum: { $cond: [{ $ne: ['$wasCompletedOnTime', null] }, 1, 0] } }
|
||||||
replacements: { approverId },
|
}
|
||||||
type: QueryTypes.SELECT
|
},
|
||||||
});
|
{
|
||||||
|
$project: {
|
||||||
|
_id: 0,
|
||||||
|
total_approvals: { $size: '$total_approvals' },
|
||||||
|
alerts_50_received: 1,
|
||||||
|
alerts_75_received: 1,
|
||||||
|
breaches: 1,
|
||||||
|
avg_hours_taken: { $divide: ['$tatHoursElapsedSum', '$tatHoursElapsedCount'] },
|
||||||
|
compliance_rate: {
|
||||||
|
$cond: [
|
||||||
|
{ $eq: ['$completed_total', 0] },
|
||||||
|
0,
|
||||||
|
{ $round: [{ $multiply: [{ $divide: ['$completed_on_time', '$completed_total'] }, 100] }, 2] }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: performance[0] || {}
|
data: performance[0] || {
|
||||||
|
total_approvals: 0,
|
||||||
|
alerts_50_received: 0,
|
||||||
|
alerts_75_received: 0,
|
||||||
|
breaches: 0,
|
||||||
|
avg_hours_taken: 0,
|
||||||
|
compliance_rate: 0
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('[TAT Controller] Error fetching approver TAT performance:', error);
|
logger.error('[TAT Controller] Error fetching approver TAT performance:', error);
|
||||||
|
|||||||
@ -158,6 +158,7 @@ export class TemplateController {
|
|||||||
templateName,
|
templateName,
|
||||||
templateDescription,
|
templateDescription,
|
||||||
templateCategory,
|
templateCategory,
|
||||||
|
workflowType, // Added
|
||||||
approvalLevelsConfig,
|
approvalLevelsConfig,
|
||||||
defaultTatHours,
|
defaultTatHours,
|
||||||
formStepsConfig,
|
formStepsConfig,
|
||||||
@ -174,9 +175,10 @@ export class TemplateController {
|
|||||||
} = req.body;
|
} = req.body;
|
||||||
|
|
||||||
const template = await this.templateService.updateTemplate(templateId, userId, {
|
const template = await this.templateService.updateTemplate(templateId, userId, {
|
||||||
templateName: templateName || name,
|
name: templateName || name,
|
||||||
templateDescription: templateDescription || description,
|
description: templateDescription || description,
|
||||||
templateCategory: templateCategory || category,
|
department: templateCategory || category,
|
||||||
|
workflowType,
|
||||||
approvalLevelsConfig: approvalLevelsConfig || approvers,
|
approvalLevelsConfig: approvalLevelsConfig || approvers,
|
||||||
defaultTatHours: (defaultTatHours || suggestedSLA) ? parseFloat(defaultTatHours || suggestedSLA) : undefined,
|
defaultTatHours: (defaultTatHours || suggestedSLA) ? parseFloat(defaultTatHours || suggestedSLA) : undefined,
|
||||||
formStepsConfig,
|
formStepsConfig,
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import { ResponseHandler } from '@utils/responseHandler';
|
|||||||
import type { AuthenticatedRequest } from '../types/express';
|
import type { AuthenticatedRequest } from '../types/express';
|
||||||
import { Priority } from '../types/common.types';
|
import { Priority } from '../types/common.types';
|
||||||
import type { UpdateWorkflowRequest } from '../types/workflow.types';
|
import type { UpdateWorkflowRequest } from '../types/workflow.types';
|
||||||
import { DocumentModel } from '@models/mongoose/Document.schema';
|
import { DocumentModel } from '../models/mongoose/Document.schema';
|
||||||
import { UserModel } from '../models/mongoose/User.schema';
|
import { UserModel } from '../models/mongoose/User.schema';
|
||||||
import { gcsStorageService } from '@services/gcsStorage.service';
|
import { gcsStorageService } from '@services/gcsStorage.service';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { WorkflowTemplate } from '../models';
|
import { WorkflowTemplateModel as WorkflowTemplate } from '../models/mongoose/WorkflowTemplate.schema';
|
||||||
import logger from '../utils/logger';
|
import logger from '../utils/logger';
|
||||||
|
|
||||||
export const createTemplate = async (req: Request, res: Response) => {
|
export const createTemplate = async (req: Request, res: Response) => {
|
||||||
@ -36,10 +36,8 @@ export const createTemplate = async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
export const getTemplates = async (req: Request, res: Response) => {
|
export const getTemplates = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const templates = await WorkflowTemplate.findAll({
|
const templates = await WorkflowTemplate.find({ isActive: true })
|
||||||
where: { isActive: true },
|
.sort({ createdAt: -1 });
|
||||||
order: [['createdAt', 'DESC']]
|
|
||||||
});
|
|
||||||
|
|
||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
@ -69,7 +67,7 @@ export const updateTemplate = async (req: Request, res: Response) => {
|
|||||||
if (suggestedSLA) updates.defaultTatHours = suggestedSLA;
|
if (suggestedSLA) updates.defaultTatHours = suggestedSLA;
|
||||||
if (isActive !== undefined) updates.isActive = isActive;
|
if (isActive !== undefined) updates.isActive = isActive;
|
||||||
|
|
||||||
const template = await WorkflowTemplate.findByPk(id);
|
const template = await WorkflowTemplate.findByIdAndUpdate(id, updates, { new: true });
|
||||||
|
|
||||||
if (!template) {
|
if (!template) {
|
||||||
return res.status(404).json({
|
return res.status(404).json({
|
||||||
@ -78,8 +76,6 @@ export const updateTemplate = async (req: Request, res: Response) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await template.update(updates);
|
|
||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
message: 'Workflow template updated successfully',
|
message: 'Workflow template updated successfully',
|
||||||
@ -98,7 +94,7 @@ export const updateTemplate = async (req: Request, res: Response) => {
|
|||||||
export const deleteTemplate = async (req: Request, res: Response) => {
|
export const deleteTemplate = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const template = await WorkflowTemplate.findByPk(id);
|
const template = await WorkflowTemplate.findById(id);
|
||||||
|
|
||||||
if (!template) {
|
if (!template) {
|
||||||
return res.status(404).json({
|
return res.status(404).json({
|
||||||
@ -107,13 +103,8 @@ export const deleteTemplate = async (req: Request, res: Response) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hard delete or Soft delete based on preference.
|
// Hard delete
|
||||||
// Since we have isActive flag, let's use that (Soft Delete) or just destroy if it's unused.
|
await template.deleteOne();
|
||||||
// For now, let's do a hard delete to match the expectation of "Delete" in the UI
|
|
||||||
// unless there are FK constraints (which sequelize handles).
|
|
||||||
// Actually, safer to Soft Delete by setting isActive = false if we want history,
|
|
||||||
// but user asked for Delete. Let's do destroy.
|
|
||||||
await template.destroy();
|
|
||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
@ -86,7 +86,7 @@ async function isAdminEmailEnabled(emailType: EmailNotificationType): Promise<bo
|
|||||||
|
|
||||||
if (dbConfigValue) {
|
if (dbConfigValue) {
|
||||||
// Parse database value (it's stored as string 'true' or 'false')
|
// Parse database value (it's stored as string 'true' or 'false')
|
||||||
const dbEnabled = dbConfigValue.toLowerCase() === 'true';
|
const dbEnabled = String(dbConfigValue).toLowerCase() === 'true';
|
||||||
|
|
||||||
if (!dbEnabled) {
|
if (!dbEnabled) {
|
||||||
logger.info('[Email] Admin has disabled email notifications globally (from database config)');
|
logger.info('[Email] Admin has disabled email notifications globally (from database config)');
|
||||||
@ -194,7 +194,7 @@ async function isAdminInAppEnabled(notificationType: string): Promise<boolean> {
|
|||||||
|
|
||||||
if (dbConfigValue) {
|
if (dbConfigValue) {
|
||||||
// Parse database value (it's stored as string 'true' or 'false')
|
// Parse database value (it's stored as string 'true' or 'false')
|
||||||
const dbEnabled = dbConfigValue.toLowerCase() === 'true';
|
const dbEnabled = String(dbConfigValue).toLowerCase() === 'true';
|
||||||
|
|
||||||
if (!dbEnabled) {
|
if (!dbEnabled) {
|
||||||
logger.info('[Notification] Admin has disabled in-app notifications globally (from database config)');
|
logger.info('[Notification] Admin has disabled in-app notifications globally (from database config)');
|
||||||
|
|||||||
@ -91,6 +91,7 @@ export interface WorkflowPausedData extends BaseEmailData {
|
|||||||
pausedTime: string;
|
pausedTime: string;
|
||||||
resumeDate: string;
|
resumeDate: string;
|
||||||
pauseReason: string;
|
pauseReason: string;
|
||||||
|
isApprover?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WorkflowResumedData extends BaseEmailData {
|
export interface WorkflowResumedData extends BaseEmailData {
|
||||||
|
|||||||
@ -1,92 +0,0 @@
|
|||||||
import { QueryInterface, QueryTypes } from 'sequelize';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Migration to add AI model configuration entries
|
|
||||||
* Adds CLAUDE_MODEL, OPENAI_MODEL, and GEMINI_MODEL to admin_configurations
|
|
||||||
*
|
|
||||||
* This migration is idempotent - it will only insert if the configs don't exist
|
|
||||||
*/
|
|
||||||
export async function up(queryInterface: QueryInterface): Promise<void> {
|
|
||||||
// Insert AI model configurations if they don't exist
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
INSERT INTO admin_configurations (
|
|
||||||
config_id, config_key, config_category, config_value, value_type,
|
|
||||||
display_name, description, default_value, is_editable, is_sensitive,
|
|
||||||
validation_rules, ui_component, options, sort_order, requires_restart,
|
|
||||||
last_modified_by, last_modified_at, created_at, updated_at
|
|
||||||
) VALUES
|
|
||||||
(
|
|
||||||
gen_random_uuid(),
|
|
||||||
'CLAUDE_MODEL',
|
|
||||||
'AI_CONFIGURATION',
|
|
||||||
'claude-sonnet-4-20250514',
|
|
||||||
'STRING',
|
|
||||||
'Claude Model',
|
|
||||||
'Claude (Anthropic) model to use for AI generation',
|
|
||||||
'claude-sonnet-4-20250514',
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
'{}'::jsonb,
|
|
||||||
'input',
|
|
||||||
NULL,
|
|
||||||
27,
|
|
||||||
false,
|
|
||||||
NULL,
|
|
||||||
NULL,
|
|
||||||
NOW(),
|
|
||||||
NOW()
|
|
||||||
),
|
|
||||||
(
|
|
||||||
gen_random_uuid(),
|
|
||||||
'OPENAI_MODEL',
|
|
||||||
'AI_CONFIGURATION',
|
|
||||||
'gpt-4o',
|
|
||||||
'STRING',
|
|
||||||
'OpenAI Model',
|
|
||||||
'OpenAI model to use for AI generation',
|
|
||||||
'gpt-4o',
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
'{}'::jsonb,
|
|
||||||
'input',
|
|
||||||
NULL,
|
|
||||||
28,
|
|
||||||
false,
|
|
||||||
NULL,
|
|
||||||
NULL,
|
|
||||||
NOW(),
|
|
||||||
NOW()
|
|
||||||
),
|
|
||||||
(
|
|
||||||
gen_random_uuid(),
|
|
||||||
'GEMINI_MODEL',
|
|
||||||
'AI_CONFIGURATION',
|
|
||||||
'gemini-2.0-flash-lite',
|
|
||||||
'STRING',
|
|
||||||
'Gemini Model',
|
|
||||||
'Gemini (Google) model to use for AI generation',
|
|
||||||
'gemini-2.0-flash-lite',
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
'{}'::jsonb,
|
|
||||||
'input',
|
|
||||||
NULL,
|
|
||||||
29,
|
|
||||||
false,
|
|
||||||
NULL,
|
|
||||||
NULL,
|
|
||||||
NOW(),
|
|
||||||
NOW()
|
|
||||||
)
|
|
||||||
ON CONFLICT (config_key) DO NOTHING
|
|
||||||
`, { type: QueryTypes.INSERT });
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function down(queryInterface: QueryInterface): Promise<void> {
|
|
||||||
// Remove the AI model configurations
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
DELETE FROM admin_configurations
|
|
||||||
WHERE config_key IN ('CLAUDE_MODEL', 'OPENAI_MODEL', 'GEMINI_MODEL')
|
|
||||||
`, { type: QueryTypes.DELETE });
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -1,322 +0,0 @@
|
|||||||
import { QueryInterface, DataTypes } from 'sequelize';
|
|
||||||
import { Sequelize } from 'sequelize';
|
|
||||||
|
|
||||||
export async function up(queryInterface: QueryInterface): Promise<void> {
|
|
||||||
// Ensure uuid-ossp extension is enabled (required for uuid_generate_v4())
|
|
||||||
await queryInterface.sequelize.query('CREATE EXTENSION IF NOT EXISTS "uuid-ossp"');
|
|
||||||
|
|
||||||
// Create dealers table with all fields from sample data
|
|
||||||
await queryInterface.createTable('dealers', {
|
|
||||||
dealer_id: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
primaryKey: true,
|
|
||||||
defaultValue: Sequelize.literal('uuid_generate_v4()')
|
|
||||||
},
|
|
||||||
sales_code: {
|
|
||||||
type: DataTypes.STRING(50),
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'Sales Code'
|
|
||||||
},
|
|
||||||
service_code: {
|
|
||||||
type: DataTypes.STRING(50),
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'Service Code'
|
|
||||||
},
|
|
||||||
gear_code: {
|
|
||||||
type: DataTypes.STRING(50),
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'Gear Code'
|
|
||||||
},
|
|
||||||
gma_code: {
|
|
||||||
type: DataTypes.STRING(50),
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'GMA CODE'
|
|
||||||
},
|
|
||||||
region: {
|
|
||||||
type: DataTypes.STRING(50),
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'Region'
|
|
||||||
},
|
|
||||||
dealership: {
|
|
||||||
type: DataTypes.STRING(255),
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'Dealership name'
|
|
||||||
},
|
|
||||||
state: {
|
|
||||||
type: DataTypes.STRING(100),
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'State'
|
|
||||||
},
|
|
||||||
district: {
|
|
||||||
type: DataTypes.STRING(100),
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'District'
|
|
||||||
},
|
|
||||||
city: {
|
|
||||||
type: DataTypes.STRING(100),
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'City'
|
|
||||||
},
|
|
||||||
location: {
|
|
||||||
type: DataTypes.STRING(255),
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'Location'
|
|
||||||
},
|
|
||||||
city_category_pst: {
|
|
||||||
type: DataTypes.STRING(50),
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'City category (PST)'
|
|
||||||
},
|
|
||||||
layout_format: {
|
|
||||||
type: DataTypes.STRING(50),
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'Layout format'
|
|
||||||
},
|
|
||||||
tier_city_category: {
|
|
||||||
type: DataTypes.STRING(100),
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'TIER City Category'
|
|
||||||
},
|
|
||||||
on_boarding_charges: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'On Boarding Charges (stored as text to allow text values)'
|
|
||||||
},
|
|
||||||
date: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'DATE (stored as text to avoid format validation)'
|
|
||||||
},
|
|
||||||
single_format_month_year: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'Single Format of Month/Year (stored as text)'
|
|
||||||
},
|
|
||||||
domain_id: {
|
|
||||||
type: DataTypes.STRING(255),
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'Domain Id'
|
|
||||||
},
|
|
||||||
replacement: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'Replacement (stored as text to allow longer values)'
|
|
||||||
},
|
|
||||||
termination_resignation_status: {
|
|
||||||
type: DataTypes.STRING(255),
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'Termination / Resignation under Proposal or Evaluation'
|
|
||||||
},
|
|
||||||
date_of_termination_resignation: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'Date Of termination/ resignation (stored as text to avoid format validation)'
|
|
||||||
},
|
|
||||||
last_date_of_operations: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'Last date of operations (stored as text to avoid format validation)'
|
|
||||||
},
|
|
||||||
old_codes: {
|
|
||||||
type: DataTypes.STRING(255),
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'Old Codes'
|
|
||||||
},
|
|
||||||
branch_details: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'Branch Details'
|
|
||||||
},
|
|
||||||
dealer_principal_name: {
|
|
||||||
type: DataTypes.STRING(255),
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'Dealer Principal Name'
|
|
||||||
},
|
|
||||||
dealer_principal_email_id: {
|
|
||||||
type: DataTypes.STRING(255),
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'Dealer Principal Email Id'
|
|
||||||
},
|
|
||||||
dp_contact_number: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'DP CONTACT NUMBER (stored as text to allow multiple numbers)'
|
|
||||||
},
|
|
||||||
dp_contacts: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'DP CONTACTS (stored as text to allow multiple contacts)'
|
|
||||||
},
|
|
||||||
showroom_address: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'Showroom Address'
|
|
||||||
},
|
|
||||||
showroom_pincode: {
|
|
||||||
type: DataTypes.STRING(10),
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'Showroom Pincode'
|
|
||||||
},
|
|
||||||
workshop_address: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'Workshop Address'
|
|
||||||
},
|
|
||||||
workshop_pincode: {
|
|
||||||
type: DataTypes.STRING(10),
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'Workshop Pincode'
|
|
||||||
},
|
|
||||||
location_district: {
|
|
||||||
type: DataTypes.STRING(100),
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'Location / District'
|
|
||||||
},
|
|
||||||
state_workshop: {
|
|
||||||
type: DataTypes.STRING(100),
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'State (for workshop)'
|
|
||||||
},
|
|
||||||
no_of_studios: {
|
|
||||||
type: DataTypes.INTEGER,
|
|
||||||
allowNull: true,
|
|
||||||
defaultValue: 0,
|
|
||||||
comment: 'No Of Studios'
|
|
||||||
},
|
|
||||||
website_update: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'Website update (stored as text to allow longer values)'
|
|
||||||
},
|
|
||||||
gst: {
|
|
||||||
type: DataTypes.STRING(50),
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'GST'
|
|
||||||
},
|
|
||||||
pan: {
|
|
||||||
type: DataTypes.STRING(50),
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'PAN'
|
|
||||||
},
|
|
||||||
firm_type: {
|
|
||||||
type: DataTypes.STRING(100),
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'Firm Type'
|
|
||||||
},
|
|
||||||
prop_managing_partners_directors: {
|
|
||||||
type: DataTypes.STRING(255),
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'Prop. / Managing Partners / Managing Directors'
|
|
||||||
},
|
|
||||||
total_prop_partners_directors: {
|
|
||||||
type: DataTypes.STRING(255),
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'Total Prop. / Partners / Directors'
|
|
||||||
},
|
|
||||||
docs_folder_link: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'DOCS Folder Link'
|
|
||||||
},
|
|
||||||
workshop_gma_codes: {
|
|
||||||
type: DataTypes.STRING(255),
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'Workshop GMA Codes'
|
|
||||||
},
|
|
||||||
existing_new: {
|
|
||||||
type: DataTypes.STRING(50),
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'Existing / New'
|
|
||||||
},
|
|
||||||
dlrcode: {
|
|
||||||
type: DataTypes.STRING(50),
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'dlrcode'
|
|
||||||
},
|
|
||||||
is_active: {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: true,
|
|
||||||
comment: 'Whether the dealer is currently active'
|
|
||||||
},
|
|
||||||
created_at: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
|
|
||||||
},
|
|
||||||
updated_at: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create indexes
|
|
||||||
await queryInterface.addIndex('dealers', ['sales_code'], {
|
|
||||||
name: 'idx_dealers_sales_code',
|
|
||||||
unique: false
|
|
||||||
});
|
|
||||||
|
|
||||||
await queryInterface.addIndex('dealers', ['service_code'], {
|
|
||||||
name: 'idx_dealers_service_code',
|
|
||||||
unique: false
|
|
||||||
});
|
|
||||||
|
|
||||||
await queryInterface.addIndex('dealers', ['gma_code'], {
|
|
||||||
name: 'idx_dealers_gma_code',
|
|
||||||
unique: false
|
|
||||||
});
|
|
||||||
|
|
||||||
await queryInterface.addIndex('dealers', ['domain_id'], {
|
|
||||||
name: 'idx_dealers_domain_id',
|
|
||||||
unique: false
|
|
||||||
});
|
|
||||||
|
|
||||||
await queryInterface.addIndex('dealers', ['region'], {
|
|
||||||
name: 'idx_dealers_region',
|
|
||||||
unique: false
|
|
||||||
});
|
|
||||||
|
|
||||||
await queryInterface.addIndex('dealers', ['state'], {
|
|
||||||
name: 'idx_dealers_state',
|
|
||||||
unique: false
|
|
||||||
});
|
|
||||||
|
|
||||||
await queryInterface.addIndex('dealers', ['city'], {
|
|
||||||
name: 'idx_dealers_city',
|
|
||||||
unique: false
|
|
||||||
});
|
|
||||||
|
|
||||||
await queryInterface.addIndex('dealers', ['district'], {
|
|
||||||
name: 'idx_dealers_district',
|
|
||||||
unique: false
|
|
||||||
});
|
|
||||||
|
|
||||||
await queryInterface.addIndex('dealers', ['dlrcode'], {
|
|
||||||
name: 'idx_dealers_dlrcode',
|
|
||||||
unique: false
|
|
||||||
});
|
|
||||||
|
|
||||||
await queryInterface.addIndex('dealers', ['is_active'], {
|
|
||||||
name: 'idx_dealers_is_active',
|
|
||||||
unique: false
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function down(queryInterface: QueryInterface): Promise<void> {
|
|
||||||
// Drop indexes first
|
|
||||||
await queryInterface.removeIndex('dealers', 'idx_dealers_sales_code');
|
|
||||||
await queryInterface.removeIndex('dealers', 'idx_dealers_service_code');
|
|
||||||
await queryInterface.removeIndex('dealers', 'idx_dealers_gma_code');
|
|
||||||
await queryInterface.removeIndex('dealers', 'idx_dealers_domain_id');
|
|
||||||
await queryInterface.removeIndex('dealers', 'idx_dealers_region');
|
|
||||||
await queryInterface.removeIndex('dealers', 'idx_dealers_state');
|
|
||||||
await queryInterface.removeIndex('dealers', 'idx_dealers_city');
|
|
||||||
await queryInterface.removeIndex('dealers', 'idx_dealers_district');
|
|
||||||
await queryInterface.removeIndex('dealers', 'idx_dealers_dlrcode');
|
|
||||||
await queryInterface.removeIndex('dealers', 'idx_dealers_is_active');
|
|
||||||
|
|
||||||
// Drop table
|
|
||||||
await queryInterface.dropTable('dealers');
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -1,92 +0,0 @@
|
|||||||
import { QueryInterface, DataTypes } from 'sequelize';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Migration to create request_summaries table
|
|
||||||
* Stores comprehensive summaries of closed workflow requests
|
|
||||||
*/
|
|
||||||
export async function up(queryInterface: QueryInterface): Promise<void> {
|
|
||||||
await queryInterface.createTable('request_summaries', {
|
|
||||||
summary_id: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
defaultValue: DataTypes.UUIDV4,
|
|
||||||
primaryKey: true,
|
|
||||||
allowNull: false
|
|
||||||
},
|
|
||||||
request_id: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
references: {
|
|
||||||
model: 'workflow_requests',
|
|
||||||
key: 'request_id'
|
|
||||||
},
|
|
||||||
onUpdate: 'CASCADE',
|
|
||||||
onDelete: 'CASCADE',
|
|
||||||
unique: true // One summary per request
|
|
||||||
},
|
|
||||||
initiator_id: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
references: {
|
|
||||||
model: 'users',
|
|
||||||
key: 'user_id'
|
|
||||||
},
|
|
||||||
onUpdate: 'CASCADE',
|
|
||||||
onDelete: 'CASCADE'
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
type: DataTypes.STRING(500),
|
|
||||||
allowNull: false
|
|
||||||
},
|
|
||||||
description: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
allowNull: true
|
|
||||||
},
|
|
||||||
closing_remarks: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
allowNull: true
|
|
||||||
},
|
|
||||||
is_ai_generated: {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: false
|
|
||||||
},
|
|
||||||
conclusion_id: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: true,
|
|
||||||
references: {
|
|
||||||
model: 'conclusion_remarks',
|
|
||||||
key: 'conclusion_id'
|
|
||||||
},
|
|
||||||
onUpdate: 'CASCADE',
|
|
||||||
onDelete: 'SET NULL'
|
|
||||||
},
|
|
||||||
created_at: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW
|
|
||||||
},
|
|
||||||
updated_at: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create indexes
|
|
||||||
await queryInterface.addIndex('request_summaries', ['request_id'], {
|
|
||||||
name: 'idx_request_summaries_request_id'
|
|
||||||
});
|
|
||||||
|
|
||||||
await queryInterface.addIndex('request_summaries', ['initiator_id'], {
|
|
||||||
name: 'idx_request_summaries_initiator_id'
|
|
||||||
});
|
|
||||||
|
|
||||||
await queryInterface.addIndex('request_summaries', ['created_at'], {
|
|
||||||
name: 'idx_request_summaries_created_at'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function down(queryInterface: QueryInterface): Promise<void> {
|
|
||||||
await queryInterface.dropTable('request_summaries');
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -1,99 +0,0 @@
|
|||||||
import { QueryInterface, DataTypes } from 'sequelize';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Migration to create shared_summaries table
|
|
||||||
* Stores sharing relationships for request summaries
|
|
||||||
*/
|
|
||||||
export async function up(queryInterface: QueryInterface): Promise<void> {
|
|
||||||
await queryInterface.createTable('shared_summaries', {
|
|
||||||
shared_summary_id: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
defaultValue: DataTypes.UUIDV4,
|
|
||||||
primaryKey: true,
|
|
||||||
allowNull: false
|
|
||||||
},
|
|
||||||
summary_id: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
references: {
|
|
||||||
model: 'request_summaries',
|
|
||||||
key: 'summary_id'
|
|
||||||
},
|
|
||||||
onUpdate: 'CASCADE',
|
|
||||||
onDelete: 'CASCADE'
|
|
||||||
},
|
|
||||||
shared_by: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
references: {
|
|
||||||
model: 'users',
|
|
||||||
key: 'user_id'
|
|
||||||
},
|
|
||||||
onUpdate: 'CASCADE',
|
|
||||||
onDelete: 'CASCADE'
|
|
||||||
},
|
|
||||||
shared_with: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
references: {
|
|
||||||
model: 'users',
|
|
||||||
key: 'user_id'
|
|
||||||
},
|
|
||||||
onUpdate: 'CASCADE',
|
|
||||||
onDelete: 'CASCADE'
|
|
||||||
},
|
|
||||||
shared_at: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW
|
|
||||||
},
|
|
||||||
viewed_at: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: true
|
|
||||||
},
|
|
||||||
is_read: {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: false
|
|
||||||
},
|
|
||||||
created_at: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW
|
|
||||||
},
|
|
||||||
updated_at: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create unique constraint to prevent duplicate shares
|
|
||||||
await queryInterface.addConstraint('shared_summaries', {
|
|
||||||
fields: ['summary_id', 'shared_with'],
|
|
||||||
type: 'unique',
|
|
||||||
name: 'uk_shared_summary'
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create indexes
|
|
||||||
await queryInterface.addIndex('shared_summaries', ['summary_id'], {
|
|
||||||
name: 'idx_shared_summaries_summary_id'
|
|
||||||
});
|
|
||||||
|
|
||||||
await queryInterface.addIndex('shared_summaries', ['shared_by'], {
|
|
||||||
name: 'idx_shared_summaries_shared_by'
|
|
||||||
});
|
|
||||||
|
|
||||||
await queryInterface.addIndex('shared_summaries', ['shared_with'], {
|
|
||||||
name: 'idx_shared_summaries_shared_with'
|
|
||||||
});
|
|
||||||
|
|
||||||
await queryInterface.addIndex('shared_summaries', ['shared_at'], {
|
|
||||||
name: 'idx_shared_summaries_shared_at'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function down(queryInterface: QueryInterface): Promise<void> {
|
|
||||||
await queryInterface.dropTable('shared_summaries');
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -1,34 +0,0 @@
|
|||||||
import { QueryInterface } from 'sequelize';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Migration: Update Request Number Format
|
|
||||||
*
|
|
||||||
* This migration documents the change in request number format from:
|
|
||||||
* - Old: REQ-YYYY-NNNNN (e.g., REQ-2025-12345)
|
|
||||||
* - New: REQ-YYYY-MM-XXXX (e.g., REQ-2025-11-0001)
|
|
||||||
*
|
|
||||||
* The counter now resets every month automatically.
|
|
||||||
*
|
|
||||||
* No schema changes are required as the request_number column (VARCHAR(20))
|
|
||||||
* is already sufficient for the new format (16 characters).
|
|
||||||
*
|
|
||||||
* Existing request numbers will remain unchanged.
|
|
||||||
* New requests will use the new format starting from this migration.
|
|
||||||
*/
|
|
||||||
export async function up(queryInterface: QueryInterface): Promise<void> {
|
|
||||||
// No schema changes needed - this is a code-level change only
|
|
||||||
// The generateRequestNumber() function in helpers.ts has been updated
|
|
||||||
// to generate the new format: REQ-YYYY-MM-XXXX
|
|
||||||
|
|
||||||
// Log the change for reference
|
|
||||||
console.log('[Migration] Request number format updated to REQ-YYYY-MM-XXXX');
|
|
||||||
console.log('[Migration] Counter will reset automatically each month');
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function down(queryInterface: QueryInterface): Promise<void> {
|
|
||||||
// No rollback needed - this is a code-level change
|
|
||||||
// To revert, simply update the generateRequestNumber() function
|
|
||||||
// in helpers.ts back to the old format
|
|
||||||
console.log('[Migration] Request number format can be reverted by updating generateRequestNumber() function');
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -1,83 +0,0 @@
|
|||||||
import { QueryInterface, DataTypes } from 'sequelize';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Migration to create activity_types table for claim management activity types
|
|
||||||
* Admin can manage activity types similar to holiday management
|
|
||||||
*/
|
|
||||||
export async function up(queryInterface: QueryInterface): Promise<void> {
|
|
||||||
await queryInterface.createTable('activity_types', {
|
|
||||||
activity_type_id: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
defaultValue: DataTypes.UUIDV4,
|
|
||||||
primaryKey: true
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
type: DataTypes.STRING(200),
|
|
||||||
allowNull: false,
|
|
||||||
unique: true,
|
|
||||||
comment: 'Activity type title/name (e.g., "Riders Mania Claims", "Legal Claims Reimbursement")'
|
|
||||||
},
|
|
||||||
item_code: {
|
|
||||||
type: DataTypes.STRING(100),
|
|
||||||
allowNull: true,
|
|
||||||
defaultValue: null,
|
|
||||||
comment: 'Optional item code for the activity type'
|
|
||||||
},
|
|
||||||
taxation_type: {
|
|
||||||
type: DataTypes.STRING(100),
|
|
||||||
allowNull: true,
|
|
||||||
defaultValue: null,
|
|
||||||
comment: 'Optional taxation type for the activity'
|
|
||||||
},
|
|
||||||
sap_ref_no: {
|
|
||||||
type: DataTypes.STRING(100),
|
|
||||||
allowNull: true,
|
|
||||||
defaultValue: null,
|
|
||||||
comment: 'Optional SAP reference number'
|
|
||||||
},
|
|
||||||
is_active: {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
defaultValue: true,
|
|
||||||
comment: 'Whether this activity type is currently active/available for selection'
|
|
||||||
},
|
|
||||||
created_by: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
references: {
|
|
||||||
model: 'users',
|
|
||||||
key: 'user_id'
|
|
||||||
},
|
|
||||||
comment: 'Admin user who created this activity type'
|
|
||||||
},
|
|
||||||
updated_by: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: true,
|
|
||||||
references: {
|
|
||||||
model: 'users',
|
|
||||||
key: 'user_id'
|
|
||||||
},
|
|
||||||
comment: 'Admin user who last updated this activity type'
|
|
||||||
},
|
|
||||||
created_at: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW
|
|
||||||
},
|
|
||||||
updated_at: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Indexes for performance
|
|
||||||
await queryInterface.sequelize.query('CREATE UNIQUE INDEX IF NOT EXISTS "activity_types_title_unique" ON "activity_types" ("title");');
|
|
||||||
await queryInterface.sequelize.query('CREATE INDEX IF NOT EXISTS "activity_types_is_active" ON "activity_types" ("is_active");');
|
|
||||||
await queryInterface.sequelize.query('CREATE INDEX IF NOT EXISTS "activity_types_item_code" ON "activity_types" ("item_code");');
|
|
||||||
await queryInterface.sequelize.query('CREATE INDEX IF NOT EXISTS "activity_types_created_by" ON "activity_types" ("created_by");');
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function down(queryInterface: QueryInterface): Promise<void> {
|
|
||||||
await queryInterface.dropTable('activity_types');
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -1,73 +0,0 @@
|
|||||||
import { QueryInterface, DataTypes } from 'sequelize';
|
|
||||||
|
|
||||||
export async function up(queryInterface: QueryInterface): Promise<void> {
|
|
||||||
// Add pause fields to approval_levels table
|
|
||||||
// Note: The 'PAUSED' enum value is added in a separate migration (20250126-add-paused-to-enum.ts)
|
|
||||||
await queryInterface.addColumn('approval_levels', 'is_paused', {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: false
|
|
||||||
});
|
|
||||||
|
|
||||||
await queryInterface.addColumn('approval_levels', 'paused_at', {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: true
|
|
||||||
});
|
|
||||||
|
|
||||||
await queryInterface.addColumn('approval_levels', 'paused_by', {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: true,
|
|
||||||
references: {
|
|
||||||
model: 'users',
|
|
||||||
key: 'user_id'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await queryInterface.addColumn('approval_levels', 'pause_reason', {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
allowNull: true
|
|
||||||
});
|
|
||||||
|
|
||||||
await queryInterface.addColumn('approval_levels', 'pause_resume_date', {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: true
|
|
||||||
});
|
|
||||||
|
|
||||||
await queryInterface.addColumn('approval_levels', 'pause_tat_start_time', {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'Original TAT start time before pause'
|
|
||||||
});
|
|
||||||
|
|
||||||
await queryInterface.addColumn('approval_levels', 'pause_elapsed_hours', {
|
|
||||||
type: DataTypes.DECIMAL(10, 2),
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'Elapsed hours at pause time'
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create index on is_paused for faster queries
|
|
||||||
await queryInterface.sequelize.query(
|
|
||||||
'CREATE INDEX IF NOT EXISTS "approval_levels_is_paused" ON "approval_levels" ("is_paused");'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Create index on pause_resume_date for auto-resume job
|
|
||||||
await queryInterface.sequelize.query(
|
|
||||||
'CREATE INDEX IF NOT EXISTS "approval_levels_pause_resume_date" ON "approval_levels" ("pause_resume_date") WHERE "is_paused" = true;'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function down(queryInterface: QueryInterface): Promise<void> {
|
|
||||||
await queryInterface.removeColumn('approval_levels', 'pause_elapsed_hours');
|
|
||||||
await queryInterface.removeColumn('approval_levels', 'pause_tat_start_time');
|
|
||||||
await queryInterface.removeColumn('approval_levels', 'pause_resume_date');
|
|
||||||
await queryInterface.removeColumn('approval_levels', 'pause_reason');
|
|
||||||
await queryInterface.removeColumn('approval_levels', 'paused_by');
|
|
||||||
await queryInterface.removeColumn('approval_levels', 'paused_at');
|
|
||||||
await queryInterface.removeColumn('approval_levels', 'is_paused');
|
|
||||||
|
|
||||||
// Note: PostgreSQL doesn't support removing enum values directly
|
|
||||||
// To fully rollback, you would need to recreate the enum type
|
|
||||||
// This is a limitation of PostgreSQL enums
|
|
||||||
// For now, we'll leave 'PAUSED' in the enum even after rollback
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -1,59 +0,0 @@
|
|||||||
import { QueryInterface, DataTypes } from 'sequelize';
|
|
||||||
|
|
||||||
export async function up(queryInterface: QueryInterface): Promise<void> {
|
|
||||||
// Add pause fields to workflow_requests table
|
|
||||||
await queryInterface.addColumn('workflow_requests', 'is_paused', {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: false
|
|
||||||
});
|
|
||||||
|
|
||||||
await queryInterface.addColumn('workflow_requests', 'paused_at', {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: true
|
|
||||||
});
|
|
||||||
|
|
||||||
await queryInterface.addColumn('workflow_requests', 'paused_by', {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: true,
|
|
||||||
references: {
|
|
||||||
model: 'users',
|
|
||||||
key: 'user_id'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await queryInterface.addColumn('workflow_requests', 'pause_reason', {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
allowNull: true
|
|
||||||
});
|
|
||||||
|
|
||||||
await queryInterface.addColumn('workflow_requests', 'pause_resume_date', {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: true
|
|
||||||
});
|
|
||||||
|
|
||||||
await queryInterface.addColumn('workflow_requests', 'pause_tat_snapshot', {
|
|
||||||
type: DataTypes.JSONB,
|
|
||||||
allowNull: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create index on is_paused for faster queries
|
|
||||||
await queryInterface.sequelize.query(
|
|
||||||
'CREATE INDEX IF NOT EXISTS "workflow_requests_is_paused" ON "workflow_requests" ("is_paused");'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Create index on pause_resume_date for auto-resume job
|
|
||||||
await queryInterface.sequelize.query(
|
|
||||||
'CREATE INDEX IF NOT EXISTS "workflow_requests_pause_resume_date" ON "workflow_requests" ("pause_resume_date") WHERE "is_paused" = true;'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function down(queryInterface: QueryInterface): Promise<void> {
|
|
||||||
await queryInterface.removeColumn('workflow_requests', 'pause_tat_snapshot');
|
|
||||||
await queryInterface.removeColumn('workflow_requests', 'pause_resume_date');
|
|
||||||
await queryInterface.removeColumn('workflow_requests', 'pause_reason');
|
|
||||||
await queryInterface.removeColumn('workflow_requests', 'paused_by');
|
|
||||||
await queryInterface.removeColumn('workflow_requests', 'paused_at');
|
|
||||||
await queryInterface.removeColumn('workflow_requests', 'is_paused');
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -1,35 +0,0 @@
|
|||||||
import { QueryInterface } from 'sequelize';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Migration to add 'PAUSED' value to enum_approval_status enum type
|
|
||||||
* This is required for the pause workflow feature
|
|
||||||
*/
|
|
||||||
export async function up(queryInterface: QueryInterface): Promise<void> {
|
|
||||||
// Add 'PAUSED' to the enum_approval_status enum type
|
|
||||||
// PostgreSQL doesn't support IF NOT EXISTS for ALTER TYPE ADD VALUE,
|
|
||||||
// so we check if it exists first
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
DO $$
|
|
||||||
BEGIN
|
|
||||||
IF NOT EXISTS (
|
|
||||||
SELECT 1 FROM pg_enum
|
|
||||||
WHERE enumlabel = 'PAUSED'
|
|
||||||
AND enumtypid = (SELECT oid FROM pg_type WHERE typname = 'enum_approval_status')
|
|
||||||
) THEN
|
|
||||||
ALTER TYPE enum_approval_status ADD VALUE 'PAUSED';
|
|
||||||
END IF;
|
|
||||||
END$$;
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function down(queryInterface: QueryInterface): Promise<void> {
|
|
||||||
// Note: PostgreSQL doesn't support removing enum values directly
|
|
||||||
// To fully rollback, you would need to:
|
|
||||||
// 1. Create a new enum without 'PAUSED'
|
|
||||||
// 2. Update all columns to use the new enum
|
|
||||||
// 3. Drop the old enum
|
|
||||||
// This is complex and risky, so we'll leave 'PAUSED' in the enum
|
|
||||||
// even after rollback. This is a limitation of PostgreSQL enums.
|
|
||||||
console.log('[Migration] Note: Cannot remove enum values in PostgreSQL. PAUSED will remain in enum_approval_status.');
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -1,35 +0,0 @@
|
|||||||
import { QueryInterface } from 'sequelize';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Migration to add 'PAUSED' value to enum_workflow_status enum type
|
|
||||||
* This allows workflows to have a PAUSED status in addition to the isPaused boolean flag
|
|
||||||
*/
|
|
||||||
export async function up(queryInterface: QueryInterface): Promise<void> {
|
|
||||||
// Add 'PAUSED' to the enum_workflow_status enum type
|
|
||||||
// PostgreSQL doesn't support IF NOT EXISTS for ALTER TYPE ADD VALUE,
|
|
||||||
// so we check if it exists first
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
DO $$
|
|
||||||
BEGIN
|
|
||||||
IF NOT EXISTS (
|
|
||||||
SELECT 1 FROM pg_enum
|
|
||||||
WHERE enumlabel = 'PAUSED'
|
|
||||||
AND enumtypid = (SELECT oid FROM pg_type WHERE typname = 'enum_workflow_status')
|
|
||||||
) THEN
|
|
||||||
ALTER TYPE enum_workflow_status ADD VALUE 'PAUSED';
|
|
||||||
END IF;
|
|
||||||
END$$;
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function down(queryInterface: QueryInterface): Promise<void> {
|
|
||||||
// Note: PostgreSQL doesn't support removing enum values directly
|
|
||||||
// To fully rollback, you would need to:
|
|
||||||
// 1. Create a new enum without 'PAUSED'
|
|
||||||
// 2. Update all columns to use the new enum
|
|
||||||
// 3. Drop the old enum
|
|
||||||
// This is complex and risky, so we'll leave 'PAUSED' in the enum
|
|
||||||
// even after rollback. This is a limitation of PostgreSQL enums.
|
|
||||||
console.log('[Migration] Note: Cannot remove enum values in PostgreSQL. PAUSED will remain in enum_workflow_status.');
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
import { QueryInterface } from 'sequelize';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Migration to update any workflow requests with IN_PROGRESS status to PENDING
|
|
||||||
* Since IN_PROGRESS is essentially the same as PENDING for workflow requests
|
|
||||||
*/
|
|
||||||
export async function up(queryInterface: QueryInterface): Promise<void> {
|
|
||||||
// Update any workflow requests with IN_PROGRESS status to PENDING
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
UPDATE workflow_requests
|
|
||||||
SET status = 'PENDING'
|
|
||||||
WHERE status = 'IN_PROGRESS';
|
|
||||||
`);
|
|
||||||
|
|
||||||
console.log('[Migration] Updated IN_PROGRESS workflow requests to PENDING');
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function down(queryInterface: QueryInterface): Promise<void> {
|
|
||||||
// Note: We cannot reliably restore IN_PROGRESS status since we don't know
|
|
||||||
// which requests were originally IN_PROGRESS vs PENDING
|
|
||||||
// This migration is one-way
|
|
||||||
console.log('[Migration] Cannot rollback - IN_PROGRESS to PENDING migration is one-way');
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -1,199 +0,0 @@
|
|||||||
import { QueryInterface, QueryTypes } from 'sequelize';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Migration to migrate from multi-provider AI to Vertex AI Gemini
|
|
||||||
*
|
|
||||||
* Removes:
|
|
||||||
* - AI_PROVIDER
|
|
||||||
* - CLAUDE_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY
|
|
||||||
* - CLAUDE_MODEL, OPENAI_MODEL, GEMINI_MODEL
|
|
||||||
* - VERTEX_AI_MODEL (moved to environment variable only)
|
|
||||||
* - VERTEX_AI_LOCATION (moved to environment variable only)
|
|
||||||
*
|
|
||||||
* Note: Both VERTEX_AI_MODEL and VERTEX_AI_LOCATION are now configured via
|
|
||||||
* environment variables only (not in admin settings).
|
|
||||||
*
|
|
||||||
* This migration is idempotent - it will only delete configs that exist.
|
|
||||||
*/
|
|
||||||
export async function up(queryInterface: QueryInterface): Promise<void> {
|
|
||||||
// Remove old AI provider configurations
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
DELETE FROM admin_configurations
|
|
||||||
WHERE config_key IN (
|
|
||||||
'AI_PROVIDER',
|
|
||||||
'CLAUDE_API_KEY',
|
|
||||||
'OPENAI_API_KEY',
|
|
||||||
'GEMINI_API_KEY',
|
|
||||||
'CLAUDE_MODEL',
|
|
||||||
'OPENAI_MODEL',
|
|
||||||
'GEMINI_MODEL',
|
|
||||||
'VERTEX_AI_MODEL',
|
|
||||||
'VERTEX_AI_LOCATION'
|
|
||||||
)
|
|
||||||
`, { type: QueryTypes.DELETE });
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function down(queryInterface: QueryInterface): Promise<void> {
|
|
||||||
// This migration only removes configs, so down migration would restore them
|
|
||||||
// However, we don't restore them as they're now environment-only
|
|
||||||
console.log('[Migration] Down migration skipped - AI configs are now environment-only');
|
|
||||||
|
|
||||||
// Restore old configurations (for rollback)
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
INSERT INTO admin_configurations (
|
|
||||||
config_id, config_key, config_category, config_value, value_type,
|
|
||||||
display_name, description, default_value, is_editable, is_sensitive,
|
|
||||||
validation_rules, ui_component, options, sort_order, requires_restart,
|
|
||||||
last_modified_by, last_modified_at, created_at, updated_at
|
|
||||||
) VALUES
|
|
||||||
(
|
|
||||||
gen_random_uuid(),
|
|
||||||
'AI_PROVIDER',
|
|
||||||
'AI_CONFIGURATION',
|
|
||||||
'claude',
|
|
||||||
'STRING',
|
|
||||||
'AI Provider',
|
|
||||||
'Active AI provider for conclusion generation (claude, openai, or gemini)',
|
|
||||||
'claude',
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
'{"enum": ["claude", "openai", "gemini"], "required": true}'::jsonb,
|
|
||||||
'select',
|
|
||||||
'["claude", "openai", "gemini"]'::jsonb,
|
|
||||||
22,
|
|
||||||
false,
|
|
||||||
NULL,
|
|
||||||
NULL,
|
|
||||||
NOW(),
|
|
||||||
NOW()
|
|
||||||
),
|
|
||||||
(
|
|
||||||
gen_random_uuid(),
|
|
||||||
'CLAUDE_API_KEY',
|
|
||||||
'AI_CONFIGURATION',
|
|
||||||
'',
|
|
||||||
'STRING',
|
|
||||||
'Claude API Key',
|
|
||||||
'API key for Claude (Anthropic) - Get from console.anthropic.com',
|
|
||||||
'',
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
'{"pattern": "^sk-ant-", "minLength": 40}'::jsonb,
|
|
||||||
'input',
|
|
||||||
NULL,
|
|
||||||
23,
|
|
||||||
false,
|
|
||||||
NULL,
|
|
||||||
NULL,
|
|
||||||
NOW(),
|
|
||||||
NOW()
|
|
||||||
),
|
|
||||||
(
|
|
||||||
gen_random_uuid(),
|
|
||||||
'OPENAI_API_KEY',
|
|
||||||
'AI_CONFIGURATION',
|
|
||||||
'',
|
|
||||||
'STRING',
|
|
||||||
'OpenAI API Key',
|
|
||||||
'API key for OpenAI (GPT-4) - Get from platform.openai.com',
|
|
||||||
'',
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
'{"pattern": "^sk-", "minLength": 40}'::jsonb,
|
|
||||||
'input',
|
|
||||||
NULL,
|
|
||||||
24,
|
|
||||||
false,
|
|
||||||
NULL,
|
|
||||||
NULL,
|
|
||||||
NOW(),
|
|
||||||
NOW()
|
|
||||||
),
|
|
||||||
(
|
|
||||||
gen_random_uuid(),
|
|
||||||
'GEMINI_API_KEY',
|
|
||||||
'AI_CONFIGURATION',
|
|
||||||
'',
|
|
||||||
'STRING',
|
|
||||||
'Gemini API Key',
|
|
||||||
'API key for Gemini (Google) - Get from ai.google.dev',
|
|
||||||
'',
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
'{"minLength": 20}'::jsonb,
|
|
||||||
'input',
|
|
||||||
NULL,
|
|
||||||
25,
|
|
||||||
false,
|
|
||||||
NULL,
|
|
||||||
NULL,
|
|
||||||
NOW(),
|
|
||||||
NOW()
|
|
||||||
),
|
|
||||||
(
|
|
||||||
gen_random_uuid(),
|
|
||||||
'CLAUDE_MODEL',
|
|
||||||
'AI_CONFIGURATION',
|
|
||||||
'claude-sonnet-4-20250514',
|
|
||||||
'STRING',
|
|
||||||
'Claude Model',
|
|
||||||
'Claude (Anthropic) model to use for AI generation',
|
|
||||||
'claude-sonnet-4-20250514',
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
'{}'::jsonb,
|
|
||||||
'input',
|
|
||||||
NULL,
|
|
||||||
27,
|
|
||||||
false,
|
|
||||||
NULL,
|
|
||||||
NULL,
|
|
||||||
NOW(),
|
|
||||||
NOW()
|
|
||||||
),
|
|
||||||
(
|
|
||||||
gen_random_uuid(),
|
|
||||||
'OPENAI_MODEL',
|
|
||||||
'AI_CONFIGURATION',
|
|
||||||
'gpt-4o',
|
|
||||||
'STRING',
|
|
||||||
'OpenAI Model',
|
|
||||||
'OpenAI model to use for AI generation',
|
|
||||||
'gpt-4o',
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
'{}'::jsonb,
|
|
||||||
'input',
|
|
||||||
NULL,
|
|
||||||
28,
|
|
||||||
false,
|
|
||||||
NULL,
|
|
||||||
NULL,
|
|
||||||
NOW(),
|
|
||||||
NOW()
|
|
||||||
),
|
|
||||||
(
|
|
||||||
gen_random_uuid(),
|
|
||||||
'GEMINI_MODEL',
|
|
||||||
'AI_CONFIGURATION',
|
|
||||||
'gemini-2.0-flash-lite',
|
|
||||||
'STRING',
|
|
||||||
'Gemini Model',
|
|
||||||
'Gemini (Google) model to use for AI generation',
|
|
||||||
'gemini-2.0-flash-lite',
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
'{}'::jsonb,
|
|
||||||
'input',
|
|
||||||
NULL,
|
|
||||||
29,
|
|
||||||
false,
|
|
||||||
NULL,
|
|
||||||
NULL,
|
|
||||||
NOW(),
|
|
||||||
NOW()
|
|
||||||
)
|
|
||||||
ON CONFLICT (config_key) DO NOTHING
|
|
||||||
`, { type: QueryTypes.INSERT });
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -1,237 +0,0 @@
|
|||||||
import { QueryInterface, DataTypes } from 'sequelize';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Migration: Create users table
|
|
||||||
*
|
|
||||||
* Purpose: Create the main users table with all fields including RBAC and SSO fields
|
|
||||||
*
|
|
||||||
* This must run FIRST before other tables that reference users
|
|
||||||
*
|
|
||||||
* Includes:
|
|
||||||
* - Basic user information (email, name, etc.)
|
|
||||||
* - SSO/Okta fields (manager, job_title, etc.)
|
|
||||||
* - RBAC role system (USER, MANAGEMENT, ADMIN)
|
|
||||||
* - Location and AD group information
|
|
||||||
*
|
|
||||||
* Created: 2025-11-12 (Updated for fresh setup)
|
|
||||||
*/
|
|
||||||
export async function up(queryInterface: QueryInterface): Promise<void> {
|
|
||||||
console.log('📋 Creating users table with RBAC and extended SSO fields...');
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Step 1: Create ENUM type for roles
|
|
||||||
console.log(' ✓ Creating user_role_enum...');
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
CREATE TYPE user_role_enum AS ENUM ('USER', 'MANAGEMENT', 'ADMIN');
|
|
||||||
`);
|
|
||||||
|
|
||||||
// Step 2: Create users table
|
|
||||||
console.log(' ✓ Creating users table...');
|
|
||||||
await queryInterface.createTable('users', {
|
|
||||||
user_id: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
primaryKey: true,
|
|
||||||
defaultValue: DataTypes.UUIDV4,
|
|
||||||
field: 'user_id',
|
|
||||||
comment: 'Primary key - UUID'
|
|
||||||
},
|
|
||||||
employee_id: {
|
|
||||||
type: DataTypes.STRING(50),
|
|
||||||
allowNull: true,
|
|
||||||
field: 'employee_id',
|
|
||||||
comment: 'HR System Employee ID (optional) - some users may not have'
|
|
||||||
},
|
|
||||||
okta_sub: {
|
|
||||||
type: DataTypes.STRING(100),
|
|
||||||
allowNull: false,
|
|
||||||
unique: true,
|
|
||||||
field: 'okta_sub',
|
|
||||||
comment: 'Okta user subject identifier - unique identifier from SSO'
|
|
||||||
},
|
|
||||||
email: {
|
|
||||||
type: DataTypes.STRING(255),
|
|
||||||
allowNull: false,
|
|
||||||
unique: true,
|
|
||||||
field: 'email',
|
|
||||||
comment: 'Primary email address - unique and required'
|
|
||||||
},
|
|
||||||
first_name: {
|
|
||||||
type: DataTypes.STRING(100),
|
|
||||||
allowNull: true,
|
|
||||||
defaultValue: '',
|
|
||||||
field: 'first_name',
|
|
||||||
comment: 'First name from SSO (optional)'
|
|
||||||
},
|
|
||||||
last_name: {
|
|
||||||
type: DataTypes.STRING(100),
|
|
||||||
allowNull: true,
|
|
||||||
defaultValue: '',
|
|
||||||
field: 'last_name',
|
|
||||||
comment: 'Last name from SSO (optional)'
|
|
||||||
},
|
|
||||||
display_name: {
|
|
||||||
type: DataTypes.STRING(200),
|
|
||||||
allowNull: true,
|
|
||||||
defaultValue: '',
|
|
||||||
field: 'display_name',
|
|
||||||
comment: 'Full display name for UI'
|
|
||||||
},
|
|
||||||
department: {
|
|
||||||
type: DataTypes.STRING(100),
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'Department/Division from SSO'
|
|
||||||
},
|
|
||||||
designation: {
|
|
||||||
type: DataTypes.STRING(100),
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'Job designation/position'
|
|
||||||
},
|
|
||||||
phone: {
|
|
||||||
type: DataTypes.STRING(20),
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'Office phone number'
|
|
||||||
},
|
|
||||||
|
|
||||||
// ============ Extended SSO/Okta Fields ============
|
|
||||||
manager: {
|
|
||||||
type: DataTypes.STRING(200),
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'Reporting manager name from SSO/AD'
|
|
||||||
},
|
|
||||||
second_email: {
|
|
||||||
type: DataTypes.STRING(255),
|
|
||||||
allowNull: true,
|
|
||||||
field: 'second_email',
|
|
||||||
comment: 'Alternate email address from SSO'
|
|
||||||
},
|
|
||||||
job_title: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'job_title',
|
|
||||||
comment: 'Detailed job title/description from SSO'
|
|
||||||
},
|
|
||||||
employee_number: {
|
|
||||||
type: DataTypes.STRING(50),
|
|
||||||
allowNull: true,
|
|
||||||
field: 'employee_number',
|
|
||||||
comment: 'HR system employee number from SSO (e.g., "00020330")'
|
|
||||||
},
|
|
||||||
postal_address: {
|
|
||||||
type: DataTypes.STRING(500),
|
|
||||||
allowNull: true,
|
|
||||||
field: 'postal_address',
|
|
||||||
comment: 'Work location/office address from SSO'
|
|
||||||
},
|
|
||||||
mobile_phone: {
|
|
||||||
type: DataTypes.STRING(20),
|
|
||||||
allowNull: true,
|
|
||||||
field: 'mobile_phone',
|
|
||||||
comment: 'Mobile contact number from SSO'
|
|
||||||
},
|
|
||||||
ad_groups: {
|
|
||||||
type: DataTypes.JSONB,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'ad_groups',
|
|
||||||
comment: 'Active Directory group memberships from SSO (memberOf array)'
|
|
||||||
},
|
|
||||||
|
|
||||||
// ============ System Fields ============
|
|
||||||
location: {
|
|
||||||
type: DataTypes.JSONB,
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'JSON object: {city, state, country, office, timezone}'
|
|
||||||
},
|
|
||||||
is_active: {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
defaultValue: true,
|
|
||||||
field: 'is_active',
|
|
||||||
comment: 'Account status - true=active, false=disabled'
|
|
||||||
},
|
|
||||||
role: {
|
|
||||||
type: DataTypes.ENUM('USER', 'MANAGEMENT', 'ADMIN'),
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: 'USER',
|
|
||||||
comment: 'RBAC role: USER (default), MANAGEMENT (read all), ADMIN (full access)'
|
|
||||||
},
|
|
||||||
last_login: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'last_login',
|
|
||||||
comment: 'Last successful login timestamp'
|
|
||||||
},
|
|
||||||
created_at: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW,
|
|
||||||
field: 'created_at'
|
|
||||||
},
|
|
||||||
updated_at: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW,
|
|
||||||
field: 'updated_at'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Step 3: Create indexes
|
|
||||||
console.log(' ✓ Creating indexes...');
|
|
||||||
|
|
||||||
await queryInterface.addIndex('users', ['email'], {
|
|
||||||
name: 'users_email_idx',
|
|
||||||
unique: true
|
|
||||||
});
|
|
||||||
|
|
||||||
await queryInterface.addIndex('users', ['okta_sub'], {
|
|
||||||
name: 'users_okta_sub_idx',
|
|
||||||
unique: true
|
|
||||||
});
|
|
||||||
|
|
||||||
await queryInterface.addIndex('users', ['employee_id'], {
|
|
||||||
name: 'users_employee_id_idx'
|
|
||||||
});
|
|
||||||
|
|
||||||
await queryInterface.addIndex('users', ['department'], {
|
|
||||||
name: 'idx_users_department'
|
|
||||||
});
|
|
||||||
|
|
||||||
await queryInterface.addIndex('users', ['is_active'], {
|
|
||||||
name: 'idx_users_is_active'
|
|
||||||
});
|
|
||||||
|
|
||||||
await queryInterface.addIndex('users', ['role'], {
|
|
||||||
name: 'idx_users_role'
|
|
||||||
});
|
|
||||||
|
|
||||||
await queryInterface.addIndex('users', ['manager'], {
|
|
||||||
name: 'idx_users_manager'
|
|
||||||
});
|
|
||||||
|
|
||||||
await queryInterface.addIndex('users', ['postal_address'], {
|
|
||||||
name: 'idx_users_postal_address'
|
|
||||||
});
|
|
||||||
|
|
||||||
// GIN indexes for JSONB fields
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
CREATE INDEX idx_users_location ON users USING gin(location jsonb_path_ops);
|
|
||||||
CREATE INDEX idx_users_ad_groups ON users USING gin(ad_groups);
|
|
||||||
`);
|
|
||||||
|
|
||||||
console.log('✅ Users table created successfully with all indexes!');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ Failed to create users table:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function down(queryInterface: QueryInterface): Promise<void> {
|
|
||||||
console.log('📋 Dropping users table...');
|
|
||||||
|
|
||||||
await queryInterface.dropTable('users');
|
|
||||||
|
|
||||||
// Drop ENUM type
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
DROP TYPE IF EXISTS user_role_enum;
|
|
||||||
`);
|
|
||||||
|
|
||||||
console.log('✅ Users table dropped!');
|
|
||||||
}
|
|
||||||
@ -1,51 +0,0 @@
|
|||||||
import { QueryInterface, DataTypes } from 'sequelize';
|
|
||||||
|
|
||||||
export async function up(queryInterface: QueryInterface): Promise<void> {
|
|
||||||
// Enums
|
|
||||||
await queryInterface.sequelize.query(`DO $$
|
|
||||||
BEGIN
|
|
||||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'enum_priority') THEN
|
|
||||||
CREATE TYPE enum_priority AS ENUM ('STANDARD','EXPRESS');
|
|
||||||
END IF;
|
|
||||||
END$$;`);
|
|
||||||
await queryInterface.sequelize.query(`DO $$
|
|
||||||
BEGIN
|
|
||||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'enum_workflow_status') THEN
|
|
||||||
CREATE TYPE enum_workflow_status AS ENUM ('DRAFT','PENDING','IN_PROGRESS','APPROVED','REJECTED','CLOSED');
|
|
||||||
END IF;
|
|
||||||
END$$;`);
|
|
||||||
|
|
||||||
await queryInterface.createTable('workflow_requests', {
|
|
||||||
request_id: { type: DataTypes.UUID, primaryKey: true, defaultValue: DataTypes.UUIDV4 },
|
|
||||||
request_number: { type: DataTypes.STRING(20), allowNull: false, unique: true },
|
|
||||||
initiator_id: { type: DataTypes.UUID, allowNull: false, references: { model: 'users', key: 'user_id' } },
|
|
||||||
template_type: { type: DataTypes.STRING(20), allowNull: false, defaultValue: 'CUSTOM' },
|
|
||||||
title: { type: DataTypes.STRING(500), allowNull: false },
|
|
||||||
description: { type: DataTypes.TEXT, allowNull: false },
|
|
||||||
priority: { type: 'enum_priority' as any, allowNull: false, defaultValue: 'STANDARD' },
|
|
||||||
status: { type: 'enum_workflow_status' as any, allowNull: false, defaultValue: 'DRAFT' },
|
|
||||||
current_level: { type: DataTypes.INTEGER, allowNull: false, defaultValue: 1 },
|
|
||||||
total_levels: { type: DataTypes.INTEGER, allowNull: false, defaultValue: 1 },
|
|
||||||
total_tat_hours: { type: DataTypes.DECIMAL(10,2), allowNull: false, defaultValue: 0 },
|
|
||||||
submission_date: { type: DataTypes.DATE, allowNull: true },
|
|
||||||
closure_date: { type: DataTypes.DATE, allowNull: true },
|
|
||||||
conclusion_remark: { type: DataTypes.TEXT, allowNull: true },
|
|
||||||
ai_generated_conclusion: { type: DataTypes.TEXT, allowNull: true },
|
|
||||||
is_draft: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: true },
|
|
||||||
is_deleted: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false },
|
|
||||||
created_at: { type: DataTypes.DATE, allowNull: false, defaultValue: DataTypes.NOW },
|
|
||||||
updated_at: { type: DataTypes.DATE, allowNull: false, defaultValue: DataTypes.NOW },
|
|
||||||
});
|
|
||||||
|
|
||||||
await queryInterface.sequelize.query('CREATE INDEX IF NOT EXISTS "workflow_requests_initiator_id" ON "workflow_requests" ("initiator_id");');
|
|
||||||
await queryInterface.sequelize.query('CREATE INDEX IF NOT EXISTS "workflow_requests_status" ON "workflow_requests" ("status");');
|
|
||||||
await queryInterface.sequelize.query('CREATE INDEX IF NOT EXISTS "workflow_requests_created_at" ON "workflow_requests" ("created_at");');
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function down(queryInterface: QueryInterface): Promise<void> {
|
|
||||||
await queryInterface.dropTable('workflow_requests');
|
|
||||||
await queryInterface.sequelize.query('DROP TYPE IF EXISTS enum_workflow_status;');
|
|
||||||
await queryInterface.sequelize.query('DROP TYPE IF EXISTS enum_priority;');
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@ -1,53 +0,0 @@
|
|||||||
import { QueryInterface, DataTypes } from 'sequelize';
|
|
||||||
|
|
||||||
export async function up(queryInterface: QueryInterface): Promise<void> {
|
|
||||||
await queryInterface.sequelize.query(`DO $$
|
|
||||||
BEGIN
|
|
||||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'enum_approval_status') THEN
|
|
||||||
CREATE TYPE enum_approval_status AS ENUM ('PENDING','IN_PROGRESS','APPROVED','REJECTED','SKIPPED');
|
|
||||||
END IF;
|
|
||||||
END$$;`);
|
|
||||||
|
|
||||||
await queryInterface.createTable('approval_levels', {
|
|
||||||
level_id: { type: DataTypes.UUID, primaryKey: true, defaultValue: DataTypes.UUIDV4 },
|
|
||||||
request_id: { type: DataTypes.UUID, allowNull: false, references: { model: 'workflow_requests', key: 'request_id' } },
|
|
||||||
level_number: { type: DataTypes.INTEGER, allowNull: false },
|
|
||||||
level_name: { type: DataTypes.STRING(100), allowNull: true },
|
|
||||||
approver_id: { type: DataTypes.UUID, allowNull: false, references: { model: 'users', key: 'user_id' } },
|
|
||||||
approver_email: { type: DataTypes.STRING(255), allowNull: false },
|
|
||||||
approver_name: { type: DataTypes.STRING(200), allowNull: false },
|
|
||||||
tat_hours: { type: DataTypes.DECIMAL(10,2), allowNull: false },
|
|
||||||
tat_days: { type: DataTypes.INTEGER, allowNull: false },
|
|
||||||
status: { type: 'enum_approval_status' as any, allowNull: false, defaultValue: 'PENDING' },
|
|
||||||
level_start_time: { type: DataTypes.DATE, allowNull: true },
|
|
||||||
level_end_time: { type: DataTypes.DATE, allowNull: true },
|
|
||||||
action_date: { type: DataTypes.DATE, allowNull: true },
|
|
||||||
comments: { type: DataTypes.TEXT, allowNull: true },
|
|
||||||
rejection_reason: { type: DataTypes.TEXT, allowNull: true },
|
|
||||||
is_final_approver: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false },
|
|
||||||
elapsed_hours: { type: DataTypes.DECIMAL(10,2), allowNull: false, defaultValue: 0 },
|
|
||||||
remaining_hours: { type: DataTypes.DECIMAL(10,2), allowNull: false, defaultValue: 0 },
|
|
||||||
tat_percentage_used: { type: DataTypes.DECIMAL(5,2), allowNull: false, defaultValue: 0 },
|
|
||||||
created_at: { type: DataTypes.DATE, allowNull: false, defaultValue: DataTypes.NOW },
|
|
||||||
updated_at: { type: DataTypes.DATE, allowNull: false, defaultValue: DataTypes.NOW },
|
|
||||||
});
|
|
||||||
|
|
||||||
await queryInterface.sequelize.query('CREATE INDEX IF NOT EXISTS "approval_levels_request_id" ON "approval_levels" ("request_id");');
|
|
||||||
await queryInterface.sequelize.query('CREATE INDEX IF NOT EXISTS "approval_levels_approver_id" ON "approval_levels" ("approver_id");');
|
|
||||||
await queryInterface.sequelize.query('CREATE INDEX IF NOT EXISTS "approval_levels_status" ON "approval_levels" ("status");');
|
|
||||||
await queryInterface.sequelize.query(`DO $$
|
|
||||||
BEGIN
|
|
||||||
IF NOT EXISTS (
|
|
||||||
SELECT 1 FROM pg_constraint WHERE conname = 'uq_approval_levels_request_level'
|
|
||||||
) THEN
|
|
||||||
ALTER TABLE "approval_levels" ADD CONSTRAINT "uq_approval_levels_request_level" UNIQUE ("request_id", "level_number");
|
|
||||||
END IF;
|
|
||||||
END$$;`);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function down(queryInterface: QueryInterface): Promise<void> {
|
|
||||||
await queryInterface.dropTable('approval_levels');
|
|
||||||
await queryInterface.sequelize.query('DROP TYPE IF EXISTS enum_approval_status;');
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@ -1,44 +0,0 @@
|
|||||||
import { QueryInterface, DataTypes } from 'sequelize';
|
|
||||||
|
|
||||||
export async function up(queryInterface: QueryInterface): Promise<void> {
|
|
||||||
await queryInterface.sequelize.query(`DO $$
|
|
||||||
BEGIN
|
|
||||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'enum_participant_type') THEN
|
|
||||||
CREATE TYPE enum_participant_type AS ENUM ('SPECTATOR','INITIATOR','APPROVER','CONSULTATION');
|
|
||||||
END IF;
|
|
||||||
END$$;`);
|
|
||||||
|
|
||||||
await queryInterface.createTable('participants', {
|
|
||||||
participant_id: { type: DataTypes.UUID, primaryKey: true, defaultValue: DataTypes.UUIDV4 },
|
|
||||||
request_id: { type: DataTypes.UUID, allowNull: false, references: { model: 'workflow_requests', key: 'request_id' } },
|
|
||||||
user_id: { type: DataTypes.UUID, allowNull: false, references: { model: 'users', key: 'user_id' } },
|
|
||||||
user_email: { type: DataTypes.STRING(255), allowNull: false },
|
|
||||||
user_name: { type: DataTypes.STRING(200), allowNull: false },
|
|
||||||
participant_type: { type: 'enum_participant_type' as any, allowNull: false },
|
|
||||||
can_comment: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: true },
|
|
||||||
can_view_documents: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: true },
|
|
||||||
can_download_documents: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false },
|
|
||||||
notification_enabled: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: true },
|
|
||||||
added_by: { type: DataTypes.UUID, allowNull: false, references: { model: 'users', key: 'user_id' } },
|
|
||||||
added_at: { type: DataTypes.DATE, allowNull: false, defaultValue: DataTypes.NOW },
|
|
||||||
is_active: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
await queryInterface.sequelize.query('CREATE INDEX IF NOT EXISTS "participants_request_id" ON "participants" ("request_id");');
|
|
||||||
await queryInterface.sequelize.query('CREATE INDEX IF NOT EXISTS "participants_user_id" ON "participants" ("user_id");');
|
|
||||||
await queryInterface.sequelize.query(`DO $$
|
|
||||||
BEGIN
|
|
||||||
IF NOT EXISTS (
|
|
||||||
SELECT 1 FROM pg_constraint WHERE conname = 'uq_participants_request_user'
|
|
||||||
) THEN
|
|
||||||
ALTER TABLE "participants" ADD CONSTRAINT "uq_participants_request_user" UNIQUE ("request_id", "user_id");
|
|
||||||
END IF;
|
|
||||||
END$$;`);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function down(queryInterface: QueryInterface): Promise<void> {
|
|
||||||
await queryInterface.dropTable('participants');
|
|
||||||
await queryInterface.sequelize.query('DROP TYPE IF EXISTS enum_participant_type;');
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@ -1,44 +0,0 @@
|
|||||||
import { QueryInterface, DataTypes } from 'sequelize';
|
|
||||||
|
|
||||||
export async function up(queryInterface: QueryInterface): Promise<void> {
|
|
||||||
await queryInterface.sequelize.query(`DO $$
|
|
||||||
BEGIN
|
|
||||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'enum_document_category') THEN
|
|
||||||
CREATE TYPE enum_document_category AS ENUM ('SUPPORTING','APPROVAL','REFERENCE','FINAL','OTHER','COMPLETION_DOC','ACTIVITY_PHOTO');
|
|
||||||
END IF;
|
|
||||||
END$$;`);
|
|
||||||
|
|
||||||
await queryInterface.createTable('documents', {
|
|
||||||
document_id: { type: DataTypes.UUID, primaryKey: true, defaultValue: DataTypes.UUIDV4 },
|
|
||||||
request_id: { type: DataTypes.UUID, allowNull: false, references: { model: 'workflow_requests', key: 'request_id' } },
|
|
||||||
uploaded_by: { type: DataTypes.UUID, allowNull: false, references: { model: 'users', key: 'user_id' } },
|
|
||||||
file_name: { type: DataTypes.STRING(255), allowNull: false },
|
|
||||||
original_file_name: { type: DataTypes.STRING(255), allowNull: false },
|
|
||||||
file_type: { type: DataTypes.STRING(100), allowNull: false },
|
|
||||||
file_extension: { type: DataTypes.STRING(10), allowNull: false },
|
|
||||||
file_size: { type: DataTypes.BIGINT, allowNull: false },
|
|
||||||
file_path: { type: DataTypes.STRING(500), allowNull: false },
|
|
||||||
storage_url: { type: DataTypes.STRING(500), allowNull: true },
|
|
||||||
mime_type: { type: DataTypes.STRING(100), allowNull: false },
|
|
||||||
checksum: { type: DataTypes.STRING(64), allowNull: false },
|
|
||||||
is_google_doc: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false },
|
|
||||||
google_doc_url: { type: DataTypes.STRING(500), allowNull: true },
|
|
||||||
category: { type: 'enum_document_category' as any, allowNull: false, defaultValue: 'OTHER' },
|
|
||||||
version: { type: DataTypes.INTEGER, allowNull: false, defaultValue: 1 },
|
|
||||||
parent_document_id: { type: DataTypes.UUID, allowNull: true, references: { model: 'documents', key: 'document_id' } },
|
|
||||||
is_deleted: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false },
|
|
||||||
download_count: { type: DataTypes.INTEGER, allowNull: false, defaultValue: 0 },
|
|
||||||
uploaded_at: { type: DataTypes.DATE, allowNull: false, defaultValue: DataTypes.NOW },
|
|
||||||
});
|
|
||||||
|
|
||||||
await queryInterface.sequelize.query('CREATE INDEX IF NOT EXISTS "documents_request_id" ON "documents" ("request_id");');
|
|
||||||
await queryInterface.sequelize.query('CREATE INDEX IF NOT EXISTS "documents_uploaded_by" ON "documents" ("uploaded_by");');
|
|
||||||
await queryInterface.sequelize.query('CREATE INDEX IF NOT EXISTS "documents_category" ON "documents" ("category");');
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function down(queryInterface: QueryInterface): Promise<void> {
|
|
||||||
await queryInterface.dropTable('documents');
|
|
||||||
await queryInterface.sequelize.query('DROP TYPE IF EXISTS enum_document_category;');
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
import { QueryInterface, DataTypes } from 'sequelize';
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
up: async (queryInterface: QueryInterface) => {
|
|
||||||
await queryInterface.createTable('subscriptions', {
|
|
||||||
subscription_id: { type: DataTypes.UUID, primaryKey: true, defaultValue: DataTypes.UUIDV4, allowNull: false },
|
|
||||||
user_id: { type: DataTypes.UUID, allowNull: false },
|
|
||||||
endpoint: { type: DataTypes.STRING(1000), allowNull: false, unique: true },
|
|
||||||
p256dh: { type: DataTypes.STRING(255), allowNull: false },
|
|
||||||
auth: { type: DataTypes.STRING(255), allowNull: false },
|
|
||||||
user_agent: { type: DataTypes.STRING(500), allowNull: true },
|
|
||||||
created_at: { type: DataTypes.DATE, allowNull: false, defaultValue: DataTypes.NOW }
|
|
||||||
});
|
|
||||||
await queryInterface.sequelize.query('CREATE INDEX IF NOT EXISTS "subscriptions_user_id" ON "subscriptions" ("user_id");');
|
|
||||||
},
|
|
||||||
down: async (queryInterface: QueryInterface) => {
|
|
||||||
await queryInterface.dropTable('subscriptions');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
@ -1,29 +0,0 @@
|
|||||||
import { QueryInterface, DataTypes } from 'sequelize';
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
up: async (queryInterface: QueryInterface) => {
|
|
||||||
await queryInterface.createTable('activities', {
|
|
||||||
activity_id: { type: DataTypes.UUID, primaryKey: true, defaultValue: DataTypes.UUIDV4, allowNull: false },
|
|
||||||
request_id: { type: DataTypes.UUID, allowNull: false },
|
|
||||||
user_id: { type: DataTypes.UUID, allowNull: true },
|
|
||||||
user_name: { type: DataTypes.STRING(255), allowNull: true },
|
|
||||||
activity_type: { type: DataTypes.STRING(100), allowNull: false },
|
|
||||||
activity_description: { type: DataTypes.TEXT, allowNull: false },
|
|
||||||
activity_category: { type: DataTypes.STRING(100), allowNull: true },
|
|
||||||
severity: { type: DataTypes.STRING(50), allowNull: true },
|
|
||||||
metadata: { type: DataTypes.JSONB, allowNull: true },
|
|
||||||
is_system_event: { type: DataTypes.BOOLEAN, allowNull: true },
|
|
||||||
ip_address: { type: DataTypes.STRING(100), allowNull: true },
|
|
||||||
user_agent: { type: DataTypes.TEXT, allowNull: true },
|
|
||||||
created_at: { type: DataTypes.DATE, allowNull: false, defaultValue: DataTypes.NOW }
|
|
||||||
});
|
|
||||||
await queryInterface.sequelize.query('CREATE INDEX IF NOT EXISTS "activities_request_id" ON "activities" ("request_id");');
|
|
||||||
await queryInterface.sequelize.query('CREATE INDEX IF NOT EXISTS "activities_created_at" ON "activities" ("created_at");');
|
|
||||||
await queryInterface.sequelize.query('CREATE INDEX IF NOT EXISTS "activities_activity_type" ON "activities" ("activity_type");');
|
|
||||||
},
|
|
||||||
down: async (queryInterface: QueryInterface) => {
|
|
||||||
await queryInterface.dropTable('activities');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
@ -1,32 +0,0 @@
|
|||||||
import { QueryInterface, DataTypes } from 'sequelize';
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
up: async (queryInterface: QueryInterface) => {
|
|
||||||
await queryInterface.createTable('work_notes', {
|
|
||||||
note_id: { type: DataTypes.UUID, primaryKey: true, defaultValue: DataTypes.UUIDV4, allowNull: false },
|
|
||||||
request_id: { type: DataTypes.UUID, allowNull: false },
|
|
||||||
user_id: { type: DataTypes.UUID, allowNull: false },
|
|
||||||
user_name: { type: DataTypes.STRING(255), allowNull: true },
|
|
||||||
user_role: { type: DataTypes.STRING(50), allowNull: true },
|
|
||||||
message: { type: DataTypes.TEXT, allowNull: false },
|
|
||||||
message_type: { type: DataTypes.STRING(50), allowNull: true },
|
|
||||||
is_priority: { type: DataTypes.BOOLEAN, allowNull: true },
|
|
||||||
has_attachment: { type: DataTypes.BOOLEAN, allowNull: true },
|
|
||||||
parent_note_id: { type: DataTypes.UUID, allowNull: true },
|
|
||||||
mentioned_users: { type: DataTypes.ARRAY(DataTypes.UUID), allowNull: true },
|
|
||||||
reactions: { type: DataTypes.JSONB, allowNull: true },
|
|
||||||
is_edited: { type: DataTypes.BOOLEAN, allowNull: true },
|
|
||||||
is_deleted: { type: DataTypes.BOOLEAN, allowNull: true },
|
|
||||||
created_at: { type: DataTypes.DATE, allowNull: false, defaultValue: DataTypes.NOW },
|
|
||||||
updated_at: { type: DataTypes.DATE, allowNull: false, defaultValue: DataTypes.NOW }
|
|
||||||
});
|
|
||||||
await queryInterface.sequelize.query('CREATE INDEX IF NOT EXISTS "work_notes_request_id" ON "work_notes" ("request_id");');
|
|
||||||
await queryInterface.sequelize.query('CREATE INDEX IF NOT EXISTS "work_notes_user_id" ON "work_notes" ("user_id");');
|
|
||||||
await queryInterface.sequelize.query('CREATE INDEX IF NOT EXISTS "work_notes_created_at" ON "work_notes" ("created_at");');
|
|
||||||
},
|
|
||||||
down: async (queryInterface: QueryInterface) => {
|
|
||||||
await queryInterface.dropTable('work_notes');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
@ -1,25 +0,0 @@
|
|||||||
import { QueryInterface, DataTypes } from 'sequelize';
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
up: async (queryInterface: QueryInterface) => {
|
|
||||||
await queryInterface.createTable('work_note_attachments', {
|
|
||||||
attachment_id: { type: DataTypes.UUID, primaryKey: true, defaultValue: DataTypes.UUIDV4, allowNull: false },
|
|
||||||
note_id: { type: DataTypes.UUID, allowNull: false },
|
|
||||||
file_name: { type: DataTypes.STRING(255), allowNull: false },
|
|
||||||
file_type: { type: DataTypes.STRING(100), allowNull: false },
|
|
||||||
file_size: { type: DataTypes.BIGINT, allowNull: false },
|
|
||||||
file_path: { type: DataTypes.STRING(500), allowNull: false },
|
|
||||||
storage_url: { type: DataTypes.STRING(500), allowNull: true },
|
|
||||||
is_downloadable: { type: DataTypes.BOOLEAN, allowNull: true },
|
|
||||||
download_count: { type: DataTypes.INTEGER, allowNull: true, defaultValue: 0 },
|
|
||||||
uploaded_at: { type: DataTypes.DATE, allowNull: false, defaultValue: DataTypes.NOW }
|
|
||||||
});
|
|
||||||
await queryInterface.sequelize.query('CREATE INDEX IF NOT EXISTS "work_note_attachments_note_id" ON "work_note_attachments" ("note_id");');
|
|
||||||
await queryInterface.sequelize.query('CREATE INDEX IF NOT EXISTS "work_note_attachments_uploaded_at" ON "work_note_attachments" ("uploaded_at");');
|
|
||||||
},
|
|
||||||
down: async (queryInterface: QueryInterface) => {
|
|
||||||
await queryInterface.dropTable('work_note_attachments');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
@ -1,49 +0,0 @@
|
|||||||
import { QueryInterface, DataTypes } from 'sequelize';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Migration to add TAT alert tracking fields to approval_levels table
|
|
||||||
* These fields track whether TAT notifications have been sent
|
|
||||||
*/
|
|
||||||
export async function up(queryInterface: QueryInterface): Promise<void> {
|
|
||||||
// Check and add columns only if they don't exist
|
|
||||||
const tableDescription = await queryInterface.describeTable('approval_levels');
|
|
||||||
|
|
||||||
if (!tableDescription.tat50_alert_sent) {
|
|
||||||
await queryInterface.addColumn('approval_levels', 'tat50_alert_sent', {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: false
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!tableDescription.tat75_alert_sent) {
|
|
||||||
await queryInterface.addColumn('approval_levels', 'tat75_alert_sent', {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: false
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!tableDescription.tat_breached) {
|
|
||||||
await queryInterface.addColumn('approval_levels', 'tat_breached', {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: false
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!tableDescription.tat_start_time) {
|
|
||||||
await queryInterface.addColumn('approval_levels', 'tat_start_time', {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function down(queryInterface: QueryInterface): Promise<void> {
|
|
||||||
await queryInterface.removeColumn('approval_levels', 'tat50_alert_sent');
|
|
||||||
await queryInterface.removeColumn('approval_levels', 'tat75_alert_sent');
|
|
||||||
await queryInterface.removeColumn('approval_levels', 'tat_breached');
|
|
||||||
await queryInterface.removeColumn('approval_levels', 'tat_start_time');
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -1,134 +0,0 @@
|
|||||||
import { QueryInterface, DataTypes } from 'sequelize';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Migration to create admin_configurations table
|
|
||||||
* Stores system-wide configuration settings
|
|
||||||
*/
|
|
||||||
export async function up(queryInterface: QueryInterface): Promise<void> {
|
|
||||||
await queryInterface.createTable('admin_configurations', {
|
|
||||||
config_id: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
defaultValue: DataTypes.UUIDV4,
|
|
||||||
primaryKey: true
|
|
||||||
},
|
|
||||||
config_key: {
|
|
||||||
type: DataTypes.STRING(100),
|
|
||||||
allowNull: false,
|
|
||||||
unique: true,
|
|
||||||
comment: 'Unique configuration key (e.g., "DEFAULT_TAT_EXPRESS", "MAX_FILE_SIZE")'
|
|
||||||
},
|
|
||||||
config_category: {
|
|
||||||
type: DataTypes.ENUM(
|
|
||||||
'TAT_SETTINGS',
|
|
||||||
'NOTIFICATION_RULES',
|
|
||||||
'DOCUMENT_POLICY',
|
|
||||||
'USER_ROLES',
|
|
||||||
'DASHBOARD_LAYOUT',
|
|
||||||
'AI_CONFIGURATION',
|
|
||||||
'WORKFLOW_SHARING',
|
|
||||||
'SYSTEM_SETTINGS'
|
|
||||||
),
|
|
||||||
allowNull: false,
|
|
||||||
comment: 'Category of the configuration'
|
|
||||||
},
|
|
||||||
config_value: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
allowNull: false,
|
|
||||||
comment: 'Configuration value (can be JSON string for complex values)'
|
|
||||||
},
|
|
||||||
value_type: {
|
|
||||||
type: DataTypes.ENUM('STRING', 'NUMBER', 'BOOLEAN', 'JSON', 'ARRAY'),
|
|
||||||
defaultValue: 'STRING',
|
|
||||||
comment: 'Data type of the value'
|
|
||||||
},
|
|
||||||
display_name: {
|
|
||||||
type: DataTypes.STRING(200),
|
|
||||||
allowNull: false,
|
|
||||||
comment: 'Human-readable name for UI display'
|
|
||||||
},
|
|
||||||
description: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'Description of what this configuration does'
|
|
||||||
},
|
|
||||||
default_value: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'Default value if reset'
|
|
||||||
},
|
|
||||||
is_editable: {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
defaultValue: true,
|
|
||||||
comment: 'Whether this config can be edited by admin'
|
|
||||||
},
|
|
||||||
is_sensitive: {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
defaultValue: false,
|
|
||||||
comment: 'Whether this contains sensitive data (e.g., API keys)'
|
|
||||||
},
|
|
||||||
validation_rules: {
|
|
||||||
type: DataTypes.JSONB,
|
|
||||||
defaultValue: {},
|
|
||||||
comment: 'Validation rules (min, max, regex, etc.)'
|
|
||||||
},
|
|
||||||
ui_component: {
|
|
||||||
type: DataTypes.STRING(50),
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'UI component type (input, select, toggle, slider, etc.)'
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
type: DataTypes.JSONB,
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'Options for select/radio inputs'
|
|
||||||
},
|
|
||||||
sort_order: {
|
|
||||||
type: DataTypes.INTEGER,
|
|
||||||
defaultValue: 0,
|
|
||||||
comment: 'Display order in admin panel'
|
|
||||||
},
|
|
||||||
requires_restart: {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
defaultValue: false,
|
|
||||||
comment: 'Whether changing this requires server restart'
|
|
||||||
},
|
|
||||||
last_modified_by: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: true,
|
|
||||||
references: {
|
|
||||||
model: 'users',
|
|
||||||
key: 'user_id'
|
|
||||||
},
|
|
||||||
comment: 'Admin who last modified this'
|
|
||||||
},
|
|
||||||
last_modified_at: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'When this was last modified'
|
|
||||||
},
|
|
||||||
created_at: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW
|
|
||||||
},
|
|
||||||
updated_at: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Indexes (with IF NOT EXISTS)
|
|
||||||
await queryInterface.sequelize.query('CREATE INDEX IF NOT EXISTS "admin_configurations_config_category" ON "admin_configurations" ("config_category");');
|
|
||||||
await queryInterface.sequelize.query('CREATE INDEX IF NOT EXISTS "admin_configurations_is_editable" ON "admin_configurations" ("is_editable");');
|
|
||||||
await queryInterface.sequelize.query('CREATE INDEX IF NOT EXISTS "admin_configurations_sort_order" ON "admin_configurations" ("sort_order");');
|
|
||||||
|
|
||||||
// Admin config table created
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function down(queryInterface: QueryInterface): Promise<void> {
|
|
||||||
await queryInterface.dropTable('admin_configurations');
|
|
||||||
await queryInterface.sequelize.query('DROP TYPE IF EXISTS "enum_admin_configurations_config_category";');
|
|
||||||
await queryInterface.sequelize.query('DROP TYPE IF EXISTS "enum_admin_configurations_value_type";');
|
|
||||||
// Admin config table dropped
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -1,106 +0,0 @@
|
|||||||
import { QueryInterface, DataTypes } from 'sequelize';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Migration to create holidays table for organization holiday calendar
|
|
||||||
* Holidays are excluded from working days in TAT calculations for STANDARD priority
|
|
||||||
*/
|
|
||||||
export async function up(queryInterface: QueryInterface): Promise<void> {
|
|
||||||
await queryInterface.createTable('holidays', {
|
|
||||||
holiday_id: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
defaultValue: DataTypes.UUIDV4,
|
|
||||||
primaryKey: true
|
|
||||||
},
|
|
||||||
holiday_date: {
|
|
||||||
type: DataTypes.DATEONLY,
|
|
||||||
allowNull: false,
|
|
||||||
unique: true,
|
|
||||||
comment: 'The date of the holiday (YYYY-MM-DD)'
|
|
||||||
},
|
|
||||||
holiday_name: {
|
|
||||||
type: DataTypes.STRING(200),
|
|
||||||
allowNull: false,
|
|
||||||
comment: 'Name/title of the holiday (e.g., "Diwali", "Republic Day")'
|
|
||||||
},
|
|
||||||
description: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'Optional description or notes about the holiday'
|
|
||||||
},
|
|
||||||
is_recurring: {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
defaultValue: false,
|
|
||||||
comment: 'Whether this holiday recurs annually (e.g., Independence Day)'
|
|
||||||
},
|
|
||||||
recurrence_rule: {
|
|
||||||
type: DataTypes.STRING(100),
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'RRULE for recurring holidays (e.g., "FREQ=YEARLY;BYMONTH=8;BYMONTHDAY=15")'
|
|
||||||
},
|
|
||||||
holiday_type: {
|
|
||||||
type: DataTypes.ENUM('NATIONAL', 'REGIONAL', 'ORGANIZATIONAL', 'OPTIONAL'),
|
|
||||||
defaultValue: 'ORGANIZATIONAL',
|
|
||||||
comment: 'Type of holiday'
|
|
||||||
},
|
|
||||||
is_active: {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
defaultValue: true,
|
|
||||||
comment: 'Whether this holiday is currently active/applicable'
|
|
||||||
},
|
|
||||||
applies_to_departments: {
|
|
||||||
type: DataTypes.ARRAY(DataTypes.STRING),
|
|
||||||
allowNull: true,
|
|
||||||
defaultValue: null,
|
|
||||||
comment: 'If null, applies to all departments. Otherwise, specific departments only'
|
|
||||||
},
|
|
||||||
applies_to_locations: {
|
|
||||||
type: DataTypes.ARRAY(DataTypes.STRING),
|
|
||||||
allowNull: true,
|
|
||||||
defaultValue: null,
|
|
||||||
comment: 'If null, applies to all locations. Otherwise, specific locations only'
|
|
||||||
},
|
|
||||||
created_by: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
references: {
|
|
||||||
model: 'users',
|
|
||||||
key: 'user_id'
|
|
||||||
},
|
|
||||||
comment: 'Admin user who created this holiday'
|
|
||||||
},
|
|
||||||
updated_by: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: true,
|
|
||||||
references: {
|
|
||||||
model: 'users',
|
|
||||||
key: 'user_id'
|
|
||||||
},
|
|
||||||
comment: 'Admin user who last updated this holiday'
|
|
||||||
},
|
|
||||||
created_at: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW
|
|
||||||
},
|
|
||||||
updated_at: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Indexes for performance (with IF NOT EXISTS)
|
|
||||||
await queryInterface.sequelize.query('CREATE INDEX IF NOT EXISTS "holidays_holiday_date" ON "holidays" ("holiday_date");');
|
|
||||||
await queryInterface.sequelize.query('CREATE INDEX IF NOT EXISTS "holidays_is_active" ON "holidays" ("is_active");');
|
|
||||||
await queryInterface.sequelize.query('CREATE INDEX IF NOT EXISTS "holidays_holiday_type" ON "holidays" ("holiday_type");');
|
|
||||||
await queryInterface.sequelize.query('CREATE INDEX IF NOT EXISTS "holidays_created_by" ON "holidays" ("created_by");');
|
|
||||||
|
|
||||||
// Holidays table created
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function down(queryInterface: QueryInterface): Promise<void> {
|
|
||||||
await queryInterface.dropTable('holidays');
|
|
||||||
await queryInterface.sequelize.query('DROP TYPE IF EXISTS "enum_holidays_holiday_type";');
|
|
||||||
// Holidays table dropped
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -1,266 +0,0 @@
|
|||||||
import { QueryInterface } from 'sequelize';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Migration to create database views for KPI reporting
|
|
||||||
* These views pre-aggregate data for faster reporting queries
|
|
||||||
*/
|
|
||||||
export async function up(queryInterface: QueryInterface): Promise<void> {
|
|
||||||
|
|
||||||
// 1. Request Volume & Status Summary View
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
CREATE OR REPLACE VIEW vw_request_volume_summary AS
|
|
||||||
SELECT
|
|
||||||
w.request_id,
|
|
||||||
w.request_number,
|
|
||||||
w.title,
|
|
||||||
w.status,
|
|
||||||
w.priority,
|
|
||||||
w.template_type,
|
|
||||||
w.submission_date,
|
|
||||||
w.closure_date,
|
|
||||||
w.created_at,
|
|
||||||
u.user_id as initiator_id,
|
|
||||||
u.display_name as initiator_name,
|
|
||||||
u.department as initiator_department,
|
|
||||||
EXTRACT(EPOCH FROM (COALESCE(w.closure_date, NOW()) - w.submission_date)) / 3600 as cycle_time_hours,
|
|
||||||
EXTRACT(EPOCH FROM (NOW() - w.submission_date)) / 3600 as age_hours,
|
|
||||||
w.current_level,
|
|
||||||
w.total_levels,
|
|
||||||
w.total_tat_hours,
|
|
||||||
CASE
|
|
||||||
WHEN w.status IN ('APPROVED', 'REJECTED', 'CLOSED') THEN 'COMPLETED'
|
|
||||||
WHEN w.status = 'DRAFT' THEN 'DRAFT'
|
|
||||||
ELSE 'IN_PROGRESS'
|
|
||||||
END as status_category
|
|
||||||
FROM workflow_requests w
|
|
||||||
LEFT JOIN users u ON w.initiator_id = u.user_id
|
|
||||||
WHERE w.is_deleted = false;
|
|
||||||
`);
|
|
||||||
|
|
||||||
// 2. TAT Compliance View
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
CREATE OR REPLACE VIEW vw_tat_compliance AS
|
|
||||||
SELECT
|
|
||||||
al.level_id,
|
|
||||||
al.request_id,
|
|
||||||
w.request_number,
|
|
||||||
w.priority,
|
|
||||||
w.status as request_status,
|
|
||||||
al.level_number,
|
|
||||||
al.approver_id,
|
|
||||||
al.approver_name,
|
|
||||||
u.department as approver_department,
|
|
||||||
al.status as level_status,
|
|
||||||
al.tat_hours as allocated_hours,
|
|
||||||
al.elapsed_hours,
|
|
||||||
al.remaining_hours,
|
|
||||||
al.tat_percentage_used,
|
|
||||||
al.level_start_time,
|
|
||||||
al.level_end_time,
|
|
||||||
al.action_date,
|
|
||||||
al.tat50_alert_sent,
|
|
||||||
al.tat75_alert_sent,
|
|
||||||
al.tat_breached,
|
|
||||||
CASE
|
|
||||||
WHEN al.status IN ('APPROVED', 'REJECTED') AND al.elapsed_hours <= al.tat_hours THEN true
|
|
||||||
WHEN al.status IN ('APPROVED', 'REJECTED') AND al.elapsed_hours > al.tat_hours THEN false
|
|
||||||
WHEN al.status IN ('PENDING', 'IN_PROGRESS') AND al.tat_percentage_used >= 100 THEN false
|
|
||||||
ELSE null
|
|
||||||
END as completed_within_tat,
|
|
||||||
CASE
|
|
||||||
WHEN al.tat_percentage_used < 50 THEN 'ON_TRACK'
|
|
||||||
WHEN al.tat_percentage_used < 75 THEN 'AT_RISK'
|
|
||||||
WHEN al.tat_percentage_used < 100 THEN 'CRITICAL'
|
|
||||||
ELSE 'BREACHED'
|
|
||||||
END as tat_status,
|
|
||||||
CASE
|
|
||||||
WHEN al.status IN ('APPROVED', 'REJECTED') THEN
|
|
||||||
al.tat_hours - al.elapsed_hours
|
|
||||||
ELSE 0
|
|
||||||
END as time_saved_hours
|
|
||||||
FROM approval_levels al
|
|
||||||
JOIN workflow_requests w ON al.request_id = w.request_id
|
|
||||||
LEFT JOIN users u ON al.approver_id = u.user_id
|
|
||||||
WHERE w.is_deleted = false;
|
|
||||||
`);
|
|
||||||
|
|
||||||
// 3. Approver Performance View
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
CREATE OR REPLACE VIEW vw_approver_performance AS
|
|
||||||
SELECT
|
|
||||||
al.approver_id,
|
|
||||||
u.display_name as approver_name,
|
|
||||||
u.department,
|
|
||||||
u.designation,
|
|
||||||
COUNT(*) as total_assignments,
|
|
||||||
COUNT(CASE WHEN al.status = 'PENDING' THEN 1 END) as pending_count,
|
|
||||||
COUNT(CASE WHEN al.status = 'IN_PROGRESS' THEN 1 END) as in_progress_count,
|
|
||||||
COUNT(CASE WHEN al.status = 'APPROVED' THEN 1 END) as approved_count,
|
|
||||||
COUNT(CASE WHEN al.status = 'REJECTED' THEN 1 END) as rejected_count,
|
|
||||||
AVG(CASE WHEN al.status IN ('APPROVED', 'REJECTED') THEN al.elapsed_hours END) as avg_response_time_hours,
|
|
||||||
SUM(CASE WHEN al.elapsed_hours <= al.tat_hours AND al.status IN ('APPROVED', 'REJECTED') THEN 1 ELSE 0 END)::FLOAT /
|
|
||||||
NULLIF(COUNT(CASE WHEN al.status IN ('APPROVED', 'REJECTED') THEN 1 END), 0) * 100 as tat_compliance_percentage,
|
|
||||||
COUNT(CASE WHEN al.tat_breached = true THEN 1 END) as breaches_count,
|
|
||||||
MIN(CASE WHEN al.status = 'PENDING' OR al.status = 'IN_PROGRESS' THEN
|
|
||||||
EXTRACT(EPOCH FROM (NOW() - al.level_start_time)) / 3600
|
|
||||||
END) as oldest_pending_hours
|
|
||||||
FROM approval_levels al
|
|
||||||
JOIN users u ON al.approver_id = u.user_id
|
|
||||||
JOIN workflow_requests w ON al.request_id = w.request_id
|
|
||||||
WHERE w.is_deleted = false
|
|
||||||
GROUP BY al.approver_id, u.display_name, u.department, u.designation;
|
|
||||||
`);
|
|
||||||
|
|
||||||
// 4. TAT Alerts Summary View
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
CREATE OR REPLACE VIEW vw_tat_alerts_summary AS
|
|
||||||
SELECT
|
|
||||||
ta.alert_id,
|
|
||||||
ta.request_id,
|
|
||||||
w.request_number,
|
|
||||||
w.title as request_title,
|
|
||||||
w.priority,
|
|
||||||
ta.level_id,
|
|
||||||
al.level_number,
|
|
||||||
ta.approver_id,
|
|
||||||
ta.alert_type,
|
|
||||||
ta.threshold_percentage,
|
|
||||||
ta.tat_hours_allocated,
|
|
||||||
ta.tat_hours_elapsed,
|
|
||||||
ta.tat_hours_remaining,
|
|
||||||
ta.alert_sent_at,
|
|
||||||
ta.expected_completion_time,
|
|
||||||
ta.is_breached,
|
|
||||||
ta.was_completed_on_time,
|
|
||||||
ta.completion_time,
|
|
||||||
al.status as level_status,
|
|
||||||
EXTRACT(EPOCH FROM (ta.alert_sent_at - ta.level_start_time)) / 3600 as hours_before_alert,
|
|
||||||
CASE
|
|
||||||
WHEN ta.completion_time IS NOT NULL THEN
|
|
||||||
EXTRACT(EPOCH FROM (ta.completion_time - ta.alert_sent_at)) / 3600
|
|
||||||
ELSE NULL
|
|
||||||
END as response_time_after_alert_hours,
|
|
||||||
ta.metadata
|
|
||||||
FROM tat_alerts ta
|
|
||||||
JOIN workflow_requests w ON ta.request_id = w.request_id
|
|
||||||
JOIN approval_levels al ON ta.level_id = al.level_id
|
|
||||||
WHERE w.is_deleted = false
|
|
||||||
ORDER BY ta.alert_sent_at DESC;
|
|
||||||
`);
|
|
||||||
|
|
||||||
// 5. Department-wise Workflow Summary View
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
CREATE OR REPLACE VIEW vw_department_summary AS
|
|
||||||
SELECT
|
|
||||||
u.department,
|
|
||||||
COUNT(DISTINCT w.request_id) as total_requests,
|
|
||||||
COUNT(DISTINCT CASE WHEN w.status = 'DRAFT' THEN w.request_id END) as draft_requests,
|
|
||||||
COUNT(DISTINCT CASE WHEN w.status IN ('PENDING', 'IN_PROGRESS') THEN w.request_id END) as open_requests,
|
|
||||||
COUNT(DISTINCT CASE WHEN w.status = 'APPROVED' THEN w.request_id END) as approved_requests,
|
|
||||||
COUNT(DISTINCT CASE WHEN w.status = 'REJECTED' THEN w.request_id END) as rejected_requests,
|
|
||||||
AVG(CASE WHEN w.closure_date IS NOT NULL THEN
|
|
||||||
EXTRACT(EPOCH FROM (w.closure_date - w.submission_date)) / 3600
|
|
||||||
END) as avg_cycle_time_hours,
|
|
||||||
COUNT(DISTINCT CASE WHEN w.priority = 'EXPRESS' THEN w.request_id END) as express_priority_count,
|
|
||||||
COUNT(DISTINCT CASE WHEN w.priority = 'STANDARD' THEN w.request_id END) as standard_priority_count
|
|
||||||
FROM users u
|
|
||||||
LEFT JOIN workflow_requests w ON u.user_id = w.initiator_id AND w.is_deleted = false
|
|
||||||
WHERE u.department IS NOT NULL
|
|
||||||
GROUP BY u.department;
|
|
||||||
`);
|
|
||||||
|
|
||||||
// 6. Daily/Weekly KPI Metrics View
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
CREATE OR REPLACE VIEW vw_daily_kpi_metrics AS
|
|
||||||
SELECT
|
|
||||||
DATE(w.created_at) as date,
|
|
||||||
COUNT(*) as requests_created,
|
|
||||||
COUNT(CASE WHEN w.submission_date IS NOT NULL AND DATE(w.submission_date) = DATE(w.created_at) THEN 1 END) as requests_submitted,
|
|
||||||
COUNT(CASE WHEN w.closure_date IS NOT NULL AND DATE(w.closure_date) = DATE(w.created_at) THEN 1 END) as requests_closed,
|
|
||||||
COUNT(CASE WHEN w.status = 'APPROVED' AND DATE(w.closure_date) = DATE(w.created_at) THEN 1 END) as requests_approved,
|
|
||||||
COUNT(CASE WHEN w.status = 'REJECTED' AND DATE(w.closure_date) = DATE(w.created_at) THEN 1 END) as requests_rejected,
|
|
||||||
AVG(CASE WHEN w.closure_date IS NOT NULL AND DATE(w.closure_date) = DATE(w.created_at) THEN
|
|
||||||
EXTRACT(EPOCH FROM (w.closure_date - w.submission_date)) / 3600
|
|
||||||
END) as avg_completion_time_hours
|
|
||||||
FROM workflow_requests w
|
|
||||||
WHERE w.is_deleted = false
|
|
||||||
GROUP BY DATE(w.created_at)
|
|
||||||
ORDER BY DATE(w.created_at) DESC;
|
|
||||||
`);
|
|
||||||
|
|
||||||
// 7. Workflow Aging Report View
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
CREATE OR REPLACE VIEW vw_workflow_aging AS
|
|
||||||
SELECT
|
|
||||||
w.request_id,
|
|
||||||
w.request_number,
|
|
||||||
w.title,
|
|
||||||
w.status,
|
|
||||||
w.priority,
|
|
||||||
w.current_level,
|
|
||||||
w.total_levels,
|
|
||||||
w.submission_date,
|
|
||||||
EXTRACT(EPOCH FROM (NOW() - w.submission_date)) / (3600 * 24) as age_days,
|
|
||||||
CASE
|
|
||||||
WHEN EXTRACT(EPOCH FROM (NOW() - w.submission_date)) / (3600 * 24) < 3 THEN 'FRESH'
|
|
||||||
WHEN EXTRACT(EPOCH FROM (NOW() - w.submission_date)) / (3600 * 24) < 7 THEN 'NORMAL'
|
|
||||||
WHEN EXTRACT(EPOCH FROM (NOW() - w.submission_date)) / (3600 * 24) < 14 THEN 'AGING'
|
|
||||||
ELSE 'CRITICAL'
|
|
||||||
END as age_category,
|
|
||||||
al.approver_name as current_approver,
|
|
||||||
al.level_start_time as current_level_start,
|
|
||||||
EXTRACT(EPOCH FROM (NOW() - al.level_start_time)) / 3600 as current_level_age_hours,
|
|
||||||
al.tat_hours as current_level_tat_hours,
|
|
||||||
al.tat_percentage_used as current_level_tat_used
|
|
||||||
FROM workflow_requests w
|
|
||||||
LEFT JOIN approval_levels al ON w.request_id = al.request_id
|
|
||||||
AND al.level_number = w.current_level
|
|
||||||
AND al.status IN ('PENDING', 'IN_PROGRESS')
|
|
||||||
WHERE w.status IN ('PENDING', 'IN_PROGRESS')
|
|
||||||
AND w.is_deleted = false
|
|
||||||
ORDER BY age_days DESC;
|
|
||||||
`);
|
|
||||||
|
|
||||||
// 8. Engagement & Quality Metrics View
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
CREATE OR REPLACE VIEW vw_engagement_metrics AS
|
|
||||||
SELECT
|
|
||||||
w.request_id,
|
|
||||||
w.request_number,
|
|
||||||
w.title,
|
|
||||||
w.status,
|
|
||||||
COUNT(DISTINCT wn.note_id) as work_notes_count,
|
|
||||||
COUNT(DISTINCT d.document_id) as documents_count,
|
|
||||||
COUNT(DISTINCT p.participant_id) as spectators_count,
|
|
||||||
COUNT(DISTINCT al.approver_id) as approvers_count,
|
|
||||||
MAX(wn.created_at) as last_comment_date,
|
|
||||||
MAX(d.uploaded_at) as last_document_date,
|
|
||||||
CASE
|
|
||||||
WHEN COUNT(DISTINCT wn.note_id) > 10 THEN 'HIGH'
|
|
||||||
WHEN COUNT(DISTINCT wn.note_id) > 5 THEN 'MEDIUM'
|
|
||||||
ELSE 'LOW'
|
|
||||||
END as engagement_level
|
|
||||||
FROM workflow_requests w
|
|
||||||
LEFT JOIN work_notes wn ON w.request_id = wn.request_id AND wn.is_deleted = false
|
|
||||||
LEFT JOIN documents d ON w.request_id = d.request_id AND d.is_deleted = false
|
|
||||||
LEFT JOIN participants p ON w.request_id = p.request_id AND p.participant_type = 'SPECTATOR'
|
|
||||||
LEFT JOIN approval_levels al ON w.request_id = al.request_id
|
|
||||||
WHERE w.is_deleted = false
|
|
||||||
GROUP BY w.request_id, w.request_number, w.title, w.status;
|
|
||||||
`);
|
|
||||||
|
|
||||||
// KPI views created
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function down(queryInterface: QueryInterface): Promise<void> {
|
|
||||||
await queryInterface.sequelize.query('DROP VIEW IF EXISTS vw_engagement_metrics;');
|
|
||||||
await queryInterface.sequelize.query('DROP VIEW IF EXISTS vw_workflow_aging;');
|
|
||||||
await queryInterface.sequelize.query('DROP VIEW IF EXISTS vw_daily_kpi_metrics;');
|
|
||||||
await queryInterface.sequelize.query('DROP VIEW IF EXISTS vw_department_summary;');
|
|
||||||
await queryInterface.sequelize.query('DROP VIEW IF EXISTS vw_tat_alerts_summary;');
|
|
||||||
await queryInterface.sequelize.query('DROP VIEW IF EXISTS vw_approver_performance;');
|
|
||||||
await queryInterface.sequelize.query('DROP VIEW IF EXISTS vw_tat_compliance;');
|
|
||||||
await queryInterface.sequelize.query('DROP VIEW IF EXISTS vw_request_volume_summary;');
|
|
||||||
// KPI views dropped
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -1,134 +0,0 @@
|
|||||||
import { QueryInterface, DataTypes } from 'sequelize';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Migration to create TAT alerts/reminders table
|
|
||||||
* Stores all TAT-related notifications sent (50%, 75%, 100%)
|
|
||||||
*/
|
|
||||||
export async function up(queryInterface: QueryInterface): Promise<void> {
|
|
||||||
await queryInterface.createTable('tat_alerts', {
|
|
||||||
alert_id: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
defaultValue: DataTypes.UUIDV4,
|
|
||||||
primaryKey: true
|
|
||||||
},
|
|
||||||
request_id: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
references: {
|
|
||||||
model: 'workflow_requests',
|
|
||||||
key: 'request_id'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
level_id: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
references: {
|
|
||||||
model: 'approval_levels',
|
|
||||||
key: 'level_id'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
approver_id: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
references: {
|
|
||||||
model: 'users',
|
|
||||||
key: 'user_id'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
alert_type: {
|
|
||||||
type: DataTypes.ENUM('TAT_50', 'TAT_75', 'TAT_100'),
|
|
||||||
allowNull: false
|
|
||||||
},
|
|
||||||
threshold_percentage: {
|
|
||||||
type: DataTypes.INTEGER,
|
|
||||||
allowNull: false,
|
|
||||||
comment: '50, 75, or 100'
|
|
||||||
},
|
|
||||||
tat_hours_allocated: {
|
|
||||||
type: DataTypes.DECIMAL(10, 2),
|
|
||||||
allowNull: false,
|
|
||||||
comment: 'Total TAT hours for this level'
|
|
||||||
},
|
|
||||||
tat_hours_elapsed: {
|
|
||||||
type: DataTypes.DECIMAL(10, 2),
|
|
||||||
allowNull: false,
|
|
||||||
comment: 'Hours elapsed when alert was sent'
|
|
||||||
},
|
|
||||||
tat_hours_remaining: {
|
|
||||||
type: DataTypes.DECIMAL(10, 2),
|
|
||||||
allowNull: false,
|
|
||||||
comment: 'Hours remaining when alert was sent'
|
|
||||||
},
|
|
||||||
level_start_time: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
comment: 'When the approval level started'
|
|
||||||
},
|
|
||||||
alert_sent_at: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW,
|
|
||||||
comment: 'When the alert was sent'
|
|
||||||
},
|
|
||||||
expected_completion_time: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
comment: 'When the level should be completed'
|
|
||||||
},
|
|
||||||
alert_message: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
allowNull: false,
|
|
||||||
comment: 'The notification message sent'
|
|
||||||
},
|
|
||||||
notification_sent: {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
defaultValue: true,
|
|
||||||
comment: 'Whether notification was successfully sent'
|
|
||||||
},
|
|
||||||
notification_channels: {
|
|
||||||
type: DataTypes.ARRAY(DataTypes.STRING),
|
|
||||||
defaultValue: [],
|
|
||||||
comment: 'push, email, sms'
|
|
||||||
},
|
|
||||||
is_breached: {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
defaultValue: false,
|
|
||||||
comment: 'Whether this was a breach alert (100%)'
|
|
||||||
},
|
|
||||||
was_completed_on_time: {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'Set when level is completed - was it on time?'
|
|
||||||
},
|
|
||||||
completion_time: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'When the level was actually completed'
|
|
||||||
},
|
|
||||||
metadata: {
|
|
||||||
type: DataTypes.JSONB,
|
|
||||||
defaultValue: {},
|
|
||||||
comment: 'Additional context (priority, request title, etc.)'
|
|
||||||
},
|
|
||||||
created_at: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Indexes for performance (with IF NOT EXISTS check)
|
|
||||||
await queryInterface.sequelize.query('CREATE INDEX IF NOT EXISTS "tat_alerts_request_id" ON "tat_alerts" ("request_id");');
|
|
||||||
await queryInterface.sequelize.query('CREATE INDEX IF NOT EXISTS "tat_alerts_level_id" ON "tat_alerts" ("level_id");');
|
|
||||||
await queryInterface.sequelize.query('CREATE INDEX IF NOT EXISTS "tat_alerts_approver_id" ON "tat_alerts" ("approver_id");');
|
|
||||||
await queryInterface.sequelize.query('CREATE INDEX IF NOT EXISTS "tat_alerts_alert_type" ON "tat_alerts" ("alert_type");');
|
|
||||||
await queryInterface.sequelize.query('CREATE INDEX IF NOT EXISTS "tat_alerts_alert_sent_at" ON "tat_alerts" ("alert_sent_at");');
|
|
||||||
await queryInterface.sequelize.query('CREATE INDEX IF NOT EXISTS "tat_alerts_is_breached" ON "tat_alerts" ("is_breached");');
|
|
||||||
await queryInterface.sequelize.query('CREATE INDEX IF NOT EXISTS "tat_alerts_was_completed_on_time" ON "tat_alerts" ("was_completed_on_time");');
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function down(queryInterface: QueryInterface): Promise<void> {
|
|
||||||
await queryInterface.dropTable('tat_alerts');
|
|
||||||
await queryInterface.sequelize.query('DROP TYPE IF EXISTS "enum_tat_alerts_alert_type";');
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -1,97 +0,0 @@
|
|||||||
import { QueryInterface, DataTypes } from 'sequelize';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Migration: Add skip-related fields to approval_levels table
|
|
||||||
* Purpose: Track approvers who were skipped by initiator
|
|
||||||
* Date: 2025-11-05
|
|
||||||
*/
|
|
||||||
|
|
||||||
export async function up(queryInterface: QueryInterface): Promise<void> {
|
|
||||||
// Check if table exists first
|
|
||||||
const tables = await queryInterface.showAllTables();
|
|
||||||
if (!tables.includes('approval_levels')) {
|
|
||||||
// Table doesn't exist yet, skipping
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get existing columns
|
|
||||||
const tableDescription = await queryInterface.describeTable('approval_levels');
|
|
||||||
|
|
||||||
// Add skip-related columns only if they don't exist
|
|
||||||
if (!tableDescription.is_skipped) {
|
|
||||||
await queryInterface.addColumn('approval_levels', 'is_skipped', {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: false,
|
|
||||||
comment: 'Indicates if this approver was skipped by initiator'
|
|
||||||
});
|
|
||||||
// Added is_skipped column
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!tableDescription.skipped_at) {
|
|
||||||
await queryInterface.addColumn('approval_levels', 'skipped_at', {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'Timestamp when approver was skipped'
|
|
||||||
});
|
|
||||||
// Added skipped_at column
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!tableDescription.skipped_by) {
|
|
||||||
await queryInterface.addColumn('approval_levels', 'skipped_by', {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: true,
|
|
||||||
references: {
|
|
||||||
model: 'users',
|
|
||||||
key: 'user_id'
|
|
||||||
},
|
|
||||||
onUpdate: 'CASCADE',
|
|
||||||
onDelete: 'SET NULL',
|
|
||||||
comment: 'User ID who skipped this approver'
|
|
||||||
});
|
|
||||||
// Added skipped_by column
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!tableDescription.skip_reason) {
|
|
||||||
await queryInterface.addColumn('approval_levels', 'skip_reason', {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'Reason for skipping this approver'
|
|
||||||
});
|
|
||||||
// Added skip_reason column
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if index exists before creating
|
|
||||||
try {
|
|
||||||
const indexes: any[] = await queryInterface.showIndex('approval_levels') as any[];
|
|
||||||
const indexExists = Array.isArray(indexes) && indexes.some((idx: any) => idx.name === 'idx_approval_levels_skipped');
|
|
||||||
|
|
||||||
if (!indexExists) {
|
|
||||||
await queryInterface.addIndex('approval_levels', ['is_skipped'], {
|
|
||||||
name: 'idx_approval_levels_skipped',
|
|
||||||
where: {
|
|
||||||
is_skipped: true
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// Index added
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// Index already exists
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip fields added
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function down(queryInterface: QueryInterface): Promise<void> {
|
|
||||||
// Remove index first
|
|
||||||
await queryInterface.removeIndex('approval_levels', 'idx_approval_levels_skipped');
|
|
||||||
|
|
||||||
// Remove columns
|
|
||||||
await queryInterface.removeColumn('approval_levels', 'skip_reason');
|
|
||||||
await queryInterface.removeColumn('approval_levels', 'skipped_by');
|
|
||||||
await queryInterface.removeColumn('approval_levels', 'skipped_at');
|
|
||||||
await queryInterface.removeColumn('approval_levels', 'is_skipped');
|
|
||||||
|
|
||||||
// Skip fields removed
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -1,76 +0,0 @@
|
|||||||
import { QueryInterface } from 'sequelize';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Migration: Convert tat_days to GENERATED STORED column
|
|
||||||
*
|
|
||||||
* This ensures tat_days is auto-calculated from tat_hours across all environments.
|
|
||||||
* Production already has this as a generated column, this migration makes other environments consistent.
|
|
||||||
*/
|
|
||||||
export async function up(queryInterface: QueryInterface): Promise<void> {
|
|
||||||
// Check if tat_days is already a generated column
|
|
||||||
const result = await queryInterface.sequelize.query(`
|
|
||||||
SELECT
|
|
||||||
a.attname as column_name,
|
|
||||||
a.attgenerated as is_generated
|
|
||||||
FROM pg_attribute a
|
|
||||||
JOIN pg_class c ON a.attrelid = c.oid
|
|
||||||
WHERE c.relname = 'approval_levels'
|
|
||||||
AND a.attname = 'tat_days'
|
|
||||||
AND NOT a.attisdropped;
|
|
||||||
`, { type: 'SELECT' });
|
|
||||||
|
|
||||||
const column = result[0] as any;
|
|
||||||
|
|
||||||
if (column && column.is_generated === 's') {
|
|
||||||
// Already a GENERATED column, skipping
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Converting tat_days to GENERATED column
|
|
||||||
|
|
||||||
// Step 1: Drop the existing regular column
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
ALTER TABLE approval_levels DROP COLUMN IF EXISTS tat_days;
|
|
||||||
`);
|
|
||||||
|
|
||||||
// Step 2: Add it back as a GENERATED STORED column
|
|
||||||
// Formula: CEIL(tat_hours / 24.0) - rounds up to nearest day
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
ALTER TABLE approval_levels
|
|
||||||
ADD COLUMN tat_days INTEGER
|
|
||||||
GENERATED ALWAYS AS (CAST(CEIL(tat_hours / 24.0) AS INTEGER)) STORED;
|
|
||||||
`);
|
|
||||||
|
|
||||||
// tat_days is now auto-calculated
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function down(queryInterface: QueryInterface): Promise<void> {
|
|
||||||
// Rolling back to regular column
|
|
||||||
|
|
||||||
// Drop the generated column
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
ALTER TABLE approval_levels DROP COLUMN IF EXISTS tat_days;
|
|
||||||
`);
|
|
||||||
|
|
||||||
// Add it back as a regular column (with default calculation for existing rows)
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
ALTER TABLE approval_levels
|
|
||||||
ADD COLUMN tat_days INTEGER;
|
|
||||||
`);
|
|
||||||
|
|
||||||
// Populate existing rows with calculated values
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
UPDATE approval_levels
|
|
||||||
SET tat_days = CAST(CEIL(tat_hours / 24.0) AS INTEGER)
|
|
||||||
WHERE tat_days IS NULL;
|
|
||||||
`);
|
|
||||||
|
|
||||||
// Make it NOT NULL after populating
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
ALTER TABLE approval_levels
|
|
||||||
ALTER COLUMN tat_days SET NOT NULL;
|
|
||||||
`);
|
|
||||||
|
|
||||||
// Rolled back successfully
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -1,109 +0,0 @@
|
|||||||
import { QueryInterface, DataTypes } from 'sequelize';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Migration to create conclusion_remarks table
|
|
||||||
* Stores AI-generated and finalized conclusion remarks for workflow requests
|
|
||||||
*/
|
|
||||||
export async function up(queryInterface: QueryInterface): Promise<void> {
|
|
||||||
await queryInterface.createTable('conclusion_remarks', {
|
|
||||||
conclusion_id: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
defaultValue: DataTypes.UUIDV4,
|
|
||||||
primaryKey: true,
|
|
||||||
allowNull: false
|
|
||||||
},
|
|
||||||
request_id: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
references: {
|
|
||||||
model: 'workflow_requests',
|
|
||||||
key: 'request_id'
|
|
||||||
},
|
|
||||||
onUpdate: 'CASCADE',
|
|
||||||
onDelete: 'CASCADE',
|
|
||||||
unique: true // One conclusion per request
|
|
||||||
},
|
|
||||||
ai_generated_remark: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
allowNull: true
|
|
||||||
},
|
|
||||||
ai_model_used: {
|
|
||||||
type: DataTypes.STRING(100),
|
|
||||||
allowNull: true
|
|
||||||
},
|
|
||||||
ai_confidence_score: {
|
|
||||||
type: DataTypes.DECIMAL(5, 2),
|
|
||||||
allowNull: true
|
|
||||||
},
|
|
||||||
final_remark: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
allowNull: true
|
|
||||||
},
|
|
||||||
edited_by: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: true,
|
|
||||||
references: {
|
|
||||||
model: 'users',
|
|
||||||
key: 'user_id'
|
|
||||||
},
|
|
||||||
onUpdate: 'CASCADE',
|
|
||||||
onDelete: 'SET NULL'
|
|
||||||
},
|
|
||||||
is_edited: {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: false
|
|
||||||
},
|
|
||||||
edit_count: {
|
|
||||||
type: DataTypes.INTEGER,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: 0
|
|
||||||
},
|
|
||||||
approval_summary: {
|
|
||||||
type: DataTypes.JSONB,
|
|
||||||
allowNull: true
|
|
||||||
},
|
|
||||||
document_summary: {
|
|
||||||
type: DataTypes.JSONB,
|
|
||||||
allowNull: true
|
|
||||||
},
|
|
||||||
key_discussion_points: {
|
|
||||||
type: DataTypes.ARRAY(DataTypes.TEXT),
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: []
|
|
||||||
},
|
|
||||||
generated_at: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: true
|
|
||||||
},
|
|
||||||
finalized_at: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: true
|
|
||||||
},
|
|
||||||
created_at: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW
|
|
||||||
},
|
|
||||||
updated_at: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add index on request_id for faster lookups
|
|
||||||
await queryInterface.addIndex('conclusion_remarks', ['request_id'], {
|
|
||||||
name: 'idx_conclusion_remarks_request_id'
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add index on finalized_at for KPI queries
|
|
||||||
await queryInterface.addIndex('conclusion_remarks', ['finalized_at'], {
|
|
||||||
name: 'idx_conclusion_remarks_finalized_at'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function down(queryInterface: QueryInterface): Promise<void> {
|
|
||||||
await queryInterface.dropTable('conclusion_remarks');
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -1,137 +0,0 @@
|
|||||||
import { QueryInterface, DataTypes } from 'sequelize';
|
|
||||||
|
|
||||||
export async function up(queryInterface: QueryInterface): Promise<void> {
|
|
||||||
// Create priority enum type
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
DO $$ BEGIN
|
|
||||||
CREATE TYPE notification_priority_enum AS ENUM ('LOW', 'MEDIUM', 'HIGH', 'URGENT');
|
|
||||||
EXCEPTION
|
|
||||||
WHEN duplicate_object THEN null;
|
|
||||||
END $$;
|
|
||||||
`);
|
|
||||||
|
|
||||||
// Create notifications table
|
|
||||||
await queryInterface.createTable('notifications', {
|
|
||||||
notification_id: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
defaultValue: DataTypes.UUIDV4,
|
|
||||||
primaryKey: true
|
|
||||||
},
|
|
||||||
user_id: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
references: {
|
|
||||||
model: 'users',
|
|
||||||
key: 'user_id'
|
|
||||||
},
|
|
||||||
onUpdate: 'CASCADE',
|
|
||||||
onDelete: 'CASCADE'
|
|
||||||
},
|
|
||||||
request_id: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: true,
|
|
||||||
references: {
|
|
||||||
model: 'workflow_requests',
|
|
||||||
key: 'request_id'
|
|
||||||
},
|
|
||||||
onUpdate: 'CASCADE',
|
|
||||||
onDelete: 'SET NULL'
|
|
||||||
},
|
|
||||||
notification_type: {
|
|
||||||
type: DataTypes.STRING(50),
|
|
||||||
allowNull: false
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
type: DataTypes.STRING(255),
|
|
||||||
allowNull: false
|
|
||||||
},
|
|
||||||
message: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
allowNull: false
|
|
||||||
},
|
|
||||||
is_read: {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
defaultValue: false,
|
|
||||||
allowNull: false
|
|
||||||
},
|
|
||||||
priority: {
|
|
||||||
type: 'notification_priority_enum',
|
|
||||||
defaultValue: 'MEDIUM',
|
|
||||||
allowNull: false
|
|
||||||
},
|
|
||||||
action_url: {
|
|
||||||
type: DataTypes.STRING(500),
|
|
||||||
allowNull: true
|
|
||||||
},
|
|
||||||
action_required: {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
defaultValue: false,
|
|
||||||
allowNull: false
|
|
||||||
},
|
|
||||||
metadata: {
|
|
||||||
type: DataTypes.JSONB,
|
|
||||||
allowNull: true,
|
|
||||||
defaultValue: {}
|
|
||||||
},
|
|
||||||
sent_via: {
|
|
||||||
type: DataTypes.ARRAY(DataTypes.STRING),
|
|
||||||
defaultValue: [],
|
|
||||||
allowNull: false
|
|
||||||
},
|
|
||||||
email_sent: {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
defaultValue: false,
|
|
||||||
allowNull: false
|
|
||||||
},
|
|
||||||
sms_sent: {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
defaultValue: false,
|
|
||||||
allowNull: false
|
|
||||||
},
|
|
||||||
push_sent: {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
defaultValue: false,
|
|
||||||
allowNull: false
|
|
||||||
},
|
|
||||||
read_at: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: true
|
|
||||||
},
|
|
||||||
expires_at: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: true
|
|
||||||
},
|
|
||||||
created_at: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create indexes for better query performance
|
|
||||||
await queryInterface.addIndex('notifications', ['user_id'], {
|
|
||||||
name: 'idx_notifications_user_id'
|
|
||||||
});
|
|
||||||
|
|
||||||
await queryInterface.addIndex('notifications', ['user_id', 'is_read'], {
|
|
||||||
name: 'idx_notifications_user_unread'
|
|
||||||
});
|
|
||||||
|
|
||||||
await queryInterface.addIndex('notifications', ['request_id'], {
|
|
||||||
name: 'idx_notifications_request_id'
|
|
||||||
});
|
|
||||||
|
|
||||||
await queryInterface.addIndex('notifications', ['created_at'], {
|
|
||||||
name: 'idx_notifications_created_at'
|
|
||||||
});
|
|
||||||
|
|
||||||
await queryInterface.addIndex('notifications', ['notification_type'], {
|
|
||||||
name: 'idx_notifications_type'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function down(queryInterface: QueryInterface): Promise<void> {
|
|
||||||
await queryInterface.dropTable('notifications');
|
|
||||||
await queryInterface.sequelize.query('DROP TYPE IF EXISTS notification_priority_enum;');
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -1,49 +0,0 @@
|
|||||||
import { QueryInterface, DataTypes } from 'sequelize';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Migration: Add breach_reason column to approval_levels table
|
|
||||||
* Purpose: Store TAT breach reason directly in approval_levels table
|
|
||||||
* Date: 2025-11-18
|
|
||||||
*/
|
|
||||||
|
|
||||||
export async function up(queryInterface: QueryInterface): Promise<void> {
|
|
||||||
// Check if table exists first
|
|
||||||
const tables = await queryInterface.showAllTables();
|
|
||||||
if (!tables.includes('approval_levels')) {
|
|
||||||
// Table doesn't exist yet, skipping
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get existing columns
|
|
||||||
const tableDescription = await queryInterface.describeTable('approval_levels');
|
|
||||||
|
|
||||||
// Add breach_reason column only if it doesn't exist
|
|
||||||
if (!tableDescription.breach_reason) {
|
|
||||||
await queryInterface.addColumn('approval_levels', 'breach_reason', {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'Reason for TAT breach - can contain paragraph-length text'
|
|
||||||
});
|
|
||||||
console.log('✅ Added breach_reason column to approval_levels table');
|
|
||||||
} else {
|
|
||||||
console.log('ℹ️ breach_reason column already exists, skipping');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function down(queryInterface: QueryInterface): Promise<void> {
|
|
||||||
// Check if table exists
|
|
||||||
const tables = await queryInterface.showAllTables();
|
|
||||||
if (!tables.includes('approval_levels')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get existing columns
|
|
||||||
const tableDescription = await queryInterface.describeTable('approval_levels');
|
|
||||||
|
|
||||||
// Remove column only if it exists
|
|
||||||
if (tableDescription.breach_reason) {
|
|
||||||
await queryInterface.removeColumn('approval_levels', 'breach_reason');
|
|
||||||
console.log('✅ Removed breach_reason column from approval_levels table');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -1,94 +0,0 @@
|
|||||||
import { QueryInterface, QueryTypes } from 'sequelize';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Migration to add AI model configuration entries
|
|
||||||
* Adds CLAUDE_MODEL, OPENAI_MODEL, and GEMINI_MODEL to admin_configurations
|
|
||||||
*
|
|
||||||
* This migration is idempotent - it will only insert if the configs don't exist.
|
|
||||||
* For existing databases, this ensures the new model configuration fields are available.
|
|
||||||
* For fresh databases, the seed scripts will handle the initial population.
|
|
||||||
*/
|
|
||||||
export async function up(queryInterface: QueryInterface): Promise<void> {
|
|
||||||
// Insert AI model configurations if they don't exist
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
INSERT INTO admin_configurations (
|
|
||||||
config_id, config_key, config_category, config_value, value_type,
|
|
||||||
display_name, description, default_value, is_editable, is_sensitive,
|
|
||||||
validation_rules, ui_component, options, sort_order, requires_restart,
|
|
||||||
last_modified_by, last_modified_at, created_at, updated_at
|
|
||||||
) VALUES
|
|
||||||
(
|
|
||||||
gen_random_uuid(),
|
|
||||||
'CLAUDE_MODEL',
|
|
||||||
'AI_CONFIGURATION',
|
|
||||||
'claude-sonnet-4-20250514',
|
|
||||||
'STRING',
|
|
||||||
'Claude Model',
|
|
||||||
'Claude (Anthropic) model to use for AI generation',
|
|
||||||
'claude-sonnet-4-20250514',
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
'{}'::jsonb,
|
|
||||||
'input',
|
|
||||||
NULL,
|
|
||||||
27,
|
|
||||||
false,
|
|
||||||
NULL,
|
|
||||||
NULL,
|
|
||||||
NOW(),
|
|
||||||
NOW()
|
|
||||||
),
|
|
||||||
(
|
|
||||||
gen_random_uuid(),
|
|
||||||
'OPENAI_MODEL',
|
|
||||||
'AI_CONFIGURATION',
|
|
||||||
'gpt-4o',
|
|
||||||
'STRING',
|
|
||||||
'OpenAI Model',
|
|
||||||
'OpenAI model to use for AI generation',
|
|
||||||
'gpt-4o',
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
'{}'::jsonb,
|
|
||||||
'input',
|
|
||||||
NULL,
|
|
||||||
28,
|
|
||||||
false,
|
|
||||||
NULL,
|
|
||||||
NULL,
|
|
||||||
NOW(),
|
|
||||||
NOW()
|
|
||||||
),
|
|
||||||
(
|
|
||||||
gen_random_uuid(),
|
|
||||||
'GEMINI_MODEL',
|
|
||||||
'AI_CONFIGURATION',
|
|
||||||
'gemini-2.0-flash-lite',
|
|
||||||
'STRING',
|
|
||||||
'Gemini Model',
|
|
||||||
'Gemini (Google) model to use for AI generation',
|
|
||||||
'gemini-2.0-flash-lite',
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
'{}'::jsonb,
|
|
||||||
'input',
|
|
||||||
NULL,
|
|
||||||
29,
|
|
||||||
false,
|
|
||||||
NULL,
|
|
||||||
NULL,
|
|
||||||
NOW(),
|
|
||||||
NOW()
|
|
||||||
)
|
|
||||||
ON CONFLICT (config_key) DO NOTHING
|
|
||||||
`, { type: QueryTypes.INSERT });
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function down(queryInterface: QueryInterface): Promise<void> {
|
|
||||||
// Remove the AI model configurations
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
DELETE FROM admin_configurations
|
|
||||||
WHERE config_key IN ('CLAUDE_MODEL', 'OPENAI_MODEL', 'GEMINI_MODEL')
|
|
||||||
`, { type: QueryTypes.DELETE });
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -1,53 +0,0 @@
|
|||||||
import { QueryInterface, DataTypes } from 'sequelize';
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
async up(queryInterface: QueryInterface): Promise<void> {
|
|
||||||
// Add notification preference columns to users table
|
|
||||||
await queryInterface.addColumn('users', 'email_notifications_enabled', {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: true,
|
|
||||||
comment: 'User preference for receiving email notifications'
|
|
||||||
});
|
|
||||||
|
|
||||||
await queryInterface.addColumn('users', 'push_notifications_enabled', {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: true,
|
|
||||||
comment: 'User preference for receiving push notifications'
|
|
||||||
});
|
|
||||||
|
|
||||||
await queryInterface.addColumn('users', 'in_app_notifications_enabled', {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: true,
|
|
||||||
comment: 'User preference for receiving in-app notifications'
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add indexes for faster queries
|
|
||||||
await queryInterface.addIndex('users', ['email_notifications_enabled'], {
|
|
||||||
name: 'idx_users_email_notifications_enabled'
|
|
||||||
});
|
|
||||||
|
|
||||||
await queryInterface.addIndex('users', ['push_notifications_enabled'], {
|
|
||||||
name: 'idx_users_push_notifications_enabled'
|
|
||||||
});
|
|
||||||
|
|
||||||
await queryInterface.addIndex('users', ['in_app_notifications_enabled'], {
|
|
||||||
name: 'idx_users_in_app_notifications_enabled'
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
async down(queryInterface: QueryInterface): Promise<void> {
|
|
||||||
// Remove indexes first
|
|
||||||
await queryInterface.removeIndex('users', 'idx_users_in_app_notifications_enabled');
|
|
||||||
await queryInterface.removeIndex('users', 'idx_users_push_notifications_enabled');
|
|
||||||
await queryInterface.removeIndex('users', 'idx_users_email_notifications_enabled');
|
|
||||||
|
|
||||||
// Remove columns
|
|
||||||
await queryInterface.removeColumn('users', 'in_app_notifications_enabled');
|
|
||||||
await queryInterface.removeColumn('users', 'push_notifications_enabled');
|
|
||||||
await queryInterface.removeColumn('users', 'email_notifications_enabled');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
@ -1,54 +0,0 @@
|
|||||||
import { QueryInterface } from 'sequelize';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add foreign key constraint for template_id after workflow_templates table exists
|
|
||||||
* This should run after both:
|
|
||||||
* - 20251210-enhance-workflow-templates (creates workflow_templates table)
|
|
||||||
* - 20251210-add-workflow-type-support (adds template_id column)
|
|
||||||
*/
|
|
||||||
export async function up(queryInterface: QueryInterface): Promise<void> {
|
|
||||||
// Check if workflow_templates table exists
|
|
||||||
const [tables] = await queryInterface.sequelize.query(`
|
|
||||||
SELECT table_name
|
|
||||||
FROM information_schema.tables
|
|
||||||
WHERE table_schema = 'public'
|
|
||||||
AND table_name = 'workflow_templates';
|
|
||||||
`);
|
|
||||||
|
|
||||||
if (tables.length > 0) {
|
|
||||||
// Check if foreign key already exists
|
|
||||||
const [constraints] = await queryInterface.sequelize.query(`
|
|
||||||
SELECT constraint_name
|
|
||||||
FROM information_schema.table_constraints
|
|
||||||
WHERE table_schema = 'public'
|
|
||||||
AND table_name = 'workflow_requests'
|
|
||||||
AND constraint_name = 'workflow_requests_template_id_fkey';
|
|
||||||
`);
|
|
||||||
|
|
||||||
if (constraints.length === 0) {
|
|
||||||
// Add foreign key constraint
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
ALTER TABLE workflow_requests
|
|
||||||
ADD CONSTRAINT workflow_requests_template_id_fkey
|
|
||||||
FOREIGN KEY (template_id)
|
|
||||||
REFERENCES workflow_templates(template_id)
|
|
||||||
ON UPDATE CASCADE
|
|
||||||
ON DELETE SET NULL;
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function down(queryInterface: QueryInterface): Promise<void> {
|
|
||||||
// Remove foreign key constraint if it exists
|
|
||||||
try {
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
ALTER TABLE workflow_requests
|
|
||||||
DROP CONSTRAINT IF EXISTS workflow_requests_template_id_fkey;
|
|
||||||
`);
|
|
||||||
} catch (error) {
|
|
||||||
// Ignore if constraint doesn't exist
|
|
||||||
console.log('Note: Foreign key constraint may not exist');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -1,116 +0,0 @@
|
|||||||
import { QueryInterface, DataTypes } from 'sequelize';
|
|
||||||
|
|
||||||
export async function up(queryInterface: QueryInterface): Promise<void> {
|
|
||||||
try {
|
|
||||||
// Check if columns already exist (for idempotency and backward compatibility)
|
|
||||||
const tableDescription = await queryInterface.describeTable('workflow_requests');
|
|
||||||
|
|
||||||
// 1. Add workflow_type column to workflow_requests (only if it doesn't exist)
|
|
||||||
if (!tableDescription.workflow_type) {
|
|
||||||
try {
|
|
||||||
await queryInterface.addColumn('workflow_requests', 'workflow_type', {
|
|
||||||
type: DataTypes.STRING(50),
|
|
||||||
allowNull: true,
|
|
||||||
defaultValue: 'NON_TEMPLATIZED'
|
|
||||||
});
|
|
||||||
console.log('✅ Added workflow_type column');
|
|
||||||
} catch (error: any) {
|
|
||||||
// Column might have been added manually, check if it exists now
|
|
||||||
const updatedDescription = await queryInterface.describeTable('workflow_requests');
|
|
||||||
if (!updatedDescription.workflow_type) {
|
|
||||||
throw error; // Re-throw if column still doesn't exist
|
|
||||||
}
|
|
||||||
console.log('Note: workflow_type column already exists (may have been added manually)');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log('Note: workflow_type column already exists, skipping');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Add template_id column (nullable, for admin templates)
|
|
||||||
// Note: Foreign key constraint will be added later if workflow_templates table exists
|
|
||||||
if (!tableDescription.template_id) {
|
|
||||||
try {
|
|
||||||
await queryInterface.addColumn('workflow_requests', 'template_id', {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: true
|
|
||||||
});
|
|
||||||
console.log('✅ Added template_id column');
|
|
||||||
} catch (error: any) {
|
|
||||||
// Column might have been added manually, check if it exists now
|
|
||||||
const updatedDescription = await queryInterface.describeTable('workflow_requests');
|
|
||||||
if (!updatedDescription.template_id) {
|
|
||||||
throw error; // Re-throw if column still doesn't exist
|
|
||||||
}
|
|
||||||
console.log('Note: template_id column already exists (may have been added manually)');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log('Note: template_id column already exists, skipping');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get updated table description for index creation
|
|
||||||
const finalTableDescription = await queryInterface.describeTable('workflow_requests');
|
|
||||||
|
|
||||||
// 3. Create index for workflow_type (only if column exists)
|
|
||||||
if (finalTableDescription.workflow_type) {
|
|
||||||
try {
|
|
||||||
await queryInterface.addIndex('workflow_requests', ['workflow_type'], {
|
|
||||||
name: 'idx_workflow_requests_workflow_type'
|
|
||||||
});
|
|
||||||
console.log('✅ Created workflow_type index');
|
|
||||||
} catch (error: any) {
|
|
||||||
// Index might already exist, ignore error
|
|
||||||
if (error.message?.includes('already exists') || error.message?.includes('duplicate')) {
|
|
||||||
console.log('Note: workflow_type index already exists');
|
|
||||||
} else {
|
|
||||||
console.log('Note: Could not create workflow_type index:', error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Create index for template_id (only if column exists)
|
|
||||||
if (finalTableDescription.template_id) {
|
|
||||||
try {
|
|
||||||
await queryInterface.addIndex('workflow_requests', ['template_id'], {
|
|
||||||
name: 'idx_workflow_requests_template_id'
|
|
||||||
});
|
|
||||||
console.log('✅ Created template_id index');
|
|
||||||
} catch (error: any) {
|
|
||||||
// Index might already exist, ignore error
|
|
||||||
if (error.message?.includes('already exists') || error.message?.includes('duplicate')) {
|
|
||||||
console.log('Note: template_id index already exists');
|
|
||||||
} else {
|
|
||||||
console.log('Note: Could not create template_id index:', error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. Update existing records to have workflow_type (if any exist and column exists)
|
|
||||||
if (finalTableDescription.workflow_type) {
|
|
||||||
try {
|
|
||||||
const [result] = await queryInterface.sequelize.query(`
|
|
||||||
UPDATE workflow_requests
|
|
||||||
SET workflow_type = 'NON_TEMPLATIZED'
|
|
||||||
WHERE workflow_type IS NULL;
|
|
||||||
`);
|
|
||||||
console.log('✅ Updated existing records with workflow_type');
|
|
||||||
} catch (error: any) {
|
|
||||||
// Ignore if table is empty or other error
|
|
||||||
console.log('Note: Could not update existing records:', error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error('Migration error:', error.message);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function down(queryInterface: QueryInterface): Promise<void> {
|
|
||||||
// Remove indexes
|
|
||||||
await queryInterface.removeIndex('workflow_requests', 'idx_workflow_requests_template_id');
|
|
||||||
await queryInterface.removeIndex('workflow_requests', 'idx_workflow_requests_workflow_type');
|
|
||||||
|
|
||||||
// Remove columns
|
|
||||||
await queryInterface.removeColumn('workflow_requests', 'template_id');
|
|
||||||
await queryInterface.removeColumn('workflow_requests', 'workflow_type');
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -1,214 +0,0 @@
|
|||||||
import { QueryInterface, DataTypes } from 'sequelize';
|
|
||||||
|
|
||||||
export async function up(queryInterface: QueryInterface): Promise<void> {
|
|
||||||
// 1. Create dealer_claim_details table
|
|
||||||
await queryInterface.createTable('dealer_claim_details', {
|
|
||||||
claim_id: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
primaryKey: true,
|
|
||||||
defaultValue: DataTypes.UUIDV4
|
|
||||||
},
|
|
||||||
request_id: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
unique: true,
|
|
||||||
references: {
|
|
||||||
model: 'workflow_requests',
|
|
||||||
key: 'request_id'
|
|
||||||
},
|
|
||||||
onDelete: 'CASCADE',
|
|
||||||
onUpdate: 'CASCADE'
|
|
||||||
},
|
|
||||||
activity_name: {
|
|
||||||
type: DataTypes.STRING(500),
|
|
||||||
allowNull: false
|
|
||||||
},
|
|
||||||
activity_type: {
|
|
||||||
type: DataTypes.STRING(100),
|
|
||||||
allowNull: false
|
|
||||||
},
|
|
||||||
dealer_code: {
|
|
||||||
type: DataTypes.STRING(50),
|
|
||||||
allowNull: false
|
|
||||||
},
|
|
||||||
dealer_name: {
|
|
||||||
type: DataTypes.STRING(200),
|
|
||||||
allowNull: false
|
|
||||||
},
|
|
||||||
dealer_email: {
|
|
||||||
type: DataTypes.STRING(255),
|
|
||||||
allowNull: true
|
|
||||||
},
|
|
||||||
dealer_phone: {
|
|
||||||
type: DataTypes.STRING(20),
|
|
||||||
allowNull: true
|
|
||||||
},
|
|
||||||
dealer_address: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
allowNull: true
|
|
||||||
},
|
|
||||||
activity_date: {
|
|
||||||
type: DataTypes.DATEONLY,
|
|
||||||
allowNull: true
|
|
||||||
},
|
|
||||||
location: {
|
|
||||||
type: DataTypes.STRING(255),
|
|
||||||
allowNull: true
|
|
||||||
},
|
|
||||||
period_start_date: {
|
|
||||||
type: DataTypes.DATEONLY,
|
|
||||||
allowNull: true
|
|
||||||
},
|
|
||||||
period_end_date: {
|
|
||||||
type: DataTypes.DATEONLY,
|
|
||||||
allowNull: true
|
|
||||||
},
|
|
||||||
created_at: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW
|
|
||||||
},
|
|
||||||
updated_at: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create indexes
|
|
||||||
await queryInterface.addIndex('dealer_claim_details', ['request_id'], {
|
|
||||||
name: 'idx_dealer_claim_details_request_id',
|
|
||||||
unique: true
|
|
||||||
});
|
|
||||||
await queryInterface.addIndex('dealer_claim_details', ['dealer_code'], {
|
|
||||||
name: 'idx_dealer_claim_details_dealer_code'
|
|
||||||
});
|
|
||||||
await queryInterface.addIndex('dealer_claim_details', ['activity_type'], {
|
|
||||||
name: 'idx_dealer_claim_details_activity_type'
|
|
||||||
});
|
|
||||||
|
|
||||||
// 2. Create dealer_proposal_details table (Step 1: Dealer Proposal)
|
|
||||||
await queryInterface.createTable('dealer_proposal_details', {
|
|
||||||
proposal_id: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
primaryKey: true,
|
|
||||||
defaultValue: DataTypes.UUIDV4
|
|
||||||
},
|
|
||||||
request_id: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
unique: true,
|
|
||||||
references: {
|
|
||||||
model: 'workflow_requests',
|
|
||||||
key: 'request_id'
|
|
||||||
},
|
|
||||||
onDelete: 'CASCADE',
|
|
||||||
onUpdate: 'CASCADE'
|
|
||||||
},
|
|
||||||
proposal_document_path: {
|
|
||||||
type: DataTypes.STRING(500),
|
|
||||||
allowNull: true
|
|
||||||
},
|
|
||||||
proposal_document_url: {
|
|
||||||
type: DataTypes.STRING(500),
|
|
||||||
allowNull: true
|
|
||||||
},
|
|
||||||
total_estimated_budget: {
|
|
||||||
type: DataTypes.DECIMAL(15, 2),
|
|
||||||
allowNull: true
|
|
||||||
},
|
|
||||||
timeline_mode: {
|
|
||||||
type: DataTypes.STRING(10),
|
|
||||||
allowNull: true
|
|
||||||
},
|
|
||||||
expected_completion_date: {
|
|
||||||
type: DataTypes.DATEONLY,
|
|
||||||
allowNull: true
|
|
||||||
},
|
|
||||||
expected_completion_days: {
|
|
||||||
type: DataTypes.INTEGER,
|
|
||||||
allowNull: true
|
|
||||||
},
|
|
||||||
dealer_comments: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
allowNull: true
|
|
||||||
},
|
|
||||||
submitted_at: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: true
|
|
||||||
},
|
|
||||||
created_at: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW
|
|
||||||
},
|
|
||||||
updated_at: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await queryInterface.addIndex('dealer_proposal_details', ['request_id'], {
|
|
||||||
name: 'idx_dealer_proposal_details_request_id',
|
|
||||||
unique: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// 3. Create dealer_completion_details table (Step 5: Dealer Completion)
|
|
||||||
await queryInterface.createTable('dealer_completion_details', {
|
|
||||||
completion_id: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
primaryKey: true,
|
|
||||||
defaultValue: DataTypes.UUIDV4
|
|
||||||
},
|
|
||||||
request_id: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
unique: true,
|
|
||||||
references: {
|
|
||||||
model: 'workflow_requests',
|
|
||||||
key: 'request_id'
|
|
||||||
},
|
|
||||||
onDelete: 'CASCADE',
|
|
||||||
onUpdate: 'CASCADE'
|
|
||||||
},
|
|
||||||
activity_completion_date: {
|
|
||||||
type: DataTypes.DATEONLY,
|
|
||||||
allowNull: false
|
|
||||||
},
|
|
||||||
number_of_participants: {
|
|
||||||
type: DataTypes.INTEGER,
|
|
||||||
allowNull: true
|
|
||||||
},
|
|
||||||
total_closed_expenses: {
|
|
||||||
type: DataTypes.DECIMAL(15, 2),
|
|
||||||
allowNull: true
|
|
||||||
},
|
|
||||||
submitted_at: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: true
|
|
||||||
},
|
|
||||||
created_at: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW
|
|
||||||
},
|
|
||||||
updated_at: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await queryInterface.addIndex('dealer_completion_details', ['request_id'], {
|
|
||||||
name: 'idx_dealer_completion_details_request_id',
|
|
||||||
unique: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function down(queryInterface: QueryInterface): Promise<void> {
|
|
||||||
await queryInterface.dropTable('dealer_completion_details');
|
|
||||||
await queryInterface.dropTable('dealer_proposal_details');
|
|
||||||
await queryInterface.dropTable('dealer_claim_details');
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -1,194 +0,0 @@
|
|||||||
import { QueryInterface, DataTypes } from 'sequelize';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Migration: Create dealer_proposal_cost_items table
|
|
||||||
*
|
|
||||||
* Purpose: Separate table for cost breakups to enable better querying, reporting, and data integrity
|
|
||||||
* This replaces the JSONB costBreakup field in dealer_proposal_details
|
|
||||||
*
|
|
||||||
* Benefits:
|
|
||||||
* - Better querying and filtering
|
|
||||||
* - Easier to update individual cost items
|
|
||||||
* - Better for analytics and reporting
|
|
||||||
* - Maintains referential integrity
|
|
||||||
*/
|
|
||||||
export async function up(queryInterface: QueryInterface): Promise<void> {
|
|
||||||
// Check if table already exists
|
|
||||||
const [tables] = await queryInterface.sequelize.query(`
|
|
||||||
SELECT table_name
|
|
||||||
FROM information_schema.tables
|
|
||||||
WHERE table_schema = 'public'
|
|
||||||
AND table_name = 'dealer_proposal_cost_items';
|
|
||||||
`);
|
|
||||||
|
|
||||||
if (tables.length === 0) {
|
|
||||||
// Create dealer_proposal_cost_items table
|
|
||||||
await queryInterface.createTable('dealer_proposal_cost_items', {
|
|
||||||
cost_item_id: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
primaryKey: true,
|
|
||||||
defaultValue: DataTypes.UUIDV4,
|
|
||||||
field: 'cost_item_id'
|
|
||||||
},
|
|
||||||
proposal_id: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
field: 'proposal_id',
|
|
||||||
references: {
|
|
||||||
model: 'dealer_proposal_details',
|
|
||||||
key: 'proposal_id'
|
|
||||||
},
|
|
||||||
onDelete: 'CASCADE',
|
|
||||||
onUpdate: 'CASCADE'
|
|
||||||
},
|
|
||||||
request_id: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
field: 'request_id',
|
|
||||||
references: {
|
|
||||||
model: 'workflow_requests',
|
|
||||||
key: 'request_id'
|
|
||||||
},
|
|
||||||
onDelete: 'CASCADE',
|
|
||||||
onUpdate: 'CASCADE',
|
|
||||||
comment: 'Denormalized for easier querying without joins'
|
|
||||||
},
|
|
||||||
item_description: {
|
|
||||||
type: DataTypes.STRING(500),
|
|
||||||
allowNull: false,
|
|
||||||
field: 'item_description'
|
|
||||||
},
|
|
||||||
amount: {
|
|
||||||
type: DataTypes.DECIMAL(15, 2),
|
|
||||||
allowNull: false,
|
|
||||||
field: 'amount',
|
|
||||||
comment: 'Cost amount in INR'
|
|
||||||
},
|
|
||||||
item_order: {
|
|
||||||
type: DataTypes.INTEGER,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: 0,
|
|
||||||
field: 'item_order',
|
|
||||||
comment: 'Order of item in the cost breakdown list'
|
|
||||||
},
|
|
||||||
created_at: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW,
|
|
||||||
field: 'created_at'
|
|
||||||
},
|
|
||||||
updated_at: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW,
|
|
||||||
field: 'updated_at'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create indexes for better query performance
|
|
||||||
await queryInterface.addIndex('dealer_proposal_cost_items', ['proposal_id'], {
|
|
||||||
name: 'idx_proposal_cost_items_proposal_id'
|
|
||||||
});
|
|
||||||
|
|
||||||
await queryInterface.addIndex('dealer_proposal_cost_items', ['request_id'], {
|
|
||||||
name: 'idx_proposal_cost_items_request_id'
|
|
||||||
});
|
|
||||||
|
|
||||||
await queryInterface.addIndex('dealer_proposal_cost_items', ['proposal_id', 'item_order'], {
|
|
||||||
name: 'idx_proposal_cost_items_proposal_order'
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('✅ Created dealer_proposal_cost_items table');
|
|
||||||
} else {
|
|
||||||
console.log('Note: dealer_proposal_cost_items table already exists');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Migrate existing JSONB costBreakup data to the new table
|
|
||||||
try {
|
|
||||||
const [existingProposals] = await queryInterface.sequelize.query(`
|
|
||||||
SELECT proposal_id, request_id, cost_breakup
|
|
||||||
FROM dealer_proposal_details
|
|
||||||
WHERE cost_breakup IS NOT NULL
|
|
||||||
AND cost_breakup::text != 'null'
|
|
||||||
AND cost_breakup::text != '[]';
|
|
||||||
`);
|
|
||||||
|
|
||||||
if (Array.isArray(existingProposals) && existingProposals.length > 0) {
|
|
||||||
console.log(`📦 Migrating ${existingProposals.length} existing proposal(s) with cost breakups...`);
|
|
||||||
|
|
||||||
for (const proposal of existingProposals as any[]) {
|
|
||||||
const proposalId = proposal.proposal_id;
|
|
||||||
const requestId = proposal.request_id;
|
|
||||||
let costBreakup = proposal.cost_breakup;
|
|
||||||
|
|
||||||
// Parse JSONB if it's a string
|
|
||||||
if (typeof costBreakup === 'string') {
|
|
||||||
try {
|
|
||||||
costBreakup = JSON.parse(costBreakup);
|
|
||||||
} catch (e) {
|
|
||||||
console.warn(`⚠️ Failed to parse costBreakup for proposal ${proposalId}:`, e);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure it's an array
|
|
||||||
if (!Array.isArray(costBreakup)) {
|
|
||||||
console.warn(`⚠️ costBreakup is not an array for proposal ${proposalId}`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insert cost items
|
|
||||||
for (let i = 0; i < costBreakup.length; i++) {
|
|
||||||
const item = costBreakup[i];
|
|
||||||
if (item && item.description && item.amount !== undefined) {
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
INSERT INTO dealer_proposal_cost_items
|
|
||||||
(proposal_id, request_id, item_description, amount, item_order, created_at, updated_at)
|
|
||||||
VALUES (:proposalId, :requestId, :description, :amount, :order, NOW(), NOW())
|
|
||||||
ON CONFLICT DO NOTHING;
|
|
||||||
`, {
|
|
||||||
replacements: {
|
|
||||||
proposalId,
|
|
||||||
requestId,
|
|
||||||
description: item.description,
|
|
||||||
amount: item.amount,
|
|
||||||
order: i
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('✅ Migrated existing cost breakups to new table');
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
console.warn('⚠️ Could not migrate existing cost breakups:', error.message);
|
|
||||||
// Don't fail the migration if migration of existing data fails
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function down(queryInterface: QueryInterface): Promise<void> {
|
|
||||||
// Drop indexes first
|
|
||||||
try {
|
|
||||||
await queryInterface.removeIndex('dealer_proposal_cost_items', 'idx_proposal_cost_items_proposal_order');
|
|
||||||
} catch (e) {
|
|
||||||
// Index might not exist
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await queryInterface.removeIndex('dealer_proposal_cost_items', 'idx_proposal_cost_items_request_id');
|
|
||||||
} catch (e) {
|
|
||||||
// Index might not exist
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await queryInterface.removeIndex('dealer_proposal_cost_items', 'idx_proposal_cost_items_proposal_id');
|
|
||||||
} catch (e) {
|
|
||||||
// Index might not exist
|
|
||||||
}
|
|
||||||
|
|
||||||
// Drop table
|
|
||||||
await queryInterface.dropTable('dealer_proposal_cost_items');
|
|
||||||
console.log('✅ Dropped dealer_proposal_cost_items table');
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -1,174 +0,0 @@
|
|||||||
import { QueryInterface, DataTypes } from 'sequelize';
|
|
||||||
|
|
||||||
export async function up(queryInterface: QueryInterface): Promise<void> {
|
|
||||||
// Check if workflow_templates table exists, if not create it
|
|
||||||
const [tables] = await queryInterface.sequelize.query(`
|
|
||||||
SELECT table_name
|
|
||||||
FROM information_schema.tables
|
|
||||||
WHERE table_schema = 'public'
|
|
||||||
AND table_name = 'workflow_templates';
|
|
||||||
`);
|
|
||||||
|
|
||||||
if (tables.length === 0) {
|
|
||||||
// Create workflow_templates table if it doesn't exist
|
|
||||||
await queryInterface.createTable('workflow_templates', {
|
|
||||||
template_id: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
primaryKey: true,
|
|
||||||
defaultValue: DataTypes.UUIDV4
|
|
||||||
},
|
|
||||||
template_name: {
|
|
||||||
type: DataTypes.STRING(200),
|
|
||||||
allowNull: false
|
|
||||||
},
|
|
||||||
template_code: {
|
|
||||||
type: DataTypes.STRING(50),
|
|
||||||
allowNull: true,
|
|
||||||
unique: true
|
|
||||||
},
|
|
||||||
template_description: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
allowNull: true
|
|
||||||
},
|
|
||||||
template_category: {
|
|
||||||
type: DataTypes.STRING(100),
|
|
||||||
allowNull: true
|
|
||||||
},
|
|
||||||
workflow_type: {
|
|
||||||
type: DataTypes.STRING(50),
|
|
||||||
allowNull: true
|
|
||||||
},
|
|
||||||
approval_levels_config: {
|
|
||||||
type: DataTypes.JSONB,
|
|
||||||
allowNull: true
|
|
||||||
},
|
|
||||||
default_tat_hours: {
|
|
||||||
type: DataTypes.DECIMAL(10, 2),
|
|
||||||
allowNull: true,
|
|
||||||
defaultValue: 24
|
|
||||||
},
|
|
||||||
form_steps_config: {
|
|
||||||
type: DataTypes.JSONB,
|
|
||||||
allowNull: true
|
|
||||||
},
|
|
||||||
user_field_mappings: {
|
|
||||||
type: DataTypes.JSONB,
|
|
||||||
allowNull: true
|
|
||||||
},
|
|
||||||
dynamic_approver_config: {
|
|
||||||
type: DataTypes.JSONB,
|
|
||||||
allowNull: true
|
|
||||||
},
|
|
||||||
is_active: {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: true
|
|
||||||
},
|
|
||||||
is_system_template: {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: false
|
|
||||||
},
|
|
||||||
usage_count: {
|
|
||||||
type: DataTypes.INTEGER,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: 0
|
|
||||||
},
|
|
||||||
created_by: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: true,
|
|
||||||
references: {
|
|
||||||
model: 'users',
|
|
||||||
key: 'user_id'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created_at: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW
|
|
||||||
},
|
|
||||||
updated_at: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create indexes
|
|
||||||
await queryInterface.addIndex('workflow_templates', ['template_code'], {
|
|
||||||
name: 'idx_workflow_templates_template_code',
|
|
||||||
unique: true
|
|
||||||
});
|
|
||||||
await queryInterface.addIndex('workflow_templates', ['workflow_type'], {
|
|
||||||
name: 'idx_workflow_templates_workflow_type'
|
|
||||||
});
|
|
||||||
await queryInterface.addIndex('workflow_templates', ['is_active'], {
|
|
||||||
name: 'idx_workflow_templates_is_active'
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Table exists, add new columns if they don't exist
|
|
||||||
const tableDescription = await queryInterface.describeTable('workflow_templates');
|
|
||||||
|
|
||||||
if (!tableDescription.form_steps_config) {
|
|
||||||
await queryInterface.addColumn('workflow_templates', 'form_steps_config', {
|
|
||||||
type: DataTypes.JSONB,
|
|
||||||
allowNull: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!tableDescription.user_field_mappings) {
|
|
||||||
await queryInterface.addColumn('workflow_templates', 'user_field_mappings', {
|
|
||||||
type: DataTypes.JSONB,
|
|
||||||
allowNull: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!tableDescription.dynamic_approver_config) {
|
|
||||||
await queryInterface.addColumn('workflow_templates', 'dynamic_approver_config', {
|
|
||||||
type: DataTypes.JSONB,
|
|
||||||
allowNull: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!tableDescription.workflow_type) {
|
|
||||||
await queryInterface.addColumn('workflow_templates', 'workflow_type', {
|
|
||||||
type: DataTypes.STRING(50),
|
|
||||||
allowNull: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!tableDescription.is_system_template) {
|
|
||||||
await queryInterface.addColumn('workflow_templates', 'is_system_template', {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: false
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function down(queryInterface: QueryInterface): Promise<void> {
|
|
||||||
// Remove columns if they exist
|
|
||||||
const tableDescription = await queryInterface.describeTable('workflow_templates');
|
|
||||||
|
|
||||||
if (tableDescription.dynamic_approver_config) {
|
|
||||||
await queryInterface.removeColumn('workflow_templates', 'dynamic_approver_config');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tableDescription.user_field_mappings) {
|
|
||||||
await queryInterface.removeColumn('workflow_templates', 'user_field_mappings');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tableDescription.form_steps_config) {
|
|
||||||
await queryInterface.removeColumn('workflow_templates', 'form_steps_config');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tableDescription.workflow_type) {
|
|
||||||
await queryInterface.removeColumn('workflow_templates', 'workflow_type');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tableDescription.is_system_template) {
|
|
||||||
await queryInterface.removeColumn('workflow_templates', 'is_system_template');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -1,197 +0,0 @@
|
|||||||
import { QueryInterface, DataTypes } from 'sequelize';
|
|
||||||
|
|
||||||
export async function up(queryInterface: QueryInterface): Promise<void> {
|
|
||||||
// Create claim_budget_tracking table for comprehensive budget management
|
|
||||||
await queryInterface.createTable('claim_budget_tracking', {
|
|
||||||
budget_id: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
primaryKey: true,
|
|
||||||
defaultValue: DataTypes.UUIDV4
|
|
||||||
},
|
|
||||||
request_id: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
unique: true,
|
|
||||||
references: {
|
|
||||||
model: 'workflow_requests',
|
|
||||||
key: 'request_id'
|
|
||||||
},
|
|
||||||
onDelete: 'CASCADE',
|
|
||||||
onUpdate: 'CASCADE'
|
|
||||||
},
|
|
||||||
// Initial Budget (from claim creation)
|
|
||||||
initial_estimated_budget: {
|
|
||||||
type: DataTypes.DECIMAL(15, 2),
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'Initial estimated budget when claim was created'
|
|
||||||
},
|
|
||||||
// Proposal Budget (from Step 1 - Dealer Proposal)
|
|
||||||
proposal_estimated_budget: {
|
|
||||||
type: DataTypes.DECIMAL(15, 2),
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'Total estimated budget from dealer proposal'
|
|
||||||
},
|
|
||||||
proposal_submitted_at: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'When dealer submitted proposal'
|
|
||||||
},
|
|
||||||
// Approved Budget (from Step 2 - Requestor Evaluation)
|
|
||||||
approved_budget: {
|
|
||||||
type: DataTypes.DECIMAL(15, 2),
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'Budget approved by requestor in Step 2'
|
|
||||||
},
|
|
||||||
approved_at: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'When budget was approved by requestor'
|
|
||||||
},
|
|
||||||
approved_by: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: true,
|
|
||||||
references: {
|
|
||||||
model: 'users',
|
|
||||||
key: 'user_id'
|
|
||||||
},
|
|
||||||
onDelete: 'SET NULL',
|
|
||||||
onUpdate: 'CASCADE',
|
|
||||||
comment: 'User who approved the budget'
|
|
||||||
},
|
|
||||||
// IO Blocked Budget (from Step 3 - Department Lead)
|
|
||||||
io_blocked_amount: {
|
|
||||||
type: DataTypes.DECIMAL(15, 2),
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'Amount blocked in IO (from internal_orders table)'
|
|
||||||
},
|
|
||||||
io_blocked_at: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'When budget was blocked in IO'
|
|
||||||
},
|
|
||||||
// Closed Expenses (from Step 5 - Dealer Completion)
|
|
||||||
closed_expenses: {
|
|
||||||
type: DataTypes.DECIMAL(15, 2),
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'Total closed expenses from completion documents'
|
|
||||||
},
|
|
||||||
closed_expenses_submitted_at: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'When completion expenses were submitted'
|
|
||||||
},
|
|
||||||
// Final Claim Amount (from Step 6 - Requestor Claim Approval)
|
|
||||||
final_claim_amount: {
|
|
||||||
type: DataTypes.DECIMAL(15, 2),
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'Final claim amount approved/modified by requestor in Step 6'
|
|
||||||
},
|
|
||||||
final_claim_amount_approved_at: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'When final claim amount was approved'
|
|
||||||
},
|
|
||||||
final_claim_amount_approved_by: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: true,
|
|
||||||
references: {
|
|
||||||
model: 'users',
|
|
||||||
key: 'user_id'
|
|
||||||
},
|
|
||||||
onDelete: 'SET NULL',
|
|
||||||
onUpdate: 'CASCADE',
|
|
||||||
comment: 'User who approved final claim amount'
|
|
||||||
},
|
|
||||||
// Credit Note (from Step 8 - Finance)
|
|
||||||
credit_note_amount: {
|
|
||||||
type: DataTypes.DECIMAL(15, 2),
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'Credit note amount issued by finance'
|
|
||||||
},
|
|
||||||
credit_note_issued_at: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'When credit note was issued'
|
|
||||||
},
|
|
||||||
// Budget Status
|
|
||||||
budget_status: {
|
|
||||||
type: DataTypes.ENUM('DRAFT', 'PROPOSED', 'APPROVED', 'BLOCKED', 'CLOSED', 'SETTLED'),
|
|
||||||
defaultValue: 'DRAFT',
|
|
||||||
allowNull: false,
|
|
||||||
comment: 'Current status of budget lifecycle'
|
|
||||||
},
|
|
||||||
// Currency
|
|
||||||
currency: {
|
|
||||||
type: DataTypes.STRING(3),
|
|
||||||
defaultValue: 'INR',
|
|
||||||
allowNull: false,
|
|
||||||
comment: 'Currency code (INR, USD, etc.)'
|
|
||||||
},
|
|
||||||
// Budget Variance
|
|
||||||
variance_amount: {
|
|
||||||
type: DataTypes.DECIMAL(15, 2),
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'Difference between approved and closed expenses (closed - approved)'
|
|
||||||
},
|
|
||||||
variance_percentage: {
|
|
||||||
type: DataTypes.DECIMAL(5, 2),
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'Variance as percentage of approved budget'
|
|
||||||
},
|
|
||||||
// Audit fields
|
|
||||||
last_modified_by: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: true,
|
|
||||||
references: {
|
|
||||||
model: 'users',
|
|
||||||
key: 'user_id'
|
|
||||||
},
|
|
||||||
onDelete: 'SET NULL',
|
|
||||||
onUpdate: 'CASCADE',
|
|
||||||
comment: 'Last user who modified budget'
|
|
||||||
},
|
|
||||||
last_modified_at: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'When budget was last modified'
|
|
||||||
},
|
|
||||||
modification_reason: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'Reason for budget modification'
|
|
||||||
},
|
|
||||||
created_at: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW
|
|
||||||
},
|
|
||||||
updated_at: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create indexes
|
|
||||||
await queryInterface.addIndex('claim_budget_tracking', ['request_id'], {
|
|
||||||
name: 'idx_claim_budget_tracking_request_id',
|
|
||||||
unique: true
|
|
||||||
});
|
|
||||||
|
|
||||||
await queryInterface.addIndex('claim_budget_tracking', ['budget_status'], {
|
|
||||||
name: 'idx_claim_budget_tracking_status'
|
|
||||||
});
|
|
||||||
|
|
||||||
await queryInterface.addIndex('claim_budget_tracking', ['approved_by'], {
|
|
||||||
name: 'idx_claim_budget_tracking_approved_by'
|
|
||||||
});
|
|
||||||
|
|
||||||
await queryInterface.addIndex('claim_budget_tracking', ['final_claim_amount_approved_by'], {
|
|
||||||
name: 'idx_claim_budget_tracking_final_approved_by'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function down(queryInterface: QueryInterface): Promise<void> {
|
|
||||||
await queryInterface.dropTable('claim_budget_tracking');
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -1,95 +0,0 @@
|
|||||||
import { QueryInterface, DataTypes } from 'sequelize';
|
|
||||||
|
|
||||||
export async function up(queryInterface: QueryInterface): Promise<void> {
|
|
||||||
// Create internal_orders table for storing IO (Internal Order) details
|
|
||||||
await queryInterface.createTable('internal_orders', {
|
|
||||||
io_id: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
primaryKey: true,
|
|
||||||
defaultValue: DataTypes.UUIDV4
|
|
||||||
},
|
|
||||||
request_id: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
references: {
|
|
||||||
model: 'workflow_requests',
|
|
||||||
key: 'request_id'
|
|
||||||
},
|
|
||||||
onDelete: 'CASCADE',
|
|
||||||
onUpdate: 'CASCADE'
|
|
||||||
},
|
|
||||||
io_number: {
|
|
||||||
type: DataTypes.STRING(50),
|
|
||||||
allowNull: false
|
|
||||||
},
|
|
||||||
io_remark: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
allowNull: true
|
|
||||||
},
|
|
||||||
io_available_balance: {
|
|
||||||
type: DataTypes.DECIMAL(15, 2),
|
|
||||||
allowNull: true
|
|
||||||
},
|
|
||||||
io_blocked_amount: {
|
|
||||||
type: DataTypes.DECIMAL(15, 2),
|
|
||||||
allowNull: true
|
|
||||||
},
|
|
||||||
io_remaining_balance: {
|
|
||||||
type: DataTypes.DECIMAL(15, 2),
|
|
||||||
allowNull: true
|
|
||||||
},
|
|
||||||
organized_by: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: true,
|
|
||||||
references: {
|
|
||||||
model: 'users',
|
|
||||||
key: 'user_id'
|
|
||||||
},
|
|
||||||
onDelete: 'SET NULL',
|
|
||||||
onUpdate: 'CASCADE'
|
|
||||||
},
|
|
||||||
organized_at: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: true
|
|
||||||
},
|
|
||||||
sap_document_number: {
|
|
||||||
type: DataTypes.STRING(100),
|
|
||||||
allowNull: true
|
|
||||||
},
|
|
||||||
status: {
|
|
||||||
type: DataTypes.ENUM('PENDING', 'BLOCKED', 'RELEASED', 'CANCELLED'),
|
|
||||||
defaultValue: 'PENDING',
|
|
||||||
allowNull: false
|
|
||||||
},
|
|
||||||
created_at: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW
|
|
||||||
},
|
|
||||||
updated_at: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create indexes
|
|
||||||
await queryInterface.addIndex('internal_orders', ['io_number'], {
|
|
||||||
name: 'idx_internal_orders_io_number'
|
|
||||||
});
|
|
||||||
|
|
||||||
await queryInterface.addIndex('internal_orders', ['organized_by'], {
|
|
||||||
name: 'idx_internal_orders_organized_by'
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create unique constraint: one IO per request (unique index on request_id)
|
|
||||||
await queryInterface.addIndex('internal_orders', ['request_id'], {
|
|
||||||
name: 'idx_internal_orders_request_id_unique',
|
|
||||||
unique: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function down(queryInterface: QueryInterface): Promise<void> {
|
|
||||||
await queryInterface.dropTable('internal_orders');
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -1,162 +0,0 @@
|
|||||||
import { QueryInterface, DataTypes } from 'sequelize';
|
|
||||||
|
|
||||||
export async function up(queryInterface: QueryInterface): Promise<void> {
|
|
||||||
await queryInterface.createTable('claim_invoices', {
|
|
||||||
invoice_id: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
primaryKey: true,
|
|
||||||
defaultValue: DataTypes.UUIDV4,
|
|
||||||
},
|
|
||||||
request_id: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
unique: true, // one invoice per request (adjust later if multiples needed)
|
|
||||||
references: { model: 'workflow_requests', key: 'request_id' },
|
|
||||||
onDelete: 'CASCADE',
|
|
||||||
onUpdate: 'CASCADE',
|
|
||||||
},
|
|
||||||
invoice_number: {
|
|
||||||
type: DataTypes.STRING(100),
|
|
||||||
allowNull: true,
|
|
||||||
},
|
|
||||||
invoice_date: {
|
|
||||||
type: DataTypes.DATEONLY,
|
|
||||||
allowNull: true,
|
|
||||||
},
|
|
||||||
invoice_amount: {
|
|
||||||
type: DataTypes.DECIMAL(15, 2),
|
|
||||||
allowNull: true,
|
|
||||||
},
|
|
||||||
dms_number: {
|
|
||||||
type: DataTypes.STRING(100),
|
|
||||||
allowNull: true,
|
|
||||||
},
|
|
||||||
invoice_file_path: {
|
|
||||||
type: DataTypes.STRING(500),
|
|
||||||
allowNull: true,
|
|
||||||
},
|
|
||||||
generation_status: {
|
|
||||||
type: DataTypes.STRING(50), // e.g., PENDING, GENERATED, SENT, FAILED, CANCELLED
|
|
||||||
allowNull: true,
|
|
||||||
},
|
|
||||||
error_message: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
allowNull: true,
|
|
||||||
},
|
|
||||||
generated_at: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: true,
|
|
||||||
},
|
|
||||||
description: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
allowNull: true,
|
|
||||||
},
|
|
||||||
created_at: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW,
|
|
||||||
},
|
|
||||||
updated_at: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await queryInterface.addIndex('claim_invoices', ['request_id'], { name: 'idx_claim_invoices_request_id', unique: true });
|
|
||||||
await queryInterface.addIndex('claim_invoices', ['invoice_number'], { name: 'idx_claim_invoices_invoice_number' });
|
|
||||||
await queryInterface.addIndex('claim_invoices', ['dms_number'], { name: 'idx_claim_invoices_dms_number' });
|
|
||||||
await queryInterface.addIndex('claim_invoices', ['generation_status'], { name: 'idx_claim_invoices_status' });
|
|
||||||
|
|
||||||
await queryInterface.createTable('claim_credit_notes', {
|
|
||||||
credit_note_id: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
primaryKey: true,
|
|
||||||
defaultValue: DataTypes.UUIDV4,
|
|
||||||
},
|
|
||||||
request_id: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
unique: true, // one credit note per request (adjust later if multiples needed)
|
|
||||||
references: { model: 'workflow_requests', key: 'request_id' },
|
|
||||||
onDelete: 'CASCADE',
|
|
||||||
onUpdate: 'CASCADE',
|
|
||||||
},
|
|
||||||
invoice_id: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: true,
|
|
||||||
references: { model: 'claim_invoices', key: 'invoice_id' },
|
|
||||||
onDelete: 'SET NULL',
|
|
||||||
onUpdate: 'CASCADE',
|
|
||||||
},
|
|
||||||
credit_note_number: {
|
|
||||||
type: DataTypes.STRING(100),
|
|
||||||
allowNull: true,
|
|
||||||
},
|
|
||||||
credit_note_date: {
|
|
||||||
type: DataTypes.DATEONLY,
|
|
||||||
allowNull: true,
|
|
||||||
},
|
|
||||||
credit_amount: {
|
|
||||||
type: DataTypes.DECIMAL(15, 2),
|
|
||||||
allowNull: true,
|
|
||||||
},
|
|
||||||
sap_document_number: {
|
|
||||||
type: DataTypes.STRING(100),
|
|
||||||
allowNull: true,
|
|
||||||
},
|
|
||||||
credit_note_file_path: {
|
|
||||||
type: DataTypes.STRING(500),
|
|
||||||
allowNull: true,
|
|
||||||
},
|
|
||||||
confirmation_status: {
|
|
||||||
type: DataTypes.STRING(50), // e.g., PENDING, GENERATED, CONFIRMED, FAILED, CANCELLED
|
|
||||||
allowNull: true,
|
|
||||||
},
|
|
||||||
error_message: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
allowNull: true,
|
|
||||||
},
|
|
||||||
confirmed_by: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: true,
|
|
||||||
references: { model: 'users', key: 'user_id' },
|
|
||||||
onDelete: 'SET NULL',
|
|
||||||
onUpdate: 'CASCADE',
|
|
||||||
},
|
|
||||||
confirmed_at: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: true,
|
|
||||||
},
|
|
||||||
reason: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
allowNull: true,
|
|
||||||
},
|
|
||||||
description: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
allowNull: true,
|
|
||||||
},
|
|
||||||
created_at: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW,
|
|
||||||
},
|
|
||||||
updated_at: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await queryInterface.addIndex('claim_credit_notes', ['request_id'], { name: 'idx_claim_credit_notes_request_id', unique: true });
|
|
||||||
await queryInterface.addIndex('claim_credit_notes', ['invoice_id'], { name: 'idx_claim_credit_notes_invoice_id' });
|
|
||||||
await queryInterface.addIndex('claim_credit_notes', ['credit_note_number'], { name: 'idx_claim_credit_notes_number' });
|
|
||||||
await queryInterface.addIndex('claim_credit_notes', ['sap_document_number'], { name: 'idx_claim_credit_notes_sap_doc' });
|
|
||||||
await queryInterface.addIndex('claim_credit_notes', ['confirmation_status'], { name: 'idx_claim_credit_notes_status' });
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function down(queryInterface: QueryInterface): Promise<void> {
|
|
||||||
await queryInterface.dropTable('claim_credit_notes');
|
|
||||||
await queryInterface.dropTable('claim_invoices');
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -1,68 +0,0 @@
|
|||||||
import { QueryInterface, DataTypes } from 'sequelize';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper function to check if a column exists in a table
|
|
||||||
*/
|
|
||||||
async function columnExists(
|
|
||||||
queryInterface: QueryInterface,
|
|
||||||
tableName: string,
|
|
||||||
columnName: string
|
|
||||||
): Promise<boolean> {
|
|
||||||
try {
|
|
||||||
const tableDescription = await queryInterface.describeTable(tableName);
|
|
||||||
return columnName in tableDescription;
|
|
||||||
} catch (error) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function up(queryInterface: QueryInterface): Promise<void> {
|
|
||||||
const columnsToRemove = [
|
|
||||||
'dms_number',
|
|
||||||
'e_invoice_number',
|
|
||||||
'e_invoice_date',
|
|
||||||
'credit_note_number',
|
|
||||||
'credit_note_date',
|
|
||||||
'credit_note_amount',
|
|
||||||
];
|
|
||||||
|
|
||||||
// Only remove columns if they exist
|
|
||||||
// This handles the case where dealer_claim_details was created without these columns
|
|
||||||
for (const columnName of columnsToRemove) {
|
|
||||||
const exists = await columnExists(queryInterface, 'dealer_claim_details', columnName);
|
|
||||||
if (exists) {
|
|
||||||
await queryInterface.removeColumn('dealer_claim_details', columnName);
|
|
||||||
console.log(` ✅ Removed column: ${columnName}`);
|
|
||||||
} else {
|
|
||||||
console.log(` ⏭️ Column ${columnName} does not exist, skipping...`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function down(queryInterface: QueryInterface): Promise<void> {
|
|
||||||
await queryInterface.addColumn('dealer_claim_details', 'dms_number', {
|
|
||||||
type: DataTypes.STRING(100),
|
|
||||||
allowNull: true,
|
|
||||||
});
|
|
||||||
await queryInterface.addColumn('dealer_claim_details', 'e_invoice_number', {
|
|
||||||
type: DataTypes.STRING(100),
|
|
||||||
allowNull: true,
|
|
||||||
});
|
|
||||||
await queryInterface.addColumn('dealer_claim_details', 'e_invoice_date', {
|
|
||||||
type: DataTypes.DATEONLY,
|
|
||||||
allowNull: true,
|
|
||||||
});
|
|
||||||
await queryInterface.addColumn('dealer_claim_details', 'credit_note_number', {
|
|
||||||
type: DataTypes.STRING(100),
|
|
||||||
allowNull: true,
|
|
||||||
});
|
|
||||||
await queryInterface.addColumn('dealer_claim_details', 'credit_note_date', {
|
|
||||||
type: DataTypes.DATEONLY,
|
|
||||||
allowNull: true,
|
|
||||||
});
|
|
||||||
await queryInterface.addColumn('dealer_claim_details', 'credit_note_amount', {
|
|
||||||
type: DataTypes.DECIMAL(15, 2),
|
|
||||||
allowNull: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -1,55 +0,0 @@
|
|||||||
import { QueryInterface, DataTypes } from 'sequelize';
|
|
||||||
|
|
||||||
export async function up(queryInterface: QueryInterface): Promise<void> {
|
|
||||||
await queryInterface.createTable('dealer_completion_expenses', {
|
|
||||||
expense_id: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
primaryKey: true,
|
|
||||||
defaultValue: DataTypes.UUIDV4,
|
|
||||||
},
|
|
||||||
request_id: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
references: { model: 'workflow_requests', key: 'request_id' },
|
|
||||||
onDelete: 'CASCADE',
|
|
||||||
onUpdate: 'CASCADE',
|
|
||||||
},
|
|
||||||
completion_id: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: true,
|
|
||||||
references: { model: 'dealer_completion_details', key: 'completion_id' },
|
|
||||||
onDelete: 'CASCADE',
|
|
||||||
onUpdate: 'CASCADE',
|
|
||||||
},
|
|
||||||
description: {
|
|
||||||
type: DataTypes.STRING(500),
|
|
||||||
allowNull: false,
|
|
||||||
},
|
|
||||||
amount: {
|
|
||||||
type: DataTypes.DECIMAL(15, 2),
|
|
||||||
allowNull: false,
|
|
||||||
},
|
|
||||||
created_at: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW,
|
|
||||||
},
|
|
||||||
updated_at: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await queryInterface.addIndex('dealer_completion_expenses', ['request_id'], {
|
|
||||||
name: 'idx_dealer_completion_expenses_request_id',
|
|
||||||
});
|
|
||||||
await queryInterface.addIndex('dealer_completion_expenses', ['completion_id'], {
|
|
||||||
name: 'idx_dealer_completion_expenses_completion_id',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function down(queryInterface: QueryInterface): Promise<void> {
|
|
||||||
await queryInterface.dropTable('dealer_completion_expenses');
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -1,240 +0,0 @@
|
|||||||
import { QueryInterface, DataTypes } from 'sequelize';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper function to check if a column exists in a table
|
|
||||||
*/
|
|
||||||
async function columnExists(
|
|
||||||
queryInterface: QueryInterface,
|
|
||||||
tableName: string,
|
|
||||||
columnName: string
|
|
||||||
): Promise<boolean> {
|
|
||||||
try {
|
|
||||||
const tableDescription = await queryInterface.describeTable(tableName);
|
|
||||||
return columnName in tableDescription;
|
|
||||||
} catch (error) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Migration: Fix column names in claim_invoices and claim_credit_notes tables
|
|
||||||
*
|
|
||||||
* This migration handles the case where tables were created with old column names
|
|
||||||
* and need to be updated to match the new schema.
|
|
||||||
*/
|
|
||||||
export async function up(queryInterface: QueryInterface): Promise<void> {
|
|
||||||
try {
|
|
||||||
// Check if claim_invoices table exists
|
|
||||||
const [invoiceTables] = await queryInterface.sequelize.query(`
|
|
||||||
SELECT table_name
|
|
||||||
FROM information_schema.tables
|
|
||||||
WHERE table_schema = 'public'
|
|
||||||
AND table_name = 'claim_invoices';
|
|
||||||
`);
|
|
||||||
|
|
||||||
if (invoiceTables.length > 0) {
|
|
||||||
// Fix claim_invoices table
|
|
||||||
const hasOldAmount = await columnExists(queryInterface, 'claim_invoices', 'amount');
|
|
||||||
const hasNewAmount = await columnExists(queryInterface, 'claim_invoices', 'invoice_amount');
|
|
||||||
|
|
||||||
if (hasOldAmount && !hasNewAmount) {
|
|
||||||
// Rename amount to invoice_amount
|
|
||||||
await queryInterface.renameColumn('claim_invoices', 'amount', 'invoice_amount');
|
|
||||||
console.log('✅ Renamed claim_invoices.amount to invoice_amount');
|
|
||||||
} else if (!hasOldAmount && !hasNewAmount) {
|
|
||||||
// Add invoice_amount if neither exists
|
|
||||||
await queryInterface.addColumn('claim_invoices', 'invoice_amount', {
|
|
||||||
type: DataTypes.DECIMAL(15, 2),
|
|
||||||
allowNull: true,
|
|
||||||
});
|
|
||||||
console.log('✅ Added invoice_amount column to claim_invoices');
|
|
||||||
} else if (hasNewAmount) {
|
|
||||||
console.log('✅ invoice_amount column already exists in claim_invoices');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for status vs generation_status
|
|
||||||
const hasStatus = await columnExists(queryInterface, 'claim_invoices', 'status');
|
|
||||||
const hasGenerationStatus = await columnExists(queryInterface, 'claim_invoices', 'generation_status');
|
|
||||||
|
|
||||||
if (hasStatus && !hasGenerationStatus) {
|
|
||||||
// Rename status to generation_status
|
|
||||||
await queryInterface.renameColumn('claim_invoices', 'status', 'generation_status');
|
|
||||||
console.log('✅ Renamed claim_invoices.status to generation_status');
|
|
||||||
} else if (!hasStatus && !hasGenerationStatus) {
|
|
||||||
// Add generation_status if neither exists
|
|
||||||
await queryInterface.addColumn('claim_invoices', 'generation_status', {
|
|
||||||
type: DataTypes.STRING(50),
|
|
||||||
allowNull: true,
|
|
||||||
});
|
|
||||||
console.log('✅ Added generation_status column to claim_invoices');
|
|
||||||
} else if (hasGenerationStatus) {
|
|
||||||
console.log('✅ generation_status column already exists in claim_invoices');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if claim_credit_notes table exists
|
|
||||||
const [creditNoteTables] = await queryInterface.sequelize.query(`
|
|
||||||
SELECT table_name
|
|
||||||
FROM information_schema.tables
|
|
||||||
WHERE table_schema = 'public'
|
|
||||||
AND table_name = 'claim_credit_notes';
|
|
||||||
`);
|
|
||||||
|
|
||||||
if (creditNoteTables.length > 0) {
|
|
||||||
// Fix claim_credit_notes table
|
|
||||||
const hasOldAmount = await columnExists(queryInterface, 'claim_credit_notes', 'credit_note_amount');
|
|
||||||
const hasNewAmount = await columnExists(queryInterface, 'claim_credit_notes', 'credit_amount');
|
|
||||||
|
|
||||||
if (hasOldAmount && !hasNewAmount) {
|
|
||||||
// Rename credit_note_amount to credit_amount
|
|
||||||
await queryInterface.renameColumn('claim_credit_notes', 'credit_note_amount', 'credit_amount');
|
|
||||||
console.log('✅ Renamed claim_credit_notes.credit_note_amount to credit_amount');
|
|
||||||
} else if (!hasOldAmount && !hasNewAmount) {
|
|
||||||
// Add credit_amount if neither exists
|
|
||||||
await queryInterface.addColumn('claim_credit_notes', 'credit_amount', {
|
|
||||||
type: DataTypes.DECIMAL(15, 2),
|
|
||||||
allowNull: true,
|
|
||||||
});
|
|
||||||
console.log('✅ Added credit_amount column to claim_credit_notes');
|
|
||||||
} else if (hasNewAmount) {
|
|
||||||
console.log('✅ credit_amount column already exists in claim_credit_notes');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for status vs confirmation_status
|
|
||||||
const hasStatus = await columnExists(queryInterface, 'claim_credit_notes', 'status');
|
|
||||||
const hasConfirmationStatus = await columnExists(queryInterface, 'claim_credit_notes', 'confirmation_status');
|
|
||||||
|
|
||||||
if (hasStatus && !hasConfirmationStatus) {
|
|
||||||
// Rename status to confirmation_status
|
|
||||||
await queryInterface.renameColumn('claim_credit_notes', 'status', 'confirmation_status');
|
|
||||||
console.log('✅ Renamed claim_credit_notes.status to confirmation_status');
|
|
||||||
} else if (!hasStatus && !hasConfirmationStatus) {
|
|
||||||
// Add confirmation_status if neither exists
|
|
||||||
await queryInterface.addColumn('claim_credit_notes', 'confirmation_status', {
|
|
||||||
type: DataTypes.STRING(50),
|
|
||||||
allowNull: true,
|
|
||||||
});
|
|
||||||
console.log('✅ Added confirmation_status column to claim_credit_notes');
|
|
||||||
} else if (hasConfirmationStatus) {
|
|
||||||
console.log('✅ confirmation_status column already exists in claim_credit_notes');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure invoice_id column exists
|
|
||||||
const hasInvoiceId = await columnExists(queryInterface, 'claim_credit_notes', 'invoice_id');
|
|
||||||
if (!hasInvoiceId) {
|
|
||||||
await queryInterface.addColumn('claim_credit_notes', 'invoice_id', {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: true,
|
|
||||||
references: {
|
|
||||||
model: 'claim_invoices',
|
|
||||||
key: 'invoice_id',
|
|
||||||
},
|
|
||||||
onDelete: 'SET NULL',
|
|
||||||
onUpdate: 'CASCADE',
|
|
||||||
});
|
|
||||||
console.log('✅ Added invoice_id column to claim_credit_notes');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure sap_document_number column exists
|
|
||||||
const hasSapDoc = await columnExists(queryInterface, 'claim_credit_notes', 'sap_document_number');
|
|
||||||
if (!hasSapDoc) {
|
|
||||||
await queryInterface.addColumn('claim_credit_notes', 'sap_document_number', {
|
|
||||||
type: DataTypes.STRING(100),
|
|
||||||
allowNull: true,
|
|
||||||
});
|
|
||||||
console.log('✅ Added sap_document_number column to claim_credit_notes');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure credit_note_file_path column exists
|
|
||||||
const hasFilePath = await columnExists(queryInterface, 'claim_credit_notes', 'credit_note_file_path');
|
|
||||||
if (!hasFilePath) {
|
|
||||||
await queryInterface.addColumn('claim_credit_notes', 'credit_note_file_path', {
|
|
||||||
type: DataTypes.STRING(500),
|
|
||||||
allowNull: true,
|
|
||||||
});
|
|
||||||
console.log('✅ Added credit_note_file_path column to claim_credit_notes');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure confirmed_by column exists
|
|
||||||
const hasConfirmedBy = await columnExists(queryInterface, 'claim_credit_notes', 'confirmed_by');
|
|
||||||
if (!hasConfirmedBy) {
|
|
||||||
await queryInterface.addColumn('claim_credit_notes', 'confirmed_by', {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: true,
|
|
||||||
references: {
|
|
||||||
model: 'users',
|
|
||||||
key: 'user_id',
|
|
||||||
},
|
|
||||||
onDelete: 'SET NULL',
|
|
||||||
onUpdate: 'CASCADE',
|
|
||||||
});
|
|
||||||
console.log('✅ Added confirmed_by column to claim_credit_notes');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure confirmed_at column exists
|
|
||||||
const hasConfirmedAt = await columnExists(queryInterface, 'claim_credit_notes', 'confirmed_at');
|
|
||||||
if (!hasConfirmedAt) {
|
|
||||||
await queryInterface.addColumn('claim_credit_notes', 'confirmed_at', {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: true,
|
|
||||||
});
|
|
||||||
console.log('✅ Added confirmed_at column to claim_credit_notes');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure invoice_file_path exists in claim_invoices
|
|
||||||
if (invoiceTables.length > 0) {
|
|
||||||
const hasFilePath = await columnExists(queryInterface, 'claim_invoices', 'invoice_file_path');
|
|
||||||
if (!hasFilePath) {
|
|
||||||
await queryInterface.addColumn('claim_invoices', 'invoice_file_path', {
|
|
||||||
type: DataTypes.STRING(500),
|
|
||||||
allowNull: true,
|
|
||||||
});
|
|
||||||
console.log('✅ Added invoice_file_path column to claim_invoices');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure error_message exists
|
|
||||||
const hasErrorMessage = await columnExists(queryInterface, 'claim_invoices', 'error_message');
|
|
||||||
if (!hasErrorMessage) {
|
|
||||||
await queryInterface.addColumn('claim_invoices', 'error_message', {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
allowNull: true,
|
|
||||||
});
|
|
||||||
console.log('✅ Added error_message column to claim_invoices');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure generated_at exists
|
|
||||||
const hasGeneratedAt = await columnExists(queryInterface, 'claim_invoices', 'generated_at');
|
|
||||||
if (!hasGeneratedAt) {
|
|
||||||
await queryInterface.addColumn('claim_invoices', 'generated_at', {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: true,
|
|
||||||
});
|
|
||||||
console.log('✅ Added generated_at column to claim_invoices');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure error_message exists in claim_credit_notes
|
|
||||||
if (creditNoteTables.length > 0) {
|
|
||||||
const hasErrorMessage = await columnExists(queryInterface, 'claim_credit_notes', 'error_message');
|
|
||||||
if (!hasErrorMessage) {
|
|
||||||
await queryInterface.addColumn('claim_credit_notes', 'error_message', {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
allowNull: true,
|
|
||||||
});
|
|
||||||
console.log('✅ Added error_message column to claim_credit_notes');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error('Migration error:', error.message);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function down(queryInterface: QueryInterface): Promise<void> {
|
|
||||||
// This migration is idempotent and safe to run multiple times
|
|
||||||
// The down migration would reverse the changes, but it's safer to keep the new schema
|
|
||||||
console.log('Note: Down migration not implemented - keeping new column names');
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -1,134 +0,0 @@
|
|||||||
import { QueryInterface, DataTypes } from 'sequelize';
|
|
||||||
|
|
||||||
export const up = async (queryInterface: QueryInterface) => {
|
|
||||||
// 1. Drop and recreate the enum type for snapshot_type to ensure all values are included
|
|
||||||
// This ensures APPROVE is always present when table is recreated
|
|
||||||
// Note: Table should be dropped manually before running this migration
|
|
||||||
try {
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
DO $$
|
|
||||||
BEGIN
|
|
||||||
-- Drop enum if it exists (cascade will handle any dependencies)
|
|
||||||
IF EXISTS (SELECT 1 FROM pg_type WHERE typname = 'enum_dealer_claim_history_snapshot_type') THEN
|
|
||||||
DROP TYPE IF EXISTS enum_dealer_claim_history_snapshot_type CASCADE;
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
-- Create enum with all values including APPROVE
|
|
||||||
CREATE TYPE enum_dealer_claim_history_snapshot_type AS ENUM ('PROPOSAL', 'COMPLETION', 'INTERNAL_ORDER', 'WORKFLOW', 'APPROVE');
|
|
||||||
END $$;
|
|
||||||
`);
|
|
||||||
} catch (error) {
|
|
||||||
// If enum creation fails, log error but continue
|
|
||||||
console.error('Enum creation error:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Create new simplified level-based dealer_claim_history table
|
|
||||||
await queryInterface.createTable('dealer_claim_history', {
|
|
||||||
history_id: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
defaultValue: DataTypes.UUIDV4,
|
|
||||||
primaryKey: true
|
|
||||||
},
|
|
||||||
request_id: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
references: {
|
|
||||||
model: 'workflow_requests',
|
|
||||||
key: 'request_id'
|
|
||||||
},
|
|
||||||
onUpdate: 'CASCADE',
|
|
||||||
onDelete: 'CASCADE'
|
|
||||||
},
|
|
||||||
approval_level_id: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: true, // Nullable for workflow-level snapshots
|
|
||||||
references: {
|
|
||||||
model: 'approval_levels',
|
|
||||||
key: 'level_id'
|
|
||||||
},
|
|
||||||
onUpdate: 'CASCADE',
|
|
||||||
onDelete: 'SET NULL'
|
|
||||||
},
|
|
||||||
level_number: {
|
|
||||||
type: DataTypes.INTEGER,
|
|
||||||
allowNull: true, // Nullable for workflow-level snapshots
|
|
||||||
comment: 'Level number for easier querying (e.g., 1=Dealer, 3=Dept Lead, 4/5=Completion)'
|
|
||||||
},
|
|
||||||
level_name: {
|
|
||||||
type: DataTypes.STRING(255),
|
|
||||||
allowNull: true, // Nullable for workflow-level snapshots
|
|
||||||
comment: 'Level name for consistent matching (e.g., "Dealer Proposal Submission", "Department Lead Approval")'
|
|
||||||
},
|
|
||||||
version: {
|
|
||||||
type: DataTypes.INTEGER,
|
|
||||||
allowNull: false,
|
|
||||||
comment: 'Version number for this specific level (starts at 1 per level)'
|
|
||||||
},
|
|
||||||
snapshot_type: {
|
|
||||||
type: DataTypes.ENUM('PROPOSAL', 'COMPLETION', 'INTERNAL_ORDER', 'WORKFLOW', 'APPROVE'),
|
|
||||||
allowNull: false,
|
|
||||||
comment: 'Type of snapshot: PROPOSAL (Step 1), COMPLETION (Step 4/5), INTERNAL_ORDER (Step 3), WORKFLOW (general), APPROVE (approver actions with comments)'
|
|
||||||
},
|
|
||||||
snapshot_data: {
|
|
||||||
type: DataTypes.JSONB,
|
|
||||||
allowNull: false,
|
|
||||||
comment: 'JSON object containing all snapshot data specific to this level and type. Structure varies by snapshot_type.'
|
|
||||||
},
|
|
||||||
change_reason: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'Reason for this version change (e.g., "Revision Requested: ...")'
|
|
||||||
},
|
|
||||||
changed_by: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
references: {
|
|
||||||
model: 'users',
|
|
||||||
key: 'user_id'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created_at: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add indexes for efficient querying
|
|
||||||
await queryInterface.addIndex('dealer_claim_history', ['request_id', 'level_number', 'version'], {
|
|
||||||
name: 'idx_history_request_level_version'
|
|
||||||
});
|
|
||||||
await queryInterface.addIndex('dealer_claim_history', ['approval_level_id', 'version'], {
|
|
||||||
name: 'idx_history_level_version'
|
|
||||||
});
|
|
||||||
await queryInterface.addIndex('dealer_claim_history', ['request_id', 'snapshot_type'], {
|
|
||||||
name: 'idx_history_request_type'
|
|
||||||
});
|
|
||||||
await queryInterface.addIndex('dealer_claim_history', ['snapshot_type', 'level_number'], {
|
|
||||||
name: 'idx_history_type_level'
|
|
||||||
});
|
|
||||||
await queryInterface.addIndex('dealer_claim_history', ['request_id', 'level_name'], {
|
|
||||||
name: 'idx_history_request_level_name'
|
|
||||||
});
|
|
||||||
await queryInterface.addIndex('dealer_claim_history', ['level_name', 'snapshot_type'], {
|
|
||||||
name: 'idx_history_level_name_type'
|
|
||||||
});
|
|
||||||
// Index for JSONB queries on snapshot_data
|
|
||||||
await queryInterface.addIndex('dealer_claim_history', ['snapshot_type'], {
|
|
||||||
name: 'idx_history_snapshot_type',
|
|
||||||
using: 'BTREE'
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const down = async (queryInterface: QueryInterface) => {
|
|
||||||
// Note: Table should be dropped manually
|
|
||||||
// Drop the enum type
|
|
||||||
try {
|
|
||||||
await queryInterface.sequelize.query(`
|
|
||||||
DROP TYPE IF EXISTS enum_dealer_claim_history_snapshot_type CASCADE;
|
|
||||||
`);
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('Enum drop warning:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@ -1,115 +0,0 @@
|
|||||||
|
|
||||||
import { QueryInterface, DataTypes } from 'sequelize';
|
|
||||||
|
|
||||||
export async function up(queryInterface: QueryInterface): Promise<void> {
|
|
||||||
try {
|
|
||||||
const tableDescription = await queryInterface.describeTable('workflow_templates');
|
|
||||||
|
|
||||||
// 1. Rename id -> template_id
|
|
||||||
if (tableDescription.id && !tableDescription.template_id) {
|
|
||||||
console.log('Renaming id to template_id...');
|
|
||||||
await queryInterface.renameColumn('workflow_templates', 'id', 'template_id');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Rename name -> template_name
|
|
||||||
if (tableDescription.name && !tableDescription.template_name) {
|
|
||||||
console.log('Renaming name to template_name...');
|
|
||||||
await queryInterface.renameColumn('workflow_templates', 'name', 'template_name');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Rename description -> template_description
|
|
||||||
if (tableDescription.description && !tableDescription.template_description) {
|
|
||||||
console.log('Renaming description to template_description...');
|
|
||||||
await queryInterface.renameColumn('workflow_templates', 'description', 'template_description');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Rename category -> template_category
|
|
||||||
if (tableDescription.category && !tableDescription.template_category) {
|
|
||||||
console.log('Renaming category to template_category...');
|
|
||||||
await queryInterface.renameColumn('workflow_templates', 'category', 'template_category');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. Rename suggested_sla -> default_tat_hours
|
|
||||||
if (tableDescription.suggested_sla && !tableDescription.default_tat_hours) {
|
|
||||||
console.log('Renaming suggested_sla to default_tat_hours...');
|
|
||||||
await queryInterface.renameColumn('workflow_templates', 'suggested_sla', 'default_tat_hours');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 6. Add missing columns
|
|
||||||
if (!tableDescription.template_code) {
|
|
||||||
console.log('Adding template_code column...');
|
|
||||||
await queryInterface.addColumn('workflow_templates', 'template_code', {
|
|
||||||
type: DataTypes.STRING(50),
|
|
||||||
allowNull: true,
|
|
||||||
unique: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!tableDescription.workflow_type) {
|
|
||||||
console.log('Adding workflow_type column...');
|
|
||||||
await queryInterface.addColumn('workflow_templates', 'workflow_type', {
|
|
||||||
type: DataTypes.STRING(50),
|
|
||||||
allowNull: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!tableDescription.approval_levels_config) {
|
|
||||||
console.log('Adding approval_levels_config column...');
|
|
||||||
await queryInterface.addColumn('workflow_templates', 'approval_levels_config', {
|
|
||||||
type: DataTypes.JSONB,
|
|
||||||
allowNull: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!tableDescription.form_steps_config) {
|
|
||||||
console.log('Adding form_steps_config column...');
|
|
||||||
await queryInterface.addColumn('workflow_templates', 'form_steps_config', {
|
|
||||||
type: DataTypes.JSONB,
|
|
||||||
allowNull: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!tableDescription.user_field_mappings) {
|
|
||||||
console.log('Adding user_field_mappings column...');
|
|
||||||
await queryInterface.addColumn('workflow_templates', 'user_field_mappings', {
|
|
||||||
type: DataTypes.JSONB,
|
|
||||||
allowNull: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!tableDescription.dynamic_approver_config) {
|
|
||||||
console.log('Adding dynamic_approver_config column...');
|
|
||||||
await queryInterface.addColumn('workflow_templates', 'dynamic_approver_config', {
|
|
||||||
type: DataTypes.JSONB,
|
|
||||||
allowNull: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!tableDescription.is_system_template) {
|
|
||||||
console.log('Adding is_system_template column...');
|
|
||||||
await queryInterface.addColumn('workflow_templates', 'is_system_template', {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: false
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!tableDescription.usage_count) {
|
|
||||||
console.log('Adding usage_count column...');
|
|
||||||
await queryInterface.addColumn('workflow_templates', 'usage_count', {
|
|
||||||
type: DataTypes.INTEGER,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: 0
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('✅ Schema validation/fix complete');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error in schema fix migration:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function down(queryInterface: QueryInterface): Promise<void> {
|
|
||||||
// Revert is complex/risky effectively, skipping for this fix-forward migration
|
|
||||||
}
|
|
||||||
@ -1,120 +0,0 @@
|
|||||||
import { DataTypes, Model, Optional } from 'sequelize';
|
|
||||||
import { sequelize } from '@config/database';
|
|
||||||
|
|
||||||
interface ActivityAttributes {
|
|
||||||
activityId: string;
|
|
||||||
requestId: string;
|
|
||||||
userId?: string | null;
|
|
||||||
userName?: string | null;
|
|
||||||
activityType: string; // activity_type
|
|
||||||
activityDescription: string; // activity_description
|
|
||||||
activityCategory?: string | null;
|
|
||||||
severity?: string | null;
|
|
||||||
metadata?: object | null;
|
|
||||||
isSystemEvent?: boolean | null;
|
|
||||||
ipAddress?: string | null;
|
|
||||||
userAgent?: string | null;
|
|
||||||
createdAt: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ActivityCreationAttributes extends Optional<ActivityAttributes, 'activityId' | 'createdAt'> {}
|
|
||||||
|
|
||||||
class Activity extends Model<ActivityAttributes, ActivityCreationAttributes> implements ActivityAttributes {
|
|
||||||
public activityId!: string;
|
|
||||||
public requestId!: string;
|
|
||||||
public userId!: string | null;
|
|
||||||
public userName!: string | null;
|
|
||||||
public activityType!: string;
|
|
||||||
public activityDescription!: string;
|
|
||||||
public activityCategory!: string | null;
|
|
||||||
public severity!: string | null;
|
|
||||||
public metadata!: object | null;
|
|
||||||
public isSystemEvent!: boolean | null;
|
|
||||||
public ipAddress!: string | null;
|
|
||||||
public userAgent!: string | null;
|
|
||||||
public createdAt!: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
Activity.init(
|
|
||||||
{
|
|
||||||
activityId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
defaultValue: DataTypes.UUIDV4,
|
|
||||||
primaryKey: true,
|
|
||||||
field: 'activity_id'
|
|
||||||
},
|
|
||||||
requestId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
field: 'request_id'
|
|
||||||
},
|
|
||||||
userId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'user_id'
|
|
||||||
},
|
|
||||||
userName: {
|
|
||||||
type: DataTypes.STRING(255),
|
|
||||||
allowNull: true,
|
|
||||||
field: 'user_name'
|
|
||||||
},
|
|
||||||
activityType: {
|
|
||||||
type: DataTypes.STRING(100),
|
|
||||||
allowNull: false,
|
|
||||||
field: 'activity_type'
|
|
||||||
},
|
|
||||||
activityDescription: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
allowNull: false,
|
|
||||||
field: 'activity_description'
|
|
||||||
},
|
|
||||||
activityCategory: {
|
|
||||||
type: DataTypes.STRING(100),
|
|
||||||
allowNull: true,
|
|
||||||
field: 'activity_category'
|
|
||||||
},
|
|
||||||
severity: {
|
|
||||||
type: DataTypes.STRING(50),
|
|
||||||
allowNull: true
|
|
||||||
},
|
|
||||||
metadata: {
|
|
||||||
type: DataTypes.JSONB,
|
|
||||||
allowNull: true
|
|
||||||
},
|
|
||||||
isSystemEvent: {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'is_system_event'
|
|
||||||
},
|
|
||||||
ipAddress: {
|
|
||||||
type: DataTypes.STRING(100),
|
|
||||||
allowNull: true,
|
|
||||||
field: 'ip_address'
|
|
||||||
},
|
|
||||||
userAgent: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'user_agent'
|
|
||||||
},
|
|
||||||
createdAt: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW,
|
|
||||||
field: 'created_at'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
sequelize,
|
|
||||||
modelName: 'Activity',
|
|
||||||
tableName: 'activities',
|
|
||||||
timestamps: false,
|
|
||||||
indexes: [
|
|
||||||
{ fields: ['request_id'] },
|
|
||||||
{ fields: ['created_at'] }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export { Activity };
|
|
||||||
|
|
||||||
|
|
||||||
@ -1,127 +0,0 @@
|
|||||||
import { DataTypes, Model, Optional } from 'sequelize';
|
|
||||||
import { sequelize } from '@config/database';
|
|
||||||
import { User } from './User';
|
|
||||||
|
|
||||||
interface ActivityTypeAttributes {
|
|
||||||
activityTypeId: string;
|
|
||||||
title: string;
|
|
||||||
itemCode?: string;
|
|
||||||
taxationType?: string;
|
|
||||||
sapRefNo?: string;
|
|
||||||
isActive: boolean;
|
|
||||||
createdBy: string;
|
|
||||||
updatedBy?: string;
|
|
||||||
createdAt: Date;
|
|
||||||
updatedAt: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ActivityTypeCreationAttributes extends Optional<ActivityTypeAttributes, 'activityTypeId' | 'itemCode' | 'taxationType' | 'sapRefNo' | 'isActive' | 'updatedBy' | 'createdAt' | 'updatedAt'> {}
|
|
||||||
|
|
||||||
class ActivityType extends Model<ActivityTypeAttributes, ActivityTypeCreationAttributes> implements ActivityTypeAttributes {
|
|
||||||
public activityTypeId!: string;
|
|
||||||
public title!: string;
|
|
||||||
public itemCode?: string;
|
|
||||||
public taxationType?: string;
|
|
||||||
public sapRefNo?: string;
|
|
||||||
public isActive!: boolean;
|
|
||||||
public createdBy!: string;
|
|
||||||
public updatedBy?: string;
|
|
||||||
public createdAt!: Date;
|
|
||||||
public updatedAt!: Date;
|
|
||||||
|
|
||||||
// Associations
|
|
||||||
public creator?: User;
|
|
||||||
public updater?: User;
|
|
||||||
}
|
|
||||||
|
|
||||||
ActivityType.init(
|
|
||||||
{
|
|
||||||
activityTypeId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
defaultValue: DataTypes.UUIDV4,
|
|
||||||
primaryKey: true,
|
|
||||||
field: 'activity_type_id'
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
type: DataTypes.STRING(200),
|
|
||||||
allowNull: false,
|
|
||||||
unique: true,
|
|
||||||
field: 'title'
|
|
||||||
},
|
|
||||||
itemCode: {
|
|
||||||
type: DataTypes.STRING(100),
|
|
||||||
allowNull: true,
|
|
||||||
defaultValue: null,
|
|
||||||
field: 'item_code'
|
|
||||||
},
|
|
||||||
taxationType: {
|
|
||||||
type: DataTypes.STRING(100),
|
|
||||||
allowNull: true,
|
|
||||||
defaultValue: null,
|
|
||||||
field: 'taxation_type'
|
|
||||||
},
|
|
||||||
sapRefNo: {
|
|
||||||
type: DataTypes.STRING(100),
|
|
||||||
allowNull: true,
|
|
||||||
defaultValue: null,
|
|
||||||
field: 'sap_ref_no'
|
|
||||||
},
|
|
||||||
isActive: {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
defaultValue: true,
|
|
||||||
field: 'is_active'
|
|
||||||
},
|
|
||||||
createdBy: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
field: 'created_by'
|
|
||||||
},
|
|
||||||
updatedBy: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'updated_by'
|
|
||||||
},
|
|
||||||
createdAt: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW,
|
|
||||||
field: 'created_at'
|
|
||||||
},
|
|
||||||
updatedAt: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW,
|
|
||||||
field: 'updated_at'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
sequelize,
|
|
||||||
modelName: 'ActivityType',
|
|
||||||
tableName: 'activity_types',
|
|
||||||
timestamps: true,
|
|
||||||
createdAt: 'created_at',
|
|
||||||
updatedAt: 'updated_at',
|
|
||||||
indexes: [
|
|
||||||
{ fields: ['title'], unique: true },
|
|
||||||
{ fields: ['is_active'] },
|
|
||||||
{ fields: ['item_code'] },
|
|
||||||
{ fields: ['created_by'] }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Associations
|
|
||||||
ActivityType.belongsTo(User, {
|
|
||||||
as: 'creator',
|
|
||||||
foreignKey: 'createdBy',
|
|
||||||
targetKey: 'userId'
|
|
||||||
});
|
|
||||||
|
|
||||||
ActivityType.belongsTo(User, {
|
|
||||||
as: 'updater',
|
|
||||||
foreignKey: 'updatedBy',
|
|
||||||
targetKey: 'userId'
|
|
||||||
});
|
|
||||||
|
|
||||||
export { ActivityType };
|
|
||||||
|
|
||||||
@ -1,307 +0,0 @@
|
|||||||
import { DataTypes, Model, Optional } from 'sequelize';
|
|
||||||
import { sequelize } from '@config/database';
|
|
||||||
import { User } from './User';
|
|
||||||
import { WorkflowRequest } from './WorkflowRequest';
|
|
||||||
import { ApprovalStatus } from '../types/common.types';
|
|
||||||
|
|
||||||
interface ApprovalLevelAttributes {
|
|
||||||
levelId: string;
|
|
||||||
requestId: string;
|
|
||||||
levelNumber: number;
|
|
||||||
levelName?: string;
|
|
||||||
approverId: string;
|
|
||||||
approverEmail: string;
|
|
||||||
approverName: string;
|
|
||||||
tatHours: number;
|
|
||||||
tatDays: number;
|
|
||||||
status: ApprovalStatus;
|
|
||||||
levelStartTime?: Date;
|
|
||||||
levelEndTime?: Date;
|
|
||||||
actionDate?: Date;
|
|
||||||
comments?: string;
|
|
||||||
rejectionReason?: string;
|
|
||||||
breachReason?: string;
|
|
||||||
isFinalApprover: boolean;
|
|
||||||
elapsedHours: number;
|
|
||||||
remainingHours: number;
|
|
||||||
tatPercentageUsed: number;
|
|
||||||
tat50AlertSent: boolean;
|
|
||||||
tat75AlertSent: boolean;
|
|
||||||
tatBreached: boolean;
|
|
||||||
tatStartTime?: Date;
|
|
||||||
isPaused: boolean;
|
|
||||||
pausedAt?: Date;
|
|
||||||
pausedBy?: string;
|
|
||||||
pauseReason?: string;
|
|
||||||
pauseResumeDate?: Date;
|
|
||||||
pauseTatStartTime?: Date;
|
|
||||||
pauseElapsedHours?: number;
|
|
||||||
createdAt: Date;
|
|
||||||
updatedAt: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ApprovalLevelCreationAttributes extends Optional<ApprovalLevelAttributes, 'levelId' | 'levelName' | 'levelStartTime' | 'levelEndTime' | 'actionDate' | 'comments' | 'rejectionReason' | 'breachReason' | 'tat50AlertSent' | 'tat75AlertSent' | 'tatBreached' | 'tatStartTime' | 'tatDays' | 'isPaused' | 'pausedAt' | 'pausedBy' | 'pauseReason' | 'pauseResumeDate' | 'pauseTatStartTime' | 'pauseElapsedHours' | 'createdAt' | 'updatedAt'> {}
|
|
||||||
|
|
||||||
class ApprovalLevel extends Model<ApprovalLevelAttributes, ApprovalLevelCreationAttributes> implements ApprovalLevelAttributes {
|
|
||||||
public levelId!: string;
|
|
||||||
public requestId!: string;
|
|
||||||
public levelNumber!: number;
|
|
||||||
public levelName?: string;
|
|
||||||
public approverId!: string;
|
|
||||||
public approverEmail!: string;
|
|
||||||
public approverName!: string;
|
|
||||||
public tatHours!: number;
|
|
||||||
public tatDays!: number;
|
|
||||||
public status!: ApprovalStatus;
|
|
||||||
public levelStartTime?: Date;
|
|
||||||
public levelEndTime?: Date;
|
|
||||||
public actionDate?: Date;
|
|
||||||
public comments?: string;
|
|
||||||
public rejectionReason?: string;
|
|
||||||
public breachReason?: string;
|
|
||||||
public isFinalApprover!: boolean;
|
|
||||||
public elapsedHours!: number;
|
|
||||||
public remainingHours!: number;
|
|
||||||
public tatPercentageUsed!: number;
|
|
||||||
public tat50AlertSent!: boolean;
|
|
||||||
public tat75AlertSent!: boolean;
|
|
||||||
public tatBreached!: boolean;
|
|
||||||
public tatStartTime?: Date;
|
|
||||||
public isPaused!: boolean;
|
|
||||||
public pausedAt?: Date;
|
|
||||||
public pausedBy?: string;
|
|
||||||
public pauseReason?: string;
|
|
||||||
public pauseResumeDate?: Date;
|
|
||||||
public pauseTatStartTime?: Date;
|
|
||||||
public pauseElapsedHours?: number;
|
|
||||||
public createdAt!: Date;
|
|
||||||
public updatedAt!: Date;
|
|
||||||
|
|
||||||
// Associations
|
|
||||||
public request?: WorkflowRequest;
|
|
||||||
public approver?: User;
|
|
||||||
}
|
|
||||||
|
|
||||||
ApprovalLevel.init(
|
|
||||||
{
|
|
||||||
levelId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
defaultValue: DataTypes.UUIDV4,
|
|
||||||
primaryKey: true,
|
|
||||||
field: 'level_id'
|
|
||||||
},
|
|
||||||
requestId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
field: 'request_id',
|
|
||||||
references: {
|
|
||||||
model: 'workflow_requests',
|
|
||||||
key: 'request_id'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
levelNumber: {
|
|
||||||
type: DataTypes.INTEGER,
|
|
||||||
allowNull: false,
|
|
||||||
field: 'level_number'
|
|
||||||
},
|
|
||||||
levelName: {
|
|
||||||
type: DataTypes.STRING(100),
|
|
||||||
allowNull: true,
|
|
||||||
field: 'level_name'
|
|
||||||
},
|
|
||||||
approverId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
field: 'approver_id',
|
|
||||||
references: {
|
|
||||||
model: 'users',
|
|
||||||
key: 'user_id'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
approverEmail: {
|
|
||||||
type: DataTypes.STRING(255),
|
|
||||||
allowNull: false,
|
|
||||||
field: 'approver_email'
|
|
||||||
},
|
|
||||||
approverName: {
|
|
||||||
type: DataTypes.STRING(200),
|
|
||||||
allowNull: false,
|
|
||||||
field: 'approver_name'
|
|
||||||
},
|
|
||||||
tatHours: {
|
|
||||||
type: DataTypes.DECIMAL(10, 2),
|
|
||||||
allowNull: false,
|
|
||||||
field: 'tat_hours'
|
|
||||||
},
|
|
||||||
tatDays: {
|
|
||||||
type: DataTypes.INTEGER,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'tat_days'
|
|
||||||
// This is a GENERATED STORED column in production DB (calculated as CEIL(tat_hours / 24.0))
|
|
||||||
// Database will auto-calculate this value - do NOT pass it during INSERT/UPDATE operations
|
|
||||||
},
|
|
||||||
status: {
|
|
||||||
type: DataTypes.ENUM('PENDING', 'IN_PROGRESS', 'APPROVED', 'REJECTED', 'SKIPPED', 'PAUSED'),
|
|
||||||
defaultValue: 'PENDING'
|
|
||||||
},
|
|
||||||
levelStartTime: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'level_start_time'
|
|
||||||
},
|
|
||||||
levelEndTime: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'level_end_time'
|
|
||||||
},
|
|
||||||
actionDate: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'action_date'
|
|
||||||
},
|
|
||||||
comments: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
allowNull: true
|
|
||||||
},
|
|
||||||
rejectionReason: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'rejection_reason'
|
|
||||||
},
|
|
||||||
breachReason: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'breach_reason',
|
|
||||||
comment: 'Reason for TAT breach - can contain paragraph-length text'
|
|
||||||
},
|
|
||||||
isFinalApprover: {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
defaultValue: false,
|
|
||||||
field: 'is_final_approver'
|
|
||||||
},
|
|
||||||
elapsedHours: {
|
|
||||||
type: DataTypes.DECIMAL(10, 2),
|
|
||||||
defaultValue: 0,
|
|
||||||
field: 'elapsed_hours'
|
|
||||||
},
|
|
||||||
remainingHours: {
|
|
||||||
type: DataTypes.DECIMAL(10, 2),
|
|
||||||
defaultValue: 0,
|
|
||||||
field: 'remaining_hours'
|
|
||||||
},
|
|
||||||
tatPercentageUsed: {
|
|
||||||
type: DataTypes.DECIMAL(5, 2),
|
|
||||||
defaultValue: 0,
|
|
||||||
field: 'tat_percentage_used'
|
|
||||||
},
|
|
||||||
tat50AlertSent: {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
defaultValue: false,
|
|
||||||
field: 'tat50_alert_sent'
|
|
||||||
},
|
|
||||||
tat75AlertSent: {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
defaultValue: false,
|
|
||||||
field: 'tat75_alert_sent'
|
|
||||||
},
|
|
||||||
tatBreached: {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
defaultValue: false,
|
|
||||||
field: 'tat_breached'
|
|
||||||
},
|
|
||||||
tatStartTime: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'tat_start_time'
|
|
||||||
},
|
|
||||||
isPaused: {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
defaultValue: false,
|
|
||||||
field: 'is_paused'
|
|
||||||
},
|
|
||||||
pausedAt: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'paused_at'
|
|
||||||
},
|
|
||||||
pausedBy: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'paused_by',
|
|
||||||
references: {
|
|
||||||
model: 'users',
|
|
||||||
key: 'user_id'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
pauseReason: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'pause_reason'
|
|
||||||
},
|
|
||||||
pauseResumeDate: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'pause_resume_date'
|
|
||||||
},
|
|
||||||
pauseTatStartTime: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'pause_tat_start_time'
|
|
||||||
},
|
|
||||||
pauseElapsedHours: {
|
|
||||||
type: DataTypes.DECIMAL(10, 2),
|
|
||||||
allowNull: true,
|
|
||||||
field: 'pause_elapsed_hours'
|
|
||||||
},
|
|
||||||
createdAt: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW,
|
|
||||||
field: 'created_at'
|
|
||||||
},
|
|
||||||
updatedAt: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW,
|
|
||||||
field: 'updated_at'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
sequelize,
|
|
||||||
modelName: 'ApprovalLevel',
|
|
||||||
tableName: 'approval_levels',
|
|
||||||
timestamps: true,
|
|
||||||
createdAt: 'created_at',
|
|
||||||
updatedAt: 'updated_at',
|
|
||||||
indexes: [
|
|
||||||
{
|
|
||||||
fields: ['request_id']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fields: ['approver_id']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fields: ['status']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
unique: true,
|
|
||||||
fields: ['request_id', 'level_number']
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Associations
|
|
||||||
ApprovalLevel.belongsTo(WorkflowRequest, {
|
|
||||||
as: 'request',
|
|
||||||
foreignKey: 'requestId',
|
|
||||||
targetKey: 'requestId'
|
|
||||||
});
|
|
||||||
|
|
||||||
ApprovalLevel.belongsTo(User, {
|
|
||||||
as: 'approver',
|
|
||||||
foreignKey: 'approverId',
|
|
||||||
targetKey: 'userId'
|
|
||||||
});
|
|
||||||
|
|
||||||
export { ApprovalLevel };
|
|
||||||
@ -1,295 +0,0 @@
|
|||||||
import { DataTypes, Model, Optional } from 'sequelize';
|
|
||||||
import { sequelize } from '@config/database';
|
|
||||||
import { WorkflowRequest } from './WorkflowRequest';
|
|
||||||
import { User } from './User';
|
|
||||||
|
|
||||||
export enum BudgetStatus {
|
|
||||||
DRAFT = 'DRAFT',
|
|
||||||
PROPOSED = 'PROPOSED',
|
|
||||||
APPROVED = 'APPROVED',
|
|
||||||
BLOCKED = 'BLOCKED',
|
|
||||||
CLOSED = 'CLOSED',
|
|
||||||
SETTLED = 'SETTLED'
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ClaimBudgetTrackingAttributes {
|
|
||||||
budgetId: string;
|
|
||||||
requestId: string;
|
|
||||||
// Initial Budget
|
|
||||||
initialEstimatedBudget?: number;
|
|
||||||
// Proposal Budget
|
|
||||||
proposalEstimatedBudget?: number;
|
|
||||||
proposalSubmittedAt?: Date;
|
|
||||||
// Approved Budget
|
|
||||||
approvedBudget?: number;
|
|
||||||
approvedAt?: Date;
|
|
||||||
approvedBy?: string;
|
|
||||||
// IO Blocked Budget
|
|
||||||
ioBlockedAmount?: number;
|
|
||||||
ioBlockedAt?: Date;
|
|
||||||
// Closed Expenses
|
|
||||||
closedExpenses?: number;
|
|
||||||
closedExpensesSubmittedAt?: Date;
|
|
||||||
// Final Claim Amount
|
|
||||||
finalClaimAmount?: number;
|
|
||||||
finalClaimAmountApprovedAt?: Date;
|
|
||||||
finalClaimAmountApprovedBy?: string;
|
|
||||||
// Credit Note
|
|
||||||
creditNoteAmount?: number;
|
|
||||||
creditNoteIssuedAt?: Date;
|
|
||||||
// Status & Metadata
|
|
||||||
budgetStatus: BudgetStatus;
|
|
||||||
currency: string;
|
|
||||||
varianceAmount?: number;
|
|
||||||
variancePercentage?: number;
|
|
||||||
// Audit
|
|
||||||
lastModifiedBy?: string;
|
|
||||||
lastModifiedAt?: Date;
|
|
||||||
modificationReason?: string;
|
|
||||||
createdAt: Date;
|
|
||||||
updatedAt: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ClaimBudgetTrackingCreationAttributes extends Optional<ClaimBudgetTrackingAttributes, 'budgetId' | 'initialEstimatedBudget' | 'proposalEstimatedBudget' | 'proposalSubmittedAt' | 'approvedBudget' | 'approvedAt' | 'approvedBy' | 'ioBlockedAmount' | 'ioBlockedAt' | 'closedExpenses' | 'closedExpensesSubmittedAt' | 'finalClaimAmount' | 'finalClaimAmountApprovedAt' | 'finalClaimAmountApprovedBy' | 'creditNoteAmount' | 'creditNoteIssuedAt' | 'varianceAmount' | 'variancePercentage' | 'lastModifiedBy' | 'lastModifiedAt' | 'modificationReason' | 'budgetStatus' | 'currency' | 'createdAt' | 'updatedAt'> {}
|
|
||||||
|
|
||||||
class ClaimBudgetTracking extends Model<ClaimBudgetTrackingAttributes, ClaimBudgetTrackingCreationAttributes> implements ClaimBudgetTrackingAttributes {
|
|
||||||
public budgetId!: string;
|
|
||||||
public requestId!: string;
|
|
||||||
public initialEstimatedBudget?: number;
|
|
||||||
public proposalEstimatedBudget?: number;
|
|
||||||
public proposalSubmittedAt?: Date;
|
|
||||||
public approvedBudget?: number;
|
|
||||||
public approvedAt?: Date;
|
|
||||||
public approvedBy?: string;
|
|
||||||
public ioBlockedAmount?: number;
|
|
||||||
public ioBlockedAt?: Date;
|
|
||||||
public closedExpenses?: number;
|
|
||||||
public closedExpensesSubmittedAt?: Date;
|
|
||||||
public finalClaimAmount?: number;
|
|
||||||
public finalClaimAmountApprovedAt?: Date;
|
|
||||||
public finalClaimAmountApprovedBy?: string;
|
|
||||||
public creditNoteAmount?: number;
|
|
||||||
public creditNoteIssuedAt?: Date;
|
|
||||||
public budgetStatus!: BudgetStatus;
|
|
||||||
public currency!: string;
|
|
||||||
public varianceAmount?: number;
|
|
||||||
public variancePercentage?: number;
|
|
||||||
public lastModifiedBy?: string;
|
|
||||||
public lastModifiedAt?: Date;
|
|
||||||
public modificationReason?: string;
|
|
||||||
public createdAt!: Date;
|
|
||||||
public updatedAt!: Date;
|
|
||||||
|
|
||||||
// Associations
|
|
||||||
public request?: WorkflowRequest;
|
|
||||||
public approver?: User;
|
|
||||||
public finalApprover?: User;
|
|
||||||
public lastModifier?: User;
|
|
||||||
}
|
|
||||||
|
|
||||||
ClaimBudgetTracking.init(
|
|
||||||
{
|
|
||||||
budgetId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
defaultValue: DataTypes.UUIDV4,
|
|
||||||
primaryKey: true,
|
|
||||||
field: 'budget_id'
|
|
||||||
},
|
|
||||||
requestId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
unique: true,
|
|
||||||
field: 'request_id',
|
|
||||||
references: {
|
|
||||||
model: 'workflow_requests',
|
|
||||||
key: 'request_id'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
initialEstimatedBudget: {
|
|
||||||
type: DataTypes.DECIMAL(15, 2),
|
|
||||||
allowNull: true,
|
|
||||||
field: 'initial_estimated_budget'
|
|
||||||
},
|
|
||||||
proposalEstimatedBudget: {
|
|
||||||
type: DataTypes.DECIMAL(15, 2),
|
|
||||||
allowNull: true,
|
|
||||||
field: 'proposal_estimated_budget'
|
|
||||||
},
|
|
||||||
proposalSubmittedAt: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'proposal_submitted_at'
|
|
||||||
},
|
|
||||||
approvedBudget: {
|
|
||||||
type: DataTypes.DECIMAL(15, 2),
|
|
||||||
allowNull: true,
|
|
||||||
field: 'approved_budget'
|
|
||||||
},
|
|
||||||
approvedAt: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'approved_at'
|
|
||||||
},
|
|
||||||
approvedBy: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'approved_by',
|
|
||||||
references: {
|
|
||||||
model: 'users',
|
|
||||||
key: 'user_id'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
ioBlockedAmount: {
|
|
||||||
type: DataTypes.DECIMAL(15, 2),
|
|
||||||
allowNull: true,
|
|
||||||
field: 'io_blocked_amount'
|
|
||||||
},
|
|
||||||
ioBlockedAt: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'io_blocked_at'
|
|
||||||
},
|
|
||||||
closedExpenses: {
|
|
||||||
type: DataTypes.DECIMAL(15, 2),
|
|
||||||
allowNull: true,
|
|
||||||
field: 'closed_expenses'
|
|
||||||
},
|
|
||||||
closedExpensesSubmittedAt: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'closed_expenses_submitted_at'
|
|
||||||
},
|
|
||||||
finalClaimAmount: {
|
|
||||||
type: DataTypes.DECIMAL(15, 2),
|
|
||||||
allowNull: true,
|
|
||||||
field: 'final_claim_amount'
|
|
||||||
},
|
|
||||||
finalClaimAmountApprovedAt: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'final_claim_amount_approved_at'
|
|
||||||
},
|
|
||||||
finalClaimAmountApprovedBy: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'final_claim_amount_approved_by',
|
|
||||||
references: {
|
|
||||||
model: 'users',
|
|
||||||
key: 'user_id'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
creditNoteAmount: {
|
|
||||||
type: DataTypes.DECIMAL(15, 2),
|
|
||||||
allowNull: true,
|
|
||||||
field: 'credit_note_amount'
|
|
||||||
},
|
|
||||||
creditNoteIssuedAt: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'credit_note_issued_at'
|
|
||||||
},
|
|
||||||
budgetStatus: {
|
|
||||||
type: DataTypes.ENUM('DRAFT', 'PROPOSED', 'APPROVED', 'BLOCKED', 'CLOSED', 'SETTLED'),
|
|
||||||
defaultValue: 'DRAFT',
|
|
||||||
allowNull: false,
|
|
||||||
field: 'budget_status'
|
|
||||||
},
|
|
||||||
currency: {
|
|
||||||
type: DataTypes.STRING(3),
|
|
||||||
defaultValue: 'INR',
|
|
||||||
allowNull: false
|
|
||||||
},
|
|
||||||
varianceAmount: {
|
|
||||||
type: DataTypes.DECIMAL(15, 2),
|
|
||||||
allowNull: true,
|
|
||||||
field: 'variance_amount'
|
|
||||||
},
|
|
||||||
variancePercentage: {
|
|
||||||
type: DataTypes.DECIMAL(5, 2),
|
|
||||||
allowNull: true,
|
|
||||||
field: 'variance_percentage'
|
|
||||||
},
|
|
||||||
lastModifiedBy: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'last_modified_by',
|
|
||||||
references: {
|
|
||||||
model: 'users',
|
|
||||||
key: 'user_id'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
lastModifiedAt: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'last_modified_at'
|
|
||||||
},
|
|
||||||
modificationReason: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'modification_reason'
|
|
||||||
},
|
|
||||||
createdAt: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW,
|
|
||||||
field: 'created_at'
|
|
||||||
},
|
|
||||||
updatedAt: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW,
|
|
||||||
field: 'updated_at'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
sequelize,
|
|
||||||
modelName: 'ClaimBudgetTracking',
|
|
||||||
tableName: 'claim_budget_tracking',
|
|
||||||
timestamps: true,
|
|
||||||
createdAt: 'created_at',
|
|
||||||
updatedAt: 'updated_at',
|
|
||||||
indexes: [
|
|
||||||
{
|
|
||||||
fields: ['request_id'],
|
|
||||||
unique: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fields: ['budget_status']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fields: ['approved_by']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fields: ['final_claim_amount_approved_by']
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Associations
|
|
||||||
ClaimBudgetTracking.belongsTo(WorkflowRequest, {
|
|
||||||
as: 'request',
|
|
||||||
foreignKey: 'requestId',
|
|
||||||
targetKey: 'requestId'
|
|
||||||
});
|
|
||||||
|
|
||||||
ClaimBudgetTracking.belongsTo(User, {
|
|
||||||
as: 'approver',
|
|
||||||
foreignKey: 'approvedBy',
|
|
||||||
targetKey: 'userId'
|
|
||||||
});
|
|
||||||
|
|
||||||
ClaimBudgetTracking.belongsTo(User, {
|
|
||||||
as: 'finalApprover',
|
|
||||||
foreignKey: 'finalClaimAmountApprovedBy',
|
|
||||||
targetKey: 'userId'
|
|
||||||
});
|
|
||||||
|
|
||||||
ClaimBudgetTracking.belongsTo(User, {
|
|
||||||
as: 'lastModifier',
|
|
||||||
foreignKey: 'lastModifiedBy',
|
|
||||||
targetKey: 'userId'
|
|
||||||
});
|
|
||||||
|
|
||||||
export { ClaimBudgetTracking };
|
|
||||||
|
|
||||||
@ -1,193 +0,0 @@
|
|||||||
import { DataTypes, Model, Optional } from 'sequelize';
|
|
||||||
import { sequelize } from '@config/database';
|
|
||||||
import { WorkflowRequest } from './WorkflowRequest';
|
|
||||||
import { ClaimInvoice } from './ClaimInvoice';
|
|
||||||
|
|
||||||
interface ClaimCreditNoteAttributes {
|
|
||||||
creditNoteId: string;
|
|
||||||
requestId: string;
|
|
||||||
invoiceId?: string;
|
|
||||||
creditNoteNumber?: string;
|
|
||||||
creditNoteDate?: Date;
|
|
||||||
creditNoteAmount?: number;
|
|
||||||
sapDocumentNumber?: string;
|
|
||||||
creditNoteFilePath?: string;
|
|
||||||
status?: string;
|
|
||||||
errorMessage?: string;
|
|
||||||
confirmedBy?: string;
|
|
||||||
confirmedAt?: Date;
|
|
||||||
reason?: string;
|
|
||||||
description?: string;
|
|
||||||
createdAt: Date;
|
|
||||||
updatedAt: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ClaimCreditNoteCreationAttributes extends Optional<ClaimCreditNoteAttributes, 'creditNoteId' | 'invoiceId' | 'creditNoteNumber' | 'creditNoteDate' | 'creditNoteAmount' | 'sapDocumentNumber' | 'creditNoteFilePath' | 'status' | 'errorMessage' | 'confirmedBy' | 'confirmedAt' | 'reason' | 'description' | 'createdAt' | 'updatedAt'> {}
|
|
||||||
|
|
||||||
class ClaimCreditNote extends Model<ClaimCreditNoteAttributes, ClaimCreditNoteCreationAttributes> implements ClaimCreditNoteAttributes {
|
|
||||||
public creditNoteId!: string;
|
|
||||||
public requestId!: string;
|
|
||||||
public invoiceId?: string;
|
|
||||||
public creditNoteNumber?: string;
|
|
||||||
public creditNoteDate?: Date;
|
|
||||||
public creditNoteAmount?: number;
|
|
||||||
public sapDocumentNumber?: string;
|
|
||||||
public creditNoteFilePath?: string;
|
|
||||||
public status?: string;
|
|
||||||
public errorMessage?: string;
|
|
||||||
public confirmedBy?: string;
|
|
||||||
public confirmedAt?: Date;
|
|
||||||
public reason?: string;
|
|
||||||
public description?: string;
|
|
||||||
public createdAt!: Date;
|
|
||||||
public updatedAt!: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
ClaimCreditNote.init(
|
|
||||||
{
|
|
||||||
creditNoteId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
defaultValue: DataTypes.UUIDV4,
|
|
||||||
primaryKey: true,
|
|
||||||
field: 'credit_note_id',
|
|
||||||
},
|
|
||||||
requestId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
unique: true,
|
|
||||||
field: 'request_id',
|
|
||||||
references: {
|
|
||||||
model: 'workflow_requests',
|
|
||||||
key: 'request_id',
|
|
||||||
},
|
|
||||||
onDelete: 'CASCADE',
|
|
||||||
onUpdate: 'CASCADE',
|
|
||||||
},
|
|
||||||
invoiceId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'invoice_id',
|
|
||||||
references: {
|
|
||||||
model: 'claim_invoices',
|
|
||||||
key: 'invoice_id',
|
|
||||||
},
|
|
||||||
onDelete: 'SET NULL',
|
|
||||||
onUpdate: 'CASCADE',
|
|
||||||
},
|
|
||||||
creditNoteNumber: {
|
|
||||||
type: DataTypes.STRING(100),
|
|
||||||
allowNull: true,
|
|
||||||
field: 'credit_note_number',
|
|
||||||
},
|
|
||||||
creditNoteDate: {
|
|
||||||
type: DataTypes.DATEONLY,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'credit_note_date',
|
|
||||||
},
|
|
||||||
creditNoteAmount: {
|
|
||||||
type: DataTypes.DECIMAL(15, 2),
|
|
||||||
allowNull: true,
|
|
||||||
field: 'credit_amount',
|
|
||||||
},
|
|
||||||
sapDocumentNumber: {
|
|
||||||
type: DataTypes.STRING(100),
|
|
||||||
allowNull: true,
|
|
||||||
field: 'sap_document_number',
|
|
||||||
},
|
|
||||||
creditNoteFilePath: {
|
|
||||||
type: DataTypes.STRING(500),
|
|
||||||
allowNull: true,
|
|
||||||
field: 'credit_note_file_path',
|
|
||||||
},
|
|
||||||
status: {
|
|
||||||
type: DataTypes.STRING(50),
|
|
||||||
allowNull: true,
|
|
||||||
field: 'confirmation_status',
|
|
||||||
},
|
|
||||||
errorMessage: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'error_message',
|
|
||||||
},
|
|
||||||
confirmedBy: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'confirmed_by',
|
|
||||||
references: {
|
|
||||||
model: 'users',
|
|
||||||
key: 'user_id',
|
|
||||||
},
|
|
||||||
onDelete: 'SET NULL',
|
|
||||||
onUpdate: 'CASCADE',
|
|
||||||
},
|
|
||||||
confirmedAt: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'confirmed_at',
|
|
||||||
},
|
|
||||||
reason: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'reason',
|
|
||||||
},
|
|
||||||
description: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'description',
|
|
||||||
},
|
|
||||||
createdAt: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW,
|
|
||||||
field: 'created_at',
|
|
||||||
},
|
|
||||||
updatedAt: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW,
|
|
||||||
field: 'updated_at',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
sequelize,
|
|
||||||
modelName: 'ClaimCreditNote',
|
|
||||||
tableName: 'claim_credit_notes',
|
|
||||||
timestamps: true,
|
|
||||||
createdAt: 'created_at',
|
|
||||||
updatedAt: 'updated_at',
|
|
||||||
indexes: [
|
|
||||||
{ unique: true, fields: ['request_id'], name: 'idx_claim_credit_notes_request_id' },
|
|
||||||
{ fields: ['invoice_id'], name: 'idx_claim_credit_notes_invoice_id' },
|
|
||||||
{ fields: ['credit_note_number'], name: 'idx_claim_credit_notes_number' },
|
|
||||||
{ fields: ['sap_document_number'], name: 'idx_claim_credit_notes_sap_doc' },
|
|
||||||
{ fields: ['confirmation_status'], name: 'idx_claim_credit_notes_status' },
|
|
||||||
],
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
WorkflowRequest.hasOne(ClaimCreditNote, {
|
|
||||||
as: 'claimCreditNote',
|
|
||||||
foreignKey: 'requestId',
|
|
||||||
sourceKey: 'requestId',
|
|
||||||
});
|
|
||||||
|
|
||||||
ClaimCreditNote.belongsTo(WorkflowRequest, {
|
|
||||||
as: 'workflowRequest',
|
|
||||||
foreignKey: 'requestId',
|
|
||||||
targetKey: 'requestId',
|
|
||||||
});
|
|
||||||
|
|
||||||
ClaimCreditNote.belongsTo(ClaimInvoice, {
|
|
||||||
as: 'claimInvoice',
|
|
||||||
foreignKey: 'invoiceId',
|
|
||||||
targetKey: 'invoiceId',
|
|
||||||
});
|
|
||||||
|
|
||||||
ClaimInvoice.hasMany(ClaimCreditNote, {
|
|
||||||
as: 'creditNotes',
|
|
||||||
foreignKey: 'invoiceId',
|
|
||||||
sourceKey: 'invoiceId',
|
|
||||||
});
|
|
||||||
|
|
||||||
export { ClaimCreditNote };
|
|
||||||
|
|
||||||
@ -1,149 +0,0 @@
|
|||||||
import { DataTypes, Model, Optional } from 'sequelize';
|
|
||||||
import { sequelize } from '@config/database';
|
|
||||||
import { WorkflowRequest } from './WorkflowRequest';
|
|
||||||
|
|
||||||
interface ClaimInvoiceAttributes {
|
|
||||||
invoiceId: string;
|
|
||||||
requestId: string;
|
|
||||||
invoiceNumber?: string;
|
|
||||||
invoiceDate?: Date;
|
|
||||||
amount?: number;
|
|
||||||
dmsNumber?: string;
|
|
||||||
invoiceFilePath?: string;
|
|
||||||
status?: string;
|
|
||||||
errorMessage?: string;
|
|
||||||
generatedAt?: Date;
|
|
||||||
description?: string;
|
|
||||||
createdAt: Date;
|
|
||||||
updatedAt: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ClaimInvoiceCreationAttributes extends Optional<ClaimInvoiceAttributes, 'invoiceId' | 'invoiceNumber' | 'invoiceDate' | 'amount' | 'dmsNumber' | 'invoiceFilePath' | 'status' | 'errorMessage' | 'generatedAt' | 'description' | 'createdAt' | 'updatedAt'> {}
|
|
||||||
|
|
||||||
class ClaimInvoice extends Model<ClaimInvoiceAttributes, ClaimInvoiceCreationAttributes> implements ClaimInvoiceAttributes {
|
|
||||||
public invoiceId!: string;
|
|
||||||
public requestId!: string;
|
|
||||||
public invoiceNumber?: string;
|
|
||||||
public invoiceDate?: Date;
|
|
||||||
public amount?: number;
|
|
||||||
public dmsNumber?: string;
|
|
||||||
public invoiceFilePath?: string;
|
|
||||||
public status?: string;
|
|
||||||
public errorMessage?: string;
|
|
||||||
public generatedAt?: Date;
|
|
||||||
public description?: string;
|
|
||||||
public createdAt!: Date;
|
|
||||||
public updatedAt!: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
ClaimInvoice.init(
|
|
||||||
{
|
|
||||||
invoiceId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
defaultValue: DataTypes.UUIDV4,
|
|
||||||
primaryKey: true,
|
|
||||||
field: 'invoice_id',
|
|
||||||
},
|
|
||||||
requestId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
unique: true,
|
|
||||||
field: 'request_id',
|
|
||||||
references: {
|
|
||||||
model: 'workflow_requests',
|
|
||||||
key: 'request_id',
|
|
||||||
},
|
|
||||||
onDelete: 'CASCADE',
|
|
||||||
onUpdate: 'CASCADE',
|
|
||||||
},
|
|
||||||
invoiceNumber: {
|
|
||||||
type: DataTypes.STRING(100),
|
|
||||||
allowNull: true,
|
|
||||||
field: 'invoice_number',
|
|
||||||
},
|
|
||||||
invoiceDate: {
|
|
||||||
type: DataTypes.DATEONLY,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'invoice_date',
|
|
||||||
},
|
|
||||||
amount: {
|
|
||||||
type: DataTypes.DECIMAL(15, 2),
|
|
||||||
allowNull: true,
|
|
||||||
field: 'invoice_amount',
|
|
||||||
},
|
|
||||||
dmsNumber: {
|
|
||||||
type: DataTypes.STRING(100),
|
|
||||||
allowNull: true,
|
|
||||||
field: 'dms_number',
|
|
||||||
},
|
|
||||||
invoiceFilePath: {
|
|
||||||
type: DataTypes.STRING(500),
|
|
||||||
allowNull: true,
|
|
||||||
field: 'invoice_file_path',
|
|
||||||
},
|
|
||||||
status: {
|
|
||||||
type: DataTypes.STRING(50),
|
|
||||||
allowNull: true,
|
|
||||||
field: 'generation_status',
|
|
||||||
},
|
|
||||||
errorMessage: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'error_message',
|
|
||||||
},
|
|
||||||
generatedAt: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'generated_at',
|
|
||||||
},
|
|
||||||
description: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'description',
|
|
||||||
},
|
|
||||||
createdAt: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW,
|
|
||||||
field: 'created_at',
|
|
||||||
},
|
|
||||||
updatedAt: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW,
|
|
||||||
field: 'updated_at',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
sequelize,
|
|
||||||
modelName: 'ClaimInvoice',
|
|
||||||
tableName: 'claim_invoices',
|
|
||||||
timestamps: true,
|
|
||||||
createdAt: 'created_at',
|
|
||||||
updatedAt: 'updated_at',
|
|
||||||
indexes: [
|
|
||||||
{ unique: true, fields: ['request_id'], name: 'idx_claim_invoices_request_id' },
|
|
||||||
{ fields: ['invoice_number'], name: 'idx_claim_invoices_invoice_number' },
|
|
||||||
{ fields: ['dms_number'], name: 'idx_claim_invoices_dms_number' },
|
|
||||||
{ fields: ['generation_status'], name: 'idx_claim_invoices_status' },
|
|
||||||
],
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
WorkflowRequest.hasOne(ClaimInvoice, {
|
|
||||||
as: 'claimInvoice',
|
|
||||||
foreignKey: 'requestId',
|
|
||||||
sourceKey: 'requestId',
|
|
||||||
});
|
|
||||||
|
|
||||||
ClaimInvoice.belongsTo(WorkflowRequest, {
|
|
||||||
as: 'workflowRequest',
|
|
||||||
foreignKey: 'requestId',
|
|
||||||
targetKey: 'requestId',
|
|
||||||
});
|
|
||||||
|
|
||||||
// Note: hasMany association with ClaimCreditNote is defined in ClaimCreditNote.ts
|
|
||||||
// to avoid circular dependency issues
|
|
||||||
|
|
||||||
export { ClaimInvoice };
|
|
||||||
|
|
||||||
@ -1,152 +0,0 @@
|
|||||||
import { DataTypes, Model, Optional } from 'sequelize';
|
|
||||||
import { sequelize } from '../config/database';
|
|
||||||
|
|
||||||
interface ConclusionRemarkAttributes {
|
|
||||||
conclusionId: string;
|
|
||||||
requestId: string;
|
|
||||||
aiGeneratedRemark: string | null;
|
|
||||||
aiModelUsed: string | null;
|
|
||||||
aiConfidenceScore: number | null;
|
|
||||||
finalRemark: string | null;
|
|
||||||
editedBy: string | null;
|
|
||||||
isEdited: boolean;
|
|
||||||
editCount: number;
|
|
||||||
approvalSummary: any;
|
|
||||||
documentSummary: any;
|
|
||||||
keyDiscussionPoints: string[];
|
|
||||||
generatedAt: Date | null;
|
|
||||||
finalizedAt: Date | null;
|
|
||||||
createdAt?: Date;
|
|
||||||
updatedAt?: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ConclusionRemarkCreationAttributes
|
|
||||||
extends Optional<ConclusionRemarkAttributes, 'conclusionId' | 'aiGeneratedRemark' | 'aiModelUsed' | 'aiConfidenceScore' | 'finalRemark' | 'editedBy' | 'isEdited' | 'editCount' | 'approvalSummary' | 'documentSummary' | 'keyDiscussionPoints' | 'generatedAt' | 'finalizedAt'> {}
|
|
||||||
|
|
||||||
class ConclusionRemark extends Model<ConclusionRemarkAttributes, ConclusionRemarkCreationAttributes>
|
|
||||||
implements ConclusionRemarkAttributes {
|
|
||||||
public conclusionId!: string;
|
|
||||||
public requestId!: string;
|
|
||||||
public aiGeneratedRemark!: string | null;
|
|
||||||
public aiModelUsed!: string | null;
|
|
||||||
public aiConfidenceScore!: number | null;
|
|
||||||
public finalRemark!: string | null;
|
|
||||||
public editedBy!: string | null;
|
|
||||||
public isEdited!: boolean;
|
|
||||||
public editCount!: number;
|
|
||||||
public approvalSummary!: any;
|
|
||||||
public documentSummary!: any;
|
|
||||||
public keyDiscussionPoints!: string[];
|
|
||||||
public generatedAt!: Date | null;
|
|
||||||
public finalizedAt!: Date | null;
|
|
||||||
public readonly createdAt!: Date;
|
|
||||||
public readonly updatedAt!: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
ConclusionRemark.init(
|
|
||||||
{
|
|
||||||
conclusionId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
defaultValue: DataTypes.UUIDV4,
|
|
||||||
primaryKey: true,
|
|
||||||
field: 'conclusion_id'
|
|
||||||
},
|
|
||||||
requestId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
field: 'request_id',
|
|
||||||
references: {
|
|
||||||
model: 'workflow_requests',
|
|
||||||
key: 'request_id'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
aiGeneratedRemark: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'ai_generated_remark'
|
|
||||||
},
|
|
||||||
aiModelUsed: {
|
|
||||||
type: DataTypes.STRING(100),
|
|
||||||
allowNull: true,
|
|
||||||
field: 'ai_model_used'
|
|
||||||
},
|
|
||||||
aiConfidenceScore: {
|
|
||||||
type: DataTypes.DECIMAL(5, 2),
|
|
||||||
allowNull: true,
|
|
||||||
field: 'ai_confidence_score'
|
|
||||||
},
|
|
||||||
finalRemark: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'final_remark'
|
|
||||||
},
|
|
||||||
editedBy: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'edited_by',
|
|
||||||
references: {
|
|
||||||
model: 'users',
|
|
||||||
key: 'user_id'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
isEdited: {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: false,
|
|
||||||
field: 'is_edited'
|
|
||||||
},
|
|
||||||
editCount: {
|
|
||||||
type: DataTypes.INTEGER,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: 0,
|
|
||||||
field: 'edit_count'
|
|
||||||
},
|
|
||||||
approvalSummary: {
|
|
||||||
type: DataTypes.JSONB,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'approval_summary'
|
|
||||||
},
|
|
||||||
documentSummary: {
|
|
||||||
type: DataTypes.JSONB,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'document_summary'
|
|
||||||
},
|
|
||||||
keyDiscussionPoints: {
|
|
||||||
type: DataTypes.ARRAY(DataTypes.TEXT),
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: [],
|
|
||||||
field: 'key_discussion_points'
|
|
||||||
},
|
|
||||||
generatedAt: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'generated_at'
|
|
||||||
},
|
|
||||||
finalizedAt: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'finalized_at'
|
|
||||||
},
|
|
||||||
createdAt: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW,
|
|
||||||
field: 'created_at'
|
|
||||||
},
|
|
||||||
updatedAt: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW,
|
|
||||||
field: 'updated_at'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
sequelize,
|
|
||||||
tableName: 'conclusion_remarks',
|
|
||||||
timestamps: true,
|
|
||||||
underscored: true
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export default ConclusionRemark;
|
|
||||||
|
|
||||||
@ -1,442 +0,0 @@
|
|||||||
import { DataTypes, Model, Optional } from 'sequelize';
|
|
||||||
import { sequelize } from '../config/database';
|
|
||||||
|
|
||||||
interface DealerAttributes {
|
|
||||||
dealerId: string;
|
|
||||||
salesCode?: string | null;
|
|
||||||
serviceCode?: string | null;
|
|
||||||
gearCode?: string | null;
|
|
||||||
gmaCode?: string | null;
|
|
||||||
region?: string | null;
|
|
||||||
dealership?: string | null;
|
|
||||||
state?: string | null;
|
|
||||||
district?: string | null;
|
|
||||||
city?: string | null;
|
|
||||||
location?: string | null;
|
|
||||||
cityCategoryPst?: string | null;
|
|
||||||
layoutFormat?: string | null;
|
|
||||||
tierCityCategory?: string | null;
|
|
||||||
onBoardingCharges?: string | null;
|
|
||||||
date?: string | null;
|
|
||||||
singleFormatMonthYear?: string | null;
|
|
||||||
domainId?: string | null;
|
|
||||||
replacement?: string | null;
|
|
||||||
terminationResignationStatus?: string | null;
|
|
||||||
dateOfTerminationResignation?: string | null;
|
|
||||||
lastDateOfOperations?: string | null;
|
|
||||||
oldCodes?: string | null;
|
|
||||||
branchDetails?: string | null;
|
|
||||||
dealerPrincipalName?: string | null;
|
|
||||||
dealerPrincipalEmailId?: string | null;
|
|
||||||
dpContactNumber?: string | null;
|
|
||||||
dpContacts?: string | null;
|
|
||||||
showroomAddress?: string | null;
|
|
||||||
showroomPincode?: string | null;
|
|
||||||
workshopAddress?: string | null;
|
|
||||||
workshopPincode?: string | null;
|
|
||||||
locationDistrict?: string | null;
|
|
||||||
stateWorkshop?: string | null;
|
|
||||||
noOfStudios?: number | null;
|
|
||||||
websiteUpdate?: string | null;
|
|
||||||
gst?: string | null;
|
|
||||||
pan?: string | null;
|
|
||||||
firmType?: string | null;
|
|
||||||
propManagingPartnersDirectors?: string | null;
|
|
||||||
totalPropPartnersDirectors?: string | null;
|
|
||||||
docsFolderLink?: string | null;
|
|
||||||
workshopGmaCodes?: string | null;
|
|
||||||
existingNew?: string | null;
|
|
||||||
dlrcode?: string | null;
|
|
||||||
isActive: boolean;
|
|
||||||
createdAt: Date;
|
|
||||||
updatedAt: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DealerCreationAttributes extends Optional<DealerAttributes, 'dealerId' | 'isActive' | 'createdAt' | 'updatedAt'> {}
|
|
||||||
|
|
||||||
class Dealer extends Model<DealerAttributes, DealerCreationAttributes> implements DealerAttributes {
|
|
||||||
public dealerId!: string;
|
|
||||||
public salesCode?: string | null;
|
|
||||||
public serviceCode?: string | null;
|
|
||||||
public gearCode?: string | null;
|
|
||||||
public gmaCode?: string | null;
|
|
||||||
public region?: string | null;
|
|
||||||
public dealership?: string | null;
|
|
||||||
public state?: string | null;
|
|
||||||
public district?: string | null;
|
|
||||||
public city?: string | null;
|
|
||||||
public location?: string | null;
|
|
||||||
public cityCategoryPst?: string | null;
|
|
||||||
public layoutFormat?: string | null;
|
|
||||||
public tierCityCategory?: string | null;
|
|
||||||
public onBoardingCharges?: string | null;
|
|
||||||
public date?: string | null;
|
|
||||||
public singleFormatMonthYear?: string | null;
|
|
||||||
public domainId?: string | null;
|
|
||||||
public replacement?: string | null;
|
|
||||||
public terminationResignationStatus?: string | null;
|
|
||||||
public dateOfTerminationResignation?: string | null;
|
|
||||||
public lastDateOfOperations?: string | null;
|
|
||||||
public oldCodes?: string | null;
|
|
||||||
public branchDetails?: string | null;
|
|
||||||
public dealerPrincipalName?: string | null;
|
|
||||||
public dealerPrincipalEmailId?: string | null;
|
|
||||||
public dpContactNumber?: string | null;
|
|
||||||
public dpContacts?: string | null;
|
|
||||||
public showroomAddress?: string | null;
|
|
||||||
public showroomPincode?: string | null;
|
|
||||||
public workshopAddress?: string | null;
|
|
||||||
public workshopPincode?: string | null;
|
|
||||||
public locationDistrict?: string | null;
|
|
||||||
public stateWorkshop?: string | null;
|
|
||||||
public noOfStudios?: number | null;
|
|
||||||
public websiteUpdate?: string | null;
|
|
||||||
public gst?: string | null;
|
|
||||||
public pan?: string | null;
|
|
||||||
public firmType?: string | null;
|
|
||||||
public propManagingPartnersDirectors?: string | null;
|
|
||||||
public totalPropPartnersDirectors?: string | null;
|
|
||||||
public docsFolderLink?: string | null;
|
|
||||||
public workshopGmaCodes?: string | null;
|
|
||||||
public existingNew?: string | null;
|
|
||||||
public dlrcode?: string | null;
|
|
||||||
public isActive!: boolean;
|
|
||||||
public readonly createdAt!: Date;
|
|
||||||
public readonly updatedAt!: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
Dealer.init(
|
|
||||||
{
|
|
||||||
dealerId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
primaryKey: true,
|
|
||||||
defaultValue: DataTypes.UUIDV4,
|
|
||||||
field: 'dealer_id'
|
|
||||||
},
|
|
||||||
salesCode: {
|
|
||||||
type: DataTypes.STRING(50),
|
|
||||||
allowNull: true,
|
|
||||||
field: 'sales_code',
|
|
||||||
comment: 'Sales Code'
|
|
||||||
},
|
|
||||||
serviceCode: {
|
|
||||||
type: DataTypes.STRING(50),
|
|
||||||
allowNull: true,
|
|
||||||
field: 'service_code',
|
|
||||||
comment: 'Service Code'
|
|
||||||
},
|
|
||||||
gearCode: {
|
|
||||||
type: DataTypes.STRING(50),
|
|
||||||
allowNull: true,
|
|
||||||
field: 'gear_code',
|
|
||||||
comment: 'Gear Code'
|
|
||||||
},
|
|
||||||
gmaCode: {
|
|
||||||
type: DataTypes.STRING(50),
|
|
||||||
allowNull: true,
|
|
||||||
field: 'gma_code',
|
|
||||||
comment: 'GMA CODE'
|
|
||||||
},
|
|
||||||
region: {
|
|
||||||
type: DataTypes.STRING(50),
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'Region'
|
|
||||||
},
|
|
||||||
dealership: {
|
|
||||||
type: DataTypes.STRING(255),
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'Dealership name'
|
|
||||||
},
|
|
||||||
state: {
|
|
||||||
type: DataTypes.STRING(100),
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'State'
|
|
||||||
},
|
|
||||||
district: {
|
|
||||||
type: DataTypes.STRING(100),
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'District'
|
|
||||||
},
|
|
||||||
city: {
|
|
||||||
type: DataTypes.STRING(100),
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'City'
|
|
||||||
},
|
|
||||||
location: {
|
|
||||||
type: DataTypes.STRING(255),
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'Location'
|
|
||||||
},
|
|
||||||
cityCategoryPst: {
|
|
||||||
type: DataTypes.STRING(50),
|
|
||||||
allowNull: true,
|
|
||||||
field: 'city_category_pst',
|
|
||||||
comment: 'City category (PST)'
|
|
||||||
},
|
|
||||||
layoutFormat: {
|
|
||||||
type: DataTypes.STRING(50),
|
|
||||||
allowNull: true,
|
|
||||||
field: 'layout_format',
|
|
||||||
comment: 'Layout format'
|
|
||||||
},
|
|
||||||
tierCityCategory: {
|
|
||||||
type: DataTypes.STRING(100),
|
|
||||||
allowNull: true,
|
|
||||||
field: 'tier_city_category',
|
|
||||||
comment: 'TIER City Category'
|
|
||||||
},
|
|
||||||
onBoardingCharges: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'on_boarding_charges',
|
|
||||||
comment: 'On Boarding Charges (stored as text to allow text values)'
|
|
||||||
},
|
|
||||||
date: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'DATE (stored as text to avoid format validation)'
|
|
||||||
},
|
|
||||||
singleFormatMonthYear: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'single_format_month_year',
|
|
||||||
comment: 'Single Format of Month/Year (stored as text)'
|
|
||||||
},
|
|
||||||
domainId: {
|
|
||||||
type: DataTypes.STRING(255),
|
|
||||||
allowNull: true,
|
|
||||||
field: 'domain_id',
|
|
||||||
comment: 'Domain Id'
|
|
||||||
},
|
|
||||||
replacement: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'Replacement (stored as text to allow longer values)'
|
|
||||||
},
|
|
||||||
terminationResignationStatus: {
|
|
||||||
type: DataTypes.STRING(255),
|
|
||||||
allowNull: true,
|
|
||||||
field: 'termination_resignation_status',
|
|
||||||
comment: 'Termination / Resignation under Proposal or Evaluation'
|
|
||||||
},
|
|
||||||
dateOfTerminationResignation: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'date_of_termination_resignation',
|
|
||||||
comment: 'Date Of termination/ resignation (stored as text to avoid format validation)'
|
|
||||||
},
|
|
||||||
lastDateOfOperations: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'last_date_of_operations',
|
|
||||||
comment: 'Last date of operations (stored as text to avoid format validation)'
|
|
||||||
},
|
|
||||||
oldCodes: {
|
|
||||||
type: DataTypes.STRING(255),
|
|
||||||
allowNull: true,
|
|
||||||
field: 'old_codes',
|
|
||||||
comment: 'Old Codes'
|
|
||||||
},
|
|
||||||
branchDetails: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'branch_details',
|
|
||||||
comment: 'Branch Details'
|
|
||||||
},
|
|
||||||
dealerPrincipalName: {
|
|
||||||
type: DataTypes.STRING(255),
|
|
||||||
allowNull: true,
|
|
||||||
field: 'dealer_principal_name',
|
|
||||||
comment: 'Dealer Principal Name'
|
|
||||||
},
|
|
||||||
dealerPrincipalEmailId: {
|
|
||||||
type: DataTypes.STRING(255),
|
|
||||||
allowNull: true,
|
|
||||||
field: 'dealer_principal_email_id',
|
|
||||||
comment: 'Dealer Principal Email Id'
|
|
||||||
},
|
|
||||||
dpContactNumber: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'dp_contact_number',
|
|
||||||
comment: 'DP CONTACT NUMBER (stored as text to allow multiple numbers)'
|
|
||||||
},
|
|
||||||
dpContacts: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'dp_contacts',
|
|
||||||
comment: 'DP CONTACTS (stored as text to allow multiple contacts)'
|
|
||||||
},
|
|
||||||
showroomAddress: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'showroom_address',
|
|
||||||
comment: 'Showroom Address'
|
|
||||||
},
|
|
||||||
showroomPincode: {
|
|
||||||
type: DataTypes.STRING(10),
|
|
||||||
allowNull: true,
|
|
||||||
field: 'showroom_pincode',
|
|
||||||
comment: 'Showroom Pincode'
|
|
||||||
},
|
|
||||||
workshopAddress: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'workshop_address',
|
|
||||||
comment: 'Workshop Address'
|
|
||||||
},
|
|
||||||
workshopPincode: {
|
|
||||||
type: DataTypes.STRING(10),
|
|
||||||
allowNull: true,
|
|
||||||
field: 'workshop_pincode',
|
|
||||||
comment: 'Workshop Pincode'
|
|
||||||
},
|
|
||||||
locationDistrict: {
|
|
||||||
type: DataTypes.STRING(100),
|
|
||||||
allowNull: true,
|
|
||||||
field: 'location_district',
|
|
||||||
comment: 'Location / District'
|
|
||||||
},
|
|
||||||
stateWorkshop: {
|
|
||||||
type: DataTypes.STRING(100),
|
|
||||||
allowNull: true,
|
|
||||||
field: 'state_workshop',
|
|
||||||
comment: 'State (for workshop)'
|
|
||||||
},
|
|
||||||
noOfStudios: {
|
|
||||||
type: DataTypes.INTEGER,
|
|
||||||
allowNull: true,
|
|
||||||
defaultValue: 0,
|
|
||||||
field: 'no_of_studios',
|
|
||||||
comment: 'No Of Studios'
|
|
||||||
},
|
|
||||||
websiteUpdate: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'website_update',
|
|
||||||
comment: 'Website update (stored as text to allow longer values)'
|
|
||||||
},
|
|
||||||
gst: {
|
|
||||||
type: DataTypes.STRING(50),
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'GST'
|
|
||||||
},
|
|
||||||
pan: {
|
|
||||||
type: DataTypes.STRING(50),
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'PAN'
|
|
||||||
},
|
|
||||||
firmType: {
|
|
||||||
type: DataTypes.STRING(100),
|
|
||||||
allowNull: true,
|
|
||||||
field: 'firm_type',
|
|
||||||
comment: 'Firm Type'
|
|
||||||
},
|
|
||||||
propManagingPartnersDirectors: {
|
|
||||||
type: DataTypes.STRING(255),
|
|
||||||
allowNull: true,
|
|
||||||
field: 'prop_managing_partners_directors',
|
|
||||||
comment: 'Prop. / Managing Partners / Managing Directors'
|
|
||||||
},
|
|
||||||
totalPropPartnersDirectors: {
|
|
||||||
type: DataTypes.STRING(255),
|
|
||||||
allowNull: true,
|
|
||||||
field: 'total_prop_partners_directors',
|
|
||||||
comment: 'Total Prop. / Partners / Directors'
|
|
||||||
},
|
|
||||||
docsFolderLink: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'docs_folder_link',
|
|
||||||
comment: 'DOCS Folder Link'
|
|
||||||
},
|
|
||||||
workshopGmaCodes: {
|
|
||||||
type: DataTypes.STRING(255),
|
|
||||||
allowNull: true,
|
|
||||||
field: 'workshop_gma_codes',
|
|
||||||
comment: 'Workshop GMA Codes'
|
|
||||||
},
|
|
||||||
existingNew: {
|
|
||||||
type: DataTypes.STRING(50),
|
|
||||||
allowNull: true,
|
|
||||||
field: 'existing_new',
|
|
||||||
comment: 'Existing / New'
|
|
||||||
},
|
|
||||||
dlrcode: {
|
|
||||||
type: DataTypes.STRING(50),
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'dlrcode'
|
|
||||||
},
|
|
||||||
isActive: {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: true,
|
|
||||||
field: 'is_active',
|
|
||||||
comment: 'Whether the dealer is currently active'
|
|
||||||
},
|
|
||||||
createdAt: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW,
|
|
||||||
field: 'created_at'
|
|
||||||
},
|
|
||||||
updatedAt: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW,
|
|
||||||
field: 'updated_at'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
sequelize,
|
|
||||||
tableName: 'dealers',
|
|
||||||
modelName: 'Dealer',
|
|
||||||
timestamps: true,
|
|
||||||
underscored: true,
|
|
||||||
indexes: [
|
|
||||||
{
|
|
||||||
fields: ['sales_code'],
|
|
||||||
name: 'idx_dealers_sales_code'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fields: ['service_code'],
|
|
||||||
name: 'idx_dealers_service_code'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fields: ['gma_code'],
|
|
||||||
name: 'idx_dealers_gma_code'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fields: ['domain_id'],
|
|
||||||
name: 'idx_dealers_domain_id'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fields: ['region'],
|
|
||||||
name: 'idx_dealers_region'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fields: ['state'],
|
|
||||||
name: 'idx_dealers_state'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fields: ['city'],
|
|
||||||
name: 'idx_dealers_city'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fields: ['district'],
|
|
||||||
name: 'idx_dealers_district'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fields: ['dlrcode'],
|
|
||||||
name: 'idx_dealers_dlrcode'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fields: ['is_active'],
|
|
||||||
name: 'idx_dealers_is_active'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export { Dealer };
|
|
||||||
export type { DealerAttributes, DealerCreationAttributes };
|
|
||||||
@ -1,167 +0,0 @@
|
|||||||
import { DataTypes, Model, Optional } from 'sequelize';
|
|
||||||
import { sequelize } from '@config/database';
|
|
||||||
import { WorkflowRequest } from './WorkflowRequest';
|
|
||||||
|
|
||||||
interface DealerClaimDetailsAttributes {
|
|
||||||
claimId: string;
|
|
||||||
requestId: string;
|
|
||||||
activityName: string;
|
|
||||||
activityType: string;
|
|
||||||
dealerCode: string;
|
|
||||||
dealerName: string;
|
|
||||||
dealerEmail?: string;
|
|
||||||
dealerPhone?: string;
|
|
||||||
dealerAddress?: string;
|
|
||||||
activityDate?: Date;
|
|
||||||
location?: string;
|
|
||||||
periodStartDate?: Date;
|
|
||||||
periodEndDate?: Date;
|
|
||||||
createdAt: Date;
|
|
||||||
updatedAt: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DealerClaimDetailsCreationAttributes extends Optional<DealerClaimDetailsAttributes, 'claimId' | 'dealerEmail' | 'dealerPhone' | 'dealerAddress' | 'activityDate' | 'location' | 'periodStartDate' | 'periodEndDate' | 'createdAt' | 'updatedAt'> {}
|
|
||||||
|
|
||||||
class DealerClaimDetails extends Model<DealerClaimDetailsAttributes, DealerClaimDetailsCreationAttributes> implements DealerClaimDetailsAttributes {
|
|
||||||
public claimId!: string;
|
|
||||||
public requestId!: string;
|
|
||||||
public activityName!: string;
|
|
||||||
public activityType!: string;
|
|
||||||
public dealerCode!: string;
|
|
||||||
public dealerName!: string;
|
|
||||||
public dealerEmail?: string;
|
|
||||||
public dealerPhone?: string;
|
|
||||||
public dealerAddress?: string;
|
|
||||||
public activityDate?: Date;
|
|
||||||
public location?: string;
|
|
||||||
public periodStartDate?: Date;
|
|
||||||
public periodEndDate?: Date;
|
|
||||||
public createdAt!: Date;
|
|
||||||
public updatedAt!: Date;
|
|
||||||
|
|
||||||
// Associations
|
|
||||||
public workflowRequest?: WorkflowRequest;
|
|
||||||
}
|
|
||||||
|
|
||||||
DealerClaimDetails.init(
|
|
||||||
{
|
|
||||||
claimId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
defaultValue: DataTypes.UUIDV4,
|
|
||||||
primaryKey: true,
|
|
||||||
field: 'claim_id'
|
|
||||||
},
|
|
||||||
requestId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
unique: true,
|
|
||||||
field: 'request_id',
|
|
||||||
references: {
|
|
||||||
model: 'workflow_requests',
|
|
||||||
key: 'request_id'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
activityName: {
|
|
||||||
type: DataTypes.STRING(500),
|
|
||||||
allowNull: false,
|
|
||||||
field: 'activity_name'
|
|
||||||
},
|
|
||||||
activityType: {
|
|
||||||
type: DataTypes.STRING(100),
|
|
||||||
allowNull: false,
|
|
||||||
field: 'activity_type'
|
|
||||||
},
|
|
||||||
dealerCode: {
|
|
||||||
type: DataTypes.STRING(50),
|
|
||||||
allowNull: false,
|
|
||||||
field: 'dealer_code'
|
|
||||||
},
|
|
||||||
dealerName: {
|
|
||||||
type: DataTypes.STRING(200),
|
|
||||||
allowNull: false,
|
|
||||||
field: 'dealer_name'
|
|
||||||
},
|
|
||||||
dealerEmail: {
|
|
||||||
type: DataTypes.STRING(255),
|
|
||||||
allowNull: true,
|
|
||||||
field: 'dealer_email'
|
|
||||||
},
|
|
||||||
dealerPhone: {
|
|
||||||
type: DataTypes.STRING(20),
|
|
||||||
allowNull: true,
|
|
||||||
field: 'dealer_phone'
|
|
||||||
},
|
|
||||||
dealerAddress: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'dealer_address'
|
|
||||||
},
|
|
||||||
activityDate: {
|
|
||||||
type: DataTypes.DATEONLY,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'activity_date'
|
|
||||||
},
|
|
||||||
location: {
|
|
||||||
type: DataTypes.STRING(255),
|
|
||||||
allowNull: true
|
|
||||||
},
|
|
||||||
periodStartDate: {
|
|
||||||
type: DataTypes.DATEONLY,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'period_start_date'
|
|
||||||
},
|
|
||||||
periodEndDate: {
|
|
||||||
type: DataTypes.DATEONLY,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'period_end_date'
|
|
||||||
},
|
|
||||||
createdAt: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW,
|
|
||||||
field: 'created_at'
|
|
||||||
},
|
|
||||||
updatedAt: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW,
|
|
||||||
field: 'updated_at'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
sequelize,
|
|
||||||
modelName: 'DealerClaimDetails',
|
|
||||||
tableName: 'dealer_claim_details',
|
|
||||||
timestamps: true,
|
|
||||||
createdAt: 'created_at',
|
|
||||||
updatedAt: 'updated_at',
|
|
||||||
indexes: [
|
|
||||||
{
|
|
||||||
unique: true,
|
|
||||||
fields: ['request_id']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fields: ['dealer_code']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fields: ['activity_type']
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Associations
|
|
||||||
DealerClaimDetails.belongsTo(WorkflowRequest, {
|
|
||||||
as: 'workflowRequest',
|
|
||||||
foreignKey: 'requestId',
|
|
||||||
targetKey: 'requestId'
|
|
||||||
});
|
|
||||||
|
|
||||||
WorkflowRequest.hasOne(DealerClaimDetails, {
|
|
||||||
as: 'claimDetails',
|
|
||||||
foreignKey: 'requestId',
|
|
||||||
sourceKey: 'requestId'
|
|
||||||
});
|
|
||||||
|
|
||||||
export { DealerClaimDetails };
|
|
||||||
|
|
||||||
@ -1,190 +0,0 @@
|
|||||||
import { DataTypes, Model, Optional } from 'sequelize';
|
|
||||||
import { sequelize } from '@config/database';
|
|
||||||
import { WorkflowRequest } from './WorkflowRequest';
|
|
||||||
import { ApprovalLevel } from './ApprovalLevel';
|
|
||||||
import { User } from './User';
|
|
||||||
|
|
||||||
export enum SnapshotType {
|
|
||||||
PROPOSAL = 'PROPOSAL',
|
|
||||||
COMPLETION = 'COMPLETION',
|
|
||||||
INTERNAL_ORDER = 'INTERNAL_ORDER',
|
|
||||||
WORKFLOW = 'WORKFLOW',
|
|
||||||
APPROVE = 'APPROVE'
|
|
||||||
}
|
|
||||||
|
|
||||||
// Type definitions for snapshot data structures
|
|
||||||
export interface ProposalSnapshotData {
|
|
||||||
documentUrl?: string;
|
|
||||||
totalBudget?: number;
|
|
||||||
comments?: string;
|
|
||||||
expectedCompletionDate?: string;
|
|
||||||
costItems?: Array<{
|
|
||||||
description: string;
|
|
||||||
amount: number;
|
|
||||||
order: number;
|
|
||||||
}>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CompletionSnapshotData {
|
|
||||||
documentUrl?: string;
|
|
||||||
totalExpenses?: number;
|
|
||||||
comments?: string;
|
|
||||||
expenses?: Array<{
|
|
||||||
description: string;
|
|
||||||
amount: number;
|
|
||||||
}>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IOSnapshotData {
|
|
||||||
ioNumber?: string;
|
|
||||||
blockedAmount?: number;
|
|
||||||
availableBalance?: number;
|
|
||||||
remainingBalance?: number;
|
|
||||||
sapDocumentNumber?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface WorkflowSnapshotData {
|
|
||||||
status?: string;
|
|
||||||
currentLevel?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ApprovalSnapshotData {
|
|
||||||
action: 'APPROVE' | 'REJECT';
|
|
||||||
comments?: string;
|
|
||||||
rejectionReason?: string;
|
|
||||||
approverName?: string;
|
|
||||||
approverEmail?: string;
|
|
||||||
levelName?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DealerClaimHistoryAttributes {
|
|
||||||
historyId: string;
|
|
||||||
requestId: string;
|
|
||||||
approvalLevelId?: string;
|
|
||||||
levelNumber?: number;
|
|
||||||
levelName?: string;
|
|
||||||
version: number;
|
|
||||||
snapshotType: SnapshotType;
|
|
||||||
snapshotData: ProposalSnapshotData | CompletionSnapshotData | IOSnapshotData | WorkflowSnapshotData | ApprovalSnapshotData | any;
|
|
||||||
changeReason?: string;
|
|
||||||
changedBy: string;
|
|
||||||
createdAt: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DealerClaimHistoryCreationAttributes extends Optional<DealerClaimHistoryAttributes, 'historyId' | 'approvalLevelId' | 'levelNumber' | 'levelName' | 'changeReason' | 'createdAt'> { }
|
|
||||||
|
|
||||||
class DealerClaimHistory extends Model<DealerClaimHistoryAttributes, DealerClaimHistoryCreationAttributes> implements DealerClaimHistoryAttributes {
|
|
||||||
public historyId!: string;
|
|
||||||
public requestId!: string;
|
|
||||||
public approvalLevelId?: string;
|
|
||||||
public levelNumber?: number;
|
|
||||||
public version!: number;
|
|
||||||
public snapshotType!: SnapshotType;
|
|
||||||
public snapshotData!: ProposalSnapshotData | CompletionSnapshotData | IOSnapshotData | WorkflowSnapshotData | any;
|
|
||||||
public changeReason?: string;
|
|
||||||
public changedBy!: string;
|
|
||||||
public createdAt!: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
DealerClaimHistory.init(
|
|
||||||
{
|
|
||||||
historyId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
defaultValue: DataTypes.UUIDV4,
|
|
||||||
primaryKey: true,
|
|
||||||
field: 'history_id'
|
|
||||||
},
|
|
||||||
requestId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
field: 'request_id',
|
|
||||||
references: {
|
|
||||||
model: 'workflow_requests',
|
|
||||||
key: 'request_id'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
approvalLevelId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'approval_level_id',
|
|
||||||
references: {
|
|
||||||
model: 'approval_levels',
|
|
||||||
key: 'level_id'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
levelNumber: {
|
|
||||||
type: DataTypes.INTEGER,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'level_number'
|
|
||||||
},
|
|
||||||
levelName: {
|
|
||||||
type: DataTypes.STRING(255),
|
|
||||||
allowNull: true,
|
|
||||||
field: 'level_name'
|
|
||||||
},
|
|
||||||
version: {
|
|
||||||
type: DataTypes.INTEGER,
|
|
||||||
allowNull: false
|
|
||||||
},
|
|
||||||
snapshotType: {
|
|
||||||
type: DataTypes.ENUM('PROPOSAL', 'COMPLETION', 'INTERNAL_ORDER', 'WORKFLOW', 'APPROVE'),
|
|
||||||
allowNull: false,
|
|
||||||
field: 'snapshot_type'
|
|
||||||
},
|
|
||||||
snapshotData: {
|
|
||||||
type: DataTypes.JSONB,
|
|
||||||
allowNull: false,
|
|
||||||
field: 'snapshot_data'
|
|
||||||
},
|
|
||||||
changeReason: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'change_reason'
|
|
||||||
},
|
|
||||||
changedBy: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
field: 'changed_by',
|
|
||||||
references: {
|
|
||||||
model: 'users',
|
|
||||||
key: 'user_id'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
createdAt: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW,
|
|
||||||
field: 'created_at'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
sequelize,
|
|
||||||
modelName: 'DealerClaimHistory',
|
|
||||||
tableName: 'dealer_claim_history',
|
|
||||||
timestamps: false,
|
|
||||||
indexes: [
|
|
||||||
{
|
|
||||||
fields: ['request_id', 'level_number', 'version'],
|
|
||||||
name: 'idx_history_request_level_version'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fields: ['approval_level_id', 'version'],
|
|
||||||
name: 'idx_history_level_version'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fields: ['request_id', 'snapshot_type'],
|
|
||||||
name: 'idx_history_request_type'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fields: ['snapshot_type', 'level_number'],
|
|
||||||
name: 'idx_history_type_level'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
DealerClaimHistory.belongsTo(WorkflowRequest, { foreignKey: 'requestId' });
|
|
||||||
DealerClaimHistory.belongsTo(ApprovalLevel, { foreignKey: 'approvalLevelId' });
|
|
||||||
DealerClaimHistory.belongsTo(User, { as: 'changer', foreignKey: 'changedBy' });
|
|
||||||
|
|
||||||
export { DealerClaimHistory };
|
|
||||||
@ -1,111 +0,0 @@
|
|||||||
import { DataTypes, Model, Optional } from 'sequelize';
|
|
||||||
import { sequelize } from '@config/database';
|
|
||||||
import { WorkflowRequest } from './WorkflowRequest';
|
|
||||||
|
|
||||||
interface DealerCompletionDetailsAttributes {
|
|
||||||
completionId: string;
|
|
||||||
requestId: string;
|
|
||||||
activityCompletionDate: Date;
|
|
||||||
numberOfParticipants?: number;
|
|
||||||
totalClosedExpenses?: number;
|
|
||||||
submittedAt?: Date;
|
|
||||||
createdAt: Date;
|
|
||||||
updatedAt: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DealerCompletionDetailsCreationAttributes extends Optional<DealerCompletionDetailsAttributes, 'completionId' | 'numberOfParticipants' | 'totalClosedExpenses' | 'submittedAt' | 'createdAt' | 'updatedAt'> {}
|
|
||||||
|
|
||||||
class DealerCompletionDetails extends Model<DealerCompletionDetailsAttributes, DealerCompletionDetailsCreationAttributes> implements DealerCompletionDetailsAttributes {
|
|
||||||
public completionId!: string;
|
|
||||||
public requestId!: string;
|
|
||||||
public activityCompletionDate!: Date;
|
|
||||||
public numberOfParticipants?: number;
|
|
||||||
public totalClosedExpenses?: number;
|
|
||||||
public submittedAt?: Date;
|
|
||||||
public createdAt!: Date;
|
|
||||||
public updatedAt!: Date;
|
|
||||||
|
|
||||||
public workflowRequest?: WorkflowRequest;
|
|
||||||
}
|
|
||||||
|
|
||||||
DealerCompletionDetails.init(
|
|
||||||
{
|
|
||||||
completionId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
defaultValue: DataTypes.UUIDV4,
|
|
||||||
primaryKey: true,
|
|
||||||
field: 'completion_id'
|
|
||||||
},
|
|
||||||
requestId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
unique: true,
|
|
||||||
field: 'request_id',
|
|
||||||
references: {
|
|
||||||
model: 'workflow_requests',
|
|
||||||
key: 'request_id'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
activityCompletionDate: {
|
|
||||||
type: DataTypes.DATEONLY,
|
|
||||||
allowNull: false,
|
|
||||||
field: 'activity_completion_date'
|
|
||||||
},
|
|
||||||
numberOfParticipants: {
|
|
||||||
type: DataTypes.INTEGER,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'number_of_participants'
|
|
||||||
},
|
|
||||||
totalClosedExpenses: {
|
|
||||||
type: DataTypes.DECIMAL(15, 2),
|
|
||||||
allowNull: true,
|
|
||||||
field: 'total_closed_expenses'
|
|
||||||
},
|
|
||||||
submittedAt: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'submitted_at'
|
|
||||||
},
|
|
||||||
createdAt: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW,
|
|
||||||
field: 'created_at'
|
|
||||||
},
|
|
||||||
updatedAt: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW,
|
|
||||||
field: 'updated_at'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
sequelize,
|
|
||||||
modelName: 'DealerCompletionDetails',
|
|
||||||
tableName: 'dealer_completion_details',
|
|
||||||
timestamps: true,
|
|
||||||
createdAt: 'created_at',
|
|
||||||
updatedAt: 'updated_at',
|
|
||||||
indexes: [
|
|
||||||
{
|
|
||||||
unique: true,
|
|
||||||
fields: ['request_id']
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
DealerCompletionDetails.belongsTo(WorkflowRequest, {
|
|
||||||
as: 'workflowRequest',
|
|
||||||
foreignKey: 'requestId',
|
|
||||||
targetKey: 'requestId'
|
|
||||||
});
|
|
||||||
|
|
||||||
WorkflowRequest.hasOne(DealerCompletionDetails, {
|
|
||||||
as: 'completionDetails',
|
|
||||||
foreignKey: 'requestId',
|
|
||||||
sourceKey: 'requestId'
|
|
||||||
});
|
|
||||||
|
|
||||||
export { DealerCompletionDetails };
|
|
||||||
|
|
||||||
@ -1,118 +0,0 @@
|
|||||||
import { DataTypes, Model, Optional } from 'sequelize';
|
|
||||||
import { sequelize } from '@config/database';
|
|
||||||
import { WorkflowRequest } from './WorkflowRequest';
|
|
||||||
import { DealerCompletionDetails } from './DealerCompletionDetails';
|
|
||||||
|
|
||||||
interface DealerCompletionExpenseAttributes {
|
|
||||||
expenseId: string;
|
|
||||||
requestId: string;
|
|
||||||
completionId?: string | null;
|
|
||||||
description: string;
|
|
||||||
amount: number;
|
|
||||||
createdAt: Date;
|
|
||||||
updatedAt: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DealerCompletionExpenseCreationAttributes extends Optional<DealerCompletionExpenseAttributes, 'expenseId' | 'completionId' | 'createdAt' | 'updatedAt'> {}
|
|
||||||
|
|
||||||
class DealerCompletionExpense extends Model<DealerCompletionExpenseAttributes, DealerCompletionExpenseCreationAttributes> implements DealerCompletionExpenseAttributes {
|
|
||||||
public expenseId!: string;
|
|
||||||
public requestId!: string;
|
|
||||||
public completionId?: string | null;
|
|
||||||
public description!: string;
|
|
||||||
public amount!: number;
|
|
||||||
public createdAt!: Date;
|
|
||||||
public updatedAt!: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
DealerCompletionExpense.init(
|
|
||||||
{
|
|
||||||
expenseId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
defaultValue: DataTypes.UUIDV4,
|
|
||||||
primaryKey: true,
|
|
||||||
field: 'expense_id',
|
|
||||||
},
|
|
||||||
requestId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
field: 'request_id',
|
|
||||||
references: {
|
|
||||||
model: 'workflow_requests',
|
|
||||||
key: 'request_id',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
completionId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'completion_id',
|
|
||||||
references: {
|
|
||||||
model: 'dealer_completion_details',
|
|
||||||
key: 'completion_id',
|
|
||||||
},
|
|
||||||
onDelete: 'CASCADE',
|
|
||||||
onUpdate: 'CASCADE',
|
|
||||||
},
|
|
||||||
description: {
|
|
||||||
type: DataTypes.STRING(500),
|
|
||||||
allowNull: false,
|
|
||||||
field: 'description',
|
|
||||||
},
|
|
||||||
amount: {
|
|
||||||
type: DataTypes.DECIMAL(15, 2),
|
|
||||||
allowNull: false,
|
|
||||||
field: 'amount',
|
|
||||||
},
|
|
||||||
createdAt: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW,
|
|
||||||
field: 'created_at',
|
|
||||||
},
|
|
||||||
updatedAt: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW,
|
|
||||||
field: 'updated_at',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
sequelize,
|
|
||||||
modelName: 'DealerCompletionExpense',
|
|
||||||
tableName: 'dealer_completion_expenses',
|
|
||||||
timestamps: true,
|
|
||||||
createdAt: 'created_at',
|
|
||||||
updatedAt: 'updated_at',
|
|
||||||
indexes: [
|
|
||||||
{ fields: ['request_id'], name: 'idx_dealer_completion_expenses_request_id' },
|
|
||||||
{ fields: ['completion_id'], name: 'idx_dealer_completion_expenses_completion_id' },
|
|
||||||
],
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
WorkflowRequest.hasMany(DealerCompletionExpense, {
|
|
||||||
as: 'completionExpenses',
|
|
||||||
foreignKey: 'requestId',
|
|
||||||
sourceKey: 'requestId',
|
|
||||||
});
|
|
||||||
|
|
||||||
DealerCompletionExpense.belongsTo(WorkflowRequest, {
|
|
||||||
as: 'workflowRequest',
|
|
||||||
foreignKey: 'requestId',
|
|
||||||
targetKey: 'requestId',
|
|
||||||
});
|
|
||||||
|
|
||||||
DealerCompletionDetails.hasMany(DealerCompletionExpense, {
|
|
||||||
as: 'expenses',
|
|
||||||
foreignKey: 'completionId',
|
|
||||||
sourceKey: 'completionId',
|
|
||||||
});
|
|
||||||
|
|
||||||
DealerCompletionExpense.belongsTo(DealerCompletionDetails, {
|
|
||||||
as: 'completion',
|
|
||||||
foreignKey: 'completionId',
|
|
||||||
targetKey: 'completionId',
|
|
||||||
});
|
|
||||||
|
|
||||||
export { DealerCompletionExpense };
|
|
||||||
|
|
||||||
@ -1,123 +0,0 @@
|
|||||||
import { DataTypes, Model, Optional } from 'sequelize';
|
|
||||||
import { sequelize } from '@config/database';
|
|
||||||
import { DealerProposalDetails } from './DealerProposalDetails';
|
|
||||||
import { WorkflowRequest } from './WorkflowRequest';
|
|
||||||
|
|
||||||
interface DealerProposalCostItemAttributes {
|
|
||||||
costItemId: string;
|
|
||||||
proposalId: string;
|
|
||||||
requestId: string;
|
|
||||||
itemDescription: string;
|
|
||||||
amount: number;
|
|
||||||
itemOrder: number;
|
|
||||||
createdAt: Date;
|
|
||||||
updatedAt: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DealerProposalCostItemCreationAttributes extends Optional<DealerProposalCostItemAttributes, 'costItemId' | 'itemOrder' | 'createdAt' | 'updatedAt'> {}
|
|
||||||
|
|
||||||
class DealerProposalCostItem extends Model<DealerProposalCostItemAttributes, DealerProposalCostItemCreationAttributes> implements DealerProposalCostItemAttributes {
|
|
||||||
public costItemId!: string;
|
|
||||||
public proposalId!: string;
|
|
||||||
public requestId!: string;
|
|
||||||
public itemDescription!: string;
|
|
||||||
public amount!: number;
|
|
||||||
public itemOrder!: number;
|
|
||||||
public createdAt!: Date;
|
|
||||||
public updatedAt!: Date;
|
|
||||||
|
|
||||||
// Associations
|
|
||||||
public proposal?: DealerProposalDetails;
|
|
||||||
public workflowRequest?: WorkflowRequest;
|
|
||||||
}
|
|
||||||
|
|
||||||
DealerProposalCostItem.init(
|
|
||||||
{
|
|
||||||
costItemId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
defaultValue: DataTypes.UUIDV4,
|
|
||||||
primaryKey: true,
|
|
||||||
field: 'cost_item_id'
|
|
||||||
},
|
|
||||||
proposalId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
field: 'proposal_id',
|
|
||||||
references: {
|
|
||||||
model: 'dealer_proposal_details',
|
|
||||||
key: 'proposal_id'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
requestId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
field: 'request_id',
|
|
||||||
references: {
|
|
||||||
model: 'workflow_requests',
|
|
||||||
key: 'request_id'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
itemDescription: {
|
|
||||||
type: DataTypes.STRING(500),
|
|
||||||
allowNull: false,
|
|
||||||
field: 'item_description'
|
|
||||||
},
|
|
||||||
amount: {
|
|
||||||
type: DataTypes.DECIMAL(15, 2),
|
|
||||||
allowNull: false
|
|
||||||
},
|
|
||||||
itemOrder: {
|
|
||||||
type: DataTypes.INTEGER,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: 0,
|
|
||||||
field: 'item_order'
|
|
||||||
},
|
|
||||||
createdAt: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW,
|
|
||||||
field: 'created_at'
|
|
||||||
},
|
|
||||||
updatedAt: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW,
|
|
||||||
field: 'updated_at'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
sequelize,
|
|
||||||
modelName: 'DealerProposalCostItem',
|
|
||||||
tableName: 'dealer_proposal_cost_items',
|
|
||||||
timestamps: true,
|
|
||||||
createdAt: 'created_at',
|
|
||||||
updatedAt: 'updated_at',
|
|
||||||
indexes: [
|
|
||||||
{ fields: ['proposal_id'], name: 'idx_proposal_cost_items_proposal_id' },
|
|
||||||
{ fields: ['request_id'], name: 'idx_proposal_cost_items_request_id' },
|
|
||||||
{ fields: ['proposal_id', 'item_order'], name: 'idx_proposal_cost_items_proposal_order' }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Associations
|
|
||||||
DealerProposalCostItem.belongsTo(DealerProposalDetails, {
|
|
||||||
as: 'proposal',
|
|
||||||
foreignKey: 'proposalId',
|
|
||||||
targetKey: 'proposalId'
|
|
||||||
});
|
|
||||||
|
|
||||||
DealerProposalCostItem.belongsTo(WorkflowRequest, {
|
|
||||||
as: 'workflowRequest',
|
|
||||||
foreignKey: 'requestId',
|
|
||||||
targetKey: 'requestId'
|
|
||||||
});
|
|
||||||
|
|
||||||
DealerProposalDetails.hasMany(DealerProposalCostItem, {
|
|
||||||
as: 'costItems',
|
|
||||||
foreignKey: 'proposalId',
|
|
||||||
sourceKey: 'proposalId'
|
|
||||||
});
|
|
||||||
|
|
||||||
export { DealerProposalCostItem };
|
|
||||||
|
|
||||||
@ -1,142 +0,0 @@
|
|||||||
import { DataTypes, Model, Optional } from 'sequelize';
|
|
||||||
import { sequelize } from '@config/database';
|
|
||||||
import { WorkflowRequest } from './WorkflowRequest';
|
|
||||||
|
|
||||||
interface DealerProposalDetailsAttributes {
|
|
||||||
proposalId: string;
|
|
||||||
requestId: string;
|
|
||||||
proposalDocumentPath?: string;
|
|
||||||
proposalDocumentUrl?: string;
|
|
||||||
// costBreakup removed - now using dealer_proposal_cost_items table
|
|
||||||
totalEstimatedBudget?: number;
|
|
||||||
timelineMode?: 'date' | 'days';
|
|
||||||
expectedCompletionDate?: Date;
|
|
||||||
expectedCompletionDays?: number;
|
|
||||||
dealerComments?: string;
|
|
||||||
submittedAt?: Date;
|
|
||||||
createdAt: Date;
|
|
||||||
updatedAt: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DealerProposalDetailsCreationAttributes extends Optional<DealerProposalDetailsAttributes, 'proposalId' | 'proposalDocumentPath' | 'proposalDocumentUrl' | 'totalEstimatedBudget' | 'timelineMode' | 'expectedCompletionDate' | 'expectedCompletionDays' | 'dealerComments' | 'submittedAt' | 'createdAt' | 'updatedAt'> {}
|
|
||||||
|
|
||||||
class DealerProposalDetails extends Model<DealerProposalDetailsAttributes, DealerProposalDetailsCreationAttributes> implements DealerProposalDetailsAttributes {
|
|
||||||
public proposalId!: string;
|
|
||||||
public requestId!: string;
|
|
||||||
public proposalDocumentPath?: string;
|
|
||||||
public proposalDocumentUrl?: string;
|
|
||||||
// costBreakup removed - now using dealer_proposal_cost_items table
|
|
||||||
public totalEstimatedBudget?: number;
|
|
||||||
public timelineMode?: 'date' | 'days';
|
|
||||||
public expectedCompletionDate?: Date;
|
|
||||||
public expectedCompletionDays?: number;
|
|
||||||
public dealerComments?: string;
|
|
||||||
public submittedAt?: Date;
|
|
||||||
public createdAt!: Date;
|
|
||||||
public updatedAt!: Date;
|
|
||||||
|
|
||||||
public workflowRequest?: WorkflowRequest;
|
|
||||||
}
|
|
||||||
|
|
||||||
DealerProposalDetails.init(
|
|
||||||
{
|
|
||||||
proposalId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
defaultValue: DataTypes.UUIDV4,
|
|
||||||
primaryKey: true,
|
|
||||||
field: 'proposal_id'
|
|
||||||
},
|
|
||||||
requestId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
unique: true,
|
|
||||||
field: 'request_id',
|
|
||||||
references: {
|
|
||||||
model: 'workflow_requests',
|
|
||||||
key: 'request_id'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
proposalDocumentPath: {
|
|
||||||
type: DataTypes.STRING(500),
|
|
||||||
allowNull: true,
|
|
||||||
field: 'proposal_document_path'
|
|
||||||
},
|
|
||||||
proposalDocumentUrl: {
|
|
||||||
type: DataTypes.STRING(500),
|
|
||||||
allowNull: true,
|
|
||||||
field: 'proposal_document_url'
|
|
||||||
},
|
|
||||||
// costBreakup field removed - now using dealer_proposal_cost_items table
|
|
||||||
totalEstimatedBudget: {
|
|
||||||
type: DataTypes.DECIMAL(15, 2),
|
|
||||||
allowNull: true,
|
|
||||||
field: 'total_estimated_budget'
|
|
||||||
},
|
|
||||||
timelineMode: {
|
|
||||||
type: DataTypes.STRING(10),
|
|
||||||
allowNull: true,
|
|
||||||
field: 'timeline_mode'
|
|
||||||
},
|
|
||||||
expectedCompletionDate: {
|
|
||||||
type: DataTypes.DATEONLY,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'expected_completion_date'
|
|
||||||
},
|
|
||||||
expectedCompletionDays: {
|
|
||||||
type: DataTypes.INTEGER,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'expected_completion_days'
|
|
||||||
},
|
|
||||||
dealerComments: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'dealer_comments'
|
|
||||||
},
|
|
||||||
submittedAt: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'submitted_at'
|
|
||||||
},
|
|
||||||
createdAt: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW,
|
|
||||||
field: 'created_at'
|
|
||||||
},
|
|
||||||
updatedAt: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW,
|
|
||||||
field: 'updated_at'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
sequelize,
|
|
||||||
modelName: 'DealerProposalDetails',
|
|
||||||
tableName: 'dealer_proposal_details',
|
|
||||||
timestamps: true,
|
|
||||||
createdAt: 'created_at',
|
|
||||||
updatedAt: 'updated_at',
|
|
||||||
indexes: [
|
|
||||||
{
|
|
||||||
unique: true,
|
|
||||||
fields: ['request_id']
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
DealerProposalDetails.belongsTo(WorkflowRequest, {
|
|
||||||
as: 'workflowRequest',
|
|
||||||
foreignKey: 'requestId',
|
|
||||||
targetKey: 'requestId'
|
|
||||||
});
|
|
||||||
|
|
||||||
WorkflowRequest.hasOne(DealerProposalDetails, {
|
|
||||||
as: 'proposalDetails',
|
|
||||||
foreignKey: 'requestId',
|
|
||||||
sourceKey: 'requestId'
|
|
||||||
});
|
|
||||||
|
|
||||||
export { DealerProposalDetails };
|
|
||||||
|
|
||||||
@ -1,217 +0,0 @@
|
|||||||
import { DataTypes, Model, Optional } from 'sequelize';
|
|
||||||
import { sequelize } from '@config/database';
|
|
||||||
import { User } from './User';
|
|
||||||
import { WorkflowRequest } from './WorkflowRequest';
|
|
||||||
|
|
||||||
interface DocumentAttributes {
|
|
||||||
documentId: string;
|
|
||||||
requestId: string;
|
|
||||||
uploadedBy: string;
|
|
||||||
fileName: string;
|
|
||||||
originalFileName: string;
|
|
||||||
fileType: string;
|
|
||||||
fileExtension: string;
|
|
||||||
fileSize: number;
|
|
||||||
filePath: string;
|
|
||||||
storageUrl?: string;
|
|
||||||
mimeType: string;
|
|
||||||
checksum: string;
|
|
||||||
isGoogleDoc: boolean;
|
|
||||||
googleDocUrl?: string;
|
|
||||||
category: string;
|
|
||||||
version: number;
|
|
||||||
parentDocumentId?: string;
|
|
||||||
isDeleted: boolean;
|
|
||||||
downloadCount: number;
|
|
||||||
uploadedAt: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DocumentCreationAttributes extends Optional<DocumentAttributes, 'documentId' | 'storageUrl' | 'googleDocUrl' | 'parentDocumentId' | 'uploadedAt'> {}
|
|
||||||
|
|
||||||
class Document extends Model<DocumentAttributes, DocumentCreationAttributes> implements DocumentAttributes {
|
|
||||||
public documentId!: string;
|
|
||||||
public requestId!: string;
|
|
||||||
public uploadedBy!: string;
|
|
||||||
public fileName!: string;
|
|
||||||
public originalFileName!: string;
|
|
||||||
public fileType!: string;
|
|
||||||
public fileExtension!: string;
|
|
||||||
public fileSize!: number;
|
|
||||||
public filePath!: string;
|
|
||||||
public storageUrl?: string;
|
|
||||||
public mimeType!: string;
|
|
||||||
public checksum!: string;
|
|
||||||
public isGoogleDoc!: boolean;
|
|
||||||
public googleDocUrl?: string;
|
|
||||||
public category!: string;
|
|
||||||
public version!: number;
|
|
||||||
public parentDocumentId?: string;
|
|
||||||
public isDeleted!: boolean;
|
|
||||||
public downloadCount!: number;
|
|
||||||
public uploadedAt!: Date;
|
|
||||||
|
|
||||||
// Associations
|
|
||||||
public request?: WorkflowRequest;
|
|
||||||
public uploader?: User;
|
|
||||||
public parentDocument?: Document;
|
|
||||||
}
|
|
||||||
|
|
||||||
Document.init(
|
|
||||||
{
|
|
||||||
documentId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
defaultValue: DataTypes.UUIDV4,
|
|
||||||
primaryKey: true,
|
|
||||||
field: 'document_id'
|
|
||||||
},
|
|
||||||
requestId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
field: 'request_id',
|
|
||||||
references: {
|
|
||||||
model: 'workflow_requests',
|
|
||||||
key: 'request_id'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
uploadedBy: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
field: 'uploaded_by',
|
|
||||||
references: {
|
|
||||||
model: 'users',
|
|
||||||
key: 'user_id'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
fileName: {
|
|
||||||
type: DataTypes.STRING(255),
|
|
||||||
allowNull: false,
|
|
||||||
field: 'file_name'
|
|
||||||
},
|
|
||||||
originalFileName: {
|
|
||||||
type: DataTypes.STRING(255),
|
|
||||||
allowNull: false,
|
|
||||||
field: 'original_file_name'
|
|
||||||
},
|
|
||||||
fileType: {
|
|
||||||
type: DataTypes.STRING(100),
|
|
||||||
allowNull: false,
|
|
||||||
field: 'file_type'
|
|
||||||
},
|
|
||||||
fileExtension: {
|
|
||||||
type: DataTypes.STRING(10),
|
|
||||||
allowNull: false,
|
|
||||||
field: 'file_extension'
|
|
||||||
},
|
|
||||||
fileSize: {
|
|
||||||
type: DataTypes.BIGINT,
|
|
||||||
allowNull: false,
|
|
||||||
field: 'file_size',
|
|
||||||
validate: {
|
|
||||||
max: 10485760 // 10MB limit
|
|
||||||
}
|
|
||||||
},
|
|
||||||
filePath: {
|
|
||||||
type: DataTypes.STRING(500),
|
|
||||||
allowNull: false,
|
|
||||||
field: 'file_path'
|
|
||||||
},
|
|
||||||
storageUrl: {
|
|
||||||
type: DataTypes.STRING(500),
|
|
||||||
allowNull: true,
|
|
||||||
field: 'storage_url'
|
|
||||||
},
|
|
||||||
mimeType: {
|
|
||||||
type: DataTypes.STRING(100),
|
|
||||||
allowNull: false,
|
|
||||||
field: 'mime_type'
|
|
||||||
},
|
|
||||||
checksum: {
|
|
||||||
type: DataTypes.STRING(64),
|
|
||||||
allowNull: false
|
|
||||||
},
|
|
||||||
isGoogleDoc: {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
defaultValue: false,
|
|
||||||
field: 'is_google_doc'
|
|
||||||
},
|
|
||||||
googleDocUrl: {
|
|
||||||
type: DataTypes.STRING(500),
|
|
||||||
allowNull: true,
|
|
||||||
field: 'google_doc_url'
|
|
||||||
},
|
|
||||||
category: {
|
|
||||||
type: DataTypes.ENUM('SUPPORTING', 'APPROVAL', 'REFERENCE', 'FINAL', 'OTHER'),
|
|
||||||
defaultValue: 'OTHER'
|
|
||||||
},
|
|
||||||
version: {
|
|
||||||
type: DataTypes.INTEGER,
|
|
||||||
defaultValue: 1
|
|
||||||
},
|
|
||||||
parentDocumentId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'parent_document_id',
|
|
||||||
references: {
|
|
||||||
model: 'documents',
|
|
||||||
key: 'document_id'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
isDeleted: {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
defaultValue: false,
|
|
||||||
field: 'is_deleted'
|
|
||||||
},
|
|
||||||
downloadCount: {
|
|
||||||
type: DataTypes.INTEGER,
|
|
||||||
defaultValue: 0,
|
|
||||||
field: 'download_count'
|
|
||||||
},
|
|
||||||
uploadedAt: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW,
|
|
||||||
field: 'uploaded_at'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
sequelize,
|
|
||||||
modelName: 'Document',
|
|
||||||
tableName: 'documents',
|
|
||||||
timestamps: false,
|
|
||||||
indexes: [
|
|
||||||
{
|
|
||||||
fields: ['request_id']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fields: ['uploaded_by']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fields: ['category']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fields: ['is_deleted']
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Associations
|
|
||||||
Document.belongsTo(WorkflowRequest, {
|
|
||||||
as: 'request',
|
|
||||||
foreignKey: 'requestId',
|
|
||||||
targetKey: 'requestId'
|
|
||||||
});
|
|
||||||
|
|
||||||
Document.belongsTo(User, {
|
|
||||||
as: 'uploader',
|
|
||||||
foreignKey: 'uploadedBy',
|
|
||||||
targetKey: 'userId'
|
|
||||||
});
|
|
||||||
|
|
||||||
Document.belongsTo(Document, {
|
|
||||||
as: 'parentDocument',
|
|
||||||
foreignKey: 'parentDocumentId',
|
|
||||||
targetKey: 'documentId'
|
|
||||||
});
|
|
||||||
|
|
||||||
export { Document };
|
|
||||||
@ -1,161 +0,0 @@
|
|||||||
import { DataTypes, Model, Optional } from 'sequelize';
|
|
||||||
import { sequelize } from '@config/database';
|
|
||||||
import { User } from './User';
|
|
||||||
|
|
||||||
export enum HolidayType {
|
|
||||||
NATIONAL = 'NATIONAL',
|
|
||||||
REGIONAL = 'REGIONAL',
|
|
||||||
ORGANIZATIONAL = 'ORGANIZATIONAL',
|
|
||||||
OPTIONAL = 'OPTIONAL'
|
|
||||||
}
|
|
||||||
|
|
||||||
interface HolidayAttributes {
|
|
||||||
holidayId: string;
|
|
||||||
holidayDate: string; // YYYY-MM-DD format
|
|
||||||
holidayName: string;
|
|
||||||
description?: string;
|
|
||||||
isRecurring: boolean;
|
|
||||||
recurrenceRule?: string;
|
|
||||||
holidayType: HolidayType;
|
|
||||||
isActive: boolean;
|
|
||||||
appliesToDepartments?: string[];
|
|
||||||
appliesToLocations?: string[];
|
|
||||||
createdBy: string;
|
|
||||||
updatedBy?: string;
|
|
||||||
createdAt: Date;
|
|
||||||
updatedAt: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface HolidayCreationAttributes extends Optional<HolidayAttributes, 'holidayId' | 'description' | 'isRecurring' | 'recurrenceRule' | 'holidayType' | 'isActive' | 'appliesToDepartments' | 'appliesToLocations' | 'updatedBy' | 'createdAt' | 'updatedAt'> {}
|
|
||||||
|
|
||||||
class Holiday extends Model<HolidayAttributes, HolidayCreationAttributes> implements HolidayAttributes {
|
|
||||||
public holidayId!: string;
|
|
||||||
public holidayDate!: string;
|
|
||||||
public holidayName!: string;
|
|
||||||
public description?: string;
|
|
||||||
public isRecurring!: boolean;
|
|
||||||
public recurrenceRule?: string;
|
|
||||||
public holidayType!: HolidayType;
|
|
||||||
public isActive!: boolean;
|
|
||||||
public appliesToDepartments?: string[];
|
|
||||||
public appliesToLocations?: string[];
|
|
||||||
public createdBy!: string;
|
|
||||||
public updatedBy?: string;
|
|
||||||
public createdAt!: Date;
|
|
||||||
public updatedAt!: Date;
|
|
||||||
|
|
||||||
// Associations
|
|
||||||
public creator?: User;
|
|
||||||
public updater?: User;
|
|
||||||
}
|
|
||||||
|
|
||||||
Holiday.init(
|
|
||||||
{
|
|
||||||
holidayId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
defaultValue: DataTypes.UUIDV4,
|
|
||||||
primaryKey: true,
|
|
||||||
field: 'holiday_id'
|
|
||||||
},
|
|
||||||
holidayDate: {
|
|
||||||
type: DataTypes.DATEONLY,
|
|
||||||
allowNull: false,
|
|
||||||
unique: true,
|
|
||||||
field: 'holiday_date'
|
|
||||||
},
|
|
||||||
holidayName: {
|
|
||||||
type: DataTypes.STRING(200),
|
|
||||||
allowNull: false,
|
|
||||||
field: 'holiday_name'
|
|
||||||
},
|
|
||||||
description: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'description'
|
|
||||||
},
|
|
||||||
isRecurring: {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
defaultValue: false,
|
|
||||||
field: 'is_recurring'
|
|
||||||
},
|
|
||||||
recurrenceRule: {
|
|
||||||
type: DataTypes.STRING(100),
|
|
||||||
allowNull: true,
|
|
||||||
field: 'recurrence_rule'
|
|
||||||
},
|
|
||||||
holidayType: {
|
|
||||||
type: DataTypes.ENUM('NATIONAL', 'REGIONAL', 'ORGANIZATIONAL', 'OPTIONAL'),
|
|
||||||
defaultValue: 'ORGANIZATIONAL',
|
|
||||||
field: 'holiday_type'
|
|
||||||
},
|
|
||||||
isActive: {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
defaultValue: true,
|
|
||||||
field: 'is_active'
|
|
||||||
},
|
|
||||||
appliesToDepartments: {
|
|
||||||
type: DataTypes.ARRAY(DataTypes.STRING),
|
|
||||||
allowNull: true,
|
|
||||||
defaultValue: null,
|
|
||||||
field: 'applies_to_departments'
|
|
||||||
},
|
|
||||||
appliesToLocations: {
|
|
||||||
type: DataTypes.ARRAY(DataTypes.STRING),
|
|
||||||
allowNull: true,
|
|
||||||
defaultValue: null,
|
|
||||||
field: 'applies_to_locations'
|
|
||||||
},
|
|
||||||
createdBy: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
field: 'created_by'
|
|
||||||
},
|
|
||||||
updatedBy: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'updated_by'
|
|
||||||
},
|
|
||||||
createdAt: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW,
|
|
||||||
field: 'created_at'
|
|
||||||
},
|
|
||||||
updatedAt: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW,
|
|
||||||
field: 'updated_at'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
sequelize,
|
|
||||||
modelName: 'Holiday',
|
|
||||||
tableName: 'holidays',
|
|
||||||
timestamps: true,
|
|
||||||
createdAt: 'created_at',
|
|
||||||
updatedAt: 'updated_at',
|
|
||||||
indexes: [
|
|
||||||
{ fields: ['holiday_date'] },
|
|
||||||
{ fields: ['is_active'] },
|
|
||||||
{ fields: ['holiday_type'] },
|
|
||||||
{ fields: ['created_by'] }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Associations
|
|
||||||
Holiday.belongsTo(User, {
|
|
||||||
as: 'creator',
|
|
||||||
foreignKey: 'createdBy',
|
|
||||||
targetKey: 'userId'
|
|
||||||
});
|
|
||||||
|
|
||||||
Holiday.belongsTo(User, {
|
|
||||||
as: 'updater',
|
|
||||||
foreignKey: 'updatedBy',
|
|
||||||
targetKey: 'userId'
|
|
||||||
});
|
|
||||||
|
|
||||||
export { Holiday };
|
|
||||||
|
|
||||||
@ -1,166 +0,0 @@
|
|||||||
import { DataTypes, Model, Optional } from 'sequelize';
|
|
||||||
import { sequelize } from '@config/database';
|
|
||||||
import { WorkflowRequest } from './WorkflowRequest';
|
|
||||||
import { User } from './User';
|
|
||||||
|
|
||||||
export enum IOStatus {
|
|
||||||
PENDING = 'PENDING',
|
|
||||||
BLOCKED = 'BLOCKED',
|
|
||||||
RELEASED = 'RELEASED',
|
|
||||||
CANCELLED = 'CANCELLED'
|
|
||||||
}
|
|
||||||
|
|
||||||
interface InternalOrderAttributes {
|
|
||||||
ioId: string;
|
|
||||||
requestId: string;
|
|
||||||
ioNumber: string;
|
|
||||||
ioRemark?: string;
|
|
||||||
ioAvailableBalance?: number;
|
|
||||||
ioBlockedAmount?: number;
|
|
||||||
ioRemainingBalance?: number;
|
|
||||||
organizedBy?: string;
|
|
||||||
organizedAt?: Date;
|
|
||||||
sapDocumentNumber?: string;
|
|
||||||
status: IOStatus;
|
|
||||||
createdAt: Date;
|
|
||||||
updatedAt: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface InternalOrderCreationAttributes extends Optional<InternalOrderAttributes, 'ioId' | 'ioRemark' | 'ioAvailableBalance' | 'ioBlockedAmount' | 'ioRemainingBalance' | 'organizedBy' | 'organizedAt' | 'sapDocumentNumber' | 'status' | 'createdAt' | 'updatedAt'> {}
|
|
||||||
|
|
||||||
class InternalOrder extends Model<InternalOrderAttributes, InternalOrderCreationAttributes> implements InternalOrderAttributes {
|
|
||||||
public ioId!: string;
|
|
||||||
public requestId!: string;
|
|
||||||
public ioNumber!: string;
|
|
||||||
public ioRemark?: string;
|
|
||||||
public ioAvailableBalance?: number;
|
|
||||||
public ioBlockedAmount?: number;
|
|
||||||
public ioRemainingBalance?: number;
|
|
||||||
public organizedBy?: string;
|
|
||||||
public organizedAt?: Date;
|
|
||||||
public sapDocumentNumber?: string;
|
|
||||||
public status!: IOStatus;
|
|
||||||
public createdAt!: Date;
|
|
||||||
public updatedAt!: Date;
|
|
||||||
|
|
||||||
// Associations
|
|
||||||
public request?: WorkflowRequest;
|
|
||||||
public organizer?: User;
|
|
||||||
}
|
|
||||||
|
|
||||||
InternalOrder.init(
|
|
||||||
{
|
|
||||||
ioId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
defaultValue: DataTypes.UUIDV4,
|
|
||||||
primaryKey: true,
|
|
||||||
field: 'io_id'
|
|
||||||
},
|
|
||||||
requestId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
field: 'request_id',
|
|
||||||
references: {
|
|
||||||
model: 'workflow_requests',
|
|
||||||
key: 'request_id'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
ioNumber: {
|
|
||||||
type: DataTypes.STRING(50),
|
|
||||||
allowNull: false,
|
|
||||||
field: 'io_number'
|
|
||||||
},
|
|
||||||
ioRemark: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'io_remark'
|
|
||||||
},
|
|
||||||
ioAvailableBalance: {
|
|
||||||
type: DataTypes.DECIMAL(15, 2),
|
|
||||||
allowNull: true,
|
|
||||||
field: 'io_available_balance'
|
|
||||||
},
|
|
||||||
ioBlockedAmount: {
|
|
||||||
type: DataTypes.DECIMAL(15, 2),
|
|
||||||
allowNull: true,
|
|
||||||
field: 'io_blocked_amount'
|
|
||||||
},
|
|
||||||
ioRemainingBalance: {
|
|
||||||
type: DataTypes.DECIMAL(15, 2),
|
|
||||||
allowNull: true,
|
|
||||||
field: 'io_remaining_balance'
|
|
||||||
},
|
|
||||||
organizedBy: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'organized_by',
|
|
||||||
references: {
|
|
||||||
model: 'users',
|
|
||||||
key: 'user_id'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
organizedAt: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'organized_at'
|
|
||||||
},
|
|
||||||
sapDocumentNumber: {
|
|
||||||
type: DataTypes.STRING(100),
|
|
||||||
allowNull: true,
|
|
||||||
field: 'sap_document_number'
|
|
||||||
},
|
|
||||||
status: {
|
|
||||||
type: DataTypes.ENUM('PENDING', 'BLOCKED', 'RELEASED', 'CANCELLED'),
|
|
||||||
defaultValue: 'PENDING',
|
|
||||||
allowNull: false
|
|
||||||
},
|
|
||||||
createdAt: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW,
|
|
||||||
field: 'created_at'
|
|
||||||
},
|
|
||||||
updatedAt: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW,
|
|
||||||
field: 'updated_at'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
sequelize,
|
|
||||||
modelName: 'InternalOrder',
|
|
||||||
tableName: 'internal_orders',
|
|
||||||
timestamps: true,
|
|
||||||
createdAt: 'created_at',
|
|
||||||
updatedAt: 'updated_at',
|
|
||||||
indexes: [
|
|
||||||
{
|
|
||||||
fields: ['request_id'],
|
|
||||||
unique: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fields: ['io_number']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fields: ['organized_by']
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Associations
|
|
||||||
InternalOrder.belongsTo(WorkflowRequest, {
|
|
||||||
as: 'request',
|
|
||||||
foreignKey: 'requestId',
|
|
||||||
targetKey: 'requestId'
|
|
||||||
});
|
|
||||||
|
|
||||||
InternalOrder.belongsTo(User, {
|
|
||||||
as: 'organizer',
|
|
||||||
foreignKey: 'organizedBy',
|
|
||||||
targetKey: 'userId'
|
|
||||||
});
|
|
||||||
|
|
||||||
export { InternalOrder };
|
|
||||||
|
|
||||||
@ -1,156 +0,0 @@
|
|||||||
import { DataTypes, Model, Optional } from 'sequelize';
|
|
||||||
import { sequelize } from '../config/database';
|
|
||||||
|
|
||||||
interface NotificationAttributes {
|
|
||||||
notificationId: string;
|
|
||||||
userId: string;
|
|
||||||
requestId?: string;
|
|
||||||
notificationType: string;
|
|
||||||
title: string;
|
|
||||||
message: string;
|
|
||||||
isRead: boolean;
|
|
||||||
priority: 'LOW' | 'MEDIUM' | 'HIGH' | 'URGENT';
|
|
||||||
actionUrl?: string;
|
|
||||||
actionRequired: boolean;
|
|
||||||
metadata?: any;
|
|
||||||
sentVia: string[];
|
|
||||||
emailSent: boolean;
|
|
||||||
smsSent: boolean;
|
|
||||||
pushSent: boolean;
|
|
||||||
readAt?: Date;
|
|
||||||
expiresAt?: Date;
|
|
||||||
createdAt: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface NotificationCreationAttributes extends Optional<NotificationAttributes, 'notificationId' | 'isRead' | 'priority' | 'actionRequired' | 'sentVia' | 'emailSent' | 'smsSent' | 'pushSent' | 'createdAt'> {}
|
|
||||||
|
|
||||||
class Notification extends Model<NotificationAttributes, NotificationCreationAttributes> implements NotificationAttributes {
|
|
||||||
public notificationId!: string;
|
|
||||||
public userId!: string;
|
|
||||||
public requestId?: string;
|
|
||||||
public notificationType!: string;
|
|
||||||
public title!: string;
|
|
||||||
public message!: string;
|
|
||||||
public isRead!: boolean;
|
|
||||||
public priority!: 'LOW' | 'MEDIUM' | 'HIGH' | 'URGENT';
|
|
||||||
public actionUrl?: string;
|
|
||||||
public actionRequired!: boolean;
|
|
||||||
public metadata?: any;
|
|
||||||
public sentVia!: string[];
|
|
||||||
public emailSent!: boolean;
|
|
||||||
public smsSent!: boolean;
|
|
||||||
public pushSent!: boolean;
|
|
||||||
public readAt?: Date;
|
|
||||||
public expiresAt?: Date;
|
|
||||||
public readonly createdAt!: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
Notification.init(
|
|
||||||
{
|
|
||||||
notificationId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
defaultValue: DataTypes.UUIDV4,
|
|
||||||
primaryKey: true,
|
|
||||||
field: 'notification_id'
|
|
||||||
},
|
|
||||||
userId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
field: 'user_id',
|
|
||||||
references: {
|
|
||||||
model: 'users',
|
|
||||||
key: 'user_id'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
requestId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'request_id',
|
|
||||||
references: {
|
|
||||||
model: 'workflow_requests',
|
|
||||||
key: 'request_id'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
notificationType: {
|
|
||||||
type: DataTypes.STRING(50),
|
|
||||||
allowNull: false,
|
|
||||||
field: 'notification_type'
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
type: DataTypes.STRING(255),
|
|
||||||
allowNull: false
|
|
||||||
},
|
|
||||||
message: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
allowNull: false
|
|
||||||
},
|
|
||||||
isRead: {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
defaultValue: false,
|
|
||||||
field: 'is_read'
|
|
||||||
},
|
|
||||||
priority: {
|
|
||||||
type: DataTypes.ENUM('LOW', 'MEDIUM', 'HIGH', 'URGENT'),
|
|
||||||
defaultValue: 'MEDIUM'
|
|
||||||
},
|
|
||||||
actionUrl: {
|
|
||||||
type: DataTypes.STRING(500),
|
|
||||||
allowNull: true,
|
|
||||||
field: 'action_url'
|
|
||||||
},
|
|
||||||
actionRequired: {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
defaultValue: false,
|
|
||||||
field: 'action_required'
|
|
||||||
},
|
|
||||||
metadata: {
|
|
||||||
type: DataTypes.JSONB,
|
|
||||||
allowNull: true
|
|
||||||
},
|
|
||||||
sentVia: {
|
|
||||||
type: DataTypes.ARRAY(DataTypes.STRING),
|
|
||||||
defaultValue: [],
|
|
||||||
field: 'sent_via'
|
|
||||||
},
|
|
||||||
emailSent: {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
defaultValue: false,
|
|
||||||
field: 'email_sent'
|
|
||||||
},
|
|
||||||
smsSent: {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
defaultValue: false,
|
|
||||||
field: 'sms_sent'
|
|
||||||
},
|
|
||||||
pushSent: {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
defaultValue: false,
|
|
||||||
field: 'push_sent'
|
|
||||||
},
|
|
||||||
readAt: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'read_at'
|
|
||||||
},
|
|
||||||
expiresAt: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'expires_at'
|
|
||||||
},
|
|
||||||
createdAt: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW,
|
|
||||||
field: 'created_at'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
sequelize,
|
|
||||||
tableName: 'notifications',
|
|
||||||
timestamps: false,
|
|
||||||
underscored: true
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export { Notification };
|
|
||||||
|
|
||||||
@ -1,170 +0,0 @@
|
|||||||
import { DataTypes, Model, Optional } from 'sequelize';
|
|
||||||
import { sequelize } from '@config/database';
|
|
||||||
import { User } from './User';
|
|
||||||
import { WorkflowRequest } from './WorkflowRequest';
|
|
||||||
import { ParticipantType } from '../types/common.types';
|
|
||||||
|
|
||||||
interface ParticipantAttributes {
|
|
||||||
participantId: string;
|
|
||||||
requestId: string;
|
|
||||||
userId: string;
|
|
||||||
userEmail: string;
|
|
||||||
userName: string;
|
|
||||||
participantType: ParticipantType;
|
|
||||||
canComment: boolean;
|
|
||||||
canViewDocuments: boolean;
|
|
||||||
canDownloadDocuments: boolean;
|
|
||||||
notificationEnabled: boolean;
|
|
||||||
addedBy: string;
|
|
||||||
addedAt: Date;
|
|
||||||
isActive: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ParticipantCreationAttributes extends Optional<ParticipantAttributes, 'participantId' | 'addedAt'> {}
|
|
||||||
|
|
||||||
class Participant extends Model<ParticipantAttributes, ParticipantCreationAttributes> implements ParticipantAttributes {
|
|
||||||
public participantId!: string;
|
|
||||||
public requestId!: string;
|
|
||||||
public userId!: string;
|
|
||||||
public userEmail!: string;
|
|
||||||
public userName!: string;
|
|
||||||
public participantType!: ParticipantType;
|
|
||||||
public canComment!: boolean;
|
|
||||||
public canViewDocuments!: boolean;
|
|
||||||
public canDownloadDocuments!: boolean;
|
|
||||||
public notificationEnabled!: boolean;
|
|
||||||
public addedBy!: string;
|
|
||||||
public addedAt!: Date;
|
|
||||||
public isActive!: boolean;
|
|
||||||
|
|
||||||
// Associations
|
|
||||||
public request?: WorkflowRequest;
|
|
||||||
public user?: User;
|
|
||||||
public addedByUser?: User;
|
|
||||||
}
|
|
||||||
|
|
||||||
Participant.init(
|
|
||||||
{
|
|
||||||
participantId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
defaultValue: DataTypes.UUIDV4,
|
|
||||||
primaryKey: true,
|
|
||||||
field: 'participant_id'
|
|
||||||
},
|
|
||||||
requestId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
field: 'request_id',
|
|
||||||
references: {
|
|
||||||
model: 'workflow_requests',
|
|
||||||
key: 'request_id'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
userId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
field: 'user_id',
|
|
||||||
references: {
|
|
||||||
model: 'users',
|
|
||||||
key: 'user_id'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
userEmail: {
|
|
||||||
type: DataTypes.STRING(255),
|
|
||||||
allowNull: false,
|
|
||||||
field: 'user_email'
|
|
||||||
},
|
|
||||||
userName: {
|
|
||||||
type: DataTypes.STRING(200),
|
|
||||||
allowNull: false,
|
|
||||||
field: 'user_name'
|
|
||||||
},
|
|
||||||
participantType: {
|
|
||||||
type: DataTypes.ENUM('SPECTATOR', 'INITIATOR', 'APPROVER', 'CONSULTATION'),
|
|
||||||
allowNull: false,
|
|
||||||
field: 'participant_type'
|
|
||||||
},
|
|
||||||
canComment: {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
defaultValue: true,
|
|
||||||
field: 'can_comment'
|
|
||||||
},
|
|
||||||
canViewDocuments: {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
defaultValue: true,
|
|
||||||
field: 'can_view_documents'
|
|
||||||
},
|
|
||||||
canDownloadDocuments: {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
defaultValue: false,
|
|
||||||
field: 'can_download_documents'
|
|
||||||
},
|
|
||||||
notificationEnabled: {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
defaultValue: true,
|
|
||||||
field: 'notification_enabled'
|
|
||||||
},
|
|
||||||
addedBy: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
field: 'added_by',
|
|
||||||
references: {
|
|
||||||
model: 'users',
|
|
||||||
key: 'user_id'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
addedAt: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW,
|
|
||||||
field: 'added_at'
|
|
||||||
},
|
|
||||||
isActive: {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
defaultValue: true,
|
|
||||||
field: 'is_active'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
sequelize,
|
|
||||||
modelName: 'Participant',
|
|
||||||
tableName: 'participants',
|
|
||||||
timestamps: false,
|
|
||||||
indexes: [
|
|
||||||
{
|
|
||||||
fields: ['request_id']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fields: ['user_id']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fields: ['participant_type']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
unique: true,
|
|
||||||
fields: ['request_id', 'user_id']
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Associations
|
|
||||||
Participant.belongsTo(WorkflowRequest, {
|
|
||||||
as: 'request',
|
|
||||||
foreignKey: 'requestId',
|
|
||||||
targetKey: 'requestId'
|
|
||||||
});
|
|
||||||
|
|
||||||
Participant.belongsTo(User, {
|
|
||||||
as: 'user',
|
|
||||||
foreignKey: 'userId',
|
|
||||||
targetKey: 'userId'
|
|
||||||
});
|
|
||||||
|
|
||||||
Participant.belongsTo(User, {
|
|
||||||
as: 'addedByUser',
|
|
||||||
foreignKey: 'addedBy',
|
|
||||||
targetKey: 'userId'
|
|
||||||
});
|
|
||||||
|
|
||||||
export { Participant };
|
|
||||||
@ -1,137 +0,0 @@
|
|||||||
import { DataTypes, Model, Optional } from 'sequelize';
|
|
||||||
import { sequelize } from '../config/database';
|
|
||||||
import { WorkflowRequest } from './WorkflowRequest';
|
|
||||||
import { User } from './User';
|
|
||||||
import ConclusionRemark from './ConclusionRemark';
|
|
||||||
|
|
||||||
interface RequestSummaryAttributes {
|
|
||||||
summaryId: string;
|
|
||||||
requestId: string;
|
|
||||||
initiatorId: string;
|
|
||||||
title: string;
|
|
||||||
description: string | null;
|
|
||||||
closingRemarks: string | null;
|
|
||||||
isAiGenerated: boolean;
|
|
||||||
conclusionId: string | null;
|
|
||||||
createdAt?: Date;
|
|
||||||
updatedAt?: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface RequestSummaryCreationAttributes
|
|
||||||
extends Optional<RequestSummaryAttributes, 'summaryId' | 'description' | 'closingRemarks' | 'isAiGenerated' | 'conclusionId' | 'createdAt' | 'updatedAt'> {}
|
|
||||||
|
|
||||||
class RequestSummary extends Model<RequestSummaryAttributes, RequestSummaryCreationAttributes>
|
|
||||||
implements RequestSummaryAttributes {
|
|
||||||
public summaryId!: string;
|
|
||||||
public requestId!: string;
|
|
||||||
public initiatorId!: string;
|
|
||||||
public title!: string;
|
|
||||||
public description!: string | null;
|
|
||||||
public closingRemarks!: string | null;
|
|
||||||
public isAiGenerated!: boolean;
|
|
||||||
public conclusionId!: string | null;
|
|
||||||
public readonly createdAt!: Date;
|
|
||||||
public readonly updatedAt!: Date;
|
|
||||||
|
|
||||||
// Associations
|
|
||||||
public request?: WorkflowRequest;
|
|
||||||
public initiator?: User;
|
|
||||||
public conclusion?: ConclusionRemark;
|
|
||||||
}
|
|
||||||
|
|
||||||
RequestSummary.init(
|
|
||||||
{
|
|
||||||
summaryId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
defaultValue: DataTypes.UUIDV4,
|
|
||||||
primaryKey: true,
|
|
||||||
field: 'summary_id'
|
|
||||||
},
|
|
||||||
requestId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
field: 'request_id',
|
|
||||||
references: {
|
|
||||||
model: 'workflow_requests',
|
|
||||||
key: 'request_id'
|
|
||||||
},
|
|
||||||
unique: true
|
|
||||||
},
|
|
||||||
initiatorId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
field: 'initiator_id',
|
|
||||||
references: {
|
|
||||||
model: 'users',
|
|
||||||
key: 'user_id'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
type: DataTypes.STRING(500),
|
|
||||||
allowNull: false
|
|
||||||
},
|
|
||||||
description: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
allowNull: true
|
|
||||||
},
|
|
||||||
closingRemarks: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'closing_remarks'
|
|
||||||
},
|
|
||||||
isAiGenerated: {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: false,
|
|
||||||
field: 'is_ai_generated'
|
|
||||||
},
|
|
||||||
conclusionId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'conclusion_id',
|
|
||||||
references: {
|
|
||||||
model: 'conclusion_remarks',
|
|
||||||
key: 'conclusion_id'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
createdAt: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW,
|
|
||||||
field: 'created_at'
|
|
||||||
},
|
|
||||||
updatedAt: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW,
|
|
||||||
field: 'updated_at'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
sequelize,
|
|
||||||
tableName: 'request_summaries',
|
|
||||||
timestamps: true,
|
|
||||||
underscored: true
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Associations
|
|
||||||
RequestSummary.belongsTo(WorkflowRequest, {
|
|
||||||
as: 'request',
|
|
||||||
foreignKey: 'requestId',
|
|
||||||
targetKey: 'requestId'
|
|
||||||
});
|
|
||||||
|
|
||||||
RequestSummary.belongsTo(User, {
|
|
||||||
as: 'initiator',
|
|
||||||
foreignKey: 'initiatorId',
|
|
||||||
targetKey: 'userId'
|
|
||||||
});
|
|
||||||
|
|
||||||
RequestSummary.belongsTo(ConclusionRemark, {
|
|
||||||
foreignKey: 'conclusionId',
|
|
||||||
targetKey: 'conclusionId'
|
|
||||||
});
|
|
||||||
|
|
||||||
export default RequestSummary;
|
|
||||||
|
|
||||||
@ -1,132 +0,0 @@
|
|||||||
import { DataTypes, Model, Optional } from 'sequelize';
|
|
||||||
import { sequelize } from '../config/database';
|
|
||||||
import RequestSummary from './RequestSummary';
|
|
||||||
import { User } from './User';
|
|
||||||
|
|
||||||
interface SharedSummaryAttributes {
|
|
||||||
sharedSummaryId: string;
|
|
||||||
summaryId: string;
|
|
||||||
sharedBy: string;
|
|
||||||
sharedWith: string;
|
|
||||||
sharedAt: Date;
|
|
||||||
viewedAt: Date | null;
|
|
||||||
isRead: boolean;
|
|
||||||
createdAt?: Date;
|
|
||||||
updatedAt?: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SharedSummaryCreationAttributes
|
|
||||||
extends Optional<SharedSummaryAttributes, 'sharedSummaryId' | 'sharedAt' | 'viewedAt' | 'isRead' | 'createdAt' | 'updatedAt'> {}
|
|
||||||
|
|
||||||
class SharedSummary extends Model<SharedSummaryAttributes, SharedSummaryCreationAttributes>
|
|
||||||
implements SharedSummaryAttributes {
|
|
||||||
public sharedSummaryId!: string;
|
|
||||||
public summaryId!: string;
|
|
||||||
public sharedBy!: string;
|
|
||||||
public sharedWith!: string;
|
|
||||||
public sharedAt!: Date;
|
|
||||||
public viewedAt!: Date | null;
|
|
||||||
public isRead!: boolean;
|
|
||||||
public readonly createdAt!: Date;
|
|
||||||
public readonly updatedAt!: Date;
|
|
||||||
|
|
||||||
// Associations
|
|
||||||
public summary?: RequestSummary;
|
|
||||||
public sharedByUser?: User;
|
|
||||||
public sharedWithUser?: User;
|
|
||||||
}
|
|
||||||
|
|
||||||
SharedSummary.init(
|
|
||||||
{
|
|
||||||
sharedSummaryId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
defaultValue: DataTypes.UUIDV4,
|
|
||||||
primaryKey: true,
|
|
||||||
field: 'shared_summary_id'
|
|
||||||
},
|
|
||||||
summaryId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
field: 'summary_id',
|
|
||||||
references: {
|
|
||||||
model: 'request_summaries',
|
|
||||||
key: 'summary_id'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
sharedBy: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
field: 'shared_by',
|
|
||||||
references: {
|
|
||||||
model: 'users',
|
|
||||||
key: 'user_id'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
sharedWith: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
field: 'shared_with',
|
|
||||||
references: {
|
|
||||||
model: 'users',
|
|
||||||
key: 'user_id'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
sharedAt: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW,
|
|
||||||
field: 'shared_at'
|
|
||||||
},
|
|
||||||
viewedAt: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'viewed_at'
|
|
||||||
},
|
|
||||||
isRead: {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: false,
|
|
||||||
field: 'is_read'
|
|
||||||
},
|
|
||||||
createdAt: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW,
|
|
||||||
field: 'created_at'
|
|
||||||
},
|
|
||||||
updatedAt: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW,
|
|
||||||
field: 'updated_at'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
sequelize,
|
|
||||||
tableName: 'shared_summaries',
|
|
||||||
timestamps: true,
|
|
||||||
underscored: true
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Associations
|
|
||||||
SharedSummary.belongsTo(RequestSummary, {
|
|
||||||
as: 'summary',
|
|
||||||
foreignKey: 'summaryId',
|
|
||||||
targetKey: 'summaryId'
|
|
||||||
});
|
|
||||||
|
|
||||||
SharedSummary.belongsTo(User, {
|
|
||||||
as: 'sharedByUser',
|
|
||||||
foreignKey: 'sharedBy',
|
|
||||||
targetKey: 'userId'
|
|
||||||
});
|
|
||||||
|
|
||||||
SharedSummary.belongsTo(User, {
|
|
||||||
as: 'sharedWithUser',
|
|
||||||
foreignKey: 'sharedWith',
|
|
||||||
targetKey: 'userId'
|
|
||||||
});
|
|
||||||
|
|
||||||
export default SharedSummary;
|
|
||||||
|
|
||||||
@ -1,77 +0,0 @@
|
|||||||
import { DataTypes, Model, Optional } from 'sequelize';
|
|
||||||
import { sequelize } from '@config/database';
|
|
||||||
|
|
||||||
interface SubscriptionAttributes {
|
|
||||||
subscriptionId: string;
|
|
||||||
userId: string;
|
|
||||||
endpoint: string;
|
|
||||||
p256dh: string;
|
|
||||||
auth: string;
|
|
||||||
userAgent?: string | null;
|
|
||||||
createdAt: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SubscriptionCreationAttributes extends Optional<SubscriptionAttributes, 'subscriptionId' | 'userAgent' | 'createdAt'> {}
|
|
||||||
|
|
||||||
class Subscription extends Model<SubscriptionAttributes, SubscriptionCreationAttributes> implements SubscriptionAttributes {
|
|
||||||
public subscriptionId!: string;
|
|
||||||
public userId!: string;
|
|
||||||
public endpoint!: string;
|
|
||||||
public p256dh!: string;
|
|
||||||
public auth!: string;
|
|
||||||
public userAgent!: string | null;
|
|
||||||
public createdAt!: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
Subscription.init(
|
|
||||||
{
|
|
||||||
subscriptionId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
defaultValue: DataTypes.UUIDV4,
|
|
||||||
primaryKey: true,
|
|
||||||
field: 'subscription_id'
|
|
||||||
},
|
|
||||||
userId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
field: 'user_id'
|
|
||||||
},
|
|
||||||
endpoint: {
|
|
||||||
type: DataTypes.STRING(1000),
|
|
||||||
allowNull: false
|
|
||||||
},
|
|
||||||
p256dh: {
|
|
||||||
type: DataTypes.STRING(255),
|
|
||||||
allowNull: false
|
|
||||||
},
|
|
||||||
auth: {
|
|
||||||
type: DataTypes.STRING(255),
|
|
||||||
allowNull: false
|
|
||||||
},
|
|
||||||
userAgent: {
|
|
||||||
type: DataTypes.STRING(500),
|
|
||||||
allowNull: true,
|
|
||||||
field: 'user_agent'
|
|
||||||
},
|
|
||||||
createdAt: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW,
|
|
||||||
field: 'created_at'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
sequelize,
|
|
||||||
modelName: 'Subscription',
|
|
||||||
tableName: 'subscriptions',
|
|
||||||
timestamps: false,
|
|
||||||
indexes: [
|
|
||||||
{ fields: ['user_id'] },
|
|
||||||
{ unique: true, fields: ['endpoint'] }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export { Subscription };
|
|
||||||
|
|
||||||
|
|
||||||
@ -1,209 +0,0 @@
|
|||||||
import { DataTypes, Model, Optional } from 'sequelize';
|
|
||||||
import { sequelize } from '@config/database';
|
|
||||||
import { WorkflowRequest } from './WorkflowRequest';
|
|
||||||
import { ApprovalLevel } from './ApprovalLevel';
|
|
||||||
import { User } from './User';
|
|
||||||
|
|
||||||
export enum TatAlertType {
|
|
||||||
TAT_50 = 'TAT_50',
|
|
||||||
TAT_75 = 'TAT_75',
|
|
||||||
TAT_100 = 'TAT_100'
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TatAlertAttributes {
|
|
||||||
alertId: string;
|
|
||||||
requestId: string;
|
|
||||||
levelId: string;
|
|
||||||
approverId: string;
|
|
||||||
alertType: TatAlertType;
|
|
||||||
thresholdPercentage: number;
|
|
||||||
tatHoursAllocated: number;
|
|
||||||
tatHoursElapsed: number;
|
|
||||||
tatHoursRemaining: number;
|
|
||||||
levelStartTime: Date;
|
|
||||||
alertSentAt: Date;
|
|
||||||
expectedCompletionTime: Date;
|
|
||||||
alertMessage: string;
|
|
||||||
notificationSent: boolean;
|
|
||||||
notificationChannels: string[];
|
|
||||||
isBreached: boolean;
|
|
||||||
wasCompletedOnTime?: boolean;
|
|
||||||
completionTime?: Date;
|
|
||||||
metadata: Record<string, any>;
|
|
||||||
createdAt: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TatAlertCreationAttributes extends Optional<TatAlertAttributes, 'alertId' | 'notificationSent' | 'notificationChannels' | 'isBreached' | 'wasCompletedOnTime' | 'completionTime' | 'metadata' | 'alertSentAt' | 'createdAt'> {}
|
|
||||||
|
|
||||||
class TatAlert extends Model<TatAlertAttributes, TatAlertCreationAttributes> implements TatAlertAttributes {
|
|
||||||
public alertId!: string;
|
|
||||||
public requestId!: string;
|
|
||||||
public levelId!: string;
|
|
||||||
public approverId!: string;
|
|
||||||
public alertType!: TatAlertType;
|
|
||||||
public thresholdPercentage!: number;
|
|
||||||
public tatHoursAllocated!: number;
|
|
||||||
public tatHoursElapsed!: number;
|
|
||||||
public tatHoursRemaining!: number;
|
|
||||||
public levelStartTime!: Date;
|
|
||||||
public alertSentAt!: Date;
|
|
||||||
public expectedCompletionTime!: Date;
|
|
||||||
public alertMessage!: string;
|
|
||||||
public notificationSent!: boolean;
|
|
||||||
public notificationChannels!: string[];
|
|
||||||
public isBreached!: boolean;
|
|
||||||
public wasCompletedOnTime?: boolean;
|
|
||||||
public completionTime?: Date;
|
|
||||||
public metadata!: Record<string, any>;
|
|
||||||
public createdAt!: Date;
|
|
||||||
|
|
||||||
// Associations
|
|
||||||
public request?: WorkflowRequest;
|
|
||||||
public level?: ApprovalLevel;
|
|
||||||
public approver?: User;
|
|
||||||
}
|
|
||||||
|
|
||||||
TatAlert.init(
|
|
||||||
{
|
|
||||||
alertId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
defaultValue: DataTypes.UUIDV4,
|
|
||||||
primaryKey: true,
|
|
||||||
field: 'alert_id'
|
|
||||||
},
|
|
||||||
requestId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
field: 'request_id'
|
|
||||||
},
|
|
||||||
levelId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
field: 'level_id'
|
|
||||||
},
|
|
||||||
approverId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
field: 'approver_id'
|
|
||||||
},
|
|
||||||
alertType: {
|
|
||||||
type: DataTypes.ENUM('TAT_50', 'TAT_75', 'TAT_100'),
|
|
||||||
allowNull: false,
|
|
||||||
field: 'alert_type'
|
|
||||||
},
|
|
||||||
thresholdPercentage: {
|
|
||||||
type: DataTypes.INTEGER,
|
|
||||||
allowNull: false,
|
|
||||||
field: 'threshold_percentage'
|
|
||||||
},
|
|
||||||
tatHoursAllocated: {
|
|
||||||
type: DataTypes.DECIMAL(10, 2),
|
|
||||||
allowNull: false,
|
|
||||||
field: 'tat_hours_allocated'
|
|
||||||
},
|
|
||||||
tatHoursElapsed: {
|
|
||||||
type: DataTypes.DECIMAL(10, 2),
|
|
||||||
allowNull: false,
|
|
||||||
field: 'tat_hours_elapsed'
|
|
||||||
},
|
|
||||||
tatHoursRemaining: {
|
|
||||||
type: DataTypes.DECIMAL(10, 2),
|
|
||||||
allowNull: false,
|
|
||||||
field: 'tat_hours_remaining'
|
|
||||||
},
|
|
||||||
levelStartTime: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
field: 'level_start_time'
|
|
||||||
},
|
|
||||||
alertSentAt: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW,
|
|
||||||
field: 'alert_sent_at'
|
|
||||||
},
|
|
||||||
expectedCompletionTime: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
field: 'expected_completion_time'
|
|
||||||
},
|
|
||||||
alertMessage: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
allowNull: false,
|
|
||||||
field: 'alert_message'
|
|
||||||
},
|
|
||||||
notificationSent: {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
defaultValue: true,
|
|
||||||
field: 'notification_sent'
|
|
||||||
},
|
|
||||||
notificationChannels: {
|
|
||||||
type: DataTypes.ARRAY(DataTypes.STRING),
|
|
||||||
defaultValue: [],
|
|
||||||
field: 'notification_channels'
|
|
||||||
},
|
|
||||||
isBreached: {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
defaultValue: false,
|
|
||||||
field: 'is_breached'
|
|
||||||
},
|
|
||||||
wasCompletedOnTime: {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'was_completed_on_time'
|
|
||||||
},
|
|
||||||
completionTime: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'completion_time'
|
|
||||||
},
|
|
||||||
metadata: {
|
|
||||||
type: DataTypes.JSONB,
|
|
||||||
defaultValue: {},
|
|
||||||
field: 'metadata'
|
|
||||||
},
|
|
||||||
createdAt: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW,
|
|
||||||
field: 'created_at'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
sequelize,
|
|
||||||
modelName: 'TatAlert',
|
|
||||||
tableName: 'tat_alerts',
|
|
||||||
timestamps: false,
|
|
||||||
indexes: [
|
|
||||||
{ fields: ['request_id'] },
|
|
||||||
{ fields: ['level_id'] },
|
|
||||||
{ fields: ['approver_id'] },
|
|
||||||
{ fields: ['alert_type'] },
|
|
||||||
{ fields: ['alert_sent_at'] },
|
|
||||||
{ fields: ['is_breached'] },
|
|
||||||
{ fields: ['was_completed_on_time'] }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Associations
|
|
||||||
TatAlert.belongsTo(WorkflowRequest, {
|
|
||||||
as: 'request',
|
|
||||||
foreignKey: 'requestId',
|
|
||||||
targetKey: 'requestId'
|
|
||||||
});
|
|
||||||
|
|
||||||
TatAlert.belongsTo(ApprovalLevel, {
|
|
||||||
as: 'level',
|
|
||||||
foreignKey: 'levelId',
|
|
||||||
targetKey: 'levelId'
|
|
||||||
});
|
|
||||||
|
|
||||||
TatAlert.belongsTo(User, {
|
|
||||||
as: 'approver',
|
|
||||||
foreignKey: 'approverId',
|
|
||||||
targetKey: 'userId'
|
|
||||||
});
|
|
||||||
|
|
||||||
export { TatAlert };
|
|
||||||
|
|
||||||
@ -1,338 +0,0 @@
|
|||||||
import { DataTypes, Model, Optional } from 'sequelize';
|
|
||||||
import { sequelize } from '../config/database';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* User Role Enum
|
|
||||||
*
|
|
||||||
* USER: Default role - can create requests, view own requests, participate in workflows
|
|
||||||
* MANAGEMENT: Enhanced visibility - can view all requests, read-only access to all data
|
|
||||||
* ADMIN: Full access - can manage system configuration, users, and all workflows
|
|
||||||
*/
|
|
||||||
export type UserRole = 'USER' | 'MANAGEMENT' | 'ADMIN';
|
|
||||||
|
|
||||||
interface UserAttributes {
|
|
||||||
userId: string;
|
|
||||||
employeeId?: string | null;
|
|
||||||
oktaSub: string;
|
|
||||||
email: string;
|
|
||||||
firstName?: string | null;
|
|
||||||
lastName?: string | null;
|
|
||||||
displayName?: string | null;
|
|
||||||
department?: string | null;
|
|
||||||
designation?: string | null;
|
|
||||||
phone?: string | null;
|
|
||||||
|
|
||||||
// Extended fields from SSO/Okta (All Optional)
|
|
||||||
manager?: string | null; // Reporting manager name
|
|
||||||
secondEmail?: string | null; // Alternate email
|
|
||||||
jobTitle?: string | null; // Detailed job description (title field from Okta)
|
|
||||||
employeeNumber?: string | null; // HR system employee number (different from employeeId)
|
|
||||||
postalAddress?: string | null; // Work location/office address
|
|
||||||
mobilePhone?: string | null; // Mobile contact (different from phone)
|
|
||||||
adGroups?: string[] | null; // Active Directory group memberships
|
|
||||||
|
|
||||||
// Location Information (JSON object)
|
|
||||||
location?: {
|
|
||||||
city?: string;
|
|
||||||
state?: string;
|
|
||||||
country?: string;
|
|
||||||
office?: string;
|
|
||||||
timezone?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Notification Preferences
|
|
||||||
emailNotificationsEnabled: boolean;
|
|
||||||
pushNotificationsEnabled: boolean;
|
|
||||||
inAppNotificationsEnabled: boolean;
|
|
||||||
|
|
||||||
isActive: boolean;
|
|
||||||
role: UserRole; // RBAC: USER, MANAGEMENT, ADMIN
|
|
||||||
lastLogin?: Date;
|
|
||||||
createdAt: Date;
|
|
||||||
updatedAt: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UserCreationAttributes extends Optional<UserAttributes, 'userId' | 'employeeId' | 'department' | 'designation' | 'phone' | 'manager' | 'secondEmail' | 'jobTitle' | 'employeeNumber' | 'postalAddress' | 'mobilePhone' | 'adGroups' | 'emailNotificationsEnabled' | 'pushNotificationsEnabled' | 'inAppNotificationsEnabled' | 'role' | 'lastLogin' | 'createdAt' | 'updatedAt'> {}
|
|
||||||
|
|
||||||
class User extends Model<UserAttributes, UserCreationAttributes> implements UserAttributes {
|
|
||||||
public userId!: string;
|
|
||||||
public employeeId?: string | null;
|
|
||||||
public oktaSub!: string;
|
|
||||||
public email!: string;
|
|
||||||
public firstName?: string | null;
|
|
||||||
public lastName?: string | null;
|
|
||||||
public displayName?: string | null;
|
|
||||||
public department?: string;
|
|
||||||
public designation?: string;
|
|
||||||
public phone?: string;
|
|
||||||
|
|
||||||
// Extended fields from SSO/Okta (All Optional)
|
|
||||||
public manager?: string | null;
|
|
||||||
public secondEmail?: string | null;
|
|
||||||
public jobTitle?: string | null;
|
|
||||||
public employeeNumber?: string | null;
|
|
||||||
public postalAddress?: string | null;
|
|
||||||
public mobilePhone?: string | null;
|
|
||||||
public adGroups?: string[] | null;
|
|
||||||
|
|
||||||
// Location Information (JSON object)
|
|
||||||
public location?: {
|
|
||||||
city?: string;
|
|
||||||
state?: string;
|
|
||||||
country?: string;
|
|
||||||
office?: string;
|
|
||||||
timezone?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Notification Preferences
|
|
||||||
public emailNotificationsEnabled!: boolean;
|
|
||||||
public pushNotificationsEnabled!: boolean;
|
|
||||||
public inAppNotificationsEnabled!: boolean;
|
|
||||||
|
|
||||||
public isActive!: boolean;
|
|
||||||
public role!: UserRole; // RBAC: USER, MANAGEMENT, ADMIN
|
|
||||||
public lastLogin?: Date;
|
|
||||||
public createdAt!: Date;
|
|
||||||
public updatedAt!: Date;
|
|
||||||
|
|
||||||
// Associations
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper Methods for Role Checking
|
|
||||||
*/
|
|
||||||
public isUserRole(): boolean {
|
|
||||||
return this.role === 'USER';
|
|
||||||
}
|
|
||||||
|
|
||||||
public isManagementRole(): boolean {
|
|
||||||
return this.role === 'MANAGEMENT';
|
|
||||||
}
|
|
||||||
|
|
||||||
public isAdminRole(): boolean {
|
|
||||||
return this.role === 'ADMIN';
|
|
||||||
}
|
|
||||||
|
|
||||||
public hasManagementAccess(): boolean {
|
|
||||||
return this.role === 'MANAGEMENT' || this.role === 'ADMIN';
|
|
||||||
}
|
|
||||||
|
|
||||||
public hasAdminAccess(): boolean {
|
|
||||||
return this.role === 'ADMIN';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
User.init(
|
|
||||||
{
|
|
||||||
userId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
defaultValue: DataTypes.UUIDV4,
|
|
||||||
primaryKey: true,
|
|
||||||
field: 'user_id'
|
|
||||||
},
|
|
||||||
employeeId: {
|
|
||||||
type: DataTypes.STRING(50),
|
|
||||||
allowNull: true, // Made optional - email is now primary identifier
|
|
||||||
field: 'employee_id',
|
|
||||||
comment: 'HR System Employee ID (optional)'
|
|
||||||
},
|
|
||||||
oktaSub: {
|
|
||||||
type: DataTypes.STRING(100),
|
|
||||||
allowNull: false,
|
|
||||||
unique: true,
|
|
||||||
field: 'okta_sub',
|
|
||||||
comment: 'Okta user sub (subject identifier) - unique identifier from Okta'
|
|
||||||
},
|
|
||||||
email: {
|
|
||||||
type: DataTypes.STRING(255),
|
|
||||||
allowNull: false,
|
|
||||||
unique: true,
|
|
||||||
validate: {
|
|
||||||
isEmail: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
firstName: {
|
|
||||||
type: DataTypes.STRING(100),
|
|
||||||
allowNull: true, // Made optional - can be derived from displayName if needed
|
|
||||||
defaultValue: '',
|
|
||||||
field: 'first_name'
|
|
||||||
},
|
|
||||||
lastName: {
|
|
||||||
type: DataTypes.STRING(100),
|
|
||||||
allowNull: true, // Made optional - can be derived from displayName if needed
|
|
||||||
defaultValue: '',
|
|
||||||
field: 'last_name'
|
|
||||||
},
|
|
||||||
displayName: {
|
|
||||||
type: DataTypes.STRING(200),
|
|
||||||
allowNull: true, // Made optional - can be generated from firstName + lastName if needed
|
|
||||||
defaultValue: '',
|
|
||||||
field: 'display_name',
|
|
||||||
comment: 'Full Name for display'
|
|
||||||
},
|
|
||||||
department: {
|
|
||||||
type: DataTypes.STRING(100),
|
|
||||||
allowNull: true
|
|
||||||
},
|
|
||||||
designation: {
|
|
||||||
type: DataTypes.STRING(100),
|
|
||||||
allowNull: true
|
|
||||||
},
|
|
||||||
phone: {
|
|
||||||
type: DataTypes.STRING(20),
|
|
||||||
allowNull: true
|
|
||||||
},
|
|
||||||
|
|
||||||
// ============ Extended SSO/Okta Fields (All Optional) ============
|
|
||||||
manager: {
|
|
||||||
type: DataTypes.STRING(200),
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'Reporting manager name from SSO/AD'
|
|
||||||
},
|
|
||||||
secondEmail: {
|
|
||||||
type: DataTypes.STRING(255),
|
|
||||||
allowNull: true,
|
|
||||||
field: 'second_email',
|
|
||||||
validate: {
|
|
||||||
isEmail: true
|
|
||||||
},
|
|
||||||
comment: 'Alternate email address from SSO'
|
|
||||||
},
|
|
||||||
jobTitle: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'job_title',
|
|
||||||
comment: 'Detailed job title/description from SSO (e.g., "Manages dealers for MotorCycle Business...")'
|
|
||||||
},
|
|
||||||
employeeNumber: {
|
|
||||||
type: DataTypes.STRING(50),
|
|
||||||
allowNull: true,
|
|
||||||
field: 'employee_number',
|
|
||||||
comment: 'HR system employee number from SSO (e.g., "00020330")'
|
|
||||||
},
|
|
||||||
postalAddress: {
|
|
||||||
type: DataTypes.STRING(500),
|
|
||||||
allowNull: true,
|
|
||||||
field: 'postal_address',
|
|
||||||
comment: 'Work location/office address from SSO (e.g., "Kolkata", "Chennai")'
|
|
||||||
},
|
|
||||||
mobilePhone: {
|
|
||||||
type: DataTypes.STRING(20),
|
|
||||||
allowNull: true,
|
|
||||||
field: 'mobile_phone',
|
|
||||||
comment: 'Mobile contact number from SSO (mobilePhone field)'
|
|
||||||
},
|
|
||||||
adGroups: {
|
|
||||||
type: DataTypes.JSONB,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'ad_groups',
|
|
||||||
comment: 'Active Directory group memberships from SSO (memberOf field) - JSON array'
|
|
||||||
},
|
|
||||||
|
|
||||||
// Location Information (JSON object)
|
|
||||||
location: {
|
|
||||||
type: DataTypes.JSONB, // Use JSONB for PostgreSQL
|
|
||||||
allowNull: true,
|
|
||||||
comment: 'JSON object containing location details (city, state, country, office, timezone)'
|
|
||||||
},
|
|
||||||
|
|
||||||
// Notification Preferences
|
|
||||||
emailNotificationsEnabled: {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: true,
|
|
||||||
field: 'email_notifications_enabled',
|
|
||||||
comment: 'User preference for receiving email notifications'
|
|
||||||
},
|
|
||||||
pushNotificationsEnabled: {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: true,
|
|
||||||
field: 'push_notifications_enabled',
|
|
||||||
comment: 'User preference for receiving push notifications'
|
|
||||||
},
|
|
||||||
inAppNotificationsEnabled: {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: true,
|
|
||||||
field: 'in_app_notifications_enabled',
|
|
||||||
comment: 'User preference for receiving in-app notifications'
|
|
||||||
},
|
|
||||||
|
|
||||||
isActive: {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
defaultValue: true,
|
|
||||||
field: 'is_active',
|
|
||||||
comment: 'Account status'
|
|
||||||
},
|
|
||||||
role: {
|
|
||||||
type: DataTypes.ENUM('USER', 'MANAGEMENT', 'ADMIN'),
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: 'USER',
|
|
||||||
comment: 'User role for access control: USER (default), MANAGEMENT (read all), ADMIN (full access)'
|
|
||||||
},
|
|
||||||
lastLogin: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'last_login'
|
|
||||||
},
|
|
||||||
createdAt: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW,
|
|
||||||
field: 'created_at'
|
|
||||||
},
|
|
||||||
updatedAt: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW,
|
|
||||||
field: 'updated_at'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
sequelize,
|
|
||||||
modelName: 'User',
|
|
||||||
tableName: 'users',
|
|
||||||
timestamps: true,
|
|
||||||
createdAt: 'created_at',
|
|
||||||
updatedAt: 'updated_at',
|
|
||||||
indexes: [
|
|
||||||
{
|
|
||||||
unique: true,
|
|
||||||
fields: ['okta_sub']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
unique: true,
|
|
||||||
fields: ['email']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fields: ['employee_id'] // Non-unique index for employee_id (now optional)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fields: ['department']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fields: ['is_active']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fields: ['role'], // Index for role-based queries
|
|
||||||
name: 'idx_users_role'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fields: ['manager'], // Index for org chart queries
|
|
||||||
name: 'idx_users_manager'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fields: ['postal_address'], // Index for location-based filtering
|
|
||||||
name: 'idx_users_postal_address'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fields: ['location'],
|
|
||||||
using: 'gin', // GIN index for JSONB queries
|
|
||||||
operator: 'jsonb_path_ops'
|
|
||||||
}
|
|
||||||
// Note: ad_groups GIN index is created in migration (can't be defined here)
|
|
||||||
]
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export { User };
|
|
||||||
@ -1,74 +0,0 @@
|
|||||||
import { DataTypes, Model, Optional } from 'sequelize';
|
|
||||||
import { sequelize } from '@config/database';
|
|
||||||
|
|
||||||
interface WorkNoteAttributes {
|
|
||||||
noteId: string;
|
|
||||||
requestId: string;
|
|
||||||
userId: string;
|
|
||||||
userName?: string | null;
|
|
||||||
userRole?: string | null;
|
|
||||||
message: string; // rich text (HTML/JSON) stored as TEXT
|
|
||||||
messageType?: string | null; // COMMENT etc
|
|
||||||
isPriority?: boolean | null;
|
|
||||||
hasAttachment?: boolean | null;
|
|
||||||
parentNoteId?: string | null;
|
|
||||||
mentionedUsers?: string[] | null;
|
|
||||||
reactions?: object | null;
|
|
||||||
isEdited?: boolean | null;
|
|
||||||
isDeleted?: boolean | null;
|
|
||||||
createdAt: Date;
|
|
||||||
updatedAt: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface WorkNoteCreationAttributes extends Optional<WorkNoteAttributes, 'noteId' | 'userName' | 'userRole' | 'messageType' | 'isPriority' | 'hasAttachment' | 'parentNoteId' | 'mentionedUsers' | 'reactions' | 'isEdited' | 'isDeleted' | 'createdAt' | 'updatedAt'> {}
|
|
||||||
|
|
||||||
class WorkNote extends Model<WorkNoteAttributes, WorkNoteCreationAttributes> implements WorkNoteAttributes {
|
|
||||||
public noteId!: string;
|
|
||||||
public requestId!: string;
|
|
||||||
public userId!: string;
|
|
||||||
public userName!: string | null;
|
|
||||||
public userRole!: string | null;
|
|
||||||
public message!: string;
|
|
||||||
public messageType!: string | null;
|
|
||||||
public isPriority!: boolean | null;
|
|
||||||
public hasAttachment!: boolean | null;
|
|
||||||
public parentNoteId!: string | null;
|
|
||||||
public mentionedUsers!: string[] | null;
|
|
||||||
public reactions!: object | null;
|
|
||||||
public isEdited!: boolean | null;
|
|
||||||
public isDeleted!: boolean | null;
|
|
||||||
public createdAt!: Date;
|
|
||||||
public updatedAt!: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
WorkNote.init(
|
|
||||||
{
|
|
||||||
noteId: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, primaryKey: true, field: 'note_id' },
|
|
||||||
requestId: { type: DataTypes.UUID, allowNull: false, field: 'request_id' },
|
|
||||||
userId: { type: DataTypes.UUID, allowNull: false, field: 'user_id' },
|
|
||||||
userName: { type: DataTypes.STRING(255), allowNull: true, field: 'user_name' },
|
|
||||||
userRole: { type: DataTypes.STRING(50), allowNull: true, field: 'user_role' },
|
|
||||||
message: { type: DataTypes.TEXT, allowNull: false },
|
|
||||||
messageType: { type: DataTypes.STRING(50), allowNull: true, field: 'message_type' },
|
|
||||||
isPriority: { type: DataTypes.BOOLEAN, allowNull: true, field: 'is_priority' },
|
|
||||||
hasAttachment: { type: DataTypes.BOOLEAN, allowNull: true, field: 'has_attachment' },
|
|
||||||
parentNoteId: { type: DataTypes.UUID, allowNull: true, field: 'parent_note_id' },
|
|
||||||
mentionedUsers: { type: DataTypes.ARRAY(DataTypes.UUID), allowNull: true, field: 'mentioned_users' },
|
|
||||||
reactions: { type: DataTypes.JSONB, allowNull: true },
|
|
||||||
isEdited: { type: DataTypes.BOOLEAN, allowNull: true, field: 'is_edited' },
|
|
||||||
isDeleted: { type: DataTypes.BOOLEAN, allowNull: true, field: 'is_deleted' },
|
|
||||||
createdAt: { type: DataTypes.DATE, allowNull: false, defaultValue: DataTypes.NOW, field: 'created_at' },
|
|
||||||
updatedAt: { type: DataTypes.DATE, allowNull: false, defaultValue: DataTypes.NOW, field: 'updated_at' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
sequelize,
|
|
||||||
modelName: 'WorkNote',
|
|
||||||
tableName: 'work_notes',
|
|
||||||
timestamps: false,
|
|
||||||
indexes: [ { fields: ['request_id'] }, { fields: ['user_id'] }, { fields: ['created_at'] } ]
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export { WorkNote };
|
|
||||||
|
|
||||||
|
|
||||||
@ -1,56 +0,0 @@
|
|||||||
import { DataTypes, Model, Optional } from 'sequelize';
|
|
||||||
import { sequelize } from '@config/database';
|
|
||||||
|
|
||||||
interface WorkNoteAttachmentAttributes {
|
|
||||||
attachmentId: string;
|
|
||||||
noteId: string;
|
|
||||||
fileName: string;
|
|
||||||
fileType: string;
|
|
||||||
fileSize: number;
|
|
||||||
filePath: string;
|
|
||||||
storageUrl?: string | null;
|
|
||||||
isDownloadable?: boolean | null;
|
|
||||||
downloadCount?: number | null;
|
|
||||||
uploadedAt: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface WorkNoteAttachmentCreationAttributes extends Optional<WorkNoteAttachmentAttributes, 'attachmentId' | 'storageUrl' | 'isDownloadable' | 'downloadCount' | 'uploadedAt'> {}
|
|
||||||
|
|
||||||
class WorkNoteAttachment extends Model<WorkNoteAttachmentAttributes, WorkNoteAttachmentCreationAttributes> implements WorkNoteAttachmentAttributes {
|
|
||||||
public attachmentId!: string;
|
|
||||||
public noteId!: string;
|
|
||||||
public fileName!: string;
|
|
||||||
public fileType!: string;
|
|
||||||
public fileSize!: number;
|
|
||||||
public filePath!: string;
|
|
||||||
public storageUrl!: string | null;
|
|
||||||
public isDownloadable!: boolean | null;
|
|
||||||
public downloadCount!: number | null;
|
|
||||||
public uploadedAt!: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
WorkNoteAttachment.init(
|
|
||||||
{
|
|
||||||
attachmentId: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, primaryKey: true, field: 'attachment_id' },
|
|
||||||
noteId: { type: DataTypes.UUID, allowNull: false, field: 'note_id' },
|
|
||||||
fileName: { type: DataTypes.STRING(255), allowNull: false, field: 'file_name' },
|
|
||||||
fileType: { type: DataTypes.STRING(100), allowNull: false, field: 'file_type' },
|
|
||||||
fileSize: { type: DataTypes.BIGINT, allowNull: false, field: 'file_size' },
|
|
||||||
filePath: { type: DataTypes.STRING(500), allowNull: false, field: 'file_path' },
|
|
||||||
storageUrl: { type: DataTypes.STRING(500), allowNull: true, field: 'storage_url' },
|
|
||||||
isDownloadable: { type: DataTypes.BOOLEAN, allowNull: true, field: 'is_downloadable' },
|
|
||||||
downloadCount: { type: DataTypes.INTEGER, allowNull: true, field: 'download_count', defaultValue: 0 },
|
|
||||||
uploadedAt: { type: DataTypes.DATE, allowNull: false, defaultValue: DataTypes.NOW, field: 'uploaded_at' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
sequelize,
|
|
||||||
modelName: 'WorkNoteAttachment',
|
|
||||||
tableName: 'work_note_attachments',
|
|
||||||
timestamps: false,
|
|
||||||
indexes: [ { fields: ['note_id'] }, { fields: ['uploaded_at'] } ]
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export { WorkNoteAttachment };
|
|
||||||
|
|
||||||
|
|
||||||
@ -1,265 +0,0 @@
|
|||||||
import { DataTypes, Model, Optional } from 'sequelize';
|
|
||||||
import { sequelize } from '@config/database';
|
|
||||||
import { User } from './User';
|
|
||||||
import { Priority, WorkflowStatus } from '../types/common.types';
|
|
||||||
|
|
||||||
interface WorkflowRequestAttributes {
|
|
||||||
requestId: string;
|
|
||||||
requestNumber: string;
|
|
||||||
initiatorId: string;
|
|
||||||
templateType: 'CUSTOM' | 'TEMPLATE' | 'DEALER CLAIM';
|
|
||||||
workflowType?: string; // 'NON_TEMPLATIZED' | 'CLAIM_MANAGEMENT' | etc.
|
|
||||||
templateId?: string; // Reference to workflow_templates if using admin template
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
priority: Priority;
|
|
||||||
status: WorkflowStatus;
|
|
||||||
currentLevel: number;
|
|
||||||
totalLevels: number;
|
|
||||||
totalTatHours: number;
|
|
||||||
submissionDate?: Date;
|
|
||||||
closureDate?: Date;
|
|
||||||
conclusionRemark?: string;
|
|
||||||
aiGeneratedConclusion?: string;
|
|
||||||
isDraft: boolean;
|
|
||||||
isDeleted: boolean;
|
|
||||||
isPaused: boolean;
|
|
||||||
pausedAt?: Date;
|
|
||||||
pausedBy?: string;
|
|
||||||
pauseReason?: string;
|
|
||||||
pauseResumeDate?: Date;
|
|
||||||
pauseTatSnapshot?: any;
|
|
||||||
createdAt: Date;
|
|
||||||
updatedAt: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface WorkflowRequestCreationAttributes extends Optional<WorkflowRequestAttributes, 'requestId' | 'submissionDate' | 'closureDate' | 'conclusionRemark' | 'aiGeneratedConclusion' | 'isPaused' | 'pausedAt' | 'pausedBy' | 'pauseReason' | 'pauseResumeDate' | 'pauseTatSnapshot' | 'createdAt' | 'updatedAt'> { }
|
|
||||||
|
|
||||||
class WorkflowRequest extends Model<WorkflowRequestAttributes, WorkflowRequestCreationAttributes> implements WorkflowRequestAttributes {
|
|
||||||
public requestId!: string;
|
|
||||||
public requestNumber!: string;
|
|
||||||
public initiatorId!: string;
|
|
||||||
public templateType!: 'CUSTOM' | 'TEMPLATE' | 'DEALER CLAIM';
|
|
||||||
public workflowType?: string;
|
|
||||||
public templateId?: string;
|
|
||||||
public title!: string;
|
|
||||||
public description!: string;
|
|
||||||
public priority!: Priority;
|
|
||||||
public status!: WorkflowStatus;
|
|
||||||
public currentLevel!: number;
|
|
||||||
public totalLevels!: number;
|
|
||||||
public totalTatHours!: number;
|
|
||||||
public submissionDate?: Date;
|
|
||||||
public closureDate?: Date;
|
|
||||||
public conclusionRemark?: string;
|
|
||||||
public aiGeneratedConclusion?: string;
|
|
||||||
public isDraft!: boolean;
|
|
||||||
public isDeleted!: boolean;
|
|
||||||
public isPaused!: boolean;
|
|
||||||
public pausedAt?: Date;
|
|
||||||
public pausedBy?: string;
|
|
||||||
public pauseReason?: string;
|
|
||||||
public pauseResumeDate?: Date;
|
|
||||||
public pauseTatSnapshot?: any;
|
|
||||||
public createdAt!: Date;
|
|
||||||
public updatedAt!: Date;
|
|
||||||
|
|
||||||
// Associations
|
|
||||||
public initiator?: User;
|
|
||||||
}
|
|
||||||
|
|
||||||
WorkflowRequest.init(
|
|
||||||
{
|
|
||||||
requestId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
defaultValue: DataTypes.UUIDV4,
|
|
||||||
primaryKey: true,
|
|
||||||
field: 'request_id'
|
|
||||||
},
|
|
||||||
requestNumber: {
|
|
||||||
type: DataTypes.STRING(20),
|
|
||||||
allowNull: false,
|
|
||||||
unique: true,
|
|
||||||
field: 'request_number'
|
|
||||||
},
|
|
||||||
initiatorId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
field: 'initiator_id',
|
|
||||||
references: {
|
|
||||||
model: 'users',
|
|
||||||
key: 'user_id'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
templateType: {
|
|
||||||
type: DataTypes.STRING(20),
|
|
||||||
defaultValue: 'CUSTOM',
|
|
||||||
field: 'template_type'
|
|
||||||
},
|
|
||||||
workflowType: {
|
|
||||||
type: DataTypes.STRING(50),
|
|
||||||
allowNull: true,
|
|
||||||
defaultValue: 'NON_TEMPLATIZED',
|
|
||||||
field: 'workflow_type',
|
|
||||||
// Don't fail if column doesn't exist (for backward compatibility with old environments)
|
|
||||||
// Sequelize will handle this gracefully if the column is missing
|
|
||||||
},
|
|
||||||
templateId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'template_id',
|
|
||||||
references: {
|
|
||||||
model: 'workflow_templates',
|
|
||||||
key: 'template_id'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
type: DataTypes.STRING(500),
|
|
||||||
allowNull: false
|
|
||||||
},
|
|
||||||
description: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
allowNull: false
|
|
||||||
},
|
|
||||||
priority: {
|
|
||||||
type: DataTypes.ENUM('STANDARD', 'EXPRESS'),
|
|
||||||
defaultValue: 'STANDARD'
|
|
||||||
},
|
|
||||||
status: {
|
|
||||||
type: DataTypes.ENUM('DRAFT', 'PENDING', 'IN_PROGRESS', 'APPROVED', 'REJECTED', 'CLOSED'),
|
|
||||||
defaultValue: 'DRAFT'
|
|
||||||
},
|
|
||||||
currentLevel: {
|
|
||||||
type: DataTypes.INTEGER,
|
|
||||||
defaultValue: 1,
|
|
||||||
field: 'current_level'
|
|
||||||
},
|
|
||||||
totalLevels: {
|
|
||||||
type: DataTypes.INTEGER,
|
|
||||||
defaultValue: 1,
|
|
||||||
field: 'total_levels',
|
|
||||||
validate: {
|
|
||||||
max: 10
|
|
||||||
}
|
|
||||||
},
|
|
||||||
totalTatHours: {
|
|
||||||
type: DataTypes.DECIMAL(10, 2),
|
|
||||||
defaultValue: 0,
|
|
||||||
field: 'total_tat_hours'
|
|
||||||
},
|
|
||||||
submissionDate: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'submission_date'
|
|
||||||
},
|
|
||||||
closureDate: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'closure_date'
|
|
||||||
},
|
|
||||||
conclusionRemark: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'conclusion_remark'
|
|
||||||
},
|
|
||||||
aiGeneratedConclusion: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'ai_generated_conclusion'
|
|
||||||
},
|
|
||||||
isDraft: {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
defaultValue: true,
|
|
||||||
field: 'is_draft'
|
|
||||||
},
|
|
||||||
isDeleted: {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
defaultValue: false,
|
|
||||||
field: 'is_deleted'
|
|
||||||
},
|
|
||||||
isPaused: {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
defaultValue: false,
|
|
||||||
field: 'is_paused'
|
|
||||||
},
|
|
||||||
pausedAt: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'paused_at'
|
|
||||||
},
|
|
||||||
pausedBy: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'paused_by',
|
|
||||||
references: {
|
|
||||||
model: 'users',
|
|
||||||
key: 'user_id'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
pauseReason: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'pause_reason'
|
|
||||||
},
|
|
||||||
pauseResumeDate: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'pause_resume_date'
|
|
||||||
},
|
|
||||||
pauseTatSnapshot: {
|
|
||||||
type: DataTypes.JSONB,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'pause_tat_snapshot'
|
|
||||||
},
|
|
||||||
createdAt: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW,
|
|
||||||
field: 'created_at'
|
|
||||||
},
|
|
||||||
updatedAt: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW,
|
|
||||||
field: 'updated_at'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
sequelize,
|
|
||||||
modelName: 'WorkflowRequest',
|
|
||||||
tableName: 'workflow_requests',
|
|
||||||
timestamps: true,
|
|
||||||
createdAt: 'created_at',
|
|
||||||
updatedAt: 'updated_at',
|
|
||||||
indexes: [
|
|
||||||
{
|
|
||||||
fields: ['initiator_id']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fields: ['status']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
unique: true,
|
|
||||||
fields: ['request_number']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fields: ['created_at']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fields: ['workflow_type']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fields: ['template_id']
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Associations
|
|
||||||
WorkflowRequest.belongsTo(User, {
|
|
||||||
as: 'initiator',
|
|
||||||
foreignKey: 'initiatorId',
|
|
||||||
targetKey: 'userId'
|
|
||||||
});
|
|
||||||
|
|
||||||
export { WorkflowRequest };
|
|
||||||
@ -1,177 +0,0 @@
|
|||||||
import { DataTypes, Model, Optional } from 'sequelize';
|
|
||||||
import { sequelize } from '../config/database';
|
|
||||||
import { User } from './User';
|
|
||||||
|
|
||||||
interface WorkflowTemplateAttributes {
|
|
||||||
templateId: string;
|
|
||||||
templateName: string;
|
|
||||||
templateCode?: string;
|
|
||||||
templateDescription?: string;
|
|
||||||
templateCategory?: string;
|
|
||||||
workflowType?: string;
|
|
||||||
approvalLevelsConfig?: any;
|
|
||||||
defaultTatHours?: number;
|
|
||||||
formStepsConfig?: any;
|
|
||||||
userFieldMappings?: any;
|
|
||||||
dynamicApproverConfig?: any;
|
|
||||||
isActive: boolean;
|
|
||||||
isSystemTemplate: boolean;
|
|
||||||
usageCount: number;
|
|
||||||
createdBy?: string;
|
|
||||||
createdAt: Date;
|
|
||||||
updatedAt: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface WorkflowTemplateCreationAttributes extends Optional<WorkflowTemplateAttributes, 'templateId' | 'templateCode' | 'templateDescription' | 'templateCategory' | 'workflowType' | 'approvalLevelsConfig' | 'defaultTatHours' | 'formStepsConfig' | 'userFieldMappings' | 'dynamicApproverConfig' | 'createdBy' | 'createdAt' | 'updatedAt'> { }
|
|
||||||
|
|
||||||
export class WorkflowTemplate extends Model<WorkflowTemplateAttributes, WorkflowTemplateCreationAttributes> implements WorkflowTemplateAttributes {
|
|
||||||
public templateId!: string;
|
|
||||||
public templateName!: string;
|
|
||||||
public templateCode?: string;
|
|
||||||
public templateDescription?: string;
|
|
||||||
public templateCategory?: string;
|
|
||||||
public workflowType?: string;
|
|
||||||
public approvalLevelsConfig?: any;
|
|
||||||
public defaultTatHours?: number;
|
|
||||||
public formStepsConfig?: any;
|
|
||||||
public userFieldMappings?: any;
|
|
||||||
public dynamicApproverConfig?: any;
|
|
||||||
public isActive!: boolean;
|
|
||||||
public isSystemTemplate!: boolean;
|
|
||||||
public usageCount!: number;
|
|
||||||
public createdBy?: string;
|
|
||||||
public createdAt!: Date;
|
|
||||||
public updatedAt!: Date;
|
|
||||||
|
|
||||||
// Associations
|
|
||||||
public creator?: User;
|
|
||||||
}
|
|
||||||
|
|
||||||
WorkflowTemplate.init(
|
|
||||||
{
|
|
||||||
templateId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
defaultValue: DataTypes.UUIDV4,
|
|
||||||
primaryKey: true,
|
|
||||||
field: 'template_id'
|
|
||||||
},
|
|
||||||
templateName: {
|
|
||||||
type: DataTypes.STRING(200),
|
|
||||||
allowNull: false,
|
|
||||||
field: 'template_name'
|
|
||||||
},
|
|
||||||
templateCode: {
|
|
||||||
type: DataTypes.STRING(50),
|
|
||||||
allowNull: true,
|
|
||||||
unique: true,
|
|
||||||
field: 'template_code'
|
|
||||||
},
|
|
||||||
templateDescription: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'template_description'
|
|
||||||
},
|
|
||||||
templateCategory: {
|
|
||||||
type: DataTypes.STRING(100),
|
|
||||||
allowNull: true,
|
|
||||||
field: 'template_category'
|
|
||||||
},
|
|
||||||
workflowType: {
|
|
||||||
type: DataTypes.STRING(50),
|
|
||||||
allowNull: true,
|
|
||||||
field: 'workflow_type'
|
|
||||||
},
|
|
||||||
approvalLevelsConfig: {
|
|
||||||
type: DataTypes.JSONB,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'approval_levels_config'
|
|
||||||
},
|
|
||||||
defaultTatHours: {
|
|
||||||
type: DataTypes.DECIMAL(10, 2),
|
|
||||||
allowNull: true,
|
|
||||||
defaultValue: 24,
|
|
||||||
field: 'default_tat_hours'
|
|
||||||
},
|
|
||||||
formStepsConfig: {
|
|
||||||
type: DataTypes.JSONB,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'form_steps_config'
|
|
||||||
},
|
|
||||||
userFieldMappings: {
|
|
||||||
type: DataTypes.JSONB,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'user_field_mappings'
|
|
||||||
},
|
|
||||||
dynamicApproverConfig: {
|
|
||||||
type: DataTypes.JSONB,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'dynamic_approver_config'
|
|
||||||
},
|
|
||||||
isActive: {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: true,
|
|
||||||
field: 'is_active'
|
|
||||||
},
|
|
||||||
isSystemTemplate: {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: false,
|
|
||||||
field: 'is_system_template'
|
|
||||||
},
|
|
||||||
usageCount: {
|
|
||||||
type: DataTypes.INTEGER,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: 0,
|
|
||||||
field: 'usage_count'
|
|
||||||
},
|
|
||||||
createdBy: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'created_by',
|
|
||||||
references: {
|
|
||||||
model: 'users',
|
|
||||||
key: 'user_id'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
createdAt: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW,
|
|
||||||
field: 'created_at'
|
|
||||||
},
|
|
||||||
updatedAt: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW,
|
|
||||||
field: 'updated_at'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
sequelize,
|
|
||||||
modelName: 'WorkflowTemplate',
|
|
||||||
tableName: 'workflow_templates',
|
|
||||||
timestamps: true,
|
|
||||||
createdAt: 'created_at',
|
|
||||||
updatedAt: 'updated_at',
|
|
||||||
indexes: [
|
|
||||||
{
|
|
||||||
unique: true,
|
|
||||||
fields: ['template_code']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fields: ['workflow_type']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fields: ['is_active']
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Associations
|
|
||||||
WorkflowTemplate.belongsTo(User, {
|
|
||||||
as: 'creator',
|
|
||||||
foreignKey: 'createdBy',
|
|
||||||
targetKey: 'userId'
|
|
||||||
});
|
|
||||||
@ -1,186 +1,20 @@
|
|||||||
import { sequelize } from '@config/database';
|
export { ActivityModel as Activity } from './mongoose/Activity.schema';
|
||||||
|
export { ActivityTypeModel as ActivityType } from './mongoose/ActivityType.schema';
|
||||||
// Import all models
|
export { AdminConfigurationModel as AdminConfiguration } from './mongoose/AdminConfiguration.schema';
|
||||||
import { User } from './User';
|
export { ApprovalLevelModel as ApprovalLevel } from './mongoose/ApprovalLevel.schema';
|
||||||
import { WorkflowRequest } from './WorkflowRequest';
|
export { ConclusionRemarkModel as ConclusionRemark } from './mongoose/ConclusionRemark.schema';
|
||||||
import { ApprovalLevel } from './ApprovalLevel';
|
export { DealerModel as Dealer } from './mongoose/Dealer.schema';
|
||||||
import { Participant } from './Participant';
|
export { DealerClaimModel as DealerClaimDetails } from './mongoose/DealerClaim.schema'; // Alias to match previous usage
|
||||||
import { Document } from './Document';
|
export { DocumentModel as Document } from './mongoose/Document.schema';
|
||||||
import { Subscription } from './Subscription';
|
export { HolidayModel as Holiday } from './mongoose/Holiday.schema';
|
||||||
import { Activity } from './Activity';
|
export { InternalOrderModel as InternalOrder } from './mongoose/InternalOrder.schema';
|
||||||
import { WorkNote } from './WorkNote';
|
export { NotificationModel as Notification } from './mongoose/Notification.schema';
|
||||||
import { WorkNoteAttachment } from './WorkNoteAttachment';
|
export { ParticipantModel as Participant } from './mongoose/Participant.schema';
|
||||||
import { TatAlert } from './TatAlert';
|
export { RequestSummaryModel as RequestSummary } from './mongoose/RequestSummary.schema';
|
||||||
import { Holiday } from './Holiday';
|
export { SubscriptionModel as Subscription } from './mongoose/Subscription.schema';
|
||||||
import { Notification } from './Notification';
|
export { TatAlertModel as TatAlert } from './mongoose/TatAlert.schema';
|
||||||
import ConclusionRemark from './ConclusionRemark';
|
export { UserModel as User } from './mongoose/User.schema';
|
||||||
import RequestSummary from './RequestSummary';
|
export { WorkNoteModel as WorkNote } from './mongoose/WorkNote.schema';
|
||||||
import SharedSummary from './SharedSummary';
|
export { WorkNoteAttachmentModel as WorkNoteAttachment } from './mongoose/WorkNoteAttachment.schema';
|
||||||
import { DealerClaimDetails } from './DealerClaimDetails';
|
export { WorkflowRequestModel as WorkflowRequest } from './mongoose/WorkflowRequest.schema';
|
||||||
import { DealerProposalDetails } from './DealerProposalDetails';
|
export { WorkflowTemplateModel as WorkflowTemplate } from './mongoose/WorkflowTemplate.schema';
|
||||||
import { DealerCompletionDetails } from './DealerCompletionDetails';
|
|
||||||
import { DealerProposalCostItem } from './DealerProposalCostItem';
|
|
||||||
import { InternalOrder } from './InternalOrder';
|
|
||||||
import { ClaimBudgetTracking } from './ClaimBudgetTracking';
|
|
||||||
import { Dealer } from './Dealer';
|
|
||||||
import { ActivityType } from './ActivityType';
|
|
||||||
import { DealerClaimHistory } from './DealerClaimHistory';
|
|
||||||
import { WorkflowTemplate } from './WorkflowTemplate';
|
|
||||||
|
|
||||||
// Define associations
|
|
||||||
const defineAssociations = () => {
|
|
||||||
// User associations
|
|
||||||
User.hasMany(WorkflowRequest, {
|
|
||||||
as: 'initiatedRequests',
|
|
||||||
foreignKey: 'initiatorId',
|
|
||||||
sourceKey: 'userId'
|
|
||||||
});
|
|
||||||
|
|
||||||
User.hasMany(ApprovalLevel, {
|
|
||||||
as: 'approvalLevels',
|
|
||||||
foreignKey: 'approverId',
|
|
||||||
sourceKey: 'userId'
|
|
||||||
});
|
|
||||||
|
|
||||||
User.hasMany(Participant, {
|
|
||||||
as: 'participations',
|
|
||||||
foreignKey: 'userId',
|
|
||||||
sourceKey: 'userId'
|
|
||||||
});
|
|
||||||
|
|
||||||
User.hasMany(Document, {
|
|
||||||
as: 'uploadedDocuments',
|
|
||||||
foreignKey: 'uploadedBy',
|
|
||||||
sourceKey: 'userId'
|
|
||||||
});
|
|
||||||
|
|
||||||
// WorkflowRequest associations
|
|
||||||
WorkflowRequest.hasMany(ApprovalLevel, {
|
|
||||||
as: 'approvalLevels',
|
|
||||||
foreignKey: 'requestId',
|
|
||||||
sourceKey: 'requestId'
|
|
||||||
});
|
|
||||||
|
|
||||||
WorkflowRequest.hasMany(Participant, {
|
|
||||||
as: 'participants',
|
|
||||||
foreignKey: 'requestId',
|
|
||||||
sourceKey: 'requestId'
|
|
||||||
});
|
|
||||||
|
|
||||||
WorkflowRequest.hasMany(Document, {
|
|
||||||
as: 'documents',
|
|
||||||
foreignKey: 'requestId',
|
|
||||||
sourceKey: 'requestId'
|
|
||||||
});
|
|
||||||
|
|
||||||
WorkflowRequest.hasOne(ConclusionRemark, {
|
|
||||||
as: 'conclusion',
|
|
||||||
foreignKey: 'requestId',
|
|
||||||
sourceKey: 'requestId'
|
|
||||||
});
|
|
||||||
|
|
||||||
ConclusionRemark.belongsTo(WorkflowRequest, {
|
|
||||||
foreignKey: 'requestId',
|
|
||||||
targetKey: 'requestId'
|
|
||||||
});
|
|
||||||
|
|
||||||
ConclusionRemark.belongsTo(User, {
|
|
||||||
as: 'editor',
|
|
||||||
foreignKey: 'editedBy',
|
|
||||||
targetKey: 'userId'
|
|
||||||
});
|
|
||||||
|
|
||||||
// RequestSummary associations
|
|
||||||
// Note: belongsTo associations are defined in the model files to avoid duplicate alias conflicts
|
|
||||||
// Only hasOne/hasMany associations are defined here
|
|
||||||
WorkflowRequest.hasOne(RequestSummary, {
|
|
||||||
as: 'summary',
|
|
||||||
foreignKey: 'requestId',
|
|
||||||
sourceKey: 'requestId'
|
|
||||||
});
|
|
||||||
|
|
||||||
RequestSummary.hasMany(SharedSummary, {
|
|
||||||
as: 'sharedSummaries',
|
|
||||||
foreignKey: 'summaryId',
|
|
||||||
sourceKey: 'summaryId'
|
|
||||||
});
|
|
||||||
|
|
||||||
// User associations for summaries
|
|
||||||
User.hasMany(RequestSummary, {
|
|
||||||
as: 'createdSummaries',
|
|
||||||
foreignKey: 'initiatorId',
|
|
||||||
sourceKey: 'userId'
|
|
||||||
});
|
|
||||||
|
|
||||||
User.hasMany(SharedSummary, {
|
|
||||||
as: 'sharedByMe',
|
|
||||||
foreignKey: 'sharedBy',
|
|
||||||
sourceKey: 'userId'
|
|
||||||
});
|
|
||||||
|
|
||||||
User.hasMany(SharedSummary, {
|
|
||||||
as: 'sharedWithMe',
|
|
||||||
foreignKey: 'sharedWith',
|
|
||||||
sourceKey: 'userId'
|
|
||||||
});
|
|
||||||
|
|
||||||
// InternalOrder associations
|
|
||||||
WorkflowRequest.hasOne(InternalOrder, {
|
|
||||||
as: 'internalOrder',
|
|
||||||
foreignKey: 'requestId',
|
|
||||||
sourceKey: 'requestId'
|
|
||||||
});
|
|
||||||
|
|
||||||
// ClaimBudgetTracking associations
|
|
||||||
WorkflowRequest.hasOne(ClaimBudgetTracking, {
|
|
||||||
as: 'budgetTracking',
|
|
||||||
foreignKey: 'requestId',
|
|
||||||
sourceKey: 'requestId'
|
|
||||||
});
|
|
||||||
|
|
||||||
// DealerClaimHistory associations
|
|
||||||
WorkflowRequest.hasMany(DealerClaimHistory, {
|
|
||||||
as: 'history',
|
|
||||||
foreignKey: 'requestId',
|
|
||||||
sourceKey: 'requestId'
|
|
||||||
});
|
|
||||||
|
|
||||||
// Note: belongsTo associations are defined in individual model files to avoid duplicate alias conflicts
|
|
||||||
// Only hasMany associations from WorkflowRequest are defined here since they're one-way
|
|
||||||
};
|
|
||||||
|
|
||||||
// Initialize associations
|
|
||||||
defineAssociations();
|
|
||||||
|
|
||||||
// Export models and sequelize instance
|
|
||||||
export {
|
|
||||||
sequelize,
|
|
||||||
User,
|
|
||||||
WorkflowRequest,
|
|
||||||
ApprovalLevel,
|
|
||||||
Participant,
|
|
||||||
Document,
|
|
||||||
Subscription,
|
|
||||||
Activity,
|
|
||||||
WorkNote,
|
|
||||||
WorkNoteAttachment,
|
|
||||||
TatAlert,
|
|
||||||
Holiday,
|
|
||||||
Notification,
|
|
||||||
ConclusionRemark,
|
|
||||||
RequestSummary,
|
|
||||||
SharedSummary,
|
|
||||||
WorkflowTemplate,
|
|
||||||
DealerClaimDetails,
|
|
||||||
DealerProposalDetails,
|
|
||||||
DealerCompletionDetails,
|
|
||||||
DealerProposalCostItem,
|
|
||||||
InternalOrder,
|
|
||||||
ClaimBudgetTracking,
|
|
||||||
Dealer,
|
|
||||||
ActivityType,
|
|
||||||
DealerClaimHistory
|
|
||||||
};
|
|
||||||
|
|
||||||
// Export default sequelize instance
|
|
||||||
export default sequelize;
|
|
||||||
|
|||||||
@ -1,22 +1,120 @@
|
|||||||
import { Schema, model, Document } from 'mongoose';
|
import mongoose, { Schema, Document } from 'mongoose';
|
||||||
|
|
||||||
export interface IAdminConfiguration extends Document {
|
export interface IAdminConfiguration extends Document {
|
||||||
configKey: string;
|
configKey: string;
|
||||||
configValue: string;
|
configCategory: string;
|
||||||
description?: string;
|
configValue: any;
|
||||||
updatedBy?: string;
|
valueType: string;
|
||||||
|
displayName: string;
|
||||||
|
description: string;
|
||||||
|
defaultValue: any;
|
||||||
|
isEditable: boolean;
|
||||||
|
isSensitive: boolean;
|
||||||
|
validationRules?: object;
|
||||||
|
uiComponent?: string;
|
||||||
|
options?: any[];
|
||||||
|
sortOrder: number;
|
||||||
|
requiresRestart: boolean;
|
||||||
|
lastModifiedBy?: string;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AdminConfigurationSchema = new Schema<IAdminConfiguration>({
|
const AdminConfigurationSchema = new Schema<IAdminConfiguration>(
|
||||||
configKey: { type: String, required: true, unique: true, index: true },
|
{
|
||||||
configValue: { type: String, required: true },
|
configKey: {
|
||||||
description: { type: String },
|
type: String,
|
||||||
updatedBy: { type: String }
|
required: true,
|
||||||
}, {
|
unique: true,
|
||||||
timestamps: true,
|
index: true,
|
||||||
collection: 'admin_configurations'
|
trim: true,
|
||||||
});
|
uppercase: true
|
||||||
|
},
|
||||||
|
configCategory: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
index: true,
|
||||||
|
enum: [
|
||||||
|
'TAT_SETTINGS',
|
||||||
|
'AI_CONFIG',
|
||||||
|
'AI_CONFIGURATION',
|
||||||
|
'DOCUMENT_POLICY',
|
||||||
|
'WORKFLOW_SHARING',
|
||||||
|
'WORKFLOW_SETTINGS',
|
||||||
|
'SYSTEM_SETTINGS',
|
||||||
|
'NOTIFICATION_SETTINGS',
|
||||||
|
'NOTIFICATION_RULES',
|
||||||
|
'SECURITY_SETTINGS',
|
||||||
|
'DASHBOARD_LAYOUT'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
configValue: {
|
||||||
|
type: Schema.Types.Mixed,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
valueType: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
enum: ['STRING', 'NUMBER', 'BOOLEAN', 'JSON', 'ARRAY']
|
||||||
|
},
|
||||||
|
displayName: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
defaultValue: {
|
||||||
|
type: Schema.Types.Mixed,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
isEditable: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
isSensitive: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
validationRules: {
|
||||||
|
type: Schema.Types.Mixed,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
uiComponent: {
|
||||||
|
type: String,
|
||||||
|
enum: ['text', 'number', 'toggle', 'select', 'multiselect', 'textarea', 'json', 'slider'],
|
||||||
|
default: 'text'
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
type: [Schema.Types.Mixed],
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
sortOrder: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
default: 0,
|
||||||
|
index: true
|
||||||
|
},
|
||||||
|
requiresRestart: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
lastModifiedBy: {
|
||||||
|
type: String,
|
||||||
|
default: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamps: true,
|
||||||
|
collection: 'admin_configurations'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export const AdminConfigurationModel = model<IAdminConfiguration>('AdminConfiguration', AdminConfigurationSchema);
|
// Compound index for efficient querying
|
||||||
|
AdminConfigurationSchema.index({ configCategory: 1, sortOrder: 1 });
|
||||||
|
|
||||||
|
export const AdminConfigurationModel = mongoose.model<IAdminConfiguration>(
|
||||||
|
'AdminConfiguration',
|
||||||
|
AdminConfigurationSchema
|
||||||
|
);
|
||||||
|
|||||||
@ -22,13 +22,14 @@ export interface IApprovalLevel extends Document {
|
|||||||
remainingHours: number;
|
remainingHours: number;
|
||||||
percentageUsed: number;
|
percentageUsed: number;
|
||||||
isBreached: boolean;
|
isBreached: boolean;
|
||||||
breachReason?: string;
|
breachReason?: string; // Added
|
||||||
};
|
};
|
||||||
|
|
||||||
status: 'PENDING' | 'IN_PROGRESS' | 'APPROVED' | 'REJECTED' | 'SKIPPED' | 'PAUSED';
|
status: 'PENDING' | 'IN_PROGRESS' | 'APPROVED' | 'REJECTED' | 'SKIPPED' | 'PAUSED';
|
||||||
actionDate?: Date;
|
actionDate?: Date;
|
||||||
comments?: string;
|
comments?: string;
|
||||||
rejectionReason?: string;
|
rejectionReason?: string;
|
||||||
|
breachReason?: string; // Top-level breach reason
|
||||||
isFinalApprover: boolean;
|
isFinalApprover: boolean;
|
||||||
|
|
||||||
alerts: {
|
alerts: {
|
||||||
@ -85,6 +86,7 @@ const ApprovalLevelSchema = new Schema<IApprovalLevel>({
|
|||||||
actionDate: Date,
|
actionDate: Date,
|
||||||
comments: String,
|
comments: String,
|
||||||
rejectionReason: String,
|
rejectionReason: String,
|
||||||
|
breachReason: String, // Top-level
|
||||||
isFinalApprover: { type: Boolean, default: false },
|
isFinalApprover: { type: Boolean, default: false },
|
||||||
|
|
||||||
alerts: {
|
alerts: {
|
||||||
|
|||||||
@ -3,17 +3,65 @@ import mongoose, { Schema, Document } from 'mongoose';
|
|||||||
export interface IConclusionRemark extends Document {
|
export interface IConclusionRemark extends Document {
|
||||||
conclusionId: string;
|
conclusionId: string;
|
||||||
requestId: string;
|
requestId: string;
|
||||||
remark: string;
|
|
||||||
authorId: string;
|
// Manual conclusion
|
||||||
|
finalRemark?: string;
|
||||||
|
|
||||||
|
// AI generated
|
||||||
|
aiGeneratedRemark?: string;
|
||||||
|
aiModelUsed?: string;
|
||||||
|
aiConfidenceScore?: number;
|
||||||
|
|
||||||
|
// Summaries
|
||||||
|
approvalSummary?: {
|
||||||
|
totalLevels?: number;
|
||||||
|
approvedLevels?: number;
|
||||||
|
averageTatUsage?: number;
|
||||||
|
};
|
||||||
|
documentSummary?: {
|
||||||
|
totalDocuments?: number;
|
||||||
|
documentNames?: string[];
|
||||||
|
};
|
||||||
|
keyDiscussionPoints?: string[];
|
||||||
|
|
||||||
|
// Metadata
|
||||||
|
editedBy?: string; // userId
|
||||||
|
isEdited?: boolean;
|
||||||
|
editCount?: number;
|
||||||
|
|
||||||
|
generatedAt?: Date;
|
||||||
|
finalizedAt?: Date;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ConclusionRemarkSchema = new Schema<IConclusionRemark>({
|
const ConclusionRemarkSchema = new Schema<IConclusionRemark>({
|
||||||
conclusionId: { type: String, required: true, unique: true },
|
conclusionId: { type: String, required: false, unique: false }, // Can be auto-generated or UUID
|
||||||
requestId: { type: String, required: true, index: true },
|
requestId: { type: String, required: true, index: true },
|
||||||
remark: { type: String, required: true },
|
|
||||||
authorId: { type: String, required: true },
|
finalRemark: String,
|
||||||
createdAt: { type: Date, default: Date.now }
|
|
||||||
|
aiGeneratedRemark: String,
|
||||||
|
aiModelUsed: String,
|
||||||
|
aiConfidenceScore: Number,
|
||||||
|
|
||||||
|
approvalSummary: {
|
||||||
|
totalLevels: Number,
|
||||||
|
approvedLevels: Number,
|
||||||
|
averageTatUsage: Number
|
||||||
|
},
|
||||||
|
documentSummary: {
|
||||||
|
totalDocuments: Number,
|
||||||
|
documentNames: [String]
|
||||||
|
},
|
||||||
|
keyDiscussionPoints: [String],
|
||||||
|
|
||||||
|
editedBy: String,
|
||||||
|
isEdited: { type: Boolean, default: false },
|
||||||
|
editCount: { type: Number, default: 0 },
|
||||||
|
|
||||||
|
generatedAt: Date,
|
||||||
|
finalizedAt: Date
|
||||||
}, {
|
}, {
|
||||||
timestamps: true,
|
timestamps: true,
|
||||||
collection: 'conclusion_remarks'
|
collection: 'conclusion_remarks'
|
||||||
|
|||||||
@ -1,26 +1,53 @@
|
|||||||
import mongoose, { Schema, Document } from 'mongoose';
|
import mongoose, { Schema, Document } from 'mongoose';
|
||||||
|
|
||||||
export interface IDealer extends Document {
|
export interface IDealer extends Document {
|
||||||
dealerCode: string; // Primary ID
|
dealerId: string;
|
||||||
dealerName: string;
|
salesCode?: string | null;
|
||||||
region: string;
|
serviceCode?: string | null;
|
||||||
state: string;
|
gearCode?: string | null;
|
||||||
city: string;
|
gmaCode?: string | null;
|
||||||
zone: string;
|
region?: string | null;
|
||||||
location: string;
|
dealership?: string | null;
|
||||||
sapCode: string;
|
state?: string | null;
|
||||||
email?: string;
|
district?: string | null;
|
||||||
phone?: string;
|
city?: string | null;
|
||||||
address?: string;
|
location?: string | null;
|
||||||
|
|
||||||
gstin?: string;
|
// Additional fields from Sequelize model
|
||||||
pan?: string;
|
cityCategoryPst?: string | null;
|
||||||
bankDetails?: {
|
layoutFormat?: string | null;
|
||||||
accountName: string;
|
tierCityCategory?: string | null;
|
||||||
accountNumber: string;
|
onBoardingCharges?: string | null;
|
||||||
bankName: string;
|
date?: string | null;
|
||||||
ifscCode: string;
|
singleFormatMonthYear?: string | null;
|
||||||
};
|
domainId?: string | null;
|
||||||
|
replacement?: string | null;
|
||||||
|
terminationResignationStatus?: string | null;
|
||||||
|
dateOfTerminationResignation?: string | null;
|
||||||
|
lastDateOfOperations?: string | null;
|
||||||
|
oldCodes?: string | null;
|
||||||
|
branchDetails?: string | null;
|
||||||
|
dealerPrincipalName?: string | null;
|
||||||
|
dealerPrincipalEmailId?: string | null;
|
||||||
|
dpContactNumber?: string | null;
|
||||||
|
dpContacts?: string | null;
|
||||||
|
showroomAddress?: string | null;
|
||||||
|
showroomPincode?: string | null;
|
||||||
|
workshopAddress?: string | null;
|
||||||
|
workshopPincode?: string | null;
|
||||||
|
locationDistrict?: string | null;
|
||||||
|
stateWorkshop?: string | null;
|
||||||
|
noOfStudios?: number | null;
|
||||||
|
websiteUpdate?: string | null;
|
||||||
|
gst?: string | null;
|
||||||
|
pan?: string | null;
|
||||||
|
firmType?: string | null;
|
||||||
|
propManagingPartnersDirectors?: string | null;
|
||||||
|
totalPropPartnersDirectors?: string | null;
|
||||||
|
docsFolderLink?: string | null;
|
||||||
|
workshopGmaCodes?: string | null;
|
||||||
|
existingNew?: string | null;
|
||||||
|
dlrcode?: string | null;
|
||||||
|
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
@ -28,34 +55,80 @@ export interface IDealer extends Document {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const DealerSchema = new Schema<IDealer>({
|
const DealerSchema = new Schema<IDealer>({
|
||||||
dealerCode: { type: String, required: true, unique: true, index: true },
|
dealerId: { type: String, required: true, unique: true, index: true },
|
||||||
dealerName: { type: String, required: true },
|
|
||||||
region: { type: String, required: true },
|
|
||||||
state: { type: String, required: true },
|
|
||||||
city: { type: String, required: true },
|
|
||||||
zone: { type: String, required: true },
|
|
||||||
location: { type: String, required: true },
|
|
||||||
sapCode: { type: String, required: true },
|
|
||||||
|
|
||||||
email: String,
|
// Codes
|
||||||
phone: String,
|
salesCode: { type: String, index: true },
|
||||||
address: String,
|
serviceCode: { type: String, index: true },
|
||||||
|
gearCode: String,
|
||||||
|
gmaCode: { type: String, index: true },
|
||||||
|
dlrcode: { type: String, index: true },
|
||||||
|
|
||||||
gstin: String,
|
// Location
|
||||||
|
region: { type: String, index: true },
|
||||||
|
state: { type: String, index: true },
|
||||||
|
district: { type: String, index: true },
|
||||||
|
city: { type: String, index: true },
|
||||||
|
location: String,
|
||||||
|
dealership: String,
|
||||||
|
|
||||||
|
// Additional Info
|
||||||
|
cityCategoryPst: String,
|
||||||
|
layoutFormat: String,
|
||||||
|
tierCityCategory: String,
|
||||||
|
onBoardingCharges: String,
|
||||||
|
date: String,
|
||||||
|
singleFormatMonthYear: String,
|
||||||
|
domainId: { type: String, index: true },
|
||||||
|
replacement: String,
|
||||||
|
terminationResignationStatus: String,
|
||||||
|
dateOfTerminationResignation: String,
|
||||||
|
lastDateOfOperations: String,
|
||||||
|
oldCodes: String,
|
||||||
|
branchDetails: String,
|
||||||
|
|
||||||
|
// Principal Info
|
||||||
|
dealerPrincipalName: String,
|
||||||
|
dealerPrincipalEmailId: String,
|
||||||
|
dpContactNumber: String,
|
||||||
|
dpContacts: String,
|
||||||
|
|
||||||
|
// Addresses
|
||||||
|
showroomAddress: String,
|
||||||
|
showroomPincode: String,
|
||||||
|
workshopAddress: String,
|
||||||
|
workshopPincode: String,
|
||||||
|
locationDistrict: String,
|
||||||
|
stateWorkshop: String,
|
||||||
|
|
||||||
|
// Other
|
||||||
|
noOfStudios: Number,
|
||||||
|
websiteUpdate: String,
|
||||||
|
gst: String,
|
||||||
pan: String,
|
pan: String,
|
||||||
bankDetails: {
|
firmType: String,
|
||||||
accountName: String,
|
propManagingPartnersDirectors: String,
|
||||||
accountNumber: String,
|
totalPropPartnersDirectors: String,
|
||||||
bankName: String,
|
docsFolderLink: String,
|
||||||
ifscCode: String
|
workshopGmaCodes: String,
|
||||||
},
|
existingNew: String,
|
||||||
|
|
||||||
isActive: { type: Boolean, default: true },
|
isActive: { type: Boolean, default: true, index: true }
|
||||||
createdAt: { type: Date, default: Date.now },
|
|
||||||
updatedAt: { type: Date, default: Date.now }
|
|
||||||
}, {
|
}, {
|
||||||
timestamps: true,
|
timestamps: true,
|
||||||
collection: 'dealers'
|
collection: 'dealers'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Indexes matching Sequelize model
|
||||||
|
DealerSchema.index({ salesCode: 1 });
|
||||||
|
DealerSchema.index({ serviceCode: 1 });
|
||||||
|
DealerSchema.index({ gmaCode: 1 });
|
||||||
|
DealerSchema.index({ domainId: 1 });
|
||||||
|
DealerSchema.index({ region: 1 });
|
||||||
|
DealerSchema.index({ state: 1 });
|
||||||
|
DealerSchema.index({ city: 1 });
|
||||||
|
DealerSchema.index({ district: 1 });
|
||||||
|
DealerSchema.index({ dlrcode: 1 });
|
||||||
|
DealerSchema.index({ isActive: 1 });
|
||||||
|
|
||||||
export const DealerModel = mongoose.model<IDealer>('Dealer', DealerSchema);
|
export const DealerModel = mongoose.model<IDealer>('Dealer', DealerSchema);
|
||||||
|
|||||||
@ -15,10 +15,17 @@ export interface IDocument extends Document {
|
|||||||
mimeType: string;
|
mimeType: string;
|
||||||
checksum?: string;
|
checksum?: string;
|
||||||
|
|
||||||
category: 'SUPPORTING' | 'INVALID_INVOICE' | 'COMMERCIAL' | 'OTHER';
|
category: 'SUPPORTING' | 'INVALID_INVOICE' | 'COMMERCIAL' | 'OTHER' | 'COMPLETION_DOC' | 'ACTIVITY_PHOTO';
|
||||||
version: number;
|
version: number;
|
||||||
isDeleted: boolean;
|
isDeleted: boolean;
|
||||||
|
|
||||||
|
// Added fields
|
||||||
|
parentDocumentId?: string;
|
||||||
|
downloadCount?: number;
|
||||||
|
isGoogleDoc?: boolean;
|
||||||
|
googleDocUrl?: string;
|
||||||
|
|
||||||
|
uploadedAt: Date; // Map to createdAt
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
}
|
}
|
||||||
@ -45,7 +52,14 @@ const DocumentSchema = new Schema<IDocument>({
|
|||||||
},
|
},
|
||||||
|
|
||||||
version: { type: Number, default: 1 },
|
version: { type: Number, default: 1 },
|
||||||
isDeleted: { type: Boolean, default: false }
|
isDeleted: { type: Boolean, default: false },
|
||||||
|
|
||||||
|
parentDocumentId: String,
|
||||||
|
downloadCount: { type: Number, default: 0 },
|
||||||
|
isGoogleDoc: { type: Boolean, default: false },
|
||||||
|
googleDocUrl: String,
|
||||||
|
|
||||||
|
uploadedAt: { type: Date, default: Date.now } // Redundant but requested by controller
|
||||||
}, {
|
}, {
|
||||||
timestamps: true,
|
timestamps: true,
|
||||||
collection: 'documents'
|
collection: 'documents'
|
||||||
|
|||||||
@ -1,21 +1,46 @@
|
|||||||
import mongoose, { Schema, Document } from 'mongoose';
|
import mongoose, { Schema, Document } from 'mongoose';
|
||||||
|
|
||||||
|
export enum HolidayType {
|
||||||
|
PUBLIC = 'PUBLIC',
|
||||||
|
OPTIONAL = 'OPTIONAL',
|
||||||
|
WEEKEND = 'WEEKEND',
|
||||||
|
ORGANIZATIONAL = 'ORGANIZATIONAL', // Added based on controller usage
|
||||||
|
NATIONAL = 'NATIONAL',
|
||||||
|
REGIONAL = 'REGIONAL'
|
||||||
|
}
|
||||||
|
|
||||||
export interface IHoliday extends Document {
|
export interface IHoliday extends Document {
|
||||||
date: Date;
|
holidayDate: Date;
|
||||||
name: string;
|
holidayName: string;
|
||||||
type: 'PUBLIC' | 'OPTIONAL' | 'WEEKEND';
|
holidayType: HolidayType;
|
||||||
year: number;
|
year: number;
|
||||||
|
appliesToDepartments?: string[];
|
||||||
|
appliesToLocations?: string[];
|
||||||
|
description?: string;
|
||||||
|
isRecurring?: boolean;
|
||||||
|
recurrenceRule?: string;
|
||||||
|
isActive?: boolean;
|
||||||
|
createdBy?: string;
|
||||||
|
updatedBy?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const HolidaySchema = new Schema<IHoliday>({
|
const HolidaySchema = new Schema<IHoliday>({
|
||||||
date: { type: Date, required: true, unique: true },
|
holidayDate: { type: Date, required: true, unique: true },
|
||||||
name: { type: String, required: true },
|
holidayName: { type: String, required: true },
|
||||||
type: {
|
holidayType: {
|
||||||
type: String,
|
type: String,
|
||||||
enum: ['PUBLIC', 'OPTIONAL', 'WEEKEND'],
|
enum: Object.values(HolidayType),
|
||||||
default: 'PUBLIC'
|
default: HolidayType.PUBLIC
|
||||||
},
|
},
|
||||||
year: { type: Number, required: true, index: true }
|
year: { type: Number, required: true, index: true },
|
||||||
|
appliesToDepartments: { type: [String], default: [] },
|
||||||
|
appliesToLocations: { type: [String], default: [] },
|
||||||
|
description: { type: String },
|
||||||
|
isRecurring: { type: Boolean, default: false },
|
||||||
|
recurrenceRule: { type: String },
|
||||||
|
isActive: { type: Boolean, default: true },
|
||||||
|
createdBy: { type: String },
|
||||||
|
updatedBy: { type: String }
|
||||||
}, {
|
}, {
|
||||||
timestamps: true,
|
timestamps: true,
|
||||||
collection: 'holidays'
|
collection: 'holidays'
|
||||||
|
|||||||
@ -7,7 +7,7 @@ export interface INotification extends Document {
|
|||||||
title: string;
|
title: string;
|
||||||
message: string;
|
message: string;
|
||||||
isRead: boolean;
|
isRead: boolean;
|
||||||
priority: 'LOW' | 'MEDIUM' | 'HIGH' | 'URGENT';
|
priority: 'LOW' | 'MEDIUM' | 'HIGH' | 'URGENT' | 'STANDARD' | 'EXPRESS';
|
||||||
actionUrl?: string;
|
actionUrl?: string;
|
||||||
actionRequired: boolean;
|
actionRequired: boolean;
|
||||||
metadata?: any;
|
metadata?: any;
|
||||||
@ -28,7 +28,7 @@ const NotificationSchema: Schema = new Schema({
|
|||||||
isRead: { type: Boolean, default: false },
|
isRead: { type: Boolean, default: false },
|
||||||
priority: {
|
priority: {
|
||||||
type: String,
|
type: String,
|
||||||
enum: ['LOW', 'MEDIUM', 'HIGH', 'URGENT'],
|
enum: ['LOW', 'MEDIUM', 'HIGH', 'URGENT', 'STANDARD', 'EXPRESS'],
|
||||||
default: 'MEDIUM'
|
default: 'MEDIUM'
|
||||||
},
|
},
|
||||||
actionUrl: { type: String, required: false },
|
actionUrl: { type: String, required: false },
|
||||||
@ -40,7 +40,14 @@ const NotificationSchema: Schema = new Schema({
|
|||||||
pushSent: { type: Boolean, default: false }
|
pushSent: { type: Boolean, default: false }
|
||||||
}, {
|
}, {
|
||||||
timestamps: true,
|
timestamps: true,
|
||||||
collection: 'notifications' // Explicit collection name
|
collection: 'notifications', // Explicit collection name
|
||||||
|
toJSON: { virtuals: true },
|
||||||
|
toObject: { virtuals: true }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Virtual for notificationId to match frontend interface
|
||||||
|
NotificationSchema.virtual('notificationId').get(function (this: INotification) {
|
||||||
|
return this._id.toHexString();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Indexes
|
// Indexes
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user