nuevas correciones al formulario y esquema base de datos para osp

This commit is contained in:
2026-02-25 12:17:33 -04:00
parent a88cf94adb
commit f910aea3cc
15 changed files with 4889 additions and 731 deletions

View File

@@ -108,10 +108,19 @@ export class MinioService implements OnModuleInit {
async delete(objectName: string): Promise<void> { async delete(objectName: string): Promise<void> {
try { try {
await this.minioClient.removeObject(this.bucketName, objectName); // Ensure we don't have a leading slash which can cause issues with removeObject
this.logger.log(`Object "${objectName}" deleted successfully.`); const cleanedName = objectName.startsWith('/')
? objectName.slice(1)
: objectName;
await this.minioClient.removeObject(this.bucketName, cleanedName);
this.logger.log(
`Object "${cleanedName}" deleted successfully from bucket "${this.bucketName}".`,
);
} catch (error: any) { } catch (error: any) {
this.logger.error(`Error deleting file: ${error.message}`); this.logger.error(
`Error deleting file "${objectName}": ${error.message}`,
);
// We don't necessarily want to throw if the file is already gone // We don't necessarily want to throw if the file is already gone
} }
} }

View File

@@ -0,0 +1,9 @@
ALTER TABLE "training_surveys" ADD COLUMN "internal_distribution_zone" text;--> statement-breakpoint
ALTER TABLE "training_surveys" ADD COLUMN "is_exporting" boolean DEFAULT false NOT NULL;--> statement-breakpoint
ALTER TABLE "training_surveys" ADD COLUMN "external_country" text;--> statement-breakpoint
ALTER TABLE "training_surveys" ADD COLUMN "external_city" text;--> statement-breakpoint
ALTER TABLE "training_surveys" ADD COLUMN "external_description" text;--> statement-breakpoint
ALTER TABLE "training_surveys" ADD COLUMN "external_quantity" text;--> statement-breakpoint
ALTER TABLE "training_surveys" ADD COLUMN "external_unit" text;--> statement-breakpoint
ALTER TABLE "training_surveys" ADD COLUMN "women_count" integer DEFAULT 0 NOT NULL;--> statement-breakpoint
ALTER TABLE "training_surveys" ADD COLUMN "men_count" integer DEFAULT 0 NOT NULL;

View File

@@ -0,0 +1,5 @@
DROP INDEX "training_surveys_index_00";--> statement-breakpoint
ALTER TABLE "training_surveys" ADD COLUMN "coor_full_name" text NOT NULL;--> statement-breakpoint
CREATE INDEX "training_surveys_index_00" ON "training_surveys" USING btree ("coor_full_name");--> statement-breakpoint
ALTER TABLE "training_surveys" DROP COLUMN "firstname";--> statement-breakpoint
ALTER TABLE "training_surveys" DROP COLUMN "lastname";

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -155,6 +155,20 @@
"when": 1771901546945, "when": 1771901546945,
"tag": "0021_warm_machine_man", "tag": "0021_warm_machine_man",
"breakpoints": true "breakpoints": true
},
{
"idx": 22,
"version": "7",
"when": 1772031518006,
"tag": "0022_nervous_dragon_lord",
"breakpoints": true
},
{
"idx": 23,
"version": "7",
"when": 1772032122473,
"tag": "0023_sticky_slayback",
"breakpoints": true
} }
] ]
} }

View File

