mejoras al formulario de registro organizaciones productivas
This commit is contained in:
@@ -1,123 +1,161 @@
|
||||
'use server';
|
||||
import { safeFetchApi } from '@/lib/fetch.api';
|
||||
import {
|
||||
TrainingSchema,
|
||||
TrainingMutate,
|
||||
trainingApiResponseSchema
|
||||
} from '../schemas/training';
|
||||
import { trainingStatisticsResponseSchema } from '../schemas/statistics';
|
||||
import {
|
||||
TrainingMutate,
|
||||
TrainingSchema,
|
||||
trainingApiResponseSchema,
|
||||
} from '../schemas/training';
|
||||
|
||||
export const getTrainingStatisticsAction = async (params: {
|
||||
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 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',
|
||||
);
|
||||
const [error, response] = await safeFetchApi(
|
||||
trainingStatisticsResponseSchema,
|
||||
`/training/statistics?${searchParams.toString()}`,
|
||||
'GET',
|
||||
);
|
||||
|
||||
if (error) throw new Error(error.message);
|
||||
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;
|
||||
return response?.data;
|
||||
};
|
||||
|
||||
export const updateTrainingAction = async (payload: TrainingSchema) => {
|
||||
const { id, ...payloadWithoutId } = payload;
|
||||
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 }),
|
||||
});
|
||||
|
||||
if (!id) throw new Error('ID es requerido para actualizar');
|
||||
const [error, response] = await safeFetchApi(
|
||||
trainingApiResponseSchema,
|
||||
`/training?${searchParams}`,
|
||||
'GET',
|
||||
);
|
||||
|
||||
const [error, data] = await safeFetchApi(
|
||||
TrainingMutate,
|
||||
`/training/${id}`,
|
||||
'PATCH',
|
||||
payloadWithoutId,
|
||||
);
|
||||
if (error) throw new Error(error.message);
|
||||
|
||||
if (error) {
|
||||
throw new Error(error.message || 'Error al actualizar el registro');
|
||||
}
|
||||
return {
|
||||
data: response?.data || [],
|
||||
meta: response?.meta || {
|
||||
page: 1,
|
||||
limit: 10,
|
||||
totalCount: 0,
|
||||
totalPages: 1,
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
nextPage: null,
|
||||
previousPage: null,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
return data;
|
||||
export const createTrainingAction = async (
|
||||
payload: TrainingSchema | FormData,
|
||||
) => {
|
||||
let payloadToSend = payload;
|
||||
let id: number | undefined;
|
||||
|
||||
if (payload instanceof FormData) {
|
||||
payload.delete('id');
|
||||
payloadToSend = payload;
|
||||
} else {
|
||||
const { id: _, ...rest } = payload;
|
||||
payloadToSend = rest as any;
|
||||
}
|
||||
|
||||
const [error, data] = await safeFetchApi(
|
||||
TrainingMutate,
|
||||
'/training',
|
||||
'POST',
|
||||
payloadToSend,
|
||||
);
|
||||
|
||||
if (error) {
|
||||
throw new Error(error.message || 'Error al crear el registro');
|
||||
}
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
export const updateTrainingAction = async (
|
||||
payload: TrainingSchema | FormData,
|
||||
) => {
|
||||
let id: string | null = null;
|
||||
let payloadToSend = payload;
|
||||
|
||||
if (payload instanceof FormData) {
|
||||
id = payload.get('id') as string;
|
||||
payload.delete('id');
|
||||
payloadToSend = payload;
|
||||
} else {
|
||||
id = payload.id?.toString() || null;
|
||||
const { id: _, ...rest } = payload;
|
||||
payloadToSend = rest as any;
|
||||
}
|
||||
|
||||
if (!id) throw new Error('ID es requerido para actualizar');
|
||||
|
||||
const [error, data] = await safeFetchApi(
|
||||
TrainingMutate,
|
||||
`/training/${id}`,
|
||||
'PATCH',
|
||||
payloadToSend,
|
||||
);
|
||||
|
||||
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'
|
||||
)
|
||||
const [error] = await safeFetchApi(
|
||||
TrainingMutate,
|
||||
`/training/${id}`,
|
||||
'DELETE',
|
||||
);
|
||||
|
||||
if (error) throw new Error(error.message || 'Error al eliminar el registro');
|
||||
if (error) throw new Error(error.message || 'Error al eliminar el registro');
|
||||
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
export const getTrainingByIdAction = async (id: number) => {
|
||||
const [error, response] = await safeFetchApi(
|
||||
TrainingMutate,
|
||||
`/training/${id}`,
|
||||
'GET',
|
||||
);
|
||||
|
||||
if (error) throw new Error(error.message);
|
||||
|
||||
return response?.data;
|
||||
};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
13
apps/web/feactures/training/components/training-header.tsx
Normal file
13
apps/web/feactures/training/components/training-header.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
'use client';
|
||||
import { Heading } from '@repo/shadcn/heading';
|
||||
|
||||
export function TrainingHeader() {
|
||||
return (
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<Heading
|
||||
title="Registro de Organizaciones Socioproductivas"
|
||||
description="Gestiona los registros de las OSP"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
39
apps/web/feactures/training/components/training-list.tsx
Normal file
39
apps/web/feactures/training/components/training-list.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
'use client';
|
||||
|
||||
import { DataTable } from '@repo/shadcn/table/data-table';
|
||||
import { DataTableSkeleton } from '@repo/shadcn/table/data-table-skeleton';
|
||||
import { useTrainingQuery } from '../hooks/use-training';
|
||||
import { columns } from './training-tables/columns';
|
||||
|
||||
interface TrainingListProps {
|
||||
initialPage: number;
|
||||
initialSearch?: string | null;
|
||||
initialLimit: number;
|
||||
}
|
||||
|
||||
export default function TrainingList({
|
||||
initialPage,
|
||||
initialSearch,
|
||||
initialLimit,
|
||||
}: TrainingListProps) {
|
||||
const filters = {
|
||||
page: initialPage,
|
||||
limit: initialLimit,
|
||||
...(initialSearch && { search: initialSearch }),
|
||||
};
|
||||
|
||||
const { data, isLoading } = useTrainingQuery(filters);
|
||||
|
||||
if (isLoading) {
|
||||
return <DataTableSkeleton columnCount={5} rowCount={initialLimit} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={data?.data || []}
|
||||
totalItems={data?.meta.totalCount || 0}
|
||||
pageSizeOptions={[10, 20, 30, 40, 50]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
'use client';
|
||||
import { AlertModal } from '@/components/modal/alert-modal';
|
||||
import { useDeleteTraining } from '@/feactures/training/hooks/use-training';
|
||||
import { TrainingSchema } from '@/feactures/training/schemas/training';
|
||||
import { Button } from '@repo/shadcn/button';
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from '@repo/shadcn/tooltip';
|
||||
import { Edit, Eye, Trash } from 'lucide-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import { TrainingViewModal } from '../training-view-modal';
|
||||
|
||||
interface CellActionProps {
|
||||
data: TrainingSchema;
|
||||
}
|
||||
|
||||
export const CellAction: React.FC<CellActionProps> = ({ data }) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [viewOpen, setViewOpen] = useState(false);
|
||||
const { mutate: deleteTraining } = useDeleteTraining();
|
||||
const router = useRouter();
|
||||
|
||||
const onConfirm = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
deleteTraining(data.id!);
|
||||
setOpen(false);
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<AlertModal
|
||||
isOpen={open}
|
||||
onClose={() => setOpen(false)}
|
||||
onConfirm={onConfirm}
|
||||
loading={loading}
|
||||
title="¿Estás seguro que desea eliminar este registro?"
|
||||
description="Esta acción no se puede deshacer."
|
||||
/>
|
||||
|
||||
<TrainingViewModal
|
||||
isOpen={viewOpen}
|
||||
onClose={() => setViewOpen(false)}
|
||||
data={data}
|
||||
/>
|
||||
|
||||
<div className="flex gap-1">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => setViewOpen(true)}
|
||||
>
|
||||
<Eye className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Ver detalle</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() =>
|
||||
router.push(`/dashboard/formulario/editar/${data.id}`)
|
||||
}
|
||||
>
|
||||
<Edit className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Editar</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => setOpen(true)}
|
||||
>
|
||||
<Trash className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Eliminar</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
'use client';
|
||||
import { TrainingSchema } from '@/feactures/training/schemas/training';
|
||||
import { Badge } from '@repo/shadcn/badge';
|
||||
import { ColumnDef } from '@tanstack/react-table';
|
||||
import { CellAction } from './cell-action';
|
||||
|
||||
export const columns: ColumnDef<TrainingSchema>[] = [
|
||||
{
|
||||
accessorKey: 'ospName',
|
||||
header: 'Nombre OSP',
|
||||
},
|
||||
{
|
||||
accessorKey: 'ospRif',
|
||||
header: 'RIF',
|
||||
},
|
||||
{
|
||||
accessorKey: 'ospType',
|
||||
header: 'Tipo',
|
||||
},
|
||||
{
|
||||
accessorKey: 'currentStatus',
|
||||
header: 'Estatus',
|
||||
cell: ({ row }) => {
|
||||
const status = row.getValue('currentStatus') as string;
|
||||
return (
|
||||
<Badge variant={status === 'ACTIVA' ? 'default' : 'secondary'}>
|
||||
{status}
|
||||
</Badge>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'visitDate',
|
||||
header: 'Fecha Visita',
|
||||
cell: ({ row }) => {
|
||||
const date = row.getValue('visitDate') as string;
|
||||
return date ? new Date(date).toLocaleString() : 'N/A';
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
header: 'Acciones',
|
||||
cell: ({ row }) => <CellAction data={row.original} />,
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,31 @@
|
||||
'use client';
|
||||
|
||||
import { Button } from '@repo/shadcn/components/ui/button';
|
||||
import { DataTableSearch } from '@repo/shadcn/table/data-table-search';
|
||||
import { Plus } from 'lucide-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useTrainingTableFilters } from './use-training-table-filters';
|
||||
|
||||
export default function TrainingTableAction() {
|
||||
const { searchQuery, setPage, setSearchQuery } = useTrainingTableFilters();
|
||||
const router = useRouter();
|
||||
return (
|
||||
<div className="flex items-center justify-between mt-4 ">
|
||||
<div className="flex items-center gap-4 flex-grow">
|
||||
<DataTableSearch
|
||||
searchKey="nombre"
|
||||
searchQuery={searchQuery || ''}
|
||||
setSearchQuery={setSearchQuery}
|
||||
setPage={setPage}
|
||||
/>
|
||||
</div>{' '}
|
||||
<Button
|
||||
onClick={() => router.push(`/dashboard/formulario/nuevo`)}
|
||||
size="sm"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
Nuevo Registro
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
'use client';
|
||||
|
||||
import { searchParams } from '@repo/shadcn/lib/searchparams';
|
||||
import { useQueryState } from 'nuqs';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
export function useTrainingTableFilters() {
|
||||
const [searchQuery, setSearchQuery] = useQueryState(
|
||||
'q',
|
||||
searchParams.q
|
||||
.withOptions({
|
||||
shallow: false,
|
||||
throttleMs: 500,
|
||||
})
|
||||
.withDefault(''),
|
||||
);
|
||||
|
||||
const [page, setPage] = useQueryState(
|
||||
'page',
|
||||
searchParams.page.withDefault(1),
|
||||
);
|
||||
|
||||
const resetFilters = useCallback(() => {
|
||||
setSearchQuery(null);
|
||||
setPage(1);
|
||||
}, [setSearchQuery, setPage]);
|
||||
|
||||
const isAnyFilterActive = useMemo(() => {
|
||||
return !!searchQuery;
|
||||
}, [searchQuery]);
|
||||
|
||||
return {
|
||||
searchQuery,
|
||||
setSearchQuery,
|
||||
page,
|
||||
setPage,
|
||||
resetFilters,
|
||||
isAnyFilterActive,
|
||||
};
|
||||
}
|
||||
285
apps/web/feactures/training/components/training-view-modal.tsx
Normal file
285
apps/web/feactures/training/components/training-view-modal.tsx
Normal file
@@ -0,0 +1,285 @@
|
||||
'use client';
|
||||
|
||||
import { Badge } from '@repo/shadcn/badge';
|
||||
import { Button } from '@repo/shadcn/button';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@repo/shadcn/components/ui/card';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@repo/shadcn/components/ui/dialog';
|
||||
import { X } from 'lucide-react';
|
||||
import React, { useState } from 'react';
|
||||
import { TrainingSchema } from '../schemas/training';
|
||||
|
||||
interface TrainingViewModalProps {
|
||||
data: TrainingSchema | null;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function TrainingViewModal({
|
||||
data,
|
||||
isOpen,
|
||||
onClose,
|
||||
}: TrainingViewModalProps) {
|
||||
const [selectedImage, setSelectedImage] = useState<string | null>(null);
|
||||
|
||||
if (!data) return null;
|
||||
|
||||
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>
|
||||
</div>
|
||||
);
|
||||
|
||||
const Section = ({
|
||||
title,
|
||||
children,
|
||||
}: {
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
}) => (
|
||||
<Card className="mb-4">
|
||||
<CardHeader className="py-3">
|
||||
<CardTitle className="text-lg">{title}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{children}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
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
|
||||
</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>
|
||||
</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>
|
||||
|
||||
{/* 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">
|
||||
<DetailItem
|
||||
label="Descripción del Producto"
|
||||
value={data.productDescription}
|
||||
/>
|
||||
</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}
|
||||
/>
|
||||
</div>
|
||||
{data.paralysisReason && (
|
||||
<div className="col-span-1 md:col-span-2 lg:col-span-3">
|
||||
<DetailItem
|
||||
label="Razones de paralización"
|
||||
value={data.paralysisReason}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</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>
|
||||
|
||||
{/* 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>
|
||||
|
||||
{/* 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
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="mt-6">
|
||||
<Button onClick={onClose} variant="outline" className="w-32">
|
||||
Cerrar
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Lightbox */}
|
||||
<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">
|
||||
<DialogHeader className="sr-only">
|
||||
<DialogTitle>Vista ampliada de la imagen</DialogTitle>
|
||||
<DialogDescription>
|
||||
Imagen ampliada del registro fotográfico de la organización.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="relative w-full h-full flex items-center justify-center p-4">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="absolute top-2 right-2 text-white hover:bg-white/20 z-10"
|
||||
onClick={() => setSelectedImage(null)}
|
||||
>
|
||||
<X className="h-6 w-6" />
|
||||
</Button>
|
||||
{selectedImage && (
|
||||
<img
|
||||
src={`${process.env.NEXT_PUBLIC_API_URL}${selectedImage}`}
|
||||
alt="Expanded view"
|
||||
className="max-w-full max-h-[90vh] object-contain"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,29 +1,44 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { TrainingSchema } from "../schemas/training";
|
||||
import { createTrainingAction, updateTrainingAction, deleteTrainingAction } from "../actions/training-actions";
|
||||
import { useSafeQuery } from '@/hooks/use-safe-query';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
createTrainingAction,
|
||||
deleteTrainingAction,
|
||||
getTrainingAction,
|
||||
getTrainingByIdAction,
|
||||
updateTrainingAction,
|
||||
} from '../actions/training-actions';
|
||||
import { TrainingSchema } from '../schemas/training';
|
||||
|
||||
export function useTrainingQuery(params = {}) {
|
||||
return useSafeQuery(['training', params], () => getTrainingAction(params));
|
||||
}
|
||||
|
||||
export function useTrainingByIdQuery(id: number) {
|
||||
return useSafeQuery(['training', id], () => getTrainingByIdAction(id));
|
||||
}
|
||||
|
||||
export function useCreateTraining() {
|
||||
const queryClient = useQueryClient();
|
||||
const mutation = useMutation({
|
||||
mutationFn: (data: TrainingSchema) => createTrainingAction(data),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['training'] }),
|
||||
})
|
||||
return mutation
|
||||
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;
|
||||
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'] }),
|
||||
})
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (id: number) => deleteTrainingAction(id),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['training'] }),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,61 +1,107 @@
|
||||
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(),
|
||||
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()
|
||||
.min(1, { message: 'Fecha y hora de visita es requerida' }),
|
||||
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' })
|
||||
.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
|
||||
.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().nullable(),
|
||||
photo2: z.string().optional().nullable(),
|
||||
photo3: z.string().optional().nullable(),
|
||||
files: z.any().optional(),
|
||||
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(),
|
||||
}),
|
||||
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,
|
||||
message: z.string(),
|
||||
data: trainingSchema,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user