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 }) => {
@ApiProperty() if (typeof value === 'string') {
@IsString() try {
productDescription: string; return JSON.parse(value);
} catch {
@ApiProperty() return [];
@IsString() }
installedCapacity: string; }
return value;
@ApiProperty() })
@IsString() equipmentList?: any[];
operationalCapacity: string;
@ApiProperty()
@ApiProperty() @IsOptional()
@IsString() @IsArray()
ospResponsibleFullname: string; @Transform(({ value }) => {
if (typeof value === 'string') {
@ApiProperty() try {
@IsString() return JSON.parse(value);
ospResponsibleCedula: string; } catch {
return [];
@ApiProperty() }
@IsString() }
ospResponsibleRif: string; return value;
})
@ApiProperty() productionList?: any[];
@IsString()
ospResponsiblePhone: string; @ApiProperty()
@IsOptional()
@ApiProperty() @IsArray()
@IsString() @Transform(({ value }) => {
ospResponsibleEmail: string; if (typeof value === 'string') {
try {
@ApiProperty() return JSON.parse(value);
@IsString() } catch {
civilState: string; return [];
}
@ApiProperty() }
@IsInt() return value;
familyBurden: number; })
productList?: any[];
@ApiProperty()
@IsInt()
numberOfChildren: number;
@ApiProperty()
@IsString()
generalObservations: string;
@ApiProperty()
@IsString()
@IsOptional()
photo1?: string;
@ApiProperty()
@IsString()
@IsOptional()
photo2?: string;
@ApiProperty()
@IsString()
@IsOptional()
photo3?: string;
@ApiProperty()
@IsString()
@IsOptional()
paralysisReason: string;
// nuevos campos coordinacion
@ApiProperty()
@IsInt()
@IsOptional()
coorState?: number;
@ApiProperty()
@IsInt()
@IsOptional()
coorMunicipality?: number;
@ApiProperty()
@IsInt()
@IsOptional()
coorParish?: number;
@ApiProperty()
@IsString()
@IsOptional()
coorPhone?: string;
// sectores
@ApiProperty()
@IsString()
ecoSector: string;
@ApiProperty()
@IsString()
productiveSector: string;
@ApiProperty()
@IsString()
centralProductiveActivity: string;
@ApiProperty()
@IsString()
mainProductiveActivity: string;
// equipamiento
@ApiProperty()
@IsString()
typesOfEquipment: string;
@ApiProperty()
@IsInt()
equipmentCount: number;
@ApiProperty()
@IsString()
equipmentDescription: string;
// materia prima
@ApiProperty()
@IsString()
rawMaterial: string;
@ApiProperty()
@IsString()
materialType: string;
@ApiProperty()
@IsInt()
rawMaterialCount: number;
// conteo de productos
@ApiProperty()
@IsInt()
productCountDaily: number;
@ApiProperty()
@IsInt()
productCountWeekly: number;
@ApiProperty()
@IsInt()
productCountMonthly: number;
@ApiProperty()
@IsString()
prodDescriptionInternal: string;
@ApiProperty()
@IsInt()
internalCount: number;
@ApiProperty()
@IsInt()
externalCount: number;
@ApiProperty()
@IsString()
prodDescriptionExternal: string;
@ApiProperty()
@IsString()
country: string;
@ApiProperty()
@IsString()
city: string;
@ApiProperty()
@IsInt()
menCount: number;
@ApiProperty()
@IsInt()
womenCount: number;
} }

View File

