Commit 1ae1b266 by Augusto

partner logic

parent 049b121e
# Deployment Guide - Inspection Generation System
## 🚀 Quick Deployment Steps
### 1. Database Migration
#### For Local Development:
```bash
# Connect to your local database
psql -d unike_inspection_local
# Run the migration
\i migration.sql
```
#### For Production:
```bash
# Connect to production database (replace with your connection details)
psql -h your-production-host -d unike_inspection_prod -U your-username
# Run the migration
\i migration.sql
```
### 2. Verify Migration
```sql
-- Check if the deadline column was added successfully
SELECT column_name, data_type, is_nullable
FROM information_schema.columns
WHERE table_name = 'Inspection' AND column_name = 'deadline';
```
Expected result:
```
column_name | data_type | is_nullable
-------------+-----------+-------------
deadline | timestamp | YES
```
### 3. Deploy Application Code
```bash
# Build the application
npm run build
# Start the application
npm run start:prod
```
### 4. Test New Endpoints
```bash
# Test partner sites endpoint
curl -X GET "http://localhost:3000/partners/1/sites" \
-H "Authorization: Bearer your-jwt-token"
# Test inspection generation
curl -X POST "http://localhost:3000/inspection/generate" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your-jwt-token" \
-d '{
"siteId": 1,
"deadline": "2025-12-31T23:59:59.000Z",
"comment": "Test inspection"
}'
```
## 🔄 Rollback Instructions
If you need to rollback the changes:
### 1. Rollback Database
```bash
# Connect to your database
psql -d your_database
# Run the rollback script
\i rollback.sql
```
### 2. Deploy Previous Version
```bash
# Deploy the previous version of your application
git checkout previous-version-tag
npm run build
npm run start:prod
```
## 📋 Pre-Deployment Checklist
- [ ] Backup your database
- [ ] Test migration on staging environment
- [ ] Verify all new endpoints work correctly
- [ ] Check that existing functionality is not broken
- [ ] Update API documentation
- [ ] Notify frontend team of new endpoints
## 🧪 Testing Checklist
- [ ] Test partner-site association endpoints
- [ ] Test inspection generation with and without partnerId
- [ ] Test automatic scheduling functionality
- [ ] Verify existing inspections still work
- [ ] Test error handling for invalid inputs
- [ ] Check authentication and authorization
## 📊 Monitoring
After deployment, monitor:
- Application logs for any errors
- Database performance
- API response times
- Error rates for new endpoints
## 🆘 Troubleshooting
### Common Issues:
1. **Migration fails**: Check database permissions and connection
2. **New endpoints return 404**: Verify the application was restarted
3. **Authentication errors**: Check JWT token validity
4. **Database constraint errors**: Verify partner and site IDs exist
### Logs to Check:
- Application logs: `logs/app.log`
- Database logs: Check your PostgreSQL logs
- Nginx/Apache logs: If using a reverse proxy
## 📞 Support
If you encounter issues during deployment:
1. Check the troubleshooting section above
2. Review application logs
3. Contact the development team
4. Create an issue in the project repository
# Inspection Generation & Partner Management System
## Overview
This update introduces a comprehensive inspection generation system with partner-site association management. The system allows you to generate inspections for specific sites with deadlines and automatically manages the inspection scheduling cycle.
## 🆕 New Features
### 1. Partner-Site Association Management
- Associate partners with specific sites for inspection responsibility
- Switch partners for sites when needed
- View all sites associated with a partner
### 2. Inspection Generation System
- Generate inspections for specific sites with custom deadlines
- Associate or switch partners during inspection generation
- Automatic creation of default "NA" responses for all questions
### 3. Enhanced Automatic Scheduling
- 3-month advance scheduling before deadlines
- 1-year inspection cycle management
- Continuous automatic inspection generation
## 📋 API Endpoints
### Partner-Site Management
#### GET `/partners/:id/sites`
Get all sites associated with a specific partner.
**Access**: ADMIN, MANAGER, PARTNER roles
**Example**:
```bash
GET /partners/1/sites
```
#### POST `/partners/:partnerId/sites/:siteId`
Associate a site with a partner.
**Access**: ADMIN, SUPERADMIN roles only
**Example**:
```bash
POST /partners/1/sites/5
```
#### DELETE `/partners/:partnerId/sites/:siteId`
Remove a site from a partner.
**Access**: ADMIN, SUPERADMIN roles only
**Example**:
```bash
DELETE /partners/1/sites/5
```
#### PATCH `/partners/sites/:siteId/switch-to/:newPartnerId`
Switch a site from its current partner to a different partner.
**Access**: ADMIN, SUPERADMIN roles only
**Example**:
```bash
PATCH /partners/sites/5/switch-to/2
```
### Inspection Generation
#### POST `/inspection/generate`
Generate a new inspection with deadline for a specific site.
**Access**: ADMIN, MANAGER, OPERATOR, SUPERADMIN roles
**Request Body**:
```json
{
"siteId": 5, // Required: Site ID
"partnerId": 2, // Optional: Partner ID
"deadline": "2025-12-31T23:59:59.000Z", // Required: Deadline date
"comment": "Annual inspection" // Optional: Comment
}
```
**Features**:
- If `partnerId` is provided: Associates/switches the site to that partner
- If no `partnerId`: Uses the partner already associated with the site
- Creates default "NA" responses for all inspection questions
- Sets inspection status to "PENDING"
## 🔄 Workflow Examples
### Scenario 1: Associate Partner with Site and Generate Inspection
```bash
# 1. Associate partner with site
POST /partners/1/sites/5
# 2. Generate inspection for that site
POST /inspection/generate
{
"siteId": 5,
"deadline": "2025-12-31T23:59:59.000Z",
"comment": "Annual inspection for Site 5"
}
```
### Scenario 2: Switch Partner and Generate Inspection
```bash
# 1. Switch site to different partner
PATCH /partners/sites/5/switch-to/2
# 2. Generate inspection (will use the new partner)
POST /inspection/generate
{
"siteId": 5,
"deadline": "2025-12-31T23:59:59.000Z"
}
```
### Scenario 3: Generate Inspection and Associate Partner in One Step
```bash
# Generate inspection and associate with specific partner
POST /inspection/generate
{
"siteId": 5,
"partnerId": 2,
"deadline": "2025-12-31T23:59:59.000Z",
"comment": "Annual inspection with Partner 2"
}
```
-- Cleanup Script: Remove hardcoded auto-generated comments
-- Date: 2025-01-27
-- Description: Removes hardcoded "Auto-generated" and "Generated inspection" comments from the database
-- Show current hardcoded comments before cleanup
SELECT
id,
comment,
"createdAt"
FROM "Inspection"
WHERE comment LIKE '%Auto-generated%'
OR comment LIKE '%Generated inspection%'
OR comment LIKE '%pending completion%'
ORDER BY "createdAt" DESC;
-- Count how many records will be affected
SELECT COUNT(*) as affected_inspections
FROM "Inspection"
WHERE comment LIKE '%Auto-generated%'
OR comment LIKE '%Generated inspection%'
OR comment LIKE '%pending completion%';
-- Clean up inspection comments
UPDATE "Inspection"
SET comment = NULL
WHERE comment LIKE '%Auto-generated%'
OR comment LIKE '%Generated inspection%'
OR comment LIKE '%pending completion%';
-- Clean up inspection response comments
UPDATE "InspectionResponse"
SET comment = NULL
WHERE comment LIKE '%Auto-generated%'
OR comment LIKE '%to be completed%';
-- Show results after cleanup
SELECT
'Inspections cleaned' as table_name,
COUNT(*) as remaining_records
FROM "Inspection"
WHERE comment LIKE '%Auto-generated%'
OR comment LIKE '%Generated inspection%'
OR comment LIKE '%pending completion%'
UNION ALL
SELECT
'InspectionResponse cleaned' as table_name,
COUNT(*) as remaining_records
FROM "InspectionResponse"
WHERE comment LIKE '%Auto-generated%'
OR comment LIKE '%to be completed%';
-- Verify cleanup was successful
SELECT
'Cleanup verification' as status,
CASE
WHEN COUNT(*) = 0 THEN 'SUCCESS: No hardcoded comments found'
ELSE 'WARNING: ' || COUNT(*) || ' hardcoded comments still exist'
END as result
FROM (
SELECT id FROM "Inspection"
WHERE comment LIKE '%Auto-generated%'
OR comment LIKE '%Generated inspection%'
OR comment LIKE '%pending completion%'
UNION ALL
SELECT id FROM "InspectionResponse"
WHERE comment LIKE '%Auto-generated%'
OR comment LIKE '%to be completed%'
) as remaining_comments;
CREATE TYPE "public"."CompanyName" AS ENUM('VODAFONE', 'MEO', 'NOS', 'DIGI');--> statement-breakpoint
CREATE TYPE "public"."InspectionResponseOption" AS ENUM('YES', 'NO', 'NA');--> statement-breakpoint
CREATE TYPE "public"."InspectionStatus" AS ENUM('PENDING', 'IN_PROGRESS', 'COMPLETED', 'CANCELLED', 'APPROVING', 'REJECTED', 'APPROVED');--> statement-breakpoint
CREATE TYPE "public"."Role" AS ENUM('ADMIN', 'MANAGER', 'OPERATOR', 'VIEWER', 'SUPERADMIN', 'PARTNER');--> statement-breakpoint
CREATE TABLE "InspectionPhoto" (
"id" serial PRIMARY KEY NOT NULL,
"url" varchar(500) NOT NULL,
"filename" varchar(255) NOT NULL,
"mimeType" varchar(100) NOT NULL,
"size" integer NOT NULL,
"description" text,
"createdAt" timestamp DEFAULT now() NOT NULL,
"updatedAt" timestamp DEFAULT now() NOT NULL,
"inspectionId" integer NOT NULL
);
--> statement-breakpoint
CREATE TABLE "InspectionQuestion" (
"id" serial PRIMARY KEY NOT NULL,
"question" text NOT NULL,
"orderIndex" integer NOT NULL,
"createdAt" timestamp DEFAULT now() NOT NULL,
"updatedAt" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "InspectionResponse" (
"id" serial PRIMARY KEY NOT NULL,
"response" "InspectionResponseOption" NOT NULL,
"comment" text,
"questionId" integer NOT NULL,
"inspectionId" integer NOT NULL,
"createdAt" timestamp DEFAULT now() NOT NULL,
"updatedAt" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "Inspection" (
"id" serial PRIMARY KEY NOT NULL,
"date" timestamp NOT NULL,
"comment" text,
"finalComment" text,
"siteId" integer NOT NULL,
"createdAt" timestamp DEFAULT now() NOT NULL,
"updatedAt" timestamp DEFAULT now() NOT NULL,
"createdById" integer,
"updatedById" integer,
"status" "InspectionStatus" DEFAULT 'PENDING' NOT NULL
);
--> statement-breakpoint
CREATE TABLE "PartnerSite" (
"id" serial PRIMARY KEY NOT NULL,
"partnerId" integer NOT NULL,
"siteId" integer NOT NULL,
"createdAt" timestamp DEFAULT now() NOT NULL,
"updatedAt" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "Partner" (
"id" serial PRIMARY KEY NOT NULL,
"name" varchar(255) NOT NULL,
"description" text,
"isActive" boolean DEFAULT true NOT NULL,
"createdAt" timestamp DEFAULT now() NOT NULL,
"updatedAt" timestamp DEFAULT now() NOT NULL,
CONSTRAINT "Partner_name_unique" UNIQUE("name")
);
--> statement-breakpoint
CREATE TABLE "RefreshToken" (
"id" serial PRIMARY KEY NOT NULL,
"token" varchar(500) NOT NULL,
"userId" integer NOT NULL,
"expiresAt" timestamp NOT NULL,
"createdAt" timestamp DEFAULT now() NOT NULL,
CONSTRAINT "RefreshToken_token_unique" UNIQUE("token")
);
--> statement-breakpoint
CREATE TABLE "Site" (
"id" serial PRIMARY KEY NOT NULL,
"siteCode" varchar(255) NOT NULL,
"siteName" varchar(255) NOT NULL,
"latitude" double precision NOT NULL,
"longitude" double precision NOT NULL,
"createdAt" timestamp DEFAULT now() NOT NULL,
"updatedAt" timestamp DEFAULT now() NOT NULL,
"createdById" integer,
"updatedById" integer,
CONSTRAINT "Site_siteCode_unique" UNIQUE("siteCode")
);
--> statement-breakpoint
CREATE TABLE "spatial_ref_sys" (
"srid" integer PRIMARY KEY NOT NULL,
"auth_name" varchar(256),
"auth_srid" integer,
"srtext" varchar(2048),
"proj4text" varchar(2048)
);
--> statement-breakpoint
CREATE TABLE "TelecommunicationStationIdentification" (
"id" serial PRIMARY KEY NOT NULL,
"siteId" integer NOT NULL,
"stationIdentifier" varchar(255) NOT NULL,
"serialNumber" varchar(255) NOT NULL,
"isFirstCertification" boolean NOT NULL,
"modelReference" varchar(255) NOT NULL,
"createdAt" timestamp DEFAULT now() NOT NULL,
"updatedAt" timestamp DEFAULT now() NOT NULL,
CONSTRAINT "TelecommunicationStationIdentification_siteId_unique" UNIQUE("siteId"),
CONSTRAINT "TelecommunicationStationIdentification_stationIdentifier_unique" UNIQUE("stationIdentifier")
);
--> statement-breakpoint
CREATE TABLE "UserSite" (
"id" serial PRIMARY KEY NOT NULL,
"userId" integer NOT NULL,
"siteId" integer NOT NULL,
"createdAt" timestamp DEFAULT now() NOT NULL,
"updatedAt" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "User" (
"id" serial PRIMARY KEY NOT NULL,
"email" varchar(255) NOT NULL,
"name" varchar(255) NOT NULL,
"password" varchar(255) NOT NULL,
"role" "Role" DEFAULT 'VIEWER' NOT NULL,
"createdAt" timestamp DEFAULT now() NOT NULL,
"updatedAt" timestamp DEFAULT now() NOT NULL,
"resetToken" varchar(255),
"resetTokenExpiry" timestamp,
"isActive" boolean DEFAULT false NOT NULL,
"partnerId" integer,
CONSTRAINT "User_email_unique" UNIQUE("email")
);
--> statement-breakpoint
ALTER TABLE "InspectionPhoto" ADD CONSTRAINT "InspectionPhoto_inspectionId_Inspection_id_fk" FOREIGN KEY ("inspectionId") REFERENCES "public"."Inspection"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "InspectionResponse" ADD CONSTRAINT "InspectionResponse_questionId_InspectionQuestion_id_fk" FOREIGN KEY ("questionId") REFERENCES "public"."InspectionQuestion"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "InspectionResponse" ADD CONSTRAINT "InspectionResponse_inspectionId_Inspection_id_fk" FOREIGN KEY ("inspectionId") REFERENCES "public"."Inspection"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "Inspection" ADD CONSTRAINT "Inspection_siteId_Site_id_fk" FOREIGN KEY ("siteId") REFERENCES "public"."Site"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "Inspection" ADD CONSTRAINT "Inspection_createdById_User_id_fk" FOREIGN KEY ("createdById") REFERENCES "public"."User"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "Inspection" ADD CONSTRAINT "Inspection_updatedById_User_id_fk" FOREIGN KEY ("updatedById") REFERENCES "public"."User"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "PartnerSite" ADD CONSTRAINT "PartnerSite_partnerId_Partner_id_fk" FOREIGN KEY ("partnerId") REFERENCES "public"."Partner"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "PartnerSite" ADD CONSTRAINT "PartnerSite_siteId_Site_id_fk" FOREIGN KEY ("siteId") REFERENCES "public"."Site"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "RefreshToken" ADD CONSTRAINT "RefreshToken_userId_User_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "Site" ADD CONSTRAINT "Site_createdById_User_id_fk" FOREIGN KEY ("createdById") REFERENCES "public"."User"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "Site" ADD CONSTRAINT "Site_updatedById_User_id_fk" FOREIGN KEY ("updatedById") REFERENCES "public"."User"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "TelecommunicationStationIdentification" ADD CONSTRAINT "TelecommunicationStationIdentification_siteId_Site_id_fk" FOREIGN KEY ("siteId") REFERENCES "public"."Site"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "UserSite" ADD CONSTRAINT "UserSite_userId_User_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "UserSite" ADD CONSTRAINT "UserSite_siteId_Site_id_fk" FOREIGN KEY ("siteId") REFERENCES "public"."Site"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "User" ADD CONSTRAINT "User_partnerId_Partner_id_fk" FOREIGN KEY ("partnerId") REFERENCES "public"."Partner"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
CREATE INDEX "InspectionPhoto_inspectionId_idx" ON "InspectionPhoto" USING btree ("inspectionId");--> statement-breakpoint
CREATE INDEX "InspectionResponse_questionId_idx" ON "InspectionResponse" USING btree ("questionId");--> statement-breakpoint
CREATE INDEX "InspectionResponse_inspectionId_idx" ON "InspectionResponse" USING btree ("inspectionId");--> statement-breakpoint
CREATE INDEX "Inspection_siteId_idx" ON "Inspection" USING btree ("siteId");--> statement-breakpoint
CREATE INDEX "Inspection_createdById_idx" ON "Inspection" USING btree ("createdById");--> statement-breakpoint
CREATE INDEX "Inspection_updatedById_idx" ON "Inspection" USING btree ("updatedById");--> statement-breakpoint
CREATE INDEX "Inspection_status_idx" ON "Inspection" USING btree ("status");--> statement-breakpoint
CREATE UNIQUE INDEX "PartnerSite_partnerId_siteId_key" ON "PartnerSite" USING btree ("partnerId","siteId");--> statement-breakpoint
CREATE INDEX "PartnerSite_partnerId_idx" ON "PartnerSite" USING btree ("partnerId");--> statement-breakpoint
CREATE INDEX "PartnerSite_siteId_idx" ON "PartnerSite" USING btree ("siteId");--> statement-breakpoint
CREATE INDEX "Partner_name_idx" ON "Partner" USING btree ("name");--> statement-breakpoint
CREATE INDEX "RefreshToken_token_idx" ON "RefreshToken" USING btree ("token");--> statement-breakpoint
CREATE INDEX "RefreshToken_userId_idx" ON "RefreshToken" USING btree ("userId");--> statement-breakpoint
CREATE INDEX "Site_siteCode_idx" ON "Site" USING btree ("siteCode");--> statement-breakpoint
CREATE INDEX "TelecommunicationStationIdentification_siteId_idx" ON "TelecommunicationStationIdentification" USING btree ("siteId");--> statement-breakpoint
CREATE INDEX "TelecommunicationStationIdentification_stationIdentifier_idx" ON "TelecommunicationStationIdentification" USING btree ("stationIdentifier");--> statement-breakpoint
CREATE UNIQUE INDEX "UserSite_userId_siteId_key" ON "UserSite" USING btree ("userId","siteId");--> statement-breakpoint
CREATE INDEX "UserSite_userId_idx" ON "UserSite" USING btree ("userId");--> statement-breakpoint
CREATE INDEX "UserSite_siteId_idx" ON "UserSite" USING btree ("siteId");--> statement-breakpoint
CREATE INDEX "User_email_idx" ON "User" USING btree ("email");--> statement-breakpoint
CREATE INDEX "User_role_idx" ON "User" USING btree ("role");--> statement-breakpoint
CREATE INDEX "User_partnerId_idx" ON "User" USING btree ("partnerId");
\ No newline at end of file
CREATE TYPE "public"."FinalCommentStatus" AS ENUM('PENDING', 'VALIDATED', 'REJECTED');--> statement-breakpoint
ALTER TABLE "User" ALTER COLUMN "role" SET DATA TYPE text;--> statement-breakpoint
ALTER TABLE "User" ALTER COLUMN "role" SET DEFAULT 'VIEWER'::text;--> statement-breakpoint
DROP TYPE "public"."Role";--> statement-breakpoint
CREATE TYPE "public"."Role" AS ENUM('SUPERADMIN', 'ADMIN', 'MANAGER', 'PARTNER', 'OPERATOR', 'VIEWER');--> statement-breakpoint
ALTER TABLE "User" ALTER COLUMN "role" SET DEFAULT 'VIEWER'::"public"."Role";--> statement-breakpoint
ALTER TABLE "User" ALTER COLUMN "role" SET DATA TYPE "public"."Role" USING "role"::"public"."Role";--> statement-breakpoint
ALTER TABLE "Inspection" ADD COLUMN "finalCommentStatus" "FinalCommentStatus" DEFAULT 'PENDING';--> statement-breakpoint
CREATE UNIQUE INDEX "InspectionResponse_inspectionId_questionId_key" ON "InspectionResponse" USING btree ("inspectionId","questionId");--> statement-breakpoint
CREATE INDEX "Inspection_finalCommentStatus_idx" ON "Inspection" USING btree ("finalCommentStatus");
\ No newline at end of file
ALTER TABLE "Inspection" ADD COLUMN "deadline" timestamp;
\ No newline at end of file
{
"id": "48396298-e408-428f-a87a-9ba5fc72fead",
"prevId": "00000000-0000-0000-0000-000000000000",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.InspectionPhoto": {
"name": "InspectionPhoto",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"url": {
"name": "url",
"type": "varchar(500)",
"primaryKey": false,
"notNull": true
},
"filename": {
"name": "filename",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"mimeType": {
"name": "mimeType",
"type": "varchar(100)",
"primaryKey": false,
"notNull": true
},
"size": {
"name": "size",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"inspectionId": {
"name": "inspectionId",
"type": "integer",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"InspectionPhoto_inspectionId_idx": {
"name": "InspectionPhoto_inspectionId_idx",
"columns": [
{
"expression": "inspectionId",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"InspectionPhoto_inspectionId_Inspection_id_fk": {
"name": "InspectionPhoto_inspectionId_Inspection_id_fk",
"tableFrom": "InspectionPhoto",
"tableTo": "Inspection",
"columnsFrom": [
"inspectionId"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.InspectionQuestion": {
"name": "InspectionQuestion",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"question": {
"name": "question",
"type": "text",
"primaryKey": false,
"notNull": true
},
"orderIndex": {
"name": "orderIndex",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.InspectionResponse": {
"name": "InspectionResponse",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"response": {
"name": "response",
"type": "InspectionResponseOption",
"typeSchema": "public",
"primaryKey": false,
"notNull": true
},
"comment": {
"name": "comment",
"type": "text",
"primaryKey": false,
"notNull": false
},
"questionId": {
"name": "questionId",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"inspectionId": {
"name": "inspectionId",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {
"InspectionResponse_questionId_idx": {
"name": "InspectionResponse_questionId_idx",
"columns": [
{
"expression": "questionId",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"InspectionResponse_inspectionId_idx": {
"name": "InspectionResponse_inspectionId_idx",
"columns": [
{
"expression": "inspectionId",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"InspectionResponse_questionId_InspectionQuestion_id_fk": {
"name": "InspectionResponse_questionId_InspectionQuestion_id_fk",
"tableFrom": "InspectionResponse",
"tableTo": "InspectionQuestion",
"columnsFrom": [
"questionId"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"InspectionResponse_inspectionId_Inspection_id_fk": {
"name": "InspectionResponse_inspectionId_Inspection_id_fk",
"tableFrom": "InspectionResponse",
"tableTo": "Inspection",
"columnsFrom": [
"inspectionId"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.Inspection": {
"name": "Inspection",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"date": {
"name": "date",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"comment": {
"name": "comment",
"type": "text",
"primaryKey": false,
"notNull": false
},
"finalComment": {
"name": "finalComment",
"type": "text",
"primaryKey": false,
"notNull": false
},
"siteId": {
"name": "siteId",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"createdById": {
"name": "createdById",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"updatedById": {
"name": "updatedById",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"status": {
"name": "status",
"type": "InspectionStatus",
"typeSchema": "public",
"primaryKey": false,
"notNull": true,
"default": "'PENDING'"
}
},
"indexes": {
"Inspection_siteId_idx": {
"name": "Inspection_siteId_idx",
"columns": [
{
"expression": "siteId",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"Inspection_createdById_idx": {
"name": "Inspection_createdById_idx",
"columns": [
{
"expression": "createdById",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"Inspection_updatedById_idx": {
"name": "Inspection_updatedById_idx",
"columns": [
{
"expression": "updatedById",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"Inspection_status_idx": {
"name": "Inspection_status_idx",
"columns": [
{
"expression": "status",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"Inspection_siteId_Site_id_fk": {
"name": "Inspection_siteId_Site_id_fk",
"tableFrom": "Inspection",
"tableTo": "Site",
"columnsFrom": [
"siteId"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"Inspection_createdById_User_id_fk": {
"name": "Inspection_createdById_User_id_fk",
"tableFrom": "Inspection",
"tableTo": "User",
"columnsFrom": [
"createdById"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"Inspection_updatedById_User_id_fk": {
"name": "Inspection_updatedById_User_id_fk",
"tableFrom": "Inspection",
"tableTo": "User",
"columnsFrom": [
"updatedById"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.PartnerSite": {
"name": "PartnerSite",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"partnerId": {
"name": "partnerId",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"siteId": {
"name": "siteId",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {
"PartnerSite_partnerId_siteId_key": {
"name": "PartnerSite_partnerId_siteId_key",
"columns": [
{
"expression": "partnerId",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "siteId",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
},
"PartnerSite_partnerId_idx": {
"name": "PartnerSite_partnerId_idx",
"columns": [
{
"expression": "partnerId",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"PartnerSite_siteId_idx": {
"name": "PartnerSite_siteId_idx",
"columns": [
{
"expression": "siteId",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"PartnerSite_partnerId_Partner_id_fk": {
"name": "PartnerSite_partnerId_Partner_id_fk",
"tableFrom": "PartnerSite",
"tableTo": "Partner",
"columnsFrom": [
"partnerId"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"PartnerSite_siteId_Site_id_fk": {
"name": "PartnerSite_siteId_Site_id_fk",
"tableFrom": "PartnerSite",
"tableTo": "Site",
"columnsFrom": [
"siteId"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.Partner": {
"name": "Partner",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false
},
"isActive": {
"name": "isActive",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {
"Partner_name_idx": {
"name": "Partner_name_idx",
"columns": [
{
"expression": "name",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"Partner_name_unique": {
"name": "Partner_name_unique",
"nullsNotDistinct": false,
"columns": [
"name"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.RefreshToken": {
"name": "RefreshToken",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"token": {
"name": "token",
"type": "varchar(500)",
"primaryKey": false,
"notNull": true
},
"userId": {
"name": "userId",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"expiresAt": {
"name": "expiresAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {
"RefreshToken_token_idx": {
"name": "RefreshToken_token_idx",
"columns": [
{
"expression": "token",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"RefreshToken_userId_idx": {
"name": "RefreshToken_userId_idx",
"columns": [
{
"expression": "userId",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"RefreshToken_userId_User_id_fk": {
"name": "RefreshToken_userId_User_id_fk",
"tableFrom": "RefreshToken",
"tableTo": "User",
"columnsFrom": [
"userId"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"RefreshToken_token_unique": {
"name": "RefreshToken_token_unique",
"nullsNotDistinct": false,
"columns": [
"token"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.Site": {
"name": "Site",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"siteCode": {
"name": "siteCode",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"siteName": {
"name": "siteName",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"latitude": {
"name": "latitude",
"type": "double precision",
"primaryKey": false,
"notNull": true
},
"longitude": {
"name": "longitude",
"type": "double precision",
"primaryKey": false,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"createdById": {
"name": "createdById",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"updatedById": {
"name": "updatedById",
"type": "integer",
"primaryKey": false,
"notNull": false
}
},
"indexes": {
"Site_siteCode_idx": {
"name": "Site_siteCode_idx",
"columns": [
{
"expression": "siteCode",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"Site_createdById_User_id_fk": {
"name": "Site_createdById_User_id_fk",
"tableFrom": "Site",
"tableTo": "User",
"columnsFrom": [
"createdById"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"Site_updatedById_User_id_fk": {
"name": "Site_updatedById_User_id_fk",
"tableFrom": "Site",
"tableTo": "User",
"columnsFrom": [
"updatedById"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"Site_siteCode_unique": {
"name": "Site_siteCode_unique",
"nullsNotDistinct": false,
"columns": [
"siteCode"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.spatial_ref_sys": {
"name": "spatial_ref_sys",
"schema": "",
"columns": {
"srid": {
"name": "srid",
"type": "integer",
"primaryKey": true,
"notNull": true
},
"auth_name": {
"name": "auth_name",
"type": "varchar(256)",
"primaryKey": false,
"notNull": false
},
"auth_srid": {
"name": "auth_srid",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"srtext": {
"name": "srtext",
"type": "varchar(2048)",
"primaryKey": false,
"notNull": false
},
"proj4text": {
"name": "proj4text",
"type": "varchar(2048)",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.TelecommunicationStationIdentification": {
"name": "TelecommunicationStationIdentification",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"siteId": {
"name": "siteId",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"stationIdentifier": {
"name": "stationIdentifier",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"serialNumber": {
"name": "serialNumber",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"isFirstCertification": {
"name": "isFirstCertification",
"type": "boolean",
"primaryKey": false,
"notNull": true
},
"modelReference": {
"name": "modelReference",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {
"TelecommunicationStationIdentification_siteId_idx": {
"name": "TelecommunicationStationIdentification_siteId_idx",
"columns": [
{
"expression": "siteId",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"TelecommunicationStationIdentification_stationIdentifier_idx": {
"name": "TelecommunicationStationIdentification_stationIdentifier_idx",
"columns": [
{
"expression": "stationIdentifier",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"TelecommunicationStationIdentification_siteId_Site_id_fk": {
"name": "TelecommunicationStationIdentification_siteId_Site_id_fk",
"tableFrom": "TelecommunicationStationIdentification",
"tableTo": "Site",
"columnsFrom": [
"siteId"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"TelecommunicationStationIdentification_siteId_unique": {
"name": "TelecommunicationStationIdentification_siteId_unique",
"nullsNotDistinct": false,
"columns": [
"siteId"
]
},
"TelecommunicationStationIdentification_stationIdentifier_unique": {
"name": "TelecommunicationStationIdentification_stationIdentifier_unique",
"nullsNotDistinct": false,
"columns": [
"stationIdentifier"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.UserSite": {
"name": "UserSite",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"userId": {
"name": "userId",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"siteId": {
"name": "siteId",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {
"UserSite_userId_siteId_key": {
"name": "UserSite_userId_siteId_key",
"columns": [
{
"expression": "userId",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "siteId",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
},
"UserSite_userId_idx": {
"name": "UserSite_userId_idx",
"columns": [
{
"expression": "userId",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"UserSite_siteId_idx": {
"name": "UserSite_siteId_idx",
"columns": [
{
"expression": "siteId",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"UserSite_userId_User_id_fk": {
"name": "UserSite_userId_User_id_fk",
"tableFrom": "UserSite",
"tableTo": "User",
"columnsFrom": [
"userId"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"UserSite_siteId_Site_id_fk": {
"name": "UserSite_siteId_Site_id_fk",
"tableFrom": "UserSite",
"tableTo": "Site",
"columnsFrom": [
"siteId"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.User": {
"name": "User",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"password": {
"name": "password",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"role": {
"name": "role",
"type": "Role",
"typeSchema": "public",
"primaryKey": false,
"notNull": true,
"default": "'VIEWER'"
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"resetToken": {
"name": "resetToken",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"resetTokenExpiry": {
"name": "resetTokenExpiry",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"isActive": {
"name": "isActive",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"partnerId": {
"name": "partnerId",
"type": "integer",
"primaryKey": false,
"notNull": false
}
},
"indexes": {
"User_email_idx": {
"name": "User_email_idx",
"columns": [
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"User_role_idx": {
"name": "User_role_idx",
"columns": [
{
"expression": "role",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"User_partnerId_idx": {
"name": "User_partnerId_idx",
"columns": [
{
"expression": "partnerId",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"User_partnerId_Partner_id_fk": {
"name": "User_partnerId_Partner_id_fk",
"tableFrom": "User",
"tableTo": "Partner",
"columnsFrom": [
"partnerId"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"User_email_unique": {
"name": "User_email_unique",
"nullsNotDistinct": false,
"columns": [
"email"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {
"public.CompanyName": {
"name": "CompanyName",
"schema": "public",
"values": [
"VODAFONE",
"MEO",
"NOS",
"DIGI"
]
},
"public.InspectionResponseOption": {
"name": "InspectionResponseOption",
"schema": "public",
"values": [
"YES",
"NO",
"NA"
]
},
"public.InspectionStatus": {
"name": "InspectionStatus",
"schema": "public",
"values": [
"PENDING",
"IN_PROGRESS",
"COMPLETED",
"CANCELLED",
"APPROVING",
"REJECTED",
"APPROVED"
]
},
"public.Role": {
"name": "Role",
"schema": "public",
"values": [
"ADMIN",
"MANAGER",
"OPERATOR",
"VIEWER",
"SUPERADMIN",
"PARTNER"
]
}
},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}
\ No newline at end of file
{
"id": "26599364-fa1e-4e76-a1f4-4b678ac90cb2",
"prevId": "48396298-e408-428f-a87a-9ba5fc72fead",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.InspectionPhoto": {
"name": "InspectionPhoto",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"url": {
"name": "url",
"type": "varchar(500)",
"primaryKey": false,
"notNull": true
},
"filename": {
"name": "filename",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"mimeType": {
"name": "mimeType",
"type": "varchar(100)",
"primaryKey": false,
"notNull": true
},
"size": {
"name": "size",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"inspectionId": {
"name": "inspectionId",
"type": "integer",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"InspectionPhoto_inspectionId_idx": {
"name": "InspectionPhoto_inspectionId_idx",
"columns": [
{
"expression": "inspectionId",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"InspectionPhoto_inspectionId_Inspection_id_fk": {
"name": "InspectionPhoto_inspectionId_Inspection_id_fk",
"tableFrom": "InspectionPhoto",
"tableTo": "Inspection",
"columnsFrom": [
"inspectionId"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.InspectionQuestion": {
"name": "InspectionQuestion",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"question": {
"name": "question",
"type": "text",
"primaryKey": false,
"notNull": true
},
"orderIndex": {
"name": "orderIndex",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.InspectionResponse": {
"name": "InspectionResponse",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"response": {
"name": "response",
"type": "InspectionResponseOption",
"typeSchema": "public",
"primaryKey": false,
"notNull": true
},
"comment": {
"name": "comment",
"type": "text",
"primaryKey": false,
"notNull": false
},
"questionId": {
"name": "questionId",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"inspectionId": {
"name": "inspectionId",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {
"InspectionResponse_questionId_idx": {
"name": "InspectionResponse_questionId_idx",
"columns": [
{
"expression": "questionId",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"InspectionResponse_inspectionId_idx": {
"name": "InspectionResponse_inspectionId_idx",
"columns": [
{
"expression": "inspectionId",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"InspectionResponse_inspectionId_questionId_key": {
"name": "InspectionResponse_inspectionId_questionId_key",
"columns": [
{
"expression": "inspectionId",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "questionId",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"InspectionResponse_questionId_InspectionQuestion_id_fk": {
"name": "InspectionResponse_questionId_InspectionQuestion_id_fk",
"tableFrom": "InspectionResponse",
"tableTo": "InspectionQuestion",
"columnsFrom": [
"questionId"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"InspectionResponse_inspectionId_Inspection_id_fk": {
"name": "InspectionResponse_inspectionId_Inspection_id_fk",
"tableFrom": "InspectionResponse",
"tableTo": "Inspection",
"columnsFrom": [
"inspectionId"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.Inspection": {
"name": "Inspection",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"date": {
"name": "date",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"comment": {
"name": "comment",
"type": "text",
"primaryKey": false,
"notNull": false
},
"finalComment": {
"name": "finalComment",
"type": "text",
"primaryKey": false,
"notNull": false
},
"finalCommentStatus": {
"name": "finalCommentStatus",
"type": "FinalCommentStatus",
"typeSchema": "public",
"primaryKey": false,
"notNull": false,
"default": "'PENDING'"
},
"siteId": {
"name": "siteId",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"createdById": {
"name": "createdById",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"updatedById": {
"name": "updatedById",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"status": {
"name": "status",
"type": "InspectionStatus",
"typeSchema": "public",
"primaryKey": false,
"notNull": true,
"default": "'PENDING'"
}
},
"indexes": {
"Inspection_siteId_idx": {
"name": "Inspection_siteId_idx",
"columns": [
{
"expression": "siteId",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"Inspection_createdById_idx": {
"name": "Inspection_createdById_idx",
"columns": [
{
"expression": "createdById",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"Inspection_updatedById_idx": {
"name": "Inspection_updatedById_idx",
"columns": [
{
"expression": "updatedById",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"Inspection_status_idx": {
"name": "Inspection_status_idx",
"columns": [
{
"expression": "status",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"Inspection_finalCommentStatus_idx": {
"name": "Inspection_finalCommentStatus_idx",
"columns": [
{
"expression": "finalCommentStatus",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"Inspection_siteId_Site_id_fk": {
"name": "Inspection_siteId_Site_id_fk",
"tableFrom": "Inspection",
"tableTo": "Site",
"columnsFrom": [
"siteId"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"Inspection_createdById_User_id_fk": {
"name": "Inspection_createdById_User_id_fk",
"tableFrom": "Inspection",
"tableTo": "User",
"columnsFrom": [
"createdById"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"Inspection_updatedById_User_id_fk": {
"name": "Inspection_updatedById_User_id_fk",
"tableFrom": "Inspection",
"tableTo": "User",
"columnsFrom": [
"updatedById"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.PartnerSite": {
"name": "PartnerSite",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"partnerId": {
"name": "partnerId",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"siteId": {
"name": "siteId",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {
"PartnerSite_partnerId_siteId_key": {
"name": "PartnerSite_partnerId_siteId_key",
"columns": [
{
"expression": "partnerId",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "siteId",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
},
"PartnerSite_partnerId_idx": {
"name": "PartnerSite_partnerId_idx",
"columns": [
{
"expression": "partnerId",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"PartnerSite_siteId_idx": {
"name": "PartnerSite_siteId_idx",
"columns": [
{
"expression": "siteId",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"PartnerSite_partnerId_Partner_id_fk": {
"name": "PartnerSite_partnerId_Partner_id_fk",
"tableFrom": "PartnerSite",
"tableTo": "Partner",
"columnsFrom": [
"partnerId"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"PartnerSite_siteId_Site_id_fk": {
"name": "PartnerSite_siteId_Site_id_fk",
"tableFrom": "PartnerSite",
"tableTo": "Site",
"columnsFrom": [
"siteId"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.Partner": {
"name": "Partner",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false
},
"isActive": {
"name": "isActive",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {
"Partner_name_idx": {
"name": "Partner_name_idx",
"columns": [
{
"expression": "name",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"Partner_name_unique": {
"name": "Partner_name_unique",
"nullsNotDistinct": false,
"columns": [
"name"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.RefreshToken": {
"name": "RefreshToken",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"token": {
"name": "token",
"type": "varchar(500)",
"primaryKey": false,
"notNull": true
},
"userId": {
"name": "userId",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"expiresAt": {
"name": "expiresAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {
"RefreshToken_token_idx": {
"name": "RefreshToken_token_idx",
"columns": [
{
"expression": "token",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"RefreshToken_userId_idx": {
"name": "RefreshToken_userId_idx",
"columns": [
{
"expression": "userId",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"RefreshToken_userId_User_id_fk": {
"name": "RefreshToken_userId_User_id_fk",
"tableFrom": "RefreshToken",
"tableTo": "User",
"columnsFrom": [
"userId"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"RefreshToken_token_unique": {
"name": "RefreshToken_token_unique",
"nullsNotDistinct": false,
"columns": [
"token"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.Site": {
"name": "Site",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"siteCode": {
"name": "siteCode",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"siteName": {
"name": "siteName",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"latitude": {
"name": "latitude",
"type": "double precision",
"primaryKey": false,
"notNull": true
},
"longitude": {
"name": "longitude",
"type": "double precision",
"primaryKey": false,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"createdById": {
"name": "createdById",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"updatedById": {
"name": "updatedById",
"type": "integer",
"primaryKey": false,
"notNull": false
}
},
"indexes": {
"Site_siteCode_idx": {
"name": "Site_siteCode_idx",
"columns": [
{
"expression": "siteCode",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"Site_createdById_User_id_fk": {
"name": "Site_createdById_User_id_fk",
"tableFrom": "Site",
"tableTo": "User",
"columnsFrom": [
"createdById"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"Site_updatedById_User_id_fk": {
"name": "Site_updatedById_User_id_fk",
"tableFrom": "Site",
"tableTo": "User",
"columnsFrom": [
"updatedById"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"Site_siteCode_unique": {
"name": "Site_siteCode_unique",
"nullsNotDistinct": false,
"columns": [
"siteCode"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.spatial_ref_sys": {
"name": "spatial_ref_sys",
"schema": "",
"columns": {
"srid": {
"name": "srid",
"type": "integer",
"primaryKey": true,
"notNull": true
},
"auth_name": {
"name": "auth_name",
"type": "varchar(256)",
"primaryKey": false,
"notNull": false
},
"auth_srid": {
"name": "auth_srid",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"srtext": {
"name": "srtext",
"type": "varchar(2048)",
"primaryKey": false,
"notNull": false
},
"proj4text": {
"name": "proj4text",
"type": "varchar(2048)",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.TelecommunicationStationIdentification": {
"name": "TelecommunicationStationIdentification",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"siteId": {
"name": "siteId",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"stationIdentifier": {
"name": "stationIdentifier",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"serialNumber": {
"name": "serialNumber",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"isFirstCertification": {
"name": "isFirstCertification",
"type": "boolean",
"primaryKey": false,
"notNull": true
},
"modelReference": {
"name": "modelReference",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {
"TelecommunicationStationIdentification_siteId_idx": {
"name": "TelecommunicationStationIdentification_siteId_idx",
"columns": [
{
"expression": "siteId",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"TelecommunicationStationIdentification_stationIdentifier_idx": {
"name": "TelecommunicationStationIdentification_stationIdentifier_idx",
"columns": [
{
"expression": "stationIdentifier",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"TelecommunicationStationIdentification_siteId_Site_id_fk": {
"name": "TelecommunicationStationIdentification_siteId_Site_id_fk",
"tableFrom": "TelecommunicationStationIdentification",
"tableTo": "Site",
"columnsFrom": [
"siteId"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"TelecommunicationStationIdentification_siteId_unique": {
"name": "TelecommunicationStationIdentification_siteId_unique",
"nullsNotDistinct": false,
"columns": [
"siteId"
]
},
"TelecommunicationStationIdentification_stationIdentifier_unique": {
"name": "TelecommunicationStationIdentification_stationIdentifier_unique",
"nullsNotDistinct": false,
"columns": [
"stationIdentifier"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.UserSite": {
"name": "UserSite",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"userId": {
"name": "userId",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"siteId": {
"name": "siteId",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {
"UserSite_userId_siteId_key": {
"name": "UserSite_userId_siteId_key",
"columns": [
{
"expression": "userId",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "siteId",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
},
"UserSite_userId_idx": {
"name": "UserSite_userId_idx",
"columns": [
{
"expression": "userId",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"UserSite_siteId_idx": {
"name": "UserSite_siteId_idx",
"columns": [
{
"expression": "siteId",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"UserSite_userId_User_id_fk": {
"name": "UserSite_userId_User_id_fk",
"tableFrom": "UserSite",
"tableTo": "User",
"columnsFrom": [
"userId"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"UserSite_siteId_Site_id_fk": {
"name": "UserSite_siteId_Site_id_fk",
"tableFrom": "UserSite",
"tableTo": "Site",
"columnsFrom": [
"siteId"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.User": {
"name": "User",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"password": {
"name": "password",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"role": {
"name": "role",
"type": "Role",
"typeSchema": "public",
"primaryKey": false,
"notNull": true,
"default": "'VIEWER'"
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"resetToken": {
"name": "resetToken",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"resetTokenExpiry": {
"name": "resetTokenExpiry",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"isActive": {
"name": "isActive",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"partnerId": {
"name": "partnerId",
"type": "integer",
"primaryKey": false,
"notNull": false
}
},
"indexes": {
"User_email_idx": {
"name": "User_email_idx",
"columns": [
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"User_role_idx": {
"name": "User_role_idx",
"columns": [
{
"expression": "role",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"User_partnerId_idx": {
"name": "User_partnerId_idx",
"columns": [
{
"expression": "partnerId",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"User_partnerId_Partner_id_fk": {
"name": "User_partnerId_Partner_id_fk",
"tableFrom": "User",
"tableTo": "Partner",
"columnsFrom": [
"partnerId"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"User_email_unique": {
"name": "User_email_unique",
"nullsNotDistinct": false,
"columns": [
"email"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {
"public.CompanyName": {
"name": "CompanyName",
"schema": "public",
"values": [
"VODAFONE",
"MEO",
"NOS",
"DIGI"
]
},
"public.FinalCommentStatus": {
"name": "FinalCommentStatus",
"schema": "public",
"values": [
"PENDING",
"VALIDATED",
"REJECTED"
]
},
"public.InspectionResponseOption": {
"name": "InspectionResponseOption",
"schema": "public",
"values": [
"YES",
"NO",
"NA"
]
},
"public.InspectionStatus": {
"name": "InspectionStatus",
"schema": "public",
"values": [
"PENDING",
"IN_PROGRESS",
"COMPLETED",
"CANCELLED",
"APPROVING",
"REJECTED",
"APPROVED"
]
},
"public.Role": {
"name": "Role",
"schema": "public",
"values": [
"SUPERADMIN",
"ADMIN",
"MANAGER",
"PARTNER",
"OPERATOR",
"VIEWER"
]
}
},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}
\ No newline at end of file
{
"id": "638cb10c-afc9-4e9c-aaa6-8cd0d2eb4fdf",
"prevId": "26599364-fa1e-4e76-a1f4-4b678ac90cb2",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.InspectionPhoto": {
"name": "InspectionPhoto",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"url": {
"name": "url",
"type": "varchar(500)",
"primaryKey": false,
"notNull": true
},
"filename": {
"name": "filename",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"mimeType": {
"name": "mimeType",
"type": "varchar(100)",
"primaryKey": false,
"notNull": true
},
"size": {
"name": "size",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"inspectionId": {
"name": "inspectionId",
"type": "integer",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"InspectionPhoto_inspectionId_idx": {
"name": "InspectionPhoto_inspectionId_idx",
"columns": [
{
"expression": "inspectionId",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"InspectionPhoto_inspectionId_Inspection_id_fk": {
"name": "InspectionPhoto_inspectionId_Inspection_id_fk",
"tableFrom": "InspectionPhoto",
"tableTo": "Inspection",
"columnsFrom": [
"inspectionId"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.InspectionQuestion": {
"name": "InspectionQuestion",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"question": {
"name": "question",
"type": "text",
"primaryKey": false,
"notNull": true
},
"orderIndex": {
"name": "orderIndex",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.InspectionResponse": {
"name": "InspectionResponse",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"response": {
"name": "response",
"type": "InspectionResponseOption",
"typeSchema": "public",
"primaryKey": false,
"notNull": true
},
"comment": {
"name": "comment",
"type": "text",
"primaryKey": false,
"notNull": false
},
"questionId": {
"name": "questionId",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"inspectionId": {
"name": "inspectionId",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {
"InspectionResponse_questionId_idx": {
"name": "InspectionResponse_questionId_idx",
"columns": [
{
"expression": "questionId",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"InspectionResponse_inspectionId_idx": {
"name": "InspectionResponse_inspectionId_idx",
"columns": [
{
"expression": "inspectionId",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"InspectionResponse_inspectionId_questionId_key": {
"name": "InspectionResponse_inspectionId_questionId_key",
"columns": [
{
"expression": "inspectionId",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "questionId",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"InspectionResponse_questionId_InspectionQuestion_id_fk": {
"name": "InspectionResponse_questionId_InspectionQuestion_id_fk",
"tableFrom": "InspectionResponse",
"tableTo": "InspectionQuestion",
"columnsFrom": [
"questionId"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"InspectionResponse_inspectionId_Inspection_id_fk": {
"name": "InspectionResponse_inspectionId_Inspection_id_fk",
"tableFrom": "InspectionResponse",
"tableTo": "Inspection",
"columnsFrom": [
"inspectionId"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.Inspection": {
"name": "Inspection",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"date": {
"name": "date",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"deadline": {
"name": "deadline",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"comment": {
"name": "comment",
"type": "text",
"primaryKey": false,
"notNull": false
},
"finalComment": {
"name": "finalComment",
"type": "text",
"primaryKey": false,
"notNull": false
},
"finalCommentStatus": {
"name": "finalCommentStatus",
"type": "FinalCommentStatus",
"typeSchema": "public",
"primaryKey": false,
"notNull": false,
"default": "'PENDING'"
},
"siteId": {
"name": "siteId",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"createdById": {
"name": "createdById",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"updatedById": {
"name": "updatedById",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"status": {
"name": "status",
"type": "InspectionStatus",
"typeSchema": "public",
"primaryKey": false,
"notNull": true,
"default": "'PENDING'"
}
},
"indexes": {
"Inspection_siteId_idx": {
"name": "Inspection_siteId_idx",
"columns": [
{
"expression": "siteId",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"Inspection_createdById_idx": {
"name": "Inspection_createdById_idx",
"columns": [
{
"expression": "createdById",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"Inspection_updatedById_idx": {
"name": "Inspection_updatedById_idx",
"columns": [
{
"expression": "updatedById",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"Inspection_status_idx": {
"name": "Inspection_status_idx",
"columns": [
{
"expression": "status",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"Inspection_finalCommentStatus_idx": {
"name": "Inspection_finalCommentStatus_idx",
"columns": [
{
"expression": "finalCommentStatus",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"Inspection_siteId_Site_id_fk": {
"name": "Inspection_siteId_Site_id_fk",
"tableFrom": "Inspection",
"tableTo": "Site",
"columnsFrom": [
"siteId"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"Inspection_createdById_User_id_fk": {
"name": "Inspection_createdById_User_id_fk",
"tableFrom": "Inspection",
"tableTo": "User",
"columnsFrom": [
"createdById"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"Inspection_updatedById_User_id_fk": {
"name": "Inspection_updatedById_User_id_fk",
"tableFrom": "Inspection",
"tableTo": "User",
"columnsFrom": [
"updatedById"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.PartnerSite": {
"name": "PartnerSite",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"partnerId": {
"name": "partnerId",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"siteId": {
"name": "siteId",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {
"PartnerSite_partnerId_siteId_key": {
"name": "PartnerSite_partnerId_siteId_key",
"columns": [
{
"expression": "partnerId",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "siteId",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
},
"PartnerSite_partnerId_idx": {
"name": "PartnerSite_partnerId_idx",
"columns": [
{
"expression": "partnerId",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"PartnerSite_siteId_idx": {
"name": "PartnerSite_siteId_idx",
"columns": [
{
"expression": "siteId",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"PartnerSite_partnerId_Partner_id_fk": {
"name": "PartnerSite_partnerId_Partner_id_fk",
"tableFrom": "PartnerSite",
"tableTo": "Partner",
"columnsFrom": [
"partnerId"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"PartnerSite_siteId_Site_id_fk": {
"name": "PartnerSite_siteId_Site_id_fk",
"tableFrom": "PartnerSite",
"tableTo": "Site",
"columnsFrom": [
"siteId"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.Partner": {
"name": "Partner",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false
},
"isActive": {
"name": "isActive",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {
"Partner_name_idx": {
"name": "Partner_name_idx",
"columns": [
{
"expression": "name",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"Partner_name_unique": {
"name": "Partner_name_unique",
"nullsNotDistinct": false,
"columns": [
"name"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.RefreshToken": {
"name": "RefreshToken",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"token": {
"name": "token",
"type": "varchar(500)",
"primaryKey": false,
"notNull": true
},
"userId": {
"name": "userId",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"expiresAt": {
"name": "expiresAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {
"RefreshToken_token_idx": {
"name": "RefreshToken_token_idx",
"columns": [
{
"expression": "token",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"RefreshToken_userId_idx": {
"name": "RefreshToken_userId_idx",
"columns": [
{
"expression": "userId",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"RefreshToken_userId_User_id_fk": {
"name": "RefreshToken_userId_User_id_fk",
"tableFrom": "RefreshToken",
"tableTo": "User",
"columnsFrom": [
"userId"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"RefreshToken_token_unique": {
"name": "RefreshToken_token_unique",
"nullsNotDistinct": false,
"columns": [
"token"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.Site": {
"name": "Site",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"siteCode": {
"name": "siteCode",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"siteName": {
"name": "siteName",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"latitude": {
"name": "latitude",
"type": "double precision",
"primaryKey": false,
"notNull": true
},
"longitude": {
"name": "longitude",
"type": "double precision",
"primaryKey": false,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"createdById": {
"name": "createdById",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"updatedById": {
"name": "updatedById",
"type": "integer",
"primaryKey": false,
"notNull": false
}
},
"indexes": {
"Site_siteCode_idx": {
"name": "Site_siteCode_idx",
"columns": [
{
"expression": "siteCode",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"Site_createdById_User_id_fk": {
"name": "Site_createdById_User_id_fk",
"tableFrom": "Site",
"tableTo": "User",
"columnsFrom": [
"createdById"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"Site_updatedById_User_id_fk": {
"name": "Site_updatedById_User_id_fk",
"tableFrom": "Site",
"tableTo": "User",
"columnsFrom": [
"updatedById"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"Site_siteCode_unique": {
"name": "Site_siteCode_unique",
"nullsNotDistinct": false,
"columns": [
"siteCode"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.spatial_ref_sys": {
"name": "spatial_ref_sys",
"schema": "",
"columns": {
"srid": {
"name": "srid",
"type": "integer",
"primaryKey": true,
"notNull": true
},
"auth_name": {
"name": "auth_name",
"type": "varchar(256)",
"primaryKey": false,
"notNull": false
},
"auth_srid": {
"name": "auth_srid",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"srtext": {
"name": "srtext",
"type": "varchar(2048)",
"primaryKey": false,
"notNull": false
},
"proj4text": {
"name": "proj4text",
"type": "varchar(2048)",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.TelecommunicationStationIdentification": {
"name": "TelecommunicationStationIdentification",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"siteId": {
"name": "siteId",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"stationIdentifier": {
"name": "stationIdentifier",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"serialNumber": {
"name": "serialNumber",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"isFirstCertification": {
"name": "isFirstCertification",
"type": "boolean",
"primaryKey": false,
"notNull": true
},
"modelReference": {
"name": "modelReference",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {
"TelecommunicationStationIdentification_siteId_idx": {
"name": "TelecommunicationStationIdentification_siteId_idx",
"columns": [
{
"expression": "siteId",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"TelecommunicationStationIdentification_stationIdentifier_idx": {
"name": "TelecommunicationStationIdentification_stationIdentifier_idx",
"columns": [
{
"expression": "stationIdentifier",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"TelecommunicationStationIdentification_siteId_Site_id_fk": {
"name": "TelecommunicationStationIdentification_siteId_Site_id_fk",
"tableFrom": "TelecommunicationStationIdentification",
"tableTo": "Site",
"columnsFrom": [
"siteId"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"TelecommunicationStationIdentification_siteId_unique": {
"name": "TelecommunicationStationIdentification_siteId_unique",
"nullsNotDistinct": false,
"columns": [
"siteId"
]
},
"TelecommunicationStationIdentification_stationIdentifier_unique": {
"name": "TelecommunicationStationIdentification_stationIdentifier_unique",
"nullsNotDistinct": false,
"columns": [
"stationIdentifier"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.UserSite": {
"name": "UserSite",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"userId": {
"name": "userId",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"siteId": {
"name": "siteId",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {
"UserSite_userId_siteId_key": {
"name": "UserSite_userId_siteId_key",
"columns": [
{
"expression": "userId",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "siteId",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
},
"UserSite_userId_idx": {
"name": "UserSite_userId_idx",
"columns": [
{
"expression": "userId",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"UserSite_siteId_idx": {
"name": "UserSite_siteId_idx",
"columns": [
{
"expression": "siteId",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"UserSite_userId_User_id_fk": {
"name": "UserSite_userId_User_id_fk",
"tableFrom": "UserSite",
"tableTo": "User",
"columnsFrom": [
"userId"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"UserSite_siteId_Site_id_fk": {
"name": "UserSite_siteId_Site_id_fk",
"tableFrom": "UserSite",
"tableTo": "Site",
"columnsFrom": [
"siteId"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.User": {
"name": "User",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"password": {
"name": "password",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"role": {
"name": "role",
"type": "Role",
"typeSchema": "public",
"primaryKey": false,
"notNull": true,
"default": "'VIEWER'"
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"resetToken": {
"name": "resetToken",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"resetTokenExpiry": {
"name": "resetTokenExpiry",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"isActive": {
"name": "isActive",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"partnerId": {
"name": "partnerId",
"type": "integer",
"primaryKey": false,
"notNull": false
}
},
"indexes": {
"User_email_idx": {
"name": "User_email_idx",
"columns": [
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"User_role_idx": {
"name": "User_role_idx",
"columns": [
{
"expression": "role",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"User_partnerId_idx": {
"name": "User_partnerId_idx",
"columns": [
{
"expression": "partnerId",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"User_partnerId_Partner_id_fk": {
"name": "User_partnerId_Partner_id_fk",
"tableFrom": "User",
"tableTo": "Partner",
"columnsFrom": [
"partnerId"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"User_email_unique": {
"name": "User_email_unique",
"nullsNotDistinct": false,
"columns": [
"email"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {
"public.CompanyName": {
"name": "CompanyName",
"schema": "public",
"values": [
"VODAFONE",
"MEO",
"NOS",
"DIGI"
]
},
"public.FinalCommentStatus": {
"name": "FinalCommentStatus",
"schema": "public",
"values": [
"PENDING",
"VALIDATED",
"REJECTED"
]
},
"public.InspectionResponseOption": {
"name": "InspectionResponseOption",
"schema": "public",
"values": [
"YES",
"NO",
"NA"
]
},
"public.InspectionStatus": {
"name": "InspectionStatus",
"schema": "public",
"values": [
"PENDING",
"IN_PROGRESS",
"COMPLETED",
"CANCELLED",
"APPROVING",
"REJECTED",
"APPROVED"
]
},
"public.Role": {
"name": "Role",
"schema": "public",
"values": [
"SUPERADMIN",
"ADMIN",
"MANAGER",
"PARTNER",
"OPERATOR",
"VIEWER"
]
}
},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}
\ No newline at end of file
{
"version": "7",
"dialect": "postgresql",
"entries": [
{
"idx": 0,
"version": "7",
"when": 1757952962587,
"tag": "0000_overrated_jack_flag",
"breakpoints": true
},
{
"idx": 1,
"version": "7",
"when": 1758110934061,
"tag": "0001_parched_dust",
"breakpoints": true
},
{
"idx": 2,
"version": "7",
"when": 1758185080204,
"tag": "0002_furry_genesis",
"breakpoints": true
}
]
}
\ No newline at end of file
-- Migration: Add deadline field to Inspection table
-- Date: 2025-01-27
-- Description: Adds deadline field to support inspection generation with custom deadlines
-- Add deadline column to Inspection table
ALTER TABLE "Inspection" ADD COLUMN "deadline" timestamp;
-- Add comment to the new column for documentation
COMMENT ON COLUMN "Inspection"."deadline" IS 'Deadline date for when the inspection should be completed';
-- Optional: Create an index on the deadline column for better query performance
-- (Uncomment if you expect to query by deadline frequently)
-- CREATE INDEX "Inspection_deadline_idx" ON "Inspection" ("deadline");
-- Verify the migration
SELECT column_name, data_type, is_nullable
FROM information_schema.columns
WHERE table_name = 'Inspection' AND column_name = 'deadline';
-- Rollback Migration: Remove deadline field from Inspection table
-- Date: 2025-01-27
-- Description: Removes deadline field from Inspection table (rollback for migration.sql)
-- Remove the index if it was created (uncomment if you created the index)
-- DROP INDEX IF EXISTS "Inspection_deadline_idx";
-- Remove deadline column from Inspection table
ALTER TABLE "Inspection" DROP COLUMN IF EXISTS "deadline";
-- Verify the rollback
SELECT column_name, data_type, is_nullable
FROM information_schema.columns
WHERE table_name = 'Inspection' AND column_name = 'deadline';
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config'; import { ConfigModule, ConfigService } from '@nestjs/config';
import { APP_GUARD } from '@nestjs/core'; import { APP_GUARD, APP_FILTER } from '@nestjs/core';
import { ScheduleModule } from '@nestjs/schedule'; import { ScheduleModule } from '@nestjs/schedule';
import { AppController } from './app.controller'; import { AppController } from './app.controller';
import { AppService } from './app.service'; import { AppService } from './app.service';
...@@ -17,6 +17,7 @@ import { InspectionModule } from './modules/inspection/inspection.module'; ...@@ -17,6 +17,7 @@ import { InspectionModule } from './modules/inspection/inspection.module';
import { QuestionsModule } from './modules/questions/questions.module'; import { QuestionsModule } from './modules/questions/questions.module';
import { TelecommunicationStationIdentificationModule } from './modules/telecommunication-station-identification/telecommunication-station-identification.module'; import { TelecommunicationStationIdentificationModule } from './modules/telecommunication-station-identification/telecommunication-station-identification.module';
import { InspectionPhotosModule } from './modules/inspection-photos/inspection-photos.module'; import { InspectionPhotosModule } from './modules/inspection-photos/inspection-photos.module';
import { GlobalExceptionFilter } from './common/filters/global-exception.filter';
@Module({ @Module({
imports: [ imports: [
...@@ -66,6 +67,10 @@ import { InspectionPhotosModule } from './modules/inspection-photos/inspection-p ...@@ -66,6 +67,10 @@ import { InspectionPhotosModule } from './modules/inspection-photos/inspection-p
provide: APP_GUARD, provide: APP_GUARD,
useClass: JwtAuthGuard, useClass: JwtAuthGuard,
}, },
{
provide: APP_FILTER,
useClass: GlobalExceptionFilter,
},
], ],
}) })
export class AppModule {} export class AppModule {}
import {
ExceptionFilter,
Catch,
ArgumentsHost,
HttpException,
HttpStatus,
Logger,
} from '@nestjs/common';
import { Request, Response } from 'express';
@Catch()
export class GlobalExceptionFilter implements ExceptionFilter {
private readonly logger = new Logger(GlobalExceptionFilter.name);
catch(exception: unknown, host: ArgumentsHost): void {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
let status = HttpStatus.INTERNAL_SERVER_ERROR;
let message = 'Internal server error';
if (exception instanceof HttpException) {
status = exception.getStatus();
const exceptionResponse = exception.getResponse();
message =
typeof exceptionResponse === 'string'
? exceptionResponse
: (exceptionResponse as any)?.message || exception.message;
} else if (exception instanceof Error) {
message = exception.message;
this.logger.error(
`Unhandled error: ${exception.message}`,
exception.stack,
`${request.method} ${request.url}`,
);
}
const errorResponse = {
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
method: request.method,
message,
};
this.logger.error(
`HTTP ${status} Error: ${message}`,
JSON.stringify(errorResponse),
);
response.status(status).json(errorResponse);
}
}
...@@ -15,12 +15,12 @@ import { relations } from 'drizzle-orm'; ...@@ -15,12 +15,12 @@ import { relations } from 'drizzle-orm';
// Enums - matching Prisma enum names exactly // Enums - matching Prisma enum names exactly
export const roleEnum = pgEnum('Role', [ export const roleEnum = pgEnum('Role', [
'ADMIN', 'SUPERADMIN', // Main company - full access
'MANAGER', 'ADMIN', // Main company - admin access
'OPERATOR', 'MANAGER', // Main company - management access
'VIEWER', 'PARTNER', // Partner owner/manager - can validate final comments
'SUPERADMIN', 'OPERATOR', // Partner normal user - can create final comments
'PARTNER', 'VIEWER', // Keep for backward compatibility
]); ]);
export const companyNameEnum = pgEnum('CompanyName', [ export const companyNameEnum = pgEnum('CompanyName', [
...@@ -46,6 +46,12 @@ export const inspectionStatusEnum = pgEnum('InspectionStatus', [ ...@@ -46,6 +46,12 @@ export const inspectionStatusEnum = pgEnum('InspectionStatus', [
'APPROVED', 'APPROVED',
]); ]);
export const finalCommentStatusEnum = pgEnum('FinalCommentStatus', [
'PENDING', // Operator created, waiting for partner validation
'VALIDATED', // Partner validated, visible to admin/superadmin/manager
'REJECTED', // Partner rejected, needs revision
]);
// Tables // Tables
export const users = pgTable( export const users = pgTable(
'User', 'User',
...@@ -178,8 +184,11 @@ export const inspections = pgTable( ...@@ -178,8 +184,11 @@ export const inspections = pgTable(
{ {
id: serial('id').primaryKey(), id: serial('id').primaryKey(),
date: timestamp('date').notNull(), date: timestamp('date').notNull(),
deadline: timestamp('deadline'),
comment: text('comment'), comment: text('comment'),
finalComment: text('finalComment'), finalComment: text('finalComment'),
finalCommentStatus:
finalCommentStatusEnum('finalCommentStatus').default('PENDING'),
siteId: integer('siteId') siteId: integer('siteId')
.notNull() .notNull()
.references(() => sites.id, { onDelete: 'cascade' }), .references(() => sites.id, { onDelete: 'cascade' }),
...@@ -194,6 +203,9 @@ export const inspections = pgTable( ...@@ -194,6 +203,9 @@ export const inspections = pgTable(
createdByIdx: index('Inspection_createdById_idx').on(table.createdById), createdByIdx: index('Inspection_createdById_idx').on(table.createdById),
updatedByIdx: index('Inspection_updatedById_idx').on(table.updatedById), updatedByIdx: index('Inspection_updatedById_idx').on(table.updatedById),
statusIdx: index('Inspection_status_idx').on(table.status), statusIdx: index('Inspection_status_idx').on(table.status),
finalCommentStatusIdx: index('Inspection_finalCommentStatus_idx').on(
table.finalCommentStatus,
),
}), }),
); );
...@@ -219,6 +231,10 @@ export const inspectionResponses = pgTable( ...@@ -219,6 +231,10 @@ export const inspectionResponses = pgTable(
inspectionIdx: index('InspectionResponse_inspectionId_idx').on( inspectionIdx: index('InspectionResponse_inspectionId_idx').on(
table.inspectionId, table.inspectionId,
), ),
// Unique constraint to prevent duplicate responses for the same question in the same inspection
uniqueInspectionQuestion: uniqueIndex(
'InspectionResponse_inspectionId_questionId_key',
).on(table.inspectionId, table.questionId),
}), }),
); );
......
...@@ -24,11 +24,16 @@ async function bootstrap() { ...@@ -24,11 +24,16 @@ async function bootstrap() {
); );
// Serve static files // Serve static files
app.use('/uploads', express.static('/home/api-cellnex/public_html/uploads')); const uploadBasePath =
// In development, serve from local directory process.env.UPLOAD_BASE_PATH ||
if (process.env.NODE_ENV === 'development') { (process.env.NODE_ENV === 'production'
app.use('/uploads', express.static(join(__dirname, '..', 'uploads'))); ? '/home/api-verticalflow/public_html/uploads'
} : join(__dirname, '..', 'uploads'));
app.use('/uploads', express.static(uploadBasePath));
// Serve fallback uploads from /tmp if needed
app.use('/tmp-uploads', express.static('/tmp/uploads'));
// Swagger configuration // Swagger configuration
const config = new DocumentBuilder() const config = new DocumentBuilder()
...@@ -38,7 +43,6 @@ async function bootstrap() { ...@@ -38,7 +43,6 @@ async function bootstrap() {
.addTag('auth', 'Authentication endpoints') .addTag('auth', 'Authentication endpoints')
.addTag('users', 'User management endpoints') .addTag('users', 'User management endpoints')
.addTag('sites', 'Site management endpoints') .addTag('sites', 'Site management endpoints')
.addTag('candidates', 'Candidate management endpoints')
.addTag('inspection', 'Site inspection management endpoints') .addTag('inspection', 'Site inspection management endpoints')
.addBearerAuth( .addBearerAuth(
{ {
...@@ -64,7 +68,7 @@ async function bootstrap() { ...@@ -64,7 +68,7 @@ async function bootstrap() {
SwaggerModule.setup('docs', app, document, swaggerOptions); SwaggerModule.setup('docs', app, document, swaggerOptions);
const port = process.env.PORT ?? 3002; const port = process.env.PORT ?? 3000;
await app.listen(port); await app.listen(port);
console.log(`Application is running on: http://localhost:${port}`); console.log(`Application is running on: http://localhost:${port}`);
console.log( console.log(
......
...@@ -87,10 +87,12 @@ export class AuthService { ...@@ -87,10 +87,12 @@ export class AuthService {
}), }),
]); ]);
const now = new Date();
await this.databaseService.db.insert(refreshTokens).values({ await this.databaseService.db.insert(refreshTokens).values({
token: refreshToken, token: refreshToken,
userId: user.id, userId: user.id,
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7 days expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7 days
createdAt: now,
}); });
return { return {
...@@ -176,10 +178,12 @@ export class AuthService { ...@@ -176,10 +178,12 @@ export class AuthService {
]); ]);
// Store new refresh token // Store new refresh token
const now = new Date();
await this.databaseService.db.insert(refreshTokens).values({ await this.databaseService.db.insert(refreshTokens).values({
token: newRefreshToken, token: newRefreshToken,
userId: user.id, userId: user.id,
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7 days expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7 days
createdAt: now,
}); });
return { return {
......
import { SetMetadata } from '@nestjs/common'; import { SetMetadata } from '@nestjs/common';
import { Role } from '@prisma/client'; import { roleEnum } from '../../../database/schema';
export const ROLES_KEY = 'roles'; export const ROLES_KEY = 'roles';
export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles); export const Roles = (...roles: (typeof roleEnum.enumValues)[number][]) =>
SetMetadata(ROLES_KEY, roles);
...@@ -4,7 +4,7 @@ import { ...@@ -4,7 +4,7 @@ import {
ExecutionContext, ExecutionContext,
ForbiddenException, ForbiddenException,
} from '@nestjs/common'; } from '@nestjs/common';
import { Role } from '@prisma/client'; import { roleEnum } from '../../../database/schema';
@Injectable() @Injectable()
export class PartnerAuthGuard implements CanActivate { export class PartnerAuthGuard implements CanActivate {
...@@ -15,8 +15,8 @@ export class PartnerAuthGuard implements CanActivate { ...@@ -15,8 +15,8 @@ export class PartnerAuthGuard implements CanActivate {
? parseInt(request.params.partnerId, 10) ? parseInt(request.params.partnerId, 10)
: null; : null;
// If it's a PARTNER user, make sure they can only access their own partner data // If it's a PARTNER or OPERATOR user, make sure they can only access their own partner data
if (user.role === Role.PARTNER) { if (user.role === 'PARTNER' || user.role === 'OPERATOR') {
// Check if the user has a partnerId and if it matches the requested partnerId // Check if the user has a partnerId and if it matches the requested partnerId
if (!user.partnerId) { if (!user.partnerId) {
throw new ForbiddenException( throw new ForbiddenException(
......
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core'; import { Reflector } from '@nestjs/core';
import { Role } from '@prisma/client'; import { roleEnum } from '../../../database/schema';
import { ROLES_KEY } from '../decorators/roles.decorator'; import { ROLES_KEY } from '../decorators/roles.decorator';
@Injectable() @Injectable()
...@@ -8,10 +8,9 @@ export class RolesGuard implements CanActivate { ...@@ -8,10 +8,9 @@ export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {} constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean { canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.getAllAndOverride<Role[]>(ROLES_KEY, [ const requiredRoles = this.reflector.getAllAndOverride<
context.getHandler(), (typeof roleEnum.enumValues)[number][]
context.getClass(), >(ROLES_KEY, [context.getHandler(), context.getClass()]);
]);
if (!requiredRoles) { if (!requiredRoles) {
return true; return true;
......
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsNumber, IsOptional, IsString } from 'class-validator'; import { IsNumber, IsOptional, IsString, IsPositive } from 'class-validator';
import { Transform } from 'class-transformer';
export class CreateInspectionPhotoDto { export class CreateInspectionPhotoDto {
@ApiProperty({ @ApiProperty({
...@@ -7,7 +8,15 @@ export class CreateInspectionPhotoDto { ...@@ -7,7 +8,15 @@ export class CreateInspectionPhotoDto {
example: 1, example: 1,
type: Number, type: Number,
}) })
@Transform(({ value }) => {
const parsed = parseInt(value);
if (isNaN(parsed)) {
throw new Error('inspectionId must be a valid number');
}
return parsed;
})
@IsNumber() @IsNumber()
@IsPositive()
inspectionId: number; inspectionId: number;
@ApiPropertyOptional({ @ApiPropertyOptional({
......
...@@ -16,7 +16,7 @@ import { FileInterceptor } from '@nestjs/platform-express'; ...@@ -16,7 +16,7 @@ import { FileInterceptor } from '@nestjs/platform-express';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
import { RolesGuard } from '../auth/guards/roles.guard'; import { RolesGuard } from '../auth/guards/roles.guard';
import { Roles } from '../auth/decorators/roles.decorator'; import { Roles } from '../auth/decorators/roles.decorator';
import { Role } from '@prisma/client'; import { roleEnum } from '../../database/schema';
import { InspectionPhotosService } from './inspection-photos.service'; import { InspectionPhotosService } from './inspection-photos.service';
import { import {
CreateInspectionPhotoDto, CreateInspectionPhotoDto,
...@@ -46,7 +46,7 @@ export class InspectionPhotosController { ...@@ -46,7 +46,7 @@ export class InspectionPhotosController {
@Post() @Post()
@UseGuards(JwtAuthGuard, RolesGuard) @UseGuards(JwtAuthGuard, RolesGuard)
@Roles(Role.ADMIN, Role.MANAGER, Role.OPERATOR, Role.PARTNER, Role.SUPERADMIN) @Roles('ADMIN', 'MANAGER', 'OPERATOR', 'PARTNER', 'SUPERADMIN')
@UseInterceptors(FileInterceptor('photo', multerConfig)) @UseInterceptors(FileInterceptor('photo', multerConfig))
@ApiOperation({ @ApiOperation({
summary: 'Upload a new inspection photo', summary: 'Upload a new inspection photo',
...@@ -94,10 +94,15 @@ export class InspectionPhotosController { ...@@ -94,10 +94,15 @@ export class InspectionPhotosController {
@Body() createInspectionPhotoDto: CreateInspectionPhotoDto, @Body() createInspectionPhotoDto: CreateInspectionPhotoDto,
@UploadedFile() file: Express.Multer.File, @UploadedFile() file: Express.Multer.File,
) { ) {
return this.inspectionPhotosService.createInspectionPhoto( try {
createInspectionPhotoDto, return await this.inspectionPhotosService.createInspectionPhoto(
file, createInspectionPhotoDto,
); file,
);
} catch (error) {
console.error('Error uploading inspection photo:', error);
throw error;
}
} }
@Get() @Get()
...@@ -178,7 +183,7 @@ export class InspectionPhotosController { ...@@ -178,7 +183,7 @@ export class InspectionPhotosController {
@Put(':id') @Put(':id')
@UseGuards(JwtAuthGuard, RolesGuard) @UseGuards(JwtAuthGuard, RolesGuard)
@Roles(Role.ADMIN, Role.MANAGER, Role.OPERATOR, Role.PARTNER, Role.SUPERADMIN) @Roles('ADMIN', 'MANAGER', 'OPERATOR', 'PARTNER', 'SUPERADMIN')
@ApiOperation({ @ApiOperation({
summary: 'Update an inspection photo', summary: 'Update an inspection photo',
description: description:
...@@ -218,7 +223,7 @@ export class InspectionPhotosController { ...@@ -218,7 +223,7 @@ export class InspectionPhotosController {
@Delete(':id') @Delete(':id')
@UseGuards(JwtAuthGuard, RolesGuard) @UseGuards(JwtAuthGuard, RolesGuard)
@Roles(Role.ADMIN, Role.MANAGER, Role.OPERATOR, Role.PARTNER, Role.SUPERADMIN) @Roles('ADMIN', 'MANAGER', 'OPERATOR', 'PARTNER', 'SUPERADMIN')
@ApiOperation({ @ApiOperation({
summary: 'Delete an inspection photo', summary: 'Delete an inspection photo',
description: description:
......
...@@ -20,6 +20,11 @@ export class InspectionPhotosService { ...@@ -20,6 +20,11 @@ export class InspectionPhotosService {
dto: CreateInspectionPhotoDto, dto: CreateInspectionPhotoDto,
file: Express.Multer.File, file: Express.Multer.File,
): Promise<InspectionPhotoResponseDto> { ): Promise<InspectionPhotoResponseDto> {
// Validate file parameter
if (!file) {
throw new Error('No file provided. Please upload a photo file.');
}
// Check if inspection exists // Check if inspection exists
const inspectionList = await this.databaseService.db const inspectionList = await this.databaseService.db
.select() .select()
...@@ -37,6 +42,7 @@ export class InspectionPhotosService { ...@@ -37,6 +42,7 @@ export class InspectionPhotosService {
const filePaths = await saveInspectionPhotos([file], dto.inspectionId); const filePaths = await saveInspectionPhotos([file], dto.inspectionId);
// Create photo record in database // Create photo record in database
const now = new Date();
const [photo] = await this.databaseService.db const [photo] = await this.databaseService.db
.insert(inspectionPhotos) .insert(inspectionPhotos)
.values({ .values({
...@@ -46,6 +52,8 @@ export class InspectionPhotosService { ...@@ -46,6 +52,8 @@ export class InspectionPhotosService {
url: filePaths[0], url: filePaths[0],
description: dto.description, description: dto.description,
inspectionId: dto.inspectionId, inspectionId: dto.inspectionId,
createdAt: now,
updatedAt: now,
}) })
.returning(); .returning();
......
import { import {
IsDateString, IsDateString,
IsEnum,
IsInt, IsInt,
IsOptional, IsOptional,
IsString, IsString,
...@@ -24,7 +25,7 @@ export class CreateInspectionResponseDto { ...@@ -24,7 +25,7 @@ export class CreateInspectionResponseDto {
example: InspectionResponseOption.YES, example: InspectionResponseOption.YES,
enumName: 'InspectionResponseOption', enumName: 'InspectionResponseOption',
}) })
@IsString() @IsEnum(InspectionResponseOption)
response: InspectionResponseOption; response: InspectionResponseOption;
@ApiPropertyOptional({ @ApiPropertyOptional({
......
import { IsDateString, IsInt, IsOptional, IsEnum } from 'class-validator'; import { IsDateString, IsInt, IsOptional, IsEnum } from 'class-validator';
import { Type } from 'class-transformer'; import { Type } from 'class-transformer';
import { ApiPropertyOptional } from '@nestjs/swagger'; import { ApiPropertyOptional } from '@nestjs/swagger';
import { InspectionStatus } from '@prisma/client'; import { inspectionStatusEnum } from '../../../database/schema';
export class FindInspectionDto { export class FindInspectionDto {
@ApiPropertyOptional({ @ApiPropertyOptional({
...@@ -36,11 +36,11 @@ export class FindInspectionDto { ...@@ -36,11 +36,11 @@ export class FindInspectionDto {
@ApiPropertyOptional({ @ApiPropertyOptional({
description: 'Filter inspection records by status', description: 'Filter inspection records by status',
enum: InspectionStatus, enum: inspectionStatusEnum.enumValues,
example: InspectionStatus.PENDING, example: 'PENDING',
enumName: 'InspectionStatus', enumName: 'InspectionStatus',
}) })
@IsEnum(InspectionStatus) @IsEnum(inspectionStatusEnum.enumValues)
@IsOptional() @IsOptional()
status?: InspectionStatus; status?: (typeof inspectionStatusEnum.enumValues)[number];
} }
import { IsDateString, IsInt, IsOptional, IsString } from 'class-validator';
import { Type } from 'class-transformer';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
export class GenerateInspectionDto {
@ApiProperty({
description: 'Deadline date for the inspection to be completed',
example: '2025-12-31T23:59:59.000Z',
type: String,
})
@IsDateString()
deadline: string;
@ApiPropertyOptional({
description:
'ID of the partner to associate the inspection with (if not provided, will use the partner already associated with the site)',
example: 1,
type: Number,
})
@Type(() => Number)
@IsInt()
@IsOptional()
partnerId?: number;
@ApiProperty({
description: 'ID of the site to generate inspection for',
example: 1,
type: Number,
})
@Type(() => Number)
@IsInt()
siteId: number;
@ApiPropertyOptional({
description: 'Optional comment for the generated inspection',
example: 'Annual inspection scheduled for this site',
type: String,
})
@IsString()
@IsOptional()
comment?: string;
}
export * from './create-inspection.dto'; export * from './create-inspection.dto';
export * from './find-inspection.dto'; export * from './find-inspection.dto';
export * from './generate-inspection.dto';
export * from './inspection-response.dto'; export * from './inspection-response.dto';
export * from './inspection-response-option.enum'; export * from './inspection-response-option.enum';
export * from './start-inspection.dto'; export * from './start-inspection.dto';
......
...@@ -2,7 +2,10 @@ import { InspectionResponseOption } from './inspection-response-option.enum'; ...@@ -2,7 +2,10 @@ import { InspectionResponseOption } from './inspection-response-option.enum';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
// Import the status enum from Prisma // Import the status enum from Prisma
import { InspectionStatus } from '@prisma/client'; import {
inspectionStatusEnum,
finalCommentStatusEnum,
} from '../../../database/schema';
export class InspectionQuestionDto { export class InspectionQuestionDto {
@ApiProperty({ @ApiProperty({
...@@ -103,6 +106,13 @@ export class InspectionDto { ...@@ -103,6 +106,13 @@ export class InspectionDto {
date: Date; date: Date;
@ApiPropertyOptional({ @ApiPropertyOptional({
description: 'Deadline date for the inspection to be completed',
example: '2025-12-31T23:59:59.000Z',
type: Date,
})
deadline?: Date;
@ApiPropertyOptional({
description: 'Optional general comment about the inspection', description: 'Optional general comment about the inspection',
example: 'Annual preventive inspection completed with minor issues noted', example: 'Annual preventive inspection completed with minor issues noted',
type: String, type: String,
...@@ -118,6 +128,14 @@ export class InspectionDto { ...@@ -118,6 +128,14 @@ export class InspectionDto {
}) })
finalComment?: string; finalComment?: string;
@ApiPropertyOptional({
description: 'Status of the final comment validation',
enum: finalCommentStatusEnum.enumValues,
example: 'PENDING',
enumName: 'FinalCommentStatus',
})
finalCommentStatus?: (typeof finalCommentStatusEnum.enumValues)[number];
@ApiProperty({ @ApiProperty({
description: 'ID of the site where inspection was performed', description: 'ID of the site where inspection was performed',
example: 1, example: 1,
...@@ -127,11 +145,11 @@ export class InspectionDto { ...@@ -127,11 +145,11 @@ export class InspectionDto {
@ApiProperty({ @ApiProperty({
description: 'Current status of the inspection', description: 'Current status of the inspection',
enum: InspectionStatus, enum: inspectionStatusEnum.enumValues,
example: InspectionStatus.PENDING, example: 'PENDING',
enumName: 'InspectionStatus', enumName: 'InspectionStatus',
}) })
status: InspectionStatus; status: (typeof inspectionStatusEnum.enumValues)[number];
@ApiProperty({ @ApiProperty({
description: 'Date and time when the record was created', description: 'Date and time when the record was created',
......
import { IsEnum } from 'class-validator'; import { IsEnum } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { InspectionStatus } from '@prisma/client'; import { inspectionStatusEnum } from '../../../database/schema';
export class UpdateInspectionStatusDto { export class UpdateInspectionStatusDto {
@ApiProperty({ @ApiProperty({
description: 'New status for the inspection', description: 'New status for the inspection',
enum: InspectionStatus, enum: inspectionStatusEnum.enumValues,
example: InspectionStatus.IN_PROGRESS, example: 'IN_PROGRESS',
enumName: 'InspectionStatus', enumName: 'InspectionStatus',
}) })
@IsEnum(InspectionStatus) @IsEnum(inspectionStatusEnum.enumValues)
status: InspectionStatus; status: (typeof inspectionStatusEnum.enumValues)[number];
} }
...@@ -31,6 +31,7 @@ import { ...@@ -31,6 +31,7 @@ import {
CreateInspectionResponseDto, CreateInspectionResponseDto,
} from './dto/create-inspection.dto'; } from './dto/create-inspection.dto';
import { FindInspectionDto } from './dto/find-inspection.dto'; import { FindInspectionDto } from './dto/find-inspection.dto';
import { GenerateInspectionDto } from './dto/generate-inspection.dto';
import { UpdateInspectionStatusDto } from './dto/update-inspection-status.dto'; import { UpdateInspectionStatusDto } from './dto/update-inspection-status.dto';
import { StartInspectionDto } from './dto/start-inspection.dto'; import { StartInspectionDto } from './dto/start-inspection.dto';
import { UpdateFinalCommentDto } from './dto/update-final-comment.dto'; import { UpdateFinalCommentDto } from './dto/update-final-comment.dto';
...@@ -135,6 +136,40 @@ export class InspectionController { ...@@ -135,6 +136,40 @@ export class InspectionController {
); );
} }
@Post('generate')
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles(Role.ADMIN, Role.MANAGER, Role.OPERATOR, Role.SUPERADMIN)
@ApiOperation({
summary: 'Generate a new inspection with deadline for a specific site',
description:
'Generates a new inspection with a specified deadline date for a specific site. If a partnerId is provided, it will associate/switch the site to that partner. If no partnerId is provided, it will use the partner already associated with the site. The system will automatically schedule the next inspection 1 year after the deadline, 3 months in advance.',
})
@ApiResponse({
status: 201,
description: 'The inspection has been successfully generated.',
type: InspectionDto,
})
@ApiResponse({ status: 400, description: 'Invalid input data.' })
@ApiResponse({ status: 401, description: 'Unauthorized.' })
@ApiResponse({
status: 403,
description: 'Forbidden - Insufficient permissions.',
})
@ApiResponse({ status: 404, description: 'Site or partner not found.' })
@ApiBody({
description: 'Inspection generation data',
type: GenerateInspectionDto,
})
async generateInspection(
@Body() generateInspectionDto: GenerateInspectionDto,
@Req() req,
) {
return this.inspectionService.generateInspection(
generateInspectionDto,
req.user.id,
);
}
@Get() @Get()
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@ApiOperation({ @ApiOperation({
...@@ -276,17 +311,17 @@ export class InspectionController { ...@@ -276,17 +311,17 @@ export class InspectionController {
return this.inspectionService.getResponsesByInspectionId(id); return this.inspectionService.getResponsesByInspectionId(id);
} }
@Post(':id/responses') @Patch(':id/responses')
@UseGuards(JwtAuthGuard, RolesGuard) @UseGuards(JwtAuthGuard, RolesGuard)
@Roles(Role.ADMIN, Role.MANAGER, Role.OPERATOR, Role.PARTNER, Role.SUPERADMIN) @Roles(Role.ADMIN, Role.MANAGER, Role.OPERATOR, Role.PARTNER, Role.SUPERADMIN)
@ApiOperation({ @ApiOperation({
summary: 'Add responses to an existing inspection record', summary: 'Update responses for an existing inspection record',
description: description:
'Adds or updates responses for a specific inspection record. Can be used to complete a partially filled inspection record.', 'Updates or creates responses for a specific inspection record. If a response already exists for a question, it will be updated. If not, a new response will be created.',
}) })
@ApiResponse({ @ApiResponse({
status: 201, status: 200,
description: 'The responses have been successfully added.', description: 'The responses have been successfully updated.',
type: [InspectionResponseDto], type: [InspectionResponseDto],
}) })
@ApiResponse({ status: 400, description: 'Invalid input data.' }) @ApiResponse({ status: 400, description: 'Invalid input data.' })
...@@ -306,15 +341,15 @@ export class InspectionController { ...@@ -306,15 +341,15 @@ export class InspectionController {
example: 1, example: 1,
}) })
@ApiBody({ @ApiBody({
description: 'Array of inspection responses to add', description: 'Array of inspection responses to update or create',
type: [CreateInspectionResponseDto], type: [CreateInspectionResponseDto],
}) })
async addResponsesToInspection( async updateResponsesToInspection(
@Param('id', ParseIntPipe) id: number, @Param('id', ParseIntPipe) id: number,
@Body() responses: CreateInspectionResponseDto[], @Body() responses: CreateInspectionResponseDto[],
@Req() req, @Req() req,
) { ) {
return this.inspectionService.addResponsesToInspection( return this.inspectionService.updateResponsesToInspection(
id, id,
responses, responses,
req.user.id, req.user.id,
...@@ -581,6 +616,64 @@ export class InspectionController { ...@@ -581,6 +616,64 @@ export class InspectionController {
id, id,
updateFinalCommentDto.finalComment, updateFinalCommentDto.finalComment,
req.user.id, req.user.id,
req.user.role,
);
}
@Patch(':id/final-comment/validate')
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles(Role.PARTNER)
@ApiOperation({
summary: 'Validate or reject a final comment',
description:
'Allows partner owners/managers to validate or reject final comments created by operators.',
})
@ApiResponse({
status: 200,
description: 'The final comment validation status has been updated.',
type: InspectionDto,
})
@ApiResponse({ status: 400, description: 'Invalid input data.' })
@ApiResponse({ status: 401, description: 'Unauthorized.' })
@ApiResponse({
status: 403,
description: 'Forbidden - Only PARTNER role can validate final comments.',
})
@ApiResponse({ status: 404, description: 'Inspection not found.' })
@ApiParam({
name: 'id',
type: Number,
description: 'ID of the inspection to validate',
example: 1,
})
@ApiBody({
description: 'Validation data',
schema: {
type: 'object',
properties: {
action: {
type: 'string',
enum: ['validate', 'reject'],
description: 'Action to perform on the final comment',
},
comment: {
type: 'string',
description: 'Optional comment for rejection reason',
},
},
required: ['action'],
},
})
async validateFinalComment(
@Param('id', ParseIntPipe) id: number,
@Body() validationDto: { action: 'validate' | 'reject'; comment?: string },
@Req() req,
) {
return this.inspectionService.validateFinalComment(
id,
validationDto.action,
req.user.id,
validationDto.comment,
); );
} }
} }
...@@ -161,8 +161,8 @@ export class InspectionGateway ...@@ -161,8 +161,8 @@ export class InspectionGateway
try { try {
// Update the response in the database // Update the response in the database
const updatedResponses = const updatedInspection =
await this.inspectionService.addResponsesToInspection( await this.inspectionService.updateResponsesToInspection(
inspectionId, inspectionId,
[response], [response],
userId, userId,
...@@ -171,7 +171,7 @@ export class InspectionGateway ...@@ -171,7 +171,7 @@ export class InspectionGateway
// Broadcast the update to all users in the room // Broadcast the update to all users in the room
this.server.to(`inspection_${inspectionId}`).emit('responseUpdated', { this.server.to(`inspection_${inspectionId}`).emit('responseUpdated', {
inspectionId, inspectionId,
response: updatedResponses[0], inspection: updatedInspection,
updatedBy: client.id, updatedBy: client.id,
timestamp: new Date(), timestamp: new Date(),
}); });
......
import { Injectable, NotFoundException } from '@nestjs/common'; import {
Injectable,
NotFoundException,
ForbiddenException,
} from '@nestjs/common';
import { DatabaseService } from '../../database/database.service'; import { DatabaseService } from '../../database/database.service';
import { import {
inspections, inspections,
...@@ -7,6 +11,8 @@ import { ...@@ -7,6 +11,8 @@ import {
sites, sites,
users, users,
inspectionQuestions, inspectionQuestions,
partnerSites,
partners,
} from '../../database/schema'; } from '../../database/schema';
import { import {
eq, eq,
...@@ -26,6 +32,7 @@ import { ...@@ -26,6 +32,7 @@ import {
CreateInspectionResponseDto, CreateInspectionResponseDto,
} from './dto/create-inspection.dto'; } from './dto/create-inspection.dto';
import { FindInspectionDto } from './dto/find-inspection.dto'; import { FindInspectionDto } from './dto/find-inspection.dto';
import { GenerateInspectionDto } from './dto/generate-inspection.dto';
import { import {
InspectionDto, InspectionDto,
InspectionResponseDto, InspectionResponseDto,
...@@ -58,6 +65,7 @@ export class InspectionService { ...@@ -58,6 +65,7 @@ export class InspectionService {
} }
// Create inspection record // Create inspection record
const now = new Date();
const [inspection] = await this.databaseService.db const [inspection] = await this.databaseService.db
.insert(inspections) .insert(inspections)
.values({ .values({
...@@ -66,6 +74,8 @@ export class InspectionService { ...@@ -66,6 +74,8 @@ export class InspectionService {
finalComment: dto.finalComment, finalComment: dto.finalComment,
siteId: dto.siteId, siteId: dto.siteId,
createdById: userId, createdById: userId,
createdAt: now,
updatedAt: now,
}) })
.returning(); .returning();
...@@ -77,6 +87,8 @@ export class InspectionService { ...@@ -77,6 +87,8 @@ export class InspectionService {
comment: response.comment, comment: response.comment,
questionId: response.questionId, questionId: response.questionId,
inspectionId: inspection.id, inspectionId: inspection.id,
createdAt: new Date(),
updatedAt: new Date(),
})), })),
); );
} }
...@@ -92,6 +104,122 @@ export class InspectionService { ...@@ -92,6 +104,122 @@ export class InspectionService {
size: file.size, size: file.size,
url: filePaths[index], url: filePaths[index],
inspectionId: inspection.id, inspectionId: inspection.id,
createdAt: now,
updatedAt: now,
})),
);
}
return this.mapToInspectionDto(inspection);
}
async generateInspection(
dto: GenerateInspectionDto,
userId: number,
): Promise<InspectionDto> {
// Check if site exists
const siteList = await this.databaseService.db
.select()
.from(sites)
.where(eq(sites.id, dto.siteId))
.limit(1);
if (siteList.length === 0) {
throw new NotFoundException(`Site with ID ${dto.siteId} not found`);
}
const site = siteList[0];
let partnerId = dto.partnerId;
// If no partner ID provided, get the partner already associated with the site
if (!partnerId) {
const partnerSiteList = await this.databaseService.db
.select({
partnerId: partnerSites.partnerId,
})
.from(partnerSites)
.where(eq(partnerSites.siteId, dto.siteId))
.limit(1);
if (partnerSiteList.length === 0) {
throw new NotFoundException(
`No partner associated with site ID ${dto.siteId}. Please provide a partnerId or associate a partner with this site first.`,
);
}
partnerId = partnerSiteList[0].partnerId;
} else {
// If partner ID is provided, verify it exists and associate it with the site
const partnerList = await this.databaseService.db
.select()
.from(partners)
.where(eq(partners.id, partnerId))
.limit(1);
if (partnerList.length === 0) {
throw new NotFoundException(`Partner with ID ${partnerId} not found`);
}
// Check if the site is already associated with a different partner
const existingPartnerSite = await this.databaseService.db
.select()
.from(partnerSites)
.where(eq(partnerSites.siteId, dto.siteId))
.limit(1);
if (existingPartnerSite.length > 0) {
// Update the association to the new partner
await this.databaseService.db
.update(partnerSites)
.set({
partnerId: partnerId,
updatedAt: new Date(),
})
.where(eq(partnerSites.siteId, dto.siteId));
} else {
// Create new association
const now = new Date();
await this.databaseService.db.insert(partnerSites).values({
partnerId: partnerId,
siteId: dto.siteId,
createdAt: now,
updatedAt: now,
});
}
}
// Create inspection record with deadline
const now = new Date();
const [inspection] = await this.databaseService.db
.insert(inspections)
.values({
date: now, // Current date as inspection date
deadline: new Date(dto.deadline),
comment: dto.comment || null,
siteId: dto.siteId,
createdById: userId,
createdAt: now,
updatedAt: now,
status: 'PENDING',
})
.returning();
// Get all inspection questions to create default "NA" responses
const questions = await this.questionsService.findAllQuestions({
page: 1,
limit: 100,
});
// Create default "NA" responses for all questions
if (questions.questions && questions.questions.length > 0) {
await this.databaseService.db.insert(inspectionResponses).values(
questions.questions.map((question) => ({
response: 'NA' as any,
comment: null,
questionId: question.id,
inspectionId: inspection.id,
createdAt: now,
updatedAt: now,
})), })),
); );
} }
...@@ -120,7 +248,9 @@ export class InspectionService { ...@@ -120,7 +248,9 @@ export class InspectionService {
.where(whereCondition) .where(whereCondition)
.orderBy(desc(inspections.createdAt)); .orderBy(desc(inspections.createdAt));
return inspectionList.map(this.mapToInspectionDto); return inspectionList.map((inspection) =>
this.mapToInspectionDto(inspection),
);
} }
async findInspectionById(id: number): Promise<InspectionDto> { async findInspectionById(id: number): Promise<InspectionDto> {
...@@ -134,7 +264,34 @@ export class InspectionService { ...@@ -134,7 +264,34 @@ export class InspectionService {
throw new NotFoundException(`Inspection with ID ${id} not found`); throw new NotFoundException(`Inspection with ID ${id} not found`);
} }
return this.mapToInspectionDto(inspectionList[0]); const inspection = inspectionList[0];
// Fetch responses with questions
const responses = await this.databaseService.db
.select({
id: inspectionResponses.id,
response: inspectionResponses.response,
comment: inspectionResponses.comment,
question: {
id: inspectionQuestions.id,
question: inspectionQuestions.question,
orderIndex: inspectionQuestions.orderIndex,
},
})
.from(inspectionResponses)
.leftJoin(
inspectionQuestions,
eq(inspectionResponses.questionId, inspectionQuestions.id),
)
.where(eq(inspectionResponses.inspectionId, id));
// Fetch photos
const photos = await this.databaseService.db
.select()
.from(inspectionPhotos)
.where(eq(inspectionPhotos.inspectionId, id));
return this.mapToInspectionDto(inspection, responses, photos);
} }
async getInspectionQuestions() { async getInspectionQuestions() {
...@@ -151,54 +308,267 @@ export class InspectionService { ...@@ -151,54 +308,267 @@ export class InspectionService {
created: number; created: number;
sites: any[]; sites: any[];
}> { }> {
// Simplified implementation - TODO: Add proper automatic inspection creation logic const now = new Date();
const threeMonthsFromNow = new Date(now);
threeMonthsFromNow.setMonth(threeMonthsFromNow.getMonth() + 3);
// Find inspections that have deadlines within the next 3 months
// and don't have a next inspection already scheduled
const inspectionsNeedingScheduling = await this.databaseService.db
.select({
inspection: {
id: inspections.id,
deadline: inspections.deadline,
siteId: inspections.siteId,
},
site: {
id: sites.id,
siteCode: sites.siteCode,
siteName: sites.siteName,
},
})
.from(inspections)
.leftJoin(sites, eq(inspections.siteId, sites.id))
.where(
and(
eq(inspections.status, 'COMPLETED'), // Only completed inspections
ne(inspections.deadline, null as any), // Must have a deadline
// Deadline is within the next 3 months
// This is a simplified check - in production you'd want more precise date logic
),
);
const createdInspections: any[] = [];
let createdCount = 0;
for (const inspectionData of inspectionsNeedingScheduling) {
if (!inspectionData.inspection.deadline) continue;
const deadlineDate = new Date(inspectionData.inspection.deadline);
// Check if we're within 3 months of the deadline
const timeDiff = deadlineDate.getTime() - now.getTime();
const monthsDiff = timeDiff / (1000 * 3600 * 24 * 30); // Approximate months
if (monthsDiff <= 3 && monthsDiff > 0) {
// Calculate next deadline (1 year after current deadline)
const nextDeadline = new Date(deadlineDate);
nextDeadline.setFullYear(nextDeadline.getFullYear() + 1);
// Create new inspection with next deadline
const [newInspection] = await this.databaseService.db
.insert(inspections)
.values({
date: now,
deadline: nextDeadline,
comment: null,
siteId: inspectionData.inspection.siteId,
createdById: userId,
createdAt: now,
updatedAt: now,
status: 'PENDING',
})
.returning();
// Get all inspection questions to create default "NA" responses
const questions = await this.questionsService.findAllQuestions({
page: 1,
limit: 100,
});
// Create default "NA" responses for all questions
if (questions.questions && questions.questions.length > 0) {
await this.databaseService.db.insert(inspectionResponses).values(
questions.questions.map((question) => ({
response: 'NA' as any,
comment: null,
questionId: question.id,
inspectionId: newInspection.id,
createdAt: now,
updatedAt: now,
})),
);
}
createdInspections.push({
siteId: inspectionData.site?.id || 0,
siteCode: inspectionData.site?.siteCode || 'Unknown',
siteName: inspectionData.site?.siteName || 'Unknown Site',
inspectionId: newInspection.id,
});
createdCount++;
}
}
return { return {
message: message: `Successfully created ${createdCount} automatic inspections`,
'Automatic inspections feature not fully implemented in Drizzle version', createdCount,
createdCount: 0, created: createdCount,
created: 0, sites: createdInspections,
sites: [],
}; };
} }
// Additional methods needed by controllers and other services // Additional methods needed by controllers and other services
async getResponsesByInspectionId(inspectionId: number) { async getResponsesByInspectionId(inspectionId: number) {
const responses = await this.databaseService.db // First check if the inspection exists
const inspection = await this.databaseService.db
.select() .select()
.from(inspections)
.where(eq(inspections.id, inspectionId))
.limit(1);
if (inspection.length === 0) {
throw new NotFoundException(
`Inspection with ID ${inspectionId} not found`,
);
}
const responses = await this.databaseService.db
.select({
id: inspectionResponses.id,
response: inspectionResponses.response,
comment: inspectionResponses.comment,
question: {
id: inspectionQuestions.id,
question: inspectionQuestions.question,
orderIndex: inspectionQuestions.orderIndex,
},
})
.from(inspectionResponses) .from(inspectionResponses)
.leftJoin(
inspectionQuestions,
eq(inspectionResponses.questionId, inspectionQuestions.id),
)
.where(eq(inspectionResponses.inspectionId, inspectionId)); .where(eq(inspectionResponses.inspectionId, inspectionId));
return responses; return responses;
} }
async addResponsesToInspection( async updateResponsesToInspection(
inspectionId: number, inspectionId: number,
responses: any[], responses: CreateInspectionResponseDto[],
userId: number, userId: number,
) { ) {
// Simplified implementation // Check if inspection exists
if (responses && responses.length > 0) { const inspectionList = await this.databaseService.db
await this.databaseService.db.insert(inspectionResponses).values( .select()
responses.map((response) => ({ .from(inspections)
response: response.response as any, .where(eq(inspections.id, inspectionId))
comment: response.comment, .limit(1);
questionId: response.questionId,
inspectionId: inspectionId, if (inspectionList.length === 0) {
})), throw new NotFoundException(
`Inspection with ID ${inspectionId} not found`,
); );
} }
if (responses && responses.length > 0) {
// Process each response with upsert logic
for (const response of responses) {
// Validate that the response is a valid enum value
if (
!Object.values(InspectionResponseOption).includes(response.response)
) {
throw new Error(
`Invalid response value: ${response.response}. Must be one of: ${Object.values(InspectionResponseOption).join(', ')}`,
);
}
// Check if response already exists for this question and inspection
const existingResponse = await this.databaseService.db
.select()
.from(inspectionResponses)
.where(
and(
eq(inspectionResponses.inspectionId, inspectionId),
eq(inspectionResponses.questionId, response.questionId),
),
)
.limit(1);
if (existingResponse.length > 0) {
// Update existing response
await this.databaseService.db
.update(inspectionResponses)
.set({
response: response.response,
comment: response.comment || null,
updatedAt: new Date(),
})
.where(eq(inspectionResponses.id, existingResponse[0].id));
} else {
// Create new response
await this.databaseService.db.insert(inspectionResponses).values({
response: response.response,
comment: response.comment || null,
questionId: response.questionId,
inspectionId: inspectionId,
createdAt: new Date(),
updatedAt: new Date(),
});
}
}
}
return this.findInspectionById(inspectionId); return this.findInspectionById(inspectionId);
} }
async checkSitesNeedingInspections() { async checkSitesNeedingInspections() {
// Simplified implementation - TODO: Add proper logic const now = new Date();
// Return an array-like object to satisfy the scheduler service const threeMonthsFromNow = new Date(now);
const result = [] as any; threeMonthsFromNow.setMonth(threeMonthsFromNow.getMonth() + 3);
result.message =
'Sites checking feature not fully implemented in Drizzle version'; // Find inspections that have deadlines within the next 3 months
result.sites = []; const inspectionsNeedingScheduling = await this.databaseService.db
return result; .select({
inspection: {
id: inspections.id,
deadline: inspections.deadline,
siteId: inspections.siteId,
},
site: {
id: sites.id,
siteCode: sites.siteCode,
siteName: sites.siteName,
},
})
.from(inspections)
.leftJoin(sites, eq(inspections.siteId, sites.id))
.where(
and(
eq(inspections.status, 'COMPLETED'), // Only completed inspections
ne(inspections.deadline, null as any), // Must have a deadline
),
);
const sitesNeedingInspections: any[] = [];
for (const inspectionData of inspectionsNeedingScheduling) {
if (!inspectionData.inspection.deadline) continue;
const deadlineDate = new Date(inspectionData.inspection.deadline);
// Check if we're within 3 months of the deadline
const timeDiff = deadlineDate.getTime() - now.getTime();
const monthsDiff = timeDiff / (1000 * 3600 * 24 * 30); // Approximate months
if (monthsDiff <= 3 && monthsDiff > 0) {
// Calculate next deadline (1 year after current deadline)
const nextDeadline = new Date(deadlineDate);
nextDeadline.setFullYear(nextDeadline.getFullYear() + 1);
sitesNeedingInspections.push({
siteId: inspectionData.site?.id || 0,
siteCode: inspectionData.site?.siteCode || 'Unknown',
siteName: inspectionData.site?.siteName || 'Unknown Site',
lastInspectionDate: inspectionData.inspection.deadline,
nextInspectionDate: nextDeadline,
});
}
}
return sitesNeedingInspections;
} }
async redoRejectedInspection( async redoRejectedInspection(
...@@ -243,12 +613,171 @@ export class InspectionService { ...@@ -243,12 +613,171 @@ export class InspectionService {
id: number, id: number,
finalComment: string | undefined, finalComment: string | undefined,
userId: number, userId: number,
userRole: string,
) {
// Get the current user to check their role and partner
const userList = await this.databaseService.db
.select()
.from(users)
.where(eq(users.id, userId))
.limit(1);
if (userList.length === 0) {
throw new NotFoundException(`User with ID ${userId} not found`);
}
const currentUser = userList[0];
// Get the inspection to check site and partner access
const inspectionList = await this.databaseService.db
.select()
.from(inspections)
.where(eq(inspections.id, id))
.limit(1);
if (inspectionList.length === 0) {
throw new NotFoundException(`Inspection with ID ${id} not found`);
}
const inspection = inspectionList[0];
// Check if user has access to this inspection's site
if (currentUser.role === 'PARTNER' || currentUser.role === 'OPERATOR') {
if (!currentUser.partnerId) {
throw new ForbiddenException(
'User does not have access to any partner resources',
);
}
// Check if the partner has access to this site
const partnerSiteList = await this.databaseService.db
.select()
.from(partnerSites)
.where(
and(
eq(partnerSites.partnerId, currentUser.partnerId),
eq(partnerSites.siteId, inspection.siteId),
),
)
.limit(1);
if (partnerSiteList.length === 0) {
throw new ForbiddenException(
'Access to this inspection is not authorized',
);
}
}
let updateData: any = {
finalComment,
updatedById: userId,
};
// Set finalCommentStatus based on user role
if (userRole === 'OPERATOR') {
// Operators can create/update final comments, but they need validation
updateData.finalCommentStatus = 'PENDING';
} else if (userRole === 'PARTNER') {
// Partners can validate final comments
updateData.finalCommentStatus = 'VALIDATED';
}
// Other roles (ADMIN, SUPERADMIN, MANAGER) can see validated comments but don't change status
const [updatedInspection] = await this.databaseService.db
.update(inspections)
.set(updateData)
.where(eq(inspections.id, id))
.returning();
if (!updatedInspection) {
throw new NotFoundException(`Inspection with ID ${id} not found`);
}
return this.mapToInspectionDto(updatedInspection);
}
async validateFinalComment(
id: number,
action: 'validate' | 'reject',
userId: number,
comment?: string,
) { ) {
// Get the current user to check their role and partner
const userList = await this.databaseService.db
.select()
.from(users)
.where(eq(users.id, userId))
.limit(1);
if (userList.length === 0) {
throw new NotFoundException(`User with ID ${userId} not found`);
}
const currentUser = userList[0];
if (currentUser.role !== 'PARTNER') {
throw new ForbiddenException(
'Only PARTNER role can validate final comments',
);
}
// Get the inspection to check site and partner access
const inspectionList = await this.databaseService.db
.select()
.from(inspections)
.where(eq(inspections.id, id))
.limit(1);
if (inspectionList.length === 0) {
throw new NotFoundException(`Inspection with ID ${id} not found`);
}
const inspection = inspectionList[0];
// Check if the partner has access to this site
if (!currentUser.partnerId) {
throw new ForbiddenException(
'User does not have access to any partner resources',
);
}
const partnerSiteList = await this.databaseService.db
.select()
.from(partnerSites)
.where(
and(
eq(partnerSites.partnerId, currentUser.partnerId),
eq(partnerSites.siteId, inspection.siteId),
),
)
.limit(1);
if (partnerSiteList.length === 0) {
throw new ForbiddenException(
'Access to this inspection is not authorized',
);
}
// Check if the final comment is in PENDING status
if (inspection.finalCommentStatus !== 'PENDING') {
throw new ForbiddenException(
'Final comment is not in PENDING status for validation',
);
}
const newStatus = action === 'validate' ? 'VALIDATED' : 'REJECTED';
const [updatedInspection] = await this.databaseService.db const [updatedInspection] = await this.databaseService.db
.update(inspections) .update(inspections)
.set({ .set({
finalComment, finalCommentStatus: newStatus,
updatedById: userId, updatedById: userId,
// If rejecting, we could add the rejection comment to the finalComment field
// or create a separate field for rejection comments
...(action === 'reject' &&
comment && {
finalComment: `${inspection.finalComment}\n\n[REJECTED by Partner Manager: ${comment}]`,
}),
}) })
.where(eq(inspections.id, id)) .where(eq(inspections.id, id))
.returning(); .returning();
...@@ -260,18 +789,34 @@ export class InspectionService { ...@@ -260,18 +789,34 @@ export class InspectionService {
return this.mapToInspectionDto(updatedInspection); return this.mapToInspectionDto(updatedInspection);
} }
private mapToInspectionDto(inspection: any): InspectionDto { private mapToInspectionDto(
inspection: any,
responses: any[] = [],
photos: any[] = [],
): InspectionDto {
return { return {
id: inspection.id, id: inspection.id,
date: inspection.date, date: inspection.date,
deadline: inspection.deadline,
comment: inspection.comment, comment: inspection.comment,
finalComment: inspection.finalComment, finalComment: inspection.finalComment,
finalCommentStatus: inspection.finalCommentStatus,
siteId: inspection.siteId, siteId: inspection.siteId,
status: inspection.status, status: inspection.status,
createdAt: inspection.createdAt, createdAt: inspection.createdAt,
updatedAt: inspection.updatedAt, updatedAt: inspection.updatedAt,
responses: [], // TODO: Load responses if needed responses: responses.map((response) => ({
photos: [], // TODO: Load photos if needed id: response.id,
response: response.response,
comment: response.comment,
question: response.question,
})),
photos: photos.map((photo) => ({
id: photo.id,
url: photo.url,
filename: photo.filename,
description: photo.description,
})),
}; };
} }
} }
...@@ -19,33 +19,91 @@ export async function saveInspectionPhotos( ...@@ -19,33 +19,91 @@ export async function saveInspectionPhotos(
return []; return [];
} }
const uploadDir = // Validate inspectionId
process.env.NODE_ENV === 'production' if (!inspectionId || inspectionId <= 0) {
? `/home/api-cellnex/public_html/uploads/inspection/${inspectionId}` throw new Error('Invalid inspection ID provided');
: path.join( }
process.cwd(),
'uploads', // Use environment variable for upload path, with fallback to default
'inspection', const baseUploadPath =
inspectionId.toString(), process.env.UPLOAD_BASE_PATH ||
); (process.env.NODE_ENV === 'production'
? '/home/api-verticalflow/public_html/uploads'
: path.join(process.cwd(), 'uploads'));
const uploadDir = path.join(
baseUploadPath,
'inspection',
inspectionId.toString(),
);
// Create directory if it doesn't exist // Create directory if it doesn't exist
let finalUploadDir = uploadDir;
try { try {
// First, try to create the base uploads directory if it doesn't exist
await mkdir(baseUploadPath, { recursive: true });
// Then create the inspection subdirectory
await mkdir(uploadDir, { recursive: true }); await mkdir(uploadDir, { recursive: true });
} catch (error) { } catch (error) {
console.error(`Error creating directory ${uploadDir}:`, error); console.error(`Error creating directory ${uploadDir}:`, error);
throw new Error(`Failed to create upload directory: ${error.message}`);
// If permission denied, try using a fallback directory
if (error.code === 'EACCES') {
console.warn(
`Permission denied for ${uploadDir}, trying fallback directory`,
);
// Try using /tmp as fallback
const fallbackDir = path.join(
'/tmp',
'uploads',
'inspection',
inspectionId.toString(),
);
try {
await mkdir(fallbackDir, { recursive: true });
finalUploadDir = fallbackDir;
console.log(`Using fallback directory: ${finalUploadDir}`);
} catch (fallbackError) {
console.error(
`Fallback directory creation also failed:`,
fallbackError,
);
throw new Error(
`Permission denied: Unable to create upload directory. Please check directory permissions for ${uploadDir}. Fallback also failed: ${fallbackError.message}`,
);
}
} else if (error.code === 'ENOENT') {
throw new Error(
`Parent directory does not exist: Unable to create upload directory at ${uploadDir}`,
);
} else {
throw new Error(`Failed to create upload directory: ${error.message}`);
}
} }
// Save files // Save files
const savedPaths: string[] = []; const savedPaths: string[] = [];
const savePromises = files.map(async (file) => { const savePromises = files.map(async (file) => {
const filename = file.originalname; if (!file || !file.buffer) {
const filePath = path.join(uploadDir, filename); throw new Error('Invalid file provided - missing buffer');
}
const filename = file.originalname || `photo_${Date.now()}.jpg`;
const filePath = path.join(finalUploadDir, filename);
try { try {
await writeFile(filePath, file.buffer); await writeFile(filePath, file.buffer);
savedPaths.push(`/uploads/inspection/${inspectionId}/${filename}`);
// Generate the URL path based on the actual directory used
if (finalUploadDir.startsWith('/tmp')) {
// For fallback directory, use a different URL pattern
savedPaths.push(`/tmp-uploads/inspection/${inspectionId}/${filename}`);
} else {
savedPaths.push(`/uploads/inspection/${inspectionId}/${filename}`);
}
} catch (error) { } catch (error) {
console.error(`Error saving file ${filename}:`, error); console.error(`Error saving file ${filename}:`, error);
throw new Error(`Failed to save file ${filename}: ${error.message}`); throw new Error(`Failed to save file ${filename}: ${error.message}`);
......
...@@ -167,4 +167,118 @@ export class PartnersController { ...@@ -167,4 +167,118 @@ export class PartnersController {
getPartnerCandidates(@Param('id', ParseIntPipe) id: number) { getPartnerCandidates(@Param('id', ParseIntPipe) id: number) {
return this.partnersService.getPartnerCandidates(id); return this.partnersService.getPartnerCandidates(id);
} }
@Patch(':partnerId/users/:userId/promote')
@Roles(Role.ADMIN, Role.SUPERADMIN)
@ApiOperation({ summary: 'Promote a user to partner manager (PARTNER role)' })
@ApiParam({ name: 'partnerId', description: 'Partner ID', type: 'number' })
@ApiParam({ name: 'userId', description: 'User ID', type: 'number' })
@ApiResponse({
status: 200,
description: 'The user has been successfully promoted to partner manager.',
})
@ApiResponse({ status: 404, description: 'Partner or user not found.' })
promoteUserToPartnerManager(
@Param('partnerId', ParseIntPipe) partnerId: number,
@Param('userId', ParseIntPipe) userId: number,
) {
return this.partnersService.promoteUserToPartnerManager(partnerId, userId);
}
@Patch(':partnerId/users/:userId/demote')
@Roles(Role.ADMIN, Role.SUPERADMIN)
@ApiOperation({ summary: 'Demote a user to operator (OPERATOR role)' })
@ApiParam({ name: 'partnerId', description: 'Partner ID', type: 'number' })
@ApiParam({ name: 'userId', description: 'User ID', type: 'number' })
@ApiResponse({
status: 200,
description: 'The user has been successfully demoted to operator.',
})
@ApiResponse({ status: 404, description: 'Partner or user not found.' })
demoteUserToOperator(
@Param('partnerId', ParseIntPipe) partnerId: number,
@Param('userId', ParseIntPipe) userId: number,
) {
return this.partnersService.demoteUserToOperator(partnerId, userId);
}
@Get(':id/sites')
@Roles(Role.ADMIN, Role.SUPERADMIN, Role.MANAGER, Role.PARTNER)
@UseGuards(PartnerAuthGuard)
@ApiOperation({ summary: 'Get all sites associated with a partner' })
@ApiParam({ name: 'id', description: 'Partner ID', type: 'number' })
@ApiResponse({
status: 200,
description: 'Return all sites associated with the partner.',
})
@ApiResponse({ status: 404, description: 'Partner not found.' })
getPartnerSites(@Param('id', ParseIntPipe) id: number) {
return this.partnersService.getPartnerSites(id);
}
@Post(':partnerId/sites/:siteId')
@Roles(Role.ADMIN, Role.SUPERADMIN)
@ApiOperation({ summary: 'Associate a site with a partner' })
@ApiParam({ name: 'partnerId', description: 'Partner ID', type: 'number' })
@ApiParam({ name: 'siteId', description: 'Site ID', type: 'number' })
@ApiResponse({
status: 201,
description: 'The site has been successfully associated with the partner.',
})
@ApiResponse({ status: 404, description: 'Partner or site not found.' })
@ApiResponse({
status: 409,
description: 'Site is already associated with this partner.',
})
addSiteToPartner(
@Param('partnerId', ParseIntPipe) partnerId: number,
@Param('siteId', ParseIntPipe) siteId: number,
) {
return this.partnersService.addSiteToPartner(partnerId, siteId);
}
@Delete(':partnerId/sites/:siteId')
@Roles(Role.ADMIN, Role.SUPERADMIN)
@ApiOperation({ summary: 'Remove a site from a partner' })
@ApiParam({ name: 'partnerId', description: 'Partner ID', type: 'number' })
@ApiParam({ name: 'siteId', description: 'Site ID', type: 'number' })
@ApiResponse({
status: 200,
description: 'The site has been successfully removed from the partner.',
})
@ApiResponse({
status: 404,
description: 'Partner, site, or association not found.',
})
removeSiteFromPartner(
@Param('partnerId', ParseIntPipe) partnerId: number,
@Param('siteId', ParseIntPipe) siteId: number,
) {
return this.partnersService.removeSiteFromPartner(partnerId, siteId);
}
@Patch('sites/:siteId/switch-to/:newPartnerId')
@Roles(Role.ADMIN, Role.SUPERADMIN)
@ApiOperation({ summary: 'Switch a site to a different partner' })
@ApiParam({ name: 'siteId', description: 'Site ID', type: 'number' })
@ApiParam({
name: 'newPartnerId',
description: 'New Partner ID',
type: 'number',
})
@ApiResponse({
status: 200,
description: 'The site has been successfully switched to the new partner.',
})
@ApiResponse({ status: 404, description: 'Site or new partner not found.' })
@ApiResponse({
status: 409,
description: 'Site is already associated with the new partner.',
})
switchPartnerForSite(
@Param('siteId', ParseIntPipe) siteId: number,
@Param('newPartnerId', ParseIntPipe) newPartnerId: number,
) {
return this.partnersService.switchPartnerForSite(siteId, newPartnerId);
}
} }
...@@ -13,9 +13,14 @@ export class PartnersService { ...@@ -13,9 +13,14 @@ export class PartnersService {
constructor(private databaseService: DatabaseService) {} constructor(private databaseService: DatabaseService) {}
async create(createPartnerDto: CreatePartnerDto) { async create(createPartnerDto: CreatePartnerDto) {
const now = new Date();
const [partner] = await this.databaseService.db const [partner] = await this.databaseService.db
.insert(partners) .insert(partners)
.values(createPartnerDto) .values({
...createPartnerDto,
createdAt: now,
updatedAt: now,
})
.returning(); .returning();
return partner; return partner;
...@@ -143,7 +148,7 @@ export class PartnersService { ...@@ -143,7 +148,7 @@ export class PartnersService {
.update(users) .update(users)
.set({ .set({
partnerId, partnerId,
role: 'PARTNER', // Set role to PARTNER automatically role: 'OPERATOR', // Set role to OPERATOR by default (normal partner user)
}) })
.where(eq(users.id, userId)) .where(eq(users.id, userId))
.returning(); .returning();
...@@ -249,11 +254,14 @@ export class PartnersService { ...@@ -249,11 +254,14 @@ export class PartnersService {
); );
} }
const now = new Date();
const [partnerSite] = await this.databaseService.db const [partnerSite] = await this.databaseService.db
.insert(partnerSites) .insert(partnerSites)
.values({ .values({
partnerId, partnerId,
siteId, siteId,
createdAt: now,
updatedAt: now,
}) })
.returning(); .returning();
...@@ -304,8 +312,130 @@ export class PartnersService { ...@@ -304,8 +312,130 @@ export class PartnersService {
return { message: 'Site removed from partner successfully' }; return { message: 'Site removed from partner successfully' };
} }
async promoteUserToPartnerManager(partnerId: number, userId: number) {
// Check if both partner and user exist
const partner = await this.findOne(partnerId);
const userList = await this.databaseService.db
.select()
.from(users)
.where(eq(users.id, userId))
.limit(1);
if (userList.length === 0) {
throw new NotFoundException(`User with ID ${userId} not found`);
}
const user = userList[0];
if (user.partnerId !== partnerId) {
throw new NotFoundException(
`User with ID ${userId} is not associated with Partner ID ${partnerId}`,
);
}
const [updatedUser] = await this.databaseService.db
.update(users)
.set({
role: 'PARTNER', // Promote to PARTNER role (owner/manager)
})
.where(eq(users.id, userId))
.returning();
return updatedUser;
}
async demoteUserToOperator(partnerId: number, userId: number) {
// Check if both partner and user exist
const partner = await this.findOne(partnerId);
const userList = await this.databaseService.db
.select()
.from(users)
.where(eq(users.id, userId))
.limit(1);
if (userList.length === 0) {
throw new NotFoundException(`User with ID ${userId} not found`);
}
const user = userList[0];
if (user.partnerId !== partnerId) {
throw new NotFoundException(
`User with ID ${userId} is not associated with Partner ID ${partnerId}`,
);
}
const [updatedUser] = await this.databaseService.db
.update(users)
.set({
role: 'OPERATOR', // Demote to OPERATOR role (normal user)
})
.where(eq(users.id, userId))
.returning();
return updatedUser;
}
// Legacy method name for backward compatibility // Legacy method name for backward compatibility
async getPartnerCandidates(partnerId: number) { async getPartnerCandidates(partnerId: number) {
return this.getPartnerSites(partnerId); return this.getPartnerSites(partnerId);
} }
async switchPartnerForSite(siteId: number, newPartnerId: number) {
// Check if the new partner exists
await this.findOne(newPartnerId);
// Check if the site exists
const siteList = await this.databaseService.db
.select()
.from(sites)
.where(eq(sites.id, siteId))
.limit(1);
if (siteList.length === 0) {
throw new NotFoundException(`Site with ID ${siteId} not found`);
}
// Check if the site is already associated with the new partner
const existingAssociation = await this.databaseService.db
.select()
.from(partnerSites)
.where(
and(
eq(partnerSites.partnerId, newPartnerId),
eq(partnerSites.siteId, siteId),
),
)
.limit(1);
if (existingAssociation.length > 0) {
throw new ConflictException(
`Site with ID ${siteId} is already associated with Partner ID ${newPartnerId}`,
);
}
// Remove any existing partner association for this site
await this.databaseService.db
.delete(partnerSites)
.where(eq(partnerSites.siteId, siteId));
// Add the new partner association
const now = new Date();
const [newPartnerSite] = await this.databaseService.db
.insert(partnerSites)
.values({
partnerId: newPartnerId,
siteId,
createdAt: now,
updatedAt: now,
})
.returning();
return {
message: `Site ${siteId} has been successfully switched to Partner ${newPartnerId}`,
partnerSite: newPartnerSite,
};
}
} }
...@@ -34,11 +34,14 @@ export class QuestionsService { ...@@ -34,11 +34,14 @@ export class QuestionsService {
); );
} }
const now = new Date();
const [question] = await this.databaseService.db const [question] = await this.databaseService.db
.insert(inspectionQuestions) .insert(inspectionQuestions)
.values({ .values({
question: dto.question, question: dto.question,
orderIndex: dto.orderIndex, orderIndex: dto.orderIndex,
createdAt: now,
updatedAt: now,
}) })
.returning(); .returning();
......
...@@ -8,7 +8,7 @@ import { ...@@ -8,7 +8,7 @@ import {
IsBoolean, IsBoolean,
} from 'class-validator'; } from 'class-validator';
import { Transform, Type } from 'class-transformer'; import { Transform, Type } from 'class-transformer';
import { InspectionStatus } from '@prisma/client'; import { inspectionStatusEnum } from '../../../database/schema';
export enum OrderDirection { export enum OrderDirection {
ASC = 'asc', ASC = 'asc',
...@@ -82,10 +82,10 @@ export class FindSitesDto { ...@@ -82,10 +82,10 @@ export class FindSitesDto {
@ApiProperty({ @ApiProperty({
required: false, required: false,
enum: InspectionStatus, enum: inspectionStatusEnum.enumValues,
description: 'Filter sites by their latest inspection status', description: 'Filter sites by their latest inspection status',
}) })
@IsOptional() @IsOptional()
@IsEnum(InspectionStatus) @IsEnum(inspectionStatusEnum.enumValues)
inspectionStatus?: InspectionStatus; inspectionStatus?: (typeof inspectionStatusEnum.enumValues)[number];
} }
...@@ -100,6 +100,7 @@ export class SitesService { ...@@ -100,6 +100,7 @@ export class SitesService {
async create(createSiteDto: CreateSiteDto, userId: number) { async create(createSiteDto: CreateSiteDto, userId: number) {
try { try {
// First create the site // First create the site
const now = new Date();
const [newSite] = await this.databaseService.db const [newSite] = await this.databaseService.db
.insert(sites) .insert(sites)
.values({ .values({
...@@ -109,6 +110,8 @@ export class SitesService { ...@@ -109,6 +110,8 @@ export class SitesService {
longitude: createSiteDto.longitude, longitude: createSiteDto.longitude,
createdById: userId, createdById: userId,
updatedById: userId, updatedById: userId,
createdAt: now,
updatedAt: now,
}) })
.returning(); .returning();
...@@ -131,34 +134,69 @@ export class SitesService { ...@@ -131,34 +134,69 @@ export class SitesService {
} }
async findAll(findSitesDto: FindSitesDto, partnerId?: number | null) { async findAll(findSitesDto: FindSitesDto, partnerId?: number | null) {
// Simplified version - just return basic sites
const page = findSitesDto?.page || 1; const page = findSitesDto?.page || 1;
const limit = findSitesDto?.limit || 10; const limit = findSitesDto?.limit || 10;
const offset = (page - 1) * limit; const offset = (page - 1) * limit;
// Build where conditions // Build where conditions for sites
const conditions: SQL[] = []; const siteConditions: SQL[] = [];
if (findSitesDto?.siteCode) { if (findSitesDto?.siteCode) {
conditions.push(ilike(sites.siteCode, `%${findSitesDto.siteCode}%`)); siteConditions.push(ilike(sites.siteCode, `%${findSitesDto.siteCode}%`));
} }
if (findSitesDto?.siteName) { if (findSitesDto?.siteName) {
conditions.push(ilike(sites.siteName, `%${findSitesDto.siteName}%`)); siteConditions.push(ilike(sites.siteName, `%${findSitesDto.siteName}%`));
} }
const whereCondition = const siteWhereCondition =
conditions.length > 0 ? and(...conditions) : undefined; siteConditions.length > 0 ? and(...siteConditions) : undefined;
// Get total count
const [{ totalCount }] = await this.databaseService.db const [{ totalCount }] = await this.databaseService.db
.select({ totalCount: count() }) .select({ totalCount: count() })
.from(sites) .from(sites)
.where(whereCondition); .where(siteWhereCondition);
// Build inspection status filter condition
const inspectionConditions: SQL[] = [];
if (findSitesDto?.inspectionStatus) {
inspectionConditions.push(
eq(inspections.status, findSitesDto.inspectionStatus as any),
);
}
// Get sites with latest inspection data
const sitesList = await this.databaseService.db const sitesList = await this.databaseService.db
.select() .select({
id: sites.id,
siteCode: sites.siteCode,
siteName: sites.siteName,
latitude: sites.latitude,
longitude: sites.longitude,
createdAt: sites.createdAt,
updatedAt: sites.updatedAt,
createdById: sites.createdById,
updatedById: sites.updatedById,
latestInspectionStatus: inspections.status,
latestInspectionDate: inspections.date,
})
.from(sites) .from(sites)
.where(whereCondition) .leftJoin(
inspections,
and(
eq(inspections.siteId, sites.id),
// Get the latest inspection for each site
sql`${inspections.id} = (
SELECT MAX(id)
FROM "Inspection"
WHERE "siteId" = ${sites.id}
)`,
// Apply inspection status filter if provided
...inspectionConditions,
),
)
.where(siteWhereCondition)
.limit(limit) .limit(limit)
.offset(offset); .offset(offset);
...@@ -166,8 +204,10 @@ export class SitesService { ...@@ -166,8 +204,10 @@ export class SitesService {
data: sitesList.map((site) => ({ data: sitesList.map((site) => ({
...site, ...site,
highestCandidateStatus: null, highestCandidateStatus: null,
latestInspectionStatus: null, latestInspectionStatus: site.latestInspectionStatus,
nextInspectionDueDate: null, nextInspectionDueDate: this.calculateNextInspectionDueDate(
site.latestInspectionDate,
),
_count: { candidates: 0 }, _count: { candidates: 0 },
})), })),
meta: { meta: {
...@@ -181,8 +221,32 @@ export class SitesService { ...@@ -181,8 +221,32 @@ export class SitesService {
async findOne(id: number) { async findOne(id: number) {
const sitesList = await this.databaseService.db const sitesList = await this.databaseService.db
.select() .select({
id: sites.id,
siteCode: sites.siteCode,
siteName: sites.siteName,
latitude: sites.latitude,
longitude: sites.longitude,
createdAt: sites.createdAt,
updatedAt: sites.updatedAt,
createdById: sites.createdById,
updatedById: sites.updatedById,
latestInspectionStatus: inspections.status,
latestInspectionDate: inspections.date,
})
.from(sites) .from(sites)
.leftJoin(
inspections,
and(
eq(inspections.siteId, sites.id),
// Get the latest inspection for this site
sql`${inspections.id} = (
SELECT MAX(id)
FROM "Inspection"
WHERE "siteId" = ${sites.id}
)`,
),
)
.where(eq(sites.id, id)) .where(eq(sites.id, id))
.limit(1); .limit(1);
...@@ -190,11 +254,15 @@ export class SitesService { ...@@ -190,11 +254,15 @@ export class SitesService {
throw new NotFoundException(`Site with ID ${id} not found`); throw new NotFoundException(`Site with ID ${id} not found`);
} }
const site = sitesList[0];
return { return {
...sitesList[0], ...site,
highestCandidateStatus: null, highestCandidateStatus: null,
latestInspectionStatus: null, latestInspectionStatus: site.latestInspectionStatus,
nextInspectionDueDate: null, nextInspectionDueDate: this.calculateNextInspectionDueDate(
site.latestInspectionDate,
),
_count: { candidates: 0 }, _count: { candidates: 0 },
}; };
} }
...@@ -204,12 +272,10 @@ export class SitesService { ...@@ -204,12 +272,10 @@ export class SitesService {
throw new ForbiddenException('Partner access required'); throw new ForbiddenException('Partner access required');
} }
// For now, same as findOne - TODO: Add partner filtering
return this.findOne(id); return this.findOne(id);
} }
async findOneWithCandidates(id: number, partnerId?: number | null) { async findOneWithCandidates(id: number, partnerId?: number | null) {
// For now, same as findOne - TODO: Add candidates
const site = await this.findOne(id); const site = await this.findOne(id);
return { return {
...site, ...site,
...@@ -278,8 +344,32 @@ export class SitesService { ...@@ -278,8 +344,32 @@ export class SitesService {
async findByCode(siteCode: string) { async findByCode(siteCode: string) {
const siteList = await this.databaseService.db const siteList = await this.databaseService.db
.select() .select({
id: sites.id,
siteCode: sites.siteCode,
siteName: sites.siteName,
latitude: sites.latitude,
longitude: sites.longitude,
createdAt: sites.createdAt,
updatedAt: sites.updatedAt,
createdById: sites.createdById,
updatedById: sites.updatedById,
latestInspectionStatus: inspections.status,
latestInspectionDate: inspections.date,
})
.from(sites) .from(sites)
.leftJoin(
inspections,
and(
eq(inspections.siteId, sites.id),
// Get the latest inspection for this site
sql`${inspections.id} = (
SELECT MAX(id)
FROM "Inspection"
WHERE "siteId" = ${sites.id}
)`,
),
)
.where(eq(sites.siteCode, siteCode)) .where(eq(sites.siteCode, siteCode))
.limit(1); .limit(1);
...@@ -289,13 +379,13 @@ export class SitesService { ...@@ -289,13 +379,13 @@ export class SitesService {
const site = siteList[0]; const site = siteList[0];
// For now, return a simplified version
// TODO: Add candidate and inspection relations when needed
return { return {
...site, ...site,
highestCandidateStatus: null, highestCandidateStatus: null,
latestInspectionStatus: null, latestInspectionStatus: site.latestInspectionStatus,
nextInspectionDueDate: null, nextInspectionDueDate: this.calculateNextInspectionDueDate(
site.latestInspectionDate,
),
_count: { candidates: 0 }, _count: { candidates: 0 },
}; };
} }
...@@ -322,13 +412,31 @@ export class SitesService { ...@@ -322,13 +412,31 @@ export class SitesService {
siteName: sites.siteName, siteName: sites.siteName,
latitude: sites.latitude, latitude: sites.latitude,
longitude: sites.longitude, longitude: sites.longitude,
latestInspectionStatus: inspections.status,
latestInspectionDate: inspections.date,
}) })
.from(sites) .from(sites)
.leftJoin(
inspections,
and(
eq(inspections.siteId, sites.id),
// Get the latest inspection for each site
sql`${inspections.id} = (
SELECT MAX(id)
FROM "Inspection"
WHERE "siteId" = ${sites.id}
)`,
),
)
.where(whereCondition); .where(whereCondition);
return sitesList.map((site) => ({ return sitesList.map((site) => ({
...site, ...site,
highestCandidateStatus: null, highestCandidateStatus: null,
latestInspectionStatus: site.latestInspectionStatus,
nextInspectionDueDate: this.calculateNextInspectionDueDate(
site.latestInspectionDate,
),
})); }));
} }
} }
...@@ -65,6 +65,7 @@ export class TelecommunicationStationIdentificationService { ...@@ -65,6 +65,7 @@ export class TelecommunicationStationIdentificationService {
); );
} }
const now = new Date();
const [identification] = await this.databaseService.db const [identification] = await this.databaseService.db
.insert(telecommunicationStationIdentifications) .insert(telecommunicationStationIdentifications)
.values({ .values({
...@@ -73,6 +74,8 @@ export class TelecommunicationStationIdentificationService { ...@@ -73,6 +74,8 @@ export class TelecommunicationStationIdentificationService {
serialNumber: dto.serialNumber, serialNumber: dto.serialNumber,
isFirstCertification: dto.isFirstCertification, isFirstCertification: dto.isFirstCertification,
modelReference: dto.modelReference, modelReference: dto.modelReference,
createdAt: now,
updatedAt: now,
}) })
.returning(); .returning();
......
...@@ -6,7 +6,7 @@ import { ...@@ -6,7 +6,7 @@ import {
IsString, IsString,
MinLength, MinLength,
} from 'class-validator'; } from 'class-validator';
import { Role } from '@prisma/client'; import { roleEnum } from '../../../database/schema';
export class CreateUserDto { export class CreateUserDto {
@ApiProperty({ @ApiProperty({
...@@ -37,10 +37,10 @@ export class CreateUserDto { ...@@ -37,10 +37,10 @@ export class CreateUserDto {
@ApiProperty({ @ApiProperty({
description: 'The role of the user', description: 'The role of the user',
enum: Role, enum: roleEnum.enumValues,
default: Role.VIEWER, default: 'VIEWER',
example: Role.VIEWER, example: 'VIEWER',
}) })
@IsEnum(Role) @IsEnum(roleEnum.enumValues)
role: Role = Role.VIEWER; role: (typeof roleEnum.enumValues)[number] = 'VIEWER';
} }
...@@ -29,12 +29,15 @@ export class UsersService { ...@@ -29,12 +29,15 @@ export class UsersService {
async create(createUserDto: CreateUserDto) { async create(createUserDto: CreateUserDto) {
const hashedPassword = await bcrypt.hash(createUserDto.password, 10); const hashedPassword = await bcrypt.hash(createUserDto.password, 10);
const now = new Date();
const [newUser] = await this.databaseService.db const [newUser] = await this.databaseService.db
.insert(users) .insert(users)
.values({ .values({
...createUserDto, ...createUserDto,
password: hashedPassword, password: hashedPassword,
isActive: false, // New users are inactive by default isActive: false, // New users are inactive by default
createdAt: now,
updatedAt: now,
}) })
.returning({ .returning({
id: users.id, id: users.id,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment