Merge branch 'main' of ssh://git.fondemi.gob.ve:222/Fondemi/sistema_base

This commit is contained in:
2025-12-15 13:06:33 -04:00
33 changed files with 5573 additions and 227 deletions

View File

@@ -1,13 +1,14 @@
'use client';
import { DataTable } from '@repo/shadcn/table/data-table';
import { DataTableSkeleton } from '@repo/shadcn/table/data-table-skeleton';
import { columns } from './product-tables/columns';
import { useProductQuery } from '../../hooks/use-query-products';
import { columns } from './product-tables/columns';
interface dataListProps {
initialPage: number;
initialSearch?: string | null;
initialLimit: number;
initialType?: string | null;
}
export default function UsersAdminList({
@@ -19,9 +20,9 @@ export default function UsersAdminList({
page: initialPage,
limit: initialLimit,
...(initialSearch && { search: initialSearch }),
}
};
const {data, isLoading} = useProductQuery(filters)
const { data, isLoading } = useProductQuery(filters);
// console.log(data?.data);

View File

@@ -0,0 +1,123 @@
'use server';
import { safeFetchApi } from '@/lib/fetch.api';
import {
TrainingSchema,
TrainingMutate,
trainingApiResponseSchema
} from '../schemas/training';
import { trainingStatisticsResponseSchema } from '../schemas/statistics';
export const getTrainingStatisticsAction = async (params: {
startDate?: string;
endDate?: string;
stateId?: number;
municipalityId?: number;
parishId?: number;
ospType?: string;
} = {}) => {
const searchParams = new URLSearchParams();
if (params.startDate) searchParams.append('startDate', params.startDate);
if (params.endDate) searchParams.append('endDate', params.endDate);
if (params.stateId) searchParams.append('stateId', params.stateId.toString());
if (params.municipalityId) searchParams.append('municipalityId', params.municipalityId.toString());
if (params.parishId) searchParams.append('parishId', params.parishId.toString());
if (params.ospType) searchParams.append('ospType', params.ospType);
const [error, response] = await safeFetchApi(
trainingStatisticsResponseSchema,
`/training/statistics?${searchParams.toString()}`,
'GET',
);
if (error) throw new Error(error.message);
return response?.data;
}
export const getTrainingAction = async (params: {
page?: number;
limit?: number;
search?: string;
sortBy?: string;
sortOrder?: 'asc' | 'desc';
}) => {
const searchParams = new URLSearchParams({
page: (params.page || 1).toString(),
limit: (params.limit || 10).toString(),
...(params.search && { search: params.search }),
...(params.sortBy && { sortBy: params.sortBy }),
...(params.sortOrder && { sortOrder: params.sortOrder }),
});
const [error, response] = await safeFetchApi(
trainingApiResponseSchema,
`/training?${searchParams}`,
'GET',
);
if (error) throw new Error(error.message);
return {
data: response?.data || [],
meta: response?.meta || {
page: 1,
limit: 10,
totalCount: 0,
totalPages: 1,
hasNextPage: false,
hasPreviousPage: false,
nextPage: null,
previousPage: null,
},
};
}
export const createTrainingAction = async (payload: TrainingSchema) => {
const { id, ...payloadWithoutId } = payload;
const [error, data] = await safeFetchApi(
TrainingMutate,
'/training',
'POST',
payloadWithoutId,
);
if (error) {
throw new Error(error.message || 'Error al crear el registro');
}
return data;
};
export const updateTrainingAction = async (payload: TrainingSchema) => {
const { id, ...payloadWithoutId } = payload;
if (!id) throw new Error('ID es requerido para actualizar');
const [error, data] = await safeFetchApi(
TrainingMutate,
`/training/${id}`,
'PATCH',
payloadWithoutId,
);
if (error) {
throw new Error(error.message || 'Error al actualizar el registro');
}
return data;
};
export const deleteTrainingAction = async (id: number) => {
const [error] = await safeFetchApi(
TrainingMutate,
`/training/${id}`,
'DELETE'
)
if (error) throw new Error(error.message || 'Error al eliminar el registro');
return true;
}

View File

@@ -0,0 +1,41 @@
-- datos basicos
nombre,
apellido,
fecha de la visita,
-->Falta
hora de la visita,
-- datos de la ubicacion
estado,
municipio,
parroquia,
nombre de la comuna,
CODIGO SITUR COMUNA,
CONSEJO COMUNAL,
CODIGO SITUR CONSEJO COMUNAL,
-- datos de la osp
actividad productiva (agricola,textil,bloquera,carpinteria,unidad de suministro),
realice una breve descripcion del requerimiento financiero,
NOMBRE DE LA ORGANIZACIÓN SOCIOPRODUCTIVA,
DIRECCIÓN DE LA ORGANIZACIÓN SOCIOPRODUCTIVA,
RIF DE LA ORGANIZACIÓN SOCIOPRODUCTIVA,
TIPO DE ORGANIZACIÓN SOCIOPRODUCTIVA,
ESTATUS ACTUAL,
AÑO DE CONSTITUCIÓN DE LA EMPRESA ,
CANTIDAD DE PRODUCTORES QUE LA CONFORMAN,
BREVE DESCRIPCIÓN DEL PRODUCTO O SERVICIO QUE OFRECE,
CAPACIDAD INSTALADA,
CAPACIDAD OPERATIVA,
¿EXPLIQUE LAS RAZONES GENERALES POR LAS CUALES LA UNIDAD DE PRODUCCIÓN TUVO QUE PARALIZARSE?
-- datos del responsable
NOMBRE Y APELLIDO DEL RESPONSABLE DE LA OSP,
CÉDULA DEL RESPONSABLE (SIN PUNTOS),
RIF DEL RESPONSABLE (SIN PUNTOS),
TELÉFONOS (COLOQUE 2 NUMEROS DE TELEFONOS),
CORREO ELECTRÓNICO,
ESTADO CIVIL DEL PRODUCTOR,
CARGA FAMILIAR,
NUMERO DE HIJOS,
-- datos adicionales
OBSERVACIONES GENERALES,
-- fotos
COLOCAR TRES (3) REGISTROS FOTOGRÁFICOS VISIBLES DEL ESPACIO Y MAQUINARIAS ACTUALMENTE (OBLIGATORIO),

View File

@@ -0,0 +1,558 @@
'use client';
import { zodResolver } from '@hookform/resolvers/zod';
import { Button } from '@repo/shadcn/button';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@repo/shadcn/form';
import { Input } from '@repo/shadcn/input';
import { Textarea } from '@repo/shadcn/textarea';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@repo/shadcn/select';
import { useForm } from 'react-hook-form';
import { useCreateTraining } from "../hooks/use-training";
import { TrainingSchema, trainingSchema } from '../schemas/training';
import { SelectSearchable } from '@repo/shadcn/select-searchable'
import React from 'react';
import { useStateQuery, useMunicipalityQuery, useParishQuery } from '@/feactures/location/hooks/use-query-location';
const PRODUCTIVE_ACTIVITIES = [
'Agricola',
'Textil',
'Bloquera',
'Carpinteria',
'Unidad de suministro'
];
interface CreateTrainingFormProps {
onSuccess?: () => void;
onCancel?: () => void;
defaultValues?: Partial<TrainingSchema>;
}
export function CreateTrainingForm({
onSuccess,
onCancel,
defaultValues,
}: CreateTrainingFormProps) {
const {
mutate: saveTraining,
isPending: isSaving,
} = useCreateTraining();
const [state, setState] = React.useState(0);
const [municipality, setMunicipality] = React.useState(0);
const [disabledMunicipality, setDisabledMunicipality] = React.useState(true);
const [disabledParish, setDisabledParish] = React.useState(true);
const { data: dataState } = useStateQuery()
const { data: dataMunicipality } = useMunicipalityQuery(state)
const { data: dataParish } = useParishQuery(municipality)
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 = dataParish?.data || [{id:0,municipalityId:0,name:'Sin Parroquias'}]
const parishOptions = Array.isArray(dataParish?.data) && dataParish.data.length > 0
? dataParish.data
: [{ id: 0, stateId: 0, name: 'Sin Parroquias' }]
const form = useForm<TrainingSchema>({
resolver: zodResolver(trainingSchema),
defaultValues: {
firstname: defaultValues?.firstname || '',
lastname: defaultValues?.lastname || '',
visitDate: defaultValues?.visitDate || new Date().toISOString().split('T')[0],
productiveActivity: defaultValues?.productiveActivity || '',
financialRequirementDescription: defaultValues?.financialRequirementDescription || '',
siturCodeCommune: defaultValues?.siturCodeCommune || '',
communalCouncil: defaultValues?.communalCouncil || '',
siturCodeCommunalCouncil: defaultValues?.siturCodeCommunalCouncil || '',
ospName: defaultValues?.ospName || '',
ospAddress: defaultValues?.ospAddress || '',
ospRif: defaultValues?.ospRif || '',
ospType: defaultValues?.ospType || '',
currentStatus: defaultValues?.currentStatus || '',
companyConstitutionYear: defaultValues?.companyConstitutionYear || new Date().getFullYear(),
producerCount: defaultValues?.producerCount || 0,
productDescription: defaultValues?.productDescription || '',
installedCapacity: defaultValues?.installedCapacity || '',
operationalCapacity: defaultValues?.operationalCapacity || '',
ospResponsibleFullname: defaultValues?.ospResponsibleFullname || '',
ospResponsibleCedula: defaultValues?.ospResponsibleCedula || '',
ospResponsibleRif: defaultValues?.ospResponsibleRif || '',
ospResponsiblePhone: defaultValues?.ospResponsiblePhone || '',
civilState: defaultValues?.civilState || '',
familyBurden: defaultValues?.familyBurden || 0,
numberOfChildren: defaultValues?.numberOfChildren || 0,
generalObservations: defaultValues?.generalObservations || '',
ospResponsibleEmail: defaultValues?.ospResponsibleEmail || '',
photo1: defaultValues?.photo1 || '',
photo2: defaultValues?.photo2 || '',
photo3: defaultValues?.photo3 || '',
paralysisReason: defaultValues?.paralysisReason || '',
state: defaultValues?.state || undefined,
municipality: defaultValues?.municipality || undefined,
parish: defaultValues?.parish || undefined,
},
mode: 'onChange',
});
const onSubmit = async (formData: TrainingSchema) => {
saveTraining(formData, {
onSuccess: () => {
form.reset();
onSuccess?.();
},
onError: (e) => {
console.error(e);
form.setError('root', {
type: 'manual',
message: 'Error al guardar el registro',
});
},
});
};
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
{form.formState.errors.root && (
<div className="text-destructive text-sm">
{form.formState.errors.root.message}
</div>
)}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* Datos Personales */}
<div className="col-span-2">
<h3 className="text-lg font-medium mb-2">Datos Básicos</h3>
</div>
<FormField control={form.control} name="firstname" render={({ field }) => (
<FormItem>
<FormLabel>Nombre</FormLabel>
<FormControl><Input {...field} /></FormControl>
<FormMessage />
</FormItem>
)} />
<FormField control={form.control} name="lastname" render={({ field }) => (
<FormItem>
<FormLabel>Apellido</FormLabel>
<FormControl><Input {...field} /></FormControl>
<FormMessage />
</FormItem>
)} />
<FormField control={form.control} name="visitDate" render={({ field }) => (
<FormItem>
<FormLabel>Fecha de la visita</FormLabel>
<FormControl>
<Input
type="date"
value={field.value ? new Date(field.value).toISOString().split('T')[0] : ''}
onChange={(e) => {
// Convert YYYY-MM-DD to ISO 8601 string
const dateValue = e.target.value;
if (dateValue) {
field.onChange(new Date(dateValue).toISOString());
} else {
field.onChange('');
}
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)} />
{/* Ubicación */}
<div className="col-span-2">
<h3 className="text-lg font-medium mb-2 mt-4">Ubicación</h3>
</div>
<FormField
control={form.control}
name="state"
render={({ field }) => (
<FormItem className="w-full">
<FormLabel>Estado</FormLabel>
<SelectSearchable
options={
stateOptions?.map((item) => ({
value: item.id.toString(),
label: item.name,
})) || []
}
onValueChange={(value: any) => { field.onChange(Number(value)); setState(value); setDisabledMunicipality(false); setDisabledParish(true) }
}
placeholder="Selecciona un estado"
defaultValue={field.value?.toString()}
// disabled={readOnly}
/>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="municipality"
render={({ field }) => (
<FormItem className="w-full">
<FormLabel>Municipio</FormLabel>
<SelectSearchable
options={
municipalityOptions?.map((item) => ({
value: item.id.toString(),
label: item.name,
})) || []
}
onValueChange={(value: any) => { field.onChange(Number(value)); setMunicipality(value); setDisabledParish(false) }
}
placeholder="Selecciona un Municipio"
defaultValue={field.value?.toString()}
disabled={disabledMunicipality}
/>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="parish"
render={({ field }) => (
<FormItem className="w-full">
<FormLabel>Parroquia</FormLabel>
<SelectSearchable
options={
parishOptions?.map((item) => ({
value: item.id.toString(),
label: item.name,
})) || []
}
onValueChange={(value: any) =>
field.onChange(Number(value))
}
placeholder="Selecciona una Parroquia"
defaultValue={field.value?.toString()}
disabled={disabledParish}
/>
<FormMessage />
</FormItem>
)}
/>
{/* <FormField control={form.control} name="state" render={({ field }) => (
<FormItem>
<FormLabel>Estado</FormLabel>
<FormControl><Input {...field} value={field.value || ''} /></FormControl>
<FormMessage />
</FormItem>
)} />
<FormField control={form.control} name="municipality" render={({ field }) => (
<FormItem>
<FormLabel>Municipio</FormLabel>
<FormControl><Input {...field} value={field.value || ''} /></FormControl>
<FormMessage />
</FormItem>
)} />
<FormField control={form.control} name="parish" render={({ field }) => (
<FormItem>
<FormLabel>Parroquia</FormLabel>
<FormControl><Input {...field} value={field.value || ''} /></FormControl>
<FormMessage />
</FormItem>
)} /> */}
<FormField control={form.control} name="siturCodeCommune" render={({ field }) => (
<FormItem>
<FormLabel>Código SITUR Comuna</FormLabel>
<FormControl><Input {...field} /></FormControl>
<FormMessage />
</FormItem>
)} />
<FormField control={form.control} name="communalCouncil" render={({ field }) => (
<FormItem>
<FormLabel>Consejo Comunal</FormLabel>
<FormControl><Input {...field} /></FormControl>
<FormMessage />
</FormItem>
)} />
<FormField control={form.control} name="siturCodeCommunalCouncil" render={({ field }) => (
<FormItem>
<FormLabel>Código SITUR Consejo Comunal</FormLabel>
<FormControl><Input {...field} /></FormControl>
<FormMessage />
</FormItem>
)} />
{/* Datos de la OSP */}
<div className="col-span-2">
<h3 className="text-lg font-medium mb-2 mt-4">Datos de la Organización Socioproductiva (OSP)</h3>
</div>
<FormField control={form.control} name="ospName" render={({ field }) => (
<FormItem>
<FormLabel>Nombre de la Organización</FormLabel>
<FormControl><Input {...field} /></FormControl>
<FormMessage />
</FormItem>
)} />
<FormField control={form.control} name="ospAddress" render={({ field }) => (
<FormItem>
<FormLabel>Dirección</FormLabel>
<FormControl><Input {...field} /></FormControl>
<FormMessage />
</FormItem>
)} />
<FormField control={form.control} name="ospRif" render={({ field }) => (
<FormItem>
<FormLabel>RIF</FormLabel>
<FormControl><Input {...field} /></FormControl>
<FormMessage />
</FormItem>
)} />
<FormField control={form.control} name="ospType" render={({ field }) => (
<FormItem>
<FormLabel>Tipo de Organización</FormLabel>
<FormControl><Input {...field} /></FormControl>
<FormMessage />
</FormItem>
)} />
<FormField control={form.control} name="productiveActivity" render={({ field }) => (
<FormItem>
<FormLabel>Actividad Productiva</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Seleccione actividad" />
</SelectTrigger>
</FormControl>
<SelectContent>
{PRODUCTIVE_ACTIVITIES.map((activity) => (
<SelectItem key={activity} value={activity}>
{activity}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)} />
<FormField control={form.control} name="currentStatus" render={({ field }) => (
<FormItem>
<FormLabel>Estatus Actual</FormLabel>
<FormControl><Input {...field} /></FormControl>
<FormMessage />
</FormItem>
)} />
<FormField control={form.control} name="companyConstitutionYear" render={({ field }) => (
<FormItem>
<FormLabel>Año de Constitución</FormLabel>
<FormControl><Input type="number" {...field} /></FormControl>
<FormMessage />
</FormItem>
)} />
<FormField control={form.control} name="producerCount" render={({ field }) => (
<FormItem>
<FormLabel>Cantidad de Productores</FormLabel>
<FormControl><Input type="number" {...field} /></FormControl>
<FormMessage />
</FormItem>
)} />
<FormField control={form.control} name="productDescription" render={({ field }) => (
<FormItem className="col-span-2">
<FormLabel>Breve descripción del producto o servicio</FormLabel>
<FormControl><Textarea {...field} /></FormControl>
<FormMessage />
</FormItem>
)} />
<FormField control={form.control} name="installedCapacity" render={({ field }) => (
<FormItem>
<FormLabel>Capacidad Instalada</FormLabel>
<FormControl><Input {...field} /></FormControl>
<FormMessage />
</FormItem>
)} />
<FormField control={form.control} name="operationalCapacity" render={({ field }) => (
<FormItem>
<FormLabel>Capacidad Operativa</FormLabel>
<FormControl><Input {...field} /></FormControl>
<FormMessage />
</FormItem>
)} />
<FormField control={form.control} name="financialRequirementDescription" render={({ field }) => (
<FormItem className="col-span-2">
<FormLabel>Descripción del Requerimiento Financiero</FormLabel>
<FormControl><Textarea {...field} /></FormControl>
<FormMessage />
</FormItem>
)} />
<FormField control={form.control} name="paralysisReason" render={({ field }) => (
<FormItem className="col-span-2">
<FormLabel>Razones de paralización (si aplica)</FormLabel>
<FormControl><Textarea {...field} /></FormControl>
<FormMessage />
</FormItem>
)} />
{/* Responsable */}
<div className="col-span-2">
<h3 className="text-lg font-medium mb-2 mt-4">Datos del Responsable</h3>
</div>
<FormField control={form.control} name="ospResponsibleFullname" render={({ field }) => (
<FormItem>
<FormLabel>Nombre y Apellido</FormLabel>
<FormControl><Input {...field} /></FormControl>
<FormMessage />
</FormItem>
)} />
<FormField control={form.control} name="ospResponsibleCedula" render={({ field }) => (
<FormItem>
<FormLabel>Cédula (sin puntos)</FormLabel>
<FormControl><Input {...field} /></FormControl>
<FormMessage />
</FormItem>
)} />
<FormField control={form.control} name="ospResponsibleRif" render={({ field }) => (
<FormItem>
<FormLabel>RIF (sin puntos)</FormLabel>
<FormControl><Input {...field} /></FormControl>
<FormMessage />
</FormItem>
)} />
<FormField control={form.control} name="ospResponsiblePhone" render={({ field }) => (
<FormItem>
<FormLabel>Teléfonos (2 números)</FormLabel>
<FormControl><Input {...field} /></FormControl>
<FormMessage />
</FormItem>
)} />
<FormField control={form.control} name="civilState" render={({ field }) => (
<FormItem>
<FormLabel>Estado Civil</FormLabel>
<FormControl><Input {...field} /></FormControl>
<FormMessage />
</FormItem>
)} />
<FormField control={form.control} name="ospResponsibleEmail" render={({ field }) => (
<FormItem>
<FormLabel>Correo Electrónico</FormLabel>
<FormControl><Input type="email" {...field} /></FormControl>
<FormMessage />
</FormItem>
)} />
<FormField control={form.control} name="familyBurden" render={({ field }) => (
<FormItem>
<FormLabel>Carga Familiar</FormLabel>
<FormControl><Input type="number" {...field} /></FormControl>
<FormMessage />
</FormItem>
)} />
<FormField control={form.control} name="numberOfChildren" render={({ field }) => (
<FormItem>
<FormLabel>Número de Hijos</FormLabel>
<FormControl><Input type="number" {...field} /></FormControl>
<FormMessage />
</FormItem>
)} />
{/* datos adicionales */}
<div className="col-span-2">
<h3 className="text-lg font-medium mb-2 mt-4">Datos Adicionales</h3>
</div>
<FormField control={form.control} name="generalObservations" render={({ field }) => (
<FormItem className="col-span-2">
<FormLabel>Observaciones Generales</FormLabel>
<FormControl><Textarea {...field} /></FormControl>
<FormMessage />
</FormItem>
)} />
{/* Fotos */}
<div className="col-span-2">
<h3 className="text-lg font-medium mb-2 mt-4">Registro Fotográfico (URLs)</h3>
</div>
<FormField control={form.control} name="photo1" render={({ field }) => (
<FormItem>
<FormLabel>Foto 1</FormLabel>
<FormControl><Input {...field} placeholder="URL de la imagen" /></FormControl>
<FormMessage />
</FormItem>
)} />
<FormField control={form.control} name="photo2" render={({ field }) => (
<FormItem>
<FormLabel>Foto 2</FormLabel>
<FormControl><Input {...field} placeholder="URL de la imagen" /></FormControl>
<FormMessage />
</FormItem>
)} />
<FormField control={form.control} name="photo3" render={({ field }) => (
<FormItem>
<FormLabel>Foto 3</FormLabel>
<FormControl><Input {...field} placeholder="URL de la imagen" /></FormControl>
<FormMessage />
</FormItem>
)} />
</div>
<div className="flex justify-end gap-4 mt-6">
<Button variant="outline" type="button" onClick={onCancel}>
Cancelar
</Button>
<Button type="submit" disabled={isSaving}>
{isSaving ? 'Guardando...' : 'Guardar'}
</Button>
</div>
</form>
</Form>
);
}

View File

@@ -0,0 +1,319 @@
'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,
} from '@repo/shadcn/select';
const OSP_TYPES = [
'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>('');
// 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' }];
// 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'];
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>
</CardHeader>
<CardContent className="h-[400px]">
<ResponsiveContainer width="100%" height="100%">
<BarChart
data={stateDistribution}
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="#00C49F" 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>
);
}

View File

@@ -0,0 +1,13 @@
import { useSafeQuery } from '@/hooks/use-safe-query';
import { getTrainingStatisticsAction } from '../actions/training-actions';
export function useTrainingStatsQuery(params: {
startDate?: string;
endDate?: string;
stateId?: number;
municipalityId?: number;
parishId?: number;
ospType?: string;
} = {}) {
return useSafeQuery(['training-statistics', JSON.stringify(params)], () => getTrainingStatisticsAction(params));
}

View File

@@ -0,0 +1,29 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { TrainingSchema } from "../schemas/training";
import { createTrainingAction, updateTrainingAction, deleteTrainingAction } from "../actions/training-actions";
export function useCreateTraining() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (data: TrainingSchema) => createTrainingAction(data),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['training'] }),
})
return mutation
}
export function useUpdateTraining() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (data: TrainingSchema) => updateTrainingAction(data),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['training'] }),
})
return mutation;
}
export function useDeleteTraining() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: number) => deleteTrainingAction(id),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['training'] }),
})
}

