From ae1b8603d00aeffe0e7cccccefa07ec444b630ae Mon Sep 17 00:00:00 2001 From: Daniel-Alvarenga Date: Sun, 4 Aug 2024 20:19:46 -0300 Subject: [PATCH 1/3] Notas and materias tables migration --- .../migration.sql | 43 +++++++++++++++++ server/prisma/schema.prisma | 46 +++++++++++++++++-- 2 files changed, 85 insertions(+), 4 deletions(-) create mode 100644 server/prisma/migrations/20240804231659_add_notas_and_materias_tables/migration.sql diff --git a/server/prisma/migrations/20240804231659_add_notas_and_materias_tables/migration.sql b/server/prisma/migrations/20240804231659_add_notas_and_materias_tables/migration.sql new file mode 100644 index 0000000..1e57e2a --- /dev/null +++ b/server/prisma/migrations/20240804231659_add_notas_and_materias_tables/migration.sql @@ -0,0 +1,43 @@ +-- CreateTable +CREATE TABLE `materias` ( + `id` VARCHAR(191) NOT NULL, + `name` VARCHAR(191) NOT NULL, + `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `updatedAt` DATETIME(3) NOT NULL, + + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `cursos_materias` ( + `cursoId` VARCHAR(191) NOT NULL, + `materiaId` VARCHAR(191) NOT NULL, + + PRIMARY KEY (`cursoId`, `materiaId`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `notas` ( + `id` VARCHAR(191) NOT NULL, + `alunoId` VARCHAR(191) NOT NULL, + `materiaId` VARCHAR(191) NOT NULL, + `bimestre` INTEGER NOT NULL, + `ano` INTEGER NOT NULL, + `mencao` ENUM('I', 'R', 'B', 'MB') NOT NULL, + `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `updatedAt` DATETIME(3) NOT NULL, + + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- AddForeignKey +ALTER TABLE `cursos_materias` ADD CONSTRAINT `cursos_materias_cursoId_fkey` FOREIGN KEY (`cursoId`) REFERENCES `cursos`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `cursos_materias` ADD CONSTRAINT `cursos_materias_materiaId_fkey` FOREIGN KEY (`materiaId`) REFERENCES `materias`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `notas` ADD CONSTRAINT `notas_alunoId_fkey` FOREIGN KEY (`alunoId`) REFERENCES `alunos`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `notas` ADD CONSTRAINT `notas_materiaId_fkey` FOREIGN KEY (`materiaId`) REFERENCES `materias`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/server/prisma/schema.prisma b/server/prisma/schema.prisma index 96fe3d6..8d0b010 100644 --- a/server/prisma/schema.prisma +++ b/server/prisma/schema.prisma @@ -23,6 +23,7 @@ model Aluno { rm String? @unique tentativasRestantes Int @default(5) validated Boolean @default(false) + notas Nota[] turmas AlunoTurma[] boletins Boletim[] atividades AlunoAtividade[] @@ -91,16 +92,17 @@ model Admin { } model Curso { - id String @id @default(uuid()) + id String @id @default(uuid()) name String turno Turno duracao String - coordenador Coordenador @relation(fields: [coordenadorId], references: [id]) + coordenador Coordenador @relation(fields: [coordenadorId], references: [id]) coordenadorId String + materias CursoMateria[] turma Turma[] vaga Vaga[] - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt @@map("cursos") } @@ -305,3 +307,39 @@ model Mensagem { @@map("mensagens") } + +model Materia { + id String @id @default(uuid()) + name String + cursos CursoMateria[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + Nota Nota[] + + @@map("materias") +} + +model CursoMateria { + cursoId String @default(uuid()) + materiaId String @default(uuid()) + curso Curso @relation(fields: [cursoId], references: [id]) + materia Materia @relation(fields: [materiaId], references: [id]) + + @@id([cursoId, materiaId]) + @@map("cursos_materias") +} + +model Nota { + id String @id @default(uuid()) + alunoId String + materiaId String + bimestre Int + ano Int + mencao Mencao + aluno Aluno @relation(fields: [alunoId], references: [id]) + materia Materia @relation(fields: [materiaId], references: [id]) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@map("notas") +} \ No newline at end of file From c63a40878816b239131b96f709448ba52c192e56 Mon Sep 17 00:00:00 2001 From: Daniel-Alvarenga Date: Sun, 4 Aug 2024 21:45:24 -0300 Subject: [PATCH 2/3] Update send boletim --- client/src/services/api/aluno.js | 3 +- .../services/aluno/SendBoletimUseCase.ts | 68 +++++++++++++++++-- 2 files changed, 65 insertions(+), 6 deletions(-) diff --git a/client/src/services/api/aluno.js b/client/src/services/api/aluno.js index 44baef7..a3a00cf 100644 --- a/client/src/services/api/aluno.js +++ b/client/src/services/api/aluno.js @@ -390,7 +390,6 @@ export const sendBoletim = async (file, token) => { }); return response; } catch (error) { - console.log("ARQUIVO: " + file); - // return error.response.data; + return error.response.data; } } \ No newline at end of file diff --git a/server/src/modules/services/aluno/SendBoletimUseCase.ts b/server/src/modules/services/aluno/SendBoletimUseCase.ts index 70678dc..35bdaca 100644 --- a/server/src/modules/services/aluno/SendBoletimUseCase.ts +++ b/server/src/modules/services/aluno/SendBoletimUseCase.ts @@ -30,7 +30,22 @@ export class SendBoletimUseCase { const boletimPath = path.resolve(boletim.path); const boletimBuffer = fs.readFileSync(boletimPath); - const link = await this.extractAuthUrlFromPdf(boletimBuffer); + const { link, info } = await this.extractInfoFromPdf(boletimBuffer); + + // console.log(`Ano Letivo / Semestre: ${info.anoLetivoSemestre}`); + // console.log(`RM: ${info.rm}`); + // console.log(`Nome do aluno: ${info.nomeAluno}`); + // console.log(`Curso: ${info.curso}`); + // console.log(`Modalidade: ${info.modalidade}`); + // console.log(`Série / Módulo: ${info.serieModulo}`); + // console.log(`Turno: ${info.turno}`); + + if (info.rm != aluno.rm){ + throw new AppError ("Boletim inválido: RM não corresponde"); + } + if (info.nomeAluno != aluno.name){ + throw new AppError ("Boletim inválido: Nome não corresponde"); + } const bucketName = 'boot'; const objectName = `aluno/${aluno.rm}/boletins/${path.basename(boletim.path)}`; @@ -54,15 +69,60 @@ export class SendBoletimUseCase { } } - async extractAuthUrlFromPdf(buffer: Buffer): Promise { + async extractInfoFromPdf(buffer: Buffer): Promise<{ link: string, info: any }> { const data = await pdf(buffer); const text = data.text; const urlMatch = text.match(/https:\/\/nsa\.cps\.sp\.gov\.br\?a=[a-z0-9-]+/i); if (!urlMatch) { - throw new AppError('URL de autenticação não encontrado no boletim.'); + throw new AppError('Boletim inválido: URL de autenticação não encontrado no boletim.'); + } + + const anoLetivoSemestreMatch = text.match(/Ano Letivo \/ Semestre: (\d{4})/); + if (!anoLetivoSemestreMatch) { + throw new AppError('Boletim inválido: Ano Letivo / Semestre não encontrado.'); + } + + const rmMatch = text.match(/RM: (\d+)/); + if (!rmMatch) { + throw new AppError('Boletim inválido: RM não encontrado.'); + } + + const nomeAlunoMatch = text.match(/Nome do aluno: ([^\n]+)/); + if (!nomeAlunoMatch) { + throw new AppError('Boletim inválido: Nome do aluno não encontrado.'); } - return urlMatch[0]; + const cursoMatch = text.match(/Curso: ([^\n]+)/); + if (!cursoMatch) { + throw new AppError('Boletim inválido: Curso não encontrado.'); + } + + const modalidadeMatch = text.match(/Modalidade: ([^\n]+)/); + if (!modalidadeMatch) { + throw new AppError('Boletim inválido: Modalidade não encontrada.'); + } + + const serieModuloMatch = text.match(/Série \/ Módulo: ([^\n]+)/); + if (!serieModuloMatch) { + throw new AppError('Boletim inválido: Série / Módulo não encontrado.'); + } + + const turnoMatch = text.match(/Turno: ([^\n]+)/); + if (!turnoMatch) { + throw new AppError('Boletim inválido: Turno não encontrado.'); + } + + const info = { + anoLetivoSemestre: anoLetivoSemestreMatch[1], + rm: rmMatch[1], + nomeAluno: nomeAlunoMatch[1], + curso: cursoMatch[1], + modalidade: modalidadeMatch[1], + serieModulo: serieModuloMatch[1], + turno: turnoMatch[1] + }; + + return { link: urlMatch[0], info: info }; } } From 76f71347bc34742aa8dd3ff67e47bb4323f17055 Mon Sep 17 00:00:00 2001 From: Daniel-Alvarenga Date: Sun, 4 Aug 2024 22:07:32 -0300 Subject: [PATCH 3/3] Boletim control routes --- server/src/minioService.ts | 19 ++++++- .../controllers/funcionarioControllers.ts | 28 +++++++++- .../src/modules/interfaces/funcionarioDTOs.ts | 5 ++ .../funcionario/CompareBoletinsUseCase.ts | 47 +++++++++++++++++ .../funcionario/GetunapprovedUsecase.ts | 52 +++++++++++++++++++ .../src/router/routes/funcionario.routes.ts | 2 + .../src/router/routes/imports/funcionario.ts | 6 ++- 7 files changed, 156 insertions(+), 3 deletions(-) create mode 100644 server/src/modules/services/funcionario/CompareBoletinsUseCase.ts create mode 100644 server/src/modules/services/funcionario/GetunapprovedUsecase.ts diff --git a/server/src/minioService.ts b/server/src/minioService.ts index ca40630..c25b166 100644 --- a/server/src/minioService.ts +++ b/server/src/minioService.ts @@ -1,5 +1,8 @@ import { Client } from "minio"; import { AppError } from './errors/error'; +import { promisify } from 'util'; +import fs from 'fs'; +import stream from 'stream'; export const minioClient = new Client({ endPoint: process.env.MINIO_END_POINT as string, @@ -18,4 +21,18 @@ export const uploadToMinio = async (bucketName: string, objectName: string, file } catch (error) { throw new AppError(`Error uploading file: ${error}`); } -}; \ No newline at end of file +}; + +export async function downloadFromMinio(bucketName: string, objectName: string): Promise { + const tempFilePath = `../uploads/tmp/${objectName}`; + const fileStream = fs.createWriteStream(tempFilePath); + + const downloadStream = await minioClient.getObject(bucketName, objectName); + downloadStream.pipe(fileStream); + + await promisify(stream.finished)(fileStream); + + const fileBuffer = fs.readFileSync(tempFilePath); + fs.unlinkSync(tempFilePath); + return fileBuffer; +} \ No newline at end of file diff --git a/server/src/modules/controllers/funcionarioControllers.ts b/server/src/modules/controllers/funcionarioControllers.ts index a877ccd..cfb5f8c 100644 --- a/server/src/modules/controllers/funcionarioControllers.ts +++ b/server/src/modules/controllers/funcionarioControllers.ts @@ -7,9 +7,11 @@ import { ValidateRecoveryUseCase } from "../services/funcionario/ValidateRecover import { RefreshTokenUseCase } from "../services/funcionario/RefreshTokenUseCase"; import { RegisterVagaUseCase } from "../services/funcionario/RegisterVagasUseCase"; import { SetEmpresaParceiraUseCase } from "../services/funcionario/SetAsParceiraUseCase"; -import { SetEmpresaParceiraDTO } from "../interfaces/funcionarioDTOs"; +import { CompareBoletimDTO, SetEmpresaParceiraDTO } from "../interfaces/funcionarioDTOs"; import { EntidadeEnum } from "../interfaces/sharedDTOs"; import { GetMessagesBetweenUseCase } from "../services/shared/GetChatUseCase"; +import { CompareBoletimUseCase } from "../services/funcionario/CompareBoletinsUseCase"; +import { GetBoletinsEmAnaliseUseCase } from "../services/funcionario/GetunapprovedUsecase"; export class InitFuncionarioController { async handle(req: Request, res: Response) { @@ -150,6 +152,30 @@ export class GetMessagesBetweenController { const result = await getMessagesBetween.execute({ email1, identifier1, email2, identifier2 }); + return res.status(201).json(result); + } +} + +export class CompareBoletimController { + async handle(req: Request, res: Response): Promise { + const { boletimId } = req.body; + const file = req.file as Express.Multer.File; + + const compareBoletimUseCase = new CompareBoletimUseCase(); + + const result = await compareBoletimUseCase.execute({boletimId, file}); + + return res.status(200).json(result); + + } +} + +export class GetBoletinsEmAnaliseController { + async handle(req: Request, res: Response) { + const getBoletinsEmAnaliseUseCase = new GetBoletinsEmAnaliseUseCase(); + + const result = await getBoletinsEmAnaliseUseCase.execute(); + return res.status(201).json(result); } } \ No newline at end of file diff --git a/server/src/modules/interfaces/funcionarioDTOs.ts b/server/src/modules/interfaces/funcionarioDTOs.ts index d04fd78..afc99a9 100644 --- a/server/src/modules/interfaces/funcionarioDTOs.ts +++ b/server/src/modules/interfaces/funcionarioDTOs.ts @@ -50,4 +50,9 @@ export interface RegisterVagaDTO { export interface SetEmpresaParceiraDTO { funcionarioId: string; emailEmpresa: string; +} + +export interface CompareBoletimDTO { + boletimId: string; + file: Express.Multer.File; } \ No newline at end of file diff --git a/server/src/modules/services/funcionario/CompareBoletinsUseCase.ts b/server/src/modules/services/funcionario/CompareBoletinsUseCase.ts new file mode 100644 index 0000000..b73e6d7 --- /dev/null +++ b/server/src/modules/services/funcionario/CompareBoletinsUseCase.ts @@ -0,0 +1,47 @@ +import fs from 'fs'; +import path from 'path'; +import { prisma } from "../../../prisma/client"; +import { AppError } from "../../../errors/error"; +import { CompareBoletimDTO } from '../../interfaces/funcionarioDTOs'; +import { downloadFromMinio } from "../../../minioService"; +import { clearUploads } from '../shared/helpers/helpers'; + +export class CompareBoletimUseCase { + async execute({ boletimId, file }: CompareBoletimDTO) { + if (!boletimId) { + throw new AppError('ID do boletim não fornecido.'); + } + + if (!file || !file.path) { + throw new AppError('Arquivo do boletim não fornecido.'); + } + + const boletim = await prisma.boletim.findUnique({ + where: { + id: boletimId, + }, + }); + + if (!boletim) { + throw new AppError('Boletim não encontrado.'); + } + + const filePath = path.resolve(file.path); + const fileBuffer = fs.readFileSync(filePath); + + const storedBoletimBuffer = await downloadFromMinio('boot', boletim.caminho); + + const isEqual = fileBuffer.equals(storedBoletimBuffer); + + const novoStatus = isEqual ? 'APROVADO' : 'RECUSADO'; + + await prisma.boletim.update({ + where: { id: boletimId }, + data: { status: novoStatus } + }); + + await clearUploads(); + + return { message: `Boletim ${novoStatus}.` }; + } +} diff --git a/server/src/modules/services/funcionario/GetunapprovedUsecase.ts b/server/src/modules/services/funcionario/GetunapprovedUsecase.ts new file mode 100644 index 0000000..a709130 --- /dev/null +++ b/server/src/modules/services/funcionario/GetunapprovedUsecase.ts @@ -0,0 +1,52 @@ +import { prisma } from "../../../prisma/client"; +import { AppError } from "../../../errors/error"; + +export class GetBoletinsEmAnaliseUseCase { + async execute() { + const boletins = await prisma.boletim.findMany({ + where: { + status: 'EM_ANALISE', + }, + include: { + aluno: { + include: { + turmas: { + include: { + turma: true, + } + } + } + } + }, + orderBy: [ + { + aluno: { + turmas: {} + } + }, + { + aluno: { + name: 'asc' + } + } + ] + }); + + if (!boletins.length) { + throw new AppError('Nenhum boletim em análise encontrado.'); + } + + const boletinsEmAnalise = boletins.map(boletim => ({ + id: boletim.id, + aluno: { + id: boletim.aluno.id, + name: boletim.aluno.name, + turma: boletim.aluno.turmas.map(turma => turma.turma.inicio).join(', ') + }, + url: boletim.link, + status: boletim.status + })); + + return boletinsEmAnalise; + } +} \ No newline at end of file diff --git a/server/src/router/routes/funcionario.routes.ts b/server/src/router/routes/funcionario.routes.ts index f15dffc..a95e16d 100644 --- a/server/src/router/routes/funcionario.routes.ts +++ b/server/src/router/routes/funcionario.routes.ts @@ -16,6 +16,7 @@ funcionarioRoutes.use(funcionarioAuthMiddleware); funcionarioRoutes.post("/register/vaga", controllers.registerVagaController.handle); funcionarioRoutes.post("/update/empresa", controllers.registerVagaController.handle); funcionarioRoutes.post("/message/send", controllers.createMessageController.handle); +funcionarioRoutes.post("/boletim/compare", controllers.compareBoletimController.handle); funcionarioRoutes.get("/auth", (req, res) => { res.status(200).send("Funcionário autenticado com sucesso."); @@ -25,6 +26,7 @@ funcionarioRoutes.get("/token/refresh", controllers.refreshTokenController.handl funcionarioRoutes.get("/cursos", controllers.getCursosController.handle); funcionarioRoutes.get("/empresas", controllers.getEmpresasController.handle); funcionarioRoutes.get("/messages/between", controllers.getMessagesBetweenController.handle); +funcionarioRoutes.get("/boletins", controllers.getBoletinsEmAnaliseController.handle); export { funcionarioRoutes }; \ No newline at end of file diff --git a/server/src/router/routes/imports/funcionario.ts b/server/src/router/routes/imports/funcionario.ts index fb79b53..f6316ca 100644 --- a/server/src/router/routes/imports/funcionario.ts +++ b/server/src/router/routes/imports/funcionario.ts @@ -7,7 +7,9 @@ import { RefreshTokenController, RegisterVagaController, FuncionarioController, - GetMessagesBetweenController + GetMessagesBetweenController, + CompareBoletimController, + GetBoletinsEmAnaliseController } from "../../../modules/controllers/funcionarioControllers"; import { GetCursosController, @@ -28,4 +30,6 @@ export const createControllers = () => ({ funcionarioController: new FuncionarioController(), createMessageController: new CreateMessageController(), getMessagesBetweenController: new GetMessagesBetweenController(), + compareBoletimController: new CompareBoletimController(), + getBoletinsEmAnaliseController: new GetBoletinsEmAnaliseController(), }); \ No newline at end of file