[GAP-001,002,003] feat(entities): Add KPIs config and assets depreciation/loans entities
- Add KpiConfig and KpiValue entities for dynamic KPIs (GAP-001) - Add ToolLoan entity for tool loans tracking (GAP-002) - Add DepreciationSchedule and DepreciationEntry entities (GAP-003) - Update index exports for assets and reports modules Implements 13 SP of critical gaps identified in EPIC-003. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
2fc091c656
commit
8275f03053
106
src/modules/assets/entities/depreciation-entry.entity.ts
Normal file
106
src/modules/assets/entities/depreciation-entry.entity.ts
Normal file
@ -0,0 +1,106 @@
|
||||
/**
|
||||
* DepreciationEntry Entity - Entradas de Depreciacion
|
||||
*
|
||||
* Registro mensual de depreciacion aplicada por activo.
|
||||
*
|
||||
* @module Assets (MAE-015)
|
||||
* @table assets.depreciation_entries
|
||||
* @gap GAP-003
|
||||
*/
|
||||
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
Index,
|
||||
Unique,
|
||||
} from 'typeorm';
|
||||
import { DepreciationSchedule } from './depreciation-schedule.entity';
|
||||
|
||||
export type DepreciationEntryStatus = 'draft' | 'posted' | 'reversed';
|
||||
|
||||
@Entity({ schema: 'assets', name: 'depreciation_entries' })
|
||||
@Index(['tenantId'])
|
||||
@Index(['scheduleId'])
|
||||
@Index(['tenantId', 'periodDate'])
|
||||
@Index(['tenantId', 'fiscalYear', 'fiscalMonth'])
|
||||
@Index(['tenantId', 'status'])
|
||||
@Unique(['scheduleId', 'periodDate'])
|
||||
export class DepreciationEntry {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string;
|
||||
|
||||
@Column({ name: 'tenant_id', type: 'uuid' })
|
||||
tenantId!: string;
|
||||
|
||||
// Programacion
|
||||
@Column({ name: 'schedule_id', type: 'uuid' })
|
||||
scheduleId!: string;
|
||||
|
||||
@ManyToOne(() => DepreciationSchedule, (schedule) => schedule.entries)
|
||||
@JoinColumn({ name: 'schedule_id' })
|
||||
schedule?: DepreciationSchedule;
|
||||
|
||||
// Periodo
|
||||
@Column({ name: 'period_date', type: 'date' })
|
||||
periodDate!: Date;
|
||||
|
||||
@Column({ name: 'fiscal_year', type: 'int' })
|
||||
fiscalYear!: number;
|
||||
|
||||
@Column({ name: 'fiscal_month', type: 'int' })
|
||||
fiscalMonth!: number;
|
||||
|
||||
// Valores
|
||||
@Column({ name: 'depreciation_amount', type: 'decimal', precision: 12, scale: 2 })
|
||||
depreciationAmount!: number;
|
||||
|
||||
@Column({ name: 'accumulated_depreciation', type: 'decimal', precision: 18, scale: 2 })
|
||||
accumulatedDepreciation!: number;
|
||||
|
||||
@Column({ name: 'book_value', type: 'decimal', precision: 18, scale: 2 })
|
||||
bookValue!: number;
|
||||
|
||||
// Para units_of_production
|
||||
@Column({ name: 'units_used', type: 'int', nullable: true })
|
||||
unitsUsed?: number;
|
||||
|
||||
// Contabilidad
|
||||
@Column({ name: 'journal_entry_id', type: 'uuid', nullable: true })
|
||||
journalEntryId?: string;
|
||||
|
||||
@Column({ name: 'is_posted', type: 'boolean', default: false })
|
||||
isPosted!: boolean;
|
||||
|
||||
@Column({ name: 'posted_at', type: 'timestamptz', nullable: true })
|
||||
postedAt?: Date;
|
||||
|
||||
@Column({ name: 'posted_by', type: 'uuid', nullable: true })
|
||||
postedBy?: string;
|
||||
|
||||
// Estado
|
||||
@Column({
|
||||
type: 'varchar',
|
||||
length: 20,
|
||||
default: 'draft',
|
||||
})
|
||||
status!: DepreciationEntryStatus;
|
||||
|
||||
// Notas
|
||||
@Column({ type: 'text', nullable: true })
|
||||
notes?: string;
|
||||
|
||||
// Auditoria
|
||||
@Column({ name: 'created_by', type: 'uuid', nullable: true })
|
||||
createdBy?: string;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
|
||||
createdAt!: Date;
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
|
||||
updatedAt!: Date;
|
||||
}
|
||||
136
src/modules/assets/entities/depreciation-schedule.entity.ts
Normal file
136
src/modules/assets/entities/depreciation-schedule.entity.ts
Normal file
@ -0,0 +1,136 @@
|
||||
/**
|
||||
* DepreciationSchedule Entity - Programacion de Depreciacion
|
||||
*
|
||||
* Configuracion de depreciacion para cada activo fijo.
|
||||
*
|
||||
* @module Assets (MAE-015)
|
||||
* @table assets.depreciation_schedule
|
||||
* @gap GAP-003
|
||||
*/
|
||||
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
ManyToOne,
|
||||
OneToMany,
|
||||
JoinColumn,
|
||||
Index,
|
||||
} from 'typeorm';
|
||||
import { Asset } from './asset.entity';
|
||||
import { DepreciationEntry } from './depreciation-entry.entity';
|
||||
|
||||
export type DepreciationMethod =
|
||||
| 'straight_line'
|
||||
| 'declining_balance'
|
||||
| 'double_declining'
|
||||
| 'sum_of_years'
|
||||
| 'units_of_production';
|
||||
|
||||
export type AssetDepreciationType = 'equipment' | 'machinery' | 'vehicle' | 'tool';
|
||||
|
||||
@Entity({ schema: 'assets', name: 'depreciation_schedule' })
|
||||
@Index(['tenantId', 'assetId'], { unique: true })
|
||||
@Index(['tenantId'])
|
||||
@Index(['tenantId', 'isActive'])
|
||||
@Index(['tenantId', 'isFullyDepreciated'])
|
||||
export class DepreciationSchedule {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string;
|
||||
|
||||
@Column({ name: 'tenant_id', type: 'uuid' })
|
||||
tenantId!: string;
|
||||
|
||||
// Activo
|
||||
@Column({ name: 'asset_id', type: 'uuid' })
|
||||
assetId!: string;
|
||||
|
||||
@ManyToOne(() => Asset)
|
||||
@JoinColumn({ name: 'asset_id' })
|
||||
asset?: Asset;
|
||||
|
||||
// Tipo de activo
|
||||
@Column({ name: 'asset_type', length: 20 })
|
||||
assetType!: AssetDepreciationType;
|
||||
|
||||
// Metodo de depreciacion
|
||||
@Column({
|
||||
type: 'enum',
|
||||
enum: ['straight_line', 'declining_balance', 'double_declining', 'sum_of_years', 'units_of_production'],
|
||||
enumName: 'depreciation_method',
|
||||
default: 'straight_line',
|
||||
})
|
||||
method!: DepreciationMethod;
|
||||
|
||||
// Valores
|
||||
@Column({ name: 'original_value', type: 'decimal', precision: 18, scale: 2 })
|
||||
originalValue!: number;
|
||||
|
||||
@Column({ name: 'salvage_value', type: 'decimal', precision: 18, scale: 2, default: 0 })
|
||||
salvageValue!: number;
|
||||
|
||||
// Campos calculados (GENERATED ALWAYS AS en DDL)
|
||||
@Column({ name: 'depreciable_amount', type: 'decimal', precision: 18, scale: 2, insert: false, update: false })
|
||||
depreciableAmount?: number;
|
||||
|
||||
// Vida util
|
||||
@Column({ name: 'useful_life_months', type: 'int' })
|
||||
usefulLifeMonths!: number;
|
||||
|
||||
@Column({ name: 'useful_life_units', type: 'int', nullable: true })
|
||||
usefulLifeUnits?: number;
|
||||
|
||||
// Depreciacion mensual calculada (GENERATED en DDL)
|
||||
@Column({ name: 'monthly_depreciation', type: 'decimal', precision: 12, scale: 2, insert: false, update: false })
|
||||
monthlyDepreciation?: number;
|
||||
|
||||
// Fechas
|
||||
@Column({ name: 'depreciation_start_date', type: 'date' })
|
||||
depreciationStartDate!: Date;
|
||||
|
||||
@Column({ name: 'depreciation_end_date', type: 'date', nullable: true })
|
||||
depreciationEndDate?: Date;
|
||||
|
||||
// Estado actual
|
||||
@Column({ name: 'accumulated_depreciation', type: 'decimal', precision: 18, scale: 2, default: 0 })
|
||||
accumulatedDepreciation!: number;
|
||||
|
||||
@Column({ name: 'current_book_value', type: 'decimal', precision: 18, scale: 2, nullable: true })
|
||||
currentBookValue?: number;
|
||||
|
||||
@Column({ name: 'last_entry_date', type: 'date', nullable: true })
|
||||
lastEntryDate?: Date;
|
||||
|
||||
// Estado
|
||||
@Column({ name: 'is_active', type: 'boolean', default: true })
|
||||
isActive!: boolean;
|
||||
|
||||
@Column({ name: 'is_fully_depreciated', type: 'boolean', default: false })
|
||||
isFullyDepreciated!: boolean;
|
||||
|
||||
// Notas y metadatos
|
||||
@Column({ type: 'text', nullable: true })
|
||||
notes?: string;
|
||||
|
||||
@Column({ type: 'jsonb', nullable: true })
|
||||
metadata?: Record<string, any>;
|
||||
|
||||
// Relaciones
|
||||
@OneToMany(() => DepreciationEntry, (entry) => entry.schedule)
|
||||
entries?: DepreciationEntry[];
|
||||
|
||||
// Auditoria
|
||||
@Column({ name: 'created_by', type: 'uuid', nullable: true })
|
||||
createdBy?: string;
|
||||
|
||||
@Column({ name: 'updated_by', type: 'uuid', nullable: true })
|
||||
updatedBy?: string;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
|
||||
createdAt!: Date;
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
|
||||
updatedAt!: Date;
|
||||
}
|
||||
@ -12,3 +12,10 @@ export * from './work-order.entity';
|
||||
export * from './work-order-part.entity';
|
||||
export * from './asset-cost.entity';
|
||||
export * from './fuel-log.entity';
|
||||
|
||||
// GAP-002: Prestamos de Herramientas
|
||||
export * from './tool-loan.entity';
|
||||
|
||||
// GAP-003: Depreciacion de Activos
|
||||
export * from './depreciation-schedule.entity';
|
||||
export * from './depreciation-entry.entity';
|
||||
|
||||
137
src/modules/assets/entities/tool-loan.entity.ts
Normal file
137
src/modules/assets/entities/tool-loan.entity.ts
Normal file
@ -0,0 +1,137 @@
|
||||
/**
|
||||
* ToolLoan Entity - Prestamos de Herramientas
|
||||
*
|
||||
* Registro de prestamos de herramientas entre obras o a empleados.
|
||||
*
|
||||
* @module Assets (MAE-015)
|
||||
* @table assets.tool_loans
|
||||
* @gap GAP-002
|
||||
*/
|
||||
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
Index,
|
||||
} from 'typeorm';
|
||||
import { Asset } from './asset.entity';
|
||||
|
||||
export type LoanStatus = 'active' | 'returned' | 'overdue' | 'lost' | 'damaged';
|
||||
|
||||
@Entity({ schema: 'assets', name: 'tool_loans' })
|
||||
@Index(['tenantId'])
|
||||
@Index(['tenantId', 'toolId'])
|
||||
@Index(['tenantId', 'employeeId'])
|
||||
@Index(['tenantId', 'status'])
|
||||
@Index(['tenantId', 'fraccionamientoOrigenId'])
|
||||
@Index(['tenantId', 'fraccionamientoDestinoId'])
|
||||
export class ToolLoan {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string;
|
||||
|
||||
@Column({ name: 'tenant_id', type: 'uuid' })
|
||||
tenantId!: string;
|
||||
|
||||
// Herramienta prestada
|
||||
@Column({ name: 'tool_id', type: 'uuid' })
|
||||
toolId!: string;
|
||||
|
||||
@ManyToOne(() => Asset)
|
||||
@JoinColumn({ name: 'tool_id' })
|
||||
tool?: Asset;
|
||||
|
||||
// Quien recibe el prestamo
|
||||
@Column({ name: 'employee_id', type: 'uuid' })
|
||||
employeeId!: string;
|
||||
|
||||
@Column({ name: 'employee_name', length: 255, nullable: true })
|
||||
employeeName?: string;
|
||||
|
||||
// Origen del prestamo
|
||||
@Column({ name: 'fraccionamiento_origen_id', type: 'uuid', nullable: true })
|
||||
fraccionamientoOrigenId?: string;
|
||||
|
||||
@Column({ name: 'fraccionamiento_origen_name', length: 255, nullable: true })
|
||||
fraccionamientoOrigenName?: string;
|
||||
|
||||
// Destino del prestamo
|
||||
@Column({ name: 'fraccionamiento_destino_id', type: 'uuid', nullable: true })
|
||||
fraccionamientoDestinoId?: string;
|
||||
|
||||
@Column({ name: 'fraccionamiento_destino_name', length: 255, nullable: true })
|
||||
fraccionamientoDestinoName?: string;
|
||||
|
||||
// Fechas
|
||||
@Column({ name: 'loan_date', type: 'date' })
|
||||
loanDate!: Date;
|
||||
|
||||
@Column({ name: 'expected_return_date', type: 'date', nullable: true })
|
||||
expectedReturnDate?: Date;
|
||||
|
||||
@Column({ name: 'actual_return_date', type: 'date', nullable: true })
|
||||
actualReturnDate?: Date;
|
||||
|
||||
// Estado
|
||||
@Column({
|
||||
type: 'enum',
|
||||
enum: ['active', 'returned', 'overdue', 'lost', 'damaged'],
|
||||
enumName: 'loan_status',
|
||||
default: 'active',
|
||||
})
|
||||
status!: LoanStatus;
|
||||
|
||||
// Condicion de salida
|
||||
@Column({ name: 'condition_out', type: 'text', nullable: true })
|
||||
conditionOut?: string;
|
||||
|
||||
@Column({ name: 'condition_out_photos', type: 'jsonb', nullable: true })
|
||||
conditionOutPhotos?: string[];
|
||||
|
||||
// Condicion de entrada
|
||||
@Column({ name: 'condition_in', type: 'text', nullable: true })
|
||||
conditionIn?: string;
|
||||
|
||||
@Column({ name: 'condition_in_photos', type: 'jsonb', nullable: true })
|
||||
conditionInPhotos?: string[];
|
||||
|
||||
// Aprobacion
|
||||
@Column({ name: 'approved_by_id', type: 'uuid', nullable: true })
|
||||
approvedById?: string;
|
||||
|
||||
@Column({ name: 'approved_by_name', length: 255, nullable: true })
|
||||
approvedByName?: string;
|
||||
|
||||
@Column({ name: 'approved_at', type: 'timestamptz', nullable: true })
|
||||
approvedAt?: Date;
|
||||
|
||||
// Devolucion
|
||||
@Column({ name: 'received_by_id', type: 'uuid', nullable: true })
|
||||
receivedById?: string;
|
||||
|
||||
@Column({ name: 'received_by_name', length: 255, nullable: true })
|
||||
receivedByName?: string;
|
||||
|
||||
// Notas y metadatos
|
||||
@Column({ type: 'text', nullable: true })
|
||||
notes?: string;
|
||||
|
||||
@Column({ type: 'jsonb', nullable: true })
|
||||
metadata?: Record<string, any>;
|
||||
|
||||
// Auditoria
|
||||
@Column({ name: 'created_by', type: 'uuid', nullable: true })
|
||||
createdBy?: string;
|
||||
|
||||
@Column({ name: 'updated_by', type: 'uuid', nullable: true })
|
||||
updatedBy?: string;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
|
||||
createdAt!: Date;
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
|
||||
updatedAt!: Date;
|
||||
}
|
||||
@ -10,6 +10,10 @@ export * from './dashboard.entity';
|
||||
export * from './dashboard-widget.entity';
|
||||
export * from './kpi-snapshot.entity';
|
||||
|
||||
// KPIs Configurables (GAP-001)
|
||||
export * from './kpi-config.entity';
|
||||
export * from './kpi-value.entity';
|
||||
|
||||
// Core report entities (from erp-core)
|
||||
export * from './report-schedule.entity';
|
||||
export * from './report-recipient.entity';
|
||||
|
||||
169
src/modules/reports/entities/kpi-config.entity.ts
Normal file
169
src/modules/reports/entities/kpi-config.entity.ts
Normal file
@ -0,0 +1,169 @@
|
||||
/**
|
||||
* KpiConfig Entity - Configuracion de KPIs Dinamicos
|
||||
*
|
||||
* Permite definir KPIs con formulas configurables, umbrales
|
||||
* y semaforizacion.
|
||||
*
|
||||
* @module Reports (MAI-006)
|
||||
* @table reports.kpis_config
|
||||
* @gap GAP-001
|
||||
*/
|
||||
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
ManyToOne,
|
||||
OneToMany,
|
||||
JoinColumn,
|
||||
Index,
|
||||
} from 'typeorm';
|
||||
import { Tenant } from '../../core/entities/tenant.entity';
|
||||
import { KpiValue } from './kpi-value.entity';
|
||||
|
||||
export type KpiCategory =
|
||||
| 'financial'
|
||||
| 'progress'
|
||||
| 'quality'
|
||||
| 'hse'
|
||||
| 'hr'
|
||||
| 'inventory'
|
||||
| 'operational';
|
||||
|
||||
export type FormulaType = 'sql' | 'expression' | 'function';
|
||||
|
||||
export type CalculationFrequency =
|
||||
| 'realtime'
|
||||
| 'hourly'
|
||||
| 'daily'
|
||||
| 'weekly'
|
||||
| 'monthly';
|
||||
|
||||
@Entity({ schema: 'reports', name: 'kpis_config' })
|
||||
@Index(['tenantId', 'code'], { unique: true })
|
||||
@Index(['tenantId'])
|
||||
@Index(['tenantId', 'category'])
|
||||
@Index(['tenantId', 'module'])
|
||||
@Index(['tenantId', 'isActive'])
|
||||
export class KpiConfig {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string;
|
||||
|
||||
@Column({ name: 'tenant_id', type: 'uuid' })
|
||||
tenantId!: string;
|
||||
|
||||
// Identificacion
|
||||
@Column({ length: 50 })
|
||||
code!: string;
|
||||
|
||||
@Column({ length: 200 })
|
||||
name!: string;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
description?: string;
|
||||
|
||||
// Clasificacion
|
||||
@Column({ length: 50 })
|
||||
category!: KpiCategory;
|
||||
|
||||
@Column({ length: 50 })
|
||||
module!: string;
|
||||
|
||||
// Formula de calculo
|
||||
@Column({ type: 'text' })
|
||||
formula!: string;
|
||||
|
||||
@Column({
|
||||
name: 'formula_type',
|
||||
type: 'varchar',
|
||||
length: 20,
|
||||
default: 'sql',
|
||||
})
|
||||
formulaType!: FormulaType;
|
||||
|
||||
@Column({ name: 'query_function', length: 255, nullable: true })
|
||||
queryFunction?: string;
|
||||
|
||||
// Parametros de la formula
|
||||
@Column({ name: 'parameters_schema', type: 'jsonb', default: '{}' })
|
||||
parametersSchema!: Record<string, any>;
|
||||
|
||||
// Unidad y formato
|
||||
@Column({ length: 20, nullable: true })
|
||||
unit?: string;
|
||||
|
||||
@Column({ name: 'decimal_places', type: 'int', default: 2 })
|
||||
decimalPlaces!: number;
|
||||
|
||||
@Column({ name: 'format_pattern', length: 50, nullable: true })
|
||||
formatPattern?: string;
|
||||
|
||||
// Umbrales de semaforizacion
|
||||
@Column({ name: 'target_value', type: 'decimal', precision: 18, scale: 4, nullable: true })
|
||||
targetValue?: number;
|
||||
|
||||
@Column({ name: 'threshold_green', type: 'decimal', precision: 18, scale: 4, nullable: true })
|
||||
thresholdGreen?: number;
|
||||
|
||||
@Column({ name: 'threshold_yellow', type: 'decimal', precision: 18, scale: 4, nullable: true })
|
||||
thresholdYellow?: number;
|
||||
|
||||
@Column({ name: 'invert_colors', type: 'boolean', default: false })
|
||||
invertColors!: boolean;
|
||||
|
||||
// Frecuencia de calculo
|
||||
@Column({
|
||||
name: 'calculation_frequency',
|
||||
type: 'varchar',
|
||||
length: 20,
|
||||
default: 'daily',
|
||||
})
|
||||
calculationFrequency!: CalculationFrequency;
|
||||
|
||||
// Visualizacion
|
||||
@Column({ name: 'display_order', type: 'int', default: 0 })
|
||||
displayOrder!: number;
|
||||
|
||||
@Column({ length: 50, nullable: true })
|
||||
icon?: string;
|
||||
|
||||
@Column({ length: 20, nullable: true })
|
||||
color?: string;
|
||||
|
||||
// Estado
|
||||
@Column({ name: 'is_active', type: 'boolean', default: true })
|
||||
isActive!: boolean;
|
||||
|
||||
@Column({ name: 'is_system', type: 'boolean', default: false })
|
||||
isSystem!: boolean;
|
||||
|
||||
// Metadatos
|
||||
@Column({ type: 'jsonb', nullable: true })
|
||||
metadata?: Record<string, any>;
|
||||
|
||||
// Relaciones
|
||||
@ManyToOne(() => Tenant)
|
||||
@JoinColumn({ name: 'tenant_id' })
|
||||
tenant?: Tenant;
|
||||
|
||||
@OneToMany(() => KpiValue, (value) => value.kpiConfig)
|
||||
values?: KpiValue[];
|
||||
|
||||
// Auditoria
|
||||
@Column({ name: 'created_by', type: 'uuid', nullable: true })
|
||||
createdBy?: string;
|
||||
|
||||
@Column({ name: 'updated_by', type: 'uuid', nullable: true })
|
||||
updatedBy?: string;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
|
||||
createdAt!: Date;
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
|
||||
updatedAt!: Date;
|
||||
|
||||
@Column({ name: 'deleted_at', type: 'timestamptz', nullable: true })
|
||||
deletedAt?: Date;
|
||||
}
|
||||
126
src/modules/reports/entities/kpi-value.entity.ts
Normal file
126
src/modules/reports/entities/kpi-value.entity.ts
Normal file
@ -0,0 +1,126 @@
|
||||
/**
|
||||
* KpiValue Entity - Valores Calculados de KPIs
|
||||
*
|
||||
* Almacena los valores calculados periodicamente para cada KPI.
|
||||
*
|
||||
* @module Reports (MAI-006)
|
||||
* @table reports.kpis_values
|
||||
* @gap GAP-001
|
||||
*/
|
||||
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
Index,
|
||||
} from 'typeorm';
|
||||
import { Tenant } from '../../core/entities/tenant.entity';
|
||||
import { KpiConfig } from './kpi-config.entity';
|
||||
|
||||
export type PeriodType = 'daily' | 'weekly' | 'monthly' | 'quarterly' | 'yearly';
|
||||
|
||||
export type KpiStatus = 'green' | 'yellow' | 'red';
|
||||
|
||||
export type TrendDirection = 'up' | 'down' | 'stable';
|
||||
|
||||
@Entity({ schema: 'reports', name: 'kpis_values' })
|
||||
@Index(['tenantId'])
|
||||
@Index(['kpiId'])
|
||||
@Index(['tenantId', 'periodStart', 'periodEnd'])
|
||||
@Index(['kpiId', 'periodStart'])
|
||||
@Index(['tenantId', 'projectId'])
|
||||
@Index(['calculatedAt'])
|
||||
export class KpiValue {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string;
|
||||
|
||||
@Column({ name: 'tenant_id', type: 'uuid' })
|
||||
tenantId!: string;
|
||||
|
||||
// KPI
|
||||
@Column({ name: 'kpi_id', type: 'uuid' })
|
||||
kpiId!: string;
|
||||
|
||||
// Periodo
|
||||
@Column({ name: 'period_start', type: 'date' })
|
||||
periodStart!: Date;
|
||||
|
||||
@Column({ name: 'period_end', type: 'date' })
|
||||
periodEnd!: Date;
|
||||
|
||||
@Column({
|
||||
name: 'period_type',
|
||||
type: 'varchar',
|
||||
length: 20,
|
||||
default: 'daily',
|
||||
})
|
||||
periodType!: PeriodType;
|
||||
|
||||
// Contexto opcional
|
||||
@Column({ name: 'project_id', type: 'uuid', nullable: true })
|
||||
projectId?: string;
|
||||
|
||||
@Column({ name: 'department_id', type: 'uuid', nullable: true })
|
||||
departmentId?: string;
|
||||
|
||||
// Valor calculado
|
||||
@Column({ type: 'decimal', precision: 18, scale: 4 })
|
||||
value!: number;
|
||||
|
||||
@Column({ name: 'previous_value', type: 'decimal', precision: 18, scale: 4, nullable: true })
|
||||
previousValue?: number;
|
||||
|
||||
// Comparacion con objetivo
|
||||
@Column({ name: 'target_value', type: 'decimal', precision: 18, scale: 4, nullable: true })
|
||||
targetValue?: number;
|
||||
|
||||
@Column({ name: 'variance_value', type: 'decimal', precision: 18, scale: 4, nullable: true })
|
||||
varianceValue?: number;
|
||||
|
||||
@Column({ name: 'variance_percentage', type: 'decimal', precision: 8, scale: 2, nullable: true })
|
||||
variancePercentage?: number;
|
||||
|
||||
// Semaforizacion calculada
|
||||
@Column({ type: 'varchar', length: 10, nullable: true })
|
||||
status?: KpiStatus;
|
||||
|
||||
@Column({ name: 'is_on_target', type: 'boolean', nullable: true })
|
||||
isOnTarget?: boolean;
|
||||
|
||||
// Tendencia
|
||||
@Column({ name: 'trend_direction', type: 'varchar', length: 10, nullable: true })
|
||||
trendDirection?: TrendDirection;
|
||||
|
||||
@Column({ name: 'change_percentage', type: 'decimal', precision: 8, scale: 2, nullable: true })
|
||||
changePercentage?: number;
|
||||
|
||||
// Desglose
|
||||
@Column({ type: 'jsonb', nullable: true })
|
||||
breakdown?: Record<string, any>;
|
||||
|
||||
// Calculo
|
||||
@Column({ name: 'calculated_at', type: 'timestamptz', default: () => 'CURRENT_TIMESTAMP' })
|
||||
calculatedAt!: Date;
|
||||
|
||||
@Column({ name: 'calculation_duration_ms', type: 'int', nullable: true })
|
||||
calculationDurationMs?: number;
|
||||
|
||||
@Column({ name: 'calculation_error', type: 'text', nullable: true })
|
||||
calculationError?: string;
|
||||
|
||||
// Relaciones
|
||||
@ManyToOne(() => Tenant)
|
||||
@JoinColumn({ name: 'tenant_id' })
|
||||
tenant?: Tenant;
|
||||
|
||||
@ManyToOne(() => KpiConfig, (config) => config.values)
|
||||
@JoinColumn({ name: 'kpi_id' })
|
||||
kpiConfig?: KpiConfig;
|
||||
|
||||
// Auditoria
|
||||
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
|
||||
createdAt!: Date;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user