@@ -48,8 +48,7 @@ export const trainingSurveys = t.pgTable(
{ {
// === 1. IDENTIFICADORES Y DATOS DE VISITA === // === 1. IDENTIFICADORES Y DATOS DE VISITA ===
id: t.serial('id').primaryKey(), id: t.serial('id').primaryKey(),
firstname: t.text('firstname').notNull(), coorFullName: t.text('coor_full_name').notNull(),
lastname: t.text('lastname').notNull(),
visitDate: t.timestamp('visit_date').notNull(), visitDate: t.timestamp('visit_date').notNull(),
coorPhone: t.text('coor_phone'), coorPhone: t.text('coor_phone'),
@@ -133,6 +132,20 @@ export const trainingSurveys = t.pgTable(
familyBurden: t.integer('family_burden'), familyBurden: t.integer('family_burden'),
numberOfChildren: t.integer('number_of_children'), numberOfChildren: t.integer('number_of_children'),
generalObservations: t.text('general_observations'), generalObservations: t.text('general_observations'),
// === 4. DATOS DE DISTRIBUCIÓN Y EXPORTACIÓN ===
internalDistributionZone: t.text('internal_distribution_zone'),
isExporting: t.boolean('is_exporting').notNull().default(false),
externalCountry: t.text('external_country'),
externalCity: t.text('external_city'),
externalDescription: t.text('external_description'),
externalQuantity: t.text('external_quantity'),
externalUnit: t.text('external_unit'),
// === 5. MANO DE OBRA ===
womenCount: t.integer('women_count').notNull().default(0),
menCount: t.integer('men_count').notNull().default(0),
// Fotos // Fotos
photo1: t.text('photo1'), photo1: t.text('photo1'),
photo2: t.text('photo2'), photo2: t.text('photo2'),
@@ -149,7 +162,7 @@ export const trainingSurveys = t.pgTable(
(trainingSurveys) => ({ (trainingSurveys) => ({
trainingSurveysIndex: t trainingSurveysIndex: t
.index('training_surveys_index_00') .index('training_surveys_index_00')
.on(trainingSurveys.firstname), .on(trainingSurveys.coorFullName),
}), }),
); );

View File

@@ -2,7 +2,6 @@ import { ApiProperty } from '@nestjs/swagger';
import { Transform, Type } from 'class-transformer'; import { Transform, Type } from 'class-transformer';
import { import {
IsArray, IsArray,
IsBoolean,
IsDateString, IsDateString,
IsEmail, IsEmail,
IsInt, IsInt,
@@ -15,11 +14,7 @@ export class CreateTrainingDto {
// === 1. DATOS BÁSICOS === // === 1. DATOS BÁSICOS ===
@ApiProperty() @ApiProperty()
@IsString() @IsString()
firstname: string; coorFullName: string;
@ApiProperty()
@IsString()
lastname: string;
@ApiProperty() @ApiProperty()
@IsDateString() @IsDateString()
@@ -77,16 +72,14 @@ export class CreateTrainingDto {
structureType?: string; structureType?: string;
@ApiProperty() @ApiProperty()
@IsBoolean() @IsString()
@IsOptional() @IsOptional()
@Transform(({ value }) => value === 'true' || value === true) // Convierte "false" -> false hasTransport?: string;
hasTransport?: boolean;
@ApiProperty() @ApiProperty()
@IsBoolean() @IsString()
@IsOptional() @IsOptional()
@Transform(({ value }) => value === 'true' || value === true) isOpenSpace?: string;
isOpenSpace?: boolean;
@ApiProperty() @ApiProperty()
@IsString() @IsString()
@@ -209,7 +202,56 @@ export class CreateTrainingDto {
@IsEmail() @IsEmail()
communalCouncilEmail?: string; communalCouncilEmail?: string;
// === 6. LISTAS (Arrays JSON) === // === 6. DISTRIBUCIÓN Y EXPORTACIÓN ===
@ApiProperty()
@IsString()
@IsOptional()
internalDistributionZone?: string;
@ApiProperty()
@IsString()
@IsOptional()
isExporting?: string;
@ApiProperty()
@IsString()
@IsOptional()
externalCountry?: string;
@ApiProperty()
@IsString()
@IsOptional()
externalCity?: string;
@ApiProperty()
@IsString()
@IsOptional()
externalDescription?: string;
@ApiProperty()
@IsString()
@IsOptional()
externalQuantity?: string;
@ApiProperty()
@IsString()
@IsOptional()
externalUnit?: string;
// === 7. MANO DE OBRA ===
@ApiProperty()
@IsInt()
@IsOptional()
@Type(() => Number)
womenCount?: number;
@ApiProperty()
@IsInt()
@IsOptional()
@Type(() => Number)
menCount?: number;
// === 8. LISTAS (Arrays JSON) ===
// Reciben un string JSON '[{...}]' y lo convierten a Objeto JS real // Reciben un string JSON '[{...}]' y lo convierten a Objeto JS real
@ApiProperty() @ApiProperty()

View File

@@ -107,17 +107,7 @@ export class TrainingService {
// 2. Total Productores (Columna plana que mantuviste) // 2. Total Productores (Columna plana que mantuviste)
this.drizzle this.drizzle
.select({ .select({
sum: sql<number>` sum: sql<number>`SUM(${trainingSurveys.womenCount} + ${trainingSurveys.menCount})`,
SUM(
(
SELECT SUM(
COALESCE((item->>'menCount')::int, 0) +
COALESCE((item->>'womenCount')::int, 0)
)
FROM jsonb_array_elements(${trainingSurveys.productList}) as item
)
)
`,
}) })
.from(trainingSurveys) .from(trainingSurveys)
.where(whereCondition), .where(whereCondition),
@@ -244,20 +234,25 @@ export class TrainingService {
private async deleteFile(fileUrl: string) { private async deleteFile(fileUrl: string) {
if (!fileUrl) return; if (!fileUrl) return;
// Extract object name from URL
// URL format: http://endpoint:port/bucket/folder/filename
// Or it could be just the path if we decided that.
// Assuming fileUrl is the full public URL from getPublicUrl
try { try {
const url = new URL(fileUrl); // If it's a full URL, we need to extract the part after the bucket name
const pathname = url.pathname; // /bucket/folder/filename if (fileUrl.startsWith('http')) {
const parts = pathname.split('/'); const url = new URL(fileUrl);
// parts[0] is '', parts[1] is bucket, parts[2..] is objectName const pathname = url.pathname; // /bucket/folder/filename
const objectName = parts.slice(2).join('/'); const parts = pathname.split('/').filter(Boolean); // ['bucket', 'folder', 'filename']
await this.minioService.delete(objectName); // The first part is the bucket name, the rest is the object name
if (parts.length >= 2) {
const objectName = parts.slice(1).join('/');
await this.minioService.delete(objectName);
return;
}
}
// If it's not a URL or doesn't match the expected format, pass it as is
await this.minioService.delete(fileUrl);
} catch (error) { } catch (error) {
// If it's not a valid URL, maybe it's just the object name stored from before // Fallback if URL parsing fails
await this.minioService.delete(fileUrl); await this.minioService.delete(fileUrl);
} }
} }
@@ -292,6 +287,9 @@ export class TrainingService {
state: Number(state) ?? null, state: Number(state) ?? null,
municipality: Number(municipality) ?? null, municipality: Number(municipality) ?? null,
parish: Number(parish) ?? null, parish: Number(parish) ?? null,
hasTransport: rest.hasTransport === 'true' ? true : false,
isOpenSpace: rest.isOpenSpace === 'true' ? true : false,
isExporting: rest.isExporting === 'true' ? true : false,
createdBy: userId, createdBy: userId,
updatedBy: userId, updatedBy: userId,
}) })
@@ -359,6 +357,12 @@ export class TrainingService {
// actualizamos el id del usuario que actualizo el registro // actualizamos el id del usuario que actualizo el registro
updateData.updatedBy = userId; updateData.updatedBy = userId;
updateData.hasTransport =
updateTrainingDto.hasTransport === 'true' ? true : false;
updateData.isOpenSpace =
updateTrainingDto.isOpenSpace === 'true' ? true : false;
updateData.isExporting =
updateTrainingDto.isExporting === 'true' ? true : false;
const [updatedRecord] = await this.drizzle const [updatedRecord] = await this.drizzle
.update(trainingSurveys) .update(trainingSurveys)
@@ -402,8 +406,7 @@ export class TrainingService {
// const records = await this.drizzle // const records = await this.drizzle
// .select({ // .select({
// firstname: trainingSurveys.firstname, // coorFullName: trainingSurveys.coorFullName,
// lastname: trainingSurveys.lastname,
// visitDate: trainingSurveys.visitDate, // visitDate: trainingSurveys.visitDate,
// stateName: states.name, // stateName: states.name,
// municipalityName: municipalities.name, // municipalityName: municipalities.name,
@@ -451,8 +454,7 @@ export class TrainingService {
// const dateStr = date.toLocaleDateString('es-VE'); // const dateStr = date.toLocaleDateString('es-VE');
// const timeStr = date.toLocaleTimeString('es-VE'); // const timeStr = date.toLocaleTimeString('es-VE');
// sheet.cell(`A${currentRow}`).value(record.firstname); // sheet.cell(`A${currentRow}`).value(record.coorFullName);
// sheet.cell(`B${currentRow}`).value(record.lastname);
// sheet.cell(`C${currentRow}`).value(dateStr); // sheet.cell(`C${currentRow}`).value(dateStr);
// sheet.cell(`D${currentRow}`).value(timeStr); // sheet.cell(`D${currentRow}`).value(timeStr);
// sheet.cell(`E${currentRow}`).value(record.stateName || ''); // sheet.cell(`E${currentRow}`).value(record.stateName || '');

View File

@@ -1,196 +1,195 @@
export const COUNTRY_OPTIONS = [ export const COUNTRY_OPTIONS = [
'Afganistán', 'Afganistán',
'Albania', 'Albania',
'Alemania', 'Alemania',
'Andorra', 'Andorra',
'Angola', 'Angola',
'Antigua y Barbuda', 'Antigua y Barbuda',
'Arabia Saudita', 'Arabia Saudita',
'Argelia', 'Argelia',
'Argentina', 'Argentina',
'Armenia', 'Armenia',
'Australia', 'Australia',
'Austria', 'Austria',
'Azerbaiyán', 'Azerbaiyán',
'Bahamas', 'Bahamas',
'Bangladés', 'Bangladés',
'Barbados', 'Barbados',
'Baréin', 'Baréin',
'Bélgica', 'Bélgica',
'Belice', 'Belice',
'Benín', 'Benín',
'Bielorrusia', 'Bielorrusia',
'Birmania', 'Birmania',
'Bolivia', 'Bolivia',
'Bosnia y Herzegovina', 'Bosnia y Herzegovina',
'Botsuana', 'Botsuana',
'Brasil', 'Brasil',
'Brunéi', 'Brunéi',
'Bulgaria', 'Bulgaria',
'Burkina Faso', 'Burkina Faso',
'Burundi', 'Burundi',
'Bután', 'Bután',
'Cabo Verde', 'Cabo Verde',
'Camboya', 'Camboya',
'Camerún', 'Camerún',
'Canadá', 'Canadá',
'Catar', 'Catar',
'Chad', 'Chad',
'Chile', 'Chile',
'China', 'China',
'Chipre', 'Chipre',
'Ciudad del Vaticano', 'Ciudad del Vaticano',
'Colombia', 'Colombia',
'Comoras', 'Comoras',
'Corea del Norte', 'Corea del Norte',
'Corea del Sur', 'Corea del Sur',
'Costa de Marfil', 'Costa de Marfil',
'Costa Rica', 'Costa Rica',
'Croacia', 'Croacia',
'Cuba', 'Cuba',
'Dinamarca', 'Dinamarca',
'Dominica', 'Dominica',
'Ecuador', 'Ecuador',
'Egipto', 'Egipto',
'El Salvador', 'El Salvador',
'Emiratos Árabes Unidos', 'Emiratos Árabes Unidos',
'Eritrea', 'Eritrea',
'Eslovaquia', 'Eslovaquia',
'Eslovenia', 'Eslovenia',
'España', 'España',
'Estados Unidos', 'Estados Unidos',
'Estonia', 'Estonia',
'Etiopía', 'Etiopía',
'Filipinas', 'Filipinas',
'Finlandia', 'Finlandia',
'Fiyi', 'Fiyi',
'Francia', 'Francia',
'Gabón', 'Gabón',
'Gambia', 'Gambia',
'Georgia', 'Georgia',
'Ghana', 'Ghana',
'Granada', 'Granada',
'Grecia', 'Grecia',
'Guatemala', 'Guatemala',
'Guyana', 'Guyana',
'Guinea', 'Guinea',
'Guinea Ecuatorial', 'Guinea Ecuatorial',
'Guinea-Bisáu', 'Guinea-Bisáu',
'Haití', 'Haití',
'Honduras', 'Honduras',
'Hungría', 'Hungría',
'India', 'India',
'Indonesia', 'Indonesia',
'Irak', 'Irak',
'Irán', 'Irán',
'Irlanda', 'Irlanda',
'Islandia', 'Islandia',
'Islas Marshall', 'Islas Marshall',
'Islas Salomón', 'Islas Salomón',
'Israel', 'Israel',
'Italia', 'Italia',
'Jamaica', 'Jamaica',
'Japón', 'Japón',
'Jordania', 'Jordania',
'Kazajistán', 'Kazajistán',
'Kenia', 'Kenia',
'Kirguistán', 'Kirguistán',
'Kiribati', 'Kiribati',
'Kuwait', 'Kuwait',
'Laos', 'Laos',
'Lesoto', 'Lesoto',
'Letonia', 'Letonia',
'Líbano', 'Líbano',
'Liberia', 'Liberia',
'Libia', 'Libia',
'Liechtenstein', 'Liechtenstein',
'Lituania', 'Lituania',
'Luxemburgo', 'Luxemburgo',
'Madagascar', 'Madagascar',
'Malasia', 'Malasia',
'Malaui', 'Malaui',
'Maldivas', 'Maldivas',
'Malí', 'Malí',
'Malta', 'Malta',
'Marruecos', 'Marruecos',
'Mauricio', 'Mauricio',
'Mauritania', 'Mauritania',
'México', 'México',
'Micronesia', 'Micronesia',
'Moldavia', 'Moldavia',
'Mónaco', 'Mónaco',
'Mongolia', 'Mongolia',
'Montenegro', 'Montenegro',
'Mozambique', 'Mozambique',
'Namibia', 'Namibia',
'Nauru', 'Nauru',
'Nepal', 'Nepal',
'Nicaragua', 'Nicaragua',
'Níger', 'Níger',
'Nigeria', 'Nigeria',
'Noruega', 'Noruega',
'Nueva Zelanda', 'Nueva Zelanda',
'Omán', 'Omán',
'Países Bajos', 'Países Bajos',
'Pakistán', 'Pakistán',
'Palaos', 'Palaos',
'Panamá', 'Panamá',
'Papúa Nueva Guinea', 'Papúa Nueva Guinea',
'Paraguay', 'Paraguay',
'Perú', 'Perú',
'Polonia', 'Polonia',
'Portugal', 'Portugal',
'Reino Unido', 'Reino Unido',
'República Centroafricana', 'República Centroafricana',
'República Checa', 'República Checa',
'República de Macedonia', 'República de Macedonia',
'República del Congo', 'República del Congo',
'República Democrática del Congo', 'República Democrática del Congo',
'República Dominicana', 'República Dominicana',
'República Sudafricana', 'República Sudafricana',
'Ruanda', 'Ruanda',
'Rumanía', 'Rumanía',
'Rusia', 'Rusia',
'Samoa', 'Samoa',
'San Cristóbal y Nieves', 'San Cristóbal y Nieves',
'San Marino', 'San Marino',
'San Vicente y las Granadinas', 'San Vicente y las Granadinas',
'Santa Lucía', 'Santa Lucía',
'Santo Tomé y Príncipe', 'Santo Tomé y Príncipe',
'Senegal', 'Senegal',
'Serbia', 'Serbia',
'Seychelles', 'Seychelles',
'Sierra Leona', 'Sierra Leona',
'Singapur', 'Singapur',
'Siria', 'Siria',
'Somalia', 'Somalia',
'Sri Lanka', 'Sri Lanka',
'Suazilandia', 'Suazilandia',
'Sudán', 'Sudán',
'Sudán del Sur', 'Sudán del Sur',
'Suecia', 'Suecia',
'Suiza', 'Suiza',
'Surinam', 'Surinam',
'Tailandia', 'Tailandia',
'Tanzania', 'Tanzania',
'Tayikistán', 'Tayikistán',
'Timor Oriental', 'Timor Oriental',
'Togo', 'Togo',
'Tonga', 'Tonga',
'Trinidad y Tobago', 'Trinidad y Tobago',
'Túnez', 'Túnez',
'Turkmenistán', 'Turkmenistán',
'Turquía', 'Turquía',
'Tuvalu', 'Tuvalu',
'Ucrania', 'Ucrania',
'Uganda', 'Uganda',
'Uruguay', 'Uruguay',
'Uzbekistán', 'Uzbekistán',
'Vanuatu', 'Vanuatu',
'Venezuela', 'Vietnam',
'Vietnam', 'Yemen',
'Yemen', 'Yibuti',
'Yibuti', 'Zambia',
'Zambia', 'Zimbabue',
'Zimbabue'
]; ];

View File

@@ -90,7 +90,7 @@ export const createTrainingAction = async (
payloadToSend = rest as any; payloadToSend = rest as any;
} }
// console.log(payloadToSend); console.log(payloadToSend);
const [error, data] = await safeFetchApi( const [error, data] = await safeFetchApi(
TrainingMutate, TrainingMutate,
@@ -124,6 +124,8 @@ export const updateTrainingAction = async (
if (!id) throw new Error('ID es requerido para actualizar'); if (!id) throw new Error('ID es requerido para actualizar');
console.log(payloadToSend);
const [error, data] = await safeFetchApi( const [error, data] = await safeFetchApi(
TrainingMutate, TrainingMutate,
`/training/${id}`, `/training/${id}`,

View File

@@ -1,7 +1,9 @@
'use client'; 'use client';
import { COUNTRY_OPTIONS } from '@/constants/countries';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { Button } from '@repo/shadcn/button'; import { Button } from '@repo/shadcn/button';
import { Separator } from '@repo/shadcn/components/ui/separator';
import { import {
Form, Form,
FormControl, FormControl,
@@ -18,6 +20,7 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from '@repo/shadcn/select'; } from '@repo/shadcn/select';
import { Switch } from '@repo/shadcn/switch';
import { Textarea } from '@repo/shadcn/textarea'; import { Textarea } from '@repo/shadcn/textarea';
import { useForm, useWatch } from 'react-hook-form'; import { useForm, useWatch } from 'react-hook-form';
import { import {
@@ -98,8 +101,7 @@ export function CreateTrainingForm({
const form = useForm<TrainingSchema>({ const form = useForm<TrainingSchema>({
resolver: zodResolver(trainingSchema), resolver: zodResolver(trainingSchema),
defaultValues: { defaultValues: {
firstname: defaultValues?.firstname || '', coorFullName: defaultValues?.coorFullName || '',
lastname: defaultValues?.lastname || '',
coorState: defaultValues?.coorState || undefined, coorState: defaultValues?.coorState || undefined,
coorMunicipality: defaultValues?.coorMunicipality || undefined, coorMunicipality: defaultValues?.coorMunicipality || undefined,
coorParish: defaultValues?.coorParish || undefined, coorParish: defaultValues?.coorParish || undefined,
@@ -157,6 +159,17 @@ export function CreateTrainingForm({
state: defaultValues?.state || undefined, state: defaultValues?.state || undefined,
municipality: defaultValues?.municipality || undefined, municipality: defaultValues?.municipality || undefined,
parish: defaultValues?.parish || undefined, parish: defaultValues?.parish || undefined,
internalDistributionZone: defaultValues?.internalDistributionZone || '',
isExporting: defaultValues?.isExporting || false,
externalCountry: defaultValues?.externalCountry || '',
externalCity: defaultValues?.externalCity || '',
externalDescription: defaultValues?.externalDescription || '',
externalQuantity: defaultValues?.externalQuantity || '',
externalUnit: defaultValues?.externalUnit || '',
womenCount: defaultValues?.womenCount || 0,
menCount: defaultValues?.menCount || 0,
}, },
mode: 'onChange', mode: 'onChange',
}); });
@@ -213,23 +226,6 @@ export function CreateTrainingForm({
mainProductiveActivity, mainProductiveActivity,
]); ]);
const { data: dataCoorState } = useStateQuery();
const { data: dataCoorMunicipality } = useMunicipalityQuery(coorState);
const { data: dataCoorParish } = useParishQuery(coorMunicipality);
const coorStateOptions = dataCoorState?.data || [
{ id: 0, name: 'Sin estados' },
];
const coorMunicipalityOptions = dataCoorMunicipality?.data?.length
? dataCoorMunicipality.data
: [{ id: 0, stateId: 0, name: 'Sin Municipios' }];
const coorParishOptions =
Array.isArray(dataCoorParish?.data) && dataCoorParish?.data?.length
? dataCoorParish.data
: [{ id: 0, stateId: 0, name: 'Sin Parroquias' }];
const stateOptions = dataState?.data || [{ id: 0, name: 'Sin estados' }]; const stateOptions = dataState?.data || [{ id: 0, name: 'Sin estados' }];
const municipalityOptions = dataMunicipality?.data?.length const municipalityOptions = dataMunicipality?.data?.length
@@ -313,6 +309,7 @@ export function CreateTrainingForm({
selectedFiles.forEach((file) => { selectedFiles.forEach((file) => {
data.append('files', file); data.append('files', file);
}); });
const mutation = defaultValues?.id ? updateTraining : createTraining; const mutation = defaultValues?.id ? updateTraining : createTraining;
mutation(data as any, { mutation(data as any, {
@@ -359,26 +356,14 @@ export function CreateTrainingForm({
<CardContent className="grid grid-cols-1 md:grid-cols-2 gap-4"> <CardContent className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField <FormField
control={form.control} control={form.control}
name="firstname" name="coorFullName"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>Nombre del Coordinador Estadal</FormLabel> <FormLabel>
Nombre y Apellido del Coordinador Estadal
</FormLabel>
<FormControl> <FormControl>
<Input {...field} placeholder="Ej. Juan" /> <Input {...field} placeholder="Ej. Juan Pérez" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="lastname"
render={({ field }) => (
<FormItem>
<FormLabel>Apellido del Coordinador Estadal</FormLabel>
<FormControl>
<Input {...field} placeholder="Ej. Pérez" />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@@ -825,7 +810,7 @@ export function CreateTrainingForm({
</FormLabel> </FormLabel>
<Select <Select
onValueChange={(val) => field.onChange(val === 'true')} onValueChange={(val) => field.onChange(val === 'true')}
defaultValue={field.value ? 'true' : 'false'} value={field.value ? 'true' : 'false'}
> >
<FormControl> <FormControl>
<SelectTrigger> <SelectTrigger>
@@ -881,7 +866,7 @@ export function CreateTrainingForm({
</FormLabel> </FormLabel>
<Select <Select
onValueChange={(val) => field.onChange(val === 'true')} onValueChange={(val) => field.onChange(val === 'true')}
defaultValue={field.value ? 'true' : 'false'} value={field.value ? 'true' : 'false'}
> >
<FormControl> <FormControl>
<SelectTrigger> <SelectTrigger>
@@ -936,6 +921,212 @@ export function CreateTrainingForm({
</CardContent> </CardContent>
</Card> </Card>
{/* Distribución y Exportación */}
<Card>
<CardHeader>
<CardTitle>Zona de Distribución y Exportación</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
<FormField
control={form.control}
name="internalDistributionZone"
render={({ field }) => (
<FormItem>
<FormLabel>
Breve Descripción de la Zona de Distribución
</FormLabel>
<FormControl>
<Input
{...field}
placeholder="Ej. Mercado local y regional"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Separator />
<FormField
control={form.control}
name="isExporting"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
<div className="space-y-0.5">
<FormLabel className="text-base">
¿El producto es para exportación?
</FormLabel>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
{form.watch('isExporting') && (
<div className="space-y-4 pt-4 border-t">
<h4 className="font-semibold text-sm">
Datos de Exportación
</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
control={form.control}
name="externalCountry"
render={({ field }) => (
<FormItem>
<FormLabel>País</FormLabel>
<Select
onValueChange={field.onChange}
defaultValue={field.value ?? undefined}
>
<FormControl>
<SelectTrigger className="w-full">
<SelectValue placeholder="Seleccione País" />
</SelectTrigger>
</FormControl>
<SelectContent>
{COUNTRY_OPTIONS.map((country: string) => (
<SelectItem key={country} value={country}>
{country}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="externalCity"
render={({ field }) => (
<FormItem>
<FormLabel>Ciudad</FormLabel>
<FormControl>
<Input {...field} value={field.value ?? ''} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
control={form.control}
name="externalDescription"
render={({ field }) => (
<FormItem>
<FormLabel>Breve Descripción</FormLabel>
<FormControl>
<Input {...field} value={field.value ?? ''} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="grid grid-cols-2 gap-2">
<FormField
control={form.control}
name="externalQuantity"
render={({ field }) => (
<FormItem>
<FormLabel>Cantidad</FormLabel>
<FormControl>
<Input
type="number"
{...field}
value={field.value ?? ''}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="externalUnit"
render={({ field }) => (
<FormItem>
<FormLabel>Unidad</FormLabel>
<Select
onValueChange={field.onChange}
defaultValue={field.value ?? undefined}
>
<FormControl>
<SelectTrigger className="w-full">
<SelectValue placeholder="Unidad" />
</SelectTrigger>
</FormControl>
<SelectContent>
{[
'KG',
'TON',
'UNID',
'LT',
'MTS',
'QQ',
'HM2',
'SACOS',
].map((unit) => (
<SelectItem key={unit} value={unit}>
{unit}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
</div>
</div>
</div>
)}
</CardContent>
</Card>
{/* Mano de Obra */}
<Card>
<CardHeader>
<CardTitle>Mano de Obra</CardTitle>
</CardHeader>
<CardContent className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
control={form.control}
name="womenCount"
render={({ field }) => (
<FormItem>
<FormLabel>Mujeres (cantidad)</FormLabel>
<FormControl>
<Input type="number" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="menCount"
render={({ field }) => (
<FormItem>
<FormLabel>Hombres (cantidad)</FormLabel>
<FormControl>
<Input type="number" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</CardContent>
</Card>
{/* 3. Detalles de la ubicación */} {/* 3. Detalles de la ubicación */}
<Card> <Card>
<CardHeader> <CardHeader>
@@ -963,12 +1154,14 @@ export function CreateTrainingForm({
name="ospGoogleMapsLink" name="ospGoogleMapsLink"
render={({ field }) => ( render={({ field }) => (
<FormItem className="col-span-1 lg:col-span-2 flex flex-col space-y-2"> <FormItem className="col-span-1 lg:col-span-2 flex flex-col space-y-2">
<FormLabel>Dirección Link Google Maps</FormLabel> <FormLabel>
Coordenadas de la Ubicación (Google Maps)
</FormLabel>
<FormControl> <FormControl>
<Input <Input
{...field} {...field}
value={field.value ?? ''} value={field.value ?? ''}
placeholder="https://maps.google.com/..." placeholder="10.123456, -66.123456"
/> />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />

View File

@@ -1,9 +1,3 @@
import { COUNTRY_OPTIONS } from '@/constants/countries';
import {
useMunicipalityQuery,
useParishQuery,
useStateQuery,
} from '@/feactures/location/hooks/use-query-location';
import { Button } from '@repo/shadcn/button'; import { Button } from '@repo/shadcn/button';
import { import {
Dialog, Dialog,
@@ -23,15 +17,6 @@ import {
} from '@repo/shadcn/components/ui/table'; } from '@repo/shadcn/components/ui/table';
import { Input } from '@repo/shadcn/input'; import { Input } from '@repo/shadcn/input';
import { Label } from '@repo/shadcn/label'; import { Label } from '@repo/shadcn/label';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@repo/shadcn/select';
import { SelectSearchable } from '@repo/shadcn/select-searchable';
import { Switch } from '@repo/shadcn/switch';
import { Trash2 } from 'lucide-react'; import { Trash2 } from 'lucide-react';
import { useState } from 'react'; import { useState } from 'react';
import { useFieldArray, useFormContext } from 'react-hook-form'; import { useFieldArray, useFormContext } from 'react-hook-form';
@@ -58,48 +43,8 @@ export function ProductActivityList() {
dailyCount: '', dailyCount: '',
weeklyCount: '', weeklyCount: '',
monthlyCount: '', monthlyCount: '',
internalDistributionZone: '',
// Internal dist
internalState: null,
internalMunicipality: null,
internalParish: null,
internalDescription: '',
internalQuantity: '',
internalUnit: '',
// External dist
externalCountry: '',
externalState: null,
externalMunicipality: null,
externalParish: null,
externalCity: '',
externalDescription: '',
externalQuantity: '',
externalUnit: '',
// Workforce
womenCount: '',
menCount: '',
isExporting: false,
}); });
// 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 = () => { const handleAdd = () => {
if (newItem.description) { if (newItem.description) {
append(newItem); append(newItem);
@@ -108,39 +53,11 @@ export function ProductActivityList() {
dailyCount: '', dailyCount: '',
weeklyCount: '', weeklyCount: '',
monthlyCount: '', monthlyCount: '',
internalDistributionZone: '',
internalState: null,
internalMunicipality: null,
internalParish: null,
internalDescription: '',
internalQuantity: '',
internalUnit: '',
externalCountry: '',
externalState: null,
externalMunicipality: null,
externalParish: null,
externalCity: '',
externalDescription: '',
externalQuantity: '',
externalUnit: '',
womenCount: '',
menCount: '',
isExporting: false,
}); });
setInternalStateId(0);
setInternalMuniId(0);
setExternalStateId(0);
setExternalMuniId(0);
setIsOpen(false); setIsOpen(false);
} }
}; };
const stateOptions = statesData?.data || [];
const internalMuniOptions = internalMuniData?.data || [];
const internalParishOptions = internalParishData?.data || [];
const externalMuniOptions = externalMuniData?.data || [];
const externalParishOptions = externalParishData?.data || [];
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
@@ -203,209 +120,6 @@ export function ProductActivityList() {
</div> </div>
</div> </div>
<hr />
<h4 className="font-semibold">Zona de Distribucción</h4>
<div className="grid grid-cols-1 gap-4">
<div className="space-y-2">
<Label>Breve Descripción de la Zona de Distribucción</Label>
<Input
value={newItem.internalDistributionZone}
onChange={(e) =>
setNewItem({
...newItem,
internalDistributionZone: e.target.value,
})
}
/>
</div>
</div>
<hr />
<div className="flex items-center space-x-2">
<Switch
id="export-toggle"
checked={newItem.isExporting}
onCheckedChange={(val: boolean) =>
setNewItem({ ...newItem, isExporting: val })
}
/>
<Label htmlFor="export-toggle">
¿El producto es para exportación?
</Label>
</div>
{newItem.isExporting && (
<>
<h4 className="font-semibold text-sm">
Datos de Exportación
</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>
{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</Label>
<div className="flex gap-2">
<Input
type="number"
className="flex-1"
value={newItem.externalQuantity}
onChange={(e) =>
setNewItem({
...newItem,
externalQuantity: e.target.value,
})
}
/>
<Select
value={newItem.externalUnit}
onValueChange={(val) =>
setNewItem({ ...newItem, externalUnit: val })
}
>
<SelectTrigger className="w-[120px]">
<SelectValue placeholder="Unidad" />
</SelectTrigger>
<SelectContent>
{UNIT_OPTIONS.map((unit) => (
<SelectItem key={unit} value={unit}>
{unit}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</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"> <div className="flex justify-end gap-4">
<Button <Button
variant="outline" variant="outline"
@@ -427,6 +141,8 @@ export function ProductActivityList() {
<TableHeader> <TableHeader>
<TableRow> <TableRow>
<TableHead>Producto/Descripción</TableHead> <TableHead>Producto/Descripción</TableHead>
<TableHead>Producción Diario</TableHead>
<TableHead>Producción Semanal</TableHead>
<TableHead>Producción Mensual</TableHead> <TableHead>Producción Mensual</TableHead>
<TableHead className="w-[50px]"></TableHead> <TableHead className="w-[50px]"></TableHead>
</TableRow> </TableRow>
@@ -455,75 +171,10 @@ export function ProductActivityList() {
{...register(`productList.${index}.monthlyCount`)} {...register(`productList.${index}.monthlyCount`)}
defaultValue={field.monthlyCount ?? ''} defaultValue={field.monthlyCount ?? ''}
/> />
<input
type="hidden"
{...register(
`productList.${index}.internalDistributionZone`,
)}
defaultValue={field.internalDistributionZone ?? ''}
/>
<input
type="hidden"
{...register(`productList.${index}.internalQuantity`)}
defaultValue={field.internalQuantity ?? ''}
/>
<input
type="hidden"
{...register(`productList.${index}.internalUnit`)}
defaultValue={field.internalUnit ?? ''}
/>
<input
type="hidden"
{...register(`productList.${index}.externalCountry`)}
defaultValue={field.externalCountry ?? ''}
/>
<input
type="hidden"
{...register(`productList.${index}.externalState`)}
defaultValue={field.externalState ?? ''}
/>
<input
type="hidden"
{...register(`productList.${index}.externalMunicipality`)}
defaultValue={field.externalMunicipality ?? ''}
/>
<input
type="hidden"
{...register(`productList.${index}.externalParish`)}
defaultValue={field.externalParish ?? ''}
/>
<input
type="hidden"
{...register(`productList.${index}.externalCity`)}
defaultValue={field.externalCity ?? ''}
/>
<input
type="hidden"
{...register(`productList.${index}.externalDescription`)}
defaultValue={field.externalDescription ?? ''}
/>
<input
type="hidden"
{...register(`productList.${index}.externalQuantity`)}
defaultValue={field.externalQuantity ?? ''}
/>
<input
type="hidden"
{...register(`productList.${index}.externalUnit`)}
defaultValue={field.externalUnit ?? ''}
/>
<input
type="hidden"
{...register(`productList.${index}.womenCount`)}
defaultValue={field.womenCount ?? ''}
/>
<input
type="hidden"
{...register(`productList.${index}.menCount`)}
defaultValue={field.menCount ?? ''}
/>
{field.description} {field.description}
</TableCell> </TableCell>
<TableCell>{field.dailyCount}</TableCell>
<TableCell>{field.weeklyCount}</TableCell>
<TableCell>{field.monthlyCount}</TableCell> <TableCell>{field.monthlyCount}</TableCell>
<TableCell> <TableCell>
<Button <Button

View File

@@ -22,7 +22,6 @@ import {
DialogTitle, DialogTitle,
} from '@repo/shadcn/components/ui/dialog'; } from '@repo/shadcn/components/ui/dialog';
import { ScrollArea } from '@repo/shadcn/components/ui/scroll-area'; import { ScrollArea } from '@repo/shadcn/components/ui/scroll-area';
import { Separator } from '@repo/shadcn/components/ui/separator';
import { import {
ExternalLink, ExternalLink,
Factory, Factory,
@@ -129,10 +128,7 @@ export function TrainingViewModal({
<div className="space-y-8"> <div className="space-y-8">
{/* 1. Datos de la Visita */} {/* 1. Datos de la Visita */}
<Section title="Datos de la Visita"> <Section title="Datos de la Visita">
<DetailItem <DetailItem label="Coordinador" value={data.coorFullName} />
label="Coordinador"
value={`${data.firstname} ${data.lastname}`}
/>
<DetailItem label="Teléfono Coord." value={data.coorPhone} /> <DetailItem label="Teléfono Coord." value={data.coorPhone} />
<DetailItem <DetailItem
label="Fecha Visita" label="Fecha Visita"
@@ -209,7 +205,11 @@ export function TrainingViewModal({
className="gap-2" className="gap-2"
> >
<a <a
href={data.ospGoogleMapsLink} href={
data.ospGoogleMapsLink.startsWith('http')
? data.ospGoogleMapsLink
: `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(data.ospGoogleMapsLink)}`
}
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
> >
@@ -229,80 +229,36 @@ export function TrainingViewModal({
<CardHeader className="pb-3"> <CardHeader className="pb-3">
<CardTitle className="text-lg flex items-center gap-2"> <CardTitle className="text-lg flex items-center gap-2">
<Package className="h-5 w-5" /> <Package className="h-5 w-5" />
Productos y Mano de Obra Productos Registrados
<Badge variant="secondary" className="ml-2"> <Badge variant="secondary" className="ml-2">
{data.productList?.length || 0} {data.productList?.length || 0}
</Badge> </Badge>
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent className="grid gap-4"> <CardContent className="grid gap-4">
{data.productList?.map((prod: any, idx: number) => ( <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div {data.productList?.map((prod: any, idx: number) => (
key={idx} <div
className="bg-muted/40 p-4 rounded-lg border text-sm" 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"> <h4 className="font-bold text-base text-primary mb-2">
{prod.productName} {prod.description}
</h4> </h4>
<Badge variant="outline"> <div className="grid grid-cols-3 gap-2">
Mano de obra:{' '} <DetailItem label="Diario" value={prod.dailyCount} />
{Number(prod.menCount || 0) + <DetailItem
Number(prod.womenCount || 0)} label="Semanal"
</Badge> value={prod.weeklyCount}
/>
<DetailItem
label="Mensual"
value={prod.monthlyCount}
/>
</div>
</div> </div>
<p className="text-muted-foreground mb-3"> ))}
{prod.description} </div>
</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}{' '}
{prod.internalUnit}
</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}{' '}
{prod.externalUnit}
</p>
<p className="text-xs text-muted-foreground">
{prod.externalDescription}
</p>
</div>
)}
</div>
</>
)}
</div>
))}
{(!data.productList || data.productList.length === 0) && ( {(!data.productList || data.productList.length === 0) && (
<p className="text-sm text-muted-foreground italic"> <p className="text-sm text-muted-foreground italic">
No hay productos registrados. No hay productos registrados.
@@ -311,6 +267,64 @@ export function TrainingViewModal({
</CardContent> </CardContent>
</Card> </Card>
{/* DISTRIBUCIÓN, EXPORTACIÓN Y MANO DE OBRA */}
<Section title="Distribución, Exportación y Mano de Obra">
<div className="col-span-full grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<div className="space-y-4">
<h4 className="text-sm font-bold border-b pb-1">
Distribución Interna
</h4>
<DetailItem
label="Zona de Distribución"
value={data.internalDistributionZone}
/>
</div>
<div className="space-y-4">
<h4 className="text-sm font-bold border-b pb-1">
Mano de Obra
</h4>
<div className="grid grid-cols-2 gap-4">
<DetailItem label="Mujeres" value={data.womenCount} />
<DetailItem label="Hombres" value={data.menCount} />
<DetailItem
label="Total"
value={
Number(data.womenCount || 0) +
Number(data.menCount || 0)
}
/>
</div>
</div>
<div className="space-y-4">
<h4 className="text-sm font-bold border-b pb-1 flex items-center gap-2">
Exportación <BooleanBadge value={data.isExporting} />
</h4>
{data.isExporting && (
<div className="space-y-3">
<DetailItem label="País" value={data.externalCountry} />
<DetailItem label="Ciudad" value={data.externalCity} />
<DetailItem
label="Descripción"
value={data.externalDescription}
/>
<div className="grid grid-cols-2 gap-2">
<DetailItem
label="Cantidad"
value={data.externalQuantity}
/>
<DetailItem
label="Unidad"
value={data.externalUnit}
/>
</div>
</div>
)}
</div>
</div>
</Section>
{/* EQUIPAMIENTO Y PRODUCCIÓN */} {/* EQUIPAMIENTO Y PRODUCCIÓN */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<Card> <Card>

View File

@@ -7,25 +7,6 @@ const productItemSchema = z.object({
dailyCount: z.coerce.string().or(z.number()).optional().nullable(), dailyCount: z.coerce.string().or(z.number()).optional().nullable(),
weeklyCount: z.coerce.string().or(z.number()).optional().nullable(), weeklyCount: z.coerce.string().or(z.number()).optional().nullable(),
monthlyCount: z.coerce.string().or(z.number()).optional().nullable(), monthlyCount: z.coerce.string().or(z.number()).optional().nullable(),
// Distribución Interna
internalDistributionZone: z.string().optional().nullable(),
internalQuantity: z.coerce.string().or(z.number()).optional().nullable(),
internalUnit: z.string().optional().nullable(),
// Distribución Externa
externalCountry: z.string().optional().nullable(),
externalState: z.number().optional().nullable(),
externalMunicipality: z.number().optional().nullable(),
externalParish: z.number().optional().nullable(),
externalCity: z.string().optional().nullable(),
externalDescription: z.string().optional().nullable(),
externalQuantity: z.coerce.string().or(z.number()).optional().nullable(),
externalUnit: z.string().optional().nullable(),
// Mano de obra
womenCount: z.coerce.string().or(z.number()).optional().nullable(),
menCount: z.coerce.string().or(z.number()).optional().nullable(),
}); });
const productionItemSchema = z.object({ const productionItemSchema = z.object({
@@ -42,8 +23,9 @@ const equipmentItemSchema = z.object({
export const trainingSchema = z.object({ export const trainingSchema = z.object({
//Datos de la visita //Datos de la visita
id: z.number().optional(), id: z.number().optional(),
firstname: z.string().min(1, { message: 'Nombre es requerido' }), coorFullName: z
lastname: z.string().min(1, { message: 'Apellido es requerido' }), .string()
.min(1, { message: 'Nombre del coordinador es requerido' }),
coorPhone: z coorPhone: z
.string() .string()
.optional() .optional()
@@ -76,14 +58,22 @@ export const trainingSchema = z.object({
.default('ACTIVA'), .default('ACTIVA'),
infrastructureMt2: z.string().optional().or(z.literal('')).nullable(), infrastructureMt2: z.string().optional().or(z.literal('')).nullable(),
hasTransport: z hasTransport: z
.preprocess((val) => val === 'true' || val === true, z.boolean()) .preprocess(
(val) => val === 'true' || val === true || val === 1 || val === '1',
z.boolean(),
)
.optional() .optional()
.nullable(), .nullable()
.default(false),
structureType: z.string().optional().or(z.literal('')).nullable(), structureType: z.string().optional().or(z.literal('')).nullable(),
isOpenSpace: z isOpenSpace: z
.preprocess((val) => val === 'true' || val === true, z.boolean()) .preprocess(
(val) => val === 'true' || val === true || val === 1 || val === '1',
z.boolean(),
)
.optional() .optional()
.nullable(), .nullable()
.default(false),
paralysisReason: z.string().optional().nullable(), paralysisReason: z.string().optional().nullable(),
//Datos del Equipamiento //Datos del Equipamiento
@@ -95,6 +85,31 @@ export const trainingSchema = z.object({
// Datos de Actividad Productiva // Datos de Actividad Productiva
productList: z.array(productItemSchema).optional().default([]), productList: z.array(productItemSchema).optional().default([]),
// Distribución y Exportación
internalDistributionZone: z
.string()
.min(1, { message: 'Zona de distribución es requerida' }),
isExporting: z
.preprocess(
(val) => val === 'true' || val === true || val === 1 || val === '1',
z.boolean(),
)
.optional()
.default(false),
externalCountry: z.string().optional().nullable(),
externalCity: z.string().optional().nullable(),
externalDescription: z.string().optional().nullable(),
externalQuantity: z.coerce.string().or(z.number()).optional().nullable(),
externalUnit: z.string().optional().nullable(),
// Mano de obra
womenCount: z.coerce
.number()
.min(0, { message: 'Cantidad de mujeres es requerida' }),
menCount: z.coerce
.number()
.min(0, { message: 'Cantidad de hombres es requerida' }),
//Detalles de la ubicación //Detalles de la ubicación
ospAddress: z ospAddress: z
.string() .string()