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, "when": 1769629815868,
"tag": "0013_cuddly_night_nurse", "tag": "0013_cuddly_night_nurse",
"breakpoints": true "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( export const trainingSurveys = t.pgTable(
'training_surveys', 'training_surveys',
{ {
// Datos basicos // === 1. IDENTIFICADORES Y DATOS DE VISITA ===
id: t.serial('id').primaryKey(), id: t.serial('id').primaryKey(),
firstname: t.text('firstname').notNull(), firstname: t.text('firstname').notNull(),
lastname: t.text('lastname').notNull(), lastname: t.text('lastname').notNull(),
visitDate: t.timestamp('visit_date').notNull(), visitDate: t.timestamp('visit_date').notNull(),
// ubicacion coorPhone: t.text('coor_phone'),
// === 2. UBICACIÓN (Claves Foráneas - Nullables) ===
state: t state: t
.integer('state') .integer('state')
.references(() => states.id, { onDelete: 'set null' }), .references(() => states.id, { onDelete: 'set null' }),
@@ -61,93 +63,89 @@ export const trainingSurveys = t.pgTable(
parish: t parish: t
.integer('parish') .integer('parish')
.references(() => parishes.id, { onDelete: 'set null' }), .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(''), communeName: t.text('commune_name').notNull().default(''),
siturCodeCommune: t.text('situr_code_commune').notNull(),
communeRif: t.text('commune_rif').notNull().default(''), communeRif: t.text('commune_rif').notNull().default(''),
communeSpokespersonName: t.text('commune_spokesperson_name').notNull().default(''), communeSpokespersonName: t
communeSpokespersonCedula: t.text('commune_spokesperson_cedula').notNull().default(''), .text('commune_spokesperson_name')
communeSpokespersonRif: t.text('commune_spokesperson_rif').notNull().default(''), .notNull()
communeSpokespersonPhone: t.text('commune_spokesperson_phone').notNull().default(''), .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(''), communeEmail: t.text('commune_email').notNull().default(''),
communalCouncil: t.text('communal_council').notNull(), communalCouncil: t.text('communal_council').notNull(),
siturCodeCommunalCouncil: t.text('situr_code_communal_council').notNull(), siturCodeCommunalCouncil: t.text('situr_code_communal_council').notNull(),
communalCouncilRif: t.text('communal_council_rif').notNull().default(''), communalCouncilRif: t.text('communal_council_rif').notNull().default(''),
communalCouncilSpokespersonName: t.text('communal_council_spokesperson_name').notNull().default(''), communalCouncilSpokespersonName: t
communalCouncilSpokespersonCedula: t.text('communal_council_spokesperson_cedula').notNull().default(''), .text('communal_council_spokesperson_name')
communalCouncilSpokespersonRif: t.text('communal_council_spokesperson_rif').notNull().default(''), .notNull()
communalCouncilSpokespersonPhone: t.text('communal_council_spokesperson_phone').notNull().default(''), .default(''),
communalCouncilEmail: t.text('communal_council_email').notNull().default(''), communalCouncilSpokespersonCedula: t
ospGoogleMapsLink: t.text('osp_google_maps_link').notNull().default(''), .text('communal_council_spokesperson_cedula')
// datos del OSP (ORGANIZACIÓN SOCIOPRODUCTIVA) .notNull()
ospName: t.text('osp_name').notNull(), .default(''),
ospAddress: t.text('osp_address').notNull(), communalCouncilSpokespersonRif: t
ospRif: t.text('osp_rif').notNull(), .text('communal_council_spokesperson_rif')
ospType: t.text('osp_type').notNull(), .notNull()
productiveActivity: t.text('productive_activity').notNull(), .default(''),
financialRequirementDescription: t communalCouncilSpokespersonPhone: t
.text('financial_requirement_description') .text('communal_council_spokesperson_phone')
.notNull()
.default(''),
communalCouncilEmail: t
.text('communal_council_email')
.notNull() .notNull()
.default(''), .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(), ospResponsibleFullname: t.text('osp_responsible_fullname').notNull(),
ospResponsibleCedula: t.text('osp_responsible_cedula').notNull(), ospResponsibleCedula: t.text('osp_responsible_cedula').notNull(),
ospResponsibleRif: t.text('osp_responsible_rif').notNull(), ospResponsibleRif: t.text('osp_responsible_rif').notNull(),
civilState: t.text('civil_state').notNull(),
ospResponsiblePhone: t.text('osp_responsible_phone').notNull(), ospResponsiblePhone: t.text('osp_responsible_phone').notNull(),
ospResponsibleEmail: t.text('osp_responsible_email').notNull(), ospResponsibleEmail: t.text('osp_responsible_email').notNull(),
civilState: t.text('civil_state').notNull(),
familyBurden: t.integer('family_burden').notNull(), familyBurden: t.integer('family_burden').notNull(),
numberOfChildren: t.integer('number_of_children').notNull(), numberOfChildren: t.integer('number_of_children').notNull(),
// datos adicionales generalObservations: t.text('general_observations'),
generalObservations: t.text('general_observations').notNull(), photo1: t.text('photo1'),
paralysisReason: t.text('paralysis_reason').notNull(),
// fotos
photo1: t.text('photo1').notNull(),
photo2: t.text('photo2'), photo2: t.text('photo2'),
photo3: t.text('photo3'), 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, ...timestamps,
}, },
(trainingSurveys) => ({ (trainingSurveys) => ({

View File

@@ -1,7 +1,16 @@
import { ApiProperty } from '@nestjs/swagger'; 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 { export class CreateTrainingDto {
// === 1. DATOS BÁSICOS ===
@ApiProperty() @ApiProperty()
@IsString() @IsString()
firstname: string; firstname: string;
@@ -12,7 +21,25 @@ export class CreateTrainingDto {
@ApiProperty() @ApiProperty()
@IsDateString() @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() @ApiProperty()
@IsString() @IsString()
@@ -20,24 +47,108 @@ export class CreateTrainingDto {
@ApiProperty() @ApiProperty()
@IsString() @IsString()
@IsOptional() currentStatus: string;
financialRequirementDescription?: string;
@ApiProperty() @ApiProperty()
@IsInt() @IsInt()
@Type(() => Number) // Convierte "2017" -> 2017
companyConstitutionYear: number;
@ApiProperty()
@IsString()
@IsOptional() @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() @ApiProperty()
@IsInt() @IsInt()
@IsOptional() @Type(() => Number) // Convierte "3" -> 3
municipality: number; familyBurden: number;
@ApiProperty() @ApiProperty()
@IsInt() @IsInt()
@IsOptional() @Type(() => Number)
parish: number; numberOfChildren: number;
// === 5. COMUNA Y CONSEJO COMUNAL ===
@ApiProperty() @ApiProperty()
@IsString() @IsString()
siturCodeCommune: string; siturCodeCommune: string;
@@ -102,217 +213,51 @@ export class CreateTrainingDto {
@IsString() @IsString()
communalCouncilEmail: string; communalCouncilEmail: string;
@ApiProperty() // === 6. LISTAS (Arrays JSON) ===
@IsString() // Reciben un string JSON '[{...}]' y lo convierten a Objeto JS real
ospGoogleMapsLink: string;
@ApiProperty() @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() @IsOptional()
productCount: number; @IsArray()
@Transform(({ value }) => {
if (typeof value === 'string') {
try {
return JSON.parse(value);
} catch {
return [];
}
}
return value;
})
equipmentList?: any[];
@ApiProperty() @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() @IsOptional()
photo1?: string; @IsArray()
@Transform(({ value }) => {
if (typeof value === 'string') {
try {
return JSON.parse(value);
} catch {
return [];
}
}
return value;
})
productionList?: any[];
@ApiProperty() @ApiProperty()
@IsString()
@IsOptional() @IsOptional()
photo2?: string; @IsArray()
@Transform(({ value }) => {
@ApiProperty() if (typeof value === 'string') {
@IsString() try {
@IsOptional() return JSON.parse(value);
photo3?: string; } catch {
return [];
@ApiProperty() }
@IsString() }
@IsOptional() return value;
paralysisReason: string; })
productList?: any[];
// 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;
} }

View File

@@ -74,95 +74,121 @@ export class TrainingService {
const filters: SQL[] = []; const filters: SQL[] = [];
if (startDate) { if (startDate)
filters.push(gte(trainingSurveys.visitDate, new Date(startDate))); filters.push(gte(trainingSurveys.visitDate, new Date(startDate)));
} if (endDate)
if (endDate) {
filters.push(lte(trainingSurveys.visitDate, new Date(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)); 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 whereCondition = filters.length > 0 ? and(...filters) : undefined;
const totalOspsResult = await this.drizzle // Ejecutamos todas las consultas en paralelo con Promise.all para mayor velocidad
.select({ count: sql<number>`count(*)` }) const [
.from(trainingSurveys) totalOspsResult,
.where(whereCondition); totalProducersResult,
const totalOsps = Number(totalOspsResult[0].count); 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 // 2. Total Productores (Columna plana que mantuviste)
.select({ sum: sql<number>`sum(${trainingSurveys.producerCount})` }) this.drizzle
.from(trainingSurveys) .select({
.where(whereCondition); sum: sql<number>`
const totalProducers = Number(totalProducersResult[0].sum || 0); 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 // 3. NUEVO: Total Productos (Contamos el largo del array JSON productList)
.select({ this.drizzle
name: trainingSurveys.currentStatus, .select({
value: sql<number>`count(*)`, sum: sql<number>`sum(jsonb_array_length(${trainingSurveys.productList}))`,
}) })
.from(trainingSurveys) .from(trainingSurveys)
.where(whereCondition) .where(whereCondition),
.groupBy(trainingSurveys.currentStatus);
const activityDistribution = await this.drizzle // 4. Distribución por Estatus
.select({ this.drizzle
name: trainingSurveys.productiveActivity, .select({
value: sql<number>`count(*)`, name: trainingSurveys.currentStatus,
}) value: sql<number>`count(*)`,
.from(trainingSurveys) })
.where(whereCondition) .from(trainingSurveys)
.groupBy(trainingSurveys.productiveActivity); .where(whereCondition)
.groupBy(trainingSurveys.currentStatus),
const typeDistribution = await this.drizzle // 5. Distribución por Actividad
.select({ this.drizzle
name: trainingSurveys.ospType, .select({
value: sql<number>`count(*)`, name: trainingSurveys.productiveActivity,
}) value: sql<number>`count(*)`,
.from(trainingSurveys) })
.where(whereCondition) .from(trainingSurveys)
.groupBy(trainingSurveys.ospType); .where(whereCondition)
.groupBy(trainingSurveys.productiveActivity),
// New Aggregations // 6. Distribución por Tipo
const stateDistribution = await this.drizzle this.drizzle
.select({ .select({
name: states.name, name: trainingSurveys.ospType,
value: sql<number>`count(${trainingSurveys.id})`, value: sql<number>`count(*)`,
}) })
.from(trainingSurveys) .from(trainingSurveys)
.leftJoin(states, eq(trainingSurveys.state, states.id)) .where(whereCondition)
.where(whereCondition) .groupBy(trainingSurveys.ospType),
.groupBy(states.name);
const yearDistribution = await this.drizzle // 7. Distribución por Estado (CORREGIDO con COALESCE)
.select({ this.drizzle
name: sql<string>`cast(${trainingSurveys.companyConstitutionYear} as text)`, .select({
value: sql<number>`count(*)`, // Si states.name es NULL, devuelve 'Sin Asignar'
}) name: sql<string>`COALESCE(${states.name}, 'Sin Asignar')`,
.from(trainingSurveys) value: sql<number>`count(${trainingSurveys.id})`,
.where(whereCondition) })
.groupBy(trainingSurveys.companyConstitutionYear) .from(trainingSurveys)
.orderBy(trainingSurveys.companyConstitutionYear); .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 { return {
totalOsps, totalOsps: Number(totalOspsResult[0]?.count || 0),
totalProducers, totalProducers: Number(totalProducersResult[0]?.sum || 0),
totalProducts: Number(totalProductsResult[0]?.sum || 0), // Dato extraído del JSON
statusDistribution: statusDistribution.map((item) => ({ statusDistribution: statusDistribution.map((item) => ({
...item, ...item,
value: Number(item.value), value: Number(item.value),
@@ -239,16 +265,30 @@ export class TrainingService {
createTrainingDto: CreateTrainingDto, createTrainingDto: CreateTrainingDto,
files: Express.Multer.File[], files: Express.Multer.File[],
) { ) {
// 1. Guardar fotos
const photoPaths = await this.saveFiles(files); 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 const [newRecord] = await this.drizzle
.insert(trainingSurveys) .insert(trainingSurveys)
.values({ .values({
...createTrainingDto, // Insertamos el resto de datos planos y las listas (arrays)
visitDate: new Date(createTrainingDto.visitDate), ...rest,
photo1: photoPaths[0] || '',
photo2: photoPaths[1] || null, // Conversión de fecha
photo3: photoPaths[2] || null, 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(); .returning();

View File

@@ -1,4 +1,5 @@
AUTH_URL = http://localhost:3000 AUTH_URL = http://localhost:3000
AUTH_SECRET=wWgIwkHIGr28ydIUPsgVGNUNxXQ+brg1XXtA8PfjJjAEPJLDP2UTghWL8aE= AUTH_SECRET=wWgIwkHIGr28ydIUPsgVGNUNxXQ+brg1XXtA8PfjJjAEPJLDP2UTghWL8aE=
API_URL=http://localhost:8000 API_URL=http://localhost:8000
NEXT_PUBLIC_API_URL=http://localhost:8000

View File

@@ -10,14 +10,14 @@ export const GeneralItems: NavItem[] = [
isActive: false, isActive: false,
items: [], // No child items items: [], // No child items
}, },
{ // {
title: 'ProduTienda', // title: 'ProduTienda',
url: '/dashboard/productos/', // url: '/dashboard/productos/',
icon: 'blocks', // icon: 'blocks',
shortcut: ['p', 'p'], // shortcut: ['p', 'p'],
isActive: false, // isActive: false,
items: [], // No child items // items: [], // No child items
}, // },
]; ];
export const AdministrationItems: NavItem[] = [ export const AdministrationItems: NavItem[] = [
@@ -78,7 +78,7 @@ export const StatisticsItems: NavItem[] = [
role: ['admin', 'superadmin', 'autoridad'], role: ['admin', 'superadmin', 'autoridad'],
}, },
{ {
title: 'OSP', title: 'Datos OSP',
shortcut: ['s', 's'], shortcut: ['s', 's'],
url: '/dashboard/estadisticas/socioproductiva', url: '/dashboard/estadisticas/socioproductiva',
icon: 'blocks', icon: 'blocks',

View File

@@ -90,6 +90,8 @@ export const createTrainingAction = async (
payloadToSend = rest as any; payloadToSend = rest as any;
} }
console.log(payloadToSend);
const [error, data] = await safeFetchApi( const [error, data] = await safeFetchApi(
TrainingMutate, TrainingMutate,
'/training', '/training',

View File

@@ -0,0 +1,171 @@
import { Button } from '@repo/shadcn/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@repo/shadcn/components/ui/dialog';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@repo/shadcn/components/ui/table';
import { Input } from '@repo/shadcn/input';
import { Label } from '@repo/shadcn/label';
import { Trash2 } from 'lucide-react';
import { useState } from 'react';
import { useFieldArray, useFormContext } from 'react-hook-form';
export function EquipmentList() {
const { control, register } = useFormContext();
const { fields, append, remove } = useFieldArray({
control,
name: 'equipmentList',
});
const [isOpen, setIsOpen] = useState(false);
const [newItem, setNewItem] = useState({
machine: '',
specifications: '',
quantity: '',
});
const handleAdd = () => {
if (newItem.machine && newItem.quantity) {
append({ ...newItem, quantity: Number(newItem.quantity) });
setNewItem({ machine: '', specifications: '', quantity: '' });
setIsOpen(false);
}
};
return (
<div className="space-y-4">
<div className="flex justify-between items-center">
<h3 className="text-lg font-medium">Datos del Equipamiento</h3>
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button variant="outline">Agregar Maquinaria</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Agregar Maquinaria/Equipo</DialogTitle>
</DialogHeader>
<DialogDescription className="sr-only">
Datos de equipamiento
</DialogDescription>
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label>Maquinaria</Label>
<Input
value={newItem.machine}
onChange={(e) =>
setNewItem({ ...newItem, machine: e.target.value })
}
placeholder="Nombre de la maquinaria"
/>
</div>
<div className="space-y-2">
<Label>Especificaciones</Label>
<Input
value={newItem.specifications}
onChange={(e) =>
setNewItem({ ...newItem, specifications: e.target.value })
}
placeholder="Especificaciones técnicas"
/>
</div>
<div className="space-y-2">
<Label>Cantidad</Label>
<Input
type="number"
value={newItem.quantity}
onChange={(e) =>
setNewItem({ ...newItem, quantity: e.target.value })
}
placeholder="0"
/>
</div>
<div className="flex justify-end gap-4">
<Button
variant="outline"
type="button"
onClick={() => setIsOpen(false)}
>
Cancelar
</Button>
<Button onClick={handleAdd}>Guardar</Button>
</div>
</div>
</DialogContent>
</Dialog>
</div>
<div className="border rounded-md">
<Table>
<TableHeader>
<TableRow>
<TableHead>Maquinaria</TableHead>
<TableHead>Especificaciones</TableHead>
<TableHead>Cantidad</TableHead>
<TableHead className="w-[50px]"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{fields.map((field, index) => (
<TableRow key={field.id}>
<TableCell>
<input
type="hidden"
{...register(`equipmentList.${index}.machine`)}
/>
{/* @ts-ignore */}
{field.machine}
</TableCell>
<TableCell>
<input
type="hidden"
{...register(`equipmentList.${index}.specifications`)}
/>
{/* @ts-ignore */}
{field.specifications}
</TableCell>
<TableCell>
<input
type="hidden"
{...register(`equipmentList.${index}.quantity`)}
/>
{/* @ts-ignore */}
{field.quantity}
</TableCell>
<TableCell>
<Button
variant="ghost"
size="icon"
onClick={() => remove(index)}
>
<Trash2 className="h-4 w-4 text-destructive" />
</Button>
</TableCell>
</TableRow>
))}
{fields.length === 0 && (
<TableRow>
<TableCell
colSpan={4}
className="text-center text-muted-foreground"
>
No hay equipamiento registrado
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
</div>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,504 @@
import { COUNTRY_OPTIONS } from '@/constants/countries';
import {
useMunicipalityQuery,
useParishQuery,
useStateQuery,
} from '@/feactures/location/hooks/use-query-location';
import { Button } from '@repo/shadcn/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@repo/shadcn/components/ui/dialog';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@repo/shadcn/components/ui/table';
import { Input } from '@repo/shadcn/input';
import { Label } from '@repo/shadcn/label';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@repo/shadcn/select';
import { SelectSearchable } from '@repo/shadcn/select-searchable';
import { Trash2 } from 'lucide-react';
import { useState } from 'react';
import { useFieldArray, useFormContext } from 'react-hook-form';
// 1. Definimos la estructura de los datos para que TypeScript no se queje
interface ProductItem {
productName: string;
description: string;
dailyCount: string;
weeklyCount: string;
monthlyCount: string;
// ... resto de propiedades opcionales si las necesitas tipar estrictamente
[key: string]: any;
}
interface ProductFormValues {
productList: ProductItem[];
}
export function ProductActivityList() {
// 2. Pasamos el tipo genérico a useFormContext
const { control, register } = useFormContext<ProductFormValues>();
const { fields, append, remove } = useFieldArray({
control,
name: 'productList',
});
const [isOpen, setIsOpen] = useState(false);
// Modal Form State
const [newItem, setNewItem] = useState<any>({
productName: '',
description: '',
dailyCount: '',
weeklyCount: '',
monthlyCount: '',
// Internal dist
internalState: undefined,
internalMunicipality: undefined,
internalParish: undefined,
internalDescription: '',
internalQuantity: '',
internalUnit: '',
// External dist
externalCountry: '',
externalState: undefined,
externalMunicipality: undefined,
externalParish: undefined,
externalCity: '',
externalDescription: '',
externalQuantity: '',
externalUnit: '',
// Workforce
womenCount: '',
menCount: '',
});
// Location logic for Internal Validation
const [internalStateId, setInternalStateId] = useState(0);
const [internalMuniId, setInternalMuniId] = useState(0);
const { data: statesData } = useStateQuery();
const { data: internalMuniData } = useMunicipalityQuery(internalStateId);
const { data: internalParishData } = useParishQuery(internalMuniId);
// Location logic for External Validation
const [externalStateId, setExternalStateId] = useState(0);
const [externalMuniId, setExternalMuniId] = useState(0);
const { data: externalMuniData } = useMunicipalityQuery(externalStateId);
const { data: externalParishData } = useParishQuery(externalMuniId);
const isVenezuela = newItem.externalCountry === 'Venezuela';
const handleAdd = () => {
if (newItem.productName) {
append(newItem);
setNewItem({
productName: '',
description: '',
dailyCount: '',
weeklyCount: '',
monthlyCount: '',
internalState: undefined,
internalMunicipality: undefined,
internalParish: undefined,
internalDescription: '',
internalQuantity: '',
internalUnit: '',
externalCountry: '',
externalState: undefined,
externalMunicipality: undefined,
externalParish: undefined,
externalCity: '',
externalDescription: '',
externalQuantity: '',
externalUnit: '',
womenCount: '',
menCount: '',
});
setInternalStateId(0);
setInternalMuniId(0);
setExternalStateId(0);
setExternalMuniId(0);
setIsOpen(false);
}
};
const stateOptions = statesData?.data || [];
const internalMuniOptions = internalMuniData?.data || [];
const internalParishOptions = internalParishData?.data || [];
const externalMuniOptions = externalMuniData?.data || [];
const externalParishOptions = externalParishData?.data || [];
return (
<div className="space-y-4">
<div className="flex justify-between items-center">
<h3 className="text-lg font-medium">Datos de Actividad Productiva</h3>
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button variant="outline">Agregar Producto/Actividad</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[800px] max-h-[80vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Detalles de Actividad Productiva</DialogTitle>
</DialogHeader>
<DialogDescription className="sr-only">
Datos de actividad productiva
</DialogDescription>
<div className="space-y-6 py-4">
{/* Basic Info */}
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label>Producto Terminado</Label>
<Input
value={newItem.productName}
onChange={(e) =>
setNewItem({ ...newItem, productName: e.target.value })
}
/>
</div>
<div className="space-y-2">
<Label>Descripción</Label>
<Input
value={newItem.description}
onChange={(e) =>
setNewItem({ ...newItem, description: e.target.value })
}
/>
</div>
</div>
<div className="grid grid-cols-3 gap-4">
<div className="space-y-2">
<Label>Cant. Diario</Label>
<Input
type="number"
value={newItem.dailyCount}
onChange={(e) =>
setNewItem({ ...newItem, dailyCount: e.target.value })
}
/>
</div>
<div className="space-y-2">
<Label>Cant. Semanal</Label>
<Input
type="number"
value={newItem.weeklyCount}
onChange={(e) =>
setNewItem({ ...newItem, weeklyCount: e.target.value })
}
/>
</div>
<div className="space-y-2">
<Label>Cant. Mensual</Label>
<Input
type="number"
value={newItem.monthlyCount}
onChange={(e) =>
setNewItem({ ...newItem, monthlyCount: e.target.value })
}
/>
</div>
</div>
<hr />
<h4 className="font-semibold">Distribución Interna</h4>
<div className="grid grid-cols-3 gap-4">
<div className="space-y-2">
<Label>Estado</Label>
<SelectSearchable
options={stateOptions.map((s) => ({
value: String(s.id),
label: s.name,
}))}
onValueChange={(val) => {
const id = Number(val);
setInternalStateId(id);
setNewItem({ ...newItem, internalState: id });
}}
placeholder="Estado"
/>
</div>
<div className="space-y-2">
<Label>Municipio</Label>
<SelectSearchable
options={internalMuniOptions.map((s) => ({
value: String(s.id),
label: s.name,
}))}
onValueChange={(val) => {
const id = Number(val);
setInternalMuniId(id);
setNewItem({ ...newItem, internalMunicipality: id });
}}
placeholder="Municipio"
disabled={!internalStateId}
/>
</div>
<div className="space-y-2">
<Label>Parroquia</Label>
<SelectSearchable
options={internalParishOptions.map((s) => ({
value: String(s.id),
label: s.name,
}))}
onValueChange={(val) =>
setNewItem({ ...newItem, internalParish: Number(val) })
}
placeholder="Parroquia"
disabled={!internalMuniId}
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label>Breve Descripción</Label>
<Input
value={newItem.internalDescription}
onChange={(e) =>
setNewItem({
...newItem,
internalDescription: e.target.value,
})
}
/>
</div>
<div className="space-y-2">
<Label>Cantidad Numérica (Kg, TON, UNID. LT)</Label>
<Input
type="number"
value={newItem.internalQuantity}
onChange={(e) =>
setNewItem({
...newItem,
internalQuantity: e.target.value,
})
}
/>
</div>
</div>
<hr />
<h4 className="font-semibold">Distribución Externa</h4>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label>País</Label>
<Select
value={newItem.externalCountry}
onValueChange={(val) =>
setNewItem({ ...newItem, externalCountry: val })
}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Seleccione País" />
</SelectTrigger>
<SelectContent>
{/* 3. CORRECCIÓN DEL MAPEO DE PAÍSES Y KEYS */}
{COUNTRY_OPTIONS.map((country: string) => (
<SelectItem key={country} value={country}>
{country}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{!isVenezuela && (
<div className="space-y-2">
<Label>Ciudad</Label>
<Input
value={newItem.externalCity}
onChange={(e) =>
setNewItem({ ...newItem, externalCity: e.target.value })
}
/>
</div>
)}
</div>
{isVenezuela && (
<div className="grid grid-cols-3 gap-4">
<div className="space-y-2">
<Label>Estado</Label>
<SelectSearchable
options={stateOptions.map((s) => ({
value: String(s.id),
label: s.name,
}))}
onValueChange={(val) => {
const id = Number(val);
setExternalStateId(id);
setNewItem({ ...newItem, externalState: id });
}}
placeholder="Estado"
/>
</div>
<div className="space-y-2">
<Label>Municipio</Label>
<SelectSearchable
options={externalMuniOptions.map((s) => ({
value: String(s.id),
label: s.name,
}))}
onValueChange={(val) => {
const id = Number(val);
setExternalMuniId(id);
setNewItem({ ...newItem, externalMunicipality: id });
}}
placeholder="Municipio"
disabled={!externalStateId}
/>
</div>
<div className="space-y-2">
<Label>Parroquia</Label>
<SelectSearchable
options={externalParishOptions.map((s) => ({
value: String(s.id),
label: s.name,
}))}
onValueChange={(val) =>
setNewItem({ ...newItem, externalParish: Number(val) })
}
placeholder="Parroquia"
disabled={!externalMuniId}
/>
</div>
</div>
)}
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label>Breve Descripción</Label>
<Input
value={newItem.externalDescription}
onChange={(e) =>
setNewItem({
...newItem,
externalDescription: e.target.value,
})
}
/>
</div>
<div className="space-y-2">
<Label>Cantidad Numérica (Kg, TON, UNID. LT)</Label>
<Input
type="number"
value={newItem.externalQuantity}
onChange={(e) =>
setNewItem({
...newItem,
externalQuantity: e.target.value,
})
}
/>
</div>
</div>
<hr />
<h4 className="font-semibold">Mano de Obra</h4>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label>Mujer (cantidad)</Label>
<Input
type="number"
value={newItem.womenCount}
onChange={(e) =>
setNewItem({ ...newItem, womenCount: e.target.value })
}
/>
</div>
<div className="space-y-2">
<Label>Hombre (cantidad)</Label>
<Input
type="number"
value={newItem.menCount}
onChange={(e) =>
setNewItem({ ...newItem, menCount: e.target.value })
}
/>
</div>
</div>
<div className="flex justify-end gap-4">
<Button
variant="outline"
type="button"
onClick={() => setIsOpen(false)}
>
Cancelar
</Button>
<Button onClick={handleAdd}>Guardar</Button>
</div>
</div>
</DialogContent>
</Dialog>
</div>
<div className="border rounded-md">
<Table>
<TableHeader>
<TableRow>
<TableHead>Producto</TableHead>
<TableHead>Descripción</TableHead>
<TableHead>Mensual</TableHead>
<TableHead className="w-[50px]"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{fields.map((field, index) => (
<TableRow key={field.id}>
<TableCell>
<input
type="hidden"
{...register(`productList.${index}.productName`)}
// field.productName ahora es válido gracias a la interface
value={field.productName}
/>
{field.productName}
</TableCell>
<TableCell>{field.description}</TableCell>
<TableCell>{field.monthlyCount}</TableCell>
<TableCell>
<Button
variant="ghost"
size="icon"
onClick={() => remove(index)}
>
<Trash2 className="h-4 w-4 text-destructive" />
</Button>
</TableCell>
</TableRow>
))}
{fields.length === 0 && (
<TableRow>
<TableCell
colSpan={4}
className="text-center text-muted-foreground"
>
No hay productos registrados
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
</div>
);
}

View File

@@ -0,0 +1,171 @@
import { Button } from '@repo/shadcn/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@repo/shadcn/components/ui/dialog';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@repo/shadcn/components/ui/table';
import { Input } from '@repo/shadcn/input';
import { Label } from '@repo/shadcn/label';
import { Trash2 } from 'lucide-react';
import { useState } from 'react';
import { useFieldArray, useFormContext } from 'react-hook-form';
export function ProductionList() {
const { control, register } = useFormContext();
const { fields, append, remove } = useFieldArray({
control,
name: 'productionList',
});
const [isOpen, setIsOpen] = useState(false);
const [newItem, setNewItem] = useState({
rawMaterial: '',
supplyType: '',
quantity: '',
});
const handleAdd = () => {
if (newItem.rawMaterial && newItem.quantity) {
append({ ...newItem, quantity: Number(newItem.quantity) });
setNewItem({ rawMaterial: '', supplyType: '', quantity: '' });
setIsOpen(false);
}
};
return (
<div className="space-y-4">
<div className="flex justify-between items-center">
<h3 className="text-lg font-medium">Datos de Producción</h3>
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button variant="outline">Agregar Producción</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Agregar Datos de Producción</DialogTitle>
</DialogHeader>
<DialogDescription className="sr-only">
Datos de producción
</DialogDescription>
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label>Materia prima requerida (mensual)</Label>
<Input
value={newItem.rawMaterial}
onChange={(e) =>
setNewItem({ ...newItem, rawMaterial: e.target.value })
}
placeholder="Descripción de materia prima"
/>
</div>
<div className="space-y-2">
<Label>Tipo de Insumo/Rubro</Label>
<Input
value={newItem.supplyType}
onChange={(e) =>
setNewItem({ ...newItem, supplyType: e.target.value })
}
placeholder="Tipo"
/>
</div>
<div className="space-y-2">
<Label>Cantidad Mensual (Kg, TON, UNID. LT)</Label>
<Input
type="number"
value={newItem.quantity}
onChange={(e) =>
setNewItem({ ...newItem, quantity: e.target.value })
}
placeholder="0"
/>
</div>
<div className="flex justify-end gap-4">
<Button
variant="outline"
type="button"
onClick={() => setIsOpen(false)}
>
Cancelar
</Button>
<Button onClick={handleAdd}>Guardar</Button>
</div>
</div>
</DialogContent>
</Dialog>
</div>
<div className="border rounded-md">
<Table>
<TableHeader>
<TableRow>
<TableHead>Materia Prima</TableHead>
<TableHead>Tipo Insumo</TableHead>
<TableHead>Cantidad (Mensual)</TableHead>
<TableHead className="w-[50px]"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{fields.map((field, index) => (
<TableRow key={field.id}>
<TableCell>
<input
type="hidden"
{...register(`productionList.${index}.rawMaterial`)}
/>
{/* @ts-ignore */}
{field.rawMaterial}
</TableCell>
<TableCell>
<input
type="hidden"
{...register(`productionList.${index}.supplyType`)}
/>
{/* @ts-ignore */}
{field.supplyType}
</TableCell>
<TableCell>
<input
type="hidden"
{...register(`productionList.${index}.quantity`)}
/>
{/* @ts-ignore */}
{field.quantity}
</TableCell>
<TableCell>
<Button
variant="ghost"
size="icon"
onClick={() => remove(index)}
>
<Trash2 className="h-4 w-4 text-destructive" />
</Button>
</TableCell>
</TableRow>
))}
{fields.length === 0 && (
<TableRow>
<TableCell
colSpan={4}
className="text-center text-muted-foreground"
>
No hay datos de producción registrados
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
</div>
);
}

View File

@@ -1,225 +1,276 @@
'use client'; 'use client';
import { useState } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@repo/shadcn/card';
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, PieChart, Pie, Cell } from 'recharts';
import { useTrainingStatsQuery } from '../hooks/use-training-statistics';
import { Input } from '@repo/shadcn/input';
import { Button } from '@repo/shadcn/button';
import { SelectSearchable } from '@repo/shadcn/select-searchable';
import { useStateQuery, useMunicipalityQuery, useParishQuery } from '@/feactures/location/hooks/use-query-location';
import { import {
Select, useMunicipalityQuery,
SelectContent, useParishQuery,
SelectItem, useStateQuery,
SelectTrigger, } from '@/feactures/location/hooks/use-query-location';
SelectValue, import { Button } from '@repo/shadcn/button';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@repo/shadcn/card';
import { Input } from '@repo/shadcn/input';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@repo/shadcn/select'; } from '@repo/shadcn/select';
import { SelectSearchable } from '@repo/shadcn/select-searchable';
import { useState } from 'react';
import {
Bar,
BarChart,
CartesianGrid,
Cell,
Legend,
Pie,
PieChart,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from 'recharts';
import { useTrainingStatsQuery } from '../hooks/use-training-statistics';
const OSP_TYPES = [ const OSP_TYPES = [
'EPSD', 'EPSD',
'EPSI', 'EPSI',
'UPF', 'UPF',
'Cooperativa', 'Cooperativa',
'Grupo de Intercambio', 'Grupo de Intercambio',
]; ];
export function TrainingStatistics() { export function TrainingStatistics() {
// Filter State // Filter State
const [startDate, setStartDate] = useState<string>(''); const [startDate, setStartDate] = useState<string>('');
const [endDate, setEndDate] = useState<string>(''); const [endDate, setEndDate] = useState<string>('');
const [stateId, setStateId] = useState<number>(0); const [stateId, setStateId] = useState<number>(0);
const [municipalityId, setMunicipalityId] = useState<number>(0); const [municipalityId, setMunicipalityId] = useState<number>(0);
const [parishId, setParishId] = useState<number>(0); const [parishId, setParishId] = useState<number>(0);
const [ospType, setOspType] = useState<string>(''); const [ospType, setOspType] = useState<string>('');
// Location Data // Location Data
const { data: dataState } = useStateQuery(); const { data: dataState } = useStateQuery();
const { data: dataMunicipality } = useMunicipalityQuery(stateId); const { data: dataMunicipality } = useMunicipalityQuery(stateId);
const { data: dataParish } = useParishQuery(municipalityId); const { data: dataParish } = useParishQuery(municipalityId);
const stateOptions = dataState?.data || [{ id: 0, name: 'Sin estados' }]; const stateOptions = dataState?.data || [{ id: 0, name: 'Sin estados' }];
const municipalityOptions = Array.isArray(dataMunicipality?.data) && dataMunicipality.data.length > 0 const municipalityOptions =
? dataMunicipality.data Array.isArray(dataMunicipality?.data) && dataMunicipality.data.length > 0
: [{ id: 0, stateId: 0, name: 'Sin Municipios' }]; ? dataMunicipality.data
const parishOptions = Array.isArray(dataParish?.data) && dataParish.data.length > 0 : [{ id: 0, stateId: 0, name: 'Sin Municipios' }];
? dataParish.data const parishOptions =
: [{ id: 0, stateId: 0, name: 'Sin Parroquias' }]; Array.isArray(dataParish?.data) && dataParish.data.length > 0
? dataParish.data
: [{ id: 0, stateId: 0, name: 'Sin Parroquias' }];
// Query with Filters // Query with Filters
const { data, isLoading, refetch } = useTrainingStatsQuery({ const { data, isLoading, refetch } = useTrainingStatsQuery({
startDate: startDate || undefined, startDate: startDate || undefined,
endDate: endDate || undefined, endDate: endDate || undefined,
stateId: stateId || undefined, stateId: stateId || undefined,
municipalityId: municipalityId || undefined, municipalityId: municipalityId || undefined,
parishId: parishId || undefined, parishId: parishId || undefined,
ospType: ospType || undefined, ospType: ospType || undefined,
}); });
const handleClearFilters = () => { const handleClearFilters = () => {
setStartDate(''); setStartDate('');
setEndDate(''); setEndDate('');
setStateId(0); setStateId(0);
setMunicipalityId(0); setMunicipalityId(0);
setParishId(0); setParishId(0);
setOspType(''); setOspType('');
}; };
if (isLoading) {
return <div className="flex justify-center p-8">Cargando estadísticas...</div>;
}
if (!data) {
return <div className="flex justify-center p-8">No hay datos disponibles.</div>;
}
const { totalOsps, totalProducers, statusDistribution, activityDistribution, typeDistribution, stateDistribution, yearDistribution } = data;
const COLORS = ['#0088FE', '#00C49F', '#FFBB28', '#FF8042', '#8884d8', '#82ca9d'];
if (isLoading) {
return ( return (
<div className="space-y-6"> <div className="flex justify-center p-8">Cargando estadísticas...</div>
{/* Filters Section */} );
<Card> }
<CardHeader>
<CardTitle>Filtros</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-4">
<div className="space-y-2">
<label className="text-sm font-medium">Fecha Inicio</label>
<Input
type="date"
value={startDate}
onChange={(e) => setStartDate(e.target.value)}
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Fecha Fin</label>
<Input
type="date"
value={endDate}
onChange={(e) => setEndDate(e.target.value)}
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Estado</label>
<SelectSearchable
options={stateOptions.map((item) => ({
value: item.id.toString(),
label: item.name,
}))}
onValueChange={(value: any) => {
setStateId(Number(value));
setMunicipalityId(0); // Reset municipality
setParishId(0); // Reset parish
}}
placeholder="Selecciona un estado"
defaultValue={stateId ? stateId.toString() : ""}
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Municipio</label>
<SelectSearchable
options={municipalityOptions.map((item) => ({
value: item.id.toString(),
label: item.name,
}))}
onValueChange={(value: any) => {
setMunicipalityId(Number(value));
setParishId(0);
}}
placeholder="Selecciona municipio"
defaultValue={municipalityId ? municipalityId.toString() : ""}
disabled={!stateId || stateId === 0}
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Parroquia</label>
<SelectSearchable
options={parishOptions.map((item) => ({
value: item.id.toString(),
label: item.name,
}))}
onValueChange={(value: any) => setParishId(Number(value))}
placeholder="Selecciona parroquia"
defaultValue={parishId ? parishId.toString() : ""}
disabled={!municipalityId || municipalityId === 0}
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Tipo de OSP</label>
<Select value={ospType} onValueChange={setOspType}>
<SelectTrigger>
<SelectValue placeholder="Todos" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Todos</SelectItem>
{OSP_TYPES.map(type => (
<SelectItem key={type} value={type}>{type}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex items-end">
<Button variant="outline" onClick={handleClearFilters}>
Limpiar Filtros
</Button>
</div>
</div>
</CardContent>
</Card>
{/* Statistics Cards */} if (!data) {
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3"> return (
<Card> <div className="flex justify-center p-8">No hay datos disponibles.</div>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> );
<CardTitle className="text-sm font-medium">Total de OSP Registradas</CardTitle> }
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{totalOsps}</div>
<p className="text-xs text-muted-foreground">
Organizaciones Socioproductivas
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Total de Productores</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{totalProducers}</div>
<p className="text-xs text-muted-foreground">
Productores asociados
</p>
</CardContent>
</Card>
<Card className="col-span-full"> const {
<CardHeader> totalOsps,
<CardTitle>Actividad Productiva</CardTitle> totalProducers,
<CardDescription>Distribución por tipo de actividad</CardDescription> statusDistribution,
</CardHeader> activityDistribution,
<CardContent className="h-[400px]"> typeDistribution,
<ResponsiveContainer width="100%" height="100%"> stateDistribution,
<BarChart yearDistribution,
data={activityDistribution} } = data;
layout="vertical"
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
>
<CartesianGrid strokeDasharray="3 3" />
<XAxis type="number" />
<YAxis dataKey="name" type="category" width={150} />
<Tooltip wrapperStyle={{ color: '#000' }} />
<Legend />
<Bar dataKey="value" fill="#8884d8" name="Cantidad" />
</BarChart>
</ResponsiveContainer>
</CardContent>
</Card>
{/* State Distribution */} const COLORS = [
<Card className="col-span-full"> '#0088FE',
'#00C49F',
'#FFBB28',
'#FF8042',
'#8884d8',
'#82ca9d',
];
return (
<div className="space-y-6">
{/* Filters Section */}
<Card>
<CardHeader>
<CardTitle>Filtros</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-4">
<div className="space-y-2">
<label className="text-sm font-medium">Fecha Inicio</label>
<Input
type="date"
value={startDate}
onChange={(e) => setStartDate(e.target.value)}
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Fecha Fin</label>
<Input
type="date"
value={endDate}
onChange={(e) => setEndDate(e.target.value)}
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Estado</label>
<SelectSearchable
options={stateOptions.map((item) => ({
value: item.id.toString(),
label: item.name,
}))}
onValueChange={(value: any) => {
setStateId(Number(value));
setMunicipalityId(0); // Reset municipality
setParishId(0); // Reset parish
}}
placeholder="Selecciona un estado"
defaultValue={stateId ? stateId.toString() : ''}
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Municipio</label>
<SelectSearchable
options={municipalityOptions.map((item) => ({
value: item.id.toString(),
label: item.name,
}))}
onValueChange={(value: any) => {
setMunicipalityId(Number(value));
setParishId(0);
}}
placeholder="Selecciona municipio"
defaultValue={municipalityId ? municipalityId.toString() : ''}
disabled={!stateId || stateId === 0}
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Parroquia</label>
<SelectSearchable
options={parishOptions.map((item) => ({
value: item.id.toString(),
label: item.name,
}))}
onValueChange={(value: any) => setParishId(Number(value))}
placeholder="Selecciona parroquia"
defaultValue={parishId ? parishId.toString() : ''}
disabled={!municipalityId || municipalityId === 0}
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Tipo de OSP</label>
<Select value={ospType} onValueChange={setOspType}>
<SelectTrigger>
<SelectValue placeholder="Todos" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Todos</SelectItem>
{OSP_TYPES.map((type) => (
<SelectItem key={type} value={type}>
{type}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex items-end">
<Button variant="outline" onClick={handleClearFilters}>
Limpiar Filtros
</Button>
</div>
</div>
</CardContent>
</Card>
{/* Statistics Cards */}
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Total de OSP Registradas
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{totalOsps}</div>
<p className="text-xs text-muted-foreground">
Organizaciones Socioproductivas
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Total de Productores
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{totalProducers}</div>
<p className="text-xs text-muted-foreground">
Productores asociados
</p>
</CardContent>
</Card>
<Card className="col-span-full">
<CardHeader>
<CardTitle>Actividad Productiva</CardTitle>
<CardDescription>
Distribución por tipo de actividad
</CardDescription>
</CardHeader>
<CardContent className="h-[400px]">
<ResponsiveContainer width="100%" height="100%">
<BarChart
data={activityDistribution}
layout="vertical"
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
>
<CartesianGrid strokeDasharray="3 3" />
<XAxis type="number" />
<YAxis dataKey="name" type="category" width={150} />
<Tooltip wrapperStyle={{ color: '#000' }} />
<Legend />
<Bar dataKey="value" fill="#8884d8" name="Cantidad" />
</BarChart>
</ResponsiveContainer>
</CardContent>
</Card>
{/* State Distribution */}
{/* <Card className="col-span-full">
<CardHeader> <CardHeader>
<CardTitle>Distribución por Estado</CardTitle> <CardTitle>Distribución por Estado</CardTitle>
<CardDescription>OSP registradas por estado</CardDescription> <CardDescription>OSP registradas por estado</CardDescription>
@@ -239,81 +290,84 @@ export function TrainingStatistics() {
</BarChart> </BarChart>
</ResponsiveContainer> </ResponsiveContainer>
</CardContent> </CardContent>
</Card> </Card> */}
{/* Year Distribution */} {/* Year Distribution */}
<Card className="col-span-full lg:col-span-1"> <Card className="col-span-full lg:col-span-1">
<CardHeader> <CardHeader>
<CardTitle>Año de Constitución</CardTitle> <CardTitle>Año de Constitución</CardTitle>
<CardDescription>Año de registro de la empresa</CardDescription> <CardDescription>Año de registro de la empresa</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="h-[400px]"> <CardContent className="h-[400px]">
<ResponsiveContainer width="100%" height="100%"> <ResponsiveContainer width="100%" height="100%">
<BarChart <BarChart
data={yearDistribution} data={yearDistribution}
margin={{ top: 5, right: 30, left: 20, bottom: 5 }} margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
> >
<CartesianGrid strokeDasharray="3 3" /> <CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="name" /> <XAxis dataKey="name" />
<YAxis /> <YAxis />
<Tooltip wrapperStyle={{ color: '#000' }} /> <Tooltip wrapperStyle={{ color: '#000' }} />
<Legend /> <Legend />
<Bar dataKey="value" fill="#FFBB28" name="Cantidad" /> <Bar dataKey="value" fill="#FFBB28" name="Cantidad" />
</BarChart> </BarChart>
</ResponsiveContainer> </ResponsiveContainer>
</CardContent> </CardContent>
</Card> </Card>
<Card className="col-span-1 md:col-span-2 lg:col-span-1"> <Card className="col-span-1 md:col-span-2 lg:col-span-1">
<CardHeader> <CardHeader>
<CardTitle>Estatus Actual</CardTitle> <CardTitle>Estatus Actual</CardTitle>
<CardDescription>Estado operativo de las OSP</CardDescription> <CardDescription>Estado operativo de las OSP</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="h-[300px]"> <CardContent className="h-[300px]">
<ResponsiveContainer width="100%" height="100%"> <ResponsiveContainer width="100%" height="100%">
<PieChart> <PieChart>
<Pie <Pie
data={statusDistribution} data={statusDistribution}
cx="50%" cx="50%"
cy="50%" cy="50%"
innerRadius={60} innerRadius={60}
outerRadius={80} outerRadius={80}
fill="#8884d8" fill="#8884d8"
paddingAngle={5} paddingAngle={5}
dataKey="value" dataKey="value"
> >
{statusDistribution.map((entry, index) => ( {statusDistribution.map((entry, index) => (
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} /> <Cell
))} key={`cell-${index}`}
</Pie> fill={COLORS[index % COLORS.length]}
<Tooltip wrapperStyle={{ color: '#000' }} /> />
<Legend /> ))}
</PieChart> </Pie>
</ResponsiveContainer> <Tooltip wrapperStyle={{ color: '#000' }} />
</CardContent> <Legend />
</Card> </PieChart>
<Card className="col-span-1 md:col-span-2 lg:col-span-1"> </ResponsiveContainer>
<CardHeader> </CardContent>
<CardTitle>Tipo de Organización</CardTitle> </Card>
<CardDescription>Clasificación de las OSP</CardDescription> <Card className="col-span-1 md:col-span-2 lg:col-span-1">
</CardHeader> <CardHeader>
<CardContent className="h-[300px]"> <CardTitle>Tipo de Organización</CardTitle>
<ResponsiveContainer width="100%" height="100%"> <CardDescription>Clasificación de las OSP</CardDescription>
<BarChart </CardHeader>
data={typeDistribution} <CardContent className="h-[300px]">
margin={{ top: 5, right: 30, left: 20, bottom: 5 }} <ResponsiveContainer width="100%" height="100%">
> <BarChart
<CartesianGrid strokeDasharray="3 3" /> data={typeDistribution}
<XAxis dataKey="name" /> margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
<YAxis /> >
<Tooltip wrapperStyle={{ color: '#000' }} /> <CartesianGrid strokeDasharray="3 3" />
<Legend /> <XAxis dataKey="name" />
<Bar dataKey="value" fill="#82ca9d" name="Cantidad" /> <YAxis />
</BarChart> <Tooltip wrapperStyle={{ color: '#000' }} />
</ResponsiveContainer> <Legend />
</CardContent> <Bar dataKey="value" fill="#82ca9d" name="Cantidad" />
</Card> </BarChart>
</div> </ResponsiveContainer>
</div> </CardContent>
); </Card>
</div>
</div>
);
} }

View File

@@ -16,7 +16,16 @@ import {
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
} from '@repo/shadcn/components/ui/dialog'; } from '@repo/shadcn/components/ui/dialog';
import { X } from 'lucide-react'; import { ScrollArea } from '@repo/shadcn/components/ui/scroll-area';
import { Separator } from '@repo/shadcn/components/ui/separator';
import {
ExternalLink,
Factory,
MapPin,
Package,
Wrench,
X,
} from 'lucide-react';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { TrainingSchema } from '../schemas/training'; import { TrainingSchema } from '../schemas/training';
@@ -37,235 +46,447 @@ export function TrainingViewModal({
const DetailItem = ({ label, value }: { label: string; value: any }) => ( const DetailItem = ({ label, value }: { label: string; value: any }) => (
<div className="space-y-1"> <div className="space-y-1">
<p className="text-sm font-medium text-muted-foreground">{label}</p> <p className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
<p className="text-sm font-semibold">{value || 'N/A'}</p> {label}
</p>
<p className="text-sm font-semibold text-foreground break-words">
{value !== null && value !== undefined && value !== '' ? value : 'N/A'}
</p>
</div> </div>
); );
const Section = ({ const Section = ({
title, title,
icon: Icon,
children, children,
}: { }: {
title: string; title: string;
icon?: React.ElementType;
children: React.ReactNode; children: React.ReactNode;
}) => ( }) => (
<Card className="mb-4"> <Card className="overflow-hidden border-l-4 border-l-primary/20">
<CardHeader className="py-3"> <CardHeader className="py-3 bg-muted/30">
<CardTitle className="text-lg">{title}</CardTitle> <CardTitle className="text-lg flex items-center gap-2">
{Icon && <Icon className="h-5 w-5 text-primary" />}
{title}
</CardTitle>
</CardHeader> </CardHeader>
<CardContent className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> <CardContent className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-x-4 gap-y-6 pt-4">
{children} {children}
</CardContent> </CardContent>
</Card> </Card>
); );
const BooleanBadge = ({ value }: { value?: boolean }) => (
<Badge variant={value ? 'default' : 'secondary'}>
{value ? 'Sí' : 'No'}
</Badge>
);
return ( return (
<> <>
<Dialog open={isOpen} onOpenChange={onClose}> <Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="sm:max-w-[800px] overflow-y-auto [&>button:last-child]:hidden"> <DialogContent className="sm:max-w-[1000px] max-h-[90vh] p-0 flex flex-col">
<DialogHeader> <DialogHeader className="px-6 py-4 border-b">
<DialogTitle className="text-2xl font-bold"> <DialogTitle className="text-2xl font-bold flex items-center gap-2">
Detalle de la Organización Socioproductiva <Factory className="h-6 w-6" />
{data.ospName}
</DialogTitle> </DialogTitle>
<DialogDescription className="sr-only"> <DialogDescription>
Resumen detallado de la información de la organización {data.ospType} {data.ospRif} {' '}
socioproductiva incluyendo ubicación, responsable y registro <span
fotográfico. className={
data.currentStatus === 'ACTIVA'
? 'text-green-600 font-medium'
: 'text-red-600'
}
>
{data.currentStatus}
</span>
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div className="mt-4 space-y-6"> <ScrollArea className="flex-1 px-6 py-6">
{/* 1. Datos de la visita */} <div className="space-y-8">
<Section title="1. Datos de la visita"> {/* 1. Datos de la Visita */}
<DetailItem <Section title="Datos de la Visita">
label="Nombre del Coordinador" <DetailItem
value={data.firstname} label="Coordinador"
/> value={`${data.firstname} ${data.lastname}`}
<DetailItem />
label="Apellido del Coordinador" <DetailItem label="Teléfono Coord." value={data.coorPhone} />
value={data.lastname} <DetailItem
/> label="Fecha Visita"
<DetailItem value={
label="Fecha y hora de la visita" data.visitDate
value={ ? new Date(data.visitDate).toLocaleString()
data.visitDate : 'N/A'
? new Date(data.visitDate).toLocaleString() }
: 'N/A' />
} </Section>
/>
</Section>
{/* 2. Datos de la OSP */} {/* 2. Sectores y Actividad */}
<Section title="2. Datos de la OSP"> <Section title="Sectores Económicos">
<DetailItem label="Nombre" value={data.ospName} /> <DetailItem label="Sector Económico" value={data.ecoSector} />
<DetailItem label="RIF" value={data.ospRif} />
<DetailItem label="Tipo" value={data.ospType} />
<DetailItem
label="Actividad Productiva"
value={data.productiveActivity}
/>
<DetailItem
label="Estatus"
value={
<Badge
variant={
data.currentStatus === 'ACTIVA' ? 'default' : 'secondary'
}
>
{data.currentStatus}
</Badge>
}
/>
<DetailItem
label="Año de Constitución"
value={data.companyConstitutionYear}
/>
<DetailItem
label="Cant. Productores"
value={data.producerCount}
/>
<DetailItem label="Cant. Productos" value={data.productCount} />
<div className="col-span-1 md:col-span-2 lg:col-span-3">
<DetailItem <DetailItem
label="Descripción del Producto" label="Sector Productivo"
value={data.productDescription} value={data.productiveSector}
/> />
</div>
<DetailItem
label="Capacidad Instalada"
value={data.installedCapacity}
/>
<DetailItem
label="Capacidad Operativa"
value={data.operationalCapacity}
/>
<div className="col-span-1 md:col-span-2 lg:col-span-3">
<DetailItem <DetailItem
label="Requerimiento Financiero" label="Actividad Central"
value={data.financialRequirementDescription} value={data.centralProductiveActivity}
/> />
</div> <DetailItem
{data.paralysisReason && ( label="Actividad Principal"
<div className="col-span-1 md:col-span-2 lg:col-span-3"> value={data.mainProductiveActivity}
/>
<div className="col-span-full">
<DetailItem <DetailItem
label="Razones de paralización" label="Actividad Específica"
value={data.paralysisReason} value={data.productiveActivity}
/> />
</div> </div>
)} </Section>
</Section>
{/* 3. Ubicación */} {/* 3. Infraestructura y Ubicación */}
<Section title="3. Detalles de la ubicación"> <Section title="Infraestructura y Ubicación" icon={MapPin}>
<DetailItem <DetailItem
label="Código SITUR Comuna" label="Año Constitución"
value={data.siturCodeCommune} value={data.companyConstitutionYear}
/> />
<DetailItem <DetailItem
label="Consejo Comunal" label="Infraestructura (m²)"
value={data.communalCouncil} value={data.infrastructureMt2}
/> />
<DetailItem <DetailItem
label="Código SITUR Consejo Comunal" label="Tipo Estructura"
value={data.siturCodeCommunalCouncil} value={data.structureType}
/> />
<div className="col-span-1 md:col-span-2 lg:col-span-3">
<DetailItem label="Dirección OSP" value={data.ospAddress} />
</div>
</Section>
{/* 4. Responsable */} <DetailItem
<Section title="4. Datos del Responsable"> label="Posee Transporte"
<DetailItem value={<BooleanBadge value={data.hasTransport} />}
label="Nombre Completo" />
value={data.ospResponsibleFullname} <DetailItem
/> label="Espacio Abierto"
<DetailItem label="Cédula" value={data.ospResponsibleCedula} /> value={<BooleanBadge value={data.isOpenSpace} />}
<DetailItem label="RIF" value={data.ospResponsibleRif} /> />
<DetailItem label="Estado Civil" value={data.civilState} />
<DetailItem label="Teléfono" value={data.ospResponsiblePhone} />
<DetailItem label="Email" value={data.ospResponsibleEmail} />
<DetailItem label="Carga Familiar" value={data.familyBurden} />
<DetailItem
label="Número de Hijos"
value={data.numberOfChildren}
/>
</Section>
{/* 5. Observaciones */} <div className="col-span-full space-y-4 mt-2">
<Card> <div className="space-y-1">
<CardHeader className="py-3"> <p className="text-xs font-medium text-muted-foreground uppercase">
<CardTitle className="text-lg"> Dirección
5. Observaciones Generales
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-sm whitespace-pre-wrap">
{data.generalObservations || 'Sin observaciones'}
</p>
</CardContent>
</Card>
{/* 6. Registro fotográfico */}
<Card>
<CardHeader className="py-3">
<CardTitle className="text-lg">
6. Registro fotográfico
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
{[data.photo1, data.photo2, data.photo3].map(
(photo, idx) =>
photo && (
<div
key={idx}
className="relative aspect-video rounded-md overflow-hidden cursor-pointer hover:opacity-90 transition-opacity bg-muted"
onClick={() => setSelectedImage(photo)}
>
<img
src={`${process.env.NEXT_PUBLIC_API_URL}${photo}`}
alt={`Registro ${idx + 1}`}
className="object-cover w-full h-full"
/>
</div>
),
)}
{![data.photo1, data.photo2, data.photo3].some(Boolean) && (
<p className="text-sm text-muted-foreground">
No hay registro fotográfico
</p> </p>
<p className="text-sm font-medium">{data.ospAddress}</p>
</div>
{data.ospGoogleMapsLink && (
<Button
variant="outline"
size="sm"
asChild
className="gap-2"
>
<a
href={data.ospGoogleMapsLink}
target="_blank"
rel="noreferrer"
>
<MapPin className="h-4 w-4" />
Ver en Google Maps
<ExternalLink className="h-3 w-3" />
</a>
</Button>
)} )}
</div> </div>
</CardContent> </Section>
</Card>
</div>
<DialogFooter className="mt-6"> {/* 4. LISTAS DETALLADAS (Lo nuevo) */}
<Button onClick={onClose} variant="outline" className="w-32">
{/* PRODUCTOS */}
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-lg flex items-center gap-2">
<Package className="h-5 w-5" />
Productos y Mano de Obra
<Badge variant="secondary" className="ml-2">
{data.productList?.length || 0}
</Badge>
</CardTitle>
</CardHeader>
<CardContent className="grid gap-4">
{data.productList?.map((prod: any, idx: number) => (
<div
key={idx}
className="bg-muted/40 p-4 rounded-lg border text-sm"
>
<div className="flex justify-between items-start mb-2">
<h4 className="font-bold text-base text-primary">
{prod.productName}
</h4>
<Badge variant="outline">
Mano de obra:{' '}
{Number(prod.menCount || 0) +
Number(prod.womenCount || 0)}
</Badge>
</div>
<p className="text-muted-foreground mb-3">
{prod.description}
</p>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-3">
<DetailItem label="Diario" value={prod.dailyCount} />
<DetailItem label="Semanal" value={prod.weeklyCount} />
<DetailItem label="Mensual" value={prod.monthlyCount} />
<DetailItem
label="Hombres / Mujeres"
value={`${prod.menCount || 0} / ${prod.womenCount || 0}`}
/>
</div>
{/* Detalles de distribución si existen */}
{(prod.internalQuantity || prod.externalQuantity) && (
<>
<Separator className="my-2" />
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{prod.internalQuantity && (
<div>
<span className="text-xs font-bold text-muted-foreground block mb-1">
DISTRIBUCIÓN INTERNA
</span>
<p>Cant: {prod.internalQuantity}</p>
<p className="text-xs text-muted-foreground">
{prod.internalDescription}
</p>
</div>
)}
{prod.externalQuantity && (
<div>
<span className="text-xs font-bold text-muted-foreground block mb-1">
EXPORTACIÓN ({prod.externalCountry})
</span>
<p>Cant: {prod.externalQuantity}</p>
<p className="text-xs text-muted-foreground">
{prod.externalDescription}
</p>
</div>
)}
</div>
</>
)}
</div>
))}
{(!data.productList || data.productList.length === 0) && (
<p className="text-sm text-muted-foreground italic">
No hay productos registrados.
</p>
)}
</CardContent>
</Card>
{/* EQUIPAMIENTO Y PRODUCCIÓN */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-lg flex items-center gap-2">
<Wrench className="h-5 w-5" />
Equipamiento
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{data.equipmentList?.map((eq: any, idx: number) => (
<div
key={idx}
className="flex justify-between items-center p-2 rounded bg-muted/40 border"
>
<div>
<p className="font-medium">{eq.machine}</p>
<p className="text-xs text-muted-foreground">
{eq.specifications}
</p>
</div>
<Badge
variant="outline"
className="text-sm font-bold h-8 w-8 flex items-center justify-center rounded-full"
>
{eq.quantity}
</Badge>
</div>
))}
{(!data.equipmentList ||
data.equipmentList.length === 0) && (
<p className="text-sm text-muted-foreground italic">
No hay equipamiento registrado.
</p>
)}
</CardContent>
</Card>
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-lg flex items-center gap-2">
<Factory className="h-5 w-5" />
Materia Prima
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{data.productionList?.map((mat: any, idx: number) => (
<div
key={idx}
className="flex justify-between items-center p-2 rounded bg-muted/40 border"
>
<div>
<p className="font-medium">{mat.rawMaterial}</p>
<p className="text-xs text-muted-foreground">
{mat.supplyType}
</p>
</div>
<Badge variant="secondary">Cant: {mat.quantity}</Badge>
</div>
))}
{(!data.productionList ||
data.productionList.length === 0) && (
<p className="text-sm text-muted-foreground italic">
No hay materia prima registrada.
</p>
)}
</CardContent>
</Card>
</div>
{/* 5. Comuna y Responsable */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Section title="Datos de la Comuna">
<DetailItem label="Comuna" value={data.communeName} />
<DetailItem
label="Código SITUR"
value={data.siturCodeCommune}
/>
<DetailItem
label="Vocero"
value={data.communeSpokespersonName}
/>
<DetailItem
label="Teléfono"
value={data.communeSpokespersonPhone}
/>
<div className="col-span-full border-t pt-4 mt-2">
<DetailItem
label="Consejo Comunal"
value={data.communalCouncil}
/>
<DetailItem
label="Vocero C.C."
value={data.communalCouncilSpokespersonName}
/>
</div>
</Section>
<Section title="Responsable OSP">
<DetailItem
label="Nombre"
value={data.ospResponsibleFullname}
/>
<DetailItem
label="Cédula"
value={data.ospResponsibleCedula}
/>
<DetailItem
label="Teléfono"
value={data.ospResponsiblePhone}
/>
<DetailItem label="Email" value={data.ospResponsibleEmail} />
<DetailItem
label="Carga Familiar"
value={data.familyBurden}
/>
<DetailItem label="Hijos" value={data.numberOfChildren} />
</Section>
</div>
{/* 6. Observaciones */}
{(data.generalObservations || data.paralysisReason) && (
<Card>
<CardHeader>
<CardTitle>Observaciones</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{data.generalObservations && (
<div>
<p className="text-xs font-bold text-muted-foreground uppercase mb-1">
Generales
</p>
<p className="text-sm">{data.generalObservations}</p>
</div>
)}
{data.paralysisReason && (
<div className="p-3 bg-red-50 dark:bg-red-900/20 rounded-md border border-red-200 dark:border-red-900">
<p className="text-xs font-bold text-red-600 dark:text-red-400 uppercase mb-1">
Motivo Paralización
</p>
<p className="text-sm">{data.paralysisReason}</p>
</div>
)}
</CardContent>
</Card>
)}
{/* 7. Fotos */}
<Section title="Registro Fotográfico">
{[data.photo1, data.photo2, data.photo3].some(Boolean) ? (
<div className="col-span-full grid grid-cols-1 sm:grid-cols-3 gap-4">
{[data.photo1, data.photo2, data.photo3].map(
(photo, idx) =>
photo && (
<div
key={idx}
className="relative aspect-video rounded-lg overflow-hidden cursor-zoom-in border hover:shadow-lg transition-all"
onClick={() => setSelectedImage(photo)}
>
<img
src={`${process.env.NEXT_PUBLIC_API_URL}${photo}`}
alt={`Evidencia ${idx + 1}`}
className="object-cover w-full h-full"
/>
</div>
),
)}
</div>
) : (
<p className="text-sm text-muted-foreground col-span-full">
No hay imágenes cargadas.
</p>
)}
</Section>
</div>
</ScrollArea>
<DialogFooter className="px-6 py-4 border-t bg-muted/20">
<Button
onClick={onClose}
variant="outline"
className="w-full sm:w-auto"
>
Cerrar Cerrar
</Button> </Button>
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
{/* Lightbox */} {/* Lightbox para imágenes */}
<Dialog <Dialog
open={!!selectedImage} open={!!selectedImage}
onOpenChange={() => setSelectedImage(null)} onOpenChange={() => setSelectedImage(null)}
> >
<DialogContent className="max-w-[95vw] max-h-[95vh] p-0 overflow-hidden bg-black/90 border-none [&>button:last-child]:hidden"> <DialogContent className="max-w-[95vw] max-h-[95vh] p-0 overflow-hidden bg-black/95 border-none">
<DialogHeader className="sr-only"> <DialogHeader className="sr-only">
<DialogTitle>Vista ampliada de la imagen</DialogTitle> <DialogTitle>Imagen Ampliada</DialogTitle>
<DialogDescription>
Imagen ampliada del registro fotográfico de la organización.
</DialogDescription>
</DialogHeader> </DialogHeader>
<div className="relative w-full h-full flex items-center justify-center p-4"> <DialogDescription></DialogDescription>
<div className="relative w-full h-full flex items-center justify-center p-2">
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
className="absolute top-2 right-2 text-white hover:bg-white/20 z-10" className="absolute top-4 right-4 text-white hover:bg-white/20 rounded-full z-50"
onClick={() => setSelectedImage(null)} onClick={() => setSelectedImage(null)}
> >
<X className="h-6 w-6" /> <X className="h-6 w-6" />
@@ -273,8 +494,8 @@ export function TrainingViewModal({
{selectedImage && ( {selectedImage && (
<img <img
src={`${process.env.NEXT_PUBLIC_API_URL}${selectedImage}`} src={`${process.env.NEXT_PUBLIC_API_URL}${selectedImage}`}
alt="Expanded view" alt="Vista ampliada"
className="max-w-full max-h-[90vh] object-contain" className="max-w-full max-h-[90vh] object-contain rounded-md"
/> />
)} )}
</div> </div>

View File

@@ -0,0 +1,169 @@
export const SECTOR_ECONOMICO = {
PRIMARIO: 'PRIMARIO',
SECUNDARIO: 'SECUNDARIO',
TERCIARIO: 'TERCIARIO',
} as const;
export const SECTOR_PRODUCTIVO = {
AGRICOLA: 'AGRÍCOLA',
MANUFACTURA: 'MANUFACTURA',
SERVICIOS: 'SERVICIOS',
TURISMO: 'TURISMO',
COMERCIO: 'COMERCIO',
} as const;
export const ACTIVIDAD_CENTRAL = {
PRODUCCION_VEGETAL: 'PRODUCCIÓN VEGETAL',
PRODUCCION_ANIMAL: 'PRODUCCIÓN ANIMAL',
PRODUCCION_VEGETAL_ANIMAL: 'PRODUCCIÓN VEGETAL Y ANIMAL',
INDUSTRIAL: 'INDUSTRIAL',
SERVICIOS: 'SERVICIOS',
TURISMO: 'TURISMO',
COMERCIO: 'COMERCIO',
} as const;
export const ACTIVIDAD_PRINCIPAL = {
AGRICULTURA: 'AGRICULTURA',
CRIA: 'CRIA',
PATIOS_PRODUCTIVOS: 'PATIOS PRODUCTIVOS O CONUCOS',
TRANSFORMACION_MATERIA: 'TRANSFORMACION DE LA MATERIA PRIMA',
TEXTIL: 'TALLER DE COFECCION TEXTIL',
CONSTRUCCION: 'CONSTRUCION',
BIENES_SERVICIOS: 'OFRECER PRODUCTOS DE BIENES Y SERVICIOS',
VISITAS_GUIADAS: 'VISITAS GUIADAS',
ALOJAMIENTO: 'ALOJAMIENTO',
TURISMO: 'TURISMO',
COMERCIO: 'COMERCIO',
} as const;
export const SECTOR_ECONOMICO_OPTIONS = [
SECTOR_ECONOMICO.PRIMARIO,
SECTOR_ECONOMICO.SECUNDARIO,
SECTOR_ECONOMICO.TERCIARIO,
];
// Map: Sector Economico -> Productive Sectors
export const SECTOR_PRODUCTIVO_MAP: Record<string, string[]> = {
[SECTOR_ECONOMICO.PRIMARIO]: [SECTOR_PRODUCTIVO.AGRICOLA],
[SECTOR_ECONOMICO.SECUNDARIO]: [SECTOR_PRODUCTIVO.MANUFACTURA],
[SECTOR_ECONOMICO.TERCIARIO]: [
SECTOR_PRODUCTIVO.SERVICIOS,
SECTOR_PRODUCTIVO.TURISMO,
SECTOR_PRODUCTIVO.COMERCIO,
],
};
// Map: Productive Sector -> Central Productive Activity
export const ACTIVIDAD_CENTRAL_MAP: Record<string, string[]> = {
[SECTOR_PRODUCTIVO.AGRICOLA]: [
ACTIVIDAD_CENTRAL.PRODUCCION_VEGETAL,
ACTIVIDAD_CENTRAL.PRODUCCION_ANIMAL,
ACTIVIDAD_CENTRAL.PRODUCCION_VEGETAL_ANIMAL,
],
[SECTOR_PRODUCTIVO.MANUFACTURA]: [ACTIVIDAD_CENTRAL.INDUSTRIAL],
[SECTOR_PRODUCTIVO.SERVICIOS]: [ACTIVIDAD_CENTRAL.SERVICIOS],
[SECTOR_PRODUCTIVO.TURISMO]: [ACTIVIDAD_CENTRAL.TURISMO],
[SECTOR_PRODUCTIVO.COMERCIO]: [ACTIVIDAD_CENTRAL.COMERCIO],
};
// Map: Central Productive Activity -> Main Productive Activity
export const ACTIVIDAD_PRINCIPAL_MAP: Record<string, string[]> = {
[ACTIVIDAD_CENTRAL.PRODUCCION_VEGETAL]: [ACTIVIDAD_PRINCIPAL.AGRICULTURA],
[ACTIVIDAD_CENTRAL.PRODUCCION_ANIMAL]: [ACTIVIDAD_PRINCIPAL.CRIA],
[ACTIVIDAD_CENTRAL.PRODUCCION_VEGETAL_ANIMAL]: [
ACTIVIDAD_PRINCIPAL.PATIOS_PRODUCTIVOS,
],
[ACTIVIDAD_CENTRAL.INDUSTRIAL]: [
ACTIVIDAD_PRINCIPAL.TRANSFORMACION_MATERIA,
ACTIVIDAD_PRINCIPAL.TEXTIL,
ACTIVIDAD_PRINCIPAL.CONSTRUCCION,
],
[ACTIVIDAD_CENTRAL.SERVICIOS]: [ACTIVIDAD_PRINCIPAL.BIENES_SERVICIOS],
[ACTIVIDAD_CENTRAL.TURISMO]: [
ACTIVIDAD_PRINCIPAL.VISITAS_GUIADAS,
ACTIVIDAD_PRINCIPAL.ALOJAMIENTO,
ACTIVIDAD_PRINCIPAL.TURISMO,
],
[ACTIVIDAD_CENTRAL.COMERCIO]: [ACTIVIDAD_PRINCIPAL.COMERCIO],
};
// Map: Main Productive Activity -> Productive Activity (The long list)
export const ACTIVIDAD_PRODUCTIVA_MAP: Record<string, string[]> = {
[ACTIVIDAD_PRINCIPAL.AGRICULTURA]: [
'SIEMBRA DE MAIZ',
'SIEMBRA DE AJI',
'SIEMBRA DE CAFÉ',
'SIEMBRA DE PLATANO',
'SIEMBRA DE CAMBUR',
'SIEMBRA DE AGUACATE',
'SIEMBRA DE FRUTAS',
'SIEMBRA DE HORTALIZAS',
'SIEMBRA DE TOMATE',
'SIEMBRA DE CACAO',
'SIEMBRA DE PIMENTON',
'SIEMBRA DE YUCA',
'SIEMBRA DE CAÑA DE AZUCAR',
'SIEMBRA DE GRANOS (CARAOTAS, FRIJOLES)',
'SIEMBRA DE ARROZ',
'SIEMBRA DE CEREALES (CEBADA, LINAZA, SOYA)',
'ELABORACION DE BIO-INSUMO (ABONO ORGANICO)',
],
[ACTIVIDAD_PRINCIPAL.CRIA]: [
'BOVINO',
'PORCINO',
'CAPRINO',
'CUNICULTURA',
'AVICOLA',
'PISCICULA',
],
[ACTIVIDAD_PRINCIPAL.PATIOS_PRODUCTIVOS]: ['SIEMBRA Y CRIA'],
[ACTIVIDAD_PRINCIPAL.TRANSFORMACION_MATERIA]: [
'ELABORACION DE PRODUCTOS QUIMICOS (LIMPIEZA E HIGIENE PERSONAL)',
'PANADERIAS',
'RESPOSTERIA',
'ELABORACION DE HARINAS PRECOCIDA',
'PLANTA ABA (ELABORACION DE ALIMENTOS BALANCEADOS PARA ANIMALES)',
'ELABORACION DE PRODUCTOS DERIVADO DE LA LECHE (VACA, CABRA, BUFFALA)',
'EMPAQUETADORAS DE GRANOS Y POLVOS',
'ELABORACION DE ACEITE COMESTIBLE',
'FABRICA DE HIELO',
'ELABORACION DE PAPELON',
'ARTESANIAS',
],
[ACTIVIDAD_PRINCIPAL.TEXTIL]: [
'ELABORACION DE UNIFORME ESCOLARES Y PRENDA DE VESTIR',
'ELABORACION DE PRENDAS INTIMAS',
'ELABORACION DE LENCERIA',
'SUBLIMACION DE TEJIDOS',
'ELABORACION DE CALZADOS',
],
[ACTIVIDAD_PRINCIPAL.CONSTRUCCION]: [
'BLOQUERAS',
'PLANTA PREMEZCLADORA DE CEMENTO',
'CARPINTERIAS',
'HERRERIAS',
],
[ACTIVIDAD_PRINCIPAL.BIENES_SERVICIOS]: [
'MERCADOS COMUNALES',
'CENTROS DE ACOPIOS Y DISTRIBUCION',
'UNIDAD DE SUMINISTRO',
'MATADERO (SALA DE MATANZA DE ANIMALES)',
'PELUQUERIA',
'BARBERIA',
'AGENCIAS DE FESTEJOS',
'LAVANDERIAS',
'REPARACION DE CALZADOS',
'TALLER DE MECANICA',
'TRANSPORTES',
],
[ACTIVIDAD_PRINCIPAL.VISITAS_GUIADAS]: ['RUTAS TURISTICAS'],
[ACTIVIDAD_PRINCIPAL.ALOJAMIENTO]: ['POSADAS', 'HOTELES'],
[ACTIVIDAD_PRINCIPAL.TURISMO]: ['AGENCIAS DE VIAJES'],
[ACTIVIDAD_PRINCIPAL.COMERCIO]: [
'VENTA DE VIVERES',
'VENTAS DE PRENDAS DE VESTIR',
'VENTA DE PRODUCTOS QUIMICOS Y DERIVADOS',
'BODEGAS COMUNALES',
'FRIGORIFICOS Y CARNICOS',
],
};

View File

@@ -1,23 +1,27 @@
import { z } from 'zod'; import { z } from 'zod';
export const statisticsItemSchema = z.object({ export const statisticsItemSchema = z.object({
name: z.string(), name: z
value: z.number(), .string()
.nullable()
.transform((val) => val || 'Sin Información'),
value: z.number(),
}); });
export const trainingStatisticsSchema = z.object({ export const trainingStatisticsSchema = z.object({
totalOsps: z.number(), totalOsps: z.number(),
totalProducers: z.number(), totalProducers: z.number(),
statusDistribution: z.array(statisticsItemSchema), totalProducts: z.number(),
activityDistribution: z.array(statisticsItemSchema), statusDistribution: z.array(statisticsItemSchema),
typeDistribution: z.array(statisticsItemSchema), activityDistribution: z.array(statisticsItemSchema),
stateDistribution: z.array(statisticsItemSchema), typeDistribution: z.array(statisticsItemSchema),
yearDistribution: z.array(statisticsItemSchema), stateDistribution: z.array(statisticsItemSchema),
yearDistribution: z.array(statisticsItemSchema),
}); });
export type TrainingStatisticsData = z.infer<typeof trainingStatisticsSchema>; export type TrainingStatisticsData = z.infer<typeof trainingStatisticsSchema>;
export const trainingStatisticsResponseSchema = z.object({ export const trainingStatisticsResponseSchema = z.object({
message: z.string(), message: z.string(),
data: trainingStatisticsSchema, data: trainingStatisticsSchema,
}); });

View File

@@ -1,154 +1,166 @@
import { z } from 'zod'; import { z } from 'zod';
// 1. Definimos el esquema de un item individual de la lista de productos
// Basado en los campos que usaste en ProductActivityList
const productItemSchema = z.object({
productName: z.string(),
description: z.string().optional(),
dailyCount: z.coerce.string().or(z.number()).optional(), // Aceptamos string o number por los inputs
weeklyCount: z.coerce.string().or(z.number()).optional(),
monthlyCount: z.coerce.string().or(z.number()).optional(),
// Distribución Interna
internalState: z.number().optional(),
internalMunicipality: z.number().optional(),
internalParish: z.number().optional(),
internalDescription: z.string().optional(),
internalQuantity: z.coerce.string().or(z.number()).optional(),
// Distribución Externa
externalCountry: z.string().optional(),
externalState: z.number().optional(),
externalMunicipality: z.number().optional(),
externalParish: z.number().optional(),
externalCity: z.string().optional(),
externalDescription: z.string().optional(),
externalQuantity: z.coerce.string().or(z.number()).optional(),
// Mano de obra
womenCount: z.coerce.string().or(z.number()).optional(),
menCount: z.coerce.string().or(z.number()).optional(),
});
const productionItemSchema = z.object({
rawMaterial: z.string(),
supplyType: z.string().optional(),
quantity: z.coerce.string().or(z.number()).optional(), // Aceptamos string o number por los inputs
});
const equipmentItemSchema = z.object({
machine: z.string(),
specifications: z.string().optional(),
quantity: z.coerce.string().or(z.number()).optional(), // Aceptamos string o number por los inputs
});
export const trainingSchema = z.object({ export const trainingSchema = z.object({
//Datos de la visita
id: z.number().optional(), id: z.number().optional(),
firstname: z.string().min(1, { message: 'Nombre es requerido' }), firstname: z.string().min(1, { message: 'Nombre es requerido' }),
lastname: z.string().min(1, { message: 'Apellido es requerido' }), lastname: z.string().min(1, { message: 'Apellido es requerido' }),
coorPhone: z.string().optional().nullable(),
visitDate: z visitDate: z
.string() .string()
.min(1, { message: 'Fecha y hora de visita es requerida' }), .min(1, { message: 'Fecha y hora de visita es requerida' }),
//Datos de la organización socioproductiva (OSP)
ospType: z.string().min(1, { message: 'Tipo de OSP es requerido' }),
ecoSector: z.string().optional().or(z.literal('')),
productiveSector: z.string().optional().or(z.literal('')),
centralProductiveActivity: z.string().optional().or(z.literal('')),
mainProductiveActivity: z.string().optional().or(z.literal('')),
productiveActivity: z productiveActivity: z
.string() .string()
.min(1, { message: 'Actividad productiva es requerida' }), .min(1, { message: 'Actividad productiva es requerida' }),
// financialRequirementDescription: z ospRif: z.string().optional().or(z.literal('')),
// .string()
// .min(1, { message: 'Descripción es requerida' }),
siturCodeCommune: z
.string()
.min(1, { message: 'Código SITUR Comuna es requerido' }),
communeName: z
.string()
.min(1, { message: 'Nombre de la Comuna es requerido' }),
communeRif: z
.string()
.min(1, { message: 'RIF de la Comuna es requerido' }),
communeSpokespersonName: z
.string()
.min(1, { message: 'Nombre del Vocero de la Comuna es requerido' }),
communeSpokespersonCedula: z
.string()
.min(1, { message: 'Cédula del Vocero de la Comuna es requerida' }),
communeSpokespersonRif: z
.string()
.min(1, { message: 'RIF del Vocero de la Comuna es requerido' }),
communeSpokespersonPhone: z
.string()
.min(1, { message: 'Teléfono del Vocero de la Comuna es requerido' }),
communeEmail: z.string().email({ message: 'Correo electrónico de la Comuna inválido' }),
communalCouncil: z
.string()
.min(1, { message: 'Consejo Comunal es requerido' }),
siturCodeCommunalCouncil: z
.string()
.min(1, { message: 'Código SITUR Consejo Comunal es requerido' }),
communalCouncilRif: z
.string()
.min(1, { message: 'RIF del Consejo Comunal es requerido' }),
communalCouncilSpokespersonName: z
.string()
.min(1, { message: 'Nombre del Vocero del Consejo Comunal es requerido' }),
communalCouncilSpokespersonCedula: z
.string()
.min(1, { message: 'Cédula del Vocero del Consejo Comunal es requerida' }),
communalCouncilSpokespersonRif: z
.string()
.min(1, { message: 'RIF del Vocero del Consejo Comunal es requerido' }),
communalCouncilSpokespersonPhone: z
.string()
.min(1, { message: 'Teléfono del Vocero del Consejo Comunal es requerido' }),
communalCouncilEmail: z
.string()
.email({ message: 'Correo electrónico del Consejo Comunal inválido' }),
ospGoogleMapsLink: z
.string()
.min(1, { message: 'Enlace de Google Maps es requerido' }),
ospName: z.string().min(1, { message: 'Nombre de la OSP es requerido' }), ospName: z.string().min(1, { message: 'Nombre de la OSP es requerido' }),
ospAddress: z companyConstitutionYear: z.coerce
.string() .number()
.min(1, { message: 'Dirección de la OSP es requerida' }), .min(1900, { message: 'Año inválido' }),
ospRif: z.string().min(1, { message: 'RIF de la OSP es requerido' }),
ospType: z.string().min(1, { message: 'Tipo de OSP es requerido' }),
currentStatus: z currentStatus: z
.string() .string()
.min(1, { message: 'Estatus actual es requerido' }) .min(1, { message: 'Estatus actual es requerido' })
.default('ACTIVA'), .default('ACTIVA'),
companyConstitutionYear: z.coerce infrastructureMt2: z.string().optional().or(z.literal('')),
.number() hasTransport: z
.min(1900, { message: 'Año inválido' }), .preprocess((val) => val === 'true' || val === true, z.boolean())
producerCount: z.coerce .optional(),
.number() structureType: z.string().optional().or(z.literal('')),
.min(0, { message: 'Cantidad de productores requerida' }), isOpenSpace: z
// productCount: z.coerce .preprocess((val) => val === 'true' || val === true, z.boolean())
// .number() .optional(),
// .min(0, { message: 'Cantidad de productos requerida' }) paralysisReason: z.string().optional().default(''),
// .optional(),
productDescription: z //Datos del Equipamiento
equipmentList: z.array(equipmentItemSchema).optional().default([]),
//Datos de Producción
productionList: z.array(productionItemSchema).optional().default([]),
// Datos de Actividad Productiva
productList: z.array(productItemSchema).optional().default([]),
//Detalles de la ubicación
ospAddress: z
.string() .string()
.min(1, { message: 'Descripción del producto es requerida' }), .min(1, { message: 'Dirección de la OSP es requerida' }),
prodDescriptionInternal: z ospGoogleMapsLink: z.string().optional().or(z.literal('')),
communeName: z.string().optional().or(z.literal('')),
siturCodeCommune: z.string().optional().or(z.literal('')),
communeRif: z.string().optional().or(z.literal('')),
communeSpokespersonName: z.string().optional().or(z.literal('')),
communeSpokespersonCedula: z.string().optional().or(z.literal('')),
communeSpokespersonRif: z.string().optional().or(z.literal('')),
communeSpokespersonPhone: z.string().optional().or(z.literal('')),
communeEmail: z
.string() .string()
.min(1, { message: 'Descripción del producto es requerida' }), .email({ message: 'Correo electrónico de la Comuna inválido' })
installedCapacity: z .optional()
.or(z.literal('')),
communalCouncil: z
.string() .string()
.min(1, { message: 'Capacidad instalada es requerida' }), .min(1, { message: 'Consejo Comunal es requerido' }),
operationalCapacity: z siturCodeCommunalCouncil: z.string().optional().or(z.literal('')),
communalCouncilRif: z.string().optional().or(z.literal('')),
communalCouncilSpokespersonName: z.string().optional().or(z.literal('')),
communalCouncilSpokespersonCedula: z.string().optional().or(z.literal('')),
communalCouncilSpokespersonRif: z.string().optional().or(z.literal('')),
communalCouncilSpokespersonPhone: z.string().optional().or(z.literal('')),
communalCouncilEmail: z
.string() .string()
.min(1, { message: 'Capacidad operativa es requerida' }), .email({ message: 'Correo electrónico del Consejo Comunal inválido' })
ospResponsibleFullname: z .optional()
.string() .or(z.literal('')),
.min(1, { message: 'Nombre del responsable es requerido' }),
//Datos del Responsable OSP
ospResponsibleCedula: z ospResponsibleCedula: z
.string() .string()
.min(1, { message: 'Cédula del responsable es requerida' }), .min(1, { message: 'Cédula del responsable es requerida' }),
ospResponsibleFullname: z
.string()
.min(1, { message: 'Nombre del responsable es requerido' }),
ospResponsibleRif: z ospResponsibleRif: z
.string() .string()
.min(1, { message: 'RIF del responsable es requerido' }), .min(1, { message: 'RIF del responsable es requerido' }),
civilState: z.string().min(1, { message: 'Estado civil es requerido' }),
ospResponsiblePhone: z ospResponsiblePhone: z
.string() .string()
.min(1, { message: 'Teléfono del responsable es requerido' }), .min(1, { message: 'Teléfono del responsable es requerido' }),
civilState: z.string().min(1, { message: 'Estado civil es requerido' }), ospResponsibleEmail: z
.string()
.email({ message: 'Correo electrónico inválido' }),
familyBurden: z.coerce familyBurden: z.coerce
.number() .number()
.min(0, { message: 'Carga familiar requerida' }), .min(0, { message: 'Carga familiar requerida' }),
numberOfChildren: z.coerce numberOfChildren: z.coerce
.number() .number()
.min(0, { message: 'Número de hijos requerido' }), .min(0, { message: 'Número de hijos requerido' }),
ospResponsibleEmail: z
.string() //Datos adicionales
.email({ message: 'Correo electrónico inválido' }),
generalObservations: z.string().optional().default(''), generalObservations: z.string().optional().default(''),
photo1: z.string().optional().nullable(),
photo2: z.string().optional().nullable(), //IMAGENES
photo3: z.string().optional().nullable(),
files: z.any().optional(), files: z.any().optional(),
paralysisReason: z.string().optional().default(''),
//no se envia la backend al crear ni editar el formulario
state: z.number().optional().nullable(), state: z.number().optional().nullable(),
municipality: z.number().optional().nullable(), municipality: z.number().optional().nullable(),
parish: z.number().optional().nullable(), parish: z.number().optional().nullable(),
coorState: z.number().optional().nullable(), coorState: z.number().optional().nullable(),
coorMunicipality: z.number().optional().nullable(), coorMunicipality: z.number().optional().nullable(),
coorParish: z.number().optional().nullable(), coorParish: z.number().optional().nullable(),
coorPhone: z.string().optional().nullable(), photo1: z.string().optional().nullable(),
ecoSector: z.string().min(1, { message: 'Sector económico es requerido' }), photo2: z.string().optional().nullable(),
productiveSector: z.string().min(1, { message: 'Sector productivo es requerido' }), photo3: z.string().optional().nullable(),
centralProductiveActivity: z.string().min(1, { message: 'Actividad productiva central es requerida' }),
mainProductiveActivity: z.string().min(1, { message: 'Actividad productiva principal es requerida' }),
typesOfEquipment: z.string().min(1, { message: 'Tipo de equipo es requerido' }),
equipmentCount: z.coerce.number().min(0, { message: 'Cantidad de equipo requerida' }),
equipmentDescription: z.string().min(1, { message: 'Descripción del equipo es requerida' }),
rawMaterial: z.string().min(1, { message: 'Material bruto es requerido' }),
materialType: z.string().min(1, { message: 'Tipo de material es requerido' }),
rawMaterialCount: z.coerce.number().min(0, { message: 'Cantidad de material bruto requerida' }),
productCountDaily: z.coerce.number().min(0, { message: 'Cantidad diaria de productos requerida' }),
productCountWeekly: z.coerce.number().min(0, { message: 'Cantidad semanal de productos requerida' }),
productCountMonthly: z.coerce.number().min(0, { message: 'Cantidad mensual de productos requerida' }),
internalCount: z.coerce.number().min(0, { message: 'Cantidad interna requerida' }),
externalCount: z.coerce.number().min(0, { message: 'Cantidad externa requerida' }),
prodDescriptionExternal: z.string().min(1, { message: 'Descripción del producto es requerida' }),
country: z.string().min(1, { message: 'País es requerido' }),
city: z.string().min(1, { message: 'Ciudad es requerida' }),
menCount: z.coerce.number().min(0, { message: 'Cantidad de hombres requerida' }),
womenCount: z.coerce.number().min(0, { message: 'Cantidad de mujeres requerida' }),
}); });
export type TrainingSchema = z.infer<typeof trainingSchema>; export type TrainingSchema = z.infer<typeof trainingSchema>;

View File

@@ -1,23 +1,22 @@
'use server'; 'use server';
import { safeFetchApi } from '@/lib/fetch.api'; import { safeFetchApi } from '@/lib/fetch.api';
import { import {
surveysApiResponseSchema,
CreateUser, CreateUser,
surveysApiResponseSchema,
UpdateUser,
UsersMutate, UsersMutate,
UpdateUser
} from '../schemas/users'; } from '../schemas/users';
import { auth } from '@/lib/auth'; import { auth } from '@/lib/auth';
export const getProfileAction = async () => { export const getProfileAction = async () => {
const session = await auth() const session = await auth();
const id = session?.user?.id const id = session?.user?.id;
const [error, response] = await safeFetchApi( const [error, response] = await safeFetchApi(
UsersMutate, UsersMutate,
`/users/${id}`, `/users/${id}`,
'GET' 'GET',
); );
if (error) throw new Error(error.message); if (error) throw new Error(error.message);
return response; return response;
@@ -33,7 +32,6 @@ export const updateProfileAction = async (payload: UpdateUser) => {
payloadWithoutId, payloadWithoutId,
); );
console.log(payload);
if (error) { if (error) {
if (error.message === 'Email already exists') { if (error.message === 'Email already exists') {
throw new Error('Ese correo ya está en uso'); throw new Error('Ese correo ya está en uso');
@@ -51,7 +49,6 @@ export const getUsersAction = async (params: {
sortBy?: string; sortBy?: string;
sortOrder?: 'asc' | 'desc'; sortOrder?: 'asc' | 'desc';
}) => { }) => {
const searchParams = new URLSearchParams({ const searchParams = new URLSearchParams({
page: (params.page || 1).toString(), page: (params.page || 1).toString(),
limit: (params.limit || 10).toString(), limit: (params.limit || 10).toString(),
@@ -83,7 +80,7 @@ export const getUsersAction = async (params: {
previousPage: null, previousPage: null,
}, },
}; };
} };
export const createUserAction = async (payload: CreateUser) => { export const createUserAction = async (payload: CreateUser) => {
const { id, confirmPassword, ...payloadWithoutId } = payload; const { id, confirmPassword, ...payloadWithoutId } = payload;
@@ -130,19 +127,14 @@ export const updateUserAction = async (payload: UpdateUser) => {
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} }
} };
export const deleteUserAction = async (id: Number) => { export const deleteUserAction = async (id: Number) => {
const [error] = await safeFetchApi( const [error] = await safeFetchApi(UsersMutate, `/users/${id}`, 'DELETE');
UsersMutate,
`/users/${id}`,
'DELETE'
)
console.log(error); console.log(error);
// if (error) throw new Error(error.message || 'Error al eliminar el usuario') // if (error) throw new Error(error.message || 'Error al eliminar el usuario')
return true; return true;
} };