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 {
// 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('/');
// parts[0] is '', parts[1] is bucket, parts[2..] is objectName
const objectName = parts.slice(2).join('/');
const parts = pathname.split('/').filter(Boolean); // ['bucket', 'folder', 'filename']
// 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

@@ -187,10 +187,9 @@ export const COUNTRY_OPTIONS = [
'Uruguay',
'Uzbekistán',
'Vanuatu',
'Venezuela',
'Vietnam',
'Yemen',
'Yibuti',
'Zambia',
'Zimbabue'
'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">
<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"
>
<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">
<h4 className="font-bold text-base text-primary mb-2">
{prod.description}
</p>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-3">
</h4>
<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} />
<DetailItem
label="Hombres / Mujeres"
value={`${prod.menCount || 0} / ${prod.womenCount || 0}`}
label="Semanal"
value={prod.weeklyCount}
/>
<DetailItem
label="Mensual"
value={prod.monthlyCount}
/>
</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()