Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 605227aeba | |||
| 3411737e6d | |||
| 3a7f8d3647 | |||
| 2a1a3bc5c0 | |||
| 6114fa2583 |
@@ -22,6 +22,6 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
|
|||||||
|
|
||||||
async validate(payload: any) {
|
async validate(payload: any) {
|
||||||
// Devuelve lo que estará disponible en Request.user
|
// Devuelve lo que estará disponible en Request.user
|
||||||
return { userId: payload.sub, email: payload.email, origen: payload.tipo };
|
return { userId: payload.sub, email: payload.email, origen: payload.tipo, tipo: payload.tipo, };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+106
-168
@@ -1,5 +1,5 @@
|
|||||||
// src/excel/excel.controller.ts
|
// src/excel/excel.controller.ts
|
||||||
import { Controller, Post, Get, UseGuards, Request, Res, UploadedFile, UseInterceptors, HttpStatus } from '@nestjs/common';
|
import { Controller, Post, Get, UseGuards, Request, Res, UploadedFile, UseInterceptors, HttpStatus, ForbiddenException } from '@nestjs/common';
|
||||||
import { AuthGuard } from '@nestjs/passport';
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
import { ApiBearerAuth } from '@nestjs/swagger';
|
import { ApiBearerAuth } from '@nestjs/swagger';
|
||||||
import { FileInterceptor } from '@nestjs/platform-express';
|
import { FileInterceptor } from '@nestjs/platform-express';
|
||||||
@@ -17,176 +17,114 @@ export class ExcelController {
|
|||||||
private readonly movimientoService: MovimientoService,
|
private readonly movimientoService: MovimientoService,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
@Post('verify')
|
// excel.controller.ts
|
||||||
@UseInterceptors(FileInterceptor('file'))
|
|
||||||
@ExcelDocumentation.verifyExcel()
|
@Post('verify')
|
||||||
async verifyExcel(@UploadedFile() file: Express.Multer.File, @Request() req) {
|
@UseGuards(AuthGuard('jwt'))
|
||||||
const userId: number = req.user.userId; // ahora sí existe
|
@UseInterceptors(FileInterceptor('file'))
|
||||||
const errors = await this.excelService.validateFile(file.buffer);
|
@ExcelDocumentation.verifyExcel()
|
||||||
|
async verifyExcel(
|
||||||
|
@UploadedFile() file: Express.Multer.File,
|
||||||
|
@Request() req
|
||||||
|
) {
|
||||||
|
const { userId, tipo } = req.user; // obtenemos ambos
|
||||||
|
const errors = await this.excelService.validateFile(file.buffer);
|
||||||
|
|
||||||
|
await this.movimientoService.log(
|
||||||
|
userId, // el ID
|
||||||
|
tipo, // aquí va tu fuente dinámica
|
||||||
|
errors.length ? 'FAILED' : 'SUCCESS',
|
||||||
|
errors.join('; ')
|
||||||
|
);
|
||||||
|
|
||||||
|
return { valid: errors.length === 0, errors };
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Post('load')
|
||||||
|
@UseGuards(AuthGuard('jwt'))
|
||||||
|
@UseInterceptors(FileInterceptor('file'))
|
||||||
|
@ExcelDocumentation.loadExcel()
|
||||||
|
async loadExcel(
|
||||||
|
@UploadedFile() file: Express.Multer.File,
|
||||||
|
@Request() req,
|
||||||
|
) {
|
||||||
|
const { userId, tipo } = req.user; // userId es number, tipo tu fuente
|
||||||
|
if (tipo !== 'LOAD') {
|
||||||
await this.movimientoService.log(
|
await this.movimientoService.log(
|
||||||
|
userId,
|
||||||
'VERIFY',
|
tipo,
|
||||||
errors.length ? 'FAILED' : 'SUCCESS',
|
'FAILED',
|
||||||
errors.join('; ')
|
'Origen no permitido para carga',
|
||||||
);
|
);
|
||||||
return { valid: errors.length === 0, errors };
|
throw new ForbiddenException('Origen no permitido para carga.');
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('load')
|
try {
|
||||||
@UseInterceptors(FileInterceptor('file'))
|
const { inserted } = await this.excelService.loadFile(file.buffer);
|
||||||
@ExcelDocumentation.loadExcel()
|
await this.movimientoService.log(
|
||||||
async loadExcel(@Request() req, @UploadedFile() file: Express.Multer.File) {
|
userId,
|
||||||
const origen = req.user.origen; // ahora sí existe
|
tipo,
|
||||||
if (origen != 'LOAD') {
|
'SUCCESS',
|
||||||
await this.movimientoService.log(
|
undefined,
|
||||||
'LOAD',
|
`inserted=${inserted}`,
|
||||||
'FAILED',
|
);
|
||||||
'Origen no permitido para carga'
|
return { inserted };
|
||||||
);
|
} catch (err) {
|
||||||
throw new Error('Origen no permitido para carga.');
|
await this.movimientoService.log(
|
||||||
}
|
userId,
|
||||||
try {
|
tipo,
|
||||||
const result = await this.excelService.loadFile(file.buffer);
|
'FAILED',
|
||||||
await this.movimientoService.log(
|
err.message,
|
||||||
|
);
|
||||||
'LOAD',
|
throw err;
|
||||||
'SUCCESS',
|
|
||||||
undefined,
|
|
||||||
`inserted=${result.inserted}`
|
|
||||||
);
|
|
||||||
return result;
|
|
||||||
} catch (err) {
|
|
||||||
await this.movimientoService.log(
|
|
||||||
|
|
||||||
'LOAD',
|
|
||||||
'FAILED',
|
|
||||||
err.message
|
|
||||||
);
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get('download')
|
|
||||||
@ExcelDocumentation.downloadExcel()
|
|
||||||
async downloadData(@Request() req, @Res() res: Response) {
|
|
||||||
const origen = req.user.origen; // ahora sí existe
|
|
||||||
console.log('Origen de descarga:', origen);
|
|
||||||
console.log('Usuario:', req.user);
|
|
||||||
|
|
||||||
if (origen == 'SOLICITA') {
|
|
||||||
try {
|
|
||||||
const csv = await this.excelService.generateCsvSolicita();
|
|
||||||
await this.movimientoService.log(
|
|
||||||
|
|
||||||
origen,
|
|
||||||
'SUCCESS',
|
|
||||||
undefined,
|
|
||||||
`size=${csv.length}`
|
|
||||||
);
|
|
||||||
return res
|
|
||||||
.status(HttpStatus.OK)
|
|
||||||
.header('Content-Type', 'text/tab-separated-values')
|
|
||||||
.header('Content-Disposition', 'attachment; filename="usuarios_solicita.tsv"')
|
|
||||||
.send(csv);
|
|
||||||
} catch (err) {
|
|
||||||
await this.movimientoService.log(
|
|
||||||
origen,
|
|
||||||
'FAILED',
|
|
||||||
'Origen no permitido para descarga'
|
|
||||||
);
|
|
||||||
return res
|
|
||||||
.status(HttpStatus.FORBIDDEN)
|
|
||||||
.json({ statusCode: 403, message: 'Origen no permitido para descarga.' });
|
|
||||||
}
|
|
||||||
} else if (origen == 'CORREO') {
|
|
||||||
try {
|
|
||||||
const csv = await this.excelService.generateCsvCorreo();
|
|
||||||
await this.movimientoService.log(
|
|
||||||
|
|
||||||
origen,
|
|
||||||
'SUCCESS',
|
|
||||||
undefined,
|
|
||||||
`size=${csv.length}`
|
|
||||||
);
|
|
||||||
return res
|
|
||||||
.status(HttpStatus.OK)
|
|
||||||
.header('Content-Type', 'text/tab-separated-values')
|
|
||||||
.header('Content-Disposition', 'attachment; filename="usuarios.tsv"')
|
|
||||||
.send(csv);
|
|
||||||
} catch (err) {
|
|
||||||
await this.movimientoService.log(
|
|
||||||
|
|
||||||
origen,
|
|
||||||
'FAILED',
|
|
||||||
err.message
|
|
||||||
);
|
|
||||||
return res
|
|
||||||
.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
|
||||||
.json({ statusCode: 500, message: 'Error generando descarga.' });
|
|
||||||
}
|
|
||||||
} else if (origen == 'AT') {
|
|
||||||
try {
|
|
||||||
const csv = await this.excelService.generateCsvAT();
|
|
||||||
await this.movimientoService.log(
|
|
||||||
|
|
||||||
origen,
|
|
||||||
'SUCCESS',
|
|
||||||
undefined,
|
|
||||||
`size=${csv.length}`
|
|
||||||
);
|
|
||||||
return res
|
|
||||||
.status(HttpStatus.OK)
|
|
||||||
.header('Content-Type', 'text/tab-separated-values')
|
|
||||||
.header('Content-Disposition', 'attachment; filename="usuarios_at.tsv"')
|
|
||||||
.send(csv);
|
|
||||||
} catch (err) {
|
|
||||||
await this.movimientoService.log(
|
|
||||||
|
|
||||||
origen,
|
|
||||||
'FAILED',
|
|
||||||
err.message
|
|
||||||
);
|
|
||||||
return res
|
|
||||||
.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
|
||||||
.json({ statusCode: 500, message: 'Error generando descarga.' });
|
|
||||||
}
|
|
||||||
} else if (origen == 'RED') {
|
|
||||||
try {
|
|
||||||
const csv = await this.excelService.generateCsvRed();
|
|
||||||
await this.movimientoService.log(
|
|
||||||
|
|
||||||
origen,
|
|
||||||
'SUCCESS',
|
|
||||||
undefined,
|
|
||||||
`size=${csv.length}`
|
|
||||||
);
|
|
||||||
return res
|
|
||||||
.status(HttpStatus.OK)
|
|
||||||
.header('Content-Type', 'text/tab-separated-values')
|
|
||||||
.header('Content-Disposition', 'attachment; filename="usuarios_red.tsv"')
|
|
||||||
.send(csv);
|
|
||||||
} catch (err) {
|
|
||||||
await this.movimientoService.log(
|
|
||||||
|
|
||||||
origen,
|
|
||||||
'FAILED',
|
|
||||||
err.message
|
|
||||||
);
|
|
||||||
return res
|
|
||||||
.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
|
||||||
.json({ statusCode: 500, message: 'Error generando descarga.' });
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
await this.movimientoService.log(
|
|
||||||
|
|
||||||
origen,
|
|
||||||
'FAILED',
|
|
||||||
'Origen no permitido para descarga'
|
|
||||||
);
|
|
||||||
return res
|
|
||||||
.status(HttpStatus.FORBIDDEN)
|
|
||||||
.json({ statusCode: 403, message: 'Origen no permitido para descarga.' });
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Get('download')
|
||||||
|
@UseGuards(AuthGuard('jwt'))
|
||||||
|
@ApiBearerAuth('bearer')
|
||||||
|
@ExcelDocumentation.downloadExcel()
|
||||||
|
async downloadData(@Request() req, @Res() res: Response) {
|
||||||
|
const { userId, tipo } = req.user;
|
||||||
|
|
||||||
|
// Mapeo: tipo → [método de ExcelService, sufijo de filename]
|
||||||
|
const generators: Record<string, [() => Promise<string>, string]> = {
|
||||||
|
SOLICITA: [() => this.excelService.generateCsvSolicita(), 'solicita'],
|
||||||
|
CORREO: [() => this.excelService.generateCsvCorreo(), 'correo'],
|
||||||
|
AT: [() => this.excelService.generateCsvAT(), 'at'],
|
||||||
|
RED: [() => this.excelService.generateCsvRed(), 'red'],
|
||||||
|
};
|
||||||
|
|
||||||
|
const entry = generators[tipo];
|
||||||
|
if (!entry) {
|
||||||
|
await this.movimientoService.log(userId, tipo, 'FAILED', 'Origen no permitido para descarga');
|
||||||
|
return res
|
||||||
|
.status(HttpStatus.FORBIDDEN)
|
||||||
|
.json({ statusCode: 403, message: 'Origen no permitido para descarga.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const [generateCsv, suffix] = entry;
|
||||||
|
try {
|
||||||
|
const csv = await generateCsv();
|
||||||
|
await this.movimientoService.log(userId, tipo, 'SUCCESS', undefined, `size=${csv.length}`);
|
||||||
|
return res
|
||||||
|
.status(HttpStatus.OK)
|
||||||
|
.header('Content-Type', 'text/tab-separated-values')
|
||||||
|
.header('Content-Disposition', `attachment; filename="usuarios_${suffix}.tsv"`)
|
||||||
|
.send(csv);
|
||||||
|
} catch (err) {
|
||||||
|
await this.movimientoService.log(userId, tipo, 'FAILED', err.message);
|
||||||
|
return res
|
||||||
|
.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||||
|
.json({ statusCode: 500, message: 'Error generando descarga.' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
+142
-52
@@ -23,6 +23,9 @@ export interface UsuarioRow {
|
|||||||
rfc: string;
|
rfc: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RuleFn = (r: UsuarioRow, rowNum: number, errors: string[]) => void;
|
||||||
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ExcelService {
|
export class ExcelService {
|
||||||
private readonly logger = new Logger(ExcelService.name);
|
private readonly logger = new Logger(ExcelService.name);
|
||||||
@@ -43,59 +46,7 @@ export class ExcelService {
|
|||||||
// … inyecta otros repositorios si los usarás
|
// … inyecta otros repositorios si los usarás
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
/** Lee el buffer del Excel y devuelve un arreglo de filas tipadas */
|
|
||||||
private async parseFile(buffer: Buffer): Promise<UsuarioRow[]> {
|
|
||||||
const wb = new Workbook();
|
|
||||||
await wb.xlsx.load(buffer);
|
|
||||||
const sheet = wb.worksheets[0];
|
|
||||||
const rows: UsuarioRow[] = [];
|
|
||||||
|
|
||||||
// Asume que la primera fila es encabezados
|
|
||||||
sheet.eachRow((row, idx) => {
|
|
||||||
if (idx === 1) return; // salto encabezados
|
|
||||||
|
|
||||||
// 1) Asegurarnos de que row.values no sea null/undefined
|
|
||||||
if (!row.values) {
|
|
||||||
this.logger.warn(`Fila ${idx + 1}: row.values vacío, se omite.`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2) row.values[0] es null, así que slice(1) sí existe
|
|
||||||
const [
|
|
||||||
cuenta,
|
|
||||||
nombreCompleto,
|
|
||||||
clave,
|
|
||||||
nomCarr,
|
|
||||||
gen,
|
|
||||||
fechnac,
|
|
||||||
apellidopa,
|
|
||||||
apellidoma,
|
|
||||||
nombres,
|
|
||||||
sexo,
|
|
||||||
tipo,
|
|
||||||
correo,
|
|
||||||
rfc,
|
|
||||||
] = (row.values as any[]).slice(1);
|
|
||||||
|
|
||||||
rows.push({
|
|
||||||
cuenta,
|
|
||||||
nombreCompleto,
|
|
||||||
clave,
|
|
||||||
nomCarr,
|
|
||||||
gen,
|
|
||||||
fechnac,
|
|
||||||
apellidopa,
|
|
||||||
apellidoma,
|
|
||||||
nombres,
|
|
||||||
sexo,
|
|
||||||
tipo,
|
|
||||||
correo,
|
|
||||||
rfc,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return rows;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Valida duplicados, celdas vacías y patrones básicos */
|
/** Valida duplicados, celdas vacías y patrones básicos */
|
||||||
validateRows(rows: UsuarioRow[]) {
|
validateRows(rows: UsuarioRow[]) {
|
||||||
@@ -148,6 +99,145 @@ export class ExcelService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Describes which rules aplicar por tipo
|
||||||
|
private typeRules: Record<string, RuleFn[]> = {
|
||||||
|
// Regla común para todos los tipos
|
||||||
|
__default: [
|
||||||
|
this.ruleNonEmpty(['cuenta', 'nombreCompleto', 'tipo']),
|
||||||
|
this.ruleUniqueCuenta(),
|
||||||
|
this.ruleNumericField('gen', /^[0-9]{4}$/),
|
||||||
|
this.ruleDateField('fechnac'),
|
||||||
|
this.ruleRFC('rfc'),
|
||||||
|
],
|
||||||
|
|
||||||
|
// Añade reglas extra para Licenciatura
|
||||||
|
Licenciatura: [
|
||||||
|
this.ruleNonEmpty(['clave', 'nomCarr']),
|
||||||
|
(r, rowNum, errors) => {
|
||||||
|
if (!/^[0-9]{5}$/.test(r.clave)) {
|
||||||
|
errors.push(`Fila ${rowNum}: clave de Licenciatura debe tener 5 dígitos.`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
// Añade reglas extra para Profesor
|
||||||
|
Profesor: [
|
||||||
|
(r, rowNum, errors) => {
|
||||||
|
if (!r.correo?.includes('@')) {
|
||||||
|
errors.push(`Fila ${rowNum}: Profesor debe tener correo válido.`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
// … puedes definir más tipos aquí …
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Métodos auxiliares que devuelven funciones de regla */
|
||||||
|
private ruleNonEmpty(fields: (keyof UsuarioRow)[]): RuleFn {
|
||||||
|
return (r, rowNum, errors) => {
|
||||||
|
for (const f of fields) {
|
||||||
|
const v = (r[f] ?? '').toString().trim();
|
||||||
|
if (!v) {
|
||||||
|
errors.push(`Fila ${rowNum}: campo "${f}" está vacío.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private ruleUniqueCuenta(): RuleFn {
|
||||||
|
const seen = new Set<string>();
|
||||||
|
return (r, rowNum, errors) => {
|
||||||
|
const c = (r.cuenta ?? '').toString().trim();
|
||||||
|
if (seen.has(c)) {
|
||||||
|
errors.push(`Fila ${rowNum}: cuenta duplicada "${c}".`);
|
||||||
|
} else {
|
||||||
|
seen.add(c);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private ruleNumericField(field: keyof UsuarioRow, regex: RegExp): RuleFn {
|
||||||
|
return (r, rowNum, errors) => {
|
||||||
|
const v = (r[field] ?? '').toString().trim();
|
||||||
|
if (v && !regex.test(v)) {
|
||||||
|
errors.push(`Fila ${rowNum}: campo "${field}" inválido: "${v}".`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private ruleDateField(field: keyof UsuarioRow): RuleFn {
|
||||||
|
return (r, rowNum, errors) => {
|
||||||
|
const v = (r[field] ?? '').toString().trim();
|
||||||
|
if (v && !/^[0-9]{8}$/.test(v)) {
|
||||||
|
errors.push(`Fila ${rowNum}: fecha "${field}" debe ser YYYYMMDD: "${v}".`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private ruleRFC(field: keyof UsuarioRow): RuleFn {
|
||||||
|
return (r, rowNum, errors) => {
|
||||||
|
const v = (r[field] ?? '').toString().trim();
|
||||||
|
if (v && !/^[A-Z0-9]+$/.test(v)) {
|
||||||
|
errors.push(`Fila ${rowNum}: RFC contiene caracteres inválidos: "${v}".`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Lee el buffer del Excel y devuelve un arreglo de filas tipadas */
|
||||||
|
private async parseFile(buffer: Buffer): Promise<UsuarioRow[]> {
|
||||||
|
const wb = new Workbook();
|
||||||
|
await wb.xlsx.load(buffer);
|
||||||
|
const sheet = wb.worksheets[0];
|
||||||
|
const rows: UsuarioRow[] = [];
|
||||||
|
|
||||||
|
sheet.eachRow((row, idx) => {
|
||||||
|
if (idx === 1) return; // salto encabezados
|
||||||
|
const vals = row.values as any[];
|
||||||
|
rows.push({
|
||||||
|
cuenta: vals[1] ?? '',
|
||||||
|
nombreCompleto: vals[2] ?? '',
|
||||||
|
clave: vals[3] ?? '',
|
||||||
|
nomCarr: vals[4] ?? '',
|
||||||
|
gen: vals[5] ?? '',
|
||||||
|
fechnac: vals[6] ?? '',
|
||||||
|
apellidopa: vals[7] ?? '',
|
||||||
|
apellidoma: vals[8] ?? '',
|
||||||
|
nombres: vals[9] ?? '',
|
||||||
|
sexo: vals[10] ?? '',
|
||||||
|
tipo: vals[11] ?? '',
|
||||||
|
correo: vals[12] ?? '',
|
||||||
|
rfc: vals[13] ?? '',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Valida cada fila según reglas comunes y específicas por tipo.
|
||||||
|
* Retorna un array de mensajes de error.
|
||||||
|
*/
|
||||||
|
validateRowsAdvanced(rows: UsuarioRow[]): string[] {
|
||||||
|
const errors: string[] = [];
|
||||||
|
rows.forEach((r, i) => {
|
||||||
|
const rowNum = i + 2;
|
||||||
|
// 1) Aplica reglas comunes
|
||||||
|
for (const rule of this.typeRules.__default) {
|
||||||
|
rule.call(this, r, rowNum, errors);
|
||||||
|
}
|
||||||
|
// 2) Aplica reglas del tipo específico
|
||||||
|
const specificRules = this.typeRules[r.tipo] ?? [];
|
||||||
|
for (const rule of specificRules) {
|
||||||
|
rule.call(this, r, rowNum, errors);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Solo valida: retorna lista de errores (vacío = ok)
|
* Solo valida: retorna lista de errores (vacío = ok)
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
// src/movimiento/movimiento.controller.ts
|
||||||
|
import { Controller, Get, UseGuards } from '@nestjs/common';
|
||||||
|
import { MovimientoService } from './movimiento.service';
|
||||||
|
import { Movimiento } from '../entities/entities';
|
||||||
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
|
import { ApiTags, ApiBearerAuth, ApiOperation, ApiResponse } from '@nestjs/swagger';
|
||||||
|
import { MovimientoDocumentation } from './movimiento.documentation';
|
||||||
|
|
||||||
|
@ApiTags('Movimientos')
|
||||||
|
@ApiBearerAuth('bearer')
|
||||||
|
@UseGuards(AuthGuard('jwt'))
|
||||||
|
@Controller('movimientos')
|
||||||
|
export class MovimientoController {
|
||||||
|
constructor(private readonly movimientoService: MovimientoService) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@MovimientoDocumentation.findAllmov()
|
||||||
|
@ApiOperation({ summary: 'Listar movimientos', description: 'Obtiene todos los registros de movimientos con usuario y origen.' })
|
||||||
|
@ApiResponse({ status: 200, description: 'Listado de movimientos', type: Movimiento, isArray: true })
|
||||||
|
async findAll(): Promise<Movimiento[]> {
|
||||||
|
return this.movimientoService.findAll();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { applyDecorators } from '@nestjs/common';
|
||||||
|
import { ApiTags, ApiBearerAuth, ApiOperation, ApiResponse } from '@nestjs/swagger';
|
||||||
|
import { Movimiento } from '../entities/entities';
|
||||||
|
|
||||||
|
export class MovimientoDocumentation {
|
||||||
|
/**
|
||||||
|
* Decorators Swagger para el endpoint GET /movimientos
|
||||||
|
*/
|
||||||
|
static findAllmov() {
|
||||||
|
return applyDecorators(
|
||||||
|
ApiTags('Movimientos'),
|
||||||
|
ApiBearerAuth('bearer'),
|
||||||
|
ApiOperation({
|
||||||
|
summary: 'Listar todos los movimientos',
|
||||||
|
description: 'Devuelve el histórico completo de movimientos, incluyendo usuario y origen.'
|
||||||
|
}),
|
||||||
|
ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'Listado de movimientos exitoso.',
|
||||||
|
type: Movimiento,
|
||||||
|
isArray: true
|
||||||
|
}),
|
||||||
|
ApiResponse({ status: 401, description: 'No autorizado.' }),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,10 +3,13 @@ import { Module } from '@nestjs/common';
|
|||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { MovimientoService } from './movimiento.service';
|
import { MovimientoService } from './movimiento.service';
|
||||||
import { Movimiento, Origen } from '../entities/entities';
|
import { Movimiento, Origen } from '../entities/entities';
|
||||||
|
import { MovimientoController } from './movimiento.controller';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [TypeOrmModule.forFeature([Movimiento, Origen])],
|
imports: [TypeOrmModule.forFeature([Movimiento, Origen])],
|
||||||
providers: [MovimientoService],
|
providers: [MovimientoService],
|
||||||
|
controllers: [MovimientoController],
|
||||||
|
|
||||||
exports: [MovimientoService],
|
exports: [MovimientoService],
|
||||||
})
|
})
|
||||||
export class MovimientoModule {}
|
export class MovimientoModule {}
|
||||||
|
|||||||
@@ -15,15 +15,16 @@ export class MovimientoService {
|
|||||||
|
|
||||||
/** Crea un registro de movimiento */
|
/** Crea un registro de movimiento */
|
||||||
async log(
|
async log(
|
||||||
origen: string,
|
usuarioId: number,
|
||||||
status: string,
|
fuente: string,
|
||||||
observaciones?: string,
|
status: string,
|
||||||
reporte?: string,
|
observaciones?: string,
|
||||||
flag = false,
|
reporte?: string,
|
||||||
|
flag = false,
|
||||||
) {
|
) {
|
||||||
// 1) Obtén el origen o falla si no existe
|
// 1) Obtén el origen o falla si no existe
|
||||||
const origin = await this.origenRepo.findOne({ where: { origen } });
|
const origin = await this.origenRepo.findOne({ where: { origen:fuente } });
|
||||||
if (!origin) throw new Error(`Origen desconocido: ${origen}`);
|
if (!origin) throw new Error(`Origen desconocido: ${fuente}`);
|
||||||
|
|
||||||
// 2) Inserta directamente usando los campos de FK
|
// 2) Inserta directamente usando los campos de FK
|
||||||
await this.movRepo.insert({
|
await this.movRepo.insert({
|
||||||
@@ -35,4 +36,17 @@ export class MovimientoService {
|
|||||||
origen: { id_origen: origin.id_origen },
|
origen: { id_origen: origin.id_origen },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/** Devuelve todos los movimientos con usuario y origen */
|
||||||
|
async findAll(): Promise<Movimiento[]> {
|
||||||
|
return this.movRepo.find({
|
||||||
|
relations: [ 'origen'],
|
||||||
|
order: { fecha_mov: 'DESC' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user