fix(hr): Fix PaginatedResult format in employee and puesto services
- Remove meta wrapper from PaginatedResult returns in findWithFilters and findAll - Return flat format with total, page, limit, totalPages at top level - Align with base.service.ts interface definition - Controllers already updated to access flat pagination structure
This commit is contained in:
parent
873693b1d1
commit
c8a01c5f14
@ -11,12 +11,17 @@ import { DataSource } from 'typeorm';
|
||||
import { PresupuestoService, CreatePresupuestoDto, AddPartidaDto, UpdatePartidaDto } from '../services/presupuesto.service';
|
||||
import { AuthMiddleware } from '../../auth/middleware/auth.middleware';
|
||||
import { AuthService } from '../../auth/services/auth.service';
|
||||
import { Presupuesto } from '../entities/presupuesto.entity';
|
||||
import { PresupuestoPartida } from '../entities/presupuesto-partida.entity';
|
||||
import { User } from '../../core/entities/user.entity';
|
||||
import { Tenant } from '../../core/entities/tenant.entity';
|
||||
import { RefreshToken } from '../../auth/entities/refresh-token.entity';
|
||||
import { ServiceContext } from '../../../shared/services/base.service';
|
||||
|
||||
/**
|
||||
* ServiceContext - Contexto local para operaciones de servicio
|
||||
*/
|
||||
interface ServiceContext {
|
||||
tenantId: string;
|
||||
userId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Crear router de presupuestos
|
||||
@ -25,14 +30,12 @@ export function createPresupuestoController(dataSource: DataSource): Router {
|
||||
const router = Router();
|
||||
|
||||
// Repositorios
|
||||
const presupuestoRepository = dataSource.getRepository(Presupuesto);
|
||||
const partidaRepository = dataSource.getRepository(PresupuestoPartida);
|
||||
const userRepository = dataSource.getRepository(User);
|
||||
const tenantRepository = dataSource.getRepository(Tenant);
|
||||
const refreshTokenRepository = dataSource.getRepository(RefreshToken);
|
||||
|
||||
// Servicios
|
||||
const presupuestoService = new PresupuestoService(presupuestoRepository, partidaRepository);
|
||||
const presupuestoService = new PresupuestoService(dataSource);
|
||||
const authService = new AuthService(userRepository, tenantRepository, refreshTokenRepository as any);
|
||||
const authMiddleware = new AuthMiddleware(authService, dataSource);
|
||||
|
||||
@ -73,7 +76,12 @@ export function createPresupuestoController(dataSource: DataSource): Router {
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: result.data,
|
||||
pagination: result.meta,
|
||||
pagination: {
|
||||
total: result.total,
|
||||
page: result.page,
|
||||
limit: result.limit,
|
||||
totalPages: result.totalPages,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
|
||||
@ -7,11 +7,23 @@
|
||||
* @module Budgets
|
||||
*/
|
||||
|
||||
import { Repository } from 'typeorm';
|
||||
import { BaseService, ServiceContext, PaginatedResult } from '../../../shared/services/base.service';
|
||||
import { DataSource, Repository, IsNull } from 'typeorm';
|
||||
import { Presupuesto } from '../entities/presupuesto.entity';
|
||||
import { PresupuestoPartida } from '../entities/presupuesto-partida.entity';
|
||||
|
||||
interface ServiceContext {
|
||||
tenantId: string;
|
||||
userId?: string;
|
||||
}
|
||||
|
||||
interface PaginatedResult<T> {
|
||||
data: T[];
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
totalPages: number;
|
||||
}
|
||||
|
||||
export interface CreatePresupuestoDto {
|
||||
code: string;
|
||||
name: string;
|
||||
@ -34,12 +46,101 @@ export interface UpdatePartidaDto {
|
||||
sequence?: number;
|
||||
}
|
||||
|
||||
export class PresupuestoService extends BaseService<Presupuesto> {
|
||||
constructor(
|
||||
repository: Repository<Presupuesto>,
|
||||
private readonly partidaRepository: Repository<PresupuestoPartida>
|
||||
) {
|
||||
super(repository);
|
||||
export class PresupuestoService {
|
||||
private repository: Repository<Presupuesto>;
|
||||
private partidaRepository: Repository<PresupuestoPartida>;
|
||||
|
||||
constructor(dataSource: DataSource) {
|
||||
this.repository = dataSource.getRepository(Presupuesto);
|
||||
this.partidaRepository = dataSource.getRepository(PresupuestoPartida);
|
||||
}
|
||||
|
||||
/**
|
||||
* Busca un presupuesto por ID
|
||||
*/
|
||||
async findById(ctx: ServiceContext, id: string): Promise<Presupuesto | null> {
|
||||
return this.repository.findOne({
|
||||
where: {
|
||||
id,
|
||||
tenantId: ctx.tenantId,
|
||||
deletedAt: IsNull(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Busca todos los presupuestos con paginación
|
||||
*/
|
||||
async findAll(
|
||||
ctx: ServiceContext,
|
||||
page = 1,
|
||||
limit = 20
|
||||
): Promise<PaginatedResult<Presupuesto>> {
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
const [data, total] = await this.repository.findAndCount({
|
||||
where: {
|
||||
tenantId: ctx.tenantId,
|
||||
deletedAt: IsNull(),
|
||||
},
|
||||
order: { createdAt: 'DESC' },
|
||||
skip,
|
||||
take: limit,
|
||||
});
|
||||
|
||||
return {
|
||||
data,
|
||||
total,
|
||||
page,
|
||||
limit,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Crea un nuevo registro
|
||||
*/
|
||||
async create(ctx: ServiceContext, data: Partial<Presupuesto>): Promise<Presupuesto> {
|
||||
const entity = this.repository.create({
|
||||
...data,
|
||||
tenantId: ctx.tenantId,
|
||||
createdById: ctx.userId,
|
||||
});
|
||||
return this.repository.save(entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Actualiza un registro
|
||||
*/
|
||||
async update(
|
||||
ctx: ServiceContext,
|
||||
id: string,
|
||||
data: Partial<Presupuesto>
|
||||
): Promise<Presupuesto | null> {
|
||||
const entity = await this.findById(ctx, id);
|
||||
if (!entity) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Object.assign(entity, data);
|
||||
entity.updatedById = ctx.userId || null;
|
||||
|
||||
return this.repository.save(entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Soft delete de un registro
|
||||
*/
|
||||
async softDelete(ctx: ServiceContext, id: string): Promise<boolean> {
|
||||
const entity = await this.findById(ctx, id);
|
||||
if (!entity) {
|
||||
return false;
|
||||
}
|
||||
|
||||
entity.deletedAt = new Date();
|
||||
entity.deletedById = ctx.userId || null;
|
||||
await this.repository.save(entity);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -66,11 +167,27 @@ export class PresupuestoService extends BaseService<Presupuesto> {
|
||||
page = 1,
|
||||
limit = 20
|
||||
): Promise<PaginatedResult<Presupuesto>> {
|
||||
return this.findAll(ctx, {
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
const [data, total] = await this.repository.findAndCount({
|
||||
where: {
|
||||
tenantId: ctx.tenantId,
|
||||
fraccionamientoId,
|
||||
isActive: true,
|
||||
deletedAt: IsNull(),
|
||||
},
|
||||
order: { createdAt: 'DESC' },
|
||||
skip,
|
||||
take: limit,
|
||||
});
|
||||
|
||||
return {
|
||||
data,
|
||||
total,
|
||||
page,
|
||||
limit,
|
||||
where: { fraccionamientoId, isActive: true } as any,
|
||||
});
|
||||
totalPages: Math.ceil(total / limit),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@ -84,8 +201,8 @@ export class PresupuestoService extends BaseService<Presupuesto> {
|
||||
where: {
|
||||
id,
|
||||
tenantId: ctx.tenantId,
|
||||
deletedAt: null,
|
||||
} as any,
|
||||
deletedAt: IsNull(),
|
||||
},
|
||||
relations: ['partidas', 'partidas.concepto'],
|
||||
});
|
||||
}
|
||||
@ -131,8 +248,8 @@ export class PresupuestoService extends BaseService<Presupuesto> {
|
||||
where: {
|
||||
id: partidaId,
|
||||
tenantId: ctx.tenantId,
|
||||
deletedAt: null,
|
||||
} as any,
|
||||
deletedAt: IsNull(),
|
||||
},
|
||||
});
|
||||
|
||||
if (!partida) {
|
||||
@ -158,7 +275,7 @@ export class PresupuestoService extends BaseService<Presupuesto> {
|
||||
where: {
|
||||
id: partidaId,
|
||||
tenantId: ctx.tenantId,
|
||||
} as any,
|
||||
},
|
||||
});
|
||||
|
||||
if (!partida) {
|
||||
@ -191,7 +308,7 @@ export class PresupuestoService extends BaseService<Presupuesto> {
|
||||
const total = parseFloat(result?.total || '0');
|
||||
|
||||
await this.repository.update(
|
||||
{ id: presupuestoId },
|
||||
{ id: presupuestoId } as any,
|
||||
{ totalAmount: total, updatedById: ctx.userId }
|
||||
);
|
||||
}
|
||||
@ -210,7 +327,7 @@ export class PresupuestoService extends BaseService<Presupuesto> {
|
||||
|
||||
// Desactivar versión anterior
|
||||
await this.repository.update(
|
||||
{ id: presupuestoId },
|
||||
{ id: presupuestoId } as any,
|
||||
{ isActive: false, updatedById: ctx.userId }
|
||||
);
|
||||
|
||||
|
||||
@ -11,23 +11,25 @@ import { DataSource } from 'typeorm';
|
||||
import { AnticipoService, CreateAnticipoDto, AnticipoFilters } from '../services/anticipo.service';
|
||||
import { AuthMiddleware } from '../../auth/middleware/auth.middleware';
|
||||
import { AuthService } from '../../auth/services/auth.service';
|
||||
import { Anticipo } from '../entities/anticipo.entity';
|
||||
import { User } from '../../core/entities/user.entity';
|
||||
import { Tenant } from '../../core/entities/tenant.entity';
|
||||
import { RefreshToken } from '../../auth/entities/refresh-token.entity';
|
||||
import { ServiceContext } from '../../../shared/services/base.service';
|
||||
|
||||
interface ServiceContext {
|
||||
tenantId: string;
|
||||
userId?: string;
|
||||
}
|
||||
|
||||
export function createAnticipoController(dataSource: DataSource): Router {
|
||||
const router = Router();
|
||||
|
||||
// Repositorios
|
||||
const anticipoRepository = dataSource.getRepository(Anticipo);
|
||||
const userRepository = dataSource.getRepository(User);
|
||||
const tenantRepository = dataSource.getRepository(Tenant);
|
||||
const refreshTokenRepository = dataSource.getRepository(RefreshToken);
|
||||
|
||||
// Servicios
|
||||
const anticipoService = new AnticipoService(anticipoRepository);
|
||||
const anticipoService = new AnticipoService(dataSource);
|
||||
const authService = new AuthService(userRepository, tenantRepository, refreshTokenRepository as any);
|
||||
const authMiddleware = new AuthMiddleware(authService, dataSource);
|
||||
|
||||
@ -69,7 +71,12 @@ export function createAnticipoController(dataSource: DataSource): Router {
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: result.data,
|
||||
pagination: result.meta,
|
||||
pagination: {
|
||||
total: result.total,
|
||||
page: result.page,
|
||||
limit: result.limit,
|
||||
totalPages: result.totalPages,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
@ -120,7 +127,12 @@ export function createAnticipoController(dataSource: DataSource): Router {
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: result.data,
|
||||
pagination: result.meta,
|
||||
pagination: {
|
||||
total: result.total,
|
||||
page: result.page,
|
||||
limit: result.limit,
|
||||
totalPages: result.totalPages,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
|
||||
@ -18,14 +18,17 @@ import {
|
||||
} from '../services/estimacion.service';
|
||||
import { AuthMiddleware } from '../../auth/middleware/auth.middleware';
|
||||
import { AuthService } from '../../auth/services/auth.service';
|
||||
import { Estimacion } from '../entities/estimacion.entity';
|
||||
import { EstimacionConcepto } from '../entities/estimacion-concepto.entity';
|
||||
import { Generador } from '../entities/generador.entity';
|
||||
import { EstimacionWorkflow } from '../entities/estimacion-workflow.entity';
|
||||
import { User } from '../../core/entities/user.entity';
|
||||
import { Tenant } from '../../core/entities/tenant.entity';
|
||||
import { RefreshToken } from '../../auth/entities/refresh-token.entity';
|
||||
import { ServiceContext } from '../../../shared/services/base.service';
|
||||
|
||||
/**
|
||||
* Local interface for service context
|
||||
*/
|
||||
interface ServiceContext {
|
||||
tenantId: string;
|
||||
userId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Crear router de estimaciones
|
||||
@ -34,22 +37,12 @@ export function createEstimacionController(dataSource: DataSource): Router {
|
||||
const router = Router();
|
||||
|
||||
// Repositorios
|
||||
const estimacionRepository = dataSource.getRepository(Estimacion);
|
||||
const conceptoRepository = dataSource.getRepository(EstimacionConcepto);
|
||||
const generadorRepository = dataSource.getRepository(Generador);
|
||||
const workflowRepository = dataSource.getRepository(EstimacionWorkflow);
|
||||
const userRepository = dataSource.getRepository(User);
|
||||
const tenantRepository = dataSource.getRepository(Tenant);
|
||||
const refreshTokenRepository = dataSource.getRepository(RefreshToken);
|
||||
|
||||
// Servicios
|
||||
const estimacionService = new EstimacionService(
|
||||
estimacionRepository,
|
||||
conceptoRepository,
|
||||
generadorRepository,
|
||||
workflowRepository,
|
||||
dataSource
|
||||
);
|
||||
const estimacionService = new EstimacionService(dataSource);
|
||||
const authService = new AuthService(userRepository, tenantRepository, refreshTokenRepository as any);
|
||||
const authMiddleware = new AuthMiddleware(authService, dataSource);
|
||||
|
||||
@ -92,7 +85,12 @@ export function createEstimacionController(dataSource: DataSource): Router {
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: result.data,
|
||||
pagination: result.meta,
|
||||
pagination: {
|
||||
total: result.total,
|
||||
page: result.page,
|
||||
limit: result.limit,
|
||||
totalPages: result.totalPages,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
|
||||
@ -11,23 +11,28 @@ import { DataSource } from 'typeorm';
|
||||
import { FondoGarantiaService, CreateFondoGarantiaDto, ReleaseFondoDto } from '../services/fondo-garantia.service';
|
||||
import { AuthMiddleware } from '../../auth/middleware/auth.middleware';
|
||||
import { AuthService } from '../../auth/services/auth.service';
|
||||
import { FondoGarantia } from '../entities/fondo-garantia.entity';
|
||||
import { User } from '../../core/entities/user.entity';
|
||||
import { Tenant } from '../../core/entities/tenant.entity';
|
||||
import { RefreshToken } from '../../auth/entities/refresh-token.entity';
|
||||
import { ServiceContext } from '../../../shared/services/base.service';
|
||||
|
||||
/**
|
||||
* Local interface for service context
|
||||
*/
|
||||
interface ServiceContext {
|
||||
tenantId: string;
|
||||
userId?: string;
|
||||
}
|
||||
|
||||
export function createFondoGarantiaController(dataSource: DataSource): Router {
|
||||
const router = Router();
|
||||
|
||||
// Repositorios
|
||||
const fondoGarantiaRepository = dataSource.getRepository(FondoGarantia);
|
||||
const userRepository = dataSource.getRepository(User);
|
||||
const tenantRepository = dataSource.getRepository(Tenant);
|
||||
const refreshTokenRepository = dataSource.getRepository(RefreshToken);
|
||||
|
||||
// Servicios
|
||||
const fondoGarantiaService = new FondoGarantiaService(fondoGarantiaRepository);
|
||||
const fondoGarantiaService = new FondoGarantiaService(dataSource);
|
||||
const authService = new AuthService(userRepository, tenantRepository, refreshTokenRepository as any);
|
||||
const authMiddleware = new AuthMiddleware(authService, dataSource);
|
||||
|
||||
@ -60,7 +65,12 @@ export function createFondoGarantiaController(dataSource: DataSource): Router {
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: result.data,
|
||||
pagination: result.meta,
|
||||
pagination: {
|
||||
total: result.total,
|
||||
page: result.page,
|
||||
limit: result.limit,
|
||||
totalPages: result.totalPages,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
|
||||
@ -11,23 +11,28 @@ import { DataSource } from 'typeorm';
|
||||
import { RetencionService, CreateRetencionDto, ReleaseRetencionDto, RetencionFilters } from '../services/retencion.service';
|
||||
import { AuthMiddleware } from '../../auth/middleware/auth.middleware';
|
||||
import { AuthService } from '../../auth/services/auth.service';
|
||||
import { Retencion } from '../entities/retencion.entity';
|
||||
import { User } from '../../core/entities/user.entity';
|
||||
import { Tenant } from '../../core/entities/tenant.entity';
|
||||
import { RefreshToken } from '../../auth/entities/refresh-token.entity';
|
||||
import { ServiceContext } from '../../../shared/services/base.service';
|
||||
|
||||
/**
|
||||
* Local interface for service context
|
||||
*/
|
||||
interface ServiceContext {
|
||||
tenantId: string;
|
||||
userId?: string;
|
||||
}
|
||||
|
||||
export function createRetencionController(dataSource: DataSource): Router {
|
||||
const router = Router();
|
||||
|
||||
// Repositorios
|
||||
const retencionRepository = dataSource.getRepository(Retencion);
|
||||
const userRepository = dataSource.getRepository(User);
|
||||
const tenantRepository = dataSource.getRepository(Tenant);
|
||||
const refreshTokenRepository = dataSource.getRepository(RefreshToken);
|
||||
|
||||
// Servicios
|
||||
const retencionService = new RetencionService(retencionRepository);
|
||||
const retencionService = new RetencionService(dataSource);
|
||||
const authService = new AuthService(userRepository, tenantRepository, refreshTokenRepository as any);
|
||||
const authMiddleware = new AuthMiddleware(authService, dataSource);
|
||||
|
||||
@ -70,7 +75,12 @@ export function createRetencionController(dataSource: DataSource): Router {
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: result.data,
|
||||
pagination: result.meta,
|
||||
pagination: {
|
||||
total: result.total,
|
||||
page: result.page,
|
||||
limit: result.limit,
|
||||
totalPages: result.totalPages,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
|
||||
@ -6,10 +6,22 @@
|
||||
* @module Estimates
|
||||
*/
|
||||
|
||||
import { Repository } from 'typeorm';
|
||||
import { BaseService, ServiceContext, PaginatedResult } from '../../../shared/services/base.service';
|
||||
import { DataSource, Repository, IsNull } from 'typeorm';
|
||||
import { Anticipo, AdvanceType } from '../entities/anticipo.entity';
|
||||
|
||||
interface ServiceContext {
|
||||
tenantId: string;
|
||||
userId?: string;
|
||||
}
|
||||
|
||||
interface PaginatedResult<T> {
|
||||
data: T[];
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
totalPages: number;
|
||||
}
|
||||
|
||||
export interface CreateAnticipoDto {
|
||||
contratoId: string;
|
||||
advanceType: AdvanceType;
|
||||
@ -45,9 +57,24 @@ export interface AnticipoStats {
|
||||
}[];
|
||||
}
|
||||
|
||||
export class AnticipoService extends BaseService<Anticipo> {
|
||||
constructor(repository: Repository<Anticipo>) {
|
||||
super(repository);
|
||||
export class AnticipoService {
|
||||
private repository: Repository<Anticipo>;
|
||||
|
||||
constructor(dataSource: DataSource) {
|
||||
this.repository = dataSource.getRepository(Anticipo);
|
||||
}
|
||||
|
||||
/**
|
||||
* Busca un anticipo por ID
|
||||
*/
|
||||
async findById(ctx: ServiceContext, id: string): Promise<Anticipo | null> {
|
||||
return this.repository.findOne({
|
||||
where: {
|
||||
id,
|
||||
tenantId: ctx.tenantId,
|
||||
deletedAt: IsNull(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -61,13 +88,13 @@ export class AnticipoService extends BaseService<Anticipo> {
|
||||
): Promise<PaginatedResult<Anticipo>> {
|
||||
const qb = this.repository
|
||||
.createQueryBuilder('a')
|
||||
.where('a.tenant_id = :tenantId', { tenantId: ctx.tenantId })
|
||||
.andWhere('a.contrato_id = :contratoId', { contratoId })
|
||||
.andWhere('a.deleted_at IS NULL');
|
||||
.where('a.tenantId = :tenantId', { tenantId: ctx.tenantId })
|
||||
.andWhere('a.contratoId = :contratoId', { contratoId })
|
||||
.andWhere('a.deletedAt IS NULL');
|
||||
|
||||
const skip = (page - 1) * limit;
|
||||
qb.orderBy('a.advance_date', 'DESC')
|
||||
.addOrderBy('a.advance_number', 'DESC')
|
||||
qb.orderBy('a.advanceDate', 'DESC')
|
||||
.addOrderBy('a.advanceNumber', 'DESC')
|
||||
.skip(skip)
|
||||
.take(limit);
|
||||
|
||||
@ -75,12 +102,10 @@ export class AnticipoService extends BaseService<Anticipo> {
|
||||
|
||||
return {
|
||||
data,
|
||||
meta: {
|
||||
total,
|
||||
page,
|
||||
limit,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@ -97,11 +122,57 @@ export class AnticipoService extends BaseService<Anticipo> {
|
||||
throw new Error('El monto neto no coincide con el cálculo (bruto - impuestos)');
|
||||
}
|
||||
|
||||
return this.create(ctx, {
|
||||
...dto,
|
||||
const anticipo = this.repository.create({
|
||||
tenantId: ctx.tenantId,
|
||||
contratoId: dto.contratoId,
|
||||
advanceType: dto.advanceType,
|
||||
advanceNumber: dto.advanceNumber,
|
||||
advanceDate: dto.advanceDate,
|
||||
grossAmount: dto.grossAmount,
|
||||
taxAmount: dto.taxAmount,
|
||||
netAmount: dto.netAmount,
|
||||
amortizationPercentage: dto.amortizationPercentage,
|
||||
amortizedAmount: 0,
|
||||
isFullyAmortized: false,
|
||||
notes: dto.notes,
|
||||
createdBy: ctx.userId,
|
||||
});
|
||||
|
||||
return this.repository.save(anticipo);
|
||||
}
|
||||
|
||||
/**
|
||||
* Actualiza un anticipo
|
||||
*/
|
||||
async update(
|
||||
ctx: ServiceContext,
|
||||
id: string,
|
||||
data: Partial<Anticipo>
|
||||
): Promise<Anticipo | null> {
|
||||
const anticipo = await this.findById(ctx, id);
|
||||
if (!anticipo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Object.assign(anticipo, data);
|
||||
anticipo.updatedById = ctx.userId;
|
||||
|
||||
return this.repository.save(anticipo);
|
||||
}
|
||||
|
||||
/**
|
||||
* Soft delete de un anticipo
|
||||
*/
|
||||
async softDelete(ctx: ServiceContext, id: string): Promise<boolean> {
|
||||
const anticipo = await this.findById(ctx, id);
|
||||
if (!anticipo) {
|
||||
return false;
|
||||
}
|
||||
|
||||
anticipo.deletedAt = new Date();
|
||||
anticipo.deletedById = ctx.userId;
|
||||
await this.repository.save(anticipo);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -122,23 +193,18 @@ export class AnticipoService extends BaseService<Anticipo> {
|
||||
throw new Error('El anticipo ya está aprobado');
|
||||
}
|
||||
|
||||
const updateData: Partial<Anticipo> = {
|
||||
approvedAt: new Date(),
|
||||
approvedById,
|
||||
};
|
||||
anticipo.approvedAt = new Date();
|
||||
anticipo.approvedById = approvedById;
|
||||
|
||||
if (notes) {
|
||||
updateData.notes = anticipo.notes
|
||||
anticipo.notes = anticipo.notes
|
||||
? `${anticipo.notes}\n\nAprobación: ${notes}`
|
||||
: `Aprobación: ${notes}`;
|
||||
}
|
||||
|
||||
const updated = await this.update(ctx, id, updateData);
|
||||
if (!updated) {
|
||||
throw new Error('Error al aprobar anticipo');
|
||||
}
|
||||
anticipo.updatedById = ctx.userId;
|
||||
|
||||
return updated;
|
||||
return this.repository.save(anticipo);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -163,16 +229,11 @@ export class AnticipoService extends BaseService<Anticipo> {
|
||||
throw new Error('El anticipo ya está marcado como pagado');
|
||||
}
|
||||
|
||||
const updated = await this.update(ctx, id, {
|
||||
paidAt: paidAt || new Date(),
|
||||
paymentReference,
|
||||
});
|
||||
anticipo.paidAt = paidAt || new Date();
|
||||
anticipo.paymentReference = paymentReference;
|
||||
anticipo.updatedById = ctx.userId;
|
||||
|
||||
if (!updated) {
|
||||
throw new Error('Error al marcar anticipo como pagado');
|
||||
}
|
||||
|
||||
return updated;
|
||||
return this.repository.save(anticipo);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -184,28 +245,28 @@ export class AnticipoService extends BaseService<Anticipo> {
|
||||
): Promise<AnticipoStats> {
|
||||
const qb = this.repository
|
||||
.createQueryBuilder('a')
|
||||
.where('a.tenant_id = :tenantId', { tenantId: ctx.tenantId })
|
||||
.andWhere('a.deleted_at IS NULL');
|
||||
.where('a.tenantId = :tenantId', { tenantId: ctx.tenantId })
|
||||
.andWhere('a.deletedAt IS NULL');
|
||||
|
||||
if (filters?.contratoId) {
|
||||
qb.andWhere('a.contrato_id = :contratoId', { contratoId: filters.contratoId });
|
||||
qb.andWhere('a.contratoId = :contratoId', { contratoId: filters.contratoId });
|
||||
}
|
||||
if (filters?.advanceType) {
|
||||
qb.andWhere('a.advance_type = :advanceType', { advanceType: filters.advanceType });
|
||||
qb.andWhere('a.advanceType = :advanceType', { advanceType: filters.advanceType });
|
||||
}
|
||||
if (filters?.isFullyAmortized !== undefined) {
|
||||
qb.andWhere('a.is_fully_amortized = :isFullyAmortized', {
|
||||
qb.andWhere('a.isFullyAmortized = :isFullyAmortized', {
|
||||
isFullyAmortized: filters.isFullyAmortized,
|
||||
});
|
||||
}
|
||||
if (filters?.startDate) {
|
||||
qb.andWhere('a.advance_date >= :startDate', { startDate: filters.startDate });
|
||||
qb.andWhere('a.advanceDate >= :startDate', { startDate: filters.startDate });
|
||||
}
|
||||
if (filters?.endDate) {
|
||||
qb.andWhere('a.advance_date <= :endDate', { endDate: filters.endDate });
|
||||
qb.andWhere('a.advanceDate <= :endDate', { endDate: filters.endDate });
|
||||
}
|
||||
if (filters?.approvedById) {
|
||||
qb.andWhere('a.approved_by = :approvedById', { approvedById: filters.approvedById });
|
||||
qb.andWhere('a.approvedById = :approvedById', { approvedById: filters.approvedById });
|
||||
}
|
||||
|
||||
const anticipos = await qb.getMany();
|
||||
@ -222,7 +283,7 @@ export class AnticipoService extends BaseService<Anticipo> {
|
||||
|
||||
const typeStats = new Map<AdvanceType, { count: number; totalAmount: number }>();
|
||||
|
||||
anticipos.forEach((anticipo) => {
|
||||
anticipos.forEach((anticipo: Anticipo) => {
|
||||
const netAmount = Number(anticipo.netAmount) || 0;
|
||||
const amortizedAmount = Number(anticipo.amortizedAmount) || 0;
|
||||
|
||||
@ -267,33 +328,33 @@ export class AnticipoService extends BaseService<Anticipo> {
|
||||
): Promise<PaginatedResult<Anticipo>> {
|
||||
const qb = this.repository
|
||||
.createQueryBuilder('a')
|
||||
.where('a.tenant_id = :tenantId', { tenantId: ctx.tenantId })
|
||||
.andWhere('a.deleted_at IS NULL');
|
||||
.where('a.tenantId = :tenantId', { tenantId: ctx.tenantId })
|
||||
.andWhere('a.deletedAt IS NULL');
|
||||
|
||||
if (filters.contratoId) {
|
||||
qb.andWhere('a.contrato_id = :contratoId', { contratoId: filters.contratoId });
|
||||
qb.andWhere('a.contratoId = :contratoId', { contratoId: filters.contratoId });
|
||||
}
|
||||
if (filters.advanceType) {
|
||||
qb.andWhere('a.advance_type = :advanceType', { advanceType: filters.advanceType });
|
||||
qb.andWhere('a.advanceType = :advanceType', { advanceType: filters.advanceType });
|
||||
}
|
||||
if (filters.isFullyAmortized !== undefined) {
|
||||
qb.andWhere('a.is_fully_amortized = :isFullyAmortized', {
|
||||
qb.andWhere('a.isFullyAmortized = :isFullyAmortized', {
|
||||
isFullyAmortized: filters.isFullyAmortized,
|
||||
});
|
||||
}
|
||||
if (filters.startDate) {
|
||||
qb.andWhere('a.advance_date >= :startDate', { startDate: filters.startDate });
|
||||
qb.andWhere('a.advanceDate >= :startDate', { startDate: filters.startDate });
|
||||
}
|
||||
if (filters.endDate) {
|
||||
qb.andWhere('a.advance_date <= :endDate', { endDate: filters.endDate });
|
||||
qb.andWhere('a.advanceDate <= :endDate', { endDate: filters.endDate });
|
||||
}
|
||||
if (filters.approvedById) {
|
||||
qb.andWhere('a.approved_by = :approvedById', { approvedById: filters.approvedById });
|
||||
qb.andWhere('a.approvedById = :approvedById', { approvedById: filters.approvedById });
|
||||
}
|
||||
|
||||
const skip = (page - 1) * limit;
|
||||
qb.orderBy('a.advance_date', 'DESC')
|
||||
.addOrderBy('a.advance_number', 'DESC')
|
||||
qb.orderBy('a.advanceDate', 'DESC')
|
||||
.addOrderBy('a.advanceNumber', 'DESC')
|
||||
.skip(skip)
|
||||
.take(limit);
|
||||
|
||||
@ -301,12 +362,10 @@ export class AnticipoService extends BaseService<Anticipo> {
|
||||
|
||||
return {
|
||||
data,
|
||||
meta: {
|
||||
total,
|
||||
page,
|
||||
limit,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@ -337,15 +396,10 @@ export class AnticipoService extends BaseService<Anticipo> {
|
||||
|
||||
const isFullyAmortized = Math.abs(newAmortizedAmount - netAmount) < 0.01;
|
||||
|
||||
const updated = await this.update(ctx, id, {
|
||||
amortizedAmount: newAmortizedAmount,
|
||||
isFullyAmortized,
|
||||
});
|
||||
anticipo.amortizedAmount = newAmortizedAmount;
|
||||
anticipo.isFullyAmortized = isFullyAmortized;
|
||||
anticipo.updatedById = ctx.userId;
|
||||
|
||||
if (!updated) {
|
||||
throw new Error('Error al actualizar amortización');
|
||||
}
|
||||
|
||||
return updated;
|
||||
return this.repository.save(anticipo);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,19 +1,31 @@
|
||||
/**
|
||||
* EstimacionService - Gestión de Estimaciones de Obra
|
||||
* EstimacionService - Gestion de Estimaciones de Obra
|
||||
*
|
||||
* Gestiona estimaciones periódicas con workflow de aprobación.
|
||||
* Incluye cálculo de anticipos, retenciones e IVA.
|
||||
* Gestiona estimaciones periodicas con workflow de aprobacion.
|
||||
* Incluye calculo de anticipos, retenciones e IVA.
|
||||
*
|
||||
* @module Estimates
|
||||
*/
|
||||
|
||||
import { Repository, DataSource } from 'typeorm';
|
||||
import { BaseService, ServiceContext, PaginatedResult } from '../../../shared/services/base.service';
|
||||
import { DataSource, Repository, IsNull } from 'typeorm';
|
||||
import { Estimacion, EstimateStatus } from '../entities/estimacion.entity';
|
||||
import { EstimacionConcepto } from '../entities/estimacion-concepto.entity';
|
||||
import { Generador } from '../entities/generador.entity';
|
||||
import { EstimacionWorkflow } from '../entities/estimacion-workflow.entity';
|
||||
|
||||
interface ServiceContext {
|
||||
tenantId: string;
|
||||
userId?: string;
|
||||
}
|
||||
|
||||
interface PaginatedResult<T> {
|
||||
data: T[];
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
totalPages: number;
|
||||
}
|
||||
|
||||
export interface CreateEstimacionDto {
|
||||
contratoId: string;
|
||||
fraccionamientoId: string;
|
||||
@ -52,19 +64,74 @@ export interface EstimacionFilters {
|
||||
periodTo?: Date;
|
||||
}
|
||||
|
||||
export class EstimacionService extends BaseService<Estimacion> {
|
||||
constructor(
|
||||
repository: Repository<Estimacion>,
|
||||
private readonly conceptoRepository: Repository<EstimacionConcepto>,
|
||||
private readonly generadorRepository: Repository<Generador>,
|
||||
private readonly workflowRepository: Repository<EstimacionWorkflow>,
|
||||
private readonly dataSource: DataSource
|
||||
) {
|
||||
super(repository);
|
||||
interface ContractEstimateSummary {
|
||||
totalEstimates: number;
|
||||
totalApproved: number;
|
||||
totalPaid: number;
|
||||
}
|
||||
|
||||
export class EstimacionService {
|
||||
private repository: Repository<Estimacion>;
|
||||
private conceptoRepository: Repository<EstimacionConcepto>;
|
||||
private generadorRepository: Repository<Generador>;
|
||||
private workflowRepository: Repository<EstimacionWorkflow>;
|
||||
|
||||
constructor(private readonly dataSource: DataSource) {
|
||||
this.repository = dataSource.getRepository(Estimacion);
|
||||
this.conceptoRepository = dataSource.getRepository(EstimacionConcepto);
|
||||
this.generadorRepository = dataSource.getRepository(Generador);
|
||||
this.workflowRepository = dataSource.getRepository(EstimacionWorkflow);
|
||||
}
|
||||
|
||||
/**
|
||||
* Crear nueva estimación
|
||||
* Busca una estimacion por ID
|
||||
*/
|
||||
async findById(ctx: ServiceContext, id: string): Promise<Estimacion | null> {
|
||||
return this.repository.findOne({
|
||||
where: {
|
||||
id,
|
||||
tenantId: ctx.tenantId,
|
||||
deletedAt: IsNull(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Actualiza una estimacion
|
||||
*/
|
||||
async update(
|
||||
ctx: ServiceContext,
|
||||
id: string,
|
||||
data: Partial<Estimacion>
|
||||
): Promise<Estimacion | null> {
|
||||
const estimacion = await this.findById(ctx, id);
|
||||
if (!estimacion) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Object.assign(estimacion, data);
|
||||
estimacion.updatedById = ctx.userId;
|
||||
|
||||
return this.repository.save(estimacion);
|
||||
}
|
||||
|
||||
/**
|
||||
* Soft delete de una estimacion
|
||||
*/
|
||||
async softDelete(ctx: ServiceContext, id: string): Promise<boolean> {
|
||||
const estimacion = await this.findById(ctx, id);
|
||||
if (!estimacion) {
|
||||
return false;
|
||||
}
|
||||
|
||||
estimacion.deletedAt = new Date();
|
||||
estimacion.deletedById = ctx.userId;
|
||||
await this.repository.save(estimacion);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Crear nueva estimacion
|
||||
*/
|
||||
async createEstimacion(
|
||||
ctx: ServiceContext,
|
||||
@ -73,21 +140,29 @@ export class EstimacionService extends BaseService<Estimacion> {
|
||||
const sequenceNumber = await this.getNextSequenceNumber(ctx, data.contratoId);
|
||||
const estimateNumber = await this.generateEstimateNumber(ctx, data.contratoId, sequenceNumber);
|
||||
|
||||
const estimacion = await this.create(ctx, {
|
||||
...data,
|
||||
const estimacion = this.repository.create({
|
||||
tenantId: ctx.tenantId,
|
||||
contratoId: data.contratoId,
|
||||
fraccionamientoId: data.fraccionamientoId,
|
||||
periodStart: data.periodStart,
|
||||
periodEnd: data.periodEnd,
|
||||
notes: data.notes,
|
||||
estimateNumber,
|
||||
sequenceNumber,
|
||||
status: 'draft',
|
||||
status: 'draft' as EstimateStatus,
|
||||
createdById: ctx.userId,
|
||||
});
|
||||
|
||||
// Registrar en workflow
|
||||
await this.addWorkflowEntry(ctx, estimacion.id, null, 'draft', 'create', 'Estimación creada');
|
||||
const savedEstimacion = await this.repository.save(estimacion);
|
||||
|
||||
return estimacion;
|
||||
// Registrar en workflow
|
||||
await this.addWorkflowEntry(ctx, savedEstimacion.id, null, 'draft', 'create', 'Estimacion creada');
|
||||
|
||||
return savedEstimacion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtener siguiente número de secuencia
|
||||
* Obtener siguiente numero de secuencia
|
||||
*/
|
||||
private async getNextSequenceNumber(
|
||||
ctx: ServiceContext,
|
||||
@ -104,7 +179,7 @@ export class EstimacionService extends BaseService<Estimacion> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Generar número de estimación
|
||||
* Generar numero de estimacion
|
||||
*/
|
||||
private async generateEstimateNumber(
|
||||
_ctx: ServiceContext,
|
||||
@ -124,11 +199,24 @@ export class EstimacionService extends BaseService<Estimacion> {
|
||||
page = 1,
|
||||
limit = 20
|
||||
): Promise<PaginatedResult<Estimacion>> {
|
||||
return this.findAll(ctx, {
|
||||
const qb = this.repository
|
||||
.createQueryBuilder('e')
|
||||
.where('e.tenant_id = :tenantId', { tenantId: ctx.tenantId })
|
||||
.andWhere('e.contrato_id = :contratoId', { contratoId })
|
||||
.andWhere('e.deleted_at IS NULL');
|
||||
|
||||
const skip = (page - 1) * limit;
|
||||
qb.orderBy('e.sequence_number', 'DESC').skip(skip).take(limit);
|
||||
|
||||
const [data, total] = await qb.getManyAndCount();
|
||||
|
||||
return {
|
||||
data,
|
||||
total,
|
||||
page,
|
||||
limit,
|
||||
where: { contratoId } as any,
|
||||
});
|
||||
totalPages: Math.ceil(total / limit),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@ -168,25 +256,23 @@ export class EstimacionService extends BaseService<Estimacion> {
|
||||
|
||||
return {
|
||||
data,
|
||||
meta: {
|
||||
total,
|
||||
page,
|
||||
limit,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtener estimación con detalles completos
|
||||
* Obtener estimacion con detalles completos
|
||||
*/
|
||||
async findWithDetails(ctx: ServiceContext, id: string): Promise<Estimacion | null> {
|
||||
return this.repository.findOne({
|
||||
where: {
|
||||
id,
|
||||
tenantId: ctx.tenantId,
|
||||
deletedAt: null,
|
||||
} as any,
|
||||
deletedAt: IsNull(),
|
||||
},
|
||||
relations: [
|
||||
'conceptos',
|
||||
'conceptos.concepto',
|
||||
@ -199,7 +285,7 @@ export class EstimacionService extends BaseService<Estimacion> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Agregar concepto a estimación
|
||||
* Agregar concepto a estimacion
|
||||
*/
|
||||
async addConcepto(
|
||||
ctx: ServiceContext,
|
||||
@ -231,7 +317,7 @@ export class EstimacionService extends BaseService<Estimacion> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Agregar generador a concepto de estimación
|
||||
* Agregar generador a concepto de estimacion
|
||||
*/
|
||||
async addGenerador(
|
||||
ctx: ServiceContext,
|
||||
@ -239,7 +325,7 @@ export class EstimacionService extends BaseService<Estimacion> {
|
||||
data: AddGeneradorDto
|
||||
): Promise<Generador> {
|
||||
const concepto = await this.conceptoRepository.findOne({
|
||||
where: { id: estimacionConceptoId, tenantId: ctx.tenantId } as any,
|
||||
where: { id: estimacionConceptoId, tenantId: ctx.tenantId },
|
||||
relations: ['estimacion'],
|
||||
});
|
||||
|
||||
@ -268,15 +354,15 @@ export class EstimacionService extends BaseService<Estimacion> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Recalcular totales de estimación
|
||||
* Recalcular totales de estimacion
|
||||
*/
|
||||
async recalculateTotals(_ctx: ServiceContext, estimacionId: string): Promise<void> {
|
||||
// Ejecutar función de PostgreSQL
|
||||
// Ejecutar funcion de PostgreSQL
|
||||
await this.dataSource.query('SELECT estimates.calculate_estimate_totals($1)', [estimacionId]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cambiar estado de estimación
|
||||
* Cambiar estado de estimacion
|
||||
*/
|
||||
async changeStatus(
|
||||
ctx: ServiceContext,
|
||||
@ -362,28 +448,28 @@ export class EstimacionService extends BaseService<Estimacion> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Enviar estimación para revisión
|
||||
* Enviar estimacion para revision
|
||||
*/
|
||||
async submit(ctx: ServiceContext, estimacionId: string): Promise<Estimacion | null> {
|
||||
return this.changeStatus(ctx, estimacionId, 'submitted', 'submit', 'Enviada para revisión');
|
||||
return this.changeStatus(ctx, estimacionId, 'submitted', 'submit', 'Enviada para revision');
|
||||
}
|
||||
|
||||
/**
|
||||
* Revisar estimación
|
||||
* Revisar estimacion
|
||||
*/
|
||||
async review(ctx: ServiceContext, estimacionId: string): Promise<Estimacion | null> {
|
||||
return this.changeStatus(ctx, estimacionId, 'reviewed', 'review', 'Revisión completada');
|
||||
return this.changeStatus(ctx, estimacionId, 'reviewed', 'review', 'Revision completada');
|
||||
}
|
||||
|
||||
/**
|
||||
* Aprobar estimación
|
||||
* Aprobar estimacion
|
||||
*/
|
||||
async approve(ctx: ServiceContext, estimacionId: string): Promise<Estimacion | null> {
|
||||
return this.changeStatus(ctx, estimacionId, 'approved', 'approve', 'Aprobada');
|
||||
}
|
||||
|
||||
/**
|
||||
* Rechazar estimación
|
||||
* Rechazar estimacion
|
||||
*/
|
||||
async reject(
|
||||
ctx: ServiceContext,
|
||||
@ -416,9 +502,3 @@ export class EstimacionService extends BaseService<Estimacion> {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
interface ContractEstimateSummary {
|
||||
totalEstimates: number;
|
||||
totalApproved: number;
|
||||
totalPaid: number;
|
||||
}
|
||||
|
||||
@ -6,10 +6,22 @@
|
||||
* @module Estimates
|
||||
*/
|
||||
|
||||
import { Repository } from 'typeorm';
|
||||
import { BaseService, ServiceContext, PaginatedResult } from '../../../shared/services/base.service';
|
||||
import { DataSource, Repository, IsNull } from 'typeorm';
|
||||
import { FondoGarantia } from '../entities/fondo-garantia.entity';
|
||||
|
||||
interface ServiceContext {
|
||||
tenantId: string;
|
||||
userId?: string;
|
||||
}
|
||||
|
||||
interface PaginatedResult<T> {
|
||||
data: T[];
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
totalPages: number;
|
||||
}
|
||||
|
||||
export interface CreateFondoGarantiaDto {
|
||||
contratoId: string;
|
||||
accumulatedAmount?: number;
|
||||
@ -39,9 +51,103 @@ export interface FondoGarantiaStats {
|
||||
fondosFullyReleased: number;
|
||||
}
|
||||
|
||||
export class FondoGarantiaService extends BaseService<FondoGarantia> {
|
||||
constructor(repository: Repository<FondoGarantia>) {
|
||||
super(repository);
|
||||
export class FondoGarantiaService {
|
||||
private repository: Repository<FondoGarantia>;
|
||||
|
||||
constructor(dataSource: DataSource) {
|
||||
this.repository = dataSource.getRepository(FondoGarantia);
|
||||
}
|
||||
|
||||
/**
|
||||
* Busca un fondo de garantía por ID
|
||||
*/
|
||||
async findById(ctx: ServiceContext, id: string): Promise<FondoGarantia | null> {
|
||||
return this.repository.findOne({
|
||||
where: {
|
||||
id,
|
||||
tenantId: ctx.tenantId,
|
||||
deletedAt: IsNull(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Busca todos los fondos de garantía con paginación
|
||||
*/
|
||||
async findAll(
|
||||
ctx: ServiceContext,
|
||||
page = 1,
|
||||
limit = 20
|
||||
): Promise<PaginatedResult<FondoGarantia>> {
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
const [data, total] = await this.repository.findAndCount({
|
||||
where: {
|
||||
tenantId: ctx.tenantId,
|
||||
deletedAt: IsNull(),
|
||||
},
|
||||
order: { createdAt: 'DESC' },
|
||||
skip,
|
||||
take: limit,
|
||||
});
|
||||
|
||||
return {
|
||||
data,
|
||||
total,
|
||||
page,
|
||||
limit,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Crea un nuevo fondo de garantía
|
||||
*/
|
||||
async create(
|
||||
ctx: ServiceContext,
|
||||
data: Partial<FondoGarantia>
|
||||
): Promise<FondoGarantia> {
|
||||
const fondo = this.repository.create({
|
||||
...data,
|
||||
tenantId: ctx.tenantId,
|
||||
createdById: ctx.userId ?? null,
|
||||
});
|
||||
|
||||
return this.repository.save(fondo);
|
||||
}
|
||||
|
||||
/**
|
||||
* Actualiza un fondo de garantía
|
||||
*/
|
||||
async update(
|
||||
ctx: ServiceContext,
|
||||
id: string,
|
||||
data: Partial<FondoGarantia>
|
||||
): Promise<FondoGarantia | null> {
|
||||
const fondo = await this.findById(ctx, id);
|
||||
if (!fondo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Object.assign(fondo, data);
|
||||
fondo.updatedById = ctx.userId ?? null;
|
||||
|
||||
return this.repository.save(fondo);
|
||||
}
|
||||
|
||||
/**
|
||||
* Soft delete de un fondo de garantía
|
||||
*/
|
||||
async softDelete(ctx: ServiceContext, id: string): Promise<boolean> {
|
||||
const fondo = await this.findById(ctx, id);
|
||||
if (!fondo) {
|
||||
return false;
|
||||
}
|
||||
|
||||
fondo.deletedAt = new Date();
|
||||
fondo.deletedById = ctx.userId ?? null;
|
||||
await this.repository.save(fondo);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -55,8 +161,8 @@ export class FondoGarantiaService extends BaseService<FondoGarantia> {
|
||||
where: {
|
||||
tenantId: ctx.tenantId,
|
||||
contratoId,
|
||||
deletedAt: null,
|
||||
} as any,
|
||||
deletedAt: IsNull(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@ -183,8 +289,8 @@ export class FondoGarantiaService extends BaseService<FondoGarantia> {
|
||||
const fondos = await this.repository.find({
|
||||
where: {
|
||||
tenantId: ctx.tenantId,
|
||||
deletedAt: null,
|
||||
} as any,
|
||||
deletedAt: IsNull(),
|
||||
},
|
||||
});
|
||||
|
||||
const stats: FondoGarantiaStats = {
|
||||
@ -260,16 +366,14 @@ export class FondoGarantiaService extends BaseService<FondoGarantia> {
|
||||
});
|
||||
}
|
||||
|
||||
const finalTotal = filteredData.length !== data.length ? filteredData.length : total;
|
||||
|
||||
return {
|
||||
data: filteredData,
|
||||
meta: {
|
||||
total: filteredData.length !== data.length ? filteredData.length : total,
|
||||
total: finalTotal,
|
||||
page,
|
||||
limit,
|
||||
totalPages: Math.ceil(
|
||||
(filteredData.length !== data.length ? filteredData.length : total) / limit
|
||||
),
|
||||
},
|
||||
totalPages: Math.ceil(finalTotal / limit),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,10 +6,22 @@
|
||||
* @module Estimates
|
||||
*/
|
||||
|
||||
import { Repository } from 'typeorm';
|
||||
import { BaseService, ServiceContext, PaginatedResult } from '../../../shared/services/base.service';
|
||||
import { DataSource, Repository, IsNull } from 'typeorm';
|
||||
import { Retencion, RetentionType } from '../entities/retencion.entity';
|
||||
|
||||
interface ServiceContext {
|
||||
tenantId: string;
|
||||
userId?: string;
|
||||
}
|
||||
|
||||
interface PaginatedResult<T> {
|
||||
data: T[];
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
totalPages: number;
|
||||
}
|
||||
|
||||
export interface CreateRetencionDto {
|
||||
estimacionId: string;
|
||||
retentionType: RetentionType;
|
||||
@ -34,9 +46,24 @@ export interface RetencionFilters {
|
||||
dateTo?: Date;
|
||||
}
|
||||
|
||||
export class RetencionService extends BaseService<Retencion> {
|
||||
constructor(repository: Repository<Retencion>) {
|
||||
super(repository);
|
||||
export class RetencionService {
|
||||
private repository: Repository<Retencion>;
|
||||
|
||||
constructor(dataSource: DataSource) {
|
||||
this.repository = dataSource.getRepository(Retencion);
|
||||
}
|
||||
|
||||
/**
|
||||
* Busca una retención por ID
|
||||
*/
|
||||
async findById(ctx: ServiceContext, id: string): Promise<Retencion | null> {
|
||||
return this.repository.findOne({
|
||||
where: {
|
||||
id,
|
||||
tenantId: ctx.tenantId,
|
||||
deletedAt: IsNull(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -50,8 +77,8 @@ export class RetencionService extends BaseService<Retencion> {
|
||||
where: {
|
||||
tenantId: ctx.tenantId,
|
||||
estimacionId,
|
||||
deletedAt: null,
|
||||
} as any,
|
||||
deletedAt: IsNull(),
|
||||
},
|
||||
order: { createdAt: 'DESC' },
|
||||
});
|
||||
}
|
||||
@ -63,11 +90,55 @@ export class RetencionService extends BaseService<Retencion> {
|
||||
ctx: ServiceContext,
|
||||
data: CreateRetencionDto
|
||||
): Promise<Retencion> {
|
||||
return this.create(ctx, {
|
||||
...data,
|
||||
const retencion = this.repository.create({
|
||||
tenantId: ctx.tenantId,
|
||||
estimacionId: data.estimacionId,
|
||||
retentionType: data.retentionType,
|
||||
description: data.description,
|
||||
percentage: data.percentage ?? null,
|
||||
amount: data.amount,
|
||||
releaseDate: data.releaseDate ?? null,
|
||||
notes: data.notes ?? null,
|
||||
releasedAmount: null,
|
||||
releasedAt: null,
|
||||
createdById: ctx.userId ?? null,
|
||||
});
|
||||
|
||||
return this.repository.save(retencion);
|
||||
}
|
||||
|
||||
/**
|
||||
* Actualiza una retención
|
||||
*/
|
||||
async update(
|
||||
ctx: ServiceContext,
|
||||
id: string,
|
||||
data: Partial<Retencion>
|
||||
): Promise<Retencion | null> {
|
||||
const retencion = await this.findById(ctx, id);
|
||||
if (!retencion) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Object.assign(retencion, data);
|
||||
retencion.updatedById = ctx.userId ?? null;
|
||||
|
||||
return this.repository.save(retencion);
|
||||
}
|
||||
|
||||
/**
|
||||
* Soft delete de una retención
|
||||
*/
|
||||
async softDelete(ctx: ServiceContext, id: string): Promise<boolean> {
|
||||
const retencion = await this.findById(ctx, id);
|
||||
if (!retencion) {
|
||||
return false;
|
||||
}
|
||||
|
||||
retencion.deletedAt = new Date();
|
||||
retencion.deletedById = ctx.userId ?? null;
|
||||
await this.repository.save(retencion);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -115,12 +186,10 @@ export class RetencionService extends BaseService<Retencion> {
|
||||
|
||||
return {
|
||||
data,
|
||||
meta: {
|
||||
total,
|
||||
page,
|
||||
limit,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -17,12 +17,17 @@ import {
|
||||
} from '../services/employee.service';
|
||||
import { AuthMiddleware } from '../../auth/middleware/auth.middleware';
|
||||
import { AuthService } from '../../auth/services/auth.service';
|
||||
import { Employee } from '../entities/employee.entity';
|
||||
import { EmployeeFraccionamiento } from '../entities/employee-fraccionamiento.entity';
|
||||
import { User } from '../../core/entities/user.entity';
|
||||
import { Tenant } from '../../core/entities/tenant.entity';
|
||||
import { RefreshToken } from '../../auth/entities/refresh-token.entity';
|
||||
import { ServiceContext } from '../../../shared/services/base.service';
|
||||
|
||||
/**
|
||||
* Local service context interface
|
||||
*/
|
||||
interface ServiceContext {
|
||||
tenantId: string;
|
||||
userId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Crear router de empleados
|
||||
@ -31,14 +36,12 @@ export function createEmployeeController(dataSource: DataSource): Router {
|
||||
const router = Router();
|
||||
|
||||
// Repositorios
|
||||
const employeeRepository = dataSource.getRepository(Employee);
|
||||
const asignacionRepository = dataSource.getRepository(EmployeeFraccionamiento);
|
||||
const userRepository = dataSource.getRepository(User);
|
||||
const tenantRepository = dataSource.getRepository(Tenant);
|
||||
const refreshTokenRepository = dataSource.getRepository(RefreshToken);
|
||||
|
||||
// Servicios
|
||||
const employeeService = new EmployeeService(employeeRepository, asignacionRepository);
|
||||
const employeeService = new EmployeeService(dataSource);
|
||||
const authService = new AuthService(userRepository, tenantRepository, refreshTokenRepository as any);
|
||||
const authMiddleware = new AuthMiddleware(authService, dataSource);
|
||||
|
||||
@ -81,7 +84,12 @@ export function createEmployeeController(dataSource: DataSource): Router {
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: result.data,
|
||||
pagination: result.meta,
|
||||
pagination: {
|
||||
total: result.total,
|
||||
page: result.page,
|
||||
limit: result.limit,
|
||||
totalPages: result.totalPages,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
|
||||
@ -11,11 +11,17 @@ import { DataSource } from 'typeorm';
|
||||
import { PuestoService, CreatePuestoDto, UpdatePuestoDto, PuestoFilters } from '../services/puesto.service';
|
||||
import { AuthMiddleware } from '../../auth/middleware/auth.middleware';
|
||||
import { AuthService } from '../../auth/services/auth.service';
|
||||
import { Puesto } from '../entities/puesto.entity';
|
||||
import { User } from '../../core/entities/user.entity';
|
||||
import { Tenant } from '../../core/entities/tenant.entity';
|
||||
import { RefreshToken } from '../../auth/entities/refresh-token.entity';
|
||||
import { ServiceContext } from '../../../shared/services/base.service';
|
||||
|
||||
/**
|
||||
* Local service context interface
|
||||
*/
|
||||
interface ServiceContext {
|
||||
tenantId: string;
|
||||
userId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Crear router de puestos
|
||||
@ -24,13 +30,12 @@ export function createPuestoController(dataSource: DataSource): Router {
|
||||
const router = Router();
|
||||
|
||||
// Repositorios
|
||||
const puestoRepository = dataSource.getRepository(Puesto);
|
||||
const userRepository = dataSource.getRepository(User);
|
||||
const tenantRepository = dataSource.getRepository(Tenant);
|
||||
const refreshTokenRepository = dataSource.getRepository(RefreshToken);
|
||||
|
||||
// Servicios
|
||||
const puestoService = new PuestoService(puestoRepository);
|
||||
const puestoService = new PuestoService(dataSource);
|
||||
const authService = new AuthService(userRepository, tenantRepository, refreshTokenRepository as any);
|
||||
const authMiddleware = new AuthMiddleware(authService, dataSource);
|
||||
|
||||
@ -71,7 +76,12 @@ export function createPuestoController(dataSource: DataSource): Router {
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: result.data,
|
||||
pagination: result.meta,
|
||||
pagination: {
|
||||
total: result.total,
|
||||
page: result.page,
|
||||
limit: result.limit,
|
||||
totalPages: result.totalPages,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
|
||||
@ -129,12 +129,10 @@ export class EmployeeService {
|
||||
|
||||
return {
|
||||
data,
|
||||
meta: {
|
||||
total,
|
||||
page,
|
||||
limit,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -71,12 +71,10 @@ export class PuestoService {
|
||||
|
||||
return {
|
||||
data,
|
||||
meta: {
|
||||
total,
|
||||
page,
|
||||
limit,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -35,7 +35,14 @@ import { QuejaAmbiental } from '../entities/queja-ambiental.entity';
|
||||
import { User } from '../../core/entities/user.entity';
|
||||
import { Tenant } from '../../core/entities/tenant.entity';
|
||||
import { RefreshToken } from '../../auth/entities/refresh-token.entity';
|
||||
import { ServiceContext } from '../../../shared/services/base.service';
|
||||
|
||||
/**
|
||||
* Service context for multi-tenant operations
|
||||
*/
|
||||
interface ServiceContext {
|
||||
tenantId: string;
|
||||
userId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Crear router de gestión ambiental
|
||||
@ -103,7 +110,12 @@ export function createAmbientalController(dataSource: DataSource): Router {
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: result.data,
|
||||
pagination: result.meta,
|
||||
pagination: {
|
||||
total: result.total,
|
||||
page: result.page,
|
||||
limit: result.limit,
|
||||
totalPages: result.totalPages,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
@ -163,7 +175,12 @@ export function createAmbientalController(dataSource: DataSource): Router {
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: result.data,
|
||||
pagination: result.meta,
|
||||
pagination: {
|
||||
total: result.total,
|
||||
page: result.page,
|
||||
limit: result.limit,
|
||||
totalPages: result.totalPages,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
@ -284,7 +301,12 @@ export function createAmbientalController(dataSource: DataSource): Router {
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: result.data,
|
||||
pagination: result.meta,
|
||||
pagination: {
|
||||
total: result.total,
|
||||
page: result.page,
|
||||
limit: result.limit,
|
||||
totalPages: result.totalPages,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
@ -417,7 +439,12 @@ export function createAmbientalController(dataSource: DataSource): Router {
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: result.data,
|
||||
pagination: result.meta,
|
||||
pagination: {
|
||||
total: result.total,
|
||||
page: result.page,
|
||||
limit: result.limit,
|
||||
totalPages: result.totalPages,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
@ -500,7 +527,12 @@ export function createAmbientalController(dataSource: DataSource): Router {
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: result.data,
|
||||
pagination: result.meta,
|
||||
pagination: {
|
||||
total: result.total,
|
||||
page: result.page,
|
||||
limit: result.limit,
|
||||
totalPages: result.totalPages,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
|
||||
@ -20,7 +20,14 @@ import { Capacitacion } from '../entities/capacitacion.entity';
|
||||
import { User } from '../../core/entities/user.entity';
|
||||
import { Tenant } from '../../core/entities/tenant.entity';
|
||||
import { RefreshToken } from '../../auth/entities/refresh-token.entity';
|
||||
import { ServiceContext } from '../../../shared/services/base.service';
|
||||
|
||||
/**
|
||||
* Service context for multi-tenant operations
|
||||
*/
|
||||
interface ServiceContext {
|
||||
tenantId: string;
|
||||
userId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Crear router de capacitaciones
|
||||
@ -76,7 +83,12 @@ export function createCapacitacionController(dataSource: DataSource): Router {
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: result.data,
|
||||
pagination: result.meta,
|
||||
pagination: {
|
||||
total: result.total,
|
||||
page: result.page,
|
||||
limit: result.limit,
|
||||
totalPages: result.totalPages,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
|
||||
@ -29,7 +29,14 @@ import { EppMovimiento } from '../entities/epp-movimiento.entity';
|
||||
import { User } from '../../core/entities/user.entity';
|
||||
import { Tenant } from '../../core/entities/tenant.entity';
|
||||
import { RefreshToken } from '../../auth/entities/refresh-token.entity';
|
||||
import { ServiceContext } from '../../../shared/services/base.service';
|
||||
|
||||
/**
|
||||
* Service context for multi-tenant operations
|
||||
*/
|
||||
interface ServiceContext {
|
||||
tenantId: string;
|
||||
userId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Crear router de EPP
|
||||
@ -101,7 +108,12 @@ export function createEppController(dataSource: DataSource): Router {
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: result.data,
|
||||
pagination: result.meta,
|
||||
pagination: {
|
||||
total: result.total,
|
||||
page: result.page,
|
||||
limit: result.limit,
|
||||
totalPages: result.totalPages,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
@ -228,7 +240,12 @@ export function createEppController(dataSource: DataSource): Router {
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: result.data,
|
||||
pagination: result.meta,
|
||||
pagination: {
|
||||
total: result.total,
|
||||
page: result.page,
|
||||
limit: result.limit,
|
||||
totalPages: result.totalPages,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
|
||||
@ -26,7 +26,14 @@ import { IncidenteAccion } from '../entities/incidente-accion.entity';
|
||||
import { User } from '../../core/entities/user.entity';
|
||||
import { Tenant } from '../../core/entities/tenant.entity';
|
||||
import { RefreshToken } from '../../auth/entities/refresh-token.entity';
|
||||
import { ServiceContext } from '../../../shared/services/base.service';
|
||||
|
||||
/**
|
||||
* Service context for multi-tenant operations
|
||||
*/
|
||||
interface ServiceContext {
|
||||
tenantId: string;
|
||||
userId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Crear router de incidentes
|
||||
@ -91,7 +98,12 @@ export function createIncidenteController(dataSource: DataSource): Router {
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: result.data,
|
||||
pagination: result.meta,
|
||||
pagination: {
|
||||
total: result.total,
|
||||
page: result.page,
|
||||
limit: result.limit,
|
||||
totalPages: result.totalPages,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
|
||||
@ -25,7 +25,14 @@ import { AlertaIndicador } from '../entities/alerta-indicador.entity';
|
||||
import { User } from '../../core/entities/user.entity';
|
||||
import { Tenant } from '../../core/entities/tenant.entity';
|
||||
import { RefreshToken } from '../../auth/entities/refresh-token.entity';
|
||||
import { ServiceContext } from '../../../shared/services/base.service';
|
||||
|
||||
/**
|
||||
* Service context for multi-tenant operations
|
||||
*/
|
||||
interface ServiceContext {
|
||||
tenantId: string;
|
||||
userId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Crear router de indicadores
|
||||
@ -97,7 +104,12 @@ export function createIndicadorController(dataSource: DataSource): Router {
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: result.data,
|
||||
pagination: result.meta,
|
||||
pagination: {
|
||||
total: result.total,
|
||||
page: result.page,
|
||||
limit: result.limit,
|
||||
totalPages: result.totalPages,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
@ -258,7 +270,12 @@ export function createIndicadorController(dataSource: DataSource): Router {
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: result.data,
|
||||
pagination: result.meta,
|
||||
pagination: {
|
||||
total: result.total,
|
||||
page: result.page,
|
||||
limit: result.limit,
|
||||
totalPages: result.totalPages,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
@ -341,7 +358,12 @@ export function createIndicadorController(dataSource: DataSource): Router {
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: result.data,
|
||||
pagination: result.meta,
|
||||
pagination: {
|
||||
total: result.total,
|
||||
page: result.page,
|
||||
limit: result.limit,
|
||||
totalPages: result.totalPages,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
|
||||
@ -25,7 +25,14 @@ import { TipoInspeccion } from '../entities/tipo-inspeccion.entity';
|
||||
import { User } from '../../core/entities/user.entity';
|
||||
import { Tenant } from '../../core/entities/tenant.entity';
|
||||
import { RefreshToken } from '../../auth/entities/refresh-token.entity';
|
||||
import { ServiceContext } from '../../../shared/services/base.service';
|
||||
|
||||
/**
|
||||
* Service context for multi-tenant operations
|
||||
*/
|
||||
interface ServiceContext {
|
||||
tenantId: string;
|
||||
userId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Crear router de inspecciones
|
||||
@ -107,7 +114,12 @@ export function createInspeccionController(dataSource: DataSource): Router {
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: result.data,
|
||||
pagination: result.meta,
|
||||
pagination: {
|
||||
total: result.total,
|
||||
page: result.page,
|
||||
limit: result.limit,
|
||||
totalPages: result.totalPages,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
@ -277,7 +289,12 @@ export function createInspeccionController(dataSource: DataSource): Router {
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: result.data,
|
||||
pagination: result.meta,
|
||||
pagination: {
|
||||
total: result.total,
|
||||
page: result.page,
|
||||
limit: result.limit,
|
||||
totalPages: result.totalPages,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
|
||||
@ -26,7 +26,14 @@ import { PermisoDocumento } from '../entities/permiso-documento.entity';
|
||||
import { User } from '../../core/entities/user.entity';
|
||||
import { Tenant } from '../../core/entities/tenant.entity';
|
||||
import { RefreshToken } from '../../auth/entities/refresh-token.entity';
|
||||
import { ServiceContext } from '../../../shared/services/base.service';
|
||||
|
||||
/**
|
||||
* Service context for multi-tenant operations
|
||||
*/
|
||||
interface ServiceContext {
|
||||
tenantId: string;
|
||||
userId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Crear router de permisos de trabajo
|
||||
|
||||
@ -37,7 +37,14 @@ import { Auditoria, ResultadoAuditoria } from '../entities/auditoria.entity';
|
||||
import { User } from '../../core/entities/user.entity';
|
||||
import { Tenant } from '../../core/entities/tenant.entity';
|
||||
import { RefreshToken } from '../../auth/entities/refresh-token.entity';
|
||||
import { ServiceContext } from '../../../shared/services/base.service';
|
||||
|
||||
/**
|
||||
* Service context for multi-tenant operations
|
||||
*/
|
||||
interface ServiceContext {
|
||||
tenantId: string;
|
||||
userId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Crear router de STPS
|
||||
|
||||
@ -177,7 +177,10 @@ export class AmbientalService {
|
||||
|
||||
return {
|
||||
data,
|
||||
meta: { total, page, limit, totalPages: Math.ceil(total / limit) },
|
||||
total,
|
||||
page,
|
||||
limit,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
};
|
||||
}
|
||||
|
||||
@ -242,7 +245,10 @@ export class AmbientalService {
|
||||
|
||||
return {
|
||||
data,
|
||||
meta: { total, page, limit, totalPages: Math.ceil(total / limit) },
|
||||
total,
|
||||
page,
|
||||
limit,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
};
|
||||
}
|
||||
|
||||
@ -341,7 +347,10 @@ export class AmbientalService {
|
||||
|
||||
return {
|
||||
data,
|
||||
meta: { total, page, limit, totalPages: Math.ceil(total / limit) },
|
||||
total,
|
||||
page,
|
||||
limit,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
};
|
||||
}
|
||||
|
||||
@ -441,7 +450,10 @@ export class AmbientalService {
|
||||
|
||||
return {
|
||||
data,
|
||||
meta: { total, page, limit, totalPages: Math.ceil(total / limit) },
|
||||
total,
|
||||
page,
|
||||
limit,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
};
|
||||
}
|
||||
|
||||
@ -526,7 +538,10 @@ export class AmbientalService {
|
||||
|
||||
return {
|
||||
data,
|
||||
meta: { total, page, limit, totalPages: Math.ceil(total / limit) },
|
||||
total,
|
||||
page,
|
||||
limit,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -77,12 +77,10 @@ export class CapacitacionService {
|
||||
|
||||
return {
|
||||
data,
|
||||
meta: {
|
||||
total,
|
||||
page,
|
||||
limit,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -131,7 +131,10 @@ export class EppService {
|
||||
|
||||
return {
|
||||
data,
|
||||
meta: { total, page, limit, totalPages: Math.ceil(total / limit) },
|
||||
total,
|
||||
page,
|
||||
limit,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
};
|
||||
}
|
||||
|
||||
@ -249,7 +252,10 @@ export class EppService {
|
||||
|
||||
return {
|
||||
data,
|
||||
meta: { total, page, limit, totalPages: Math.ceil(total / limit) },
|
||||
total,
|
||||
page,
|
||||
limit,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -143,12 +143,10 @@ export class IncidenteService {
|
||||
|
||||
return {
|
||||
data,
|
||||
meta: {
|
||||
total,
|
||||
page,
|
||||
limit,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -91,7 +91,10 @@ export class IndicadorService {
|
||||
|
||||
return {
|
||||
data,
|
||||
meta: { total, page, limit, totalPages: Math.ceil(total / limit) },
|
||||
total,
|
||||
page,
|
||||
limit,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
};
|
||||
}
|
||||
|
||||
@ -187,7 +190,10 @@ export class IndicadorService {
|
||||
|
||||
return {
|
||||
data,
|
||||
meta: { total, page, limit, totalPages: Math.ceil(total / limit) },
|
||||
total,
|
||||
page,
|
||||
limit,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
};
|
||||
}
|
||||
|
||||
@ -248,7 +254,10 @@ export class IndicadorService {
|
||||
|
||||
return {
|
||||
data,
|
||||
meta: { total, page, limit, totalPages: Math.ceil(total / limit) },
|
||||
total,
|
||||
page,
|
||||
limit,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -143,12 +143,10 @@ export class InspeccionService {
|
||||
|
||||
return {
|
||||
data,
|
||||
meta: {
|
||||
total,
|
||||
page,
|
||||
limit,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@ -258,12 +256,10 @@ export class InspeccionService {
|
||||
|
||||
return {
|
||||
data,
|
||||
meta: {
|
||||
total,
|
||||
page,
|
||||
limit,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -133,7 +133,10 @@ export class PermisoTrabajoService {
|
||||
|
||||
return {
|
||||
data,
|
||||
meta: { total, page, limit, totalPages: Math.ceil(total / limit) },
|
||||
total,
|
||||
page,
|
||||
limit,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -195,7 +195,10 @@ export class StpsService {
|
||||
|
||||
return {
|
||||
data,
|
||||
meta: { total, page, limit, totalPages: Math.ceil(total / limit) },
|
||||
total,
|
||||
page,
|
||||
limit,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
};
|
||||
}
|
||||
|
||||
@ -559,7 +562,10 @@ export class StpsService {
|
||||
|
||||
return {
|
||||
data,
|
||||
meta: { total, page, limit, totalPages: Math.ceil(total / limit) },
|
||||
total,
|
||||
page,
|
||||
limit,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -18,7 +18,14 @@ import { AuthService } from '../../auth/services/auth.service';
|
||||
import { User } from '../../core/entities/user.entity';
|
||||
import { Tenant } from '../../core/entities/tenant.entity';
|
||||
import { RefreshToken } from '../../auth/entities/refresh-token.entity';
|
||||
import { ServiceContext } from '../../../shared/services/base.service';
|
||||
|
||||
/**
|
||||
* Service context for multi-tenant operations
|
||||
*/
|
||||
interface ServiceContext {
|
||||
tenantId: string;
|
||||
userId?: string;
|
||||
}
|
||||
|
||||
export function createAsignacionController(dataSource: DataSource): Router {
|
||||
const router = Router();
|
||||
@ -70,7 +77,16 @@ export function createAsignacionController(dataSource: DataSource): Router {
|
||||
const limit = Math.min(parseInt(req.query.limit as string) || 20, 100);
|
||||
|
||||
const result = await service.findWithFilters(getContext(req), filters, page, limit);
|
||||
res.status(200).json({ success: true, data: result.data, pagination: result.meta });
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: result.data,
|
||||
pagination: {
|
||||
total: result.meta.total,
|
||||
page: result.meta.page,
|
||||
limit: result.meta.limit,
|
||||
totalPages: result.meta.totalPages,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
|
||||
@ -17,7 +17,14 @@ import { AuthService } from '../../auth/services/auth.service';
|
||||
import { User } from '../../core/entities/user.entity';
|
||||
import { Tenant } from '../../core/entities/tenant.entity';
|
||||
import { RefreshToken } from '../../auth/entities/refresh-token.entity';
|
||||
import { ServiceContext } from '../../../shared/services/base.service';
|
||||
|
||||
/**
|
||||
* Service context for multi-tenant operations
|
||||
*/
|
||||
interface ServiceContext {
|
||||
tenantId: string;
|
||||
userId?: string;
|
||||
}
|
||||
|
||||
export function createDerechohabienteController(dataSource: DataSource): Router {
|
||||
const router = Router();
|
||||
@ -68,7 +75,16 @@ export function createDerechohabienteController(dataSource: DataSource): Router
|
||||
const limit = Math.min(parseInt(req.query.limit as string) || 20, 100);
|
||||
|
||||
const result = await service.findWithFilters(getContext(req), filters, page, limit);
|
||||
res.status(200).json({ success: true, data: result.data, pagination: result.meta });
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: result.data,
|
||||
pagination: {
|
||||
total: result.meta.total,
|
||||
page: result.meta.page,
|
||||
limit: result.meta.limit,
|
||||
totalPages: result.meta.totalPages,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
|
||||
@ -19,7 +19,11 @@ import { ConsumoObra } from '../entities/consumo-obra.entity';
|
||||
import { User } from '../../core/entities/user.entity';
|
||||
import { Tenant } from '../../core/entities/tenant.entity';
|
||||
import { RefreshToken } from '../../auth/entities/refresh-token.entity';
|
||||
import { ServiceContext } from '../../../shared/services/base.service';
|
||||
|
||||
interface ServiceContext {
|
||||
tenantId: string;
|
||||
userId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Crear router de consumos
|
||||
@ -78,7 +82,12 @@ export function createConsumoObraController(dataSource: DataSource): Router {
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: result.data,
|
||||
pagination: result.meta,
|
||||
pagination: {
|
||||
total: result.total,
|
||||
page: result.page,
|
||||
limit: result.limit,
|
||||
totalPages: result.totalPages,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
|
||||
@ -22,7 +22,11 @@ import { RequisicionLinea } from '../entities/requisicion-linea.entity';
|
||||
import { User } from '../../core/entities/user.entity';
|
||||
import { Tenant } from '../../core/entities/tenant.entity';
|
||||
import { RefreshToken } from '../../auth/entities/refresh-token.entity';
|
||||
import { ServiceContext } from '../../../shared/services/base.service';
|
||||
|
||||
interface ServiceContext {
|
||||
tenantId: string;
|
||||
userId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Crear router de requisiciones
|
||||
@ -81,7 +85,12 @@ export function createRequisicionController(dataSource: DataSource): Router {
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: result.data,
|
||||
pagination: result.meta,
|
||||
pagination: {
|
||||
total: result.total,
|
||||
page: result.page,
|
||||
limit: result.limit,
|
||||
totalPages: result.totalPages,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
|
||||
@ -8,7 +8,19 @@
|
||||
|
||||
import { Repository, FindOptionsWhere } from 'typeorm';
|
||||
import { ConsumoObra } from '../entities/consumo-obra.entity';
|
||||
import { ServiceContext, PaginatedResult } from '../../../shared/services/base.service';
|
||||
|
||||
interface ServiceContext {
|
||||
tenantId: string;
|
||||
userId?: string;
|
||||
}
|
||||
|
||||
interface PaginatedResult<T> {
|
||||
data: T[];
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
totalPages: number;
|
||||
}
|
||||
|
||||
export interface CreateConsumoDto {
|
||||
fraccionamientoId: string;
|
||||
@ -94,12 +106,10 @@ export class ConsumoObraService {
|
||||
|
||||
return {
|
||||
data,
|
||||
meta: {
|
||||
total,
|
||||
page,
|
||||
limit,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -10,7 +10,19 @@
|
||||
import { Repository, FindOptionsWhere } from 'typeorm';
|
||||
import { RequisicionObra, RequisitionStatus } from '../entities/requisicion-obra.entity';
|
||||
import { RequisicionLinea } from '../entities/requisicion-linea.entity';
|
||||
import { ServiceContext, PaginatedResult } from '../../../shared/services/base.service';
|
||||
|
||||
interface ServiceContext {
|
||||
tenantId: string;
|
||||
userId?: string;
|
||||
}
|
||||
|
||||
interface PaginatedResult<T> {
|
||||
data: T[];
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
totalPages: number;
|
||||
}
|
||||
|
||||
export interface CreateRequisicionDto {
|
||||
fraccionamientoId: string;
|
||||
@ -98,12 +110,10 @@ export class RequisicionService {
|
||||
|
||||
return {
|
||||
data,
|
||||
meta: {
|
||||
total,
|
||||
page,
|
||||
limit,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -17,7 +17,14 @@ import { FotoAvance } from '../entities/foto-avance.entity';
|
||||
import { User } from '../../core/entities/user.entity';
|
||||
import { Tenant } from '../../core/entities/tenant.entity';
|
||||
import { RefreshToken } from '../../auth/entities/refresh-token.entity';
|
||||
import { ServiceContext } from '../../../shared/services/base.service';
|
||||
|
||||
/**
|
||||
* Service context for multi-tenant operations
|
||||
*/
|
||||
interface ServiceContext {
|
||||
tenantId: string;
|
||||
userId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Crear router de avances de obra
|
||||
@ -77,7 +84,12 @@ export function createAvanceObraController(dataSource: DataSource): Router {
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: result.data,
|
||||
pagination: result.meta,
|
||||
pagination: {
|
||||
total: result.meta.total,
|
||||
page: result.meta.page,
|
||||
limit: result.meta.limit,
|
||||
totalPages: result.meta.totalPages,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
|
||||
@ -15,7 +15,14 @@ import { BitacoraObra } from '../entities/bitacora-obra.entity';
|
||||
import { User } from '../../core/entities/user.entity';
|
||||
import { Tenant } from '../../core/entities/tenant.entity';
|
||||
import { RefreshToken } from '../../auth/entities/refresh-token.entity';
|
||||
import { ServiceContext } from '../../../shared/services/base.service';
|
||||
|
||||
/**
|
||||
* Service context for multi-tenant operations
|
||||
*/
|
||||
interface ServiceContext {
|
||||
tenantId: string;
|
||||
userId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Crear router de bitácora de obra
|
||||
@ -77,7 +84,12 @@ export function createBitacoraObraController(dataSource: DataSource): Router {
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: result.data,
|
||||
pagination: result.meta,
|
||||
pagination: {
|
||||
total: result.meta.total,
|
||||
page: result.meta.page,
|
||||
limit: result.meta.limit,
|
||||
totalPages: result.meta.totalPages,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
|
||||
@ -18,7 +18,11 @@ import { AuthService } from '../../auth/services/auth.service';
|
||||
import { User } from '../../core/entities/user.entity';
|
||||
import { Tenant } from '../../core/entities/tenant.entity';
|
||||
import { RefreshToken } from '../../auth/entities/refresh-token.entity';
|
||||
import { ServiceContext } from '../../../shared/services/base.service';
|
||||
|
||||
interface ServiceContext {
|
||||
tenantId: string;
|
||||
userId?: string;
|
||||
}
|
||||
|
||||
export function createComparativoController(dataSource: DataSource): Router {
|
||||
const router = Router();
|
||||
@ -77,7 +81,16 @@ export function createComparativoController(dataSource: DataSource): Router {
|
||||
const limit = Math.min(parseInt(req.query.limit as string) || 20, 100);
|
||||
|
||||
const result = await service.findWithFilters(getContext(req), filters, page, limit);
|
||||
res.status(200).json({ success: true, data: result.data, pagination: result.meta });
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: result.data,
|
||||
pagination: {
|
||||
total: result.total,
|
||||
page: result.page,
|
||||
limit: result.limit,
|
||||
totalPages: result.totalPages,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
|
||||
@ -10,7 +10,19 @@ import { Repository, FindOptionsWhere } from 'typeorm';
|
||||
import { ComparativoCotizaciones, ComparativoStatus } from '../entities/comparativo-cotizaciones.entity';
|
||||
import { ComparativoProveedor } from '../entities/comparativo-proveedor.entity';
|
||||
import { ComparativoProducto } from '../entities/comparativo-producto.entity';
|
||||
import { ServiceContext, PaginatedResult } from '../../../shared/services/base.service';
|
||||
|
||||
interface ServiceContext {
|
||||
tenantId: string;
|
||||
userId?: string;
|
||||
}
|
||||
|
||||
interface PaginatedResult<T> {
|
||||
data: T[];
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
totalPages: number;
|
||||
}
|
||||
|
||||
export interface CreateComparativoDto {
|
||||
requisicionId?: string;
|
||||
@ -99,12 +111,10 @@ export class ComparativoService {
|
||||
|
||||
return {
|
||||
data,
|
||||
meta: {
|
||||
total,
|
||||
page,
|
||||
limit,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -18,23 +18,25 @@ import {
|
||||
} from '../services/dashboard.service';
|
||||
import { AuthMiddleware } from '../../auth/middleware/auth.middleware';
|
||||
import { AuthService } from '../../auth/services/auth.service';
|
||||
import { Dashboard } from '../entities/dashboard.entity';
|
||||
import { User } from '../../core/entities/user.entity';
|
||||
import { Tenant } from '../../core/entities/tenant.entity';
|
||||
import { RefreshToken } from '../../auth/entities/refresh-token.entity';
|
||||
import { ServiceContext } from '../../../shared/services/base.service';
|
||||
|
||||
interface ServiceContext {
|
||||
tenantId: string;
|
||||
userId?: string;
|
||||
}
|
||||
|
||||
export function createDashboardController(dataSource: DataSource): Router {
|
||||
const router = Router();
|
||||
|
||||
// Repositorios
|
||||
const dashboardRepository = dataSource.getRepository(Dashboard);
|
||||
const userRepository = dataSource.getRepository(User);
|
||||
const tenantRepository = dataSource.getRepository(Tenant);
|
||||
const refreshTokenRepository = dataSource.getRepository(RefreshToken);
|
||||
|
||||
// Servicios
|
||||
const dashboardService = new DashboardService(dashboardRepository, dataSource);
|
||||
const dashboardService = new DashboardService(dataSource);
|
||||
const authService = new AuthService(userRepository, tenantRepository, refreshTokenRepository as any);
|
||||
const authMiddleware = new AuthMiddleware(authService, dataSource);
|
||||
|
||||
@ -76,7 +78,12 @@ export function createDashboardController(dataSource: DataSource): Router {
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: result.data,
|
||||
pagination: result.meta,
|
||||
pagination: {
|
||||
total: result.total,
|
||||
page: result.page,
|
||||
limit: result.limit,
|
||||
totalPages: result.totalPages,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
|
||||
@ -11,23 +11,25 @@ import { DataSource } from 'typeorm';
|
||||
import { KpiService, CreateKpiSnapshotDto, KpiFilters } from '../services/kpi.service';
|
||||
import { AuthMiddleware } from '../../auth/middleware/auth.middleware';
|
||||
import { AuthService } from '../../auth/services/auth.service';
|
||||
import { KpiSnapshot } from '../entities/kpi-snapshot.entity';
|
||||
import { User } from '../../core/entities/user.entity';
|
||||
import { Tenant } from '../../core/entities/tenant.entity';
|
||||
import { RefreshToken } from '../../auth/entities/refresh-token.entity';
|
||||
import { ServiceContext } from '../../../shared/services/base.service';
|
||||
|
||||
interface ServiceContext {
|
||||
tenantId: string;
|
||||
userId?: string;
|
||||
}
|
||||
|
||||
export function createKpiController(dataSource: DataSource): Router {
|
||||
const router = Router();
|
||||
|
||||
// Repositorios
|
||||
const kpiRepository = dataSource.getRepository(KpiSnapshot);
|
||||
const userRepository = dataSource.getRepository(User);
|
||||
const tenantRepository = dataSource.getRepository(Tenant);
|
||||
const refreshTokenRepository = dataSource.getRepository(RefreshToken);
|
||||
|
||||
// Servicios
|
||||
const kpiService = new KpiService(kpiRepository);
|
||||
const kpiService = new KpiService(dataSource);
|
||||
const authService = new AuthService(userRepository, tenantRepository, refreshTokenRepository as any);
|
||||
const authMiddleware = new AuthMiddleware(authService, dataSource);
|
||||
|
||||
@ -69,7 +71,12 @@ export function createKpiController(dataSource: DataSource): Router {
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: result.data,
|
||||
pagination: result.meta,
|
||||
pagination: {
|
||||
total: result.total,
|
||||
page: result.page,
|
||||
limit: result.limit,
|
||||
totalPages: result.totalPages,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
|
||||
@ -11,23 +11,25 @@ import { DataSource } from 'typeorm';
|
||||
import { ReportService, CreateReportDto, UpdateReportDto, ReportFilters, ExecuteReportDto } from '../services/report.service';
|
||||
import { AuthMiddleware } from '../../auth/middleware/auth.middleware';
|
||||
import { AuthService } from '../../auth/services/auth.service';
|
||||
import { Report } from '../entities/report.entity';
|
||||
import { User } from '../../core/entities/user.entity';
|
||||
import { Tenant } from '../../core/entities/tenant.entity';
|
||||
import { RefreshToken } from '../../auth/entities/refresh-token.entity';
|
||||
import { ServiceContext } from '../../../shared/services/base.service';
|
||||
|
||||
interface ServiceContext {
|
||||
tenantId: string;
|
||||
userId?: string;
|
||||
}
|
||||
|
||||
export function createReportController(dataSource: DataSource): Router {
|
||||
const router = Router();
|
||||
|
||||
// Repositorios
|
||||
const reportRepository = dataSource.getRepository(Report);
|
||||
const userRepository = dataSource.getRepository(User);
|
||||
const tenantRepository = dataSource.getRepository(Tenant);
|
||||
const refreshTokenRepository = dataSource.getRepository(RefreshToken);
|
||||
|
||||
// Servicios
|
||||
const reportService = new ReportService(reportRepository, dataSource);
|
||||
const reportService = new ReportService(dataSource);
|
||||
const authService = new AuthService(userRepository, tenantRepository, refreshTokenRepository as any);
|
||||
const authMiddleware = new AuthMiddleware(authService, dataSource);
|
||||
|
||||
@ -67,7 +69,12 @@ export function createReportController(dataSource: DataSource): Router {
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: result.data,
|
||||
pagination: result.meta,
|
||||
pagination: {
|
||||
total: result.total,
|
||||
page: result.page,
|
||||
limit: result.limit,
|
||||
totalPages: result.totalPages,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
@ -279,7 +286,12 @@ export function createReportController(dataSource: DataSource): Router {
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: result.data,
|
||||
pagination: result.meta,
|
||||
pagination: {
|
||||
total: result.total,
|
||||
page: result.page,
|
||||
limit: result.limit,
|
||||
totalPages: result.totalPages,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
|
||||
@ -7,10 +7,22 @@
|
||||
*/
|
||||
|
||||
import { Repository, DataSource } from 'typeorm';
|
||||
import { BaseService, ServiceContext, PaginatedResult } from '../../../shared/services/base.service';
|
||||
import { Dashboard, DashboardType, DashboardVisibility } from '../entities/dashboard.entity';
|
||||
import { DashboardWidget, WidgetType, DataSourceType } from '../entities/dashboard-widget.entity';
|
||||
|
||||
interface ServiceContext {
|
||||
tenantId: string;
|
||||
userId?: string;
|
||||
}
|
||||
|
||||
interface PaginatedResult<T> {
|
||||
data: T[];
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
totalPages: number;
|
||||
}
|
||||
|
||||
export interface CreateDashboardDto {
|
||||
code: string;
|
||||
name: string;
|
||||
@ -66,17 +78,61 @@ export interface DashboardFilters {
|
||||
search?: string;
|
||||
}
|
||||
|
||||
export class DashboardService extends BaseService<Dashboard> {
|
||||
export class DashboardService {
|
||||
private repository: Repository<Dashboard>;
|
||||
private widgetRepository: Repository<DashboardWidget>;
|
||||
|
||||
constructor(
|
||||
repository: Repository<Dashboard>,
|
||||
dataSource: DataSource
|
||||
) {
|
||||
super(repository);
|
||||
constructor(dataSource: DataSource) {
|
||||
this.repository = dataSource.getRepository(Dashboard);
|
||||
this.widgetRepository = dataSource.getRepository(DashboardWidget);
|
||||
}
|
||||
|
||||
/**
|
||||
* Buscar por ID
|
||||
*/
|
||||
async findById(ctx: ServiceContext, id: string): Promise<Dashboard | null> {
|
||||
return this.repository.findOne({
|
||||
where: {
|
||||
id,
|
||||
tenantId: ctx.tenantId,
|
||||
deletedAt: null,
|
||||
} as any,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Crear entidad
|
||||
*/
|
||||
async create(ctx: ServiceContext, data: Partial<Dashboard>): Promise<Dashboard> {
|
||||
const entity = this.repository.create({
|
||||
...data,
|
||||
tenantId: ctx.tenantId,
|
||||
});
|
||||
return this.repository.save(entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Actualizar entidad
|
||||
*/
|
||||
async update(ctx: ServiceContext, id: string, data: Partial<Dashboard>): Promise<Dashboard | null> {
|
||||
await this.repository.update(
|
||||
{ id, tenantId: ctx.tenantId, deletedAt: null } as any,
|
||||
data as any
|
||||
);
|
||||
return this.findById(ctx, id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Soft delete
|
||||
*/
|
||||
async softDelete(ctx: ServiceContext, id: string): Promise<boolean> {
|
||||
const result = await this.repository.update(
|
||||
{ id, tenantId: ctx.tenantId, deletedAt: null } as any,
|
||||
{ deletedAt: new Date() } as any
|
||||
);
|
||||
return (result.affected ?? 0) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Buscar dashboards con filtros
|
||||
*/
|
||||
|
||||
@ -6,10 +6,22 @@
|
||||
* @module Reports
|
||||
*/
|
||||
|
||||
import { Repository, LessThanOrEqual } from 'typeorm';
|
||||
import { BaseService, ServiceContext, PaginatedResult } from '../../../shared/services/base.service';
|
||||
import { Repository, DataSource, LessThanOrEqual } from 'typeorm';
|
||||
import { KpiSnapshot, KpiCategory, KpiPeriodType, TrendDirection } from '../entities/kpi-snapshot.entity';
|
||||
|
||||
interface ServiceContext {
|
||||
tenantId: string;
|
||||
userId?: string;
|
||||
}
|
||||
|
||||
interface PaginatedResult<T> {
|
||||
data: T[];
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
totalPages: number;
|
||||
}
|
||||
|
||||
export interface CreateKpiSnapshotDto {
|
||||
kpiCode: string;
|
||||
kpiName: string;
|
||||
@ -66,9 +78,34 @@ export interface KpiSummary {
|
||||
}[];
|
||||
}
|
||||
|
||||
export class KpiService extends BaseService<KpiSnapshot> {
|
||||
constructor(repository: Repository<KpiSnapshot>) {
|
||||
super(repository);
|
||||
export class KpiService {
|
||||
private repository: Repository<KpiSnapshot>;
|
||||
|
||||
constructor(dataSource: DataSource) {
|
||||
this.repository = dataSource.getRepository(KpiSnapshot);
|
||||
}
|
||||
|
||||
/**
|
||||
* Buscar por ID
|
||||
*/
|
||||
async findById(ctx: ServiceContext, id: string): Promise<KpiSnapshot | null> {
|
||||
return this.repository.findOne({
|
||||
where: {
|
||||
id,
|
||||
tenantId: ctx.tenantId,
|
||||
} as any,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Crear entidad
|
||||
*/
|
||||
async create(ctx: ServiceContext, data: Partial<KpiSnapshot>): Promise<KpiSnapshot> {
|
||||
const entity = this.repository.create({
|
||||
...data,
|
||||
tenantId: ctx.tenantId,
|
||||
});
|
||||
return this.repository.save(entity);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -110,12 +147,10 @@ export class KpiService extends BaseService<KpiSnapshot> {
|
||||
|
||||
return {
|
||||
data,
|
||||
meta: {
|
||||
total,
|
||||
page,
|
||||
limit,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -6,11 +6,23 @@
|
||||
* @module Reports
|
||||
*/
|
||||
|
||||
import { Repository, DataSource } from 'typeorm';
|
||||
import { BaseService, ServiceContext, PaginatedResult } from '../../../shared/services/base.service';
|
||||
import { DataSource, Repository, IsNull } from 'typeorm';
|
||||
import { Report, ReportType, ReportFormat, ReportFrequency } from '../entities/report.entity';
|
||||
import { ReportExecution, ExecutionStatus } from '../entities/report-execution.entity';
|
||||
|
||||
interface ServiceContext {
|
||||
tenantId: string;
|
||||
userId?: string;
|
||||
}
|
||||
|
||||
interface PaginatedResult<T> {
|
||||
data: T[];
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
totalPages: number;
|
||||
}
|
||||
|
||||
export interface CreateReportDto {
|
||||
code: string;
|
||||
name: string;
|
||||
@ -49,17 +61,57 @@ export interface ExecuteReportDto {
|
||||
distribute?: boolean;
|
||||
}
|
||||
|
||||
export class ReportService extends BaseService<Report> {
|
||||
export class ReportService {
|
||||
private repository: Repository<Report>;
|
||||
private executionRepository: Repository<ReportExecution>;
|
||||
|
||||
constructor(
|
||||
repository: Repository<Report>,
|
||||
dataSource: DataSource
|
||||
) {
|
||||
super(repository);
|
||||
constructor(dataSource: DataSource) {
|
||||
this.repository = dataSource.getRepository(Report);
|
||||
this.executionRepository = dataSource.getRepository(ReportExecution);
|
||||
}
|
||||
|
||||
/**
|
||||
* Buscar reporte por ID
|
||||
*/
|
||||
async findById(ctx: ServiceContext, id: string): Promise<Report | null> {
|
||||
return this.repository.findOne({
|
||||
where: { id, tenantId: ctx.tenantId, deletedAt: IsNull() },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Crear reporte
|
||||
*/
|
||||
async create(ctx: ServiceContext, data: CreateReportDto & { isActive: boolean; isSystem: boolean; executionCount: number }): Promise<Report> {
|
||||
const report = this.repository.create({
|
||||
...data,
|
||||
tenantId: ctx.tenantId,
|
||||
});
|
||||
return this.repository.save(report);
|
||||
}
|
||||
|
||||
/**
|
||||
* Actualizar reporte
|
||||
*/
|
||||
async update(ctx: ServiceContext, id: string, data: Partial<UpdateReportDto>): Promise<Report | null> {
|
||||
await this.repository.update(
|
||||
{ id, tenantId: ctx.tenantId, deletedAt: IsNull() },
|
||||
data
|
||||
);
|
||||
return this.findById(ctx, id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Soft delete reporte
|
||||
*/
|
||||
async softDelete(ctx: ServiceContext, id: string): Promise<boolean> {
|
||||
const result = await this.repository.update(
|
||||
{ id, tenantId: ctx.tenantId, deletedAt: IsNull() },
|
||||
{ deletedAt: new Date() }
|
||||
);
|
||||
return result.affected! > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Buscar reportes con filtros
|
||||
*/
|
||||
@ -96,12 +148,10 @@ export class ReportService extends BaseService<Report> {
|
||||
|
||||
return {
|
||||
data,
|
||||
meta: {
|
||||
total,
|
||||
page,
|
||||
limit,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@ -288,12 +338,10 @@ export class ReportService extends BaseService<Report> {
|
||||
|
||||
return {
|
||||
data,
|
||||
meta: {
|
||||
total,
|
||||
page,
|
||||
limit,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,15 @@
|
||||
import { query, queryOne, getClient, PoolClient } from '../../config/database.js';
|
||||
import { NotFoundError, ValidationError } from '../errors/index.js';
|
||||
import { PaginationMeta } from '../types/index.js';
|
||||
/**
|
||||
* BaseService - Base service class (deprecated)
|
||||
*
|
||||
* NOTE: This base class is deprecated. New services should be standalone
|
||||
* and use TypeORM Repository pattern directly with DataSource injection.
|
||||
*
|
||||
* The ServiceContext and PaginatedResult interfaces are still exported
|
||||
* for backwards compatibility with existing code.
|
||||
*
|
||||
* @deprecated Use standalone service pattern with TypeORM Repository instead
|
||||
* @module Shared
|
||||
*/
|
||||
|
||||
/**
|
||||
* Contexto de servicio para multi-tenant
|
||||
@ -36,7 +45,6 @@ export interface BasePaginationFilters {
|
||||
* Opciones para construcción de queries
|
||||
*/
|
||||
export interface QueryOptions {
|
||||
client?: PoolClient;
|
||||
includeDeleted?: boolean;
|
||||
}
|
||||
|
||||
@ -55,28 +63,41 @@ export interface BaseServiceConfig {
|
||||
/**
|
||||
* Clase base abstracta para servicios CRUD con soporte multi-tenant
|
||||
*
|
||||
* Proporciona implementaciones reutilizables para:
|
||||
* - Paginación con filtros
|
||||
* - Búsqueda por texto
|
||||
* - CRUD básico
|
||||
* - Soft delete
|
||||
* - Transacciones
|
||||
* @deprecated Use standalone service pattern with TypeORM Repository instead
|
||||
*
|
||||
* @example
|
||||
* Example of new pattern:
|
||||
* ```typescript
|
||||
* class PartnersService extends BaseService<Partner, CreatePartnerDto, UpdatePartnerDto> {
|
||||
* protected config: BaseServiceConfig = {
|
||||
* tableName: 'partners',
|
||||
* schema: 'core',
|
||||
* selectFields: 'id, tenant_id, name, email, phone, created_at',
|
||||
* searchFields: ['name', 'email', 'tax_id'],
|
||||
* defaultSortField: 'name',
|
||||
* softDelete: true,
|
||||
* };
|
||||
* import { DataSource, Repository, IsNull } from 'typeorm';
|
||||
*
|
||||
* interface ServiceContext {
|
||||
* tenantId: string;
|
||||
* userId?: string;
|
||||
* }
|
||||
*
|
||||
* interface PaginatedResult<T> {
|
||||
* data: T[];
|
||||
* total: number;
|
||||
* page: number;
|
||||
* limit: number;
|
||||
* totalPages: number;
|
||||
* }
|
||||
*
|
||||
* export class MyService {
|
||||
* private repository: Repository<MyEntity>;
|
||||
*
|
||||
* constructor(dataSource: DataSource) {
|
||||
* this.repository = dataSource.getRepository(MyEntity);
|
||||
* }
|
||||
*
|
||||
* async findById(ctx: ServiceContext, id: string): Promise<MyEntity | null> {
|
||||
* return this.repository.findOne({
|
||||
* where: { id, tenantId: ctx.tenantId, deletedAt: IsNull() },
|
||||
* });
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export abstract class BaseService<T, CreateDto, UpdateDto> {
|
||||
export abstract class BaseService<T, _CreateDto = unknown, _UpdateDto = unknown> {
|
||||
protected abstract config: BaseServiceConfig;
|
||||
|
||||
/**
|
||||
@ -86,345 +107,16 @@ export abstract class BaseService<T, CreateDto, UpdateDto> {
|
||||
return `${this.config.schema}.${this.config.tableName}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene todos los registros con paginación y filtros
|
||||
*/
|
||||
async findAll(
|
||||
tenantId: string,
|
||||
filters: BasePaginationFilters & Record<string, any> = {},
|
||||
options: QueryOptions = {}
|
||||
): Promise<PaginatedResult<T>> {
|
||||
const {
|
||||
page = 1,
|
||||
limit = 20,
|
||||
sortBy = this.config.defaultSortField || 'created_at',
|
||||
sortOrder = 'desc',
|
||||
search,
|
||||
...customFilters
|
||||
} = filters;
|
||||
|
||||
const offset = (page - 1) * limit;
|
||||
const params: any[] = [tenantId];
|
||||
let paramIndex = 2;
|
||||
|
||||
// Construir WHERE clause
|
||||
let whereClause = 'WHERE tenant_id = $1';
|
||||
|
||||
// Soft delete
|
||||
if (this.config.softDelete && !options.includeDeleted) {
|
||||
whereClause += ' AND deleted_at IS NULL';
|
||||
}
|
||||
|
||||
// Búsqueda por texto
|
||||
if (search && this.config.searchFields?.length) {
|
||||
const searchConditions = this.config.searchFields
|
||||
.map(field => `${field} ILIKE $${paramIndex}`)
|
||||
.join(' OR ');
|
||||
whereClause += ` AND (${searchConditions})`;
|
||||
params.push(`%${search}%`);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
// Filtros custom
|
||||
for (const [key, value] of Object.entries(customFilters)) {
|
||||
if (value !== undefined && value !== null && value !== '') {
|
||||
whereClause += ` AND ${key} = $${paramIndex++}`;
|
||||
params.push(value);
|
||||
}
|
||||
}
|
||||
|
||||
// Validar sortBy para prevenir SQL injection
|
||||
const safeSortBy = this.sanitizeFieldName(sortBy);
|
||||
const safeSortOrder = sortOrder === 'asc' ? 'ASC' : 'DESC';
|
||||
|
||||
// Query de conteo
|
||||
const countSql = `
|
||||
SELECT COUNT(*) as count
|
||||
FROM ${this.fullTableName}
|
||||
${whereClause}
|
||||
`;
|
||||
|
||||
// Query de datos
|
||||
const dataSql = `
|
||||
SELECT ${this.config.selectFields}
|
||||
FROM ${this.fullTableName}
|
||||
${whereClause}
|
||||
ORDER BY ${safeSortBy} ${safeSortOrder}
|
||||
LIMIT $${paramIndex++} OFFSET $${paramIndex}
|
||||
`;
|
||||
|
||||
if (options.client) {
|
||||
const [countResult, dataResult] = await Promise.all([
|
||||
options.client.query(countSql, params),
|
||||
options.client.query(dataSql, [...params, limit, offset]),
|
||||
]);
|
||||
|
||||
const total = parseInt(countResult.rows[0]?.count || '0', 10);
|
||||
|
||||
return {
|
||||
data: dataResult.rows as T[],
|
||||
total,
|
||||
page,
|
||||
limit,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
};
|
||||
}
|
||||
|
||||
const [countRows, dataRows] = await Promise.all([
|
||||
query<{ count: string }>(countSql, params),
|
||||
query<T>(dataSql, [...params, limit, offset]),
|
||||
]);
|
||||
|
||||
const total = parseInt(countRows[0]?.count || '0', 10);
|
||||
|
||||
return {
|
||||
data: dataRows,
|
||||
total,
|
||||
page,
|
||||
limit,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene un registro por ID
|
||||
*/
|
||||
async findById(
|
||||
id: string,
|
||||
tenantId: string,
|
||||
options: QueryOptions = {}
|
||||
): Promise<T | null> {
|
||||
let whereClause = 'WHERE id = $1 AND tenant_id = $2';
|
||||
|
||||
if (this.config.softDelete && !options.includeDeleted) {
|
||||
whereClause += ' AND deleted_at IS NULL';
|
||||
}
|
||||
|
||||
const sql = `
|
||||
SELECT ${this.config.selectFields}
|
||||
FROM ${this.fullTableName}
|
||||
${whereClause}
|
||||
`;
|
||||
|
||||
if (options.client) {
|
||||
const result = await options.client.query(sql, [id, tenantId]);
|
||||
return result.rows[0] as T || null;
|
||||
}
|
||||
const rows = await query<T>(sql, [id, tenantId]);
|
||||
return rows[0] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene un registro por ID o lanza error si no existe
|
||||
*/
|
||||
async findByIdOrFail(
|
||||
id: string,
|
||||
tenantId: string,
|
||||
options: QueryOptions = {}
|
||||
): Promise<T> {
|
||||
const entity = await this.findById(id, tenantId, options);
|
||||
if (!entity) {
|
||||
throw new NotFoundError(`${this.config.tableName} with id ${id} not found`);
|
||||
}
|
||||
return entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica si existe un registro
|
||||
*/
|
||||
async exists(
|
||||
id: string,
|
||||
tenantId: string,
|
||||
options: QueryOptions = {}
|
||||
): Promise<boolean> {
|
||||
let whereClause = 'WHERE id = $1 AND tenant_id = $2';
|
||||
|
||||
if (this.config.softDelete && !options.includeDeleted) {
|
||||
whereClause += ' AND deleted_at IS NULL';
|
||||
}
|
||||
|
||||
const sql = `
|
||||
SELECT 1 FROM ${this.fullTableName}
|
||||
${whereClause}
|
||||
LIMIT 1
|
||||
`;
|
||||
|
||||
if (options.client) {
|
||||
const result = await options.client.query(sql, [id, tenantId]);
|
||||
return result.rows.length > 0;
|
||||
}
|
||||
const rows = await query(sql, [id, tenantId]);
|
||||
return rows.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Soft delete de un registro
|
||||
*/
|
||||
async softDelete(
|
||||
id: string,
|
||||
tenantId: string,
|
||||
userId: string,
|
||||
options: QueryOptions = {}
|
||||
): Promise<boolean> {
|
||||
if (!this.config.softDelete) {
|
||||
throw new ValidationError('Soft delete not enabled for this entity');
|
||||
}
|
||||
|
||||
const sql = `
|
||||
UPDATE ${this.fullTableName}
|
||||
SET deleted_at = CURRENT_TIMESTAMP,
|
||||
deleted_by = $3,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $1 AND tenant_id = $2 AND deleted_at IS NULL
|
||||
RETURNING id
|
||||
`;
|
||||
|
||||
if (options.client) {
|
||||
const result = await options.client.query(sql, [id, tenantId, userId]);
|
||||
return result.rows.length > 0;
|
||||
}
|
||||
const rows = await query(sql, [id, tenantId, userId]);
|
||||
return rows.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hard delete de un registro
|
||||
*/
|
||||
async hardDelete(
|
||||
id: string,
|
||||
tenantId: string,
|
||||
options: QueryOptions = {}
|
||||
): Promise<boolean> {
|
||||
const sql = `
|
||||
DELETE FROM ${this.fullTableName}
|
||||
WHERE id = $1 AND tenant_id = $2
|
||||
RETURNING id
|
||||
`;
|
||||
|
||||
if (options.client) {
|
||||
const result = await options.client.query(sql, [id, tenantId]);
|
||||
return result.rows.length > 0;
|
||||
}
|
||||
const rows = await query(sql, [id, tenantId]);
|
||||
return rows.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cuenta registros con filtros
|
||||
*/
|
||||
async count(
|
||||
tenantId: string,
|
||||
filters: Record<string, any> = {},
|
||||
options: QueryOptions = {}
|
||||
): Promise<number> {
|
||||
const params: any[] = [tenantId];
|
||||
let paramIndex = 2;
|
||||
let whereClause = 'WHERE tenant_id = $1';
|
||||
|
||||
if (this.config.softDelete && !options.includeDeleted) {
|
||||
whereClause += ' AND deleted_at IS NULL';
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(filters)) {
|
||||
if (value !== undefined && value !== null) {
|
||||
whereClause += ` AND ${key} = $${paramIndex++}`;
|
||||
params.push(value);
|
||||
}
|
||||
}
|
||||
|
||||
const sql = `
|
||||
SELECT COUNT(*) as count
|
||||
FROM ${this.fullTableName}
|
||||
${whereClause}
|
||||
`;
|
||||
|
||||
if (options.client) {
|
||||
const result = await options.client.query(sql, params);
|
||||
return parseInt(result.rows[0]?.count || '0', 10);
|
||||
}
|
||||
const rows = await query<{ count: string }>(sql, params);
|
||||
return parseInt(rows[0]?.count || '0', 10);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ejecuta una función dentro de una transacción
|
||||
*/
|
||||
protected async withTransaction<R>(
|
||||
fn: (client: PoolClient) => Promise<R>
|
||||
): Promise<R> {
|
||||
const client = await getClient();
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
const result = await fn(client);
|
||||
await client.query('COMMIT');
|
||||
return result;
|
||||
} catch (error) {
|
||||
await client.query('ROLLBACK');
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitiza nombre de campo para prevenir SQL injection
|
||||
*/
|
||||
protected sanitizeFieldName(field: string): string {
|
||||
// Solo permite caracteres alfanuméricos y guiones bajos
|
||||
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(field)) {
|
||||
return this.config.defaultSortField || 'created_at';
|
||||
}
|
||||
return field;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construye un INSERT dinámico
|
||||
*/
|
||||
protected buildInsertQuery(
|
||||
data: Record<string, any>,
|
||||
additionalFields: Record<string, any> = {}
|
||||
): { sql: string; params: any[] } {
|
||||
const allData = { ...data, ...additionalFields };
|
||||
const fields = Object.keys(allData);
|
||||
const values = Object.values(allData);
|
||||
const placeholders = fields.map((_, i) => `$${i + 1}`);
|
||||
|
||||
const sql = `
|
||||
INSERT INTO ${this.fullTableName} (${fields.join(', ')})
|
||||
VALUES (${placeholders.join(', ')})
|
||||
RETURNING ${this.config.selectFields}
|
||||
`;
|
||||
|
||||
return { sql, params: values };
|
||||
}
|
||||
|
||||
/**
|
||||
* Construye un UPDATE dinámico
|
||||
*/
|
||||
protected buildUpdateQuery(
|
||||
id: string,
|
||||
tenantId: string,
|
||||
data: Record<string, any>
|
||||
): { sql: string; params: any[] } {
|
||||
const fields = Object.keys(data).filter(k => data[k] !== undefined);
|
||||
const setClauses = fields.map((f, i) => `${f} = $${i + 1}`);
|
||||
const values = fields.map(f => data[f]);
|
||||
|
||||
// Agregar updated_at automáticamente
|
||||
setClauses.push(`updated_at = CURRENT_TIMESTAMP`);
|
||||
|
||||
const paramIndex = fields.length + 1;
|
||||
|
||||
const sql = `
|
||||
UPDATE ${this.fullTableName}
|
||||
SET ${setClauses.join(', ')}
|
||||
WHERE id = $${paramIndex} AND tenant_id = $${paramIndex + 1}
|
||||
RETURNING ${this.config.selectFields}
|
||||
`;
|
||||
|
||||
return { sql, params: [...values, id, tenantId] };
|
||||
}
|
||||
|
||||
/**
|
||||
* Redondea a N decimales
|
||||
*/
|
||||
|
||||
Loading…
Reference in New Issue
Block a user