View File

@@ -0,0 +1,23 @@
import { z } from 'zod';
export const statisticsItemSchema = z.object({
name: z.string(),
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),
});
export type TrainingStatisticsData = z.infer<typeof trainingStatisticsSchema>;
export const trainingStatisticsResponseSchema = z.object({
message: z.string(),
data: trainingStatisticsSchema,
});

View File

@@ -0,0 +1,61 @@
import { z } from 'zod';
export const trainingSchema = z.object({
id: z.number().optional(),
firstname: z.string().min(1, { message: "Nombre es requerido" }),
lastname: z.string().min(1, { message: "Apellido es requerido" }),
visitDate: z.string().or(z.date()).transform((val) => new Date(val).toISOString()),
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" }),
communalCouncil: z.string().min(1, { message: "Consejo Comunal es requerido" }),
siturCodeCommunalCouncil: z.string().min(1, { message: "Código SITUR Consejo Comunal es requerido" }),
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" }),
currentStatus: z.string().min(1, { message: "Estatus actual es requerido" }),
companyConstitutionYear: z.coerce.number().min(1900, { message: "Año inválido" }),
producerCount: z.coerce.number().min(1, { message: "Cantidad de productores requerida" }),
productDescription: z.string().min(1, { message: "Descripción del producto es requerida" }),
installedCapacity: z.string().min(1, { message: "Capacidad instalada es requerida" }),
operationalCapacity: z.string().min(1, { message: "Capacidad operativa es requerida" }),
ospResponsibleFullname: z.string().min(1, { message: "Nombre del responsable es requerido" }),
ospResponsibleCedula: z.string().min(1, { message: "Cédula del responsable es requerida" }),
ospResponsibleRif: z.string().min(1, { message: "RIF del responsable es requerido" }),
ospResponsiblePhone: z.string().min(1, { message: "Teléfono del responsable es requerido" }),
civilState: z.string().min(1, { message: "Estado civil es requerido" }),
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" }),
generalObservations: z.string().optional().default(''),
photo1: z.string().optional().default(''),
photo2: z.string().optional().default(''),
photo3: z.string().optional().default(''),
paralysisReason: z.string().optional().default(''),
state: z.number().optional().nullable(),
municipality: z.number().optional().nullable(),
parish: z.number().optional().nullable(),
});
export type TrainingSchema = z.infer<typeof trainingSchema>;
export const trainingApiResponseSchema = z.object({
message: z.string(),
data: z.array(trainingSchema),
meta: z.object({
page: z.number(),
limit: z.number(),
totalCount: z.number(),
totalPages: z.number(),
hasNextPage: z.boolean(),
hasPreviousPage: z.boolean(),
nextPage: z.number().nullable(),
previousPage: z.number().nullable(),
}),
});
export const TrainingMutate = z.object({
message: z.string(),
data: trainingSchema,
});

View File

@@ -78,9 +78,7 @@ export function ModalForm({
parish: undefined
}
console.log(defaultValues);
// console.log(defaultValues);
const form = useForm<UpdateUser>({
resolver: zodResolver(updateUser),