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

@@ -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>