Commit 08db2e37 by Augusto Fonte

Inspection changes

parent c8c5d592
{ {
"name": "api-cellnex", "name": "api-cellnex",
"version": "0.0.1", "version": "0.0.2",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "api-cellnex", "name": "api-cellnex",
"version": "0.0.1", "version": "0.0.2",
"license": "UNLICENSED", "license": "UNLICENSED",
"dependencies": { "dependencies": {
"@nestjs-modules/mailer": "^2.0.2", "@nestjs-modules/mailer": "^2.0.2",
...@@ -17,14 +17,16 @@ ...@@ -17,14 +17,16 @@
"@nestjs/passport": "^11.0.5", "@nestjs/passport": "^11.0.5",
"@nestjs/platform-express": "^11.1.0", "@nestjs/platform-express": "^11.1.0",
"@nestjs/swagger": "^11.1.1", "@nestjs/swagger": "^11.1.1",
"@prisma/client": "^6.6.0", "@prisma/client": "^6.9.0",
"@types/nodemailer": "^6.4.17", "@types/nodemailer": "^6.4.17",
"@types/uuid": "^10.0.0", "@types/uuid": "^10.0.0",
"adm-zip": "^0.5.16",
"bcrypt": "^5.1.1", "bcrypt": "^5.1.1",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"class-validator": "^0.14.1", "class-validator": "^0.14.1",
"csv-parser": "^3.2.0", "csv-parser": "^3.2.0",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"fast-xml-parser": "^5.2.5",
"jimp": "^0.22.12", "jimp": "^0.22.12",
"multer": "^1.4.5-lts.2", "multer": "^1.4.5-lts.2",
"nodemailer": "^6.10.0", "nodemailer": "^6.10.0",
...@@ -4457,9 +4459,9 @@ ...@@ -4457,9 +4459,9 @@
} }
}, },
"node_modules/@prisma/client": { "node_modules/@prisma/client": {
"version": "6.6.0", "version": "6.9.0",
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.6.0.tgz", "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.9.0.tgz",
"integrity": "sha512-vfp73YT/BHsWWOAuthKQ/1lBgESSqYqAWZEYyTdGXyFAHpmewwWL2Iz6ErIzkj4aHbuc6/cGSsE6ZY+pBO04Cg==", "integrity": "sha512-Gg7j1hwy3SgF1KHrh0PZsYvAaykeR0PaxusnLXydehS96voYCGt1U5zVR31NIouYc63hWzidcrir1a7AIyCsNQ==",
"hasInstallScript": true, "hasInstallScript": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"engines": { "engines": {
...@@ -5983,6 +5985,15 @@ ...@@ -5983,6 +5985,15 @@
"node": ">=0.8" "node": ">=0.8"
} }
}, },
"node_modules/adm-zip": {
"version": "0.5.16",
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz",
"integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==",
"license": "MIT",
"engines": {
"node": ">=12.0"
}
},
"node_modules/agent-base": { "node_modules/agent-base": {
"version": "6.0.2", "version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
...@@ -8753,6 +8764,24 @@ ...@@ -8753,6 +8764,24 @@
], ],
"license": "BSD-3-Clause" "license": "BSD-3-Clause"
}, },
"node_modules/fast-xml-parser": {
"version": "5.2.5",
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz",
"integrity": "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/NaturalIntelligence"
}
],
"license": "MIT",
"dependencies": {
"strnum": "^2.1.0"
},
"bin": {
"fxparser": "src/cli/cli.js"
}
},
"node_modules/fastq": { "node_modules/fastq": {
"version": "1.19.1", "version": "1.19.1",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
...@@ -15245,6 +15274,18 @@ ...@@ -15245,6 +15274,18 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/strnum": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.1.tgz",
"integrity": "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/NaturalIntelligence"
}
],
"license": "MIT"
},
"node_modules/strtok3": { "node_modules/strtok3": {
"version": "9.1.1", "version": "9.1.1",
"resolved": "https://registry.npmjs.org/strtok3/-/strtok3-9.1.1.tgz", "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-9.1.1.tgz",
......
...@@ -29,14 +29,16 @@ ...@@ -29,14 +29,16 @@
"@nestjs/passport": "^11.0.5", "@nestjs/passport": "^11.0.5",
"@nestjs/platform-express": "^11.1.0", "@nestjs/platform-express": "^11.1.0",
"@nestjs/swagger": "^11.1.1", "@nestjs/swagger": "^11.1.1",
"@prisma/client": "^6.6.0", "@prisma/client": "^6.9.0",
"@types/nodemailer": "^6.4.17", "@types/nodemailer": "^6.4.17",
"@types/uuid": "^10.0.0", "@types/uuid": "^10.0.0",
"adm-zip": "^0.5.16",
"bcrypt": "^5.1.1", "bcrypt": "^5.1.1",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"class-validator": "^0.14.1", "class-validator": "^0.14.1",
"csv-parser": "^3.2.0", "csv-parser": "^3.2.0",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"fast-xml-parser": "^5.2.5",
"jimp": "^0.22.12", "jimp": "^0.22.12",
"multer": "^1.4.5-lts.2", "multer": "^1.4.5-lts.2",
"nodemailer": "^6.10.0", "nodemailer": "^6.10.0",
......
import fs from 'fs';
import AdmZip from 'adm-zip';
import { XMLParser } from 'fast-xml-parser';
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function importKmzToSites(kmzPath: string) {
// 1. Extract KML from KMZ
const zip = new AdmZip(kmzPath);
const kmlEntry = zip.getEntries().find((e) => e.entryName.endsWith('.kml'));
if (!kmlEntry) throw new Error('No KML file found in KMZ');
const kmlString = kmlEntry.getData().toString('utf8');
// 2. Parse KML
const parser = new XMLParser({ ignoreAttributes: false });
const kml = parser.parse(kmlString);
// 3. Extract Placemarks (handle both array and single object)
const placemarks = (() => {
const doc = kml.kml.Document;
if (Array.isArray(doc.Placemark)) return doc.Placemark;
if (doc.Placemark) return [doc.Placemark];
return [];
})();
for (const placemark of placemarks) {
const name = placemark.name || '';
const [longitude, latitude] = placemark.Point.coordinates
.split(',')
.map(Number);
// You can customize how you extract siteCode/siteName here
const siteCode = name;
const siteName = name;
// 4. Insert into Site model (skip if missing lat/lon)
if (latitude && longitude && siteCode) {
try {
await prisma.site.create({
data: {
siteCode,
siteName,
latitude,
longitude,
},
});
console.log(`Imported site: ${siteCode} (${latitude}, ${longitude})`);
} catch (err) {
console.error(`Failed to import site ${siteCode}:`, err.message);
}
}
}
console.log('Import complete!');
}
// Usage: npx ts-node prisma/import-sites-from-kmz.ts path/to/your/file.kmz
const kmzPath = process.argv[2];
if (!kmzPath) {
console.error(
'Usage: npx ts-node prisma/import-sites-from-kmz.ts <file.kmz>',
);
process.exit(1);
}
importKmzToSites(kmzPath)
.catch(console.error)
.finally(() => prisma.$disconnect());
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function main() {
console.log('Seeding maintenance questions...');
// Clear existing questions
await prisma.maintenanceQuestion.deleteMany({});
// Create questions
const questions = [
{
question: 'Site access condition',
orderIndex: 1,
},
{
question: 'Site infrastructure condition',
orderIndex: 2,
},
{
question: 'Equipment condition',
orderIndex: 3,
},
{
question: 'Power system condition',
orderIndex: 4,
},
{
question: 'Cooling system condition',
orderIndex: 5,
},
{
question: 'Security features condition',
orderIndex: 6,
},
{
question: 'Safety equipment presence and condition',
orderIndex: 7,
},
{
question: 'Site cleanliness',
orderIndex: 8,
},
{
question: 'Vegetation control',
orderIndex: 9,
},
{
question: 'Surrounding area condition',
orderIndex: 10,
},
];
for (const question of questions) {
await prisma.maintenanceQuestion.create({
data: question,
});
}
console.log('Maintenance questions have been seeded!');
}
main()
.catch((e) => {
console.error(e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});
\ No newline at end of file
...@@ -23,6 +23,103 @@ async function main() { ...@@ -23,6 +23,103 @@ async function main() {
}); });
console.log('Created/Updated superadmin user:', superadmin); console.log('Created/Updated superadmin user:', superadmin);
// Inspection questions to seed
const inspectionQuestions = [
{
question:
'Cada componente de carril-guia foi ficado pelo meus uma vez, conforme dispositivo na secção B',
},
{
question:
'As distâncias entre fixações são no máx 1,68cm (2,24m escadas gémeas ZAL, Conforme dispositivo na secção B',
},
{
question:
'Nas uniões, as juntas são todas inferiores a 5mm, conforme dispositivo na secção B',
},
{
question:
'As uniões roscadas entre edificação e os elementos de fixação correspondem ai dispositivo da secção B',
},
{
question:
'Os elementos de fixação estão corretamente montados e todas as uniões roscadas (incluindo as de fábrica) estão firmemente apertadas. (binários de aperto conforme secção B)',
},
{
question:
'Todas as uniões roscadas estão protegidas contra o desaperto, em conformidade com o dispositivo da secção E',
},
{
question:
"No início do percurrso de subida encontra-se montado um 'batente de bloqueio inferior' em conformidade com o dispositivo em B",
},
{
question:
"No fim do percurso de subida encontra-se montado um 'batente de bloqueio superior' ou um 'batente de bloqueio terminal', em conforme indicado na secção B",
},
{
question:
"No início do percurso de subida, não montado diretamente ai nível do solo, estão montados dois (2) batentes de bloqueio 'inferior', conforme indicado na secção B",
},
{
question:
'o carril-guia passa pelo menos 1 metro acima da aresta superior do patamar, conforme o dispositivo na secção B',
},
{
question:
'Em conformidade com o dispositivo na secção B, não existem extensões acima da escada com mais de 38cm sem reforço de lonfarina (52,5xm em escadas YAL e ZAL)',
},
{
question:
'O reforço da longarina está corretamente montada conforme o dispositivo na secção B',
},
{
question:
'O ângulo de flexão máximo nas peças flexiveis foi observado (ver secção C)',
},
{
question:
'Todos os troços estão montados corretamente, em conformidade com o dispositivo da secção B',
},
{
question:
'As uniões dos trilhos-guia estão corretamente instaladas, conforme com o dispositivo da secção B',
},
{ question: 'A passagem entre trilhos-guia está alinhada' },
{ question: 'O carril-guia está livre de sujidades' },
{
question:
'Só foram utilizados elementos de fixação e uniões roscadas protegidos contra a corrosão (inspeções: os elementos de fixação e uniões não apresentam corrosão)',
},
{
question:
'O aparelho anti queda Soll só se deixa montar no sentido correto do seu duncionamento no percurso de subida e descida',
},
{ question: 'Existe a placa de identificação do fabricante' },
{
question:
'Foi realizado um percurso de ensaio e não forma detetadas quaisquer falhas',
},
{ question: 'Só foram instalados componentes do fabricante' },
{
question:
'A Escada e/ou elementos de suporte não apresentam danos visiveis, indício de deficiente fixação ou falhas de componentes que ponham em causa a sua utilização',
},
];
// Seed InspectionQuestions
await prisma.inspectionQuestion.deleteMany();
for (let i = 0; i < inspectionQuestions.length; i++) {
const q = inspectionQuestions[i];
await prisma.inspectionQuestion.create({
data: {
question: q.question,
orderIndex: i + 1,
},
});
}
console.log('Seeded InspectionQuestions:', inspectionQuestions.length);
} }
main() main()
......
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function main() {
console.log('Starting CandidateSite seeding...');
// Get all candidates
const candidates = await prisma.candidate.findMany({
select: {
id: true,
latitude: true,
longitude: true,
},
});
console.log(`Found ${candidates.length} candidates`);
// Get all sites
const sites = await prisma.site.findMany({
select: {
id: true,
},
});
console.log(`Found ${sites.length} sites`);
// Create a map of candidates by their coordinates for easy lookup
const candidateMap = new Map(
candidates.map(c => [`${c.latitude},${c.longitude}`, c])
);
// Get the seed data from seedCandidates.ts
const seedData = [
// Site ID 1 candidates
{
candidateCode: 'A',
latitude: 38.726545,
longitude: -9.419486,
siteId: 1
},
{
candidateCode: 'B',
latitude: 38.725969,
longitude: -9.423131,
siteId: 1
},
{
candidateCode: 'C',
latitude: 38.730217,
longitude: -9.418326,
siteId: 1
},
{
candidateCode: 'D',
latitude: 38.725250,
longitude: -9.424500,
siteId: 1
},
{
candidateCode: 'E',
latitude: 38.726738,
longitude: -9.419803,
siteId: 1
},
{
candidateCode: 'F',
latitude: 38.726868,
longitude: -9.419859,
siteId: 1
},
{
candidateCode: 'G',
latitude: 38.726508,
longitude: -9.419673,
siteId: 1
},
// Site ID 2 candidates
{
candidateCode: 'A',
latitude: 38.644019,
longitude: -9.237759,
siteId: 2
},
{
candidateCode: 'C',
latitude: 38.644997,
longitude: -9.238472,
siteId: 2
},
{
candidateCode: 'D',
latitude: 38.644873,
longitude: -9.238874,
siteId: 2
},
{
candidateCode: 'E',
latitude: 38.644799,
longitude: -9.239149,
siteId: 2
},
{
candidateCode: 'F',
latitude: 38.644422,
longitude: -9.239560,
siteId: 2
},
{
candidateCode: 'G',
latitude: 38.644416,
longitude: -9.240363,
siteId: 2
},
{
candidateCode: 'H',
latitude: 38.644703,
longitude: -9.237950,
siteId: 2
},
{
candidateCode: 'I',
latitude: 38.645030,
longitude: -9.238106,
siteId: 2
},
{
candidateCode: 'J',
latitude: 38.644929,
longitude: -9.237269,
siteId: 2
},
{
candidateCode: 'L',
latitude: 38.645336,
longitude: -9.237468,
siteId: 2
},
{
candidateCode: 'M',
latitude: 38.645248,
longitude: -9.237779,
siteId: 2
},
{
candidateCode: 'N',
latitude: 38.644829,
longitude: -9.237575,
siteId: 2
},
{
candidateCode: 'O',
latitude: 38.643395,
longitude: -9.237417,
siteId: 2
},
{
candidateCode: 'P',
latitude: 38.643972,
longitude: -9.237750,
siteId: 2
},
{
candidateCode: 'Q',
latitude: 38.641333,
longitude: -9.236250,
siteId: 2
},
// Site ID 3 candidates
{
candidateCode: 'A',
latitude: 38.703867,
longitude: -9.409794,
siteId: 3
},
{
candidateCode: 'B',
latitude: 38.705352,
longitude: -9.409230,
siteId: 3
},
{
candidateCode: 'C',
latitude: 38.705252,
longitude: -9.408116,
siteId: 3
},
{
candidateCode: 'D',
latitude: 38.705479,
longitude: -9.410360,
siteId: 3
},
{
candidateCode: 'E',
latitude: 38.706143,
longitude: -9.408228,
siteId: 3
},
{
candidateCode: 'F',
latitude: 38.707038,
longitude: -9.408328,
siteId: 3
},
{
candidateCode: 'G',
latitude: 38.706369,
longitude: -9.408576,
siteId: 3
},
{
candidateCode: 'H',
latitude: 38.716133,
longitude: -9.240754,
siteId: 3
},
{
candidateCode: 'I',
latitude: 38.715948,
longitude: -9.240704,
siteId: 3
},
{
candidateCode: 'J',
latitude: 38.716057,
longitude: -9.241199,
siteId: 3
},
{
candidateCode: 'L',
latitude: 38.716237,
longitude: -9.241284,
siteId: 3
},
{
candidateCode: 'M',
latitude: 38.704317,
longitude: -9.404516,
siteId: 3
},
// Site ID 5 candidates
{
candidateCode: 'A',
latitude: 38.721221,
longitude: -9.154731,
siteId: 5
},
{
candidateCode: 'B',
latitude: 38.720636,
longitude: -9.152795,
siteId: 5
},
{
candidateCode: 'C',
latitude: 38.721725,
longitude: -9.151891,
siteId: 5
},
// Site ID 6 candidates
{
candidateCode: 'A',
latitude: 37.075038,
longitude: -8.125136,
siteId: 6
},
{
candidateCode: 'B',
latitude: 37.074373,
longitude: -8.120280,
siteId: 6
},
{
candidateCode: 'C',
latitude: 37.076107,
longitude: -8.117430,
siteId: 6
},
{
candidateCode: 'D',
latitude: 37.074852,
longitude: -8.118025,
siteId: 6
},
{
candidateCode: 'E',
latitude: 37.075541,
longitude: -8.125299,
siteId: 6
},
{
candidateCode: 'F',
latitude: 37.076201,
longitude: -8.124550,
siteId: 6
},
{
candidateCode: 'G',
latitude: 37.075943,
longitude: -8.124319,
siteId: 6
},
// Site ID 7 candidates
{
candidateCode: 'A',
latitude: 39.494514,
longitude: -8.613901,
siteId: 7
},
{
candidateCode: 'B',
latitude: 39.495646,
longitude: -8.610714,
siteId: 7
},
{
candidateCode: 'C',
latitude: 39.495916,
longitude: -8.610367,
siteId: 7
},
{
candidateCode: 'D',
latitude: 39.495205,
longitude: -8.612732,
siteId: 7
},
// Site ID 9 candidates
{
candidateCode: 'A',
latitude: 38.717448,
longitude: -9.149730,
siteId: 9
},
{
candidateCode: 'B',
latitude: 38.717032,
longitude: -9.148821,
siteId: 9
},
{
candidateCode: 'C',
latitude: 38.717222,
longitude: -9.149457,
siteId: 9
},
{
candidateCode: 'D',
latitude: 38.717007,
longitude: -9.149382,
siteId: 9
},
{
candidateCode: 'E',
latitude: 38.715710,
longitude: -9.149208,
siteId: 9
},
{
candidateCode: 'F',
latitude: 38.716928,
longitude: -9.148390,
siteId: 9
},
{
candidateCode: 'G',
latitude: 38.715507,
longitude: -9.148643,
siteId: 9
},
{
candidateCode: 'H',
latitude: 38.715750,
longitude: -9.148213,
siteId: 9
},
{
candidateCode: 'I',
latitude: 38.716006,
longitude: -9.147743,
siteId: 9
},
{
candidateCode: 'J',
latitude: 38.716320,
longitude: -9.147463,
siteId: 9
},
{
candidateCode: 'L',
latitude: 38.715906,
longitude: -9.146578,
siteId: 9
},
{
candidateCode: 'M',
latitude: 38.715973,
longitude: -9.146910,
siteId: 9
},
{
candidateCode: 'N',
latitude: 38.715556,
longitude: -9.146083,
siteId: 9
},
{
candidateCode: 'O',
latitude: 38.716139,
longitude: -9.148722,
siteId: 9
}
];
// Create CandidateSite entries for each candidate
let createdCount = 0;
let skippedCount = 0;
for (const data of seedData) {
const key = `${data.latitude},${data.longitude}`;
const candidate = candidateMap.get(key);
if (candidate) {
try {
// Create the relationship for the original site
await prisma.candidateSite.create({
data: {
candidateId: candidate.id,
siteId: data.siteId,
},
});
createdCount++;
// If this is a site ID 3 candidate, also create a relationship for site ID 4
if (data.siteId === 3) {
await prisma.candidateSite.create({
data: {
candidateId: candidate.id,
siteId: 4, // Site ID 4
},
});
createdCount++;
}
} catch (error) {
if (error.code === 'P2002') {
// Unique constraint violation - relationship already exists
skippedCount++;
} else {
console.error(`Error creating relationship for candidate ${candidate.id} and site ${data.siteId}:`, error);
}
}
} else {
console.warn(`Could not find matching candidate for coordinates: ${key}`);
}
}
console.log(`CandidateSite seeding completed: ${createdCount} relationships created, ${skippedCount} skipped (already existed)`);
}
main()
.catch((error) => {
console.error('Seeding failed:', error);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});
\ No newline at end of file
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function main() {
// Get the superadmin user
const superadmin = await prisma.user.findUnique({
where: { email: 'augusto.fonte@brandit.pt' },
});
if (!superadmin) {
console.error('Superadmin user not found. Please run the main seed.ts first.');
return;
}
// Get all sites to associate candidates with
const sites = await prisma.site.findMany();
if (sites.length === 0) {
console.error('No sites found. Please run the seedSites.ts first.');
return;
}
// Example candidates data - you can replace this with your actual data
const candidatesData = [
// Site ID 1 candidates
{
candidateCode: 'A',
latitude: 38.726545,
longitude: -9.419486,
type: 'GF',
address: 'Rua das Tojas, Alvide - Cascais',
comment: '05/08/2024: PD enviadas - Negociação Cellnex',
currentStatus: 'REJECTED',
onGoing: false,
siteId: sites[0].id
},
{
candidateCode: 'B',
latitude: 38.725969,
longitude: -9.423131,
type: 'GF',
address: 'Site CLX 008U7_ALVIDE',
comment: '06/01/2025: MEO reprovou o local',
currentStatus: 'REJECTED',
onGoing: false,
siteId: sites[0].id
},
{
candidateCode: 'C',
latitude: 38.730217,
longitude: -9.418326,
type: 'RT',
address: 'Hospital de Cascais',
comment: '10/02/2025: PD enviadas - Negociação Cellnex',
currentStatus: 'NEGOTIATION_ONGOING',
onGoing: true,
siteId: sites[0].id
},
{
candidateCode: 'D',
latitude: 38.725250,
longitude: -9.424500,
type: 'GF',
address: '',
comment: 'Terreno da brisa - possibilidade de construçao de Greenfield - em validaçao MNO',
currentStatus: 'NEGOTIATION_ONGOING',
onGoing: true,
siteId: sites[0].id
},
{
candidateCode: 'E',
latitude: 38.726738,
longitude: -9.419803,
type: 'GF',
address: '',
comment: 'Rejeitam a colocaçao do site',
currentStatus: 'REJECTED',
onGoing: false,
siteId: sites[0].id
},
{
candidateCode: 'F',
latitude: 38.726868,
longitude: -9.419859,
type: 'GF',
address: '',
comment: 'Rejeitam a colocaçao do site',
currentStatus: 'REJECTED',
onGoing: false,
siteId: sites[0].id
},
{
candidateCode: 'G',
latitude: 38.726508,
longitude: -9.419673,
type: 'GF',
address: '',
comment: 'Rejeitam a colocaçao do site',
currentStatus: 'REJECTED',
onGoing: false,
siteId: sites[0].id
},
// Site ID 2 candidates
{
candidateCode: 'A',
latitude: 38.644019,
longitude: -9.237759,
type: 'RT',
address: 'Av General Humberto Delgado, 27, Costa Caparica',
comment: '22/01/2025: SS enviado - Negociação Cellnex',
currentStatus: 'NEGOTIATION_ONGOING',
onGoing: true,
siteId: sites[1].id
},
{
candidateCode: 'C',
latitude: 38.644997,
longitude: -9.238472,
type: 'RT',
address: '',
comment: 'REJEITADO',
currentStatus: 'REJECTED',
onGoing: false,
siteId: sites[1].id
},
{
candidateCode: 'D',
latitude: 38.644873,
longitude: -9.238874,
type: 'RT',
address: '',
comment: 'REJEITADO',
currentStatus: 'REJECTED',
onGoing: false,
siteId: sites[1].id
},
{
candidateCode: 'E',
latitude: 38.644799,
longitude: -9.239149,
type: 'RT',
address: '',
comment: 'REJEITADO',
currentStatus: 'REJECTED',
onGoing: false,
siteId: sites[1].id
},
{
candidateCode: 'F',
latitude: 38.644422,
longitude: -9.239560,
type: 'RT',
address: '',
comment: 'REJEITADO',
currentStatus: 'REJECTED',
onGoing: false,
siteId: sites[1].id
},
{
candidateCode: 'G',
latitude: 38.644416,
longitude: -9.240363,
type: 'RT',
address: '',
comment: 'REJEITADO',
currentStatus: 'REJECTED',
onGoing: false,
siteId: sites[1].id
},
{
candidateCode: 'H',
latitude: 38.644703,
longitude: -9.237950,
type: 'RT',
address: '',
comment: 'REJEITADO',
currentStatus: 'REJECTED',
onGoing: false,
siteId: sites[1].id
},
{
candidateCode: 'I',
latitude: 38.645030,
longitude: -9.238106,
type: 'RT',
address: '',
comment: 'REJEITADO',
currentStatus: 'REJECTED',
onGoing: false,
siteId: sites[1].id
},
{
candidateCode: 'J',
latitude: 38.644929,
longitude: -9.237269,
type: 'RT',
address: '',
comment: 'REJEITADO',
currentStatus: 'REJECTED',
onGoing: false,
siteId: sites[1].id
},
{
candidateCode: 'L',
latitude: 38.645336,
longitude: -9.237468,
type: 'RT',
address: '',
comment: 'REJEITADO',
currentStatus: 'REJECTED',
onGoing: false,
siteId: sites[1].id
},
{
candidateCode: 'M',
latitude: 38.645248,
longitude: -9.237779,
type: 'RT',
address: '',
comment: 'REJEITADO',
currentStatus: 'REJECTED',
onGoing: false,
siteId: sites[1].id
},
{
candidateCode: 'N',
latitude: 38.644829,
longitude: -9.237575,
type: 'RT',
address: '',
comment: 'REJEITADO',
currentStatus: 'REJECTED',
onGoing: false,
siteId: sites[1].id
},
{
candidateCode: 'O',
latitude: 38.643395,
longitude: -9.237417,
type: 'RT',
address: '',
comment: 'REJEITADO PELO COND - NÃO TEM INTERESSE',
currentStatus: 'REJECTED',
onGoing: false,
siteId: sites[1].id
},
{
candidateCode: 'P',
latitude: 38.643972,
longitude: -9.237750,
type: 'RT',
address: '',
comment: 'Interesse por parte do cand. - MEO diz que local não é o ideal mas estao a reconsiderar',
currentStatus: 'NEGOTIATION_ONGOING',
onGoing: true,
siteId: sites[1].id
},
{
candidateCode: 'Q',
latitude: 38.641333,
longitude: -9.236250,
type: 'RT',
address: '',
comment: 'MEO validou o local, mas não conseguimos fechar a negociaçao com o candidato - em novas negociaçoes',
currentStatus: 'NEGOTIATION_ONGOING',
onGoing: true,
siteId: sites[1].id
},
// Site ID 3 candidates
{
candidateCode: 'A',
latitude: 38.703867,
longitude: -9.409794,
type: 'RT',
address: 'R Dom António Guedes de Herédia, 68, Estoril',
comment: '14-01-2025 A parte superior foi cedida ao proprietário do ultimo piso, pelo que não será possível a instalação',
currentStatus: 'REJECTED',
onGoing: false,
siteId: sites[2].id
},
{
candidateCode: 'B',
latitude: 38.705352,
longitude: -9.409230,
type: 'RT',
address: 'Av. São Pedro no1, Estoril',
comment: '12-02-2025: Condminio vai reanalisar uma eventual contra-proposta',
currentStatus: 'REJECTED',
onGoing: false,
siteId: sites[2].id
},
{
candidateCode: 'C',
latitude: 38.705252,
longitude: -9.408116,
type: 'RT',
address: 'Av Estrangeiros, 175, 2765-410 Monte Estoril',
comment: '17-02-2025: Aguarda aprovação de local e condições',
currentStatus: 'NEGOTIATION_ONGOING',
onGoing: true,
siteId: sites[2].id
},
{
candidateCode: 'D',
latitude: 38.705479,
longitude: -9.410360,
type: 'RT',
address: 'Av Faial, 218, Estoril',
comment: '11/02/2025: Proposta rejeitada pelo condominio',
currentStatus: 'REJECTED',
onGoing: false,
siteId: sites[2].id
},
{
candidateCode: 'E',
latitude: 38.706143,
longitude: -9.408228,
type: 'RT',
address: 'Rua Alegre, nº1, Estoril',
comment: '12-03-2025: Conseguimos falar com uma condómina que nos informou que o administrador do condomínio está ausente do país e que só regressa em junho. Não disponibilizou contactos, apesar da insistência, e também não mostrou qualquer interesse em receber/analisar uma proposta.',
currentStatus: 'NEGOTIATION_ONGOING',
onGoing: true,
siteId: sites[2].id
},
{
candidateCode: 'F',
latitude: 38.707038,
longitude: -9.408328,
type: 'RT',
address: 'Rua Conde Moser, Estoril',
comment: '',
currentStatus: 'NEGOTIATION_ONGOING',
onGoing: true,
siteId: sites[2].id
},
{
candidateCode: 'G',
latitude: 38.706369,
longitude: -9.408576,
type: 'RT',
address: 'Rua Conde Moser 284, Estoril',
comment: '21-03-2025 Proposta em analise no proprietário',
currentStatus: 'NEGOTIATION_ONGOING',
onGoing: true,
siteId: sites[2].id
},
{
candidateCode: 'H',
latitude: 38.716133,
longitude: -9.240754,
type: 'RT',
address: 'ADMIN INTERNA',
comment: 'REJEITADO pelo candidato',
currentStatus: 'REJECTED',
onGoing: false,
siteId: sites[2].id
},
{
candidateCode: 'I',
latitude: 38.715948,
longitude: -9.240704,
type: 'RT',
address: 'AD URBIS',
comment: 'REJEITADO pelo candidato',
currentStatus: 'REJECTED',
onGoing: false,
siteId: sites[2].id
},
{
candidateCode: 'J',
latitude: 38.716057,
longitude: -9.241199,
type: 'RT',
address: 'MCM CONDOMÍNIOS',
comment: 'REJEITADO pelo candidato',
currentStatus: 'REJECTED',
onGoing: false,
siteId: sites[2].id
},
{
candidateCode: 'L',
latitude: 38.716237,
longitude: -9.241284,
type: 'RT',
address: 'CERLENA - SOCIEDADE CIVIL DE INVESTIMENTOS IMOBILIÁRIOS S.A.R.L.',
comment: 'AGC 18-05 - REJEITADO',
currentStatus: 'REJECTED',
onGoing: false,
siteId: sites[2].id
},
{
candidateCode: 'M',
latitude: 38.704317,
longitude: -9.404516,
type: 'GF',
address: '',
comment: 'REJEITADO pelo candidato',
currentStatus: 'REJECTED',
onGoing: false,
siteId: sites[2].id
},
// Site ID 5 candidates
{
candidateCode: 'A',
latitude: 38.721221,
longitude: -9.154731,
type: 'RT',
address: 'Rua de São Francisco Sales, 17, Lisboa',
comment: '12-03-2025 Falámos com moradores mas não disponibilizaram contactos do administrador de condominio',
currentStatus: 'NEGOTIATION_ONGOING',
onGoing: true,
siteId: sites[4].id
},
{
candidateCode: 'B',
latitude: 38.720636,
longitude: -9.152795,
type: 'RT',
address: 'Rua Alexanre Herculano, 53',
comment: 'Rejeitado por parte do candidato',
currentStatus: 'REJECTED',
onGoing: false,
siteId: sites[4].id
},
{
candidateCode: 'C',
latitude: 38.721725,
longitude: -9.151891,
type: 'RT',
address: 'Rua Alexanre Herculano, 50',
comment: 'Candidato com licenciamento feito - bloqueado pelo condominio em fase de construçao - Legal a rever',
currentStatus: 'NEGOTIATION_ONGOING',
onGoing: true,
siteId: sites[4].id
},
// Site ID 6 candidates
{
candidateCode: 'A',
latitude: 37.075038,
longitude: -8.125136,
type: 'GF',
address: 'Marina Vilamoura',
comment: '05-05-2023: PD enviadas- Negociação Cellnex',
currentStatus: 'REJECTED',
onGoing: false,
siteId: sites[5].id
},
{
candidateCode: 'B',
latitude: 37.074373,
longitude: -8.120280,
type: 'RT',
address: 'Tivoli Marina Vilamoura Algarve Resort',
comment: '10-07-2024: Proposta de instalação enviada (Negociação Cellnex)',
currentStatus: 'REJECTED',
onGoing: false,
siteId: sites[5].id
},
{
candidateCode: 'C',
latitude: 37.076107,
longitude: -8.117430,
type: 'RT',
address: 'Hotel Vila Galé Marina',
comment: '17-02-2025 proposta em analise no diretor do Hotel.',
currentStatus: 'REJECTED',
onGoing: false,
siteId: sites[5].id
},
{
candidateCode: 'D',
latitude: 37.074852,
longitude: -8.118025,
type: 'RT',
address: 'Dom Pedro Marina Hotel - Vilamoura',
comment: '18-02-2025: Visita técnica - Aguarda validação do local',
currentStatus: 'REJECTED',
onGoing: false,
siteId: sites[5].id
},
{
candidateCode: 'E',
latitude: 37.075541,
longitude: -8.125299,
type: 'GF',
address: 'Terreno junto à Marina Vilamoura (antigo posto da GNR)',
comment: '27-02-2025: Terreno do estado para aquisição em concurso mas não é compativel com a instalação de uma estção radiocom.',
currentStatus: 'REJECTED',
onGoing: false,
siteId: sites[5].id
},
{
candidateCode: 'F',
latitude: 37.076201,
longitude: -8.124550,
type: 'GF',
address: 'Clube Vela Vilamoura',
comment: '26-02-2025: Clube tem um projeto a iniciar que irá ocupar todo o espaço, pelo que não é possível a instalação',
currentStatus: 'REJECTED',
onGoing: false,
siteId: sites[5].id
},
{
candidateCode: 'G',
latitude: 37.075943,
longitude: -8.124319,
type: 'GF',
address: 'Etar Vilamoura',
comment: '18-03-2025: Inframoura respondeu que a ETAR está no espaço da Marina de Vilamoura',
currentStatus: 'NEGOTIATION_ONGOING',
onGoing: true,
siteId: sites[5].id
},
// Site ID 7 candidates
{
candidateCode: 'A',
latitude: 39.494514,
longitude: -8.613901,
type: 'GF',
address: '',
comment: 'Em validaçao de cand identificado que pretende 3.6k ano',
currentStatus: 'NEGOTIATION_ONGOING',
onGoing: true,
siteId: sites[6].id
},
{
candidateCode: 'B',
latitude: 39.495646,
longitude: -8.610714,
type: 'GF',
address: 'Zibreira, Torres Novas',
comment: 'Candidato validado - pretende 12k ano',
currentStatus: 'NEGOTIATION_ONGOING',
onGoing: true,
siteId: sites[6].id
},
{
candidateCode: 'C',
latitude: 39.495916,
longitude: -8.610367,
type: 'GF',
address: '',
comment: '02-04-2025: Pesquisa contactos',
currentStatus: 'NEGOTIATION_ONGOING',
onGoing: true,
siteId: sites[6].id
},
{
candidateCode: 'D',
latitude: 39.495205,
longitude: -8.612732,
type: 'GF',
address: '',
comment: '02-04-2025: Pesquisa contactos',
currentStatus: 'NEGOTIATION_ONGOING',
onGoing: true,
siteId: sites[6].id
},
// Site ID 9 candidates
{
candidateCode: 'A',
latitude: 38.717448,
longitude: -9.149730,
type: 'RT',
address: 'R Escola Politécnica, N9, Lisboa',
comment: '16-01-2025: PD enviadas (Negociação Cellnex)',
currentStatus: 'REJECTED',
onGoing: false,
siteId: sites[7].id
},
{
candidateCode: 'B',
latitude: 38.717032,
longitude: -9.148821,
type: 'RT',
address: '',
comment: 'REJEITADO pelo candidato',
currentStatus: 'REJECTED',
onGoing: false,
siteId: sites[7].id
},
{
candidateCode: 'C',
latitude: 38.717222,
longitude: -9.149457,
type: 'RT',
address: '',
comment: 'REJEITADO pelo candidato',
currentStatus: 'REJECTED',
onGoing: false,
siteId: sites[7].id
},
{
candidateCode: 'D',
latitude: 38.717007,
longitude: -9.149382,
type: 'RT',
address: '',
comment: 'REJEITADO pelo candidato',
currentStatus: 'REJECTED',
onGoing: false,
siteId: sites[7].id
},
{
candidateCode: 'E',
latitude: 38.715710,
longitude: -9.149208,
type: 'RT',
address: '',
comment: 'Recusão a colocação de qualquer equipamento que não seja da embaixada',
currentStatus: 'REJECTED',
onGoing: false,
siteId: sites[7].id
},
{
candidateCode: 'F',
latitude: 38.716928,
longitude: -9.148390,
type: 'RT',
address: '',
comment: 'Contato com a direção, no qual informam que pelo valor de 400€ não aceitam e dão resposta com o valor que acham justo para eles - Aceitam 2.500€ mês',
currentStatus: 'REJECTED',
onGoing: false,
siteId: sites[7].id
},
{
candidateCode: 'G',
latitude: 38.715507,
longitude: -9.148643,
type: 'RT',
address: '',
comment: 'Contato com a direção, no qual informam que pelo valor de 400€ não aceitam e dão resposta com o valor que acham justo para eles - Aceitam 2.500€ mês',
currentStatus: 'REJECTED',
onGoing: false,
siteId: sites[7].id
},
{
candidateCode: 'H',
latitude: 38.715750,
longitude: -9.148213,
type: 'RT',
address: '',
comment: 'Contato com a direção, no qual informam que pelo valor de 400€ não aceitam e dão resposta com o valor que acham justo para eles - Aceitam 2.500€ mês',
currentStatus: 'REJECTED',
onGoing: false,
siteId: sites[7].id
},
{
candidateCode: 'I',
latitude: 38.716006,
longitude: -9.147743,
type: 'RT',
address: '',
comment: 'Cellnex não aceita local',
currentStatus: 'REJECTED',
onGoing: false,
siteId: sites[7].id
},
{
candidateCode: 'J',
latitude: 38.716320,
longitude: -9.147463,
type: 'RT',
address: '',
comment: 'Contato com a direção, no qual informam que na empresa será dificil aceitar a colocação',
currentStatus: 'REJECTED',
onGoing: false,
siteId: sites[7].id
},
{
candidateCode: 'L',
latitude: 38.715906,
longitude: -9.146578,
type: 'RT',
address: '',
comment: 'Recusada proposta de adaptaçao possivel pelo candidato',
currentStatus: 'REJECTED',
onGoing: false,
siteId: sites[7].id
},
{
candidateCode: 'M',
latitude: 38.715973,
longitude: -9.146910,
type: 'RT',
address: '',
comment: 'Senhorio não aceita a antena no seu edificio',
currentStatus: 'REJECTED',
onGoing: false,
siteId: sites[7].id
},
{
candidateCode: 'N',
latitude: 38.715556,
longitude: -9.146083,
type: 'RT',
address: 'Dom Pedro V, 53, Lisboa, 1250 Portugal - PENSÃO LONDRES',
comment: 'Fizemos visita tecnica ao loca com a equipa de manutenção - em elaboração de proposta a apresentar',
currentStatus: 'NEGOTIATION_ONGOING',
onGoing: true,
siteId: sites[7].id
},
{
candidateCode: 'O',
latitude: 38.716139,
longitude: -9.148722,
type: 'RT',
address: 'Jardim do principe real',
comment: 'Em apresentação de proposta com a junta de freguesia',
currentStatus: 'NEGOTIATION_ONGOING',
onGoing: true,
siteId: sites[7].id
}
];
// Create candidates and their comments
for (const candidateData of candidatesData) {
const { comment, ...candidateFields } = candidateData;
// Create the candidate
const candidate = await prisma.candidate.create({
data: {
...candidateFields,
createdById: superadmin.id,
updatedById: superadmin.id,
},
});
// If there's a comment, create it
if (comment) {
await prisma.comment.create({
data: {
content: comment,
candidateId: candidate.id,
createdById: superadmin.id,
},
});
}
}
console.log('Candidates seeded successfully!');
}
// Run the seeding function directly
main()
.catch((e) => {
console.error(e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});
\ No newline at end of file
...@@ -15,7 +15,7 @@ import { join } from 'path'; ...@@ -15,7 +15,7 @@ import { join } from 'path';
import { JwtAuthGuard } from './modules/auth/guards/jwt-auth.guard'; import { JwtAuthGuard } from './modules/auth/guards/jwt-auth.guard';
import { DashboardModule } from './modules/dashboard/dashboard.module'; import { DashboardModule } from './modules/dashboard/dashboard.module';
import { PartnersModule } from './modules/partners/partners.module'; import { PartnersModule } from './modules/partners/partners.module';
import { MaintenanceModule } from './modules/maintenance/maintenance.module'; import { InspectionModule } from './modules/inspection/inspection.module';
@Module({ @Module({
imports: [ imports: [
...@@ -55,7 +55,7 @@ import { MaintenanceModule } from './modules/maintenance/maintenance.module'; ...@@ -55,7 +55,7 @@ import { MaintenanceModule } from './modules/maintenance/maintenance.module';
CommentsModule, CommentsModule,
DashboardModule, DashboardModule,
PartnersModule, PartnersModule,
MaintenanceModule, InspectionModule,
], ],
controllers: [AppController], controllers: [AppController],
providers: [ providers: [
......
...@@ -39,7 +39,7 @@ async function bootstrap() { ...@@ -39,7 +39,7 @@ async function bootstrap() {
.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('candidates', 'Candidate management endpoints')
.addTag('maintenance', 'Site maintenance management endpoints') .addTag('inspection', 'Site inspection management endpoints')
.addBearerAuth( .addBearerAuth(
{ {
type: 'http', type: 'http',
......
...@@ -6,12 +6,12 @@ import { ...@@ -6,12 +6,12 @@ import {
ValidateNested, ValidateNested,
} from 'class-validator'; } from 'class-validator';
import { Type } from 'class-transformer'; import { Type } from 'class-transformer';
import { MaintenanceResponseOption } from './maintenance-response-option.enum'; import { InspectionResponseOption } from './inspection-response-option.enum';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
export class CreateMaintenanceResponseDto { export class CreateInspectionResponseDto {
@ApiProperty({ @ApiProperty({
description: 'The ID of the maintenance question being answered', description: 'The ID of the inspection question being answered',
example: 1, example: 1,
type: Number, type: Number,
}) })
...@@ -19,13 +19,13 @@ export class CreateMaintenanceResponseDto { ...@@ -19,13 +19,13 @@ export class CreateMaintenanceResponseDto {
questionId: number; questionId: number;
@ApiProperty({ @ApiProperty({
description: 'The response to the maintenance question', description: 'The response to the inspection question',
enum: MaintenanceResponseOption, enum: InspectionResponseOption,
example: MaintenanceResponseOption.YES, example: InspectionResponseOption.YES,
enumName: 'MaintenanceResponseOption', enumName: 'InspectionResponseOption',
}) })
@IsString() @IsString()
response: MaintenanceResponseOption; response: InspectionResponseOption;
@ApiPropertyOptional({ @ApiPropertyOptional({
description: description:
...@@ -39,9 +39,9 @@ export class CreateMaintenanceResponseDto { ...@@ -39,9 +39,9 @@ export class CreateMaintenanceResponseDto {
comment?: string; comment?: string;
} }
export class CreateMaintenanceDto { export class CreateInspectionDto {
@ApiProperty({ @ApiProperty({
description: 'Date when the maintenance was performed', description: 'Date when the inspection was performed',
example: '2025-05-21T13:00:00.000Z', example: '2025-05-21T13:00:00.000Z',
type: String, type: String,
}) })
...@@ -49,7 +49,7 @@ export class CreateMaintenanceDto { ...@@ -49,7 +49,7 @@ export class CreateMaintenanceDto {
date: string; date: string;
@ApiProperty({ @ApiProperty({
description: 'ID of the site where the maintenance was performed', description: 'ID of the site where the inspection was performed',
example: 1, example: 1,
type: Number, type: Number,
}) })
...@@ -57,8 +57,8 @@ export class CreateMaintenanceDto { ...@@ -57,8 +57,8 @@ export class CreateMaintenanceDto {
siteId: number; siteId: number;
@ApiPropertyOptional({ @ApiPropertyOptional({
description: 'Optional general comment about the maintenance', description: 'Optional general comment about the inspection',
example: 'Regular annual maintenance. Site is in good overall condition.', example: 'Regular annual inspection. Site is in good overall condition.',
type: String, type: String,
}) })
@IsString() @IsString()
...@@ -66,8 +66,8 @@ export class CreateMaintenanceDto { ...@@ -66,8 +66,8 @@ export class CreateMaintenanceDto {
comment?: string; comment?: string;
@ApiProperty({ @ApiProperty({
description: 'Responses to maintenance questions', description: 'Responses to inspection questions',
type: [CreateMaintenanceResponseDto], type: [CreateInspectionResponseDto],
example: [ example: [
{ {
questionId: 1, questionId: 1,
...@@ -87,6 +87,6 @@ export class CreateMaintenanceDto { ...@@ -87,6 +87,6 @@ export class CreateMaintenanceDto {
], ],
}) })
@ValidateNested({ each: true }) @ValidateNested({ each: true })
@Type(() => CreateMaintenanceResponseDto) @Type(() => CreateInspectionResponseDto)
responses: CreateMaintenanceResponseDto[]; responses: CreateInspectionResponseDto[];
} }
import { IsDateString, IsInt, IsOptional } from 'class-validator'; import { IsDateString, IsInt, IsOptional } from 'class-validator';
import { ApiPropertyOptional } from '@nestjs/swagger'; import { ApiPropertyOptional } from '@nestjs/swagger';
export class FindMaintenanceDto { export class FindInspectionDto {
@ApiPropertyOptional({ @ApiPropertyOptional({
description: 'Filter maintenance records by site ID', description: 'Filter inspection records by site ID',
example: 1, example: 1,
type: Number, type: Number,
}) })
...@@ -13,7 +13,7 @@ export class FindMaintenanceDto { ...@@ -13,7 +13,7 @@ export class FindMaintenanceDto {
@ApiPropertyOptional({ @ApiPropertyOptional({
description: description:
'Filter maintenance records with date greater than or equal to this date', 'Filter inspection records with date greater than or equal to this date',
example: '2025-01-01T00:00:00.000Z', example: '2025-01-01T00:00:00.000Z',
type: String, type: String,
}) })
...@@ -23,7 +23,7 @@ export class FindMaintenanceDto { ...@@ -23,7 +23,7 @@ export class FindMaintenanceDto {
@ApiPropertyOptional({ @ApiPropertyOptional({
description: description:
'Filter maintenance records with date less than or equal to this date', 'Filter inspection records with date less than or equal to this date',
example: '2025-12-31T23:59:59.999Z', example: '2025-12-31T23:59:59.999Z',
type: String, type: String,
}) })
......
export * from './create-inspection.dto';
export * from './find-inspection.dto';
export * from './inspection-response.dto';
export * from './inspection-response-option.enum';
/** /**
* Response options for maintenance questions * Response options for inspection questions
* *
* YES - Item is in good condition/working properly * YES - Item is in good condition/working properly
* NO - Item needs attention/repair * NO - Item needs attention/repair
* NA - Not applicable for this site * NA - Not applicable for this site
*/ */
export enum MaintenanceResponseOption { export enum InspectionResponseOption {
YES = 'YES', YES = 'YES',
NO = 'NO', NO = 'NO',
NA = 'NA', NA = 'NA',
......
import { MaintenanceResponseOption } from './maintenance-response-option.enum'; import { InspectionResponseOption } from './inspection-response-option.enum';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
export class MaintenanceQuestionDto { export class InspectionQuestionDto {
@ApiProperty({ @ApiProperty({
description: 'Unique identifier of the maintenance question', description: 'Unique identifier of the inspection question',
example: 1, example: 1,
type: Number, type: Number,
}) })
id: number; id: number;
@ApiProperty({ @ApiProperty({
description: 'Text of the maintenance question', description: 'Text of the inspection question',
example: 'Site access condition', example: 'Site access condition',
type: String, type: String,
}) })
...@@ -24,9 +24,9 @@ export class MaintenanceQuestionDto { ...@@ -24,9 +24,9 @@ export class MaintenanceQuestionDto {
orderIndex: number; orderIndex: number;
} }
export class MaintenanceResponseDto { export class InspectionResponseDto {
@ApiProperty({ @ApiProperty({
description: 'Unique identifier of the maintenance response', description: 'Unique identifier of the inspection response',
example: 1, example: 1,
type: Number, type: Number,
}) })
...@@ -34,11 +34,11 @@ export class MaintenanceResponseDto { ...@@ -34,11 +34,11 @@ export class MaintenanceResponseDto {
@ApiProperty({ @ApiProperty({
description: 'Response option selected for the question', description: 'Response option selected for the question',
enum: MaintenanceResponseOption, enum: InspectionResponseOption,
example: MaintenanceResponseOption.YES, example: InspectionResponseOption.YES,
enumName: 'MaintenanceResponseOption', enumName: 'InspectionResponseOption',
}) })
response: MaintenanceResponseOption; response: InspectionResponseOption;
@ApiPropertyOptional({ @ApiPropertyOptional({
description: 'Optional comment providing additional details', description: 'Optional comment providing additional details',
...@@ -49,14 +49,14 @@ export class MaintenanceResponseDto { ...@@ -49,14 +49,14 @@ export class MaintenanceResponseDto {
@ApiProperty({ @ApiProperty({
description: 'The question this response answers', description: 'The question this response answers',
type: MaintenanceQuestionDto, type: InspectionQuestionDto,
}) })
question: MaintenanceQuestionDto; question: InspectionQuestionDto;
} }
export class MaintenancePhotoDto { export class InspectionPhotoDto {
@ApiProperty({ @ApiProperty({
description: 'Unique identifier of the maintenance photo', description: 'Unique identifier of the inspection photo',
example: 1, example: 1,
type: Number, type: Number,
}) })
...@@ -64,7 +64,7 @@ export class MaintenancePhotoDto { ...@@ -64,7 +64,7 @@ export class MaintenancePhotoDto {
@ApiProperty({ @ApiProperty({
description: 'URL to access the photo', description: 'URL to access the photo',
example: '/uploads/maintenance/1/photo1.jpg', example: '/uploads/inspection/1/photo1.jpg',
type: String, type: String,
}) })
url: string; url: string;
...@@ -77,30 +77,30 @@ export class MaintenancePhotoDto { ...@@ -77,30 +77,30 @@ export class MaintenancePhotoDto {
filename: string; filename: string;
} }
export class MaintenanceDto { export class InspectionDto {
@ApiProperty({ @ApiProperty({
description: 'Unique identifier of the maintenance record', description: 'Unique identifier of the inspection record',
example: 1, example: 1,
type: Number, type: Number,
}) })
id: number; id: number;
@ApiProperty({ @ApiProperty({
description: 'Date when the maintenance was performed', description: 'Date when the inspection was performed',
example: '2025-05-21T13:00:00.000Z', example: '2025-05-21T13:00:00.000Z',
type: Date, type: Date,
}) })
date: Date; date: Date;
@ApiPropertyOptional({ @ApiPropertyOptional({
description: 'Optional general comment about the maintenance', description: 'Optional general comment about the inspection',
example: 'Annual preventive maintenance completed with minor issues noted', example: 'Annual preventive inspection completed with minor issues noted',
type: String, type: String,
}) })
comment?: string; comment?: string;
@ApiProperty({ @ApiProperty({
description: 'ID of the site where maintenance was performed', description: 'ID of the site where inspection was performed',
example: 1, example: 1,
type: Number, type: Number,
}) })
...@@ -121,16 +121,16 @@ export class MaintenanceDto { ...@@ -121,16 +121,16 @@ export class MaintenanceDto {
updatedAt: Date; updatedAt: Date;
@ApiProperty({ @ApiProperty({
description: 'Responses to maintenance questions', description: 'Responses to inspection questions',
type: [MaintenanceResponseDto], type: [InspectionResponseDto],
isArray: true, isArray: true,
}) })
responses: MaintenanceResponseDto[]; responses: InspectionResponseDto[];
@ApiProperty({ @ApiProperty({
description: 'Photos attached to the maintenance record', description: 'Photos attached to the inspection record',
type: [MaintenancePhotoDto], type: [InspectionPhotoDto],
isArray: true, isArray: true,
}) })
photos: MaintenancePhotoDto[]; photos: InspectionPhotoDto[];
} }
...@@ -16,12 +16,12 @@ import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; ...@@ -16,12 +16,12 @@ 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 { Role } from '@prisma/client';
import { MaintenanceService } from './maintenance.service'; import { InspectionService } from './inspection.service';
import { import {
CreateMaintenanceDto, CreateInspectionDto,
CreateMaintenanceResponseDto, CreateInspectionResponseDto,
} from './dto/create-maintenance.dto'; } from './dto/create-inspection.dto';
import { FindMaintenanceDto } from './dto/find-maintenance.dto'; import { FindInspectionDto } from './dto/find-inspection.dto';
import { multerConfig } from '../../common/multer/multer.config'; import { multerConfig } from '../../common/multer/multer.config';
import { import {
ApiTags, ApiTags,
...@@ -35,30 +35,30 @@ import { ...@@ -35,30 +35,30 @@ import {
getSchemaPath, getSchemaPath,
} from '@nestjs/swagger'; } from '@nestjs/swagger';
import { import {
MaintenanceDto, InspectionDto,
MaintenanceQuestionDto, InspectionQuestionDto,
MaintenanceResponseDto, InspectionResponseDto,
} from './dto/maintenance-response.dto'; } from './dto/inspection-response.dto';
@ApiTags('maintenance') @ApiTags('inspection')
@Controller('maintenance') @Controller('inspection')
@ApiBearerAuth('access-token') @ApiBearerAuth('access-token')
export class MaintenanceController { export class InspectionController {
constructor(private readonly maintenanceService: MaintenanceService) {} constructor(private readonly inspectionService: InspectionService) {}
@Post() @Post()
@UseGuards(JwtAuthGuard, RolesGuard) @UseGuards(JwtAuthGuard, RolesGuard)
@Roles(Role.ADMIN, Role.MANAGER, Role.OPERATOR, Role.PARTNER) @Roles(Role.ADMIN, Role.MANAGER, Role.OPERATOR, Role.PARTNER)
@UseInterceptors(FilesInterceptor('photos', 10, multerConfig)) @UseInterceptors(FilesInterceptor('photos', 10, multerConfig))
@ApiOperation({ @ApiOperation({
summary: 'Create a new maintenance record', summary: 'Create a new inspection record',
description: description:
'Creates a new maintenance record for a site with responses to maintenance questions and optional photos. Only users with ADMIN, MANAGER, OPERATOR, or PARTNER roles can create maintenance records.', 'Creates a new inspection record for a site with responses to inspection questions and optional photos. Only users with ADMIN, MANAGER, OPERATOR, or PARTNER roles can create inspection records.',
}) })
@ApiResponse({ @ApiResponse({
status: 201, status: 201,
description: 'The maintenance record has been successfully created.', description: 'The inspection record has been successfully created.',
type: MaintenanceDto, type: InspectionDto,
}) })
@ApiResponse({ status: 400, description: 'Invalid input data.' }) @ApiResponse({ status: 400, description: 'Invalid input data.' })
@ApiResponse({ status: 401, description: 'Unauthorized.' }) @ApiResponse({ status: 401, description: 'Unauthorized.' })
...@@ -70,7 +70,7 @@ export class MaintenanceController { ...@@ -70,7 +70,7 @@ export class MaintenanceController {
@ApiBearerAuth('access-token') @ApiBearerAuth('access-token')
@ApiConsumes('multipart/form-data') @ApiConsumes('multipart/form-data')
@ApiBody({ @ApiBody({
description: 'Maintenance data with optional photos', description: 'Inspection data with optional photos',
schema: { schema: {
type: 'object', type: 'object',
required: ['date', 'siteId', 'responses'], required: ['date', 'siteId', 'responses'],
...@@ -79,25 +79,25 @@ export class MaintenanceController { ...@@ -79,25 +79,25 @@ export class MaintenanceController {
type: 'string', type: 'string',
format: 'date-time', format: 'date-time',
example: '2025-05-21T13:00:00.000Z', example: '2025-05-21T13:00:00.000Z',
description: 'Date when the maintenance was performed', description: 'Date when the inspection was performed',
}, },
siteId: { siteId: {
type: 'integer', type: 'integer',
example: 1, example: 1,
description: 'ID of the site where the maintenance was performed', description: 'ID of the site where the inspection was performed',
}, },
comment: { comment: {
type: 'string', type: 'string',
example: example:
'Regular annual maintenance. Site is in good overall condition.', 'Regular annual inspection. Site is in good overall condition.',
description: 'Optional general comment about the maintenance', description: 'Optional general comment about the inspection',
}, },
responses: { responses: {
type: 'array', type: 'array',
items: { items: {
$ref: getSchemaPath(CreateMaintenanceDto), $ref: getSchemaPath(CreateInspectionDto),
}, },
description: 'Responses to maintenance questions', description: 'Responses to inspection questions',
}, },
photos: { photos: {
type: 'array', type: 'array',
...@@ -111,13 +111,13 @@ export class MaintenanceController { ...@@ -111,13 +111,13 @@ export class MaintenanceController {
}, },
}, },
}) })
async createMaintenance( async createInspection(
@Body() createMaintenanceDto: CreateMaintenanceDto, @Body() createInspectionDto: CreateInspectionDto,
@UploadedFiles() files: Express.Multer.File[], @UploadedFiles() files: Express.Multer.File[],
@Req() req, @Req() req,
) { ) {
return this.maintenanceService.createMaintenance( return this.inspectionService.createInspection(
createMaintenanceDto, createInspectionDto,
req.user.id, req.user.id,
files, files,
); );
...@@ -126,14 +126,14 @@ export class MaintenanceController { ...@@ -126,14 +126,14 @@ export class MaintenanceController {
@Get() @Get()
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@ApiOperation({ @ApiOperation({
summary: 'Find all maintenance records with optional filters', summary: 'Find all inspection records with optional filters',
description: description:
'Retrieves a list of maintenance records. Can be filtered by site ID and date range.', 'Retrieves a list of inspection records. Can be filtered by site ID and date range.',
}) })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
description: 'List of maintenance records.', description: 'List of inspection records.',
type: [MaintenanceDto], type: [InspectionDto],
}) })
@ApiResponse({ status: 401, description: 'Unauthorized.' }) @ApiResponse({ status: 401, description: 'Unauthorized.' })
@ApiQuery({ @ApiQuery({
...@@ -157,110 +157,110 @@ export class MaintenanceController { ...@@ -157,110 +157,110 @@ export class MaintenanceController {
description: 'Filter by end date (inclusive)', description: 'Filter by end date (inclusive)',
example: '2025-12-31T23:59:59.999Z', example: '2025-12-31T23:59:59.999Z',
}) })
async findAllMaintenance(@Query() findMaintenanceDto: FindMaintenanceDto) { async findAllInspection(@Query() findInspectionDto: FindInspectionDto) {
return this.maintenanceService.findAllMaintenance(findMaintenanceDto); return this.inspectionService.findAllInspection(findInspectionDto);
} }
@Get('questions') @Get('questions')
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@ApiOperation({ @ApiOperation({
summary: 'Get all maintenance questions', summary: 'Get all inspection questions',
description: description:
'Retrieves the list of predefined maintenance questions that need to be answered during maintenance.', 'Retrieves the list of predefined inspection questions that need to be answered during inspection.',
}) })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
description: 'List of maintenance questions.', description: 'List of inspection questions.',
type: [MaintenanceQuestionDto], type: [InspectionQuestionDto],
}) })
@ApiResponse({ status: 401, description: 'Unauthorized.' }) @ApiResponse({ status: 401, description: 'Unauthorized.' })
async getMaintenanceQuestions() { async getInspectionQuestions() {
return this.maintenanceService.getMaintenanceQuestions(); return this.inspectionService.getInspectionQuestions();
} }
@Get('questions/:id') @Get('questions/:id')
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@ApiOperation({ @ApiOperation({
summary: 'Get a specific maintenance question by ID', summary: 'Get a specific inspection question by ID',
description: 'Retrieves a specific maintenance question by its ID.', description: 'Retrieves a specific inspection question by its ID.',
}) })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
description: 'The maintenance question.', description: 'The inspection question.',
type: MaintenanceQuestionDto, type: InspectionQuestionDto,
}) })
@ApiResponse({ status: 401, description: 'Unauthorized.' }) @ApiResponse({ status: 401, description: 'Unauthorized.' })
@ApiResponse({ status: 404, description: 'Question not found.' }) @ApiResponse({ status: 404, description: 'Question not found.' })
@ApiParam({ @ApiParam({
name: 'id', name: 'id',
type: Number, type: Number,
description: 'ID of the maintenance question to retrieve', description: 'ID of the inspection question to retrieve',
example: 1, example: 1,
}) })
async getMaintenanceQuestionById(@Param('id', ParseIntPipe) id: number) { async getInspectionQuestionById(@Param('id', ParseIntPipe) id: number) {
return this.maintenanceService.getMaintenanceQuestionById(id); return this.inspectionService.getInspectionQuestionById(id);
} }
@Get(':id') @Get(':id')
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@ApiOperation({ @ApiOperation({
summary: 'Find maintenance record by ID', summary: 'Find inspection record by ID',
description: description:
'Retrieves a specific maintenance record by its ID, including all responses and photos.', 'Retrieves a specific inspection record by its ID, including all responses and photos.',
}) })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
description: 'The maintenance record.', description: 'The inspection record.',
type: MaintenanceDto, type: InspectionDto,
}) })
@ApiResponse({ status: 401, description: 'Unauthorized.' }) @ApiResponse({ status: 401, description: 'Unauthorized.' })
@ApiResponse({ status: 404, description: 'Maintenance record not found.' }) @ApiResponse({ status: 404, description: 'Inspection record not found.' })
@ApiParam({ @ApiParam({
name: 'id', name: 'id',
type: Number, type: Number,
description: 'ID of the maintenance record to retrieve', description: 'ID of the inspection record to retrieve',
example: 1, example: 1,
}) })
async findMaintenanceById(@Param('id', ParseIntPipe) id: number) { async findInspectionById(@Param('id', ParseIntPipe) id: number) {
return this.maintenanceService.findMaintenanceById(id); return this.inspectionService.findInspectionById(id);
} }
@Get(':id/responses') @Get(':id/responses')
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@ApiOperation({ @ApiOperation({
summary: 'Get responses for a specific maintenance record', summary: 'Get responses for a specific inspection record',
description: description:
'Retrieves all responses for a specific maintenance record including the associated questions.', 'Retrieves all responses for a specific inspection record including the associated questions.',
}) })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
description: 'List of maintenance responses.', description: 'List of inspection responses.',
type: [MaintenanceResponseDto], type: [InspectionResponseDto],
}) })
@ApiResponse({ status: 401, description: 'Unauthorized.' }) @ApiResponse({ status: 401, description: 'Unauthorized.' })
@ApiResponse({ status: 404, description: 'Maintenance record not found.' }) @ApiResponse({ status: 404, description: 'Inspection record not found.' })
@ApiParam({ @ApiParam({
name: 'id', name: 'id',
type: Number, type: Number,
description: 'ID of the maintenance record', description: 'ID of the inspection record',
example: 1, example: 1,
}) })
async getResponsesByMaintenanceId(@Param('id', ParseIntPipe) id: number) { async getResponsesByInspectionId(@Param('id', ParseIntPipe) id: number) {
return this.maintenanceService.getResponsesByMaintenanceId(id); return this.inspectionService.getResponsesByInspectionId(id);
} }
@Post(':id/responses') @Post(':id/responses')
@UseGuards(JwtAuthGuard, RolesGuard) @UseGuards(JwtAuthGuard, RolesGuard)
@Roles(Role.ADMIN, Role.MANAGER, Role.OPERATOR, Role.PARTNER) @Roles(Role.ADMIN, Role.MANAGER, Role.OPERATOR, Role.PARTNER)
@ApiOperation({ @ApiOperation({
summary: 'Add responses to an existing maintenance record', summary: 'Add responses to an existing inspection record',
description: description:
'Adds or updates responses for a specific maintenance record. Can be used to complete a partially filled maintenance record.', 'Adds or updates responses for a specific inspection record. Can be used to complete a partially filled inspection record.',
}) })
@ApiResponse({ @ApiResponse({
status: 201, status: 201,
description: 'The responses have been successfully added.', description: 'The responses have been successfully added.',
type: [MaintenanceResponseDto], type: [InspectionResponseDto],
}) })
@ApiResponse({ status: 400, description: 'Invalid input data.' }) @ApiResponse({ status: 400, description: 'Invalid input data.' })
@ApiResponse({ status: 401, description: 'Unauthorized.' }) @ApiResponse({ status: 401, description: 'Unauthorized.' })
...@@ -270,24 +270,24 @@ export class MaintenanceController { ...@@ -270,24 +270,24 @@ export class MaintenanceController {
}) })
@ApiResponse({ @ApiResponse({
status: 404, status: 404,
description: 'Maintenance record or question not found.', description: 'Inspection record or question not found.',
}) })
@ApiParam({ @ApiParam({
name: 'id', name: 'id',
type: Number, type: Number,
description: 'ID of the maintenance record', description: 'ID of the inspection record',
example: 1, example: 1,
}) })
@ApiBody({ @ApiBody({
description: 'Array of maintenance responses to add', description: 'Array of inspection responses to add',
type: [CreateMaintenanceResponseDto], type: [CreateInspectionResponseDto],
}) })
async addResponsesToMaintenance( async addResponsesToInspection(
@Param('id', ParseIntPipe) id: number, @Param('id', ParseIntPipe) id: number,
@Body() responses: CreateMaintenanceResponseDto[], @Body() responses: CreateInspectionResponseDto[],
@Req() req, @Req() req,
) { ) {
return this.maintenanceService.addResponsesToMaintenance( return this.inspectionService.addResponsesToInspection(
id, id,
responses, responses,
req.user.id, req.user.id,
......
/** /**
* Example requests and responses for the Maintenance API * Example requests and responses for the Inspection API
* This file is for documentation purposes only * This file is for documentation purposes only
*/ */
/** /**
* Example request for creating a maintenance record * Example request for creating a inspection record
*/ */
export const createMaintenanceExample = { export const createInspectionExample = {
date: '2025-05-21T13:00:00.000Z', date: '2025-05-21T13:00:00.000Z',
siteId: 1, siteId: 1,
comment: 'Regular annual maintenance. Site is in good overall condition.', comment: 'Regular annual inspection. Site is in good overall condition.',
responses: [ responses: [
{ {
questionId: 1, questionId: 1,
...@@ -41,12 +41,12 @@ export const createMaintenanceExample = { ...@@ -41,12 +41,12 @@ export const createMaintenanceExample = {
}; };
/** /**
* Example response for a maintenance record * Example response for a inspection record
*/ */
export const maintenanceResponseExample = { export const inspectionResponseExample = {
id: 1, id: 1,
date: '2025-05-21T13:00:00.000Z', date: '2025-05-21T13:00:00.000Z',
comment: 'Regular annual maintenance. Site is in good overall condition.', comment: 'Regular annual inspection. Site is in good overall condition.',
siteId: 1, siteId: 1,
createdAt: '2025-05-21T13:15:30.000Z', createdAt: '2025-05-21T13:15:30.000Z',
updatedAt: '2025-05-21T13:15:30.000Z', updatedAt: '2025-05-21T13:15:30.000Z',
...@@ -106,26 +106,26 @@ export const maintenanceResponseExample = { ...@@ -106,26 +106,26 @@ export const maintenanceResponseExample = {
photos: [ photos: [
{ {
id: 1, id: 1,
url: '/uploads/maintenance/1/entrance.jpg', url: '/uploads/inspection/1/entrance.jpg',
filename: 'entrance.jpg', filename: 'entrance.jpg',
}, },
{ {
id: 2, id: 2,
url: '/uploads/maintenance/1/damaged_fence.jpg', url: '/uploads/inspection/1/damaged_fence.jpg',
filename: 'damaged_fence.jpg', filename: 'damaged_fence.jpg',
}, },
{ {
id: 3, id: 3,
url: '/uploads/maintenance/1/equipment.jpg', url: '/uploads/inspection/1/equipment.jpg',
filename: 'equipment.jpg', filename: 'equipment.jpg',
}, },
], ],
}; };
/** /**
* Example response for maintenance questions * Example response for inspection questions
*/ */
export const maintenanceQuestionsExample = [ export const inspectionQuestionsExample = [
{ {
id: 1, id: 1,
question: 'Site access condition', question: 'Site access condition',
......
import { Module } from '@nestjs/common';
import { InspectionController } from './inspection.controller';
import { InspectionService } from './inspection.service';
import { PrismaModule } from '../../common/prisma/prisma.module';
@Module({
imports: [PrismaModule],
controllers: [InspectionController],
providers: [InspectionService],
exports: [InspectionService],
})
export class InspectionModule {}
import { Injectable, NotFoundException } from '@nestjs/common'; import { Injectable, NotFoundException } from '@nestjs/common';
import { PrismaService } from '../../common/prisma/prisma.service'; import { PrismaService } from '../../common/prisma/prisma.service';
import { import {
CreateMaintenanceDto, CreateInspectionDto,
CreateMaintenanceResponseDto, CreateInspectionResponseDto,
} from './dto/create-maintenance.dto'; } from './dto/create-inspection.dto';
import { FindMaintenanceDto } from './dto/find-maintenance.dto'; import { FindInspectionDto } from './dto/find-inspection.dto';
import { import {
MaintenanceDto, InspectionDto,
MaintenanceResponseDto, InspectionResponseDto,
MaintenancePhotoDto, InspectionPhotoDto,
} from './dto/maintenance-response.dto'; } from './dto/inspection-response.dto';
import { MaintenanceResponseOption } from './dto/maintenance-response-option.enum'; import { InspectionResponseOption } from './dto/inspection-response-option.enum';
import { saveMaintenancePhotos } from './maintenance.utils'; import { saveInspectionPhotos } from './inspection.utils';
@Injectable() @Injectable()
export class MaintenanceService { export class InspectionService {
constructor(private prisma: PrismaService) { } constructor(private prisma: PrismaService) {}
async createMaintenance( async createInspection(
dto: CreateMaintenanceDto, dto: CreateInspectionDto,
userId: number, userId: number,
files?: Express.Multer.File[], files?: Express.Multer.File[],
): Promise<MaintenanceDto> { ): Promise<InspectionDto> {
// Check if site exists // Check if site exists
const site = await this.prisma.site.findUnique({ const site = await this.prisma.site.findUnique({
where: { id: dto.siteId }, where: { id: dto.siteId },
...@@ -31,8 +31,8 @@ export class MaintenanceService { ...@@ -31,8 +31,8 @@ export class MaintenanceService {
throw new NotFoundException(`Site with ID ${dto.siteId} not found`); throw new NotFoundException(`Site with ID ${dto.siteId} not found`);
} }
// Create maintenance record // Create inspection record
const maintenance = await this.prisma.maintenance.create({ const inspection = await this.prisma.inspection.create({
data: { data: {
date: new Date(dto.date), date: new Date(dto.date),
comment: dto.comment, comment: dto.comment,
...@@ -60,26 +60,26 @@ export class MaintenanceService { ...@@ -60,26 +60,26 @@ export class MaintenanceService {
if (files && files.length > 0) { if (files && files.length > 0) {
try { try {
// Save files to disk // Save files to disk
const filePaths = await saveMaintenancePhotos(files, maintenance.id); const filePaths = await saveInspectionPhotos(files, inspection.id);
// Create photo records in database // Create photo records in database
const photoPromises = files.map((file, index) => { const photoPromises = files.map((file, index) => {
return this.prisma.maintenancePhoto.create({ return this.prisma.inspectionPhoto.create({
data: { data: {
filename: file.originalname, filename: file.originalname,
mimeType: file.mimetype, mimeType: file.mimetype,
size: file.size, size: file.size,
url: filePaths[index], url: filePaths[index],
maintenance: { connect: { id: maintenance.id } }, inspection: { connect: { id: inspection.id } },
}, },
}); });
}); });
await Promise.all(photoPromises); await Promise.all(photoPromises);
// Fetch the updated maintenance record with photos // Fetch the updated inspection record with photos
const updatedMaintenance = await this.prisma.maintenance.findUnique({ const updatedInspection = await this.prisma.inspection.findUnique({
where: { id: maintenance.id }, where: { id: inspection.id },
include: { include: {
responses: { responses: {
include: { include: {
...@@ -90,17 +90,17 @@ export class MaintenanceService { ...@@ -90,17 +90,17 @@ export class MaintenanceService {
}, },
}); });
return this.mapToDto(updatedMaintenance); return this.mapToDto(updatedInspection);
} catch (error) { } catch (error) {
console.error('Error processing maintenance photos:', error); console.error('Error processing inspection photos:', error);
// Continue without photos if there's an error // Continue without photos if there's an error
} }
} }
return this.mapToDto(maintenance); return this.mapToDto(inspection);
} }
async findAllMaintenance(dto: FindMaintenanceDto): Promise<MaintenanceDto[]> { async findAllInspection(dto: FindInspectionDto): Promise<InspectionDto[]> {
const filter: any = {}; const filter: any = {};
if (dto.siteId) { if (dto.siteId) {
...@@ -119,7 +119,7 @@ export class MaintenanceService { ...@@ -119,7 +119,7 @@ export class MaintenanceService {
} }
} }
const maintenances = await this.prisma.maintenance.findMany({ const inspections = await this.prisma.inspection.findMany({
where: filter, where: filter,
include: { include: {
responses: { responses: {
...@@ -134,11 +134,11 @@ export class MaintenanceService { ...@@ -134,11 +134,11 @@ export class MaintenanceService {
}, },
}); });
return maintenances.map(this.mapToDto); return inspections.map(this.mapToDto);
} }
async findMaintenanceById(id: number): Promise<MaintenanceDto> { async findInspectionById(id: number): Promise<InspectionDto> {
const maintenance = await this.prisma.maintenance.findUnique({ const inspection = await this.prisma.inspection.findUnique({
where: { id }, where: { id },
include: { include: {
responses: { responses: {
...@@ -150,40 +150,40 @@ export class MaintenanceService { ...@@ -150,40 +150,40 @@ export class MaintenanceService {
}, },
}); });
if (!maintenance) { if (!inspection) {
throw new NotFoundException(`Maintenance with ID ${id} not found`); throw new NotFoundException(`Inspection with ID ${id} not found`);
} }
return this.mapToDto(maintenance); return this.mapToDto(inspection);
} }
async getMaintenanceQuestions() { async getInspectionQuestions() {
return this.prisma.maintenanceQuestion.findMany({ return this.prisma.inspectionQuestion.findMany({
orderBy: { orderBy: {
orderIndex: 'asc', orderIndex: 'asc',
}, },
}); });
} }
async getMaintenanceQuestionById(id: number) { async getInspectionQuestionById(id: number) {
const question = await this.prisma.maintenanceQuestion.findUnique({ const question = await this.prisma.inspectionQuestion.findUnique({
where: { id }, where: { id },
}); });
if (!question) { if (!question) {
throw new NotFoundException( throw new NotFoundException(
`Maintenance question with ID ${id} not found`, `Inspection question with ID ${id} not found`,
); );
} }
return question; return question;
} }
async getResponsesByMaintenanceId( async getResponsesByInspectionId(
maintenanceId: number, inspectionId: number,
): Promise<MaintenanceResponseDto[]> { ): Promise<InspectionResponseDto[]> {
const maintenance = await this.prisma.maintenance.findUnique({ const inspection = await this.prisma.inspection.findUnique({
where: { id: maintenanceId }, where: { id: inspectionId },
include: { include: {
responses: { responses: {
include: { include: {
...@@ -193,15 +193,15 @@ export class MaintenanceService { ...@@ -193,15 +193,15 @@ export class MaintenanceService {
}, },
}); });
if (!maintenance) { if (!inspection) {
throw new NotFoundException( throw new NotFoundException(
`Maintenance with ID ${maintenanceId} not found`, `Inspection with ID ${inspectionId} not found`,
); );
} }
return maintenance.responses.map((response) => ({ return inspection.responses.map((response) => ({
id: response.id, id: response.id,
response: response.response as unknown as MaintenanceResponseOption, response: response.response as unknown as InspectionResponseOption,
comment: response.comment ?? undefined, comment: response.comment ?? undefined,
question: { question: {
id: response.question.id, id: response.question.id,
...@@ -211,19 +211,19 @@ export class MaintenanceService { ...@@ -211,19 +211,19 @@ export class MaintenanceService {
})); }));
} }
async addResponsesToMaintenance( async addResponsesToInspection(
maintenanceId: number, inspectionId: number,
responses: CreateMaintenanceResponseDto[], responses: CreateInspectionResponseDto[],
userId: number, userId: number,
): Promise<MaintenanceResponseDto[]> { ): Promise<InspectionResponseDto[]> {
// Check if maintenance exists // Check if inspection exists
const maintenance = await this.prisma.maintenance.findUnique({ const inspection = await this.prisma.inspection.findUnique({
where: { id: maintenanceId }, where: { id: inspectionId },
}); });
if (!maintenance) { if (!inspection) {
throw new NotFoundException( throw new NotFoundException(
`Maintenance with ID ${maintenanceId} not found`, `Inspection with ID ${inspectionId} not found`,
); );
} }
...@@ -242,7 +242,7 @@ export class MaintenanceService { ...@@ -242,7 +242,7 @@ export class MaintenanceService {
const createdResponses: ResponseWithQuestion[] = []; const createdResponses: ResponseWithQuestion[] = [];
for (const response of responses) { for (const response of responses) {
// Check if question exists // Check if question exists
const question = await this.prisma.maintenanceQuestion.findUnique({ const question = await this.prisma.inspectionQuestion.findUnique({
where: { id: response.questionId }, where: { id: response.questionId },
}); });
...@@ -253,9 +253,9 @@ export class MaintenanceService { ...@@ -253,9 +253,9 @@ export class MaintenanceService {
} }
// Check if a response already exists for this question // Check if a response already exists for this question
const existingResponse = await this.prisma.maintenanceResponse.findFirst({ const existingResponse = await this.prisma.inspectionResponse.findFirst({
where: { where: {
maintenanceId, inspectionId,
questionId: response.questionId, questionId: response.questionId,
}, },
}); });
...@@ -263,7 +263,7 @@ export class MaintenanceService { ...@@ -263,7 +263,7 @@ export class MaintenanceService {
let result; let result;
if (existingResponse) { if (existingResponse) {
// Update existing response // Update existing response
result = await this.prisma.maintenanceResponse.update({ result = await this.prisma.inspectionResponse.update({
where: { id: existingResponse.id }, where: { id: existingResponse.id },
data: { data: {
response: response.response, response: response.response,
...@@ -275,12 +275,12 @@ export class MaintenanceService { ...@@ -275,12 +275,12 @@ export class MaintenanceService {
}); });
} else { } else {
// Create new response // Create new response
result = await this.prisma.maintenanceResponse.create({ result = await this.prisma.inspectionResponse.create({
data: { data: {
response: response.response, response: response.response,
comment: response.comment, comment: response.comment,
question: { connect: { id: response.questionId } }, question: { connect: { id: response.questionId } },
maintenance: { connect: { id: maintenanceId } }, inspection: { connect: { id: inspectionId } },
}, },
include: { include: {
question: true, question: true,
...@@ -291,9 +291,9 @@ export class MaintenanceService { ...@@ -291,9 +291,9 @@ export class MaintenanceService {
createdResponses.push(result); createdResponses.push(result);
} }
// Update the maintenance record's updatedBy and updatedAt // Update the inspection record's updatedBy and updatedAt
await this.prisma.maintenance.update({ await this.prisma.inspection.update({
where: { id: maintenanceId }, where: { id: inspectionId },
data: { data: {
updatedBy: { connect: { id: userId } }, updatedBy: { connect: { id: userId } },
}, },
...@@ -302,7 +302,7 @@ export class MaintenanceService { ...@@ -302,7 +302,7 @@ export class MaintenanceService {
// Map responses to DTOs // Map responses to DTOs
return createdResponses.map((response) => ({ return createdResponses.map((response) => ({
id: response.id, id: response.id,
response: response.response as unknown as MaintenanceResponseOption, response: response.response as unknown as InspectionResponseOption,
comment: response.comment ?? undefined, comment: response.comment ?? undefined,
question: { question: {
id: response.question.id, id: response.question.id,
...@@ -312,17 +312,17 @@ export class MaintenanceService { ...@@ -312,17 +312,17 @@ export class MaintenanceService {
})); }));
} }
private mapToDto(maintenance: any): MaintenanceDto { private mapToDto(inspection: any): InspectionDto {
return { return {
id: maintenance.id, id: inspection.id,
date: maintenance.date, date: inspection.date,
comment: maintenance.comment, comment: inspection.comment,
siteId: maintenance.siteId, siteId: inspection.siteId,
createdAt: maintenance.createdAt, createdAt: inspection.createdAt,
updatedAt: maintenance.updatedAt, updatedAt: inspection.updatedAt,
responses: maintenance.responses.map((response) => ({ responses: inspection.responses.map((response) => ({
id: response.id, id: response.id,
response: response.response as unknown as MaintenanceResponseOption, response: response.response as unknown as InspectionResponseOption,
comment: response.comment ?? undefined, comment: response.comment ?? undefined,
question: { question: {
id: response.question.id, id: response.question.id,
...@@ -330,7 +330,7 @@ export class MaintenanceService { ...@@ -330,7 +330,7 @@ export class MaintenanceService {
orderIndex: response.question.orderIndex, orderIndex: response.question.orderIndex,
}, },
})), })),
photos: maintenance.photos.map((photo) => ({ photos: inspection.photos.map((photo) => ({
id: photo.id, id: photo.id,
url: photo.url, url: photo.url,
filename: photo.filename, filename: photo.filename,
......
...@@ -6,14 +6,14 @@ const mkdir = promisify(fs.mkdir); ...@@ -6,14 +6,14 @@ const mkdir = promisify(fs.mkdir);
const writeFile = promisify(fs.writeFile); const writeFile = promisify(fs.writeFile);
/** /**
* Saves uploaded maintenance photos to the file system * Saves uploaded inspection photos to the file system
* @param files Array of uploaded files * @param files Array of uploaded files
* @param maintenanceId The ID of the maintenance record * @param inspectionId The ID of the inspection record
* @returns Array of saved file paths * @returns Array of saved file paths
*/ */
export async function saveMaintenancePhotos( export async function saveInspectionPhotos(
files: Express.Multer.File[], files: Express.Multer.File[],
maintenanceId: number, inspectionId: number,
): Promise<string[]> { ): Promise<string[]> {
if (!files || files.length === 0) { if (!files || files.length === 0) {
return []; return [];
...@@ -21,12 +21,12 @@ export async function saveMaintenancePhotos( ...@@ -21,12 +21,12 @@ export async function saveMaintenancePhotos(
const uploadDir = const uploadDir =
process.env.NODE_ENV === 'production' process.env.NODE_ENV === 'production'
? `/home/api-cellnex/public_html/uploads/maintenance/${maintenanceId}` ? `/home/api-cellnex/public_html/uploads/inspection/${inspectionId}`
: path.join( : path.join(
process.cwd(), process.cwd(),
'uploads', 'uploads',
'maintenance', 'inspection',
maintenanceId.toString(), inspectionId.toString(),
); );
// Create directory if it doesn't exist // Create directory if it doesn't exist
...@@ -45,7 +45,7 @@ export async function saveMaintenancePhotos( ...@@ -45,7 +45,7 @@ export async function saveMaintenancePhotos(
try { try {
await writeFile(filePath, file.buffer); await writeFile(filePath, file.buffer);
savedPaths.push(`/uploads/maintenance/${maintenanceId}/${filename}`); 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}`);
......
export * from './create-maintenance.dto';
export * from './find-maintenance.dto';
export * from './maintenance-response.dto';
export * from './maintenance-response-option.enum';
import { Module } from '@nestjs/common';
import { MaintenanceController } from './maintenance.controller';
import { MaintenanceService } from './maintenance.service';
import { PrismaModule } from '../../common/prisma/prisma.module';
@Module({
imports: [PrismaModule],
controllers: [MaintenanceController],
providers: [MaintenanceService],
exports: [MaintenanceService],
})
export class MaintenanceModule {}
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