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> {
try {
await this.minioClient.removeObject(this.bucketName, objectName);
this.logger.log(`Object "${objectName}" deleted successfully.`);
// Ensure we don't have a leading slash which can cause issues with removeObject
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) {
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
}
}

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,
"tag": "0021_warm_machine_man",
"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 ===
id: t.serial('id').primaryKey(),
firstname: t.text('firstname').notNull(),
lastname: t.text('lastname').notNull(),
coorFullName: t.text('coor_full_name').notNull(),
visitDate: t.timestamp('visit_date').notNull(),
coorPhone: t.text('coor_phone'),
@@ -133,6 +132,20 @@ export const trainingSurveys = t.pgTable(
familyBurden: t.integer('family_burden'),
numberOfChildren: t.integer('number_of_children'),
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
photo1: t.text('photo1'),
photo2: t.text('photo2'),
@@ -149,7 +162,7 @@ export const trainingSurveys = t.pgTable(
(trainingSurveys) => ({
trainingSurveysIndex: t
.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 {
IsArray,
IsBoolean,
IsDateString,
IsEmail,
IsInt,
@@ -15,11 +14,7 @@ export class CreateTrainingDto {
// === 1. DATOS BÁSICOS ===
@ApiProperty()
@IsString()
firstname: string;
@ApiProperty()
@IsString()
lastname: string;
coorFullName: string;
@ApiProperty()
@IsDateString()
@@ -77,16 +72,14 @@ export class CreateTrainingDto {
structureType?: string;
@ApiProperty()
@IsBoolean()
@IsString()
@IsOptional()
@Transform(({ value }) => value === 'true' || value === true) // Convierte "false" -> false
hasTransport?: boolean;
hasTransport?: string;
@ApiProperty()
@IsBoolean()
@IsString()
@IsOptional()
@Transform(({ value }) => value === 'true' || value === true)
isOpenSpace?: boolean;
isOpenSpace?: string;
@ApiProperty()
@IsString()
@@ -209,7 +202,56 @@ export class CreateTrainingDto {
@IsEmail()
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
@ApiProperty()

View File

@@ -107,17 +107,7 @@ export class TrainingService {
// 2. Total Productores (Columna plana que mantuviste)
this.drizzle
.select({
sum: sql<number>`
SUM(
(
SELECT SUM(
COALESCE((item->>'menCount')::int, 0) +
COALESCE((item->>'womenCount')::int, 0)
)
FROM jsonb_array_elements(${trainingSurveys.productList}) as item
)
)
`,
sum: sql<number>`SUM(${trainingSurveys.womenCount} + ${trainingSurveys.menCount})`,
})
.from(trainingSurveys)
.where(whereCondition),
@@ -244,20 +234,25 @@ export class TrainingService {
private async deleteFile(fileUrl: string) {
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 {
const url = new URL(fileUrl);
const pathname = url.pathname; // /bucket/folder/filename
const parts = pathname.split('/');
// parts[0] is '', parts[1] is bucket, parts[2..] is objectName
const objectName = parts.slice(2).join('/');
// If it's a full URL, we need to extract the part after the bucket name
if (fileUrl.startsWith('http')) {
const url = new URL(fileUrl);
const pathname = url.pathname; // /bucket/folder/filename
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) {
// 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);
}
}
@@ -292,6 +287,9 @@ export class TrainingService {
state: Number(state) ?? null,
municipality: Number(municipality) ?? 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,
updatedBy: userId,
})
@@ -359,6 +357,12 @@ export class TrainingService {
// actualizamos el id del usuario que actualizo el registro
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
.update(trainingSurveys)
@@ -402,8 +406,7 @@ export class TrainingService {
// const records = await this.drizzle
// .select({
// firstname: trainingSurveys.firstname,
// lastname: trainingSurveys.lastname,
// coorFullName: trainingSurveys.coorFullName,
// visitDate: trainingSurveys.visitDate,
// stateName: states.name,
// municipalityName: municipalities.name,
@@ -451,8 +454,7 @@ export class TrainingService {
// const dateStr = date.toLocaleDateString('es-VE');
// const timeStr = date.toLocaleTimeString('es-VE');
// sheet.cell(`A${currentRow}`).value(record.firstname);
// sheet.cell(`B${currentRow}`).value(record.lastname);
// sheet.cell(`A${currentRow}`).value(record.coorFullName);
// sheet.cell(`C${currentRow}`).value(dateStr);
// sheet.cell(`D${currentRow}`).value(timeStr);
// sheet.cell(`E${currentRow}`).value(record.stateName || '');

View File

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

View File

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

View File

@@ -1,7 +1,9 @@
'use client';
import { COUNTRY_OPTIONS } from '@/constants/countries';
import { zodResolver } from '@hookform/resolvers/zod';
import { Button } from '@repo/shadcn/button';
import { Separator } from '@repo/shadcn/components/ui/separator';
import {
Form,
FormControl,
@@ -18,6 +20,7 @@ import {
SelectTrigger,
SelectValue,
} from '@repo/shadcn/select';
import { Switch } from '@repo/shadcn/switch';
import { Textarea } from '@repo/shadcn/textarea';
import { useForm, useWatch } from 'react-hook-form';
import {
@@ -98,8 +101,7 @@ export function CreateTrainingForm({
const form = useForm<TrainingSchema>({
resolver: zodResolver(trainingSchema),
defaultValues: {
firstname: defaultValues?.firstname || '',
lastname: defaultValues?.lastname || '',
coorFullName: defaultValues?.coorFullName || '',
coorState: defaultValues?.coorState || undefined,
coorMunicipality: defaultValues?.coorMunicipality || undefined,
coorParish: defaultValues?.coorParish || undefined,
@@ -157,6 +159,17 @@ export function CreateTrainingForm({
state: defaultValues?.state || undefined,
municipality: defaultValues?.municipality || 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',
});
@@ -213,23 +226,6 @@ export function CreateTrainingForm({
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 municipalityOptions = dataMunicipality?.data?.length
@@ -313,6 +309,7 @@ export function CreateTrainingForm({
selectedFiles.forEach((file) => {
data.append('files', file);
});
const mutation = defaultValues?.id ? updateTraining : createTraining;
mutation(data as any, {
@@ -359,26 +356,14 @@ export function CreateTrainingForm({
<CardContent className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
control={form.control}
name="firstname"
name="coorFullName"
render={({ field }) => (
<FormItem>
<FormLabel>Nombre del Coordinador Estadal</FormLabel>
<FormLabel>
Nombre y Apellido del Coordinador Estadal
</FormLabel>
<FormControl>
<Input {...field} placeholder="Ej. Juan" />
</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" />
<Input {...field} placeholder="Ej. Juan Pérez" />
</FormControl>
<FormMessage />
</FormItem>
@@ -825,7 +810,7 @@ export function CreateTrainingForm({
</FormLabel>
<Select
onValueChange={(val) => field.onChange(val === 'true')}
defaultValue={field.value ? 'true' : 'false'}
value={field.value ? 'true' : 'false'}
>
<FormControl>
<SelectTrigger>
@@ -881,7 +866,7 @@ export function CreateTrainingForm({
</FormLabel>
<Select
onValueChange={(val) => field.onChange(val === 'true')}
defaultValue={field.value ? 'true' : 'false'}
value={field.value ? 'true' : 'false'}
>
<FormControl>
<SelectTrigger>
@@ -936,6 +921,212 @@ export function CreateTrainingForm({
</CardContent>
</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 */}
<Card>
<CardHeader>
@@ -963,12 +1154,14 @@ export function CreateTrainingForm({
name="ospGoogleMapsLink"
render={({ field }) => (
<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>
<Input
{...field}
value={field.value ?? ''}
placeholder="https://maps.google.com/..."
placeholder="10.123456, -66.123456"
/>
</FormControl>
<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 {
Dialog,
@@ -23,15 +17,6 @@ import {
} 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 { Switch } from '@repo/shadcn/switch';
import { Trash2 } from 'lucide-react';
import { useState } from 'react';
import { useFieldArray, useFormContext } from 'react-hook-form';
@@ -58,48 +43,8 @@ export function ProductActivityList() {
dailyCount: '',
weeklyCount: '',
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 = () => {
if (newItem.description) {
append(newItem);
@@ -108,39 +53,11 @@ export function ProductActivityList() {
dailyCount: '',
weeklyCount: '',
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);
}
};
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">
@@ -203,209 +120,6 @@ export function ProductActivityList() {
</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">
<Button
variant="outline"
@@ -427,6 +141,8 @@ export function ProductActivityList() {
<TableHeader>
<TableRow>
<TableHead>Producto/Descripción</TableHead>
<TableHead>Producción Diario</TableHead>
<TableHead>Producción Semanal</TableHead>
<TableHead>Producción Mensual</TableHead>
<TableHead className="w-[50px]"></TableHead>
</TableRow>
@@ -455,75 +171,10 @@ export function ProductActivityList() {
{...register(`productList.${index}.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}
</TableCell>
<TableCell>{field.dailyCount}</TableCell>
<TableCell>{field.weeklyCount}</TableCell>
<TableCell>{field.monthlyCount}</TableCell>
<TableCell>
<Button

View File

@@ -22,7 +22,6 @@ import {
DialogTitle,
} from '@repo/shadcn/components/ui/dialog';
import { ScrollArea } from '@repo/shadcn/components/ui/scroll-area';
import { Separator } from '@repo/shadcn/components/ui/separator';
import {
ExternalLink,
Factory,
@@ -129,10 +128,7 @@ export function TrainingViewModal({
<div className="space-y-8">
{/* 1. Datos de la Visita */}
<Section title="Datos de la Visita">
<DetailItem
label="Coordinador"
value={`${data.firstname} ${data.lastname}`}
/>
<DetailItem label="Coordinador" value={data.coorFullName} />
<DetailItem label="Teléfono Coord." value={data.coorPhone} />
<DetailItem
label="Fecha Visita"
@@ -209,7 +205,11 @@ export function TrainingViewModal({
className="gap-2"
>
<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"
rel="noreferrer"
>
@@ -229,80 +229,36 @@ export function TrainingViewModal({
<CardHeader className="pb-3">
<CardTitle className="text-lg flex items-center gap-2">
<Package className="h-5 w-5" />
Productos y Mano de Obra
Productos Registrados
<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}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{data.productList?.map((prod: any, idx: number) => (
<div
key={idx}
className="bg-muted/40 p-4 rounded-lg border text-sm"
>
<h4 className="font-bold text-base text-primary mb-2">
{prod.description}
</h4>
<Badge variant="outline">
Mano de obra:{' '}
{Number(prod.menCount || 0) +
Number(prod.womenCount || 0)}
</Badge>
<div className="grid grid-cols-3 gap-2">
<DetailItem label="Diario" value={prod.dailyCount} />
<DetailItem
label="Semanal"
value={prod.weeklyCount}
/>
<DetailItem
label="Mensual"
value={prod.monthlyCount}
/>
</div>
</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}{' '}
{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>
))}
))}
</div>
{(!data.productList || data.productList.length === 0) && (
<p className="text-sm text-muted-foreground italic">
No hay productos registrados.
@@ -311,6 +267,64 @@ export function TrainingViewModal({
</CardContent>
</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 */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<Card>

View File

@@ -7,25 +7,6 @@ const productItemSchema = z.object({
dailyCount: 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(),
// 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({
@@ -42,8 +23,9 @@ const equipmentItemSchema = z.object({
export const trainingSchema = z.object({
//Datos de la visita
id: z.number().optional(),
firstname: z.string().min(1, { message: 'Nombre es requerido' }),
lastname: z.string().min(1, { message: 'Apellido es requerido' }),
coorFullName: z
.string()
.min(1, { message: 'Nombre del coordinador es requerido' }),
coorPhone: z
.string()
.optional()
@@ -76,14 +58,22 @@ export const trainingSchema = z.object({
.default('ACTIVA'),
infrastructureMt2: z.string().optional().or(z.literal('')).nullable(),
hasTransport: z
.preprocess((val) => val === 'true' || val === true, z.boolean())
.preprocess(
(val) => val === 'true' || val === true || val === 1 || val === '1',
z.boolean(),
)
.optional()
.nullable(),
.nullable()
.default(false),
structureType: z.string().optional().or(z.literal('')).nullable(),
isOpenSpace: z
.preprocess((val) => val === 'true' || val === true, z.boolean())
.preprocess(
(val) => val === 'true' || val === true || val === 1 || val === '1',
z.boolean(),
)
.optional()
.nullable(),
.nullable()
.default(false),
paralysisReason: z.string().optional().nullable(),
//Datos del Equipamiento
@@ -95,6 +85,31 @@ export const trainingSchema = z.object({
// Datos de Actividad Productiva
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
ospAddress: z
.string()