correciones al formulario osp

This commit is contained in:
2026-01-28 22:42:19 -04:00
parent d2908f1e4c
commit 8efe595f73
23 changed files with 9036 additions and 1685 deletions

View File

@@ -0,0 +1,9 @@
ALTER TABLE "training_surveys" ADD COLUMN "infrastructure_mt2" text DEFAULT '' NOT NULL;--> statement-breakpoint
ALTER TABLE "training_surveys" ADD COLUMN "has_transport" boolean DEFAULT false NOT NULL;--> statement-breakpoint
ALTER TABLE "training_surveys" ADD COLUMN "structure_type" text DEFAULT '' NOT NULL;--> statement-breakpoint
ALTER TABLE "training_surveys" ADD COLUMN "is_open_space" boolean DEFAULT false NOT NULL;--> statement-breakpoint
ALTER TABLE "training_surveys" ADD COLUMN "equipment_list" jsonb DEFAULT '[]'::jsonb NOT NULL;--> statement-breakpoint
ALTER TABLE "training_surveys" ADD COLUMN "production_list" jsonb DEFAULT '[]'::jsonb NOT NULL;--> statement-breakpoint
ALTER TABLE "training_surveys" ADD COLUMN "product_list" jsonb DEFAULT '[]'::jsonb NOT NULL;--> statement-breakpoint
ALTER TABLE "training_surveys" ADD COLUMN "internal_distribution_list" jsonb DEFAULT '[]'::jsonb NOT NULL;--> statement-breakpoint
ALTER TABLE "training_surveys" ADD COLUMN "external_distribution_list" jsonb DEFAULT '[]'::jsonb NOT NULL;

View File

@@ -0,0 +1 @@
ALTER TABLE "training_surveys" ALTER COLUMN "photo1" DROP NOT NULL;

View File

@@ -0,0 +1,36 @@
ALTER TABLE "training_surveys" DROP CONSTRAINT "training_surveys_coor_state_states_id_fk";
--> statement-breakpoint
ALTER TABLE "training_surveys" DROP CONSTRAINT "training_surveys_coor_municipality_municipalities_id_fk";
--> statement-breakpoint
ALTER TABLE "training_surveys" DROP CONSTRAINT "training_surveys_coor_parish_parishes_id_fk";
--> statement-breakpoint
ALTER TABLE "training_surveys" ALTER COLUMN "general_observations" DROP NOT NULL;--> statement-breakpoint
ALTER TABLE "training_surveys" ALTER COLUMN "paralysis_reason" DROP NOT NULL;--> statement-breakpoint
ALTER TABLE "training_surveys" DROP COLUMN "financial_requirement_description";--> statement-breakpoint
ALTER TABLE "training_surveys" DROP COLUMN "producer_count";--> statement-breakpoint
ALTER TABLE "training_surveys" DROP COLUMN "product_count";--> statement-breakpoint
ALTER TABLE "training_surveys" DROP COLUMN "product_description";--> statement-breakpoint
ALTER TABLE "training_surveys" DROP COLUMN "installed_capacity";--> statement-breakpoint
ALTER TABLE "training_surveys" DROP COLUMN "operational_capacity";--> statement-breakpoint
ALTER TABLE "training_surveys" DROP COLUMN "coor_state";--> statement-breakpoint
ALTER TABLE "training_surveys" DROP COLUMN "coor_municipality";--> statement-breakpoint
ALTER TABLE "training_surveys" DROP COLUMN "coor_parish";--> statement-breakpoint
ALTER TABLE "training_surveys" DROP COLUMN "types_of_equipment";--> statement-breakpoint
ALTER TABLE "training_surveys" DROP COLUMN "equipment_count";--> statement-breakpoint
ALTER TABLE "training_surveys" DROP COLUMN "equipment_description";--> statement-breakpoint
ALTER TABLE "training_surveys" DROP COLUMN "raw_material";--> statement-breakpoint
ALTER TABLE "training_surveys" DROP COLUMN "material_type";--> statement-breakpoint
ALTER TABLE "training_surveys" DROP COLUMN "raw_material_count";--> statement-breakpoint
ALTER TABLE "training_surveys" DROP COLUMN "product_count_daily";--> statement-breakpoint
ALTER TABLE "training_surveys" DROP COLUMN "product_count_weekly";--> statement-breakpoint
ALTER TABLE "training_surveys" DROP COLUMN "product_count_monthly";--> statement-breakpoint
ALTER TABLE "training_surveys" DROP COLUMN "prod_description_internal";--> statement-breakpoint
ALTER TABLE "training_surveys" DROP COLUMN "internal_count";--> statement-breakpoint
ALTER TABLE "training_surveys" DROP COLUMN "external_count";--> statement-breakpoint
ALTER TABLE "training_surveys" DROP COLUMN "prod_description_external";--> statement-breakpoint
ALTER TABLE "training_surveys" DROP COLUMN "country";--> statement-breakpoint
ALTER TABLE "training_surveys" DROP COLUMN "city";--> statement-breakpoint
ALTER TABLE "training_surveys" DROP COLUMN "men_count";--> statement-breakpoint
ALTER TABLE "training_surveys" DROP COLUMN "women_count";--> statement-breakpoint
ALTER TABLE "training_surveys" DROP COLUMN "internal_distribution_list";--> statement-breakpoint
ALTER TABLE "training_surveys" DROP COLUMN "external_distribution_list";

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -99,6 +99,27 @@
"when": 1769629815868,
"tag": "0013_cuddly_night_nurse",
"breakpoints": true
},
{
"idx": 14,
"version": "7",
"when": 1769646908602,
"tag": "0014_deep_meteorite",
"breakpoints": true
},
{
"idx": 15,
"version": "7",
"when": 1769648728698,
"tag": "0015_concerned_wild_pack",
"breakpoints": true
},
{
"idx": 16,
"version": "7",
"when": 1769653021994,
"tag": "0016_silent_tag",
"breakpoints": true
}
]
}

