Commit 08db2e37 by Augusto Fonte

Inspection changes

parent c8c5d592
{
"name": "api-cellnex",
"version": "0.0.1",
"version": "0.0.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "api-cellnex",
"version": "0.0.1",
"version": "0.0.2",
"license": "UNLICENSED",
"dependencies": {
"@nestjs-modules/mailer": "^2.0.2",
......@@ -17,14 +17,16 @@
"@nestjs/passport": "^11.0.5",
"@nestjs/platform-express": "^11.1.0",
"@nestjs/swagger": "^11.1.1",
"@prisma/client": "^6.6.0",
"@prisma/client": "^6.9.0",
"@types/nodemailer": "^6.4.17",
"@types/uuid": "^10.0.0",
"adm-zip": "^0.5.16",
"bcrypt": "^5.1.1",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"csv-parser": "^3.2.0",
"date-fns": "^4.1.0",
"fast-xml-parser": "^5.2.5",
"jimp": "^0.22.12",
"multer": "^1.4.5-lts.2",
"nodemailer": "^6.10.0",
......@@ -4457,9 +4459,9 @@
}
},
"node_modules/@prisma/client": {
"version": "6.6.0",
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.6.0.tgz",
"integrity": "sha512-vfp73YT/BHsWWOAuthKQ/1lBgESSqYqAWZEYyTdGXyFAHpmewwWL2Iz6ErIzkj4aHbuc6/cGSsE6ZY+pBO04Cg==",
"version": "6.9.0",
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.9.0.tgz",
"integrity": "sha512-Gg7j1hwy3SgF1KHrh0PZsYvAaykeR0PaxusnLXydehS96voYCGt1U5zVR31NIouYc63hWzidcrir1a7AIyCsNQ==",
"hasInstallScript": true,
"license": "Apache-2.0",
"engines": {
......@@ -5983,6 +5985,15 @@
"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": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
......@@ -8753,6 +8764,24 @@
],
"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": {
"version": "1.19.1",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
......@@ -15245,6 +15274,18 @@
"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": {
"version": "9.1.1",
"resolved": "https://registry.npmjs.org/strtok3/-/strtok3-9.1.1.tgz",
......
......@@ -29,14 +29,16 @@
"@nestjs/passport": "^11.0.5",
"@nestjs/platform-express": "^11.1.0",
"@nestjs/swagger": "^11.1.1",
"@prisma/client": "^6.6.0",
"@prisma/client": "^6.9.0",
"@types/nodemailer": "^6.4.17",
"@types/uuid": "^10.0.0",
"adm-zip": "^0.5.16",
"bcrypt": "^5.1.1",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"csv-parser": "^3.2.0",
"date-fns": "^4.1.0",
"fast-xml-parser": "^5.2.5",
"jimp": "^0.22.12",
"multer": "^1.4.5-lts.2",
"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() {
});
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()
......
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';
import { JwtAuthGuard } from './modules/auth/guards/jwt-auth.guard';
import { DashboardModule } from './modules/dashboard/dashboard.module';
import { PartnersModule } from './modules/partners/partners.module';
import { MaintenanceModule } from './modules/maintenance/maintenance.module';
import { InspectionModule } from './modules/inspection/inspection.module';
@Module({
imports: [
......@@ -55,7 +55,7 @@ import { MaintenanceModule } from './modules/maintenance/maintenance.module';
CommentsModule,
DashboardModule,
PartnersModule,
MaintenanceModule,
InspectionModule,
],
controllers: [AppController],
providers: [
......
......@@ -39,7 +39,7 @@ async function bootstrap() {
.addTag('users', 'User management endpoints')
.addTag('sites', 'Site management endpoints')
.addTag('candidates', 'Candidate management endpoints')
.addTag('maintenance', 'Site maintenance management endpoints')
.addTag('inspection', 'Site inspection management endpoints')
.addBearerAuth(
{
type: 'http',
......
......@@ -6,12 +6,12 @@ import {
ValidateNested,
} from 'class-validator';
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';
export class CreateMaintenanceResponseDto {
export class CreateInspectionResponseDto {
@ApiProperty({
description: 'The ID of the maintenance question being answered',
description: 'The ID of the inspection question being answered',
example: 1,
type: Number,
})
......@@ -19,13 +19,13 @@ export class CreateMaintenanceResponseDto {
questionId: number;
@ApiProperty({
description: 'The response to the maintenance question',
enum: MaintenanceResponseOption,
example: MaintenanceResponseOption.YES,
enumName: 'MaintenanceResponseOption',
description: 'The response to the inspection question',
enum: InspectionResponseOption,
example: InspectionResponseOption.YES,
enumName: 'InspectionResponseOption',
})
@IsString()
response: MaintenanceResponseOption;
response: InspectionResponseOption;
@ApiPropertyOptional({
description:
......@@ -39,9 +39,9 @@ export class CreateMaintenanceResponseDto {
comment?: string;
}
export class CreateMaintenanceDto {
export class CreateInspectionDto {
@ApiProperty({
description: 'Date when the maintenance was performed',
description: 'Date when the inspection was performed',
example: '2025-05-21T13:00:00.000Z',
type: String,
})
......@@ -49,7 +49,7 @@ export class CreateMaintenanceDto {
date: string;
@ApiProperty({
description: 'ID of the site where the maintenance was performed',
description: 'ID of the site where the inspection was performed',
example: 1,
type: Number,
})
......@@ -57,8 +57,8 @@ export class CreateMaintenanceDto {
siteId: number;
@ApiPropertyOptional({
description: 'Optional general comment about the maintenance',
example: 'Regular annual maintenance. Site is in good overall condition.',
description: 'Optional general comment about the inspection',
example: 'Regular annual inspection. Site is in good overall condition.',
type: String,
})
@IsString()
......@@ -66,8 +66,8 @@ export class CreateMaintenanceDto {
comment?: string;
@ApiProperty({
description: 'Responses to maintenance questions',
type: [CreateMaintenanceResponseDto],
description: 'Responses to inspection questions',
type: [CreateInspectionResponseDto],
example: [
{
questionId: 1,
......@@ -87,6 +87,6 @@ export class CreateMaintenanceDto {
],
})
@ValidateNested({ each: true })
@Type(() => CreateMaintenanceResponseDto)
responses: CreateMaintenanceResponseDto[];
@Type(() => CreateInspectionResponseDto)
responses: CreateInspectionResponseDto[];
}
import { IsDateString, IsInt, IsOptional } from 'class-validator';
import { ApiPropertyOptional } from '@nestjs/swagger';
export class FindMaintenanceDto {
export class FindInspectionDto {
@ApiPropertyOptional({
description: 'Filter maintenance records by site ID',
description: 'Filter inspection records by site ID',
example: 1,
type: Number,
})
......@@ -13,7 +13,7 @@ export class FindMaintenanceDto {
@ApiPropertyOptional({
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',
type: String,
})
......@@ -23,7 +23,7 @@ export class FindMaintenanceDto {
@ApiPropertyOptional({
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',
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
* NO - Item needs attention/repair
* NA - Not applicable for this site
*/
export enum MaintenanceResponseOption {
export enum InspectionResponseOption {
YES = 'YES',
NO = 'NO',
NA = 'NA',
......
import { MaintenanceResponseOption } from './maintenance-response-option.enum';
import { InspectionResponseOption } from './inspection-response-option.enum';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
export class MaintenanceQuestionDto {
export class InspectionQuestionDto {
@ApiProperty({
description: 'Unique identifier of the maintenance question',
description: 'Unique identifier of the inspection question',
example: 1,
type: Number,
})
id: number;
@ApiProperty({
description: 'Text of the maintenance question',
description: 'Text of the inspection question',
example: 'Site access condition',
type: String,
})
......@@ -24,9 +24,9 @@ export class MaintenanceQuestionDto {
orderIndex: number;
}
export class MaintenanceResponseDto {
export class InspectionResponseDto {
@ApiProperty({
description: 'Unique identifier of the maintenance response',
description: 'Unique identifier of the inspection response',
example: 1,
type: Number,
})
......@@ -34,11 +34,11 @@ export class MaintenanceResponseDto {
@ApiProperty({
description: 'Response option selected for the question',
enum: MaintenanceResponseOption,
example: MaintenanceResponseOption.YES,
enumName: 'MaintenanceResponseOption',
enum: InspectionResponseOption,
example: InspectionResponseOption.YES,
enumName: 'InspectionResponseOption',
})
response: MaintenanceResponseOption;
response: InspectionResponseOption;
@ApiPropertyOptional({
description: 'Optional comment providing additional details',
......@@ -49,14 +49,14 @@ export class MaintenanceResponseDto {
@ApiProperty({
description: 'The question this response answers',
type: MaintenanceQuestionDto,
type: InspectionQuestionDto,
})
question: MaintenanceQuestionDto;
question: InspectionQuestionDto;
}
export class MaintenancePhotoDto {
export class InspectionPhotoDto {
@ApiProperty({
description: 'Unique identifier of the maintenance photo',
description: 'Unique identifier of the inspection photo',
example: 1,
type: Number,
})
......@@ -64,7 +64,7 @@ export class MaintenancePhotoDto {
@ApiProperty({
description: 'URL to access the photo',
example: '/uploads/maintenance/1/photo1.jpg',
example: '/uploads/inspection/1/photo1.jpg',
type: String,
})
url: string;
......@@ -77,30 +77,30 @@ export class MaintenancePhotoDto {
filename: string;
}
export class MaintenanceDto {
export class InspectionDto {
@ApiProperty({
description: 'Unique identifier of the maintenance record',
description: 'Unique identifier of the inspection record',
example: 1,
type: Number,
})
id: number;
@ApiProperty({
description: 'Date when the maintenance was performed',
description: 'Date when the inspection was performed',
example: '2025-05-21T13:00:00.000Z',
type: Date,
})
date: Date;
@ApiPropertyOptional({
description: 'Optional general comment about the maintenance',
example: 'Annual preventive maintenance completed with minor issues noted',
description: 'Optional general comment about the inspection',
example: 'Annual preventive inspection completed with minor issues noted',
type: String,
})
comment?: string;
@ApiProperty({
description: 'ID of the site where maintenance was performed',
description: 'ID of the site where inspection was performed',
example: 1,
type: Number,
})
......@@ -121,16 +121,16 @@ export class MaintenanceDto {
updatedAt: Date;
@ApiProperty({
description: 'Responses to maintenance questions',
type: [MaintenanceResponseDto],
description: 'Responses to inspection questions',
type: [InspectionResponseDto],
isArray: true,
})
responses: MaintenanceResponseDto[];
responses: InspectionResponseDto[];
@ApiProperty({
description: 'Photos attached to the maintenance record',
type: [MaintenancePhotoDto],
description: 'Photos attached to the inspection record',
type: [InspectionPhotoDto],
isArray: true,
})
photos: MaintenancePhotoDto[];
photos: InspectionPhotoDto[];
}
......@@ -16,12 +16,12 @@ import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
import { RolesGuard } from '../auth/guards/roles.guard';
import { Roles } from '../auth/decorators/roles.decorator';
import { Role } from '@prisma/client';
import { MaintenanceService } from './maintenance.service';
import { InspectionService } from './inspection.service';
import {
CreateMaintenanceDto,
CreateMaintenanceResponseDto,
} from './dto/create-maintenance.dto';
import { FindMaintenanceDto } from './dto/find-maintenance.dto';
CreateInspectionDto,
CreateInspectionResponseDto,
} from './dto/create-inspection.dto';
import { FindInspectionDto } from './dto/find-inspection.dto';
import { multerConfig } from '../../common/multer/multer.config';
import {
ApiTags,
......@@ -35,30 +35,30 @@ import {
getSchemaPath,
} from '@nestjs/swagger';
import {
MaintenanceDto,
MaintenanceQuestionDto,
MaintenanceResponseDto,
} from './dto/maintenance-response.dto';
InspectionDto,
InspectionQuestionDto,
InspectionResponseDto,
} from './dto/inspection-response.dto';
@ApiTags('maintenance')
@Controller('maintenance')
@ApiTags('inspection')
@Controller('inspection')
@ApiBearerAuth('access-token')
export class MaintenanceController {
constructor(private readonly maintenanceService: MaintenanceService) {}
export class InspectionController {
constructor(private readonly inspectionService: InspectionService) {}
@Post()
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles(Role.ADMIN, Role.MANAGER, Role.OPERATOR, Role.PARTNER)
@UseInterceptors(FilesInterceptor('photos', 10, multerConfig))
@ApiOperation({
summary: 'Create a new maintenance record',
summary: 'Create a new inspection record',
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({
status: 201,
description: 'The maintenance record has been successfully created.',
type: MaintenanceDto,
description: 'The inspection record has been successfully created.',
type: InspectionDto,
})
@ApiResponse({ status: 400, description: 'Invalid input data.' })
@ApiResponse({ status: 401, description: 'Unauthorized.' })
......@@ -70,7 +70,7 @@ export class MaintenanceController {
@ApiBearerAuth('access-token')
@ApiConsumes('multipart/form-data')
@ApiBody({
description: 'Maintenance data with optional photos',
description: 'Inspection data with optional photos',
schema: {
type: 'object',
required: ['date', 'siteId', 'responses'],
......@@ -79,25 +79,25 @@ export class MaintenanceController {
type: 'string',
format: 'date-time',
example: '2025-05-21T13:00:00.000Z',
description: 'Date when the maintenance was performed',
description: 'Date when the inspection was performed',
},
siteId: {
type: 'integer',
example: 1,
description: 'ID of the site where the maintenance was performed',
description: 'ID of the site where the inspection was performed',
},
comment: {
type: 'string',
example:
'Regular annual maintenance. Site is in good overall condition.',
description: 'Optional general comment about the maintenance',
'Regular annual inspection. Site is in good overall condition.',
description: 'Optional general comment about the inspection',
},
responses: {
type: 'array',
items: {
$ref: getSchemaPath(CreateMaintenanceDto),
$ref: getSchemaPath(CreateInspectionDto),
},
description: 'Responses to maintenance questions',
description: 'Responses to inspection questions',
},
photos: {
type: 'array',
......@@ -111,13 +111,13 @@ export class MaintenanceController {
},
},
})
async createMaintenance(
@Body() createMaintenanceDto: CreateMaintenanceDto,
async createInspection(
@Body() createInspectionDto: CreateInspectionDto,
@UploadedFiles() files: Express.Multer.File[],
@Req() req,
) {
return this.maintenanceService.createMaintenance(
createMaintenanceDto,
return this.inspectionService.createInspection(
createInspectionDto,
req.user.id,
files,
);
......@@ -126,14 +126,14 @@ export class MaintenanceController {
@Get()
@UseGuards(JwtAuthGuard)
@ApiOperation({
summary: 'Find all maintenance records with optional filters',
summary: 'Find all inspection records with optional filters',
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({
status: 200,
description: 'List of maintenance records.',
type: [MaintenanceDto],
description: 'List of inspection records.',
type: [InspectionDto],
})
@ApiResponse({ status: 401, description: 'Unauthorized.' })
@ApiQuery({
......@@ -157,110 +157,110 @@ export class MaintenanceController {
description: 'Filter by end date (inclusive)',
example: '2025-12-31T23:59:59.999Z',
})
async findAllMaintenance(@Query() findMaintenanceDto: FindMaintenanceDto) {
return this.maintenanceService.findAllMaintenance(findMaintenanceDto);
async findAllInspection(@Query() findInspectionDto: FindInspectionDto) {
return this.inspectionService.findAllInspection(findInspectionDto);
}
@Get('questions')
@UseGuards(JwtAuthGuard)
@ApiOperation({
summary: 'Get all maintenance questions',
summary: 'Get all inspection questions',
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({
status: 200,
description: 'List of maintenance questions.',
type: [MaintenanceQuestionDto],
description: 'List of inspection questions.',
type: [InspectionQuestionDto],
})
@ApiResponse({ status: 401, description: 'Unauthorized.' })
async getMaintenanceQuestions() {
return this.maintenanceService.getMaintenanceQuestions();
async getInspectionQuestions() {
return this.inspectionService.getInspectionQuestions();
}
@Get('questions/:id')
@UseGuards(JwtAuthGuard)
@ApiOperation({
summary: 'Get a specific maintenance question by ID',
description: 'Retrieves a specific maintenance question by its ID.',
summary: 'Get a specific inspection question by ID',
description: 'Retrieves a specific inspection question by its ID.',
})
@ApiResponse({
status: 200,
description: 'The maintenance question.',
type: MaintenanceQuestionDto,
description: 'The inspection question.',
type: InspectionQuestionDto,
})
@ApiResponse({ status: 401, description: 'Unauthorized.' })
@ApiResponse({ status: 404, description: 'Question not found.' })
@ApiParam({
name: 'id',
type: Number,
description: 'ID of the maintenance question to retrieve',
description: 'ID of the inspection question to retrieve',
example: 1,
})
async getMaintenanceQuestionById(@Param('id', ParseIntPipe) id: number) {
return this.maintenanceService.getMaintenanceQuestionById(id);
async getInspectionQuestionById(@Param('id', ParseIntPipe) id: number) {
return this.inspectionService.getInspectionQuestionById(id);
}
@Get(':id')
@UseGuards(JwtAuthGuard)
@ApiOperation({
summary: 'Find maintenance record by ID',
summary: 'Find inspection record by ID',
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({
status: 200,
description: 'The maintenance record.',
type: MaintenanceDto,
description: 'The inspection record.',
type: InspectionDto,
})
@ApiResponse({ status: 401, description: 'Unauthorized.' })
@ApiResponse({ status: 404, description: 'Maintenance record not found.' })
@ApiResponse({ status: 404, description: 'Inspection record not found.' })
@ApiParam({
name: 'id',
type: Number,
description: 'ID of the maintenance record to retrieve',
description: 'ID of the inspection record to retrieve',
example: 1,
})
async findMaintenanceById(@Param('id', ParseIntPipe) id: number) {
return this.maintenanceService.findMaintenanceById(id);
async findInspectionById(@Param('id', ParseIntPipe) id: number) {
return this.inspectionService.findInspectionById(id);
}
@Get(':id/responses')
@UseGuards(JwtAuthGuard)
@ApiOperation({
summary: 'Get responses for a specific maintenance record',
summary: 'Get responses for a specific inspection record',
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({
status: 200,
description: 'List of maintenance responses.',
type: [MaintenanceResponseDto],
description: 'List of inspection responses.',
type: [InspectionResponseDto],
})
@ApiResponse({ status: 401, description: 'Unauthorized.' })
@ApiResponse({ status: 404, description: 'Maintenance record not found.' })
@ApiResponse({ status: 404, description: 'Inspection record not found.' })
@ApiParam({
name: 'id',
type: Number,
description: 'ID of the maintenance record',
description: 'ID of the inspection record',
example: 1,
})
async getResponsesByMaintenanceId(@Param('id', ParseIntPipe) id: number) {
return this.maintenanceService.getResponsesByMaintenanceId(id);
async getResponsesByInspectionId(@Param('id', ParseIntPipe) id: number) {
return this.inspectionService.getResponsesByInspectionId(id);
}
@Post(':id/responses')
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles(Role.ADMIN, Role.MANAGER, Role.OPERATOR, Role.PARTNER)
@ApiOperation({
summary: 'Add responses to an existing maintenance record',
summary: 'Add responses to an existing inspection record',
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({
status: 201,
description: 'The responses have been successfully added.',
type: [MaintenanceResponseDto],
type: [InspectionResponseDto],
})
@ApiResponse({ status: 400, description: 'Invalid input data.' })
@ApiResponse({ status: 401, description: 'Unauthorized.' })
......@@ -270,24 +270,24 @@ export class MaintenanceController {
})
@ApiResponse({
status: 404,
description: 'Maintenance record or question not found.',
description: 'Inspection record or question not found.',
})
@ApiParam({
name: 'id',
type: Number,
description: 'ID of the maintenance record',
description: 'ID of the inspection record',
example: 1,
})
@ApiBody({
description: 'Array of maintenance responses to add',
type: [CreateMaintenanceResponseDto],
description: 'Array of inspection responses to add',
type: [CreateInspectionResponseDto],
})
async addResponsesToMaintenance(
async addResponsesToInspection(
@Param('id', ParseIntPipe) id: number,
@Body() responses: CreateMaintenanceResponseDto[],
@Body() responses: CreateInspectionResponseDto[],
@Req() req,
) {
return this.maintenanceService.addResponsesToMaintenance(
return this.inspectionService.addResponsesToInspection(
id,
responses,
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
*/
/**
* 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',
siteId: 1,
comment: 'Regular annual maintenance. Site is in good overall condition.',
comment: 'Regular annual inspection. Site is in good overall condition.',
responses: [
{
questionId: 1,
......@@ -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,
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,
createdAt: '2025-05-21T13:15:30.000Z',
updatedAt: '2025-05-21T13:15:30.000Z',
......@@ -106,26 +106,26 @@ export const maintenanceResponseExample = {
photos: [
{
id: 1,
url: '/uploads/maintenance/1/entrance.jpg',
url: '/uploads/inspection/1/entrance.jpg',
filename: 'entrance.jpg',
},
{
id: 2,
url: '/uploads/maintenance/1/damaged_fence.jpg',
url: '/uploads/inspection/1/damaged_fence.jpg',
filename: 'damaged_fence.jpg',
},
{
id: 3,
url: '/uploads/maintenance/1/equipment.jpg',
url: '/uploads/inspection/1/equipment.jpg',
filename: 'equipment.jpg',
},
],
};
/**
* Example response for maintenance questions
* Example response for inspection questions
*/
export const maintenanceQuestionsExample = [
export const inspectionQuestionsExample = [
{
id: 1,
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 { PrismaService } from '../../common/prisma/prisma.service';
import {
CreateMaintenanceDto,
CreateMaintenanceResponseDto,
} from './dto/create-maintenance.dto';
import { FindMaintenanceDto } from './dto/find-maintenance.dto';
CreateInspectionDto,
CreateInspectionResponseDto,
} from './dto/create-inspection.dto';
import { FindInspectionDto } from './dto/find-inspection.dto';
import {
MaintenanceDto,
MaintenanceResponseDto,
MaintenancePhotoDto,
} from './dto/maintenance-response.dto';
import { MaintenanceResponseOption } from './dto/maintenance-response-option.enum';
import { saveMaintenancePhotos } from './maintenance.utils';
InspectionDto,
InspectionResponseDto,
InspectionPhotoDto,
} from './dto/inspection-response.dto';
import { InspectionResponseOption } from './dto/inspection-response-option.enum';
import { saveInspectionPhotos } from './inspection.utils';
@Injectable()
export class MaintenanceService {
constructor(private prisma: PrismaService) { }
export class InspectionService {
constructor(private prisma: PrismaService) {}
async createMaintenance(
dto: CreateMaintenanceDto,
async createInspection(
dto: CreateInspectionDto,
userId: number,
files?: Express.Multer.File[],
): Promise<MaintenanceDto> {
): Promise<InspectionDto> {
// Check if site exists
const site = await this.prisma.site.findUnique({
where: { id: dto.siteId },
......@@ -31,8 +31,8 @@ export class MaintenanceService {
throw new NotFoundException(`Site with ID ${dto.siteId} not found`);
}
// Create maintenance record
const maintenance = await this.prisma.maintenance.create({
// Create inspection record
const inspection = await this.prisma.inspection.create({
data: {
date: new Date(dto.date),
comment: dto.comment,
......@@ -60,26 +60,26 @@ export class MaintenanceService {
if (files && files.length > 0) {
try {
// Save files to disk
const filePaths = await saveMaintenancePhotos(files, maintenance.id);
const filePaths = await saveInspectionPhotos(files, inspection.id);
// Create photo records in database
const photoPromises = files.map((file, index) => {
return this.prisma.maintenancePhoto.create({
return this.prisma.inspectionPhoto.create({
data: {
filename: file.originalname,
mimeType: file.mimetype,
size: file.size,
url: filePaths[index],
maintenance: { connect: { id: maintenance.id } },
inspection: { connect: { id: inspection.id } },
},
});
});
await Promise.all(photoPromises);
// Fetch the updated maintenance record with photos
const updatedMaintenance = await this.prisma.maintenance.findUnique({
where: { id: maintenance.id },
// Fetch the updated inspection record with photos
const updatedInspection = await this.prisma.inspection.findUnique({
where: { id: inspection.id },
include: {
responses: {
include: {
......@@ -90,17 +90,17 @@ export class MaintenanceService {
},
});
return this.mapToDto(updatedMaintenance);
return this.mapToDto(updatedInspection);
} catch (error) {
console.error('Error processing maintenance photos:', error);
console.error('Error processing inspection photos:', 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 = {};
if (dto.siteId) {
......@@ -119,7 +119,7 @@ export class MaintenanceService {
}
}
const maintenances = await this.prisma.maintenance.findMany({
const inspections = await this.prisma.inspection.findMany({
where: filter,
include: {
responses: {
......@@ -134,11 +134,11 @@ export class MaintenanceService {
},
});
return maintenances.map(this.mapToDto);
return inspections.map(this.mapToDto);
}
async findMaintenanceById(id: number): Promise<MaintenanceDto> {
const maintenance = await this.prisma.maintenance.findUnique({
async findInspectionById(id: number): Promise<InspectionDto> {
const inspection = await this.prisma.inspection.findUnique({
where: { id },
include: {
responses: {
......@@ -150,40 +150,40 @@ export class MaintenanceService {
},
});
if (!maintenance) {
throw new NotFoundException(`Maintenance with ID ${id} not found`);
if (!inspection) {
throw new NotFoundException(`Inspection with ID ${id} not found`);
}
return this.mapToDto(maintenance);
return this.mapToDto(inspection);
}
async getMaintenanceQuestions() {
return this.prisma.maintenanceQuestion.findMany({
async getInspectionQuestions() {
return this.prisma.inspectionQuestion.findMany({
orderBy: {
orderIndex: 'asc',
},
});
}
async getMaintenanceQuestionById(id: number) {
const question = await this.prisma.maintenanceQuestion.findUnique({
async getInspectionQuestionById(id: number) {
const question = await this.prisma.inspectionQuestion.findUnique({
where: { id },
});
if (!question) {
throw new NotFoundException(
`Maintenance question with ID ${id} not found`,
`Inspection question with ID ${id} not found`,
);
}
return question;
}
async getResponsesByMaintenanceId(
maintenanceId: number,
): Promise<MaintenanceResponseDto[]> {
const maintenance = await this.prisma.maintenance.findUnique({
where: { id: maintenanceId },
async getResponsesByInspectionId(
inspectionId: number,
): Promise<InspectionResponseDto[]> {
const inspection = await this.prisma.inspection.findUnique({
where: { id: inspectionId },
include: {
responses: {
include: {
......@@ -193,15 +193,15 @@ export class MaintenanceService {
},
});
if (!maintenance) {
if (!inspection) {
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,
response: response.response as unknown as MaintenanceResponseOption,
response: response.response as unknown as InspectionResponseOption,
comment: response.comment ?? undefined,
question: {
id: response.question.id,
......@@ -211,19 +211,19 @@ export class MaintenanceService {
}));
}
async addResponsesToMaintenance(
maintenanceId: number,
responses: CreateMaintenanceResponseDto[],
async addResponsesToInspection(
inspectionId: number,
responses: CreateInspectionResponseDto[],
userId: number,
): Promise<MaintenanceResponseDto[]> {
// Check if maintenance exists
const maintenance = await this.prisma.maintenance.findUnique({
where: { id: maintenanceId },
): Promise<InspectionResponseDto[]> {
// Check if inspection exists
const inspection = await this.prisma.inspection.findUnique({
where: { id: inspectionId },
});
if (!maintenance) {
if (!inspection) {
throw new NotFoundException(
`Maintenance with ID ${maintenanceId} not found`,
`Inspection with ID ${inspectionId} not found`,
);
}
......@@ -242,7 +242,7 @@ export class MaintenanceService {
const createdResponses: ResponseWithQuestion[] = [];
for (const response of responses) {
// Check if question exists
const question = await this.prisma.maintenanceQuestion.findUnique({
const question = await this.prisma.inspectionQuestion.findUnique({
where: { id: response.questionId },
});
......@@ -253,9 +253,9 @@ export class MaintenanceService {
}
// Check if a response already exists for this question
const existingResponse = await this.prisma.maintenanceResponse.findFirst({
const existingResponse = await this.prisma.inspectionResponse.findFirst({
where: {
maintenanceId,
inspectionId,
questionId: response.questionId,
},
});
......@@ -263,7 +263,7 @@ export class MaintenanceService {
let result;
if (existingResponse) {
// Update existing response
result = await this.prisma.maintenanceResponse.update({
result = await this.prisma.inspectionResponse.update({
where: { id: existingResponse.id },
data: {
response: response.response,
......@@ -275,12 +275,12 @@ export class MaintenanceService {
});
} else {
// Create new response
result = await this.prisma.maintenanceResponse.create({
result = await this.prisma.inspectionResponse.create({
data: {
response: response.response,
comment: response.comment,
question: { connect: { id: response.questionId } },
maintenance: { connect: { id: maintenanceId } },
inspection: { connect: { id: inspectionId } },
},
include: {
question: true,
......@@ -291,9 +291,9 @@ export class MaintenanceService {
createdResponses.push(result);
}
// Update the maintenance record's updatedBy and updatedAt
await this.prisma.maintenance.update({
where: { id: maintenanceId },
// Update the inspection record's updatedBy and updatedAt
await this.prisma.inspection.update({
where: { id: inspectionId },
data: {
updatedBy: { connect: { id: userId } },
},
......@@ -302,7 +302,7 @@ export class MaintenanceService {
// Map responses to DTOs
return createdResponses.map((response) => ({
id: response.id,
response: response.response as unknown as MaintenanceResponseOption,
response: response.response as unknown as InspectionResponseOption,
comment: response.comment ?? undefined,
question: {
id: response.question.id,
......@@ -312,17 +312,17 @@ export class MaintenanceService {
}));
}
private mapToDto(maintenance: any): MaintenanceDto {
private mapToDto(inspection: any): InspectionDto {
return {
id: maintenance.id,
date: maintenance.date,
comment: maintenance.comment,
siteId: maintenance.siteId,
createdAt: maintenance.createdAt,
updatedAt: maintenance.updatedAt,
responses: maintenance.responses.map((response) => ({
id: inspection.id,
date: inspection.date,
comment: inspection.comment,
siteId: inspection.siteId,
createdAt: inspection.createdAt,
updatedAt: inspection.updatedAt,
responses: inspection.responses.map((response) => ({
id: response.id,
response: response.response as unknown as MaintenanceResponseOption,
response: response.response as unknown as InspectionResponseOption,
comment: response.comment ?? undefined,
question: {
id: response.question.id,
......@@ -330,7 +330,7 @@ export class MaintenanceService {
orderIndex: response.question.orderIndex,
},
})),
photos: maintenance.photos.map((photo) => ({
photos: inspection.photos.map((photo) => ({
id: photo.id,
url: photo.url,
filename: photo.filename,
......
......@@ -6,14 +6,14 @@ const mkdir = promisify(fs.mkdir);
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 maintenanceId The ID of the maintenance record
* @param inspectionId The ID of the inspection record
* @returns Array of saved file paths
*/
export async function saveMaintenancePhotos(
export async function saveInspectionPhotos(
files: Express.Multer.File[],
maintenanceId: number,
inspectionId: number,
): Promise<string[]> {
if (!files || files.length === 0) {
return [];
......@@ -21,12 +21,12 @@ export async function saveMaintenancePhotos(
const uploadDir =
process.env.NODE_ENV === 'production'
? `/home/api-cellnex/public_html/uploads/maintenance/${maintenanceId}`
? `/home/api-cellnex/public_html/uploads/inspection/${inspectionId}`
: path.join(
process.cwd(),
'uploads',
'maintenance',
maintenanceId.toString(),
'inspection',
inspectionId.toString(),
);
// Create directory if it doesn't exist
......@@ -45,7 +45,7 @@ export async function saveMaintenancePhotos(
try {
await writeFile(filePath, file.buffer);
savedPaths.push(`/uploads/maintenance/${maintenanceId}/${filename}`);
savedPaths.push(`/uploads/inspection/${inspectionId}/${filename}`);
} catch (error) {
console.error(`Error saving file ${filename}:`, error);
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