correciones al formulario osp

This commit is contained in:
2026-01-28 22:42:19 -04:00
parent d2908f1e4c
commit 8efe595f73
23 changed files with 9036 additions and 1685 deletions

View File

@@ -90,6 +90,8 @@ export const createTrainingAction = async (
payloadToSend = rest as any;
}
console.log(payloadToSend);
const [error, data] = await safeFetchApi(
TrainingMutate,
'/training',

View File

@@ -0,0 +1,171 @@
import { Button } from '@repo/shadcn/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@repo/shadcn/components/ui/dialog';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@repo/shadcn/components/ui/table';
import { Input } from '@repo/shadcn/input';
import { Label } from '@repo/shadcn/label';
import { Trash2 } from 'lucide-react';
import { useState } from 'react';
import { useFieldArray, useFormContext } from 'react-hook-form';
export function EquipmentList() {
const { control, register } = useFormContext();
const { fields, append, remove } = useFieldArray({
control,
name: 'equipmentList',
});
const [isOpen, setIsOpen] = useState(false);
const [newItem, setNewItem] = useState({
machine: '',
specifications: '',
quantity: '',
});
const handleAdd = () => {
if (newItem.machine && newItem.quantity) {
append({ ...newItem, quantity: Number(newItem.quantity) });
setNewItem({ machine: '', specifications: '', quantity: '' });
setIsOpen(false);
}
};
return (
<div className="space-y-4">
<div className="flex justify-between items-center">
<h3 className="text-lg font-medium">Datos del Equipamiento</h3>
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button variant="outline">Agregar Maquinaria</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Agregar Maquinaria/Equipo</DialogTitle>
</DialogHeader>
<DialogDescription className="sr-only">
Datos de equipamiento
</DialogDescription>
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label>Maquinaria</Label>
<Input
value={newItem.machine}
onChange={(e) =>
setNewItem({ ...newItem, machine: e.target.value })
}
placeholder="Nombre de la maquinaria"
/>
</div>
<div className="space-y-2">
<Label>Especificaciones</Label>
<Input
value={newItem.specifications}
onChange={(e) =>
setNewItem({ ...newItem, specifications: e.target.value })
}
placeholder="Especificaciones técnicas"
/>
</div>
<div className="space-y-2">
<Label>Cantidad</Label>
<Input
type="number"
value={newItem.quantity}
onChange={(e) =>
setNewItem({ ...newItem, quantity: e.target.value })
}
placeholder="0"
/>
</div>
<div className="flex justify-end gap-4">
<Button
variant="outline"
type="button"
onClick={() => setIsOpen(false)}
>
Cancelar
</Button>
<Button onClick={handleAdd}>Guardar</Button>
</div>
</div>
</DialogContent>
</Dialog>
</div>
<div className="border rounded-md">
<Table>
<TableHeader>
<TableRow>
<TableHead>Maquinaria</TableHead>
<TableHead>Especificaciones</TableHead>
<TableHead>Cantidad</TableHead>
<TableHead className="w-[50px]"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{fields.map((field, index) => (
<TableRow key={field.id}>
<TableCell>
<input
type="hidden"
{...register(`equipmentList.${index}.machine`)}
/>
{/* @ts-ignore */}
{field.machine}
</TableCell>
<TableCell>
<input
type="hidden"
{...register(`equipmentList.${index}.specifications`)}
/>
{/* @ts-ignore */}
{field.specifications}
</TableCell>
<TableCell>
<input
type="hidden"
{...register(`equipmentList.${index}.quantity`)}
/>
{/* @ts-ignore */}
{field.quantity}
</TableCell>
<TableCell>
<Button
variant="ghost"
size="icon"
onClick={() => remove(index)}
>
<Trash2 className="h-4 w-4 text-destructive" />
</Button>
</TableCell>
</TableRow>
))}
{fields.length === 0 && (
<TableRow>
<TableCell
colSpan={4}
className="text-center text-muted-foreground"
>
No hay equipamiento registrado
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
</div>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,504 @@
import { COUNTRY_OPTIONS } from '@/constants/countries';
import {
useMunicipalityQuery,
useParishQuery,
useStateQuery,
} from '@/feactures/location/hooks/use-query-location';
import { Button } from '@repo/shadcn/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@repo/shadcn/components/ui/dialog';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@repo/shadcn/components/ui/table';
import { Input } from '@repo/shadcn/input';
import { Label } from '@repo/shadcn/label';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@repo/shadcn/select';
import { SelectSearchable } from '@repo/shadcn/select-searchable';
import { Trash2 } from 'lucide-react';
import { useState } from 'react';
import { useFieldArray, useFormContext } from 'react-hook-form';
// 1. Definimos la estructura de los datos para que TypeScript no se queje
interface ProductItem {
productName: string;
description: string;
dailyCount: string;
weeklyCount: string;
monthlyCount: string;
// ... resto de propiedades opcionales si las necesitas tipar estrictamente
[key: string]: any;
}
interface ProductFormValues {
productList: ProductItem[];
}
export function ProductActivityList() {
// 2. Pasamos el tipo genérico a useFormContext
const { control, register } = useFormContext<ProductFormValues>();
const { fields, append, remove } = useFieldArray({
control,
name: 'productList',
});
const [isOpen, setIsOpen] = useState(false);
// Modal Form State
const [newItem, setNewItem] = useState<any>({
productName: '',
description: '',
dailyCount: '',
weeklyCount: '',
monthlyCount: '',
// Internal dist
internalState: undefined,
internalMunicipality: undefined,
internalParish: undefined,
internalDescription: '',
internalQuantity: '',
internalUnit: '',
// External dist
externalCountry: '',
externalState: undefined,
externalMunicipality: undefined,
externalParish: undefined,
externalCity: '',
externalDescription: '',
externalQuantity: '',
externalUnit: '',
// Workforce
womenCount: '',
menCount: '',
});
// Location logic for Internal Validation
const [internalStateId, setInternalStateId] = useState(0);
const [internalMuniId, setInternalMuniId] = useState(0);
const { data: statesData } = useStateQuery();
const { data: internalMuniData } = useMunicipalityQuery(internalStateId);
const { data: internalParishData } = useParishQuery(internalMuniId);
// Location logic for External Validation
const [externalStateId, setExternalStateId] = useState(0);
const [externalMuniId, setExternalMuniId] = useState(0);
const { data: externalMuniData } = useMunicipalityQuery(externalStateId);
const { data: externalParishData } = useParishQuery(externalMuniId);
const isVenezuela = newItem.externalCountry === 'Venezuela';
const handleAdd = () => {
if (newItem.productName) {
append(newItem);
setNewItem({
productName: '',
description: '',
dailyCount: '',
weeklyCount: '',
monthlyCount: '',
internalState: undefined,
internalMunicipality: undefined,
internalParish: undefined,
internalDescription: '',
internalQuantity: '',
internalUnit: '',
externalCountry: '',
externalState: undefined,
externalMunicipality: undefined,
externalParish: undefined,
externalCity: '',
externalDescription: '',
externalQuantity: '',
externalUnit: '',
womenCount: '',
menCount: '',
});
setInternalStateId(0);
setInternalMuniId(0);
setExternalStateId(0);
setExternalMuniId(0);
setIsOpen(false);
}
};
const stateOptions = statesData?.data || [];
const internalMuniOptions = internalMuniData?.data || [];
const internalParishOptions = internalParishData?.data || [];
const externalMuniOptions = externalMuniData?.data || [];
const externalParishOptions = externalParishData?.data || [];
return (
<div className="space-y-4">
<div className="flex justify-between items-center">
<h3 className="text-lg font-medium">Datos de Actividad Productiva</h3>
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button variant="outline">Agregar Producto/Actividad</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[800px] max-h-[80vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Detalles de Actividad Productiva</DialogTitle>
</DialogHeader>
<DialogDescription className="sr-only">
Datos de actividad productiva
</DialogDescription>
<div className="space-y-6 py-4">
{/* Basic Info */}
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label>Producto Terminado</Label>
<Input
value={newItem.productName}
onChange={(e) =>
setNewItem({ ...newItem, productName: e.target.value })
}
/>
</div>
<div className="space-y-2">
<Label>Descripción</Label>
<Input
value={newItem.description}
onChange={(e) =>
setNewItem({ ...newItem, description: e.target.value })
}
/>
</div>
</div>
<div className="grid grid-cols-3 gap-4">
<div className="space-y-2">
<Label>Cant. Diario</Label>
<Input
type="number"
value={newItem.dailyCount}
onChange={(e) =>
setNewItem({ ...newItem, dailyCount: e.target.value })
}
/>
</div>
<div className="space-y-2">
<Label>Cant. Semanal</Label>
<Input
type="number"
value={newItem.weeklyCount}
onChange={(e) =>
setNewItem({ ...newItem, weeklyCount: e.target.value })
}
/>
</div>
<div className="space-y-2">
<Label>Cant. Mensual</Label>
<Input
type="number"
value={newItem.monthlyCount}
onChange={(e) =>
setNewItem({ ...newItem, monthlyCount: e.target.value })
}
/>
</div>
</div>
<hr />
<h4 className="font-semibold">Distribución Interna</h4>
<div className="grid grid-cols-3 gap-4">
<div className="space-y-2">
<Label>Estado</Label>
<SelectSearchable
options={stateOptions.map((s) => ({
value: String(s.id),
label: s.name,
}))}
onValueChange={(val) => {
const id = Number(val);
setInternalStateId(id);
setNewItem({ ...newItem, internalState: id });
}}
placeholder="Estado"
/>
</div>
<div className="space-y-2">
<Label>Municipio</Label>
<SelectSearchable
options={internalMuniOptions.map((s) => ({
value: String(s.id),
label: s.name,
}))}
onValueChange={(val) => {
const id = Number(val);
setInternalMuniId(id);
setNewItem({ ...newItem, internalMunicipality: id });
}}
placeholder="Municipio"
disabled={!internalStateId}
/>
</div>
<div className="space-y-2">
<Label>Parroquia</Label>
<SelectSearchable
options={internalParishOptions.map((s) => ({
value: String(s.id),
label: s.name,
}))}
onValueChange={(val) =>
setNewItem({ ...newItem, internalParish: Number(val) })
}
placeholder="Parroquia"
disabled={!internalMuniId}
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label>Breve Descripción</Label>
<Input
value={newItem.internalDescription}
onChange={(e) =>
setNewItem({
...newItem,
internalDescription: e.target.value,
})
}
/>
</div>
<div className="space-y-2">
<Label>Cantidad Numérica (Kg, TON, UNID. LT)</Label>
<Input
type="number"
value={newItem.internalQuantity}
onChange={(e) =>
setNewItem({
...newItem,
internalQuantity: e.target.value,
})
}
/>
</div>
</div>
<hr />
<h4 className="font-semibold">Distribución Externa</h4>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label>País</Label>
<Select
value={newItem.externalCountry}
onValueChange={(val) =>
setNewItem({ ...newItem, externalCountry: val })
}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Seleccione País" />
</SelectTrigger>
<SelectContent>
{/* 3. CORRECCIÓN DEL MAPEO DE PAÍSES Y KEYS */}
{COUNTRY_OPTIONS.map((country: string) => (
<SelectItem key={country} value={country}>
{country}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{!isVenezuela && (
<div className="space-y-2">
<Label>Ciudad</Label>
<Input
value={newItem.externalCity}
onChange={(e) =>
setNewItem({ ...newItem, externalCity: e.target.value })
}
/>
</div>
)}
</div>
{isVenezuela && (
<div className="grid grid-cols-3 gap-4">
<div className="space-y-2">
<Label>Estado</Label>
<SelectSearchable
options={stateOptions.map((s) => ({
value: String(s.id),
label: s.name,
}))}
onValueChange={(val) => {
const id = Number(val);
setExternalStateId(id);
setNewItem({ ...newItem, externalState: id });
}}
placeholder="Estado"
/>
</div>
<div className="space-y-2">
<Label>Municipio</Label>
<SelectSearchable
options={externalMuniOptions.map((s) => ({
value: String(s.id),
label: s.name,
}))}
onValueChange={(val) => {
const id = Number(val);
setExternalMuniId(id);
setNewItem({ ...newItem, externalMunicipality: id });
}}
placeholder="Municipio"
disabled={!externalStateId}
/>
</div>
<div className="space-y-2">
<Label>Parroquia</Label>
<SelectSearchable
options={externalParishOptions.map((s) => ({
value: String(s.id),
label: s.name,
}))}
onValueChange={(val) =>
setNewItem({ ...newItem, externalParish: Number(val) })
}
placeholder="Parroquia"
disabled={!externalMuniId}
/>
</div>
</div>
)}
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label>Breve Descripción</Label>
<Input
value={newItem.externalDescription}
onChange={(e) =>
setNewItem({
...newItem,
externalDescription: e.target.value,
})
}
/>
</div>
<div className="space-y-2">
<Label>Cantidad Numérica (Kg, TON, UNID. LT)</Label>
<Input
type="number"
value={newItem.externalQuantity}
onChange={(e) =>
setNewItem({
...newItem,
externalQuantity: e.target.value,
})
}
/>
</div>
</div>
<hr />
<h4 className="font-semibold">Mano de Obra</h4>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label>Mujer (cantidad)</Label>
<Input
type="number"
value={newItem.womenCount}
onChange={(e) =>
setNewItem({ ...newItem, womenCount: e.target.value })
}
/>
</div>
<div className="space-y-2">
<Label>Hombre (cantidad)</Label>
<Input
type="number"
value={newItem.menCount}
onChange={(e) =>
setNewItem({ ...newItem, menCount: e.target.value })
}
/>
</div>
</div>
<div className="flex justify-end gap-4">
<Button
variant="outline"
type="button"
onClick={() => setIsOpen(false)}
>
Cancelar
</Button>
<Button onClick={handleAdd}>Guardar</Button>
</div>
</div>
</DialogContent>
</Dialog>
</div>
<div className="border rounded-md">
<Table>
<TableHeader>
<TableRow>
<TableHead>Producto</TableHead>
<TableHead>Descripción</TableHead>
<TableHead>Mensual</TableHead>
<TableHead className="w-[50px]"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{fields.map((field, index) => (
<TableRow key={field.id}>
<TableCell>
<input
type="hidden"
{...register(`productList.${index}.productName`)}
// field.productName ahora es válido gracias a la interface
value={field.productName}
/>
{field.productName}
</TableCell>
<TableCell>{field.description}</TableCell>
<TableCell>{field.monthlyCount}</TableCell>
<TableCell>
<Button
variant="ghost"
size="icon"
onClick={() => remove(index)}
>
<Trash2 className="h-4 w-4 text-destructive" />
</Button>
</TableCell>
</TableRow>
))}
{fields.length === 0 && (
<TableRow>
<TableCell
colSpan={4}
className="text-center text-muted-foreground"
>
No hay productos registrados
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
</div>
);
}

View File

@@ -0,0 +1,171 @@
import { Button } from '@repo/shadcn/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@repo/shadcn/components/ui/dialog';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@repo/shadcn/components/ui/table';
import { Input } from '@repo/shadcn/input';
import { Label } from '@repo/shadcn/label';
import { Trash2 } from 'lucide-react';
import { useState } from 'react';
import { useFieldArray, useFormContext } from 'react-hook-form';
export function ProductionList() {
const { control, register } = useFormContext();
const { fields, append, remove } = useFieldArray({
control,
name: 'productionList',
});
const [isOpen, setIsOpen] = useState(false);
const [newItem, setNewItem] = useState({
rawMaterial: '',
supplyType: '',
quantity: '',
});
const handleAdd = () => {
if (newItem.rawMaterial && newItem.quantity) {
append({ ...newItem, quantity: Number(newItem.quantity) });
setNewItem({ rawMaterial: '', supplyType: '', quantity: '' });
setIsOpen(false);
}
};
return (
<div className="space-y-4">
<div className="flex justify-between items-center">
<h3 className="text-lg font-medium">Datos de Producción</h3>
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button variant="outline">Agregar Producción</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Agregar Datos de Producción</DialogTitle>
</DialogHeader>
<DialogDescription className="sr-only">
Datos de producción
</DialogDescription>
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label>Materia prima requerida (mensual)</Label>
<Input
value={newItem.rawMaterial}
onChange={(e) =>
setNewItem({ ...newItem, rawMaterial: e.target.value })
}
placeholder="Descripción de materia prima"
/>
</div>
<div className="space-y-2">
<Label>Tipo de Insumo/Rubro</Label>
<Input
value={newItem.supplyType}
onChange={(e) =>
setNewItem({ ...newItem, supplyType: e.target.value })
}
placeholder="Tipo"
/>
</div>
<div className="space-y-2">
<Label>Cantidad Mensual (Kg, TON, UNID. LT)</Label>
<Input
type="number"
value={newItem.quantity}
onChange={(e) =>
setNewItem({ ...newItem, quantity: e.target.value })
}
placeholder="0"
/>
</div>
<div className="flex justify-end gap-4">
<Button
variant="outline"
type="button"
onClick={() => setIsOpen(false)}
>
Cancelar
</Button>
<Button onClick={handleAdd}>Guardar</Button>
</div>
</div>
</DialogContent>
</Dialog>
</div>
<div className="border rounded-md">
<Table>
<TableHeader>
<TableRow>
<TableHead>Materia Prima</TableHead>
<TableHead>Tipo Insumo</TableHead>
<TableHead>Cantidad (Mensual)</TableHead>
<TableHead className="w-[50px]"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{fields.map((field, index) => (
<TableRow key={field.id}>
<TableCell>
<input
type="hidden"
{...register(`productionList.${index}.rawMaterial`)}
/>
{/* @ts-ignore */}
{field.rawMaterial}
</TableCell>
<TableCell>
<input
type="hidden"
{...register(`productionList.${index}.supplyType`)}
/>
{/* @ts-ignore */}
{field.supplyType}
</TableCell>
<TableCell>
<input
type="hidden"
{...register(`productionList.${index}.quantity`)}
/>
{/* @ts-ignore */}
{field.quantity}
</TableCell>
<TableCell>
<Button
variant="ghost"
size="icon"
onClick={() => remove(index)}
>
<Trash2 className="h-4 w-4 text-destructive" />
</Button>
</TableCell>
</TableRow>
))}
{fields.length === 0 && (
<TableRow>
<TableCell
colSpan={4}
className="text-center text-muted-foreground"
>
No hay datos de producción registrados
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
</div>
);
}

View File

@@ -1,225 +1,276 @@
'use client';
import { useState } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@repo/shadcn/card';
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, PieChart, Pie, Cell } from 'recharts';
import { useTrainingStatsQuery } from '../hooks/use-training-statistics';
import { Input } from '@repo/shadcn/input';
import { Button } from '@repo/shadcn/button';
import { SelectSearchable } from '@repo/shadcn/select-searchable';
import { useStateQuery, useMunicipalityQuery, useParishQuery } from '@/feactures/location/hooks/use-query-location';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
useMunicipalityQuery,
useParishQuery,
useStateQuery,
} from '@/feactures/location/hooks/use-query-location';
import { Button } from '@repo/shadcn/button';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@repo/shadcn/card';
import { Input } from '@repo/shadcn/input';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@repo/shadcn/select';
import { SelectSearchable } from '@repo/shadcn/select-searchable';
import { useState } from 'react';
import {
Bar,
BarChart,
CartesianGrid,
Cell,
Legend,
Pie,
PieChart,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from 'recharts';
import { useTrainingStatsQuery } from '../hooks/use-training-statistics';
const OSP_TYPES = [
'EPSD',
'EPSI',
'UPF',
'Cooperativa',
'Grupo de Intercambio',
'EPSD',
'EPSI',
'UPF',
'Cooperativa',
'Grupo de Intercambio',
];
export function TrainingStatistics() {
// Filter State
const [startDate, setStartDate] = useState<string>('');
const [endDate, setEndDate] = useState<string>('');
const [stateId, setStateId] = useState<number>(0);
const [municipalityId, setMunicipalityId] = useState<number>(0);
const [parishId, setParishId] = useState<number>(0);
const [ospType, setOspType] = useState<string>('');
// Filter State
const [startDate, setStartDate] = useState<string>('');
const [endDate, setEndDate] = useState<string>('');
const [stateId, setStateId] = useState<number>(0);
const [municipalityId, setMunicipalityId] = useState<number>(0);
const [parishId, setParishId] = useState<number>(0);
const [ospType, setOspType] = useState<string>('');
// Location Data
const { data: dataState } = useStateQuery();
const { data: dataMunicipality } = useMunicipalityQuery(stateId);
const { data: dataParish } = useParishQuery(municipalityId);
// Location Data
const { data: dataState } = useStateQuery();
const { data: dataMunicipality } = useMunicipalityQuery(stateId);
const { data: dataParish } = useParishQuery(municipalityId);
const stateOptions = dataState?.data || [{ id: 0, name: 'Sin estados' }];
const municipalityOptions = Array.isArray(dataMunicipality?.data) && dataMunicipality.data.length > 0
? dataMunicipality.data
: [{ id: 0, stateId: 0, name: 'Sin Municipios' }];
const parishOptions = Array.isArray(dataParish?.data) && dataParish.data.length > 0
? dataParish.data
: [{ id: 0, stateId: 0, name: 'Sin Parroquias' }];
const stateOptions = dataState?.data || [{ id: 0, name: 'Sin estados' }];
const municipalityOptions =
Array.isArray(dataMunicipality?.data) && dataMunicipality.data.length > 0
? dataMunicipality.data
: [{ id: 0, stateId: 0, name: 'Sin Municipios' }];
const parishOptions =
Array.isArray(dataParish?.data) && dataParish.data.length > 0
? dataParish.data
: [{ id: 0, stateId: 0, name: 'Sin Parroquias' }];
// Query with Filters
const { data, isLoading, refetch } = useTrainingStatsQuery({
startDate: startDate || undefined,
endDate: endDate || undefined,
stateId: stateId || undefined,
municipalityId: municipalityId || undefined,
parishId: parishId || undefined,
ospType: ospType || undefined,
});
// Query with Filters
const { data, isLoading, refetch } = useTrainingStatsQuery({
startDate: startDate || undefined,
endDate: endDate || undefined,
stateId: stateId || undefined,
municipalityId: municipalityId || undefined,
parishId: parishId || undefined,
ospType: ospType || undefined,
});
const handleClearFilters = () => {
setStartDate('');
setEndDate('');
setStateId(0);
setMunicipalityId(0);
setParishId(0);
setOspType('');
};
if (isLoading) {
return <div className="flex justify-center p-8">Cargando estadísticas...</div>;
}
if (!data) {
return <div className="flex justify-center p-8">No hay datos disponibles.</div>;
}
const { totalOsps, totalProducers, statusDistribution, activityDistribution, typeDistribution, stateDistribution, yearDistribution } = data;
const COLORS = ['#0088FE', '#00C49F', '#FFBB28', '#FF8042', '#8884d8', '#82ca9d'];
const handleClearFilters = () => {
setStartDate('');
setEndDate('');
setStateId(0);
setMunicipalityId(0);
setParishId(0);
setOspType('');
};
if (isLoading) {
return (
<div className="space-y-6">
{/* Filters Section */}
<Card>
<CardHeader>
<CardTitle>Filtros</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-4">
<div className="space-y-2">
<label className="text-sm font-medium">Fecha Inicio</label>
<Input
type="date"
value={startDate}
onChange={(e) => setStartDate(e.target.value)}
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Fecha Fin</label>
<Input
type="date"
value={endDate}
onChange={(e) => setEndDate(e.target.value)}
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Estado</label>
<SelectSearchable
options={stateOptions.map((item) => ({
value: item.id.toString(),
label: item.name,
}))}
onValueChange={(value: any) => {
setStateId(Number(value));
setMunicipalityId(0); // Reset municipality
setParishId(0); // Reset parish
}}
placeholder="Selecciona un estado"
defaultValue={stateId ? stateId.toString() : ""}
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Municipio</label>
<SelectSearchable
options={municipalityOptions.map((item) => ({
value: item.id.toString(),
label: item.name,
}))}
onValueChange={(value: any) => {
setMunicipalityId(Number(value));
setParishId(0);
}}
placeholder="Selecciona municipio"
defaultValue={municipalityId ? municipalityId.toString() : ""}
disabled={!stateId || stateId === 0}
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Parroquia</label>
<SelectSearchable
options={parishOptions.map((item) => ({
value: item.id.toString(),
label: item.name,
}))}
onValueChange={(value: any) => setParishId(Number(value))}
placeholder="Selecciona parroquia"
defaultValue={parishId ? parishId.toString() : ""}
disabled={!municipalityId || municipalityId === 0}
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Tipo de OSP</label>
<Select value={ospType} onValueChange={setOspType}>
<SelectTrigger>
<SelectValue placeholder="Todos" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Todos</SelectItem>
{OSP_TYPES.map(type => (
<SelectItem key={type} value={type}>{type}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex items-end">
<Button variant="outline" onClick={handleClearFilters}>
Limpiar Filtros
</Button>
</div>
</div>
</CardContent>
</Card>
<div className="flex justify-center p-8">Cargando estadísticas...</div>
);
}
{/* Statistics Cards */}
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Total de OSP Registradas</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{totalOsps}</div>
<p className="text-xs text-muted-foreground">
Organizaciones Socioproductivas
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Total de Productores</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{totalProducers}</div>
<p className="text-xs text-muted-foreground">
Productores asociados
</p>
</CardContent>
</Card>
if (!data) {
return (
<div className="flex justify-center p-8">No hay datos disponibles.</div>
);
}
<Card className="col-span-full">
<CardHeader>
<CardTitle>Actividad Productiva</CardTitle>
<CardDescription>Distribución por tipo de actividad</CardDescription>
</CardHeader>
<CardContent className="h-[400px]">
<ResponsiveContainer width="100%" height="100%">
<BarChart
data={activityDistribution}
layout="vertical"
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
>
<CartesianGrid strokeDasharray="3 3" />
<XAxis type="number" />
<YAxis dataKey="name" type="category" width={150} />
<Tooltip wrapperStyle={{ color: '#000' }} />
<Legend />
<Bar dataKey="value" fill="#8884d8" name="Cantidad" />
</BarChart>
</ResponsiveContainer>
</CardContent>
</Card>
const {
totalOsps,
totalProducers,
statusDistribution,
activityDistribution,
typeDistribution,
stateDistribution,
yearDistribution,
} = data;
{/* State Distribution */}
<Card className="col-span-full">
const COLORS = [
'#0088FE',
'#00C49F',
'#FFBB28',
'#FF8042',
'#8884d8',
'#82ca9d',
];
return (
<div className="space-y-6">
{/* Filters Section */}
<Card>
<CardHeader>
<CardTitle>Filtros</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-4">
<div className="space-y-2">
<label className="text-sm font-medium">Fecha Inicio</label>
<Input
type="date"
value={startDate}
onChange={(e) => setStartDate(e.target.value)}
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Fecha Fin</label>
<Input
type="date"
value={endDate}
onChange={(e) => setEndDate(e.target.value)}
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Estado</label>
<SelectSearchable
options={stateOptions.map((item) => ({
value: item.id.toString(),
label: item.name,
}))}
onValueChange={(value: any) => {
setStateId(Number(value));
setMunicipalityId(0); // Reset municipality
setParishId(0); // Reset parish
}}
placeholder="Selecciona un estado"
defaultValue={stateId ? stateId.toString() : ''}
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Municipio</label>
<SelectSearchable
options={municipalityOptions.map((item) => ({
value: item.id.toString(),
label: item.name,
}))}
onValueChange={(value: any) => {
setMunicipalityId(Number(value));
setParishId(0);
}}
placeholder="Selecciona municipio"
defaultValue={municipalityId ? municipalityId.toString() : ''}
disabled={!stateId || stateId === 0}
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Parroquia</label>
<SelectSearchable
options={parishOptions.map((item) => ({
value: item.id.toString(),
label: item.name,
}))}
onValueChange={(value: any) => setParishId(Number(value))}
placeholder="Selecciona parroquia"
defaultValue={parishId ? parishId.toString() : ''}
disabled={!municipalityId || municipalityId === 0}
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Tipo de OSP</label>
<Select value={ospType} onValueChange={setOspType}>
<SelectTrigger>
<SelectValue placeholder="Todos" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Todos</SelectItem>
{OSP_TYPES.map((type) => (
<SelectItem key={type} value={type}>
{type}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex items-end">
<Button variant="outline" onClick={handleClearFilters}>
Limpiar Filtros
</Button>
</div>
</div>
</CardContent>
</Card>
{/* Statistics Cards */}
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Total de OSP Registradas
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{totalOsps}</div>
<p className="text-xs text-muted-foreground">
Organizaciones Socioproductivas
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Total de Productores
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{totalProducers}</div>
<p className="text-xs text-muted-foreground">
Productores asociados
</p>
</CardContent>
</Card>
<Card className="col-span-full">
<CardHeader>
<CardTitle>Actividad Productiva</CardTitle>
<CardDescription>
Distribución por tipo de actividad
</CardDescription>
</CardHeader>
<CardContent className="h-[400px]">
<ResponsiveContainer width="100%" height="100%">
<BarChart
data={activityDistribution}
layout="vertical"
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
>
<CartesianGrid strokeDasharray="3 3" />
<XAxis type="number" />
<YAxis dataKey="name" type="category" width={150} />
<Tooltip wrapperStyle={{ color: '#000' }} />
<Legend />
<Bar dataKey="value" fill="#8884d8" name="Cantidad" />
</BarChart>
</ResponsiveContainer>
</CardContent>
</Card>
{/* State Distribution */}
{/* <Card className="col-span-full">
<CardHeader>
<CardTitle>Distribución por Estado</CardTitle>
<CardDescription>OSP registradas por estado</CardDescription>
@@ -239,81 +290,84 @@ export function TrainingStatistics() {
</BarChart>
</ResponsiveContainer>
</CardContent>
</Card>
</Card> */}
{/* Year Distribution */}
<Card className="col-span-full lg:col-span-1">
<CardHeader>
<CardTitle>Año de Constitución</CardTitle>
<CardDescription>Año de registro de la empresa</CardDescription>
</CardHeader>
<CardContent className="h-[400px]">
<ResponsiveContainer width="100%" height="100%">
<BarChart
data={yearDistribution}
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="name" />
<YAxis />
<Tooltip wrapperStyle={{ color: '#000' }} />
<Legend />
<Bar dataKey="value" fill="#FFBB28" name="Cantidad" />
</BarChart>
</ResponsiveContainer>
</CardContent>
</Card>
{/* Year Distribution */}
<Card className="col-span-full lg:col-span-1">
<CardHeader>
<CardTitle>Año de Constitución</CardTitle>
<CardDescription>Año de registro de la empresa</CardDescription>
</CardHeader>
<CardContent className="h-[400px]">
<ResponsiveContainer width="100%" height="100%">
<BarChart
data={yearDistribution}
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="name" />
<YAxis />
<Tooltip wrapperStyle={{ color: '#000' }} />
<Legend />
<Bar dataKey="value" fill="#FFBB28" name="Cantidad" />
</BarChart>
</ResponsiveContainer>
</CardContent>
</Card>
<Card className="col-span-1 md:col-span-2 lg:col-span-1">
<CardHeader>
<CardTitle>Estatus Actual</CardTitle>
<CardDescription>Estado operativo de las OSP</CardDescription>
</CardHeader>
<CardContent className="h-[300px]">
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
data={statusDistribution}
cx="50%"
cy="50%"
innerRadius={60}
outerRadius={80}
fill="#8884d8"
paddingAngle={5}
dataKey="value"
>
{statusDistribution.map((entry, index) => (
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
))}
</Pie>
<Tooltip wrapperStyle={{ color: '#000' }} />
<Legend />
</PieChart>
</ResponsiveContainer>
</CardContent>
</Card>
<Card className="col-span-1 md:col-span-2 lg:col-span-1">
<CardHeader>
<CardTitle>Tipo de Organización</CardTitle>
<CardDescription>Clasificación de las OSP</CardDescription>
</CardHeader>
<CardContent className="h-[300px]">
<ResponsiveContainer width="100%" height="100%">
<BarChart
data={typeDistribution}
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="name" />
<YAxis />
<Tooltip wrapperStyle={{ color: '#000' }} />
<Legend />
<Bar dataKey="value" fill="#82ca9d" name="Cantidad" />
</BarChart>
</ResponsiveContainer>
</CardContent>
</Card>
</div>
</div>
);
<Card className="col-span-1 md:col-span-2 lg:col-span-1">
<CardHeader>
<CardTitle>Estatus Actual</CardTitle>
<CardDescription>Estado operativo de las OSP</CardDescription>
</CardHeader>
<CardContent className="h-[300px]">
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
data={statusDistribution}
cx="50%"
cy="50%"
innerRadius={60}
outerRadius={80}
fill="#8884d8"
paddingAngle={5}
dataKey="value"
>
{statusDistribution.map((entry, index) => (
<Cell
key={`cell-${index}`}
fill={COLORS[index % COLORS.length]}
/>
))}
</Pie>
<Tooltip wrapperStyle={{ color: '#000' }} />
<Legend />
</PieChart>
</ResponsiveContainer>
</CardContent>
</Card>
<Card className="col-span-1 md:col-span-2 lg:col-span-1">
<CardHeader>
<CardTitle>Tipo de Organización</CardTitle>
<CardDescription>Clasificación de las OSP</CardDescription>
</CardHeader>
<CardContent className="h-[300px]">
<ResponsiveContainer width="100%" height="100%">
<BarChart
data={typeDistribution}
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="name" />
<YAxis />
<Tooltip wrapperStyle={{ color: '#000' }} />
<Legend />
<Bar dataKey="value" fill="#82ca9d" name="Cantidad" />
</BarChart>
</ResponsiveContainer>
</CardContent>
</Card>
</div>
</div>
);
}

View File

@@ -16,7 +16,16 @@ import {
DialogHeader,
DialogTitle,
} from '@repo/shadcn/components/ui/dialog';
import { X } from 'lucide-react';
import { ScrollArea } from '@repo/shadcn/components/ui/scroll-area';
import { Separator } from '@repo/shadcn/components/ui/separator';
import {
ExternalLink,
Factory,
MapPin,
Package,
Wrench,
X,
} from 'lucide-react';
import React, { useState } from 'react';
import { TrainingSchema } from '../schemas/training';
@@ -37,235 +46,447 @@ export function TrainingViewModal({
const DetailItem = ({ label, value }: { label: string; value: any }) => (
<div className="space-y-1">
<p className="text-sm font-medium text-muted-foreground">{label}</p>
<p className="text-sm font-semibold">{value || 'N/A'}</p>
<p className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
{label}
</p>
<p className="text-sm font-semibold text-foreground break-words">
{value !== null && value !== undefined && value !== '' ? value : 'N/A'}
</p>
</div>
);
const Section = ({
title,
icon: Icon,
children,
}: {
title: string;
icon?: React.ElementType;
children: React.ReactNode;
}) => (
<Card className="mb-4">
<CardHeader className="py-3">
<CardTitle className="text-lg">{title}</CardTitle>
<Card className="overflow-hidden border-l-4 border-l-primary/20">
<CardHeader className="py-3 bg-muted/30">
<CardTitle className="text-lg flex items-center gap-2">
{Icon && <Icon className="h-5 w-5 text-primary" />}
{title}
</CardTitle>
</CardHeader>
<CardContent className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<CardContent className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-x-4 gap-y-6 pt-4">
{children}
</CardContent>
</Card>
);
const BooleanBadge = ({ value }: { value?: boolean }) => (
<Badge variant={value ? 'default' : 'secondary'}>
{value ? 'Sí' : 'No'}
</Badge>
);
return (
<>
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="sm:max-w-[800px] overflow-y-auto [&>button:last-child]:hidden">
<DialogHeader>
<DialogTitle className="text-2xl font-bold">
Detalle de la Organización Socioproductiva
<DialogContent className="sm:max-w-[1000px] max-h-[90vh] p-0 flex flex-col">
<DialogHeader className="px-6 py-4 border-b">
<DialogTitle className="text-2xl font-bold flex items-center gap-2">
<Factory className="h-6 w-6" />
{data.ospName}
</DialogTitle>
<DialogDescription className="sr-only">
Resumen detallado de la información de la organización
socioproductiva incluyendo ubicación, responsable y registro
fotográfico.
<DialogDescription>
{data.ospType} {data.ospRif} {' '}
<span
className={
data.currentStatus === 'ACTIVA'
? 'text-green-600 font-medium'
: 'text-red-600'
}
>
{data.currentStatus}
</span>
</DialogDescription>
</DialogHeader>
<div className="mt-4 space-y-6">
{/* 1. Datos de la visita */}
<Section title="1. Datos de la visita">
<DetailItem
label="Nombre del Coordinador"
value={data.firstname}
/>
<DetailItem
label="Apellido del Coordinador"
value={data.lastname}
/>
<DetailItem
label="Fecha y hora de la visita"
value={
data.visitDate
? new Date(data.visitDate).toLocaleString()
: 'N/A'
}
/>
</Section>
<ScrollArea className="flex-1 px-6 py-6">
<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="Teléfono Coord." value={data.coorPhone} />
<DetailItem
label="Fecha Visita"
value={
data.visitDate
? new Date(data.visitDate).toLocaleString()
: 'N/A'
}
/>
</Section>
{/* 2. Datos de la OSP */}
<Section title="2. Datos de la OSP">
<DetailItem label="Nombre" value={data.ospName} />
<DetailItem label="RIF" value={data.ospRif} />
<DetailItem label="Tipo" value={data.ospType} />
<DetailItem
label="Actividad Productiva"
value={data.productiveActivity}
/>
<DetailItem
label="Estatus"
value={
<Badge
variant={
data.currentStatus === 'ACTIVA' ? 'default' : 'secondary'
}
>
{data.currentStatus}
</Badge>
}
/>
<DetailItem
label="Año de Constitución"
value={data.companyConstitutionYear}
/>
<DetailItem
label="Cant. Productores"
value={data.producerCount}
/>
<DetailItem label="Cant. Productos" value={data.productCount} />
<div className="col-span-1 md:col-span-2 lg:col-span-3">
{/* 2. Sectores y Actividad */}
<Section title="Sectores Económicos">
<DetailItem label="Sector Económico" value={data.ecoSector} />
<DetailItem
label="Descripción del Producto"
value={data.productDescription}
label="Sector Productivo"
value={data.productiveSector}
/>
</div>
<DetailItem
label="Capacidad Instalada"
value={data.installedCapacity}
/>
<DetailItem
label="Capacidad Operativa"
value={data.operationalCapacity}
/>
<div className="col-span-1 md:col-span-2 lg:col-span-3">
<DetailItem
label="Requerimiento Financiero"
value={data.financialRequirementDescription}
label="Actividad Central"
value={data.centralProductiveActivity}
/>
</div>
{data.paralysisReason && (
<div className="col-span-1 md:col-span-2 lg:col-span-3">
<DetailItem
label="Actividad Principal"
value={data.mainProductiveActivity}
/>
<div className="col-span-full">
<DetailItem
label="Razones de paralización"
value={data.paralysisReason}
label="Actividad Específica"
value={data.productiveActivity}
/>
</div>
)}
</Section>
</Section>
{/* 3. Ubicación */}
<Section title="3. Detalles de la ubicación">
<DetailItem
label="Código SITUR Comuna"
value={data.siturCodeCommune}
/>
<DetailItem
label="Consejo Comunal"
value={data.communalCouncil}
/>
<DetailItem
label="Código SITUR Consejo Comunal"
value={data.siturCodeCommunalCouncil}
/>
<div className="col-span-1 md:col-span-2 lg:col-span-3">
<DetailItem label="Dirección OSP" value={data.ospAddress} />
</div>
</Section>
{/* 3. Infraestructura y Ubicación */}
<Section title="Infraestructura y Ubicación" icon={MapPin}>
<DetailItem
label="Año Constitución"
value={data.companyConstitutionYear}
/>
<DetailItem
label="Infraestructura (m²)"
value={data.infrastructureMt2}
/>
<DetailItem
label="Tipo Estructura"
value={data.structureType}
/>
{/* 4. Responsable */}
<Section title="4. Datos del Responsable">
<DetailItem
label="Nombre Completo"
value={data.ospResponsibleFullname}
/>
<DetailItem label="Cédula" value={data.ospResponsibleCedula} />
<DetailItem label="RIF" value={data.ospResponsibleRif} />
<DetailItem label="Estado Civil" value={data.civilState} />
<DetailItem label="Teléfono" value={data.ospResponsiblePhone} />
<DetailItem label="Email" value={data.ospResponsibleEmail} />
<DetailItem label="Carga Familiar" value={data.familyBurden} />
<DetailItem
label="Número de Hijos"
value={data.numberOfChildren}
/>
</Section>
<DetailItem
label="Posee Transporte"
value={<BooleanBadge value={data.hasTransport} />}
/>
<DetailItem
label="Espacio Abierto"
value={<BooleanBadge value={data.isOpenSpace} />}
/>
{/* 5. Observaciones */}
<Card>
<CardHeader className="py-3">
<CardTitle className="text-lg">
5. Observaciones Generales
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-sm whitespace-pre-wrap">
{data.generalObservations || 'Sin observaciones'}
</p>
</CardContent>
</Card>
{/* 6. Registro fotográfico */}
<Card>
<CardHeader className="py-3">
<CardTitle className="text-lg">
6. Registro fotográfico
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
{[data.photo1, data.photo2, data.photo3].map(
(photo, idx) =>
photo && (
<div
key={idx}
className="relative aspect-video rounded-md overflow-hidden cursor-pointer hover:opacity-90 transition-opacity bg-muted"
onClick={() => setSelectedImage(photo)}
>
<img
src={`${process.env.NEXT_PUBLIC_API_URL}${photo}`}
alt={`Registro ${idx + 1}`}
className="object-cover w-full h-full"
/>
</div>
),
)}
{![data.photo1, data.photo2, data.photo3].some(Boolean) && (
<p className="text-sm text-muted-foreground">
No hay registro fotográfico
<div className="col-span-full space-y-4 mt-2">
<div className="space-y-1">
<p className="text-xs font-medium text-muted-foreground uppercase">
Dirección
</p>
<p className="text-sm font-medium">{data.ospAddress}</p>
</div>
{data.ospGoogleMapsLink && (
<Button
variant="outline"
size="sm"
asChild
className="gap-2"
>
<a
href={data.ospGoogleMapsLink}
target="_blank"
rel="noreferrer"
>
<MapPin className="h-4 w-4" />
Ver en Google Maps
<ExternalLink className="h-3 w-3" />
</a>
</Button>
)}
</div>
</CardContent>
</Card>
</div>
</Section>
<DialogFooter className="mt-6">
<Button onClick={onClose} variant="outline" className="w-32">
{/* 4. LISTAS DETALLADAS (Lo nuevo) */}
{/* PRODUCTOS */}
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-lg flex items-center gap-2">
<Package className="h-5 w-5" />
Productos y Mano de Obra
<Badge variant="secondary" className="ml-2">
{data.productList?.length || 0}
</Badge>
</CardTitle>
</CardHeader>
<CardContent className="grid gap-4">
{data.productList?.map((prod: any, idx: number) => (
<div
key={idx}
className="bg-muted/40 p-4 rounded-lg border text-sm"
>
<div className="flex justify-between items-start mb-2">
<h4 className="font-bold text-base text-primary">
{prod.productName}
</h4>
<Badge variant="outline">
Mano de obra:{' '}
{Number(prod.menCount || 0) +
Number(prod.womenCount || 0)}
</Badge>
</div>
<p className="text-muted-foreground mb-3">
{prod.description}
</p>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-3">
<DetailItem label="Diario" value={prod.dailyCount} />
<DetailItem label="Semanal" value={prod.weeklyCount} />
<DetailItem label="Mensual" value={prod.monthlyCount} />
<DetailItem
label="Hombres / Mujeres"
value={`${prod.menCount || 0} / ${prod.womenCount || 0}`}
/>
</div>
{/* Detalles de distribución si existen */}
{(prod.internalQuantity || prod.externalQuantity) && (
<>
<Separator className="my-2" />
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{prod.internalQuantity && (
<div>
<span className="text-xs font-bold text-muted-foreground block mb-1">
DISTRIBUCIÓN INTERNA
</span>
<p>Cant: {prod.internalQuantity}</p>
<p className="text-xs text-muted-foreground">
{prod.internalDescription}
</p>
</div>
)}
{prod.externalQuantity && (
<div>
<span className="text-xs font-bold text-muted-foreground block mb-1">
EXPORTACIÓN ({prod.externalCountry})
</span>
<p>Cant: {prod.externalQuantity}</p>
<p className="text-xs text-muted-foreground">
{prod.externalDescription}
</p>
</div>
)}
</div>
</>
)}
</div>
))}
{(!data.productList || data.productList.length === 0) && (
<p className="text-sm text-muted-foreground italic">
No hay productos registrados.
</p>
)}
</CardContent>
</Card>
{/* EQUIPAMIENTO Y PRODUCCIÓN */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-lg flex items-center gap-2">
<Wrench className="h-5 w-5" />
Equipamiento
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{data.equipmentList?.map((eq: any, idx: number) => (
<div
key={idx}
className="flex justify-between items-center p-2 rounded bg-muted/40 border"
>
<div>
<p className="font-medium">{eq.machine}</p>
<p className="text-xs text-muted-foreground">
{eq.specifications}
</p>
</div>
<Badge
variant="outline"
className="text-sm font-bold h-8 w-8 flex items-center justify-center rounded-full"
>
{eq.quantity}
</Badge>
</div>
))}
{(!data.equipmentList ||
data.equipmentList.length === 0) && (
<p className="text-sm text-muted-foreground italic">
No hay equipamiento registrado.
</p>
)}
</CardContent>
</Card>
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-lg flex items-center gap-2">
<Factory className="h-5 w-5" />
Materia Prima
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{data.productionList?.map((mat: any, idx: number) => (
<div
key={idx}
className="flex justify-between items-center p-2 rounded bg-muted/40 border"
>
<div>
<p className="font-medium">{mat.rawMaterial}</p>
<p className="text-xs text-muted-foreground">
{mat.supplyType}
</p>
</div>
<Badge variant="secondary">Cant: {mat.quantity}</Badge>
</div>
))}
{(!data.productionList ||
data.productionList.length === 0) && (
<p className="text-sm text-muted-foreground italic">
No hay materia prima registrada.
</p>
)}
</CardContent>
</Card>
</div>
{/* 5. Comuna y Responsable */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Section title="Datos de la Comuna">
<DetailItem label="Comuna" value={data.communeName} />
<DetailItem
label="Código SITUR"
value={data.siturCodeCommune}
/>
<DetailItem
label="Vocero"
value={data.communeSpokespersonName}
/>
<DetailItem
label="Teléfono"
value={data.communeSpokespersonPhone}
/>
<div className="col-span-full border-t pt-4 mt-2">
<DetailItem
label="Consejo Comunal"
value={data.communalCouncil}
/>
<DetailItem
label="Vocero C.C."
value={data.communalCouncilSpokespersonName}
/>
</div>
</Section>
<Section title="Responsable OSP">
<DetailItem
label="Nombre"
value={data.ospResponsibleFullname}
/>
<DetailItem
label="Cédula"
value={data.ospResponsibleCedula}
/>
<DetailItem
label="Teléfono"
value={data.ospResponsiblePhone}
/>
<DetailItem label="Email" value={data.ospResponsibleEmail} />
<DetailItem
label="Carga Familiar"
value={data.familyBurden}
/>
<DetailItem label="Hijos" value={data.numberOfChildren} />
</Section>
</div>
{/* 6. Observaciones */}
{(data.generalObservations || data.paralysisReason) && (
<Card>
<CardHeader>
<CardTitle>Observaciones</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{data.generalObservations && (
<div>
<p className="text-xs font-bold text-muted-foreground uppercase mb-1">
Generales
</p>
<p className="text-sm">{data.generalObservations}</p>
</div>
)}
{data.paralysisReason && (
<div className="p-3 bg-red-50 dark:bg-red-900/20 rounded-md border border-red-200 dark:border-red-900">
<p className="text-xs font-bold text-red-600 dark:text-red-400 uppercase mb-1">
Motivo Paralización
</p>
<p className="text-sm">{data.paralysisReason}</p>
</div>
)}
</CardContent>
</Card>
)}
{/* 7. Fotos */}
<Section title="Registro Fotográfico">
{[data.photo1, data.photo2, data.photo3].some(Boolean) ? (
<div className="col-span-full grid grid-cols-1 sm:grid-cols-3 gap-4">
{[data.photo1, data.photo2, data.photo3].map(
(photo, idx) =>
photo && (
<div
key={idx}
className="relative aspect-video rounded-lg overflow-hidden cursor-zoom-in border hover:shadow-lg transition-all"
onClick={() => setSelectedImage(photo)}
>
<img
src={`${process.env.NEXT_PUBLIC_API_URL}${photo}`}
alt={`Evidencia ${idx + 1}`}
className="object-cover w-full h-full"
/>
</div>
),
)}
</div>
) : (
<p className="text-sm text-muted-foreground col-span-full">
No hay imágenes cargadas.
</p>
)}
</Section>
</div>
</ScrollArea>
<DialogFooter className="px-6 py-4 border-t bg-muted/20">
<Button
onClick={onClose}
variant="outline"
className="w-full sm:w-auto"
>
Cerrar
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Lightbox */}
{/* Lightbox para imágenes */}
<Dialog
open={!!selectedImage}
onOpenChange={() => setSelectedImage(null)}
>
<DialogContent className="max-w-[95vw] max-h-[95vh] p-0 overflow-hidden bg-black/90 border-none [&>button:last-child]:hidden">
<DialogContent className="max-w-[95vw] max-h-[95vh] p-0 overflow-hidden bg-black/95 border-none">
<DialogHeader className="sr-only">
<DialogTitle>Vista ampliada de la imagen</DialogTitle>
<DialogDescription>
Imagen ampliada del registro fotográfico de la organización.
</DialogDescription>
<DialogTitle>Imagen Ampliada</DialogTitle>
</DialogHeader>
<div className="relative w-full h-full flex items-center justify-center p-4">
<DialogDescription></DialogDescription>
<div className="relative w-full h-full flex items-center justify-center p-2">
<Button
variant="ghost"
size="icon"
className="absolute top-2 right-2 text-white hover:bg-white/20 z-10"
className="absolute top-4 right-4 text-white hover:bg-white/20 rounded-full z-50"
onClick={() => setSelectedImage(null)}
>
<X className="h-6 w-6" />
@@ -273,8 +494,8 @@ export function TrainingViewModal({
{selectedImage && (
<img
src={`${process.env.NEXT_PUBLIC_API_URL}${selectedImage}`}
alt="Expanded view"
className="max-w-full max-h-[90vh] object-contain"
alt="Vista ampliada"
className="max-w-full max-h-[90vh] object-contain rounded-md"
/>
)}
</div>

View File

@@ -0,0 +1,169 @@
export const SECTOR_ECONOMICO = {
PRIMARIO: 'PRIMARIO',
SECUNDARIO: 'SECUNDARIO',
TERCIARIO: 'TERCIARIO',
} as const;
export const SECTOR_PRODUCTIVO = {
AGRICOLA: 'AGRÍCOLA',
MANUFACTURA: 'MANUFACTURA',
SERVICIOS: 'SERVICIOS',
TURISMO: 'TURISMO',
COMERCIO: 'COMERCIO',
} as const;
export const ACTIVIDAD_CENTRAL = {
PRODUCCION_VEGETAL: 'PRODUCCIÓN VEGETAL',
PRODUCCION_ANIMAL: 'PRODUCCIÓN ANIMAL',
PRODUCCION_VEGETAL_ANIMAL: 'PRODUCCIÓN VEGETAL Y ANIMAL',
INDUSTRIAL: 'INDUSTRIAL',
SERVICIOS: 'SERVICIOS',
TURISMO: 'TURISMO',
COMERCIO: 'COMERCIO',
} as const;
export const ACTIVIDAD_PRINCIPAL = {
AGRICULTURA: 'AGRICULTURA',
CRIA: 'CRIA',
PATIOS_PRODUCTIVOS: 'PATIOS PRODUCTIVOS O CONUCOS',
TRANSFORMACION_MATERIA: 'TRANSFORMACION DE LA MATERIA PRIMA',
TEXTIL: 'TALLER DE COFECCION TEXTIL',
CONSTRUCCION: 'CONSTRUCION',
BIENES_SERVICIOS: 'OFRECER PRODUCTOS DE BIENES Y SERVICIOS',
VISITAS_GUIADAS: 'VISITAS GUIADAS',
ALOJAMIENTO: 'ALOJAMIENTO',
TURISMO: 'TURISMO',
COMERCIO: 'COMERCIO',
} as const;
export const SECTOR_ECONOMICO_OPTIONS = [
SECTOR_ECONOMICO.PRIMARIO,
SECTOR_ECONOMICO.SECUNDARIO,
SECTOR_ECONOMICO.TERCIARIO,
];
// Map: Sector Economico -> Productive Sectors
export const SECTOR_PRODUCTIVO_MAP: Record<string, string[]> = {
[SECTOR_ECONOMICO.PRIMARIO]: [SECTOR_PRODUCTIVO.AGRICOLA],
[SECTOR_ECONOMICO.SECUNDARIO]: [SECTOR_PRODUCTIVO.MANUFACTURA],
[SECTOR_ECONOMICO.TERCIARIO]: [
SECTOR_PRODUCTIVO.SERVICIOS,
SECTOR_PRODUCTIVO.TURISMO,
SECTOR_PRODUCTIVO.COMERCIO,
],
};
// Map: Productive Sector -> Central Productive Activity
export const ACTIVIDAD_CENTRAL_MAP: Record<string, string[]> = {
[SECTOR_PRODUCTIVO.AGRICOLA]: [
ACTIVIDAD_CENTRAL.PRODUCCION_VEGETAL,
ACTIVIDAD_CENTRAL.PRODUCCION_ANIMAL,
ACTIVIDAD_CENTRAL.PRODUCCION_VEGETAL_ANIMAL,
],
[SECTOR_PRODUCTIVO.MANUFACTURA]: [ACTIVIDAD_CENTRAL.INDUSTRIAL],
[SECTOR_PRODUCTIVO.SERVICIOS]: [ACTIVIDAD_CENTRAL.SERVICIOS],
[SECTOR_PRODUCTIVO.TURISMO]: [ACTIVIDAD_CENTRAL.TURISMO],
[SECTOR_PRODUCTIVO.COMERCIO]: [ACTIVIDAD_CENTRAL.COMERCIO],
};
// Map: Central Productive Activity -> Main Productive Activity
export const ACTIVIDAD_PRINCIPAL_MAP: Record<string, string[]> = {
[ACTIVIDAD_CENTRAL.PRODUCCION_VEGETAL]: [ACTIVIDAD_PRINCIPAL.AGRICULTURA],
[ACTIVIDAD_CENTRAL.PRODUCCION_ANIMAL]: [ACTIVIDAD_PRINCIPAL.CRIA],
[ACTIVIDAD_CENTRAL.PRODUCCION_VEGETAL_ANIMAL]: [
ACTIVIDAD_PRINCIPAL.PATIOS_PRODUCTIVOS,
],
[ACTIVIDAD_CENTRAL.INDUSTRIAL]: [
ACTIVIDAD_PRINCIPAL.TRANSFORMACION_MATERIA,
ACTIVIDAD_PRINCIPAL.TEXTIL,
ACTIVIDAD_PRINCIPAL.CONSTRUCCION,
],
[ACTIVIDAD_CENTRAL.SERVICIOS]: [ACTIVIDAD_PRINCIPAL.BIENES_SERVICIOS],
[ACTIVIDAD_CENTRAL.TURISMO]: [
ACTIVIDAD_PRINCIPAL.VISITAS_GUIADAS,
ACTIVIDAD_PRINCIPAL.ALOJAMIENTO,
ACTIVIDAD_PRINCIPAL.TURISMO,
],
[ACTIVIDAD_CENTRAL.COMERCIO]: [ACTIVIDAD_PRINCIPAL.COMERCIO],
};
// Map: Main Productive Activity -> Productive Activity (The long list)
export const ACTIVIDAD_PRODUCTIVA_MAP: Record<string, string[]> = {
[ACTIVIDAD_PRINCIPAL.AGRICULTURA]: [
'SIEMBRA DE MAIZ',
'SIEMBRA DE AJI',
'SIEMBRA DE CAFÉ',
'SIEMBRA DE PLATANO',
'SIEMBRA DE CAMBUR',
'SIEMBRA DE AGUACATE',
'SIEMBRA DE FRUTAS',
'SIEMBRA DE HORTALIZAS',
'SIEMBRA DE TOMATE',
'SIEMBRA DE CACAO',
'SIEMBRA DE PIMENTON',
'SIEMBRA DE YUCA',
'SIEMBRA DE CAÑA DE AZUCAR',
'SIEMBRA DE GRANOS (CARAOTAS, FRIJOLES)',
'SIEMBRA DE ARROZ',
'SIEMBRA DE CEREALES (CEBADA, LINAZA, SOYA)',
'ELABORACION DE BIO-INSUMO (ABONO ORGANICO)',
],
[ACTIVIDAD_PRINCIPAL.CRIA]: [
'BOVINO',
'PORCINO',
'CAPRINO',
'CUNICULTURA',
'AVICOLA',
'PISCICULA',
],
[ACTIVIDAD_PRINCIPAL.PATIOS_PRODUCTIVOS]: ['SIEMBRA Y CRIA'],
[ACTIVIDAD_PRINCIPAL.TRANSFORMACION_MATERIA]: [
'ELABORACION DE PRODUCTOS QUIMICOS (LIMPIEZA E HIGIENE PERSONAL)',
'PANADERIAS',
'RESPOSTERIA',
'ELABORACION DE HARINAS PRECOCIDA',
'PLANTA ABA (ELABORACION DE ALIMENTOS BALANCEADOS PARA ANIMALES)',
'ELABORACION DE PRODUCTOS DERIVADO DE LA LECHE (VACA, CABRA, BUFFALA)',
'EMPAQUETADORAS DE GRANOS Y POLVOS',
'ELABORACION DE ACEITE COMESTIBLE',
'FABRICA DE HIELO',
'ELABORACION DE PAPELON',
'ARTESANIAS',
],
[ACTIVIDAD_PRINCIPAL.TEXTIL]: [
'ELABORACION DE UNIFORME ESCOLARES Y PRENDA DE VESTIR',
'ELABORACION DE PRENDAS INTIMAS',
'ELABORACION DE LENCERIA',
'SUBLIMACION DE TEJIDOS',
'ELABORACION DE CALZADOS',
],
[ACTIVIDAD_PRINCIPAL.CONSTRUCCION]: [
'BLOQUERAS',
'PLANTA PREMEZCLADORA DE CEMENTO',
'CARPINTERIAS',
'HERRERIAS',
],
[ACTIVIDAD_PRINCIPAL.BIENES_SERVICIOS]: [
'MERCADOS COMUNALES',
'CENTROS DE ACOPIOS Y DISTRIBUCION',
'UNIDAD DE SUMINISTRO',
'MATADERO (SALA DE MATANZA DE ANIMALES)',
'PELUQUERIA',
'BARBERIA',
'AGENCIAS DE FESTEJOS',
'LAVANDERIAS',
'REPARACION DE CALZADOS',
'TALLER DE MECANICA',
'TRANSPORTES',
],
[ACTIVIDAD_PRINCIPAL.VISITAS_GUIADAS]: ['RUTAS TURISTICAS'],
[ACTIVIDAD_PRINCIPAL.ALOJAMIENTO]: ['POSADAS', 'HOTELES'],
[ACTIVIDAD_PRINCIPAL.TURISMO]: ['AGENCIAS DE VIAJES'],
[ACTIVIDAD_PRINCIPAL.COMERCIO]: [
'VENTA DE VIVERES',
'VENTAS DE PRENDAS DE VESTIR',
'VENTA DE PRODUCTOS QUIMICOS Y DERIVADOS',
'BODEGAS COMUNALES',
'FRIGORIFICOS Y CARNICOS',
],
};

View File

@@ -1,23 +1,27 @@
import { z } from 'zod';
export const statisticsItemSchema = z.object({
name: z.string(),
value: z.number(),
name: z
.string()
.nullable()
.transform((val) => val || 'Sin Información'),
value: z.number(),
});
export const trainingStatisticsSchema = z.object({
totalOsps: z.number(),
totalProducers: z.number(),
statusDistribution: z.array(statisticsItemSchema),
activityDistribution: z.array(statisticsItemSchema),
typeDistribution: z.array(statisticsItemSchema),
stateDistribution: z.array(statisticsItemSchema),
yearDistribution: z.array(statisticsItemSchema),
totalOsps: z.number(),
totalProducers: z.number(),
totalProducts: z.number(),
statusDistribution: z.array(statisticsItemSchema),
activityDistribution: z.array(statisticsItemSchema),
typeDistribution: z.array(statisticsItemSchema),
stateDistribution: z.array(statisticsItemSchema),
yearDistribution: z.array(statisticsItemSchema),
});
export type TrainingStatisticsData = z.infer<typeof trainingStatisticsSchema>;
export const trainingStatisticsResponseSchema = z.object({
message: z.string(),
data: trainingStatisticsSchema,
message: z.string(),
data: trainingStatisticsSchema,
});

View File

@@ -1,154 +1,166 @@
import { z } from 'zod';
// 1. Definimos el esquema de un item individual de la lista de productos
// Basado en los campos que usaste en ProductActivityList
const productItemSchema = z.object({
productName: z.string(),
description: z.string().optional(),
dailyCount: z.coerce.string().or(z.number()).optional(), // Aceptamos string o number por los inputs
weeklyCount: z.coerce.string().or(z.number()).optional(),
monthlyCount: z.coerce.string().or(z.number()).optional(),
// Distribución Interna
internalState: z.number().optional(),
internalMunicipality: z.number().optional(),
internalParish: z.number().optional(),
internalDescription: z.string().optional(),
internalQuantity: z.coerce.string().or(z.number()).optional(),
// Distribución Externa
externalCountry: z.string().optional(),
externalState: z.number().optional(),
externalMunicipality: z.number().optional(),
externalParish: z.number().optional(),
externalCity: z.string().optional(),
externalDescription: z.string().optional(),
externalQuantity: z.coerce.string().or(z.number()).optional(),
// Mano de obra
womenCount: z.coerce.string().or(z.number()).optional(),
menCount: z.coerce.string().or(z.number()).optional(),
});
const productionItemSchema = z.object({
rawMaterial: z.string(),
supplyType: z.string().optional(),
quantity: z.coerce.string().or(z.number()).optional(), // Aceptamos string o number por los inputs
});
const equipmentItemSchema = z.object({
machine: z.string(),
specifications: z.string().optional(),
quantity: z.coerce.string().or(z.number()).optional(), // Aceptamos string o number por los inputs
});
export const trainingSchema = z.object({
//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' }),
coorPhone: z.string().optional().nullable(),
visitDate: z
.string()
.min(1, { message: 'Fecha y hora de visita es requerida' }),
//Datos de la organización socioproductiva (OSP)
ospType: z.string().min(1, { message: 'Tipo de OSP es requerido' }),
ecoSector: z.string().optional().or(z.literal('')),
productiveSector: z.string().optional().or(z.literal('')),
centralProductiveActivity: z.string().optional().or(z.literal('')),
mainProductiveActivity: z.string().optional().or(z.literal('')),
productiveActivity: z
.string()
.min(1, { message: 'Actividad productiva es requerida' }),
// financialRequirementDescription: z
// .string()
// .min(1, { message: 'Descripción es requerida' }),
siturCodeCommune: z
.string()
.min(1, { message: 'Código SITUR Comuna es requerido' }),
communeName: z
.string()
.min(1, { message: 'Nombre de la Comuna es requerido' }),
communeRif: z
.string()
.min(1, { message: 'RIF de la Comuna es requerido' }),
communeSpokespersonName: z
.string()
.min(1, { message: 'Nombre del Vocero de la Comuna es requerido' }),
communeSpokespersonCedula: z
.string()
.min(1, { message: 'Cédula del Vocero de la Comuna es requerida' }),
communeSpokespersonRif: z
.string()
.min(1, { message: 'RIF del Vocero de la Comuna es requerido' }),
communeSpokespersonPhone: z
.string()
.min(1, { message: 'Teléfono del Vocero de la Comuna es requerido' }),
communeEmail: z.string().email({ message: 'Correo electrónico de la Comuna inválido' }),
communalCouncil: z
.string()
.min(1, { message: 'Consejo Comunal es requerido' }),
siturCodeCommunalCouncil: z
.string()
.min(1, { message: 'Código SITUR Consejo Comunal es requerido' }),
communalCouncilRif: z
.string()
.min(1, { message: 'RIF del Consejo Comunal es requerido' }),
communalCouncilSpokespersonName: z
.string()
.min(1, { message: 'Nombre del Vocero del Consejo Comunal es requerido' }),
communalCouncilSpokespersonCedula: z
.string()
.min(1, { message: 'Cédula del Vocero del Consejo Comunal es requerida' }),
communalCouncilSpokespersonRif: z
.string()
.min(1, { message: 'RIF del Vocero del Consejo Comunal es requerido' }),
communalCouncilSpokespersonPhone: z
.string()
.min(1, { message: 'Teléfono del Vocero del Consejo Comunal es requerido' }),
communalCouncilEmail: z
.string()
.email({ message: 'Correo electrónico del Consejo Comunal inválido' }),
ospGoogleMapsLink: z
.string()
.min(1, { message: 'Enlace de Google Maps es requerido' }),
ospRif: z.string().optional().or(z.literal('')),
ospName: z.string().min(1, { message: 'Nombre de la OSP es requerido' }),
ospAddress: z
.string()
.min(1, { message: 'Dirección de la OSP es requerida' }),
ospRif: z.string().min(1, { message: 'RIF de la OSP es requerido' }),
ospType: z.string().min(1, { message: 'Tipo de OSP es requerido' }),
companyConstitutionYear: z.coerce
.number()
.min(1900, { message: 'Año inválido' }),
currentStatus: z
.string()
.min(1, { message: 'Estatus actual es requerido' })
.default('ACTIVA'),
companyConstitutionYear: z.coerce
.number()
.min(1900, { message: 'Año inválido' }),
producerCount: z.coerce
.number()
.min(0, { message: 'Cantidad de productores requerida' }),
// productCount: z.coerce
// .number()
// .min(0, { message: 'Cantidad de productos requerida' })
// .optional(),
productDescription: z
infrastructureMt2: z.string().optional().or(z.literal('')),
hasTransport: z
.preprocess((val) => val === 'true' || val === true, z.boolean())
.optional(),
structureType: z.string().optional().or(z.literal('')),
isOpenSpace: z
.preprocess((val) => val === 'true' || val === true, z.boolean())
.optional(),
paralysisReason: z.string().optional().default(''),
//Datos del Equipamiento
equipmentList: z.array(equipmentItemSchema).optional().default([]),
//Datos de Producción
productionList: z.array(productionItemSchema).optional().default([]),
// Datos de Actividad Productiva
productList: z.array(productItemSchema).optional().default([]),
//Detalles de la ubicación
ospAddress: z
.string()
.min(1, { message: 'Descripción del producto es requerida' }),
prodDescriptionInternal: z
.min(1, { message: 'Dirección de la OSP es requerida' }),
ospGoogleMapsLink: z.string().optional().or(z.literal('')),
communeName: z.string().optional().or(z.literal('')),
siturCodeCommune: z.string().optional().or(z.literal('')),
communeRif: z.string().optional().or(z.literal('')),
communeSpokespersonName: z.string().optional().or(z.literal('')),
communeSpokespersonCedula: z.string().optional().or(z.literal('')),
communeSpokespersonRif: z.string().optional().or(z.literal('')),
communeSpokespersonPhone: z.string().optional().or(z.literal('')),
communeEmail: z
.string()
.min(1, { message: 'Descripción del producto es requerida' }),
installedCapacity: z
.email({ message: 'Correo electrónico de la Comuna inválido' })
.optional()
.or(z.literal('')),
communalCouncil: z
.string()
.min(1, { message: 'Capacidad instalada es requerida' }),
operationalCapacity: z
.min(1, { message: 'Consejo Comunal es requerido' }),
siturCodeCommunalCouncil: z.string().optional().or(z.literal('')),
communalCouncilRif: z.string().optional().or(z.literal('')),
communalCouncilSpokespersonName: z.string().optional().or(z.literal('')),
communalCouncilSpokespersonCedula: z.string().optional().or(z.literal('')),
communalCouncilSpokespersonRif: z.string().optional().or(z.literal('')),
communalCouncilSpokespersonPhone: z.string().optional().or(z.literal('')),
communalCouncilEmail: z
.string()
.min(1, { message: 'Capacidad operativa es requerida' }),
ospResponsibleFullname: z
.string()
.min(1, { message: 'Nombre del responsable es requerido' }),
.email({ message: 'Correo electrónico del Consejo Comunal inválido' })
.optional()
.or(z.literal('')),
//Datos del Responsable OSP
ospResponsibleCedula: z
.string()
.min(1, { message: 'Cédula del responsable es requerida' }),
ospResponsibleFullname: z
.string()
.min(1, { message: 'Nombre del responsable es requerido' }),
ospResponsibleRif: z
.string()
.min(1, { message: 'RIF del responsable es requerido' }),
civilState: z.string().min(1, { message: 'Estado civil es requerido' }),
ospResponsiblePhone: z
.string()
.min(1, { message: 'Teléfono del responsable es requerido' }),
civilState: z.string().min(1, { message: 'Estado civil es requerido' }),
ospResponsibleEmail: z
.string()
.email({ message: 'Correo electrónico inválido' }),
familyBurden: z.coerce
.number()
.min(0, { message: 'Carga familiar requerida' }),
numberOfChildren: z.coerce
.number()
.min(0, { message: 'Número de hijos requerido' }),
ospResponsibleEmail: z
.string()
.email({ message: 'Correo electrónico inválido' }),
//Datos adicionales
generalObservations: z.string().optional().default(''),
photo1: z.string().optional().nullable(),
photo2: z.string().optional().nullable(),
photo3: z.string().optional().nullable(),
//IMAGENES
files: z.any().optional(),
paralysisReason: z.string().optional().default(''),
//no se envia la backend al crear ni editar el formulario
state: z.number().optional().nullable(),
municipality: z.number().optional().nullable(),
parish: z.number().optional().nullable(),
coorState: z.number().optional().nullable(),
coorMunicipality: z.number().optional().nullable(),
coorParish: z.number().optional().nullable(),
coorPhone: z.string().optional().nullable(),
ecoSector: z.string().min(1, { message: 'Sector económico es requerido' }),
productiveSector: z.string().min(1, { message: 'Sector productivo es requerido' }),
centralProductiveActivity: z.string().min(1, { message: 'Actividad productiva central es requerida' }),
mainProductiveActivity: z.string().min(1, { message: 'Actividad productiva principal es requerida' }),
typesOfEquipment: z.string().min(1, { message: 'Tipo de equipo es requerido' }),
equipmentCount: z.coerce.number().min(0, { message: 'Cantidad de equipo requerida' }),
equipmentDescription: z.string().min(1, { message: 'Descripción del equipo es requerida' }),
rawMaterial: z.string().min(1, { message: 'Material bruto es requerido' }),
materialType: z.string().min(1, { message: 'Tipo de material es requerido' }),
rawMaterialCount: z.coerce.number().min(0, { message: 'Cantidad de material bruto requerida' }),
productCountDaily: z.coerce.number().min(0, { message: 'Cantidad diaria de productos requerida' }),
productCountWeekly: z.coerce.number().min(0, { message: 'Cantidad semanal de productos requerida' }),
productCountMonthly: z.coerce.number().min(0, { message: 'Cantidad mensual de productos requerida' }),
internalCount: z.coerce.number().min(0, { message: 'Cantidad interna requerida' }),
externalCount: z.coerce.number().min(0, { message: 'Cantidad externa requerida' }),
prodDescriptionExternal: z.string().min(1, { message: 'Descripción del producto es requerida' }),
country: z.string().min(1, { message: 'País es requerido' }),
city: z.string().min(1, { message: 'Ciudad es requerida' }),
menCount: z.coerce.number().min(0, { message: 'Cantidad de hombres requerida' }),
womenCount: z.coerce.number().min(0, { message: 'Cantidad de mujeres requerida' }),
photo1: z.string().optional().nullable(),
photo2: z.string().optional().nullable(),
photo3: z.string().optional().nullable(),
});
export type TrainingSchema = z.infer<typeof trainingSchema>;