correciones al formulario osp
This commit is contained in:
9
apps/api/src/database/migrations/0014_deep_meteorite.sql
Normal file
9
apps/api/src/database/migrations/0014_deep_meteorite.sql
Normal 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;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "training_surveys" ALTER COLUMN "photo1" DROP NOT NULL;
|
||||||
36
apps/api/src/database/migrations/0016_silent_tag.sql
Normal file
36
apps/api/src/database/migrations/0016_silent_tag.sql
Normal 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";
|
||||||
2222
apps/api/src/database/migrations/meta/0014_snapshot.json
Normal file
2222
apps/api/src/database/migrations/meta/0014_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
2222
apps/api/src/database/migrations/meta/0015_snapshot.json
Normal file
2222
apps/api/src/database/migrations/meta/0015_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
1994
apps/api/src/database/migrations/meta/0016_snapshot.json
Normal file
1994
apps/api/src/database/migrations/meta/0016_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -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) => ({
|
||||||
|
|||||||
@@ -1,7 +1,16 @@
|
|||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { IsDateString, IsInt, IsOptional, IsString } from 'class-validator';
|
import { Transform, Type } from 'class-transformer';
|
||||||
|
import {
|
||||||
|
IsArray,
|
||||||
|
IsBoolean,
|
||||||
|
IsDateString,
|
||||||
|
IsInt,
|
||||||
|
IsOptional,
|
||||||
|
IsString,
|
||||||
|
} from 'class-validator';
|
||||||
|
|
||||||
export class CreateTrainingDto {
|
export class CreateTrainingDto {
|
||||||
|
// === 1. DATOS BÁSICOS ===
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@IsString()
|
@IsString()
|
||||||
firstname: string;
|
firstname: string;
|
||||||
@@ -12,7 +21,25 @@ export class CreateTrainingDto {
|
|||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@IsDateString()
|
@IsDateString()
|
||||||
visitDate: string;
|
visitDate: string; // Llega como string ISO "2024-11-11T10:00"
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
coorPhone?: string;
|
||||||
|
|
||||||
|
// === 2. DATOS OSP ===
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
ospName: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
ospRif: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
ospType: string; // 'UPF', etc.
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@IsString()
|
@IsString()
|
||||||
@@ -20,24 +47,108 @@ export class CreateTrainingDto {
|
|||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsOptional()
|
currentStatus: string;
|
||||||
financialRequirementDescription?: string;
|
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@IsInt()
|
@IsInt()
|
||||||
|
@Type(() => Number) // Convierte "2017" -> 2017
|
||||||
|
companyConstitutionYear: number;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
state: number;
|
ospAddress: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
ospGoogleMapsLink?: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
infrastructureMt2?: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
structureType?: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsBoolean()
|
||||||
|
@IsOptional()
|
||||||
|
@Transform(({ value }) => value === 'true' || value === true) // Convierte "false" -> false
|
||||||
|
hasTransport?: boolean;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsBoolean()
|
||||||
|
@IsOptional()
|
||||||
|
@Transform(({ value }) => value === 'true' || value === true)
|
||||||
|
isOpenSpace?: boolean;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
paralysisReason?: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
generalObservations?: string;
|
||||||
|
|
||||||
|
// === 3. SECTORES ===
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
ecoSector: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
productiveSector: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
centralProductiveActivity: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
mainProductiveActivity: string;
|
||||||
|
|
||||||
|
// === 4. DATOS RESPONSABLE ===
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
ospResponsibleFullname: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
ospResponsibleCedula: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
ospResponsibleRif: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
ospResponsiblePhone: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
ospResponsibleEmail: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
civilState: string;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@IsInt()
|
@IsInt()
|
||||||
@IsOptional()
|
@Type(() => Number) // Convierte "3" -> 3
|
||||||
municipality: number;
|
familyBurden: number;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@IsInt()
|
@IsInt()
|
||||||
@IsOptional()
|
@Type(() => Number)
|
||||||
parish: number;
|
numberOfChildren: number;
|
||||||
|
|
||||||
|
// === 5. COMUNA Y CONSEJO COMUNAL ===
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@IsString()
|
@IsString()
|
||||||
siturCodeCommune: string;
|
siturCodeCommune: string;
|
||||||
@@ -102,217 +213,51 @@ export class CreateTrainingDto {
|
|||||||
@IsString()
|
@IsString()
|
||||||
communalCouncilEmail: string;
|
communalCouncilEmail: string;
|
||||||
|
|
||||||
@ApiProperty()
|
// === 6. LISTAS (Arrays JSON) ===
|
||||||
@IsString()
|
// Reciben un string JSON '[{...}]' y lo convierten a Objeto JS real
|
||||||
ospGoogleMapsLink: string;
|
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@IsString()
|
|
||||||
ospName: string;
|
|
||||||
|
|
||||||
@ApiProperty()
|
|
||||||
@IsString()
|
|
||||||
ospAddress: string;
|
|
||||||
|
|
||||||
@ApiProperty()
|
|
||||||
@IsString()
|
|
||||||
ospRif: string;
|
|
||||||
|
|
||||||
@ApiProperty()
|
|
||||||
@IsString()
|
|
||||||
ospType: string;
|
|
||||||
|
|
||||||
@ApiProperty()
|
|
||||||
@IsString()
|
|
||||||
currentStatus: string;
|
|
||||||
|
|
||||||
@ApiProperty()
|
|
||||||
@IsInt()
|
|
||||||
companyConstitutionYear: number;
|
|
||||||
|
|
||||||
@ApiProperty()
|
|
||||||
@IsInt()
|
|
||||||
producerCount: number;
|
|
||||||
|
|
||||||
@ApiProperty()
|
|
||||||
@IsInt()
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
productCount: number;
|
@IsArray()
|
||||||
|
@Transform(({ value }) => {
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
try {
|
||||||
|
return JSON.parse(value);
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
})
|
||||||
|
equipmentList?: any[];
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@IsString()
|
|
||||||
productDescription: string;
|
|
||||||
|
|
||||||
@ApiProperty()
|
|
||||||
@IsString()
|
|
||||||
installedCapacity: string;
|
|
||||||
|
|
||||||
@ApiProperty()
|
|
||||||
@IsString()
|
|
||||||
operationalCapacity: string;
|
|
||||||
|
|
||||||
@ApiProperty()
|
|
||||||
@IsString()
|
|
||||||
ospResponsibleFullname: string;
|
|
||||||
|
|
||||||
@ApiProperty()
|
|
||||||
@IsString()
|
|
||||||
ospResponsibleCedula: string;
|
|
||||||
|
|
||||||
@ApiProperty()
|
|
||||||
@IsString()
|
|
||||||
ospResponsibleRif: string;
|
|
||||||
|
|
||||||
@ApiProperty()
|
|
||||||
@IsString()
|
|
||||||
ospResponsiblePhone: string;
|
|
||||||
|
|
||||||
@ApiProperty()
|
|
||||||
@IsString()
|
|
||||||
ospResponsibleEmail: string;
|
|
||||||
|
|
||||||
@ApiProperty()
|
|
||||||
@IsString()
|
|
||||||
civilState: string;
|
|
||||||
|
|
||||||
@ApiProperty()
|
|
||||||
@IsInt()
|
|
||||||
familyBurden: number;
|
|
||||||
|
|
||||||
@ApiProperty()
|
|
||||||
@IsInt()
|
|
||||||
numberOfChildren: number;
|
|
||||||
|
|
||||||
@ApiProperty()
|
|
||||||
@IsString()
|
|
||||||
generalObservations: string;
|
|
||||||
|
|
||||||
@ApiProperty()
|
|
||||||
@IsString()
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
photo1?: string;
|
@IsArray()
|
||||||
|
@Transform(({ value }) => {
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
try {
|
||||||
|
return JSON.parse(value);
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
})
|
||||||
|
productionList?: any[];
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@IsString()
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
photo2?: string;
|
@IsArray()
|
||||||
|
@Transform(({ value }) => {
|
||||||
@ApiProperty()
|
if (typeof value === 'string') {
|
||||||
@IsString()
|
try {
|
||||||
@IsOptional()
|
return JSON.parse(value);
|
||||||
photo3?: string;
|
} catch {
|
||||||
|
return [];
|
||||||
@ApiProperty()
|
}
|
||||||
@IsString()
|
}
|
||||||
@IsOptional()
|
return value;
|
||||||
paralysisReason: string;
|
})
|
||||||
|
productList?: any[];
|
||||||
// nuevos campos coordinacion
|
|
||||||
@ApiProperty()
|
|
||||||
@IsInt()
|
|
||||||
@IsOptional()
|
|
||||||
coorState?: number;
|
|
||||||
|
|
||||||
@ApiProperty()
|
|
||||||
@IsInt()
|
|
||||||
@IsOptional()
|
|
||||||
coorMunicipality?: number;
|
|
||||||
|
|
||||||
@ApiProperty()
|
|
||||||
@IsInt()
|
|
||||||
@IsOptional()
|
|
||||||
coorParish?: number;
|
|
||||||
|
|
||||||
@ApiProperty()
|
|
||||||
@IsString()
|
|
||||||
@IsOptional()
|
|
||||||
coorPhone?: string;
|
|
||||||
|
|
||||||
// sectores
|
|
||||||
@ApiProperty()
|
|
||||||
@IsString()
|
|
||||||
ecoSector: string;
|
|
||||||
|
|
||||||
@ApiProperty()
|
|
||||||
@IsString()
|
|
||||||
productiveSector: string;
|
|
||||||
|
|
||||||
@ApiProperty()
|
|
||||||
@IsString()
|
|
||||||
centralProductiveActivity: string;
|
|
||||||
|
|
||||||
@ApiProperty()
|
|
||||||
@IsString()
|
|
||||||
mainProductiveActivity: string;
|
|
||||||
|
|
||||||
// equipamiento
|
|
||||||
@ApiProperty()
|
|
||||||
@IsString()
|
|
||||||
typesOfEquipment: string;
|
|
||||||
|
|
||||||
@ApiProperty()
|
|
||||||
@IsInt()
|
|
||||||
equipmentCount: number;
|
|
||||||
|
|
||||||
@ApiProperty()
|
|
||||||
@IsString()
|
|
||||||
equipmentDescription: string;
|
|
||||||
|
|
||||||
// materia prima
|
|
||||||
@ApiProperty()
|
|
||||||
@IsString()
|
|
||||||
rawMaterial: string;
|
|
||||||
|
|
||||||
@ApiProperty()
|
|
||||||
@IsString()
|
|
||||||
materialType: string;
|
|
||||||
|
|
||||||
@ApiProperty()
|
|
||||||
@IsInt()
|
|
||||||
rawMaterialCount: number;
|
|
||||||
|
|
||||||
// conteo de productos
|
|
||||||
@ApiProperty()
|
|
||||||
@IsInt()
|
|
||||||
productCountDaily: number;
|
|
||||||
|
|
||||||
@ApiProperty()
|
|
||||||
@IsInt()
|
|
||||||
productCountWeekly: number;
|
|
||||||
|
|
||||||
@ApiProperty()
|
|
||||||
@IsInt()
|
|
||||||
productCountMonthly: number;
|
|
||||||
|
|
||||||
@ApiProperty()
|
|
||||||
@IsString()
|
|
||||||
prodDescriptionInternal: string;
|
|
||||||
|
|
||||||
@ApiProperty()
|
|
||||||
@IsInt()
|
|
||||||
internalCount: number;
|
|
||||||
|
|
||||||
@ApiProperty()
|
|
||||||
@IsInt()
|
|
||||||
externalCount: number;
|
|
||||||
|
|
||||||
@ApiProperty()
|
|
||||||
@IsString()
|
|
||||||
prodDescriptionExternal: string;
|
|
||||||
|
|
||||||
@ApiProperty()
|
|
||||||
@IsString()
|
|
||||||
country: string;
|
|
||||||
|
|
||||||
@ApiProperty()
|
|
||||||
@IsString()
|
|
||||||
city: string;
|
|
||||||
|
|
||||||
@ApiProperty()
|
|
||||||
@IsInt()
|
|
||||||
menCount: number;
|
|
||||||
|
|
||||||
@ApiProperty()
|
|
||||||
@IsInt()
|
|
||||||
womenCount: number;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,95 +74,121 @@ export class TrainingService {
|
|||||||
|
|
||||||
const filters: SQL[] = [];
|
const filters: SQL[] = [];
|
||||||
|
|
||||||
if (startDate) {
|
if (startDate)
|
||||||
filters.push(gte(trainingSurveys.visitDate, new Date(startDate)));
|
filters.push(gte(trainingSurveys.visitDate, new Date(startDate)));
|
||||||
}
|
if (endDate)
|
||||||
|
|
||||||
if (endDate) {
|
|
||||||
filters.push(lte(trainingSurveys.visitDate, new Date(endDate)));
|
filters.push(lte(trainingSurveys.visitDate, new Date(endDate)));
|
||||||
}
|
if (stateId) filters.push(eq(trainingSurveys.state, stateId));
|
||||||
|
if (municipalityId)
|
||||||
if (stateId) {
|
|
||||||
filters.push(eq(trainingSurveys.state, stateId));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (municipalityId) {
|
|
||||||
filters.push(eq(trainingSurveys.municipality, municipalityId));
|
filters.push(eq(trainingSurveys.municipality, municipalityId));
|
||||||
}
|
if (parishId) filters.push(eq(trainingSurveys.parish, parishId));
|
||||||
|
if (ospType) filters.push(eq(trainingSurveys.ospType, ospType));
|
||||||
if (parishId) {
|
|
||||||
filters.push(eq(trainingSurveys.parish, parishId));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ospType) {
|
|
||||||
filters.push(eq(trainingSurveys.ospType, ospType));
|
|
||||||
}
|
|
||||||
|
|
||||||
const whereCondition = filters.length > 0 ? and(...filters) : undefined;
|
const whereCondition = filters.length > 0 ? and(...filters) : undefined;
|
||||||
|
|
||||||
const totalOspsResult = await this.drizzle
|
// Ejecutamos todas las consultas en paralelo con Promise.all para mayor velocidad
|
||||||
.select({ count: sql<number>`count(*)` })
|
const [
|
||||||
.from(trainingSurveys)
|
totalOspsResult,
|
||||||
.where(whereCondition);
|
totalProducersResult,
|
||||||
const totalOsps = Number(totalOspsResult[0].count);
|
totalProductsResult, // Nuevo: Calculado desde el JSON
|
||||||
|
statusDistribution,
|
||||||
|
activityDistribution,
|
||||||
|
typeDistribution,
|
||||||
|
stateDistribution,
|
||||||
|
yearDistribution,
|
||||||
|
] = await Promise.all([
|
||||||
|
// 1. Total OSPs
|
||||||
|
this.drizzle
|
||||||
|
.select({ count: sql<number>`count(*)` })
|
||||||
|
.from(trainingSurveys)
|
||||||
|
.where(whereCondition),
|
||||||
|
|
||||||
const totalProducersResult = await this.drizzle
|
// 2. Total Productores (Columna plana que mantuviste)
|
||||||
.select({ sum: sql<number>`sum(${trainingSurveys.producerCount})` })
|
this.drizzle
|
||||||
.from(trainingSurveys)
|
.select({
|
||||||
.where(whereCondition);
|
sum: sql<number>`
|
||||||
const totalProducers = Number(totalProducersResult[0].sum || 0);
|
SUM(
|
||||||
|
(
|
||||||
|
SELECT SUM(
|
||||||
|
COALESCE((item->>'menCount')::int, 0) +
|
||||||
|
COALESCE((item->>'womenCount')::int, 0)
|
||||||
|
)
|
||||||
|
FROM jsonb_array_elements(${trainingSurveys.productList}) as item
|
||||||
|
)
|
||||||
|
)
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
.from(trainingSurveys)
|
||||||
|
.where(whereCondition),
|
||||||
|
|
||||||
const statusDistribution = await this.drizzle
|
// 3. NUEVO: Total Productos (Contamos el largo del array JSON productList)
|
||||||
.select({
|
this.drizzle
|
||||||
name: trainingSurveys.currentStatus,
|
.select({
|
||||||
value: sql<number>`count(*)`,
|
sum: sql<number>`sum(jsonb_array_length(${trainingSurveys.productList}))`,
|
||||||
})
|
})
|
||||||
.from(trainingSurveys)
|
.from(trainingSurveys)
|
||||||
.where(whereCondition)
|
.where(whereCondition),
|
||||||
.groupBy(trainingSurveys.currentStatus);
|
|
||||||
|
|
||||||
const activityDistribution = await this.drizzle
|
// 4. Distribución por Estatus
|
||||||
.select({
|
this.drizzle
|
||||||
name: trainingSurveys.productiveActivity,
|
.select({
|
||||||
value: sql<number>`count(*)`,
|
name: trainingSurveys.currentStatus,
|
||||||
})
|
value: sql<number>`count(*)`,
|
||||||
.from(trainingSurveys)
|
})
|
||||||
.where(whereCondition)
|
.from(trainingSurveys)
|
||||||
.groupBy(trainingSurveys.productiveActivity);
|
.where(whereCondition)
|
||||||
|
.groupBy(trainingSurveys.currentStatus),
|
||||||
|
|
||||||
const typeDistribution = await this.drizzle
|
// 5. Distribución por Actividad
|
||||||
.select({
|
this.drizzle
|
||||||
name: trainingSurveys.ospType,
|
.select({
|
||||||
value: sql<number>`count(*)`,
|
name: trainingSurveys.productiveActivity,
|
||||||
})
|
value: sql<number>`count(*)`,
|
||||||
.from(trainingSurveys)
|
})
|
||||||
.where(whereCondition)
|
.from(trainingSurveys)
|
||||||
.groupBy(trainingSurveys.ospType);
|
.where(whereCondition)
|
||||||
|
.groupBy(trainingSurveys.productiveActivity),
|
||||||
|
|
||||||
// New Aggregations
|
// 6. Distribución por Tipo
|
||||||
const stateDistribution = await this.drizzle
|
this.drizzle
|
||||||
.select({
|
.select({
|
||||||
name: states.name,
|
name: trainingSurveys.ospType,
|
||||||
value: sql<number>`count(${trainingSurveys.id})`,
|
value: sql<number>`count(*)`,
|
||||||
})
|
})
|
||||||
.from(trainingSurveys)
|
.from(trainingSurveys)
|
||||||
.leftJoin(states, eq(trainingSurveys.state, states.id))
|
.where(whereCondition)
|
||||||
.where(whereCondition)
|
.groupBy(trainingSurveys.ospType),
|
||||||
.groupBy(states.name);
|
|
||||||
|
|
||||||
const yearDistribution = await this.drizzle
|
// 7. Distribución por Estado (CORREGIDO con COALESCE)
|
||||||
.select({
|
this.drizzle
|
||||||
name: sql<string>`cast(${trainingSurveys.companyConstitutionYear} as text)`,
|
.select({
|
||||||
value: sql<number>`count(*)`,
|
// Si states.name es NULL, devuelve 'Sin Asignar'
|
||||||
})
|
name: sql<string>`COALESCE(${states.name}, 'Sin Asignar')`,
|
||||||
.from(trainingSurveys)
|
value: sql<number>`count(${trainingSurveys.id})`,
|
||||||
.where(whereCondition)
|
})
|
||||||
.groupBy(trainingSurveys.companyConstitutionYear)
|
.from(trainingSurveys)
|
||||||
.orderBy(trainingSurveys.companyConstitutionYear);
|
.leftJoin(states, eq(trainingSurveys.state, states.id))
|
||||||
|
.where(whereCondition)
|
||||||
|
// Importante: Agrupar también por el resultado del COALESCE o por states.name
|
||||||
|
.groupBy(states.name),
|
||||||
|
|
||||||
|
// 8. Distribución por Año
|
||||||
|
this.drizzle
|
||||||
|
.select({
|
||||||
|
name: sql<string>`cast(${trainingSurveys.companyConstitutionYear} as text)`,
|
||||||
|
value: sql<number>`count(*)`,
|
||||||
|
})
|
||||||
|
.from(trainingSurveys)
|
||||||
|
.where(whereCondition)
|
||||||
|
.groupBy(trainingSurveys.companyConstitutionYear)
|
||||||
|
.orderBy(trainingSurveys.companyConstitutionYear),
|
||||||
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
totalOsps,
|
totalOsps: Number(totalOspsResult[0]?.count || 0),
|
||||||
totalProducers,
|
totalProducers: Number(totalProducersResult[0]?.sum || 0),
|
||||||
|
totalProducts: Number(totalProductsResult[0]?.sum || 0), // Dato extraído del JSON
|
||||||
|
|
||||||
statusDistribution: statusDistribution.map((item) => ({
|
statusDistribution: statusDistribution.map((item) => ({
|
||||||
...item,
|
...item,
|
||||||
value: Number(item.value),
|
value: Number(item.value),
|
||||||
@@ -239,16 +265,30 @@ export class TrainingService {
|
|||||||
createTrainingDto: CreateTrainingDto,
|
createTrainingDto: CreateTrainingDto,
|
||||||
files: Express.Multer.File[],
|
files: Express.Multer.File[],
|
||||||
) {
|
) {
|
||||||
|
// 1. Guardar fotos
|
||||||
const photoPaths = await this.saveFiles(files);
|
const photoPaths = await this.saveFiles(files);
|
||||||
|
|
||||||
|
// 2. Extraer solo visitDate para formatearlo.
|
||||||
|
// Ya NO extraemos state, municipality, etc. porque no vienen en el DTO.
|
||||||
|
const { visitDate, ...rest } = createTrainingDto;
|
||||||
|
|
||||||
const [newRecord] = await this.drizzle
|
const [newRecord] = await this.drizzle
|
||||||
.insert(trainingSurveys)
|
.insert(trainingSurveys)
|
||||||
.values({
|
.values({
|
||||||
...createTrainingDto,
|
// Insertamos el resto de datos planos y las listas (arrays)
|
||||||
visitDate: new Date(createTrainingDto.visitDate),
|
...rest,
|
||||||
photo1: photoPaths[0] || '',
|
|
||||||
photo2: photoPaths[1] || null,
|
// Conversión de fecha
|
||||||
photo3: photoPaths[2] || null,
|
visitDate: new Date(visitDate),
|
||||||
|
|
||||||
|
// 3. Asignar fotos de forma segura
|
||||||
|
photo1: photoPaths[0] ?? null,
|
||||||
|
photo2: photoPaths[1] ?? null,
|
||||||
|
photo3: photoPaths[2] ?? null,
|
||||||
|
|
||||||
|
// NOTA: Como las columnas state, municipality, etc. en la BD
|
||||||
|
// tienen "onDelete: set null" o son nullables, al no pasarlas aquí,
|
||||||
|
// Postgres automáticamente las guardará como NULL.
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
171
apps/web/feactures/training/components/equipment-list.tsx
Normal file
171
apps/web/feactures/training/components/equipment-list.tsx
Normal 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
504
apps/web/feactures/training/components/product-activity-list.tsx
Normal file
504
apps/web/feactures/training/components/product-activity-list.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
171
apps/web/feactures/training/components/production-list.tsx
Normal file
171
apps/web/feactures/training/components/production-list.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,225 +1,276 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@repo/shadcn/card';
|
|
||||||
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, PieChart, Pie, Cell } from 'recharts';
|
|
||||||
import { useTrainingStatsQuery } from '../hooks/use-training-statistics';
|
|
||||||
import { Input } from '@repo/shadcn/input';
|
|
||||||
import { Button } from '@repo/shadcn/button';
|
|
||||||
import { SelectSearchable } from '@repo/shadcn/select-searchable';
|
|
||||||
import { useStateQuery, useMunicipalityQuery, useParishQuery } from '@/feactures/location/hooks/use-query-location';
|
|
||||||
import {
|
import {
|
||||||
Select,
|
useMunicipalityQuery,
|
||||||
SelectContent,
|
useParishQuery,
|
||||||
SelectItem,
|
useStateQuery,
|
||||||
SelectTrigger,
|
} from '@/feactures/location/hooks/use-query-location';
|
||||||
SelectValue,
|
import { Button } from '@repo/shadcn/button';
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from '@repo/shadcn/card';
|
||||||
|
import { Input } from '@repo/shadcn/input';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
} from '@repo/shadcn/select';
|
} from '@repo/shadcn/select';
|
||||||
|
import { SelectSearchable } from '@repo/shadcn/select-searchable';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import {
|
||||||
|
Bar,
|
||||||
|
BarChart,
|
||||||
|
CartesianGrid,
|
||||||
|
Cell,
|
||||||
|
Legend,
|
||||||
|
Pie,
|
||||||
|
PieChart,
|
||||||
|
ResponsiveContainer,
|
||||||
|
Tooltip,
|
||||||
|
XAxis,
|
||||||
|
YAxis,
|
||||||
|
} from 'recharts';
|
||||||
|
import { useTrainingStatsQuery } from '../hooks/use-training-statistics';
|
||||||
|
|
||||||
const OSP_TYPES = [
|
const OSP_TYPES = [
|
||||||
'EPSD',
|
'EPSD',
|
||||||
'EPSI',
|
'EPSI',
|
||||||
'UPF',
|
'UPF',
|
||||||
'Cooperativa',
|
'Cooperativa',
|
||||||
'Grupo de Intercambio',
|
'Grupo de Intercambio',
|
||||||
];
|
];
|
||||||
|
|
||||||
export function TrainingStatistics() {
|
export function TrainingStatistics() {
|
||||||
// Filter State
|
// Filter State
|
||||||
const [startDate, setStartDate] = useState<string>('');
|
const [startDate, setStartDate] = useState<string>('');
|
||||||
const [endDate, setEndDate] = useState<string>('');
|
const [endDate, setEndDate] = useState<string>('');
|
||||||
const [stateId, setStateId] = useState<number>(0);
|
const [stateId, setStateId] = useState<number>(0);
|
||||||
const [municipalityId, setMunicipalityId] = useState<number>(0);
|
const [municipalityId, setMunicipalityId] = useState<number>(0);
|
||||||
const [parishId, setParishId] = useState<number>(0);
|
const [parishId, setParishId] = useState<number>(0);
|
||||||
const [ospType, setOspType] = useState<string>('');
|
const [ospType, setOspType] = useState<string>('');
|
||||||
|
|
||||||
// Location Data
|
// Location Data
|
||||||
const { data: dataState } = useStateQuery();
|
const { data: dataState } = useStateQuery();
|
||||||
const { data: dataMunicipality } = useMunicipalityQuery(stateId);
|
const { data: dataMunicipality } = useMunicipalityQuery(stateId);
|
||||||
const { data: dataParish } = useParishQuery(municipalityId);
|
const { data: dataParish } = useParishQuery(municipalityId);
|
||||||
|
|
||||||
const stateOptions = dataState?.data || [{ id: 0, name: 'Sin estados' }];
|
const stateOptions = dataState?.data || [{ id: 0, name: 'Sin estados' }];
|
||||||
const municipalityOptions = Array.isArray(dataMunicipality?.data) && dataMunicipality.data.length > 0
|
const municipalityOptions =
|
||||||
? dataMunicipality.data
|
Array.isArray(dataMunicipality?.data) && dataMunicipality.data.length > 0
|
||||||
: [{ id: 0, stateId: 0, name: 'Sin Municipios' }];
|
? dataMunicipality.data
|
||||||
const parishOptions = Array.isArray(dataParish?.data) && dataParish.data.length > 0
|
: [{ id: 0, stateId: 0, name: 'Sin Municipios' }];
|
||||||
? dataParish.data
|
const parishOptions =
|
||||||
: [{ id: 0, stateId: 0, name: 'Sin Parroquias' }];
|
Array.isArray(dataParish?.data) && dataParish.data.length > 0
|
||||||
|
? dataParish.data
|
||||||
|
: [{ id: 0, stateId: 0, name: 'Sin Parroquias' }];
|
||||||
|
|
||||||
// Query with Filters
|
// Query with Filters
|
||||||
const { data, isLoading, refetch } = useTrainingStatsQuery({
|
const { data, isLoading, refetch } = useTrainingStatsQuery({
|
||||||
startDate: startDate || undefined,
|
startDate: startDate || undefined,
|
||||||
endDate: endDate || undefined,
|
endDate: endDate || undefined,
|
||||||
stateId: stateId || undefined,
|
stateId: stateId || undefined,
|
||||||
municipalityId: municipalityId || undefined,
|
municipalityId: municipalityId || undefined,
|
||||||
parishId: parishId || undefined,
|
parishId: parishId || undefined,
|
||||||
ospType: ospType || undefined,
|
ospType: ospType || undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleClearFilters = () => {
|
const handleClearFilters = () => {
|
||||||
setStartDate('');
|
setStartDate('');
|
||||||
setEndDate('');
|
setEndDate('');
|
||||||
setStateId(0);
|
setStateId(0);
|
||||||
setMunicipalityId(0);
|
setMunicipalityId(0);
|
||||||
setParishId(0);
|
setParishId(0);
|
||||||
setOspType('');
|
setOspType('');
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return <div className="flex justify-center p-8">Cargando estadísticas...</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!data) {
|
|
||||||
return <div className="flex justify-center p-8">No hay datos disponibles.</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { totalOsps, totalProducers, statusDistribution, activityDistribution, typeDistribution, stateDistribution, yearDistribution } = data;
|
|
||||||
|
|
||||||
const COLORS = ['#0088FE', '#00C49F', '#FFBB28', '#FF8042', '#8884d8', '#82ca9d'];
|
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="flex justify-center p-8">Cargando estadísticas...</div>
|
||||||
{/* Filters Section */}
|
);
|
||||||
<Card>
|
}
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Filtros</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label className="text-sm font-medium">Fecha Inicio</label>
|
|
||||||
<Input
|
|
||||||
type="date"
|
|
||||||
value={startDate}
|
|
||||||
onChange={(e) => setStartDate(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label className="text-sm font-medium">Fecha Fin</label>
|
|
||||||
<Input
|
|
||||||
type="date"
|
|
||||||
value={endDate}
|
|
||||||
onChange={(e) => setEndDate(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label className="text-sm font-medium">Estado</label>
|
|
||||||
<SelectSearchable
|
|
||||||
options={stateOptions.map((item) => ({
|
|
||||||
value: item.id.toString(),
|
|
||||||
label: item.name,
|
|
||||||
}))}
|
|
||||||
onValueChange={(value: any) => {
|
|
||||||
setStateId(Number(value));
|
|
||||||
setMunicipalityId(0); // Reset municipality
|
|
||||||
setParishId(0); // Reset parish
|
|
||||||
}}
|
|
||||||
placeholder="Selecciona un estado"
|
|
||||||
defaultValue={stateId ? stateId.toString() : ""}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label className="text-sm font-medium">Municipio</label>
|
|
||||||
<SelectSearchable
|
|
||||||
options={municipalityOptions.map((item) => ({
|
|
||||||
value: item.id.toString(),
|
|
||||||
label: item.name,
|
|
||||||
}))}
|
|
||||||
onValueChange={(value: any) => {
|
|
||||||
setMunicipalityId(Number(value));
|
|
||||||
setParishId(0);
|
|
||||||
}}
|
|
||||||
placeholder="Selecciona municipio"
|
|
||||||
defaultValue={municipalityId ? municipalityId.toString() : ""}
|
|
||||||
disabled={!stateId || stateId === 0}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label className="text-sm font-medium">Parroquia</label>
|
|
||||||
<SelectSearchable
|
|
||||||
options={parishOptions.map((item) => ({
|
|
||||||
value: item.id.toString(),
|
|
||||||
label: item.name,
|
|
||||||
}))}
|
|
||||||
onValueChange={(value: any) => setParishId(Number(value))}
|
|
||||||
placeholder="Selecciona parroquia"
|
|
||||||
defaultValue={parishId ? parishId.toString() : ""}
|
|
||||||
disabled={!municipalityId || municipalityId === 0}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label className="text-sm font-medium">Tipo de OSP</label>
|
|
||||||
<Select value={ospType} onValueChange={setOspType}>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder="Todos" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="all">Todos</SelectItem>
|
|
||||||
{OSP_TYPES.map(type => (
|
|
||||||
<SelectItem key={type} value={type}>{type}</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-end">
|
|
||||||
<Button variant="outline" onClick={handleClearFilters}>
|
|
||||||
Limpiar Filtros
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Statistics Cards */}
|
if (!data) {
|
||||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
return (
|
||||||
<Card>
|
<div className="flex justify-center p-8">No hay datos disponibles.</div>
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
);
|
||||||
<CardTitle className="text-sm font-medium">Total de OSP Registradas</CardTitle>
|
}
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="text-2xl font-bold">{totalOsps}</div>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
Organizaciones Socioproductivas
|
|
||||||
</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
||||||
<CardTitle className="text-sm font-medium">Total de Productores</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="text-2xl font-bold">{totalProducers}</div>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
Productores asociados
|
|
||||||
</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card className="col-span-full">
|
const {
|
||||||
<CardHeader>
|
totalOsps,
|
||||||
<CardTitle>Actividad Productiva</CardTitle>
|
totalProducers,
|
||||||
<CardDescription>Distribución por tipo de actividad</CardDescription>
|
statusDistribution,
|
||||||
</CardHeader>
|
activityDistribution,
|
||||||
<CardContent className="h-[400px]">
|
typeDistribution,
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
stateDistribution,
|
||||||
<BarChart
|
yearDistribution,
|
||||||
data={activityDistribution}
|
} = data;
|
||||||
layout="vertical"
|
|
||||||
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
|
|
||||||
>
|
|
||||||
<CartesianGrid strokeDasharray="3 3" />
|
|
||||||
<XAxis type="number" />
|
|
||||||
<YAxis dataKey="name" type="category" width={150} />
|
|
||||||
<Tooltip wrapperStyle={{ color: '#000' }} />
|
|
||||||
<Legend />
|
|
||||||
<Bar dataKey="value" fill="#8884d8" name="Cantidad" />
|
|
||||||
</BarChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* State Distribution */}
|
const COLORS = [
|
||||||
<Card className="col-span-full">
|
'#0088FE',
|
||||||
|
'#00C49F',
|
||||||
|
'#FFBB28',
|
||||||
|
'#FF8042',
|
||||||
|
'#8884d8',
|
||||||
|
'#82ca9d',
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Filters Section */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Filtros</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm font-medium">Fecha Inicio</label>
|
||||||
|
<Input
|
||||||
|
type="date"
|
||||||
|
value={startDate}
|
||||||
|
onChange={(e) => setStartDate(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm font-medium">Fecha Fin</label>
|
||||||
|
<Input
|
||||||
|
type="date"
|
||||||
|
value={endDate}
|
||||||
|
onChange={(e) => setEndDate(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm font-medium">Estado</label>
|
||||||
|
<SelectSearchable
|
||||||
|
options={stateOptions.map((item) => ({
|
||||||
|
value: item.id.toString(),
|
||||||
|
label: item.name,
|
||||||
|
}))}
|
||||||
|
onValueChange={(value: any) => {
|
||||||
|
setStateId(Number(value));
|
||||||
|
setMunicipalityId(0); // Reset municipality
|
||||||
|
setParishId(0); // Reset parish
|
||||||
|
}}
|
||||||
|
placeholder="Selecciona un estado"
|
||||||
|
defaultValue={stateId ? stateId.toString() : ''}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm font-medium">Municipio</label>
|
||||||
|
<SelectSearchable
|
||||||
|
options={municipalityOptions.map((item) => ({
|
||||||
|
value: item.id.toString(),
|
||||||
|
label: item.name,
|
||||||
|
}))}
|
||||||
|
onValueChange={(value: any) => {
|
||||||
|
setMunicipalityId(Number(value));
|
||||||
|
setParishId(0);
|
||||||
|
}}
|
||||||
|
placeholder="Selecciona municipio"
|
||||||
|
defaultValue={municipalityId ? municipalityId.toString() : ''}
|
||||||
|
disabled={!stateId || stateId === 0}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm font-medium">Parroquia</label>
|
||||||
|
<SelectSearchable
|
||||||
|
options={parishOptions.map((item) => ({
|
||||||
|
value: item.id.toString(),
|
||||||
|
label: item.name,
|
||||||
|
}))}
|
||||||
|
onValueChange={(value: any) => setParishId(Number(value))}
|
||||||
|
placeholder="Selecciona parroquia"
|
||||||
|
defaultValue={parishId ? parishId.toString() : ''}
|
||||||
|
disabled={!municipalityId || municipalityId === 0}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm font-medium">Tipo de OSP</label>
|
||||||
|
<Select value={ospType} onValueChange={setOspType}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Todos" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">Todos</SelectItem>
|
||||||
|
{OSP_TYPES.map((type) => (
|
||||||
|
<SelectItem key={type} value={type}>
|
||||||
|
{type}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-end">
|
||||||
|
<Button variant="outline" onClick={handleClearFilters}>
|
||||||
|
Limpiar Filtros
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Statistics Cards */}
|
||||||
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">
|
||||||
|
Total de OSP Registradas
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">{totalOsps}</div>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Organizaciones Socioproductivas
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">
|
||||||
|
Total de Productores
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">{totalProducers}</div>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Productores asociados
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="col-span-full">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Actividad Productiva</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Distribución por tipo de actividad
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="h-[400px]">
|
||||||
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
|
<BarChart
|
||||||
|
data={activityDistribution}
|
||||||
|
layout="vertical"
|
||||||
|
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
|
||||||
|
>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" />
|
||||||
|
<XAxis type="number" />
|
||||||
|
<YAxis dataKey="name" type="category" width={150} />
|
||||||
|
<Tooltip wrapperStyle={{ color: '#000' }} />
|
||||||
|
<Legend />
|
||||||
|
<Bar dataKey="value" fill="#8884d8" name="Cantidad" />
|
||||||
|
</BarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* State Distribution */}
|
||||||
|
{/* <Card className="col-span-full">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Distribución por Estado</CardTitle>
|
<CardTitle>Distribución por Estado</CardTitle>
|
||||||
<CardDescription>OSP registradas por estado</CardDescription>
|
<CardDescription>OSP registradas por estado</CardDescription>
|
||||||
@@ -239,81 +290,84 @@ export function TrainingStatistics() {
|
|||||||
</BarChart>
|
</BarChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card> */}
|
||||||
|
|
||||||
{/* Year Distribution */}
|
{/* Year Distribution */}
|
||||||
<Card className="col-span-full lg:col-span-1">
|
<Card className="col-span-full lg:col-span-1">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Año de Constitución</CardTitle>
|
<CardTitle>Año de Constitución</CardTitle>
|
||||||
<CardDescription>Año de registro de la empresa</CardDescription>
|
<CardDescription>Año de registro de la empresa</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="h-[400px]">
|
<CardContent className="h-[400px]">
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
<BarChart
|
<BarChart
|
||||||
data={yearDistribution}
|
data={yearDistribution}
|
||||||
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
|
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
|
||||||
>
|
>
|
||||||
<CartesianGrid strokeDasharray="3 3" />
|
<CartesianGrid strokeDasharray="3 3" />
|
||||||
<XAxis dataKey="name" />
|
<XAxis dataKey="name" />
|
||||||
<YAxis />
|
<YAxis />
|
||||||
<Tooltip wrapperStyle={{ color: '#000' }} />
|
<Tooltip wrapperStyle={{ color: '#000' }} />
|
||||||
<Legend />
|
<Legend />
|
||||||
<Bar dataKey="value" fill="#FFBB28" name="Cantidad" />
|
<Bar dataKey="value" fill="#FFBB28" name="Cantidad" />
|
||||||
</BarChart>
|
</BarChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card className="col-span-1 md:col-span-2 lg:col-span-1">
|
<Card className="col-span-1 md:col-span-2 lg:col-span-1">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Estatus Actual</CardTitle>
|
<CardTitle>Estatus Actual</CardTitle>
|
||||||
<CardDescription>Estado operativo de las OSP</CardDescription>
|
<CardDescription>Estado operativo de las OSP</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="h-[300px]">
|
<CardContent className="h-[300px]">
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
<PieChart>
|
<PieChart>
|
||||||
<Pie
|
<Pie
|
||||||
data={statusDistribution}
|
data={statusDistribution}
|
||||||
cx="50%"
|
cx="50%"
|
||||||
cy="50%"
|
cy="50%"
|
||||||
innerRadius={60}
|
innerRadius={60}
|
||||||
outerRadius={80}
|
outerRadius={80}
|
||||||
fill="#8884d8"
|
fill="#8884d8"
|
||||||
paddingAngle={5}
|
paddingAngle={5}
|
||||||
dataKey="value"
|
dataKey="value"
|
||||||
>
|
>
|
||||||
{statusDistribution.map((entry, index) => (
|
{statusDistribution.map((entry, index) => (
|
||||||
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
|
<Cell
|
||||||
))}
|
key={`cell-${index}`}
|
||||||
</Pie>
|
fill={COLORS[index % COLORS.length]}
|
||||||
<Tooltip wrapperStyle={{ color: '#000' }} />
|
/>
|
||||||
<Legend />
|
))}
|
||||||
</PieChart>
|
</Pie>
|
||||||
</ResponsiveContainer>
|
<Tooltip wrapperStyle={{ color: '#000' }} />
|
||||||
</CardContent>
|
<Legend />
|
||||||
</Card>
|
</PieChart>
|
||||||
<Card className="col-span-1 md:col-span-2 lg:col-span-1">
|
</ResponsiveContainer>
|
||||||
<CardHeader>
|
</CardContent>
|
||||||
<CardTitle>Tipo de Organización</CardTitle>
|
</Card>
|
||||||
<CardDescription>Clasificación de las OSP</CardDescription>
|
<Card className="col-span-1 md:col-span-2 lg:col-span-1">
|
||||||
</CardHeader>
|
<CardHeader>
|
||||||
<CardContent className="h-[300px]">
|
<CardTitle>Tipo de Organización</CardTitle>
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
<CardDescription>Clasificación de las OSP</CardDescription>
|
||||||
<BarChart
|
</CardHeader>
|
||||||
data={typeDistribution}
|
<CardContent className="h-[300px]">
|
||||||
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
>
|
<BarChart
|
||||||
<CartesianGrid strokeDasharray="3 3" />
|
data={typeDistribution}
|
||||||
<XAxis dataKey="name" />
|
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
|
||||||
<YAxis />
|
>
|
||||||
<Tooltip wrapperStyle={{ color: '#000' }} />
|
<CartesianGrid strokeDasharray="3 3" />
|
||||||
<Legend />
|
<XAxis dataKey="name" />
|
||||||
<Bar dataKey="value" fill="#82ca9d" name="Cantidad" />
|
<YAxis />
|
||||||
</BarChart>
|
<Tooltip wrapperStyle={{ color: '#000' }} />
|
||||||
</ResponsiveContainer>
|
<Legend />
|
||||||
</CardContent>
|
<Bar dataKey="value" fill="#82ca9d" name="Cantidad" />
|
||||||
</Card>
|
</BarChart>
|
||||||
</div>
|
</ResponsiveContainer>
|
||||||
</div>
|
</CardContent>
|
||||||
);
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,16 @@ import {
|
|||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from '@repo/shadcn/components/ui/dialog';
|
} from '@repo/shadcn/components/ui/dialog';
|
||||||
import { X } from 'lucide-react';
|
import { ScrollArea } from '@repo/shadcn/components/ui/scroll-area';
|
||||||
|
import { Separator } from '@repo/shadcn/components/ui/separator';
|
||||||
|
import {
|
||||||
|
ExternalLink,
|
||||||
|
Factory,
|
||||||
|
MapPin,
|
||||||
|
Package,
|
||||||
|
Wrench,
|
||||||
|
X,
|
||||||
|
} from 'lucide-react';
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { TrainingSchema } from '../schemas/training';
|
import { TrainingSchema } from '../schemas/training';
|
||||||
|
|
||||||
@@ -37,235 +46,447 @@ export function TrainingViewModal({
|
|||||||
|
|
||||||
const DetailItem = ({ label, value }: { label: string; value: any }) => (
|
const DetailItem = ({ label, value }: { label: string; value: any }) => (
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<p className="text-sm font-medium text-muted-foreground">{label}</p>
|
<p className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
||||||
<p className="text-sm font-semibold">{value || 'N/A'}</p>
|
{label}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm font-semibold text-foreground break-words">
|
||||||
|
{value !== null && value !== undefined && value !== '' ? value : 'N/A'}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const Section = ({
|
const Section = ({
|
||||||
title,
|
title,
|
||||||
|
icon: Icon,
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
title: string;
|
title: string;
|
||||||
|
icon?: React.ElementType;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) => (
|
}) => (
|
||||||
<Card className="mb-4">
|
<Card className="overflow-hidden border-l-4 border-l-primary/20">
|
||||||
<CardHeader className="py-3">
|
<CardHeader className="py-3 bg-muted/30">
|
||||||
<CardTitle className="text-lg">{title}</CardTitle>
|
<CardTitle className="text-lg flex items-center gap-2">
|
||||||
|
{Icon && <Icon className="h-5 w-5 text-primary" />}
|
||||||
|
{title}
|
||||||
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
<CardContent className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-x-4 gap-y-6 pt-4">
|
||||||
{children}
|
{children}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const BooleanBadge = ({ value }: { value?: boolean }) => (
|
||||||
|
<Badge variant={value ? 'default' : 'secondary'}>
|
||||||
|
{value ? 'Sí' : 'No'}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||||
<DialogContent className="sm:max-w-[800px] overflow-y-auto [&>button:last-child]:hidden">
|
<DialogContent className="sm:max-w-[1000px] max-h-[90vh] p-0 flex flex-col">
|
||||||
<DialogHeader>
|
<DialogHeader className="px-6 py-4 border-b">
|
||||||
<DialogTitle className="text-2xl font-bold">
|
<DialogTitle className="text-2xl font-bold flex items-center gap-2">
|
||||||
Detalle de la Organización Socioproductiva
|
<Factory className="h-6 w-6" />
|
||||||
|
{data.ospName}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription className="sr-only">
|
<DialogDescription>
|
||||||
Resumen detallado de la información de la organización
|
{data.ospType} • {data.ospRif} •{' '}
|
||||||
socioproductiva incluyendo ubicación, responsable y registro
|
<span
|
||||||
fotográfico.
|
className={
|
||||||
|
data.currentStatus === 'ACTIVA'
|
||||||
|
? 'text-green-600 font-medium'
|
||||||
|
: 'text-red-600'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{data.currentStatus}
|
||||||
|
</span>
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="mt-4 space-y-6">
|
<ScrollArea className="flex-1 px-6 py-6">
|
||||||
{/* 1. Datos de la visita */}
|
<div className="space-y-8">
|
||||||
<Section title="1. Datos de la visita">
|
{/* 1. Datos de la Visita */}
|
||||||
<DetailItem
|
<Section title="Datos de la Visita">
|
||||||
label="Nombre del Coordinador"
|
<DetailItem
|
||||||
value={data.firstname}
|
label="Coordinador"
|
||||||
/>
|
value={`${data.firstname} ${data.lastname}`}
|
||||||
<DetailItem
|
/>
|
||||||
label="Apellido del Coordinador"
|
<DetailItem label="Teléfono Coord." value={data.coorPhone} />
|
||||||
value={data.lastname}
|
<DetailItem
|
||||||
/>
|
label="Fecha Visita"
|
||||||
<DetailItem
|
value={
|
||||||
label="Fecha y hora de la visita"
|
data.visitDate
|
||||||
value={
|
? new Date(data.visitDate).toLocaleString()
|
||||||
data.visitDate
|
: 'N/A'
|
||||||
? new Date(data.visitDate).toLocaleString()
|
}
|
||||||
: 'N/A'
|
/>
|
||||||
}
|
</Section>
|
||||||
/>
|
|
||||||
</Section>
|
|
||||||
|
|
||||||
{/* 2. Datos de la OSP */}
|
{/* 2. Sectores y Actividad */}
|
||||||
<Section title="2. Datos de la OSP">
|
<Section title="Sectores Económicos">
|
||||||
<DetailItem label="Nombre" value={data.ospName} />
|
<DetailItem label="Sector Económico" value={data.ecoSector} />
|
||||||
<DetailItem label="RIF" value={data.ospRif} />
|
|
||||||
<DetailItem label="Tipo" value={data.ospType} />
|
|
||||||
<DetailItem
|
|
||||||
label="Actividad Productiva"
|
|
||||||
value={data.productiveActivity}
|
|
||||||
/>
|
|
||||||
<DetailItem
|
|
||||||
label="Estatus"
|
|
||||||
value={
|
|
||||||
<Badge
|
|
||||||
variant={
|
|
||||||
data.currentStatus === 'ACTIVA' ? 'default' : 'secondary'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{data.currentStatus}
|
|
||||||
</Badge>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<DetailItem
|
|
||||||
label="Año de Constitución"
|
|
||||||
value={data.companyConstitutionYear}
|
|
||||||
/>
|
|
||||||
<DetailItem
|
|
||||||
label="Cant. Productores"
|
|
||||||
value={data.producerCount}
|
|
||||||
/>
|
|
||||||
<DetailItem label="Cant. Productos" value={data.productCount} />
|
|
||||||
<div className="col-span-1 md:col-span-2 lg:col-span-3">
|
|
||||||
<DetailItem
|
<DetailItem
|
||||||
label="Descripción del Producto"
|
label="Sector Productivo"
|
||||||
value={data.productDescription}
|
value={data.productiveSector}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
<DetailItem
|
|
||||||
label="Capacidad Instalada"
|
|
||||||
value={data.installedCapacity}
|
|
||||||
/>
|
|
||||||
<DetailItem
|
|
||||||
label="Capacidad Operativa"
|
|
||||||
value={data.operationalCapacity}
|
|
||||||
/>
|
|
||||||
<div className="col-span-1 md:col-span-2 lg:col-span-3">
|
|
||||||
<DetailItem
|
<DetailItem
|
||||||
label="Requerimiento Financiero"
|
label="Actividad Central"
|
||||||
value={data.financialRequirementDescription}
|
value={data.centralProductiveActivity}
|
||||||
/>
|
/>
|
||||||
</div>
|
<DetailItem
|
||||||
{data.paralysisReason && (
|
label="Actividad Principal"
|
||||||
<div className="col-span-1 md:col-span-2 lg:col-span-3">
|
value={data.mainProductiveActivity}
|
||||||
|
/>
|
||||||
|
<div className="col-span-full">
|
||||||
<DetailItem
|
<DetailItem
|
||||||
label="Razones de paralización"
|
label="Actividad Específica"
|
||||||
value={data.paralysisReason}
|
value={data.productiveActivity}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
</Section>
|
||||||
</Section>
|
|
||||||
|
|
||||||
{/* 3. Ubicación */}
|
{/* 3. Infraestructura y Ubicación */}
|
||||||
<Section title="3. Detalles de la ubicación">
|
<Section title="Infraestructura y Ubicación" icon={MapPin}>
|
||||||
<DetailItem
|
<DetailItem
|
||||||
label="Código SITUR Comuna"
|
label="Año Constitución"
|
||||||
value={data.siturCodeCommune}
|
value={data.companyConstitutionYear}
|
||||||
/>
|
/>
|
||||||
<DetailItem
|
<DetailItem
|
||||||
label="Consejo Comunal"
|
label="Infraestructura (m²)"
|
||||||
value={data.communalCouncil}
|
value={data.infrastructureMt2}
|
||||||
/>
|
/>
|
||||||
<DetailItem
|
<DetailItem
|
||||||
label="Código SITUR Consejo Comunal"
|
label="Tipo Estructura"
|
||||||
value={data.siturCodeCommunalCouncil}
|
value={data.structureType}
|
||||||
/>
|
/>
|
||||||
<div className="col-span-1 md:col-span-2 lg:col-span-3">
|
|
||||||
<DetailItem label="Dirección OSP" value={data.ospAddress} />
|
|
||||||
</div>
|
|
||||||
</Section>
|
|
||||||
|
|
||||||
{/* 4. Responsable */}
|
<DetailItem
|
||||||
<Section title="4. Datos del Responsable">
|
label="Posee Transporte"
|
||||||
<DetailItem
|
value={<BooleanBadge value={data.hasTransport} />}
|
||||||
label="Nombre Completo"
|
/>
|
||||||
value={data.ospResponsibleFullname}
|
<DetailItem
|
||||||
/>
|
label="Espacio Abierto"
|
||||||
<DetailItem label="Cédula" value={data.ospResponsibleCedula} />
|
value={<BooleanBadge value={data.isOpenSpace} />}
|
||||||
<DetailItem label="RIF" value={data.ospResponsibleRif} />
|
/>
|
||||||
<DetailItem label="Estado Civil" value={data.civilState} />
|
|
||||||
<DetailItem label="Teléfono" value={data.ospResponsiblePhone} />
|
|
||||||
<DetailItem label="Email" value={data.ospResponsibleEmail} />
|
|
||||||
<DetailItem label="Carga Familiar" value={data.familyBurden} />
|
|
||||||
<DetailItem
|
|
||||||
label="Número de Hijos"
|
|
||||||
value={data.numberOfChildren}
|
|
||||||
/>
|
|
||||||
</Section>
|
|
||||||
|
|
||||||
{/* 5. Observaciones */}
|
<div className="col-span-full space-y-4 mt-2">
|
||||||
<Card>
|
<div className="space-y-1">
|
||||||
<CardHeader className="py-3">
|
<p className="text-xs font-medium text-muted-foreground uppercase">
|
||||||
<CardTitle className="text-lg">
|
Dirección
|
||||||
5. Observaciones Generales
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<p className="text-sm whitespace-pre-wrap">
|
|
||||||
{data.generalObservations || 'Sin observaciones'}
|
|
||||||
</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* 6. Registro fotográfico */}
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="py-3">
|
|
||||||
<CardTitle className="text-lg">
|
|
||||||
6. Registro fotográfico
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
|
|
||||||
{[data.photo1, data.photo2, data.photo3].map(
|
|
||||||
(photo, idx) =>
|
|
||||||
photo && (
|
|
||||||
<div
|
|
||||||
key={idx}
|
|
||||||
className="relative aspect-video rounded-md overflow-hidden cursor-pointer hover:opacity-90 transition-opacity bg-muted"
|
|
||||||
onClick={() => setSelectedImage(photo)}
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src={`${process.env.NEXT_PUBLIC_API_URL}${photo}`}
|
|
||||||
alt={`Registro ${idx + 1}`}
|
|
||||||
className="object-cover w-full h-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
)}
|
|
||||||
{![data.photo1, data.photo2, data.photo3].some(Boolean) && (
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
No hay registro fotográfico
|
|
||||||
</p>
|
</p>
|
||||||
|
<p className="text-sm font-medium">{data.ospAddress}</p>
|
||||||
|
</div>
|
||||||
|
{data.ospGoogleMapsLink && (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
asChild
|
||||||
|
className="gap-2"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href={data.ospGoogleMapsLink}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
<MapPin className="h-4 w-4" />
|
||||||
|
Ver en Google Maps
|
||||||
|
<ExternalLink className="h-3 w-3" />
|
||||||
|
</a>
|
||||||
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</Section>
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DialogFooter className="mt-6">
|
{/* 4. LISTAS DETALLADAS (Lo nuevo) */}
|
||||||
<Button onClick={onClose} variant="outline" className="w-32">
|
|
||||||
|
{/* PRODUCTOS */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="pb-3">
|
||||||
|
<CardTitle className="text-lg flex items-center gap-2">
|
||||||
|
<Package className="h-5 w-5" />
|
||||||
|
Productos y Mano de Obra
|
||||||
|
<Badge variant="secondary" className="ml-2">
|
||||||
|
{data.productList?.length || 0}
|
||||||
|
</Badge>
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="grid gap-4">
|
||||||
|
{data.productList?.map((prod: any, idx: number) => (
|
||||||
|
<div
|
||||||
|
key={idx}
|
||||||
|
className="bg-muted/40 p-4 rounded-lg border text-sm"
|
||||||
|
>
|
||||||
|
<div className="flex justify-between items-start mb-2">
|
||||||
|
<h4 className="font-bold text-base text-primary">
|
||||||
|
{prod.productName}
|
||||||
|
</h4>
|
||||||
|
<Badge variant="outline">
|
||||||
|
Mano de obra:{' '}
|
||||||
|
{Number(prod.menCount || 0) +
|
||||||
|
Number(prod.womenCount || 0)}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<p className="text-muted-foreground mb-3">
|
||||||
|
{prod.description}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-3">
|
||||||
|
<DetailItem label="Diario" value={prod.dailyCount} />
|
||||||
|
<DetailItem label="Semanal" value={prod.weeklyCount} />
|
||||||
|
<DetailItem label="Mensual" value={prod.monthlyCount} />
|
||||||
|
<DetailItem
|
||||||
|
label="Hombres / Mujeres"
|
||||||
|
value={`${prod.menCount || 0} / ${prod.womenCount || 0}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Detalles de distribución si existen */}
|
||||||
|
{(prod.internalQuantity || prod.externalQuantity) && (
|
||||||
|
<>
|
||||||
|
<Separator className="my-2" />
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
{prod.internalQuantity && (
|
||||||
|
<div>
|
||||||
|
<span className="text-xs font-bold text-muted-foreground block mb-1">
|
||||||
|
DISTRIBUCIÓN INTERNA
|
||||||
|
</span>
|
||||||
|
<p>Cant: {prod.internalQuantity}</p>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{prod.internalDescription}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{prod.externalQuantity && (
|
||||||
|
<div>
|
||||||
|
<span className="text-xs font-bold text-muted-foreground block mb-1">
|
||||||
|
EXPORTACIÓN ({prod.externalCountry})
|
||||||
|
</span>
|
||||||
|
<p>Cant: {prod.externalQuantity}</p>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{prod.externalDescription}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{(!data.productList || data.productList.length === 0) && (
|
||||||
|
<p className="text-sm text-muted-foreground italic">
|
||||||
|
No hay productos registrados.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* EQUIPAMIENTO Y PRODUCCIÓN */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="pb-3">
|
||||||
|
<CardTitle className="text-lg flex items-center gap-2">
|
||||||
|
<Wrench className="h-5 w-5" />
|
||||||
|
Equipamiento
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-3">
|
||||||
|
{data.equipmentList?.map((eq: any, idx: number) => (
|
||||||
|
<div
|
||||||
|
key={idx}
|
||||||
|
className="flex justify-between items-center p-2 rounded bg-muted/40 border"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<p className="font-medium">{eq.machine}</p>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{eq.specifications}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="text-sm font-bold h-8 w-8 flex items-center justify-center rounded-full"
|
||||||
|
>
|
||||||
|
{eq.quantity}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{(!data.equipmentList ||
|
||||||
|
data.equipmentList.length === 0) && (
|
||||||
|
<p className="text-sm text-muted-foreground italic">
|
||||||
|
No hay equipamiento registrado.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="pb-3">
|
||||||
|
<CardTitle className="text-lg flex items-center gap-2">
|
||||||
|
<Factory className="h-5 w-5" />
|
||||||
|
Materia Prima
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-3">
|
||||||
|
{data.productionList?.map((mat: any, idx: number) => (
|
||||||
|
<div
|
||||||
|
key={idx}
|
||||||
|
className="flex justify-between items-center p-2 rounded bg-muted/40 border"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<p className="font-medium">{mat.rawMaterial}</p>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{mat.supplyType}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Badge variant="secondary">Cant: {mat.quantity}</Badge>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{(!data.productionList ||
|
||||||
|
data.productionList.length === 0) && (
|
||||||
|
<p className="text-sm text-muted-foreground italic">
|
||||||
|
No hay materia prima registrada.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 5. Comuna y Responsable */}
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
|
<Section title="Datos de la Comuna">
|
||||||
|
<DetailItem label="Comuna" value={data.communeName} />
|
||||||
|
<DetailItem
|
||||||
|
label="Código SITUR"
|
||||||
|
value={data.siturCodeCommune}
|
||||||
|
/>
|
||||||
|
<DetailItem
|
||||||
|
label="Vocero"
|
||||||
|
value={data.communeSpokespersonName}
|
||||||
|
/>
|
||||||
|
<DetailItem
|
||||||
|
label="Teléfono"
|
||||||
|
value={data.communeSpokespersonPhone}
|
||||||
|
/>
|
||||||
|
<div className="col-span-full border-t pt-4 mt-2">
|
||||||
|
<DetailItem
|
||||||
|
label="Consejo Comunal"
|
||||||
|
value={data.communalCouncil}
|
||||||
|
/>
|
||||||
|
<DetailItem
|
||||||
|
label="Vocero C.C."
|
||||||
|
value={data.communalCouncilSpokespersonName}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section title="Responsable OSP">
|
||||||
|
<DetailItem
|
||||||
|
label="Nombre"
|
||||||
|
value={data.ospResponsibleFullname}
|
||||||
|
/>
|
||||||
|
<DetailItem
|
||||||
|
label="Cédula"
|
||||||
|
value={data.ospResponsibleCedula}
|
||||||
|
/>
|
||||||
|
<DetailItem
|
||||||
|
label="Teléfono"
|
||||||
|
value={data.ospResponsiblePhone}
|
||||||
|
/>
|
||||||
|
<DetailItem label="Email" value={data.ospResponsibleEmail} />
|
||||||
|
<DetailItem
|
||||||
|
label="Carga Familiar"
|
||||||
|
value={data.familyBurden}
|
||||||
|
/>
|
||||||
|
<DetailItem label="Hijos" value={data.numberOfChildren} />
|
||||||
|
</Section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 6. Observaciones */}
|
||||||
|
{(data.generalObservations || data.paralysisReason) && (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Observaciones</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
{data.generalObservations && (
|
||||||
|
<div>
|
||||||
|
<p className="text-xs font-bold text-muted-foreground uppercase mb-1">
|
||||||
|
Generales
|
||||||
|
</p>
|
||||||
|
<p className="text-sm">{data.generalObservations}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{data.paralysisReason && (
|
||||||
|
<div className="p-3 bg-red-50 dark:bg-red-900/20 rounded-md border border-red-200 dark:border-red-900">
|
||||||
|
<p className="text-xs font-bold text-red-600 dark:text-red-400 uppercase mb-1">
|
||||||
|
Motivo Paralización
|
||||||
|
</p>
|
||||||
|
<p className="text-sm">{data.paralysisReason}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 7. Fotos */}
|
||||||
|
<Section title="Registro Fotográfico">
|
||||||
|
{[data.photo1, data.photo2, data.photo3].some(Boolean) ? (
|
||||||
|
<div className="col-span-full grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||||
|
{[data.photo1, data.photo2, data.photo3].map(
|
||||||
|
(photo, idx) =>
|
||||||
|
photo && (
|
||||||
|
<div
|
||||||
|
key={idx}
|
||||||
|
className="relative aspect-video rounded-lg overflow-hidden cursor-zoom-in border hover:shadow-lg transition-all"
|
||||||
|
onClick={() => setSelectedImage(photo)}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={`${process.env.NEXT_PUBLIC_API_URL}${photo}`}
|
||||||
|
alt={`Evidencia ${idx + 1}`}
|
||||||
|
className="object-cover w-full h-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="text-sm text-muted-foreground col-span-full">
|
||||||
|
No hay imágenes cargadas.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</Section>
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
|
||||||
|
<DialogFooter className="px-6 py-4 border-t bg-muted/20">
|
||||||
|
<Button
|
||||||
|
onClick={onClose}
|
||||||
|
variant="outline"
|
||||||
|
className="w-full sm:w-auto"
|
||||||
|
>
|
||||||
Cerrar
|
Cerrar
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
{/* Lightbox */}
|
{/* Lightbox para imágenes */}
|
||||||
<Dialog
|
<Dialog
|
||||||
open={!!selectedImage}
|
open={!!selectedImage}
|
||||||
onOpenChange={() => setSelectedImage(null)}
|
onOpenChange={() => setSelectedImage(null)}
|
||||||
>
|
>
|
||||||
<DialogContent className="max-w-[95vw] max-h-[95vh] p-0 overflow-hidden bg-black/90 border-none [&>button:last-child]:hidden">
|
<DialogContent className="max-w-[95vw] max-h-[95vh] p-0 overflow-hidden bg-black/95 border-none">
|
||||||
<DialogHeader className="sr-only">
|
<DialogHeader className="sr-only">
|
||||||
<DialogTitle>Vista ampliada de la imagen</DialogTitle>
|
<DialogTitle>Imagen Ampliada</DialogTitle>
|
||||||
<DialogDescription>
|
|
||||||
Imagen ampliada del registro fotográfico de la organización.
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="relative w-full h-full flex items-center justify-center p-4">
|
<DialogDescription></DialogDescription>
|
||||||
|
<div className="relative w-full h-full flex items-center justify-center p-2">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="absolute top-2 right-2 text-white hover:bg-white/20 z-10"
|
className="absolute top-4 right-4 text-white hover:bg-white/20 rounded-full z-50"
|
||||||
onClick={() => setSelectedImage(null)}
|
onClick={() => setSelectedImage(null)}
|
||||||
>
|
>
|
||||||
<X className="h-6 w-6" />
|
<X className="h-6 w-6" />
|
||||||
@@ -273,8 +494,8 @@ export function TrainingViewModal({
|
|||||||
{selectedImage && (
|
{selectedImage && (
|
||||||
<img
|
<img
|
||||||
src={`${process.env.NEXT_PUBLIC_API_URL}${selectedImage}`}
|
src={`${process.env.NEXT_PUBLIC_API_URL}${selectedImage}`}
|
||||||
alt="Expanded view"
|
alt="Vista ampliada"
|
||||||
className="max-w-full max-h-[90vh] object-contain"
|
className="max-w-full max-h-[90vh] object-contain rounded-md"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
169
apps/web/feactures/training/constants/osp-data.ts
Normal file
169
apps/web/feactures/training/constants/osp-data.ts
Normal 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',
|
||||||
|
],
|
||||||
|
};
|
||||||
@@ -1,23 +1,27 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
export const statisticsItemSchema = z.object({
|
export const statisticsItemSchema = z.object({
|
||||||
name: z.string(),
|
name: z
|
||||||
value: z.number(),
|
.string()
|
||||||
|
.nullable()
|
||||||
|
.transform((val) => val || 'Sin Información'),
|
||||||
|
value: z.number(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const trainingStatisticsSchema = z.object({
|
export const trainingStatisticsSchema = z.object({
|
||||||
totalOsps: z.number(),
|
totalOsps: z.number(),
|
||||||
totalProducers: z.number(),
|
totalProducers: z.number(),
|
||||||
statusDistribution: z.array(statisticsItemSchema),
|
totalProducts: z.number(),
|
||||||
activityDistribution: z.array(statisticsItemSchema),
|
statusDistribution: z.array(statisticsItemSchema),
|
||||||
typeDistribution: z.array(statisticsItemSchema),
|
activityDistribution: z.array(statisticsItemSchema),
|
||||||
stateDistribution: z.array(statisticsItemSchema),
|
typeDistribution: z.array(statisticsItemSchema),
|
||||||
yearDistribution: z.array(statisticsItemSchema),
|
stateDistribution: z.array(statisticsItemSchema),
|
||||||
|
yearDistribution: z.array(statisticsItemSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TrainingStatisticsData = z.infer<typeof trainingStatisticsSchema>;
|
export type TrainingStatisticsData = z.infer<typeof trainingStatisticsSchema>;
|
||||||
|
|
||||||
export const trainingStatisticsResponseSchema = z.object({
|
export const trainingStatisticsResponseSchema = z.object({
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
data: trainingStatisticsSchema,
|
data: trainingStatisticsSchema,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user