@@ -74,83 +74,106 @@ 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
const [
totalOspsResult,
totalProducersResult,
totalProductsResult, // Nuevo: Calculado desde el JSON
statusDistribution,
activityDistribution,
typeDistribution,
stateDistribution,
yearDistribution,
] = await Promise.all([
// 1. Total OSPs
this.drizzle
.select({ count: sql<number>`count(*)` }) .select({ count: sql<number>`count(*)` })
.from(trainingSurveys) .from(trainingSurveys)
.where(whereCondition); .where(whereCondition),
const totalOsps = Number(totalOspsResult[0].count);
const totalProducersResult = await this.drizzle // 2. Total Productores (Columna plana que mantuviste)
.select({ sum: sql<number>`sum(${trainingSurveys.producerCount})` }) this.drizzle
.select({
sum: sql<number>`
SUM(
(
SELECT SUM(
COALESCE((item->>'menCount')::int, 0) +
COALESCE((item->>'womenCount')::int, 0)
)
FROM jsonb_array_elements(${trainingSurveys.productList}) as item
)
)
`,
})
.from(trainingSurveys) .from(trainingSurveys)
.where(whereCondition); .where(whereCondition),
const totalProducers = Number(totalProducersResult[0].sum || 0);
const statusDistribution = await this.drizzle // 3. NUEVO: Total Productos (Contamos el largo del array JSON productList)
this.drizzle
.select({
sum: sql<number>`sum(jsonb_array_length(${trainingSurveys.productList}))`,
})
.from(trainingSurveys)
.where(whereCondition),
// 4. Distribución por Estatus
this.drizzle
.select({ .select({
name: trainingSurveys.currentStatus, name: trainingSurveys.currentStatus,
value: sql<number>`count(*)`, value: sql<number>`count(*)`,
}) })
.from(trainingSurveys) .from(trainingSurveys)
.where(whereCondition) .where(whereCondition)
.groupBy(trainingSurveys.currentStatus); .groupBy(trainingSurveys.currentStatus),
const activityDistribution = await this.drizzle // 5. Distribución por Actividad
this.drizzle
.select({ .select({
name: trainingSurveys.productiveActivity, name: trainingSurveys.productiveActivity,
value: sql<number>`count(*)`, value: sql<number>`count(*)`,
}) })
.from(trainingSurveys) .from(trainingSurveys)
.where(whereCondition) .where(whereCondition)
.groupBy(trainingSurveys.productiveActivity); .groupBy(trainingSurveys.productiveActivity),
const typeDistribution = await this.drizzle // 6. Distribución por Tipo
this.drizzle
.select({ .select({
name: trainingSurveys.ospType, name: trainingSurveys.ospType,
value: sql<number>`count(*)`, value: sql<number>`count(*)`,
}) })
.from(trainingSurveys) .from(trainingSurveys)
.where(whereCondition) .where(whereCondition)
.groupBy(trainingSurveys.ospType); .groupBy(trainingSurveys.ospType),
// New Aggregations // 7. Distribución por Estado (CORREGIDO con COALESCE)
const stateDistribution = await this.drizzle this.drizzle
.select({ .select({
name: states.name, // Si states.name es NULL, devuelve 'Sin Asignar'
name: sql<string>`COALESCE(${states.name}, 'Sin Asignar')`,
value: sql<number>`count(${trainingSurveys.id})`, value: sql<number>`count(${trainingSurveys.id})`,
}) })
.from(trainingSurveys) .from(trainingSurveys)
.leftJoin(states, eq(trainingSurveys.state, states.id)) .leftJoin(states, eq(trainingSurveys.state, states.id))
.where(whereCondition) .where(whereCondition)
.groupBy(states.name); // Importante: Agrupar también por el resultado del COALESCE o por states.name
.groupBy(states.name),
const yearDistribution = await this.drizzle // 8. Distribución por Año
this.drizzle
.select({ .select({
name: sql<string>`cast(${trainingSurveys.companyConstitutionYear} as text)`, name: sql<string>`cast(${trainingSurveys.companyConstitutionYear} as text)`,
value: sql<number>`count(*)`, value: sql<number>`count(*)`,
@@ -158,11 +181,14 @@ export class TrainingService {
.from(trainingSurveys) .from(trainingSurveys)
.where(whereCondition) .where(whereCondition)
.groupBy(trainingSurveys.companyConstitutionYear) .groupBy(trainingSurveys.companyConstitutionYear)
.orderBy(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,13 +1,19 @@
'use client'; 'use client';
import { useState } from 'react'; import {
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@repo/shadcn/card'; useMunicipalityQuery,
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, PieChart, Pie, Cell } from 'recharts'; useParishQuery,
import { useTrainingStatsQuery } from '../hooks/use-training-statistics'; useStateQuery,
import { Input } from '@repo/shadcn/input'; } from '@/feactures/location/hooks/use-query-location';
import { Button } from '@repo/shadcn/button'; import { Button } from '@repo/shadcn/button';
import { SelectSearchable } from '@repo/shadcn/select-searchable'; import {
import { useStateQuery, useMunicipalityQuery, useParishQuery } from '@/feactures/location/hooks/use-query-location'; Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@repo/shadcn/card';
import { Input } from '@repo/shadcn/input';
import { import {
Select, Select,
SelectContent, SelectContent,
@@ -15,6 +21,22 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, 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',
@@ -39,10 +61,12 @@ export function TrainingStatistics() {
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 =
Array.isArray(dataMunicipality?.data) && dataMunicipality.data.length > 0
? dataMunicipality.data ? dataMunicipality.data
: [{ id: 0, stateId: 0, name: 'Sin Municipios' }]; : [{ id: 0, stateId: 0, name: 'Sin Municipios' }];
const parishOptions = Array.isArray(dataParish?.data) && dataParish.data.length > 0 const parishOptions =
Array.isArray(dataParish?.data) && dataParish.data.length > 0
? dataParish.data ? dataParish.data
: [{ id: 0, stateId: 0, name: 'Sin Parroquias' }]; : [{ id: 0, stateId: 0, name: 'Sin Parroquias' }];
@@ -66,16 +90,35 @@ export function TrainingStatistics() {
}; };
if (isLoading) { if (isLoading) {
return <div className="flex justify-center p-8">Cargando estadísticas...</div>; return (
<div className="flex justify-center p-8">Cargando estadísticas...</div>
);
} }
if (!data) { if (!data) {
return <div className="flex justify-center p-8">No hay datos disponibles.</div>; return (
<div className="flex justify-center p-8">No hay datos disponibles.</div>
);
} }
const { totalOsps, totalProducers, statusDistribution, activityDistribution, typeDistribution, stateDistribution, yearDistribution } = data; const {
totalOsps,
totalProducers,
statusDistribution,
activityDistribution,
typeDistribution,
stateDistribution,
yearDistribution,
} = data;
const COLORS = ['#0088FE', '#00C49F', '#FFBB28', '#FF8042', '#8884d8', '#82ca9d']; const COLORS = [
'#0088FE',
'#00C49F',
'#FFBB28',
'#FF8042',
'#8884d8',
'#82ca9d',
];
return ( return (
<div className="space-y-6"> <div className="space-y-6">
@@ -115,7 +158,7 @@ export function TrainingStatistics() {
setParishId(0); // Reset parish setParishId(0); // Reset parish
}} }}
placeholder="Selecciona un estado" placeholder="Selecciona un estado"
defaultValue={stateId ? stateId.toString() : ""} defaultValue={stateId ? stateId.toString() : ''}
/> />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
@@ -130,7 +173,7 @@ export function TrainingStatistics() {
setParishId(0); setParishId(0);
}} }}
placeholder="Selecciona municipio" placeholder="Selecciona municipio"
defaultValue={municipalityId ? municipalityId.toString() : ""} defaultValue={municipalityId ? municipalityId.toString() : ''}
disabled={!stateId || stateId === 0} disabled={!stateId || stateId === 0}
/> />
</div> </div>
@@ -143,7 +186,7 @@ export function TrainingStatistics() {
}))} }))}
onValueChange={(value: any) => setParishId(Number(value))} onValueChange={(value: any) => setParishId(Number(value))}
placeholder="Selecciona parroquia" placeholder="Selecciona parroquia"
defaultValue={parishId ? parishId.toString() : ""} defaultValue={parishId ? parishId.toString() : ''}
disabled={!municipalityId || municipalityId === 0} disabled={!municipalityId || municipalityId === 0}
/> />
</div> </div>
@@ -155,8 +198,10 @@ export function TrainingStatistics() {
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="all">Todos</SelectItem> <SelectItem value="all">Todos</SelectItem>
{OSP_TYPES.map(type => ( {OSP_TYPES.map((type) => (
<SelectItem key={type} value={type}>{type}</SelectItem> <SelectItem key={type} value={type}>
{type}
</SelectItem>
))} ))}
</SelectContent> </SelectContent>
</Select> </Select>
@@ -174,7 +219,9 @@ export function TrainingStatistics() {
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3"> <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
<Card> <Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> <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> <CardTitle className="text-sm font-medium">
Total de OSP Registradas
</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="text-2xl font-bold">{totalOsps}</div> <div className="text-2xl font-bold">{totalOsps}</div>
@@ -185,7 +232,9 @@ export function TrainingStatistics() {
</Card> </Card>
<Card> <Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Total de Productores</CardTitle> <CardTitle className="text-sm font-medium">
Total de Productores
</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="text-2xl font-bold">{totalProducers}</div> <div className="text-2xl font-bold">{totalProducers}</div>
@@ -198,7 +247,9 @@ export function TrainingStatistics() {
<Card className="col-span-full"> <Card className="col-span-full">
<CardHeader> <CardHeader>
<CardTitle>Actividad Productiva</CardTitle> <CardTitle>Actividad Productiva</CardTitle>
<CardDescription>Distribución por tipo de actividad</CardDescription> <CardDescription>
Distribución por tipo de actividad
</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="h-[400px]"> <CardContent className="h-[400px]">
<ResponsiveContainer width="100%" height="100%"> <ResponsiveContainer width="100%" height="100%">
@@ -219,7 +270,7 @@ export function TrainingStatistics() {
</Card> </Card>
{/* State Distribution */} {/* State Distribution */}
<Card className="col-span-full"> {/* <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,7 +290,7 @@ 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">
@@ -283,7 +334,10 @@ export function TrainingStatistics() {
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}`}
fill={COLORS[index % COLORS.length]}
/>
))} ))}
</Pie> </Pie>
<Tooltip wrapperStyle={{ color: '#000' }} /> <Tooltip wrapperStyle={{ color: '#000' }} />

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,56 +46,77 @@ 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 */}
<Section title="Datos de la Visita">
<DetailItem <DetailItem
label="Nombre del Coordinador" label="Coordinador"
value={data.firstname} value={`${data.firstname} ${data.lastname}`}
/> />
<DetailItem label="Teléfono Coord." value={data.coorPhone} />
<DetailItem <DetailItem
label="Apellido del Coordinador" label="Fecha Visita"
value={data.lastname}
/>
<DetailItem
label="Fecha y hora de la visita"
value={ value={
data.visitDate data.visitDate
? new Date(data.visitDate).toLocaleString() ? new Date(data.visitDate).toLocaleString()
@@ -95,177 +125,368 @@ export function TrainingViewModal({
/> />
</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 <DetailItem
label="Actividad Productiva" label="Sector Productivo"
value={data.productiveSector}
/>
<DetailItem
label="Actividad Central"
value={data.centralProductiveActivity}
/>
<DetailItem
label="Actividad Principal"
value={data.mainProductiveActivity}
/>
<div className="col-span-full">
<DetailItem
label="Actividad Específica"
value={data.productiveActivity} value={data.productiveActivity}
/> />
</div>
</Section>
{/* 3. Infraestructura y Ubicación */}
<Section title="Infraestructura y Ubicación" icon={MapPin}>
<DetailItem <DetailItem
label="Estatus" label="Año Constitución"
value={
<Badge
variant={
data.currentStatus === 'ACTIVA' ? 'default' : 'secondary'
}
>
{data.currentStatus}
</Badge>
}
/>
<DetailItem
label="Año de Constitución"
value={data.companyConstitutionYear} value={data.companyConstitutionYear}
/> />
<DetailItem <DetailItem
label="Cant. Productores" label="Infraestructura (m²)"
value={data.producerCount} value={data.infrastructureMt2}
/> />
<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="Tipo Estructura"
value={data.productDescription} value={data.structureType}
/>
</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
label="Requerimiento Financiero"
value={data.financialRequirementDescription}
/>
</div>
{data.paralysisReason && (
<div className="col-span-1 md:col-span-2 lg:col-span-3">
<DetailItem
label="Razones de paralización"
value={data.paralysisReason}
/> />
<DetailItem
label="Posee Transporte"
value={<BooleanBadge value={data.hasTransport} />}
/>
<DetailItem
label="Espacio Abierto"
value={<BooleanBadge value={data.isOpenSpace} />}
/>
<div className="col-span-full space-y-4 mt-2">
<div className="space-y-1">
<p className="text-xs font-medium text-muted-foreground uppercase">
Dirección
</p>
<p className="text-sm font-medium">{data.ospAddress}</p>
</div> </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>
</Section> </Section>
{/* 3. Ubicación */} {/* 4. LISTAS DETALLADAS (Lo nuevo) */}
<Section title="3. Detalles de la ubicación">
{/* 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 <DetailItem
label="Código SITUR Comuna" 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} 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 <DetailItem
label="Consejo Comunal" label="Consejo Comunal"
value={data.communalCouncil} value={data.communalCouncil}
/> />
<DetailItem <DetailItem
label="Código SITUR Consejo Comunal" label="Vocero C.C."
value={data.siturCodeCommunalCouncil} value={data.communalCouncilSpokespersonName}
/> />
<div className="col-span-1 md:col-span-2 lg:col-span-3">
<DetailItem label="Dirección OSP" value={data.ospAddress} />
</div> </div>
</Section> </Section>
{/* 4. Responsable */} <Section title="Responsable OSP">
<Section title="4. Datos del Responsable">
<DetailItem <DetailItem
label="Nombre Completo" label="Nombre"
value={data.ospResponsibleFullname} value={data.ospResponsibleFullname}
/> />
<DetailItem label="Cédula" value={data.ospResponsibleCedula} />
<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 <DetailItem
label="Número de Hijos" label="Cédula"
value={data.numberOfChildren} 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> </Section>
</div>
{/* 5. Observaciones */} {/* 6. Observaciones */}
{(data.generalObservations || data.paralysisReason) && (
<Card> <Card>
<CardHeader className="py-3"> <CardHeader>
<CardTitle className="text-lg"> <CardTitle>Observaciones</CardTitle>
5. Observaciones Generales
</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent className="space-y-4">
<p className="text-sm whitespace-pre-wrap"> {data.generalObservations && (
{data.generalObservations || 'Sin observaciones'} <div>
<p className="text-xs font-bold text-muted-foreground uppercase mb-1">
Generales
</p> </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> </CardContent>
</Card> </Card>
)}
{/* 6. Registro fotográfico */} {/* 7. Fotos */}
<Card> <Section title="Registro Fotográfico">
<CardHeader className="py-3"> {[data.photo1, data.photo2, data.photo3].some(Boolean) ? (
<CardTitle className="text-lg"> <div className="col-span-full grid grid-cols-1 sm:grid-cols-3 gap-4">
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( {[data.photo1, data.photo2, data.photo3].map(
(photo, idx) => (photo, idx) =>
photo && ( photo && (
<div <div
key={idx} key={idx}
className="relative aspect-video rounded-md overflow-hidden cursor-pointer hover:opacity-90 transition-opacity bg-muted" className="relative aspect-video rounded-lg overflow-hidden cursor-zoom-in border hover:shadow-lg transition-all"
onClick={() => setSelectedImage(photo)} onClick={() => setSelectedImage(photo)}
> >
<img <img
src={`${process.env.NEXT_PUBLIC_API_URL}${photo}`} src={`${process.env.NEXT_PUBLIC_API_URL}${photo}`}
alt={`Registro ${idx + 1}`} alt={`Evidencia ${idx + 1}`}
className="object-cover w-full h-full" className="object-cover w-full h-full"
/> />
</div> </div>
), ),
)} )}
{![data.photo1, data.photo2, data.photo3].some(Boolean) && ( </div>
<p className="text-sm text-muted-foreground"> ) : (
No hay registro fotográfico <p className="text-sm text-muted-foreground col-span-full">
No hay imágenes cargadas.
</p> </p>
)} )}
</Section>
</div> </div>
</CardContent> </ScrollArea>
</Card>
</div>
<DialogFooter className="mt-6"> <DialogFooter className="px-6 py-4 border-t bg-muted/20">
<Button onClick={onClose} variant="outline" className="w-32"> <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,13 +1,17 @@
import { z } from 'zod'; import { z } from 'zod';
export const statisticsItemSchema = z.object({ export const statisticsItemSchema = z.object({
name: z.string(), name: z
.string()
.nullable()
.transform((val) => val || 'Sin Información'),
value: z.number(), 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(),
totalProducts: z.number(),
statusDistribution: z.array(statisticsItemSchema), statusDistribution: z.array(statisticsItemSchema),
activityDistribution: z.array(statisticsItemSchema), activityDistribution: z.array(statisticsItemSchema),
typeDistribution: z.array(statisticsItemSchema), typeDistribution: z.array(statisticsItemSchema),

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;
} };