View File

@@ -46,12 +46,14 @@ export const answersSurveys = t.pgTable(
export const trainingSurveys = t.pgTable(
'training_surveys',
{
// Datos basicos
// === 1. IDENTIFICADORES Y DATOS DE VISITA ===
id: t.serial('id').primaryKey(),
firstname: t.text('firstname').notNull(),
lastname: t.text('lastname').notNull(),
visitDate: t.timestamp('visit_date').notNull(),
// ubicacion
coorPhone: t.text('coor_phone'),
// === 2. UBICACIÓN (Claves Foráneas - Nullables) ===
state: t
.integer('state')
.references(() => states.id, { onDelete: 'set null' }),
@@ -61,93 +63,89 @@ export const trainingSurveys = t.pgTable(
parish: t
.integer('parish')
.references(() => parishes.id, { onDelete: 'set null' }),
siturCodeCommune: t.text('situr_code_commune').notNull(),
// === 3. DATOS DE LA OSP (Organización Socioproductiva) ===
ospType: t.text('osp_type').notNull(), // UPF, EPS, etc.
ecoSector: t.text('eco_sector').notNull().default(''),
productiveSector: t.text('productive_sector').notNull().default(''),
centralProductiveActivity: t
.text('central_productive_activity')
.notNull()
.default(''),
mainProductiveActivity: t
.text('main_productive_activity')
.notNull()
.default(''),
productiveActivity: t.text('productive_activity').notNull(),
ospRif: t.text('osp_rif').notNull(),
ospName: t.text('osp_name').notNull(),
companyConstitutionYear: t.integer('company_constitution_year').notNull(),
currentStatus: t.text('current_status').notNull().default('ACTIVA'),
infrastructureMt2: t.text('infrastructure_mt2').notNull().default(''),
hasTransport: t.boolean('has_transport').notNull().default(false),
structureType: t.text('structure_type').notNull().default(''),
isOpenSpace: t.boolean('is_open_space').notNull().default(false),
paralysisReason: t.text('paralysis_reason'),
equipmentList: t.jsonb('equipment_list').notNull().default([]),
productionList: t.jsonb('production_list').notNull().default([]),
productList: t.jsonb('product_list').notNull().default([]),
ospAddress: t.text('osp_address').notNull(),
ospGoogleMapsLink: t.text('osp_google_maps_link').notNull().default(''),
communeName: t.text('commune_name').notNull().default(''),
siturCodeCommune: t.text('situr_code_commune').notNull(),
communeRif: t.text('commune_rif').notNull().default(''),
communeSpokespersonName: t.text('commune_spokesperson_name').notNull().default(''),
communeSpokespersonCedula: t.text('commune_spokesperson_cedula').notNull().default(''),
communeSpokespersonRif: t.text('commune_spokesperson_rif').notNull().default(''),
communeSpokespersonPhone: t.text('commune_spokesperson_phone').notNull().default(''),
communeSpokespersonName: t
.text('commune_spokesperson_name')
.notNull()
.default(''),
communeSpokespersonCedula: t
.text('commune_spokesperson_cedula')
.notNull()
.default(''),
communeSpokespersonRif: t
.text('commune_spokesperson_rif')
.notNull()
.default(''),
communeSpokespersonPhone: t
.text('commune_spokesperson_phone')
.notNull()
.default(''),
communeEmail: t.text('commune_email').notNull().default(''),
communalCouncil: t.text('communal_council').notNull(),
siturCodeCommunalCouncil: t.text('situr_code_communal_council').notNull(),
communalCouncilRif: t.text('communal_council_rif').notNull().default(''),
communalCouncilSpokespersonName: t.text('communal_council_spokesperson_name').notNull().default(''),
communalCouncilSpokespersonCedula: t.text('communal_council_spokesperson_cedula').notNull().default(''),
communalCouncilSpokespersonRif: t.text('communal_council_spokesperson_rif').notNull().default(''),
communalCouncilSpokespersonPhone: t.text('communal_council_spokesperson_phone').notNull().default(''),
communalCouncilEmail: t.text('communal_council_email').notNull().default(''),
ospGoogleMapsLink: t.text('osp_google_maps_link').notNull().default(''),
// datos del OSP (ORGANIZACIÓN SOCIOPRODUCTIVA)
ospName: t.text('osp_name').notNull(),
ospAddress: t.text('osp_address').notNull(),
ospRif: t.text('osp_rif').notNull(),
ospType: t.text('osp_type').notNull(),
productiveActivity: t.text('productive_activity').notNull(),
financialRequirementDescription: t
.text('financial_requirement_description')
communalCouncilSpokespersonName: t
.text('communal_council_spokesperson_name')
.notNull()
.default(''),
communalCouncilSpokespersonCedula: t
.text('communal_council_spokesperson_cedula')
.notNull()
.default(''),
communalCouncilSpokespersonRif: t
.text('communal_council_spokesperson_rif')
.notNull()
.default(''),
communalCouncilSpokespersonPhone: t
.text('communal_council_spokesperson_phone')
.notNull()
.default(''),
communalCouncilEmail: t
.text('communal_council_email')
.notNull()
.default(''),
currentStatus: t.text('current_status').notNull().default('ACTIVA'),
companyConstitutionYear: t.integer('company_constitution_year').notNull(),
producerCount: t.integer('producer_count').notNull(),
productCount: t.integer('product_count').notNull().default(0),
productDescription: t.text('product_description').notNull(),
installedCapacity: t.text('installed_capacity').notNull(),
operationalCapacity: t.text('operational_capacity').notNull(),
// datos del responsable
ospResponsibleFullname: t.text('osp_responsible_fullname').notNull(),
ospResponsibleCedula: t.text('osp_responsible_cedula').notNull(),
ospResponsibleRif: t.text('osp_responsible_rif').notNull(),
civilState: t.text('civil_state').notNull(),
ospResponsiblePhone: t.text('osp_responsible_phone').notNull(),
ospResponsibleEmail: t.text('osp_responsible_email').notNull(),
civilState: t.text('civil_state').notNull(),
familyBurden: t.integer('family_burden').notNull(),
numberOfChildren: t.integer('number_of_children').notNull(),
// datos adicionales
generalObservations: t.text('general_observations').notNull(),
paralysisReason: t.text('paralysis_reason').notNull(),
// fotos
photo1: t.text('photo1').notNull(),
generalObservations: t.text('general_observations'),
photo1: t.text('photo1'),
photo2: t.text('photo2'),
photo3: t.text('photo3'),
// nuevos campos coordinacion
coorState: t
.integer('coor_state')
.references(() => states.id, { onDelete: 'set null' }),
coorMunicipality: t
.integer('coor_municipality')
.references(() => municipalities.id, { onDelete: 'set null' }),
coorParish: t
.integer('coor_parish')
.references(() => parishes.id, { onDelete: 'set null' }),
coorPhone: t.text('coor_phone'),
// sectores
ecoSector: t.text('eco_sector').notNull().default(''),
productiveSector: t.text('productive_sector').notNull().default(''),
centralProductiveActivity: t.text('central_productive_activity').notNull().default(''),
mainProductiveActivity: t.text('main_productive_activity').notNull().default(''),
// equipamiento
typesOfEquipment: t.text('types_of_equipment').notNull().default(''),
equipmentCount: t.integer('equipment_count').notNull().default(0),
equipmentDescription: t.text('equipment_description').notNull().default(''),
// materia prima
rawMaterial: t.text('raw_material').notNull().default(''),
materialType: t.text('material_type').notNull().default(''),
rawMaterialCount: t.integer('raw_material_count').notNull().default(0),
// conteo de productos
productCountDaily: t.integer('product_count_daily').notNull().default(0),
productCountWeekly: t.integer('product_count_weekly').notNull().default(0),
productCountMonthly: t.integer('product_count_monthly').notNull().default(0),
// nuevos campos adicionales
prodDescriptionInternal: t.text('prod_description_internal').notNull().default(''),
internalCount: t.integer('internal_count').notNull().default(0),
externalCount: t.integer('external_count').notNull().default(0),
prodDescriptionExternal: t.text('prod_description_external').notNull().default(''),
country: t.text('country').notNull().default(''),
city: t.text('city').notNull().default(''),
menCount: t.integer('men_count').notNull().default(0),
womenCount: t.integer('women_count').notNull().default(0),
...timestamps,
},
(trainingSurveys) => ({

View File

@@ -1,7 +1,16 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsDateString, IsInt, IsOptional, IsString } from 'class-validator';
import { Transform, Type } from 'class-transformer';
import {
IsArray,
IsBoolean,
IsDateString,
IsInt,
IsOptional,
IsString,
} from 'class-validator';
export class CreateTrainingDto {
// === 1. DATOS BÁSICOS ===
@ApiProperty()
@IsString()
firstname: string;
@@ -12,7 +21,25 @@ export class CreateTrainingDto {
@ApiProperty()
@IsDateString()
visitDate: string;
visitDate: string; // Llega como string ISO "2024-11-11T10:00"
@ApiProperty()
@IsString()
@IsOptional()
coorPhone?: string;
// === 2. DATOS OSP ===
@ApiProperty()
@IsString()
ospName: string;
@ApiProperty()
@IsString()
ospRif: string;
@ApiProperty()
@IsString()
ospType: string; // 'UPF', etc.
@ApiProperty()
@IsString()
@@ -20,24 +47,108 @@ export class CreateTrainingDto {
@ApiProperty()
@IsString()
@IsOptional()
financialRequirementDescription?: string;
currentStatus: string;
@ApiProperty()
@IsInt()
@Type(() => Number) // Convierte "2017" -> 2017
companyConstitutionYear: number;
@ApiProperty()
@IsString()
@IsOptional()
state: number;
ospAddress: string;
@ApiProperty()
@IsString()
@IsOptional()
ospGoogleMapsLink?: string;
@ApiProperty()
@IsString()
@IsOptional()
infrastructureMt2?: string;
@ApiProperty()
@IsString()
@IsOptional()
structureType?: string;
@ApiProperty()
@IsBoolean()
@IsOptional()
@Transform(({ value }) => value === 'true' || value === true) // Convierte "false" -> false
hasTransport?: boolean;
@ApiProperty()
@IsBoolean()
@IsOptional()
@Transform(({ value }) => value === 'true' || value === true)
isOpenSpace?: boolean;
@ApiProperty()
@IsString()
@IsOptional()
paralysisReason?: string;
@ApiProperty()
@IsString()
@IsOptional()
generalObservations?: string;
// === 3. SECTORES ===
@ApiProperty()
@IsString()
ecoSector: string;
@ApiProperty()
@IsString()
productiveSector: string;
@ApiProperty()
@IsString()
centralProductiveActivity: string;
@ApiProperty()
@IsString()
mainProductiveActivity: string;
// === 4. DATOS RESPONSABLE ===
@ApiProperty()
@IsString()
ospResponsibleFullname: string;
@ApiProperty()
@IsString()
ospResponsibleCedula: string;
@ApiProperty()
@IsString()
ospResponsibleRif: string;
@ApiProperty()
@IsString()
ospResponsiblePhone: string;
@ApiProperty()
@IsString()
ospResponsibleEmail: string;
@ApiProperty()
@IsString()
civilState: string;
@ApiProperty()
@IsInt()
@IsOptional()
municipality: number;
@Type(() => Number) // Convierte "3" -> 3
familyBurden: number;
@ApiProperty()
@IsInt()
@IsOptional()
parish: number;
@Type(() => Number)
numberOfChildren: number;
// === 5. COMUNA Y CONSEJO COMUNAL ===
@ApiProperty()
@IsString()
siturCodeCommune: string;
@@ -102,217 +213,51 @@ export class CreateTrainingDto {
@IsString()
communalCouncilEmail: string;
@ApiProperty()
@IsString()
ospGoogleMapsLink: string;
// === 6. LISTAS (Arrays JSON) ===
// Reciben un string JSON '[{...}]' y lo convierten a Objeto JS real
@ApiProperty()
@IsString()
ospName: string;
@ApiProperty()
@IsString()
ospAddress: string;
@ApiProperty()
@IsString()
ospRif: string;
@ApiProperty()
@IsString()
ospType: string;
@ApiProperty()
@IsString()
currentStatus: string;
@ApiProperty()
@IsInt()
companyConstitutionYear: number;
@ApiProperty()
@IsInt()
producerCount: number;
@ApiProperty()
@IsInt()
@IsOptional()
productCount: number;
@IsArray()
@Transform(({ value }) => {
if (typeof value === 'string') {
try {
return JSON.parse(value);
} catch {
return [];
}
}
return value;
})
equipmentList?: any[];
@ApiProperty()
@IsString()
productDescription: string;
@ApiProperty()
@IsString()
installedCapacity: string;
@ApiProperty()
@IsString()
operationalCapacity: string;
@ApiProperty()
@IsString()
ospResponsibleFullname: string;
@ApiProperty()
@IsString()
ospResponsibleCedula: string;
@ApiProperty()
@IsString()
ospResponsibleRif: string;
@ApiProperty()
@IsString()
ospResponsiblePhone: string;
@ApiProperty()
@IsString()
ospResponsibleEmail: string;
@ApiProperty()
@IsString()
civilState: string;
@ApiProperty()
@IsInt()
familyBurden: number;
@ApiProperty()
@IsInt()
numberOfChildren: number;
@ApiProperty()
@IsString()
generalObservations: string;
@ApiProperty()
@IsString()
@IsOptional()
photo1?: string;
@IsArray()
@Transform(({ value }) => {
if (typeof value === 'string') {
try {
return JSON.parse(value);
} catch {
return [];
}
}
return value;
})
productionList?: any[];
@ApiProperty()
@IsString()
@IsOptional()
photo2?: string;
@ApiProperty()
@IsString()
@IsOptional()
photo3?: string;
@ApiProperty()
@IsString()
@IsOptional()
paralysisReason: string;
// nuevos campos coordinacion
@ApiProperty()
@IsInt()
@IsOptional()
coorState?: number;
@ApiProperty()
@IsInt()
@IsOptional()
coorMunicipality?: number;
@ApiProperty()
@IsInt()
@IsOptional()
coorParish?: number;
@ApiProperty()
@IsString()
@IsOptional()
coorPhone?: string;
// sectores
@ApiProperty()
@IsString()
ecoSector: string;
@ApiProperty()
@IsString()
productiveSector: string;
@ApiProperty()
@IsString()
centralProductiveActivity: string;
@ApiProperty()
@IsString()
mainProductiveActivity: string;
// equipamiento
@ApiProperty()
@IsString()
typesOfEquipment: string;
@ApiProperty()
@IsInt()
equipmentCount: number;
@ApiProperty()
@IsString()
equipmentDescription: string;
// materia prima
@ApiProperty()
@IsString()
rawMaterial: string;
@ApiProperty()
@IsString()
materialType: string;
@ApiProperty()
@IsInt()
rawMaterialCount: number;
// conteo de productos
@ApiProperty()
@IsInt()
productCountDaily: number;
@ApiProperty()
@IsInt()
productCountWeekly: number;
@ApiProperty()
@IsInt()
productCountMonthly: number;
@ApiProperty()
@IsString()
prodDescriptionInternal: string;
@ApiProperty()
@IsInt()
internalCount: number;
@ApiProperty()
@IsInt()
externalCount: number;
@ApiProperty()
@IsString()
prodDescriptionExternal: string;
@ApiProperty()
@IsString()
country: string;
@ApiProperty()
@IsString()
city: string;
@ApiProperty()
@IsInt()
menCount: number;
@ApiProperty()
@IsInt()
womenCount: number;
@IsArray()
@Transform(({ value }) => {
if (typeof value === 'string') {
try {
return JSON.parse(value);
} catch {
return [];
}
}
return value;
})
productList?: any[];
}

View File

@@ -74,95 +74,121 @@ export class TrainingService {
const filters: SQL[] = [];
if (startDate) {
if (startDate)
filters.push(gte(trainingSurveys.visitDate, new Date(startDate)));
}
if (endDate) {
if (endDate)
filters.push(lte(trainingSurveys.visitDate, new Date(endDate)));
}
if (stateId) {
filters.push(eq(trainingSurveys.state, stateId));
}
if (municipalityId) {
if (stateId) filters.push(eq(trainingSurveys.state, stateId));
if (municipalityId)
filters.push(eq(trainingSurveys.municipality, municipalityId));
}
if (parishId) {
filters.push(eq(trainingSurveys.parish, parishId));
}
if (ospType) {
filters.push(eq(trainingSurveys.ospType, ospType));
}
if (parishId) filters.push(eq(trainingSurveys.parish, parishId));
if (ospType) filters.push(eq(trainingSurveys.ospType, ospType));
const whereCondition = filters.length > 0 ? and(...filters) : undefined;
const totalOspsResult = await this.drizzle
.select({ count: sql<number>`count(*)` })
.from(trainingSurveys)
.where(whereCondition);
const totalOsps = Number(totalOspsResult[0].count);
// Ejecutamos todas las consultas en paralelo con Promise.all para mayor velocidad
const [
totalOspsResult,
totalProducersResult,
totalProductsResult, // Nuevo: Calculado desde el JSON
statusDistribution,
activityDistribution,
typeDistribution,
stateDistribution,
yearDistribution,
] = await Promise.all([
// 1. Total OSPs
this.drizzle
.select({ count: sql<number>`count(*)` })
.from(trainingSurveys)
.where(whereCondition),
const totalProducersResult = await this.drizzle
.select({ sum: sql<number>`sum(${trainingSurveys.producerCount})` })
.from(trainingSurveys)
.where(whereCondition);
const totalProducers = Number(totalProducersResult[0].sum || 0);
// 2. Total Productores (Columna plana que mantuviste)
this.drizzle
.select({
sum: sql<number>`
SUM(
(
SELECT SUM(
COALESCE((item->>'menCount')::int, 0) +
COALESCE((item->>'womenCount')::int, 0)
)
FROM jsonb_array_elements(${trainingSurveys.productList}) as item
)
)
`,
})
.from(trainingSurveys)
.where(whereCondition),
const statusDistribution = await this.drizzle
.select({
name: trainingSurveys.currentStatus,
value: sql<number>`count(*)`,
})
.from(trainingSurveys)
.where(whereCondition)
.groupBy(trainingSurveys.currentStatus);
// 3. NUEVO: Total Productos (Contamos el largo del array JSON productList)
this.drizzle
.select({
sum: sql<number>`sum(jsonb_array_length(${trainingSurveys.productList}))`,
})
.from(trainingSurveys)
.where(whereCondition),
const activityDistribution = await this.drizzle
.select({
name: trainingSurveys.productiveActivity,
value: sql<number>`count(*)`,
})
.from(trainingSurveys)
.where(whereCondition)
.groupBy(trainingSurveys.productiveActivity);
// 4. Distribución por Estatus
this.drizzle
.select({
name: trainingSurveys.currentStatus,
value: sql<number>`count(*)`,
})
.from(trainingSurveys)
.where(whereCondition)
.groupBy(trainingSurveys.currentStatus),
const typeDistribution = await this.drizzle
.select({
name: trainingSurveys.ospType,
value: sql<number>`count(*)`,
})
.from(trainingSurveys)
.where(whereCondition)
.groupBy(trainingSurveys.ospType);
// 5. Distribución por Actividad
this.drizzle
.select({
name: trainingSurveys.productiveActivity,
value: sql<number>`count(*)`,
})
.from(trainingSurveys)
.where(whereCondition)
.groupBy(trainingSurveys.productiveActivity),
// New Aggregations
const stateDistribution = await this.drizzle
.select({
name: states.name,
value: sql<number>`count(${trainingSurveys.id})`,
})
.from(trainingSurveys)
.leftJoin(states, eq(trainingSurveys.state, states.id))
.where(whereCondition)
.groupBy(states.name);
// 6. Distribución por Tipo
this.drizzle
.select({
name: trainingSurveys.ospType,
value: sql<number>`count(*)`,
})
.from(trainingSurveys)
.where(whereCondition)
.groupBy(trainingSurveys.ospType),
const yearDistribution = await this.drizzle
.select({
name: sql<string>`cast(${trainingSurveys.companyConstitutionYear} as text)`,
value: sql<number>`count(*)`,
})
.from(trainingSurveys)
.where(whereCondition)
.groupBy(trainingSurveys.companyConstitutionYear)
.orderBy(trainingSurveys.companyConstitutionYear);
// 7. Distribución por Estado (CORREGIDO con COALESCE)
this.drizzle
.select({
// Si states.name es NULL, devuelve 'Sin Asignar'
name: sql<string>`COALESCE(${states.name}, 'Sin Asignar')`,
value: sql<number>`count(${trainingSurveys.id})`,
})
.from(trainingSurveys)
.leftJoin(states, eq(trainingSurveys.state, states.id))
.where(whereCondition)
// Importante: Agrupar también por el resultado del COALESCE o por states.name
.groupBy(states.name),
// 8. Distribución por Año
this.drizzle
.select({
name: sql<string>`cast(${trainingSurveys.companyConstitutionYear} as text)`,
value: sql<number>`count(*)`,
})
.from(trainingSurveys)
.where(whereCondition)
.groupBy(trainingSurveys.companyConstitutionYear)
.orderBy(trainingSurveys.companyConstitutionYear),
]);
return {
totalOsps,
totalProducers,
totalOsps: Number(totalOspsResult[0]?.count || 0),
totalProducers: Number(totalProducersResult[0]?.sum || 0),
totalProducts: Number(totalProductsResult[0]?.sum || 0), // Dato extraído del JSON
statusDistribution: statusDistribution.map((item) => ({
...item,
value: Number(item.value),
@@ -239,16 +265,30 @@ export class TrainingService {
createTrainingDto: CreateTrainingDto,
files: Express.Multer.File[],
) {
// 1. Guardar fotos
const photoPaths = await this.saveFiles(files);
// 2. Extraer solo visitDate para formatearlo.
// Ya NO extraemos state, municipality, etc. porque no vienen en el DTO.
const { visitDate, ...rest } = createTrainingDto;
const [newRecord] = await this.drizzle
.insert(trainingSurveys)
.values({
...createTrainingDto,
visitDate: new Date(createTrainingDto.visitDate),
photo1: photoPaths[0] || '',
photo2: photoPaths[1] || null,
photo3: photoPaths[2] || null,
// Insertamos el resto de datos planos y las listas (arrays)
...rest,
// Conversión de fecha
visitDate: new Date(visitDate),
// 3. Asignar fotos de forma segura
photo1: photoPaths[0] ?? null,
photo2: photoPaths[1] ?? null,
photo3: photoPaths[2] ?? null,
// NOTA: Como las columnas state, municipality, etc. en la BD
// tienen "onDelete: set null" o son nullables, al no pasarlas aquí,
// Postgres automáticamente las guardará como NULL.
})
.returning();