Tabla de inventario agregada

This commit is contained in:
2025-06-20 14:43:35 -04:00
parent 0a65946a8a
commit ed2a1da038
33 changed files with 3272 additions and 10 deletions

View File

@@ -0,0 +1,153 @@
'use server';
import { safeFetchApi } from '@/lib/fetch.api';
import {
surveysApiResponseSchema,
CreateUser,
productMutate,
UpdateUser
} from '../schemas/inventory';
import { auth } from '@/lib/auth';
export const getProfileAction = async () => {
const session = await auth()
const id = session?.user?.id
const [error, response] = await safeFetchApi(
productMutate,
`/users/${id}`,
'GET'
);
if (error) throw new Error(error.message);
return response;
};
export const updateProfileAction = async (payload: UpdateUser) => {
const { id, ...payloadWithoutId } = payload;
const [error, data] = await safeFetchApi(
productMutate,
`/users/profile/${id}`,
'PATCH',
payloadWithoutId,
);
console.log(payload);
if (error) {
if (error.message === 'Email already exists') {
throw new Error('Ese correo ya está en uso');
}
// console.error('Error:', error);
throw new Error('Error al crear el usuario');
}
return data;
};
export const getInventoryAction = 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(
surveysApiResponseSchema,
`/inventory?${searchParams}`,
'GET',
);
if (error) {
console.error(error);
throw new Error(error.message);
}
// const transformedData = response?.data ? transformSurvey(response?.data) : undefined;
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 createUserAction = async (payload: CreateUser) => {
const { id, confirmPassword, ...payloadWithoutId } = payload;
const [error, data] = await safeFetchApi(
productMutate,
'/users',
'POST',
payloadWithoutId,
);
if (error) {
if (error.message === 'Username already exists') {
throw new Error('Ese usuario ya existe');
}
if (error.message === 'Email already exists') {
throw new Error('Ese correo ya está en uso');
}
// console.error('Error:', error);
throw new Error('Error al crear el usuario');
}
return payloadWithoutId;
};
export const updateUserAction = async (payload: UpdateUser) => {
try {
const { id, ...payloadWithoutId } = payload;
const [error, data] = await safeFetchApi(
productMutate,
`/users/${id}`,
'PATCH',
payloadWithoutId,
);
// console.log(data);
if (error) {
console.error(error);
throw new Error(error?.message || 'Error al actualizar el usuario');
}
return data;
} catch (error) {
console.error(error);
}
}
export const deleteUserAction = async (id: Number) => {
const [error] = await safeFetchApi(
productMutate,
`/users/${id}`,
'DELETE'
)
console.log(error);
// if (error) throw new Error(error.message || 'Error al eliminar el usuario')
return true;
}

View File

@@ -0,0 +1,222 @@
'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 {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@repo/shadcn/select';
import { useForm } from 'react-hook-form';
import { useCreateUser } from "../../hooks/use-mutation-users";
import { CreateUser, createUser } from '../../schemas/inventory';
const ROLES = {
// 1: 'Superadmin',
2: 'Administrador',
3: 'autoridad',
4: 'Gerente',
5: 'Usuario',
6: 'Productor',
7: 'Organización'
}
interface CreateUserFormProps {
onSuccess?: () => void;
onCancel?: () => void;
defaultValues?: Partial<CreateUser>;
}
export function CreateUserForm({
onSuccess,
onCancel,
defaultValues,
}: CreateUserFormProps) {
const {
mutate: saveAccountingAccounts,
isPending: isSaving,
isError,
} = useCreateUser();
// const { data: AccoutingAccounts } = useSurveyMutation();
const defaultformValues = {
username: defaultValues?.username || '',
fullname: defaultValues?.fullname || '',
email: defaultValues?.email || '',
password: '',
confirmPassword: '',
id: defaultValues?.id,
phone: defaultValues?.phone || '',
role: defaultValues?.role,
}
const form = useForm<CreateUser>({
resolver: zodResolver(createUser),
defaultValues: defaultformValues,
mode: 'onChange', // Enable real-time validation
});
const onSubmit = async (data: CreateUser) => {
const formData = data
saveAccountingAccounts(formData, {
onSuccess: () => {
form.reset();
onSuccess?.();
},
onError: (e) => {
form.setError('root', {
type: 'manual',
message: e.message,
});
},
});
};
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-2 gap-4">
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Usuario</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="fullname"
render={({ field }) => (
<FormItem>
<FormLabel>Nombre completo</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Correo</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="phone"
render={({ field }) => (
<FormItem>
<FormLabel>Teléfono</FormLabel>
<FormControl>
<Input {...field} value={field.value?.toString() ?? ''}/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Contraseña</FormLabel>
<FormControl>
<Input type="password" {...field}/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='confirmPassword'
render={({ field }) => (
<FormItem>
<FormLabel>Confirmar Contraseña</FormLabel>
<FormControl>
<Input type="password" {...field}/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="role"
render={({ field }) => (
<FormItem className="w-full">
<FormLabel>Rol</FormLabel>
<Select
onValueChange={(value) => field.onChange(Number(value))}
defaultValue={String(field.value)}
>
<FormControl>
<SelectTrigger className="w-full">
<SelectValue placeholder="Selecciona un rol" />
</SelectTrigger>
</FormControl>
<SelectContent className="w-full min-w-[200px]">
{Object.entries(ROLES).map(([value, label]) => (
<SelectItem key={value} value={value}>
{label}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="flex justify-end gap-4">
<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,47 @@
'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-users';
interface SurveysAdminListProps {
initialPage: number;
initialSearch?: string | null;
initialLimit: number;
initialType?: string | null;
}
export default function UsersAdminList({
initialPage,
initialSearch,
initialLimit,
initialType,
}: SurveysAdminListProps) {
const filters = {
page: initialPage,
limit: initialLimit,
...(initialSearch && { search: initialSearch }),
...(initialType && { type: initialType }),
};
const {data, isLoading} = useProductQuery(filters)
// const {data, isLoading} = useUsersQuery(filters)
console.log(data?.data);
if (isLoading) {
return <DataTableSkeleton columnCount={6} rowCount={initialLimit} />;
}
return (
<DataTable
columns={columns}
data={data?.data || []}
totalItems={data?.meta.totalCount || 0}
pageSizeOptions={[10, 20, 30, 40, 50]}
/>
);
}

View File

@@ -0,0 +1,90 @@
'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { AlertModal } from '@/components/modal/alert-modal';
import { Button } from '@repo/shadcn/button';
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@repo/shadcn/tooltip';
import { Edit, Trash, User } from 'lucide-react';
import { InventoryTable } from '@/feactures/inventory/schemas/inventory';
import { useDeleteUser } from '@/feactures/users/hooks/use-mutation-users';
import { AccountPlanModal } from '../user-modal';
interface CellActionProps {
data: InventoryTable;
}
export const CellAction: React.FC<CellActionProps> = ({ data }) => {
const [loading, setLoading] = useState(false);
const [open, setOpen] = useState(false);
const [edit, setEdit] = useState(false);
const { mutate: deleteUser } = useDeleteUser();
const router = useRouter();
const onConfirm = async () => {
try {
setLoading(true);
deleteUser(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 deshabilitar este usuario?"
description="Esta acción no se puede deshacer."
/>
<AccountPlanModal open={edit} onOpenChange={setEdit} defaultValues={data}/>
<div className="flex gap-1">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="outline"
size="icon"
onClick={() => setEdit(true)}
>
<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>Deshabilitar</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
</>
);
};

View File

@@ -0,0 +1,41 @@
import { Badge } from "@repo/shadcn/badge";
import { ColumnDef } from '@tanstack/react-table';
import { CellAction } from './cell-action';
import { InventoryTable } from '../../../schemas/inventory';
export const columns: ColumnDef<InventoryTable>[] = [
{
accessorKey: 'urlImg',
header: 'img',
cell: ({ row }) => {
const status = row.getValue("urlImg") as string | undefined;
return (
<img src={`http://localhost:3000/${status}`} alt="Image" width={64} height={64} className="rounded"/>
)
},
},
{
accessorKey: 'title',
header: 'Producto',
},
{
accessorKey: "description",
header: "Descripcion",
},
{
accessorKey: 'price',
header: 'Precio',
cell: ({ row }) => `${row.original.price}$`
},
{
accessorKey: 'stock',
header: 'Stock',
},
{
id: 'actions',
header: 'Acciones',
cell: ({ row }) => <CellAction data={row.original} />,
},
];

View File

@@ -0,0 +1,59 @@
'use client';
import { PUBLISHED_TYPES } from '@/feactures/surveys/schemas/surveys-options';
import { searchParams } from '@repo/shadcn/lib/searchparams';
import { useQueryState } from 'nuqs';
import { useCallback, useMemo } from 'react';
export const TYPE_OPTIONS = Object.entries(PUBLISHED_TYPES).map(
([value, label]) => ({
value,
label,
}),
);
export function useSurveyTableFilters() {
const [searchQuery, setSearchQuery] = useQueryState(
'q',
searchParams.q
.withOptions({
shallow: false,
throttleMs: 500, // Add 500ms delay
// Removed dedupingInterval as it's not a valid option
})
.withDefault(''),
);
const [typeFilter, setTypeFilter] = useQueryState(
'published',
searchParams.q.withOptions({ shallow: false }).withDefault(''),
);
const [page, setPage] = useQueryState(
'page',
searchParams.page.withDefault(1),
);
const resetFilters = useCallback(() => {
setSearchQuery(null);
setTypeFilter(null);
setPage(1);
}, [setSearchQuery, setPage]);
const isAnyFilterActive = useMemo(() => {
return !!searchQuery || !!typeFilter;
}, [searchQuery]);
return {
searchQuery,
setSearchQuery,
page,
setPage,
resetFilters,
isAnyFilterActive,
typeFilter,
setTypeFilter
};
}

View File

@@ -0,0 +1,36 @@
'use client';
import { DataTableFilterBox } from '@repo/shadcn/table/data-table-filter-box';
import { DataTableSearch } from '@repo/shadcn/table/data-table-search';
import {
TYPE_OPTIONS,
useSurveyTableFilters,
} from './use-survey-table-filters';
export default function UserTableAction() {
const {
typeFilter,
searchQuery,
setPage,
setTypeFilter,
setSearchQuery,
} = useSurveyTableFilters();
return (
<div className="flex flex-wrap items-center gap-4 pt-2">
<DataTableSearch
searchKey={searchQuery}
searchQuery={searchQuery || ''}
setSearchQuery={setSearchQuery}
setPage={setPage}
/>
{/* <DataTableFilterBox
filterKey="type"
title="Estado"
options={TYPE_OPTIONS}
setFilterValue={setTypeFilter}
filterValue={typeFilter}
/> */}
</div>
);
}

View File

@@ -0,0 +1,227 @@
'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 {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@repo/shadcn/select';
import { useForm } from 'react-hook-form';
import { useUpdateUser } from "@/feactures/users/hooks/use-mutation-users";
import { UpdateUser, updateUser } from '@/feactures/users/schemas/users';
const ROLES = {
// 1: 'Superadmin',
2: 'Administrador',
3: 'autoridad',
4: 'Gerente',
5: 'Usuario',
6: 'Productor',
7: 'Organización'
}
interface UserFormProps {
onSuccess?: () => void;
onCancel?: () => void;
defaultValues?: Partial<UpdateUser>;
}
export function UpdateUserForm({
onSuccess,
onCancel,
defaultValues,
}: UserFormProps) {
const {
mutate: saveAccountingAccounts,
isPending: isSaving,
isError,
} = useUpdateUser();
const defaultformValues = {
username: defaultValues?.username || '',
fullname: defaultValues?.fullname || '',
email: defaultValues?.email || '',
password: '',
id: defaultValues?.id,
phone: defaultValues?.phone || '',
role: undefined,
isActive: defaultValues?.isActive
}
// console.log(defaultValues);
const form = useForm<UpdateUser>({
resolver: zodResolver(updateUser),
defaultValues: defaultformValues,
mode: 'onChange', // Enable real-time validation
});
const onSubmit = async (data: UpdateUser) => {
const formData = data
saveAccountingAccounts(formData, {
onSuccess: () => {
form.reset();
onSuccess?.();
},
onError: () => {
form.setError('root', {
type: 'manual',
message: 'Error al guardar la cuenta contable',
});
},
});
};
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-2 gap-4">
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Usuario</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="fullname"
render={({ field }) => (
<FormItem>
<FormLabel>Nombre completo</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Correo</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="phone"
render={({ field }) => (
<FormItem>
<FormLabel>Teléfono</FormLabel>
<FormControl>
<Input {...field} value={field.value?.toString() ?? ''}/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='password'
render={({ field }) => (
<FormItem>
<FormLabel>Nueva Contraseña</FormLabel>
<FormControl>
<Input type="password" {...field}/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="role"
render={({ field }) => (
<FormItem className="w-full">
<FormLabel>Rol</FormLabel>
<Select onValueChange={(value) => field.onChange(Number(value))}
// defaultValue={String(field.value)}
>
<FormControl>
<SelectTrigger className="w-full">
<SelectValue placeholder="Selecciona un rol" />
</SelectTrigger>
</FormControl>
<SelectContent className="w-full min-w-[200px]">
{Object.entries(ROLES).map(([value, label]) => (
<SelectItem key={value} value={value}>
{label}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="isActive"
render={({ field }) => (
<FormItem className="w-full">
<FormLabel>Estatus</FormLabel>
<Select defaultValue={String(field.value)} onValueChange={(value) => field.onChange(Boolean(value))}>
<SelectTrigger className="w-full">
<SelectValue placeholder="Seleccione un estatus" />
</SelectTrigger>
<SelectContent>
<SelectItem value="true">Activo</SelectItem>
<SelectItem value="false">Inactivo</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="flex justify-end gap-4">
<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,69 @@
'use client';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@repo/shadcn/dialog';
import { AccountPlan } from '@/feactures/users/schemas/account-plan.schema';
import { CreateUserForm } from './create-user-form';
import { UpdateUserForm } from './update-user-form';
interface AccountPlanModalProps {
open: boolean;
onOpenChange: (open: boolean) => void;
defaultValues?: Partial<AccountPlan>;
}
export function AccountPlanModal({
open,
onOpenChange,
defaultValues,
}: AccountPlanModalProps) {
const handleSuccess = () => {
onOpenChange(false);
};
const handleCancel = () => {
onOpenChange(false);
};
return (
<Dialog
open={open}
onOpenChange={(open) => {
if (!open) {
onOpenChange(false);
}
}}
>
<DialogContent className="sm:max-w-[600px] z-50 backdrop-blur-lg bg-background/80">
<DialogHeader>
<DialogTitle>
{defaultValues?.id
? 'Actualizar usuario'
: 'Crear usuario'}
</DialogTitle>
<DialogDescription>
Complete los campos para {defaultValues?.id ? 'actualizar' : 'crear'} un usuario
</DialogDescription>
</DialogHeader>
{defaultValues?.id ? (
<UpdateUserForm
onSuccess={handleSuccess}
onCancel={handleCancel}
defaultValues={defaultValues}
/>
): (
<CreateUserForm
onSuccess={handleSuccess}
onCancel={handleCancel}
defaultValues={defaultValues}
/>
)}
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,27 @@
'use client';
import { useRouter } from 'next/navigation';
import { Button } from '@repo/shadcn/button';
import { Heading } from '@repo/shadcn/heading';
import { Plus } from 'lucide-react';
import { useState } from 'react';
import { AccountPlanModal } from './user-modal';
export function UsersHeader() {
const [open, setOpen] = useState(false);
// const router = useRouter();
return (
<>
<div className="flex items-start justify-between">
<Heading
title="Administración del inventario"
description="Gestione aquí los productos que usted registre en la plataforma"
/>
<Button onClick={() => setOpen(true)} size="sm">
<Plus className="mr-2 h-4 w-4" /> Agregar Usuario
</Button>
</div>
<AccountPlanModal open={open} onOpenChange={setOpen} />
</>
);
}

View File

@@ -0,0 +1,57 @@
'use client';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@repo/shadcn/dialog';
import { AccountPlan } from '@/feactures/users/schemas/account-plan.schema';
import { ModalForm } from './update-user-form';
interface AccountPlanModalProps {
open: boolean;
onOpenChange: (open: boolean) => void;
defaultValues?: Partial<AccountPlan>;
}
export function AccountPlanModal({
open,
onOpenChange,
defaultValues,
}: AccountPlanModalProps) {
const handleSuccess = () => {
onOpenChange(false);
};
const handleCancel = () => {
onOpenChange(false);
};
return (
<Dialog
open={open}
onOpenChange={(open) => {
if (!open) {
onOpenChange(false);
}
}}
>
<DialogContent className="sm:max-w-[600px] z-50 backdrop-blur-lg bg-background/80">
<DialogHeader>
<DialogTitle>Actualizar Perfil</DialogTitle>
<DialogDescription>
Complete los campos para actualizar sus datos.<br/>Los campos vacios no seran actualizados.
</DialogDescription>
</DialogHeader>
<ModalForm
onSuccess={handleSuccess}
onCancel={handleCancel}
defaultValues={defaultValues}
/>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,85 @@
// 'use client';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@repo/shadcn/select';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@repo/shadcn/form';
import { UpdateUser, updateUser } from '@/feactures/users/schemas/users';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
interface SelectListProps {
label: string
// values:
values: Array<object>
form: any
name: string
handleChange: any
}
export function SelectList({ label, values, form, name, handleChange }: SelectListProps) {
// const { label, values, form, name } = props;
// handleChange
// const defaultformValues = {
// username: '',
// fullname: '',
// email: '',
// password: '',
// id: 0,
// phone: '',
// role: undefined,
// isActive: false
// }
// const form = useForm<UpdateUser>({
// resolver: zodResolver(updateUser),
// defaultValues: defaultformValues,
// mode: 'onChange', // Enable real-time validation
// });
return <FormField
control={form.control}
name={name}
render={({ field }) => (
<FormItem className="w-full">
<FormLabel>{label}</FormLabel>
<Select onValueChange={handleChange}
// defaultValue={String(field.value)}
>
<FormControl>
<SelectTrigger className="w-full">
<SelectValue placeholder="Selecciona una opción" />
</SelectTrigger>
</FormControl>
<SelectContent className="w-full min-w-[200px]">
{values.map((item: any) => (
<SelectItem key={item.id} value={item.id}>
{item.name}
</SelectItem>
))}
{/* <SelectItem key={0} value="0">Hola1</SelectItem>
<SelectItem key={1} value="1">Hola2</SelectItem> */}
{/* {Object.entries(values).map(([id, label]) => (
<SelectItem key={id} value={id}>
{label}
</SelectItem>
))} */}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
}

View File

@@ -0,0 +1,28 @@
'use client'
import { SurveyResponse } from '@/feactures/surveys/components/survey-response';
import { useSurveysByIdQuery } from '@/feactures/surveys/hooks/use-query-surveys';
import { notFound, useParams } from 'next/navigation';
export default function SurveyPage() {
const params = useParams();
const surveyId = params?.id as string | undefined;
if (!surveyId || surveyId === '') {
notFound();
}
const { data: survey, isLoading } = useSurveysByIdQuery(Number(surveyId));
console.log('🎯 useSurveysByIdQuery ejecutado, data:', survey, 'isLoading:', isLoading);
if (!survey?.data || !survey?.data.published) {
notFound();
}
return (
<SurveyResponse survey={survey?.data} />
);
}

View File

@@ -0,0 +1,268 @@
'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 {
// Select,
// SelectContent,
// SelectItem,
// SelectTrigger,
// SelectValue,
// } from '@repo/shadcn/select';
import { SelectSearchable } from '@repo/shadcn/select-searchable'
import { useForm } from 'react-hook-form';
import { useUpdateProfile } from "@/feactures/users/hooks/use-mutation-users";
import { UpdateUser, updateUser } from '@/feactures/users/schemas/users';
import { toast } from 'sonner';
import React from 'react';
import { useStateQuery, useMunicipalityQuery, useParishQuery } from '@/feactures/location/hooks/use-query-location';
interface UserFormProps {
onSuccess?: () => void;
onCancel?: () => void;
defaultValues?: Partial<UpdateUser>;
}
export function ModalForm({
onSuccess,
onCancel,
defaultValues,
}: UserFormProps) {
const {
mutate: saveAccountingAccounts,
isPending: isSaving,
isError,
} = useUpdateProfile();
const [state, setState] = React.useState(0);
const [municipality, setMunicipality] = React.useState(0);
const [parish, setParish] = 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 defaultformValues = {
username: defaultValues?.username || '',
fullname: defaultValues?.fullname || '',
email: defaultValues?.email || '',
password: '',
id: defaultValues?.id,
phone: defaultValues?.phone || '',
role: undefined,
isActive: defaultValues?.isActive,
state: defaultValues?.state,
municipality: defaultValues?.municipality,
parish: defaultValues?.parish
}
// console.log(defaultValues);
const form = useForm<UpdateUser>({
resolver: zodResolver(updateUser),
defaultValues: defaultformValues,
mode: 'onChange', // Enable real-time validation
});
const onSubmit = async (data: UpdateUser) => {
const formData = data
saveAccountingAccounts(formData, {
onSuccess: () => {
form.reset();
onSuccess?.();
toast.success('Actualizado exitosamente!');
},
onError: (e) => {
form.setError('root', {
type: 'manual',
message: e.message,
});
// toast.error(e.message);
},
});
};
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-2 gap-4">
<FormField
control={form.control}
name="fullname"
render={({ field }) => (
<FormItem>
<FormLabel>Nombre completo</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Correo</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="phone"
render={({ field }) => (
<FormItem>
<FormLabel>Teléfono</FormLabel>
<FormControl>
<Input {...field} value={field.value?.toString() ?? ''}/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<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='password'
render={({ field }) => (
<FormItem>
<FormLabel>Nueva Contraseña</FormLabel>
<FormControl>
<Input type="password" {...field}/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="flex justify-end gap-4">
<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,75 @@
'use client';
import { useUserByProfile } from '@/feactures/users/hooks/use-query-users';
import { Button } from '@repo/shadcn/button';
import { Edit, Edit2 } from 'lucide-react';
import { useState } from 'react';
import { AccountPlanModal } from './modal-profile';
export function Profile() {
const [open, setOpen] = useState(false);
const { data } = useUserByProfile();
// console.log("🎯 data:", data);
return (
<div>
<Button onClick={() => setOpen(true)} size="sm">
<Edit2 className="mr-2 h-4 w-4" /> Editar Perfil
</Button>
<AccountPlanModal open={open} onOpenChange={setOpen} defaultValues={data?.data}/>
<h2 className='mt-3 mb-1'>Datos del usuario</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<section className='border bg-muted p-2 rounded-md'>
<p className='font-bold text-lg'>Usuario:</p>
<p>{data?.data.username || 'Sin Nombre de Usuario'}</p>
</section>
<section className='border bg-muted p-2 rounded-md'>
<p className='font-bold text-lg'>Rol:</p>
<p>{data?.data.role || 'Sin Rol'}</p>
</section>
</div>
<h2 className='mt-3 mb-1'>Información personal</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<section className='border bg-muted p-2 rounded-md'>
<p className='font-bold text-lg'>Nombre completo:</p>
<p>{data?.data.fullname || 'Sin nombre y apellido'}</p>
</section>
<section className='border bg-muted p-2 rounded-md'>
<p className='font-bold text-lg'>Correo:</p>
<p>{data?.data.email || 'Sin correo'}</p>
</section>
<section className='border bg-muted p-2 rounded-md'>
<p className='font-bold text-lg'>Teléfono:</p>
<p>{data?.data.phone || 'Sin teléfono'}</p>
</section>
</div>
<h2 className='mt-3 mb-1'>Información de ubicación</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<section className='border bg-muted p-2 rounded-md'>
<p className='font-bold text-lg'>Estado:</p>
<p>{data?.data.state || 'Sin Estado'}</p>
</section>
<section className='border bg-muted p-2 rounded-md'>
<p className='font-bold text-lg'>Municipio:</p>
<p>{data?.data.municipality || 'Sin Municipio'}</p>
</section>
<section className='border bg-muted p-2 rounded-md'>
<p className='font-bold text-lg'>Parroquia:</p>
<p>{data?.data.parish || 'Sin Parroquia'}</p>
</section>
</div>
</div>
);
}

View File

@@ -0,0 +1,45 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { CreateUser, UpdateUser } from "../schemas/inventory";
import { updateUserAction, createUserAction, deleteUserAction, updateProfileAction } from "../actions/actions";
// Create mutation
export function useCreateUser() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (data: CreateUser) => createUserAction(data),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['users'] }),
// onError: (e) => console.error('Error:', e),
})
return mutation
}
// Update mutation
export function useUpdateUser() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (data: UpdateUser) => updateUserAction(data),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['users'] }),
onError: (e) => console.error('Error:', e)
})
return mutation;
}
export function useUpdateProfile() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (data: UpdateUser) => updateProfileAction(data),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['users'] }),
// onError: (e) => console.error('Error:', e)
})
return mutation;
}
// Delete mutation
export function useDeleteUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: number) => deleteUserAction(id),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['users'] }),
onError: (e) => console.error('Error:', e)
})
}

View File

@@ -0,0 +1,12 @@
'use client'
import { useSafeQuery } from "@/hooks/use-safe-query";
import { getUsersAction,getProfileAction} from "../actions/actions";
// Hook for users
export function useUsersQuery(params = {}) {
return useSafeQuery(['users',params], () => getUsersAction(params))
}
export function useUserByProfile() {
return useSafeQuery(['users'], () => getProfileAction())
}

View File

@@ -0,0 +1,8 @@
'use client'
import { useSafeQuery } from "@/hooks/use-safe-query";
import { getInventoryAction} from "../actions/actions";
// Hook for users
export function useProductQuery(params = {}) {
return useSafeQuery(['product',params], () => getInventoryAction(params))
}

View File

@@ -0,0 +1,19 @@
export const ACCOUNT_TYPES = {
activo: 'Activo',
pasivo: 'Pasivo',
patrimonio: 'Patrimonio',
ingreso: 'Ingreso',
gasto: 'Gasto',
costo: 'Costo',
cuenta_orden: 'Cuenta de Orden',
} as const;
export const ACCOUNT_LEVELS = {
1: 'Nivel 1 - Cuenta Principal',
2: 'Nivel 2 - Subcuenta',
3: 'Nivel 3 - Cuenta Detallada',
4: 'Nivel 4 - Cuenta Auxiliar',
} as const;
export type AccountType = keyof typeof ACCOUNT_TYPES;
export type AccountLevel = keyof typeof ACCOUNT_LEVELS;

View File

@@ -0,0 +1,83 @@
import { z } from 'zod';
export const accountPlanSchema = z
.object({
id: z.number().optional(),
savingBankId: z.number(),
code: z
.string()
.min(1, 'El código es requerido')
.max(50, 'El código no puede tener más de 50 caracteres')
.regex(/^[\d.]+$/, 'El código debe contener solo números y puntos'),
name: z
.string()
.min(1, 'El nombre es requerido')
.max(100, 'El nombre no puede tener más de 100 caracteres'),
type: z.enum(
[
'activo',
'pasivo',
'patrimonio',
'ingreso',
'gasto',
'costo',
'cuenta_orden',
],
{
required_error: 'El tipo de cuenta es requerido',
invalid_type_error: 'Tipo de cuenta inválido',
},
),
description: z.string().optional().nullable(),
level: z
.number()
.min(1, 'El nivel debe ser mayor a 0')
.max(4, 'El nivel no puede ser mayor a 4'),
parent_account_id: z.number().nullable(),
created_at: z.string().optional(),
updated_at: z.string().optional(),
})
.refine(
(data) => {
if (data.level > 1 && !data.parent_account_id) {
return false;
}
return true;
},
{
message: 'Las cuentas de nivel superior a 1 requieren una cuenta padre',
path: ['parent_account_id'],
},
);
export type AccountPlan = z.infer<typeof accountPlanSchema>;
// Response schemas for the API
export const accountPlanResponseSchema = z.object({
message: z.string(),
data: accountPlanSchema,
});
export const accountPlanDeleteResponseSchema = z.object({
message: z.string(),
});
export const accountPlanListResponseSchema = z.object({
message: z.string(),
data: z.array(accountPlanSchema),
});
export const accountPlanPaginationResponseSchema = z.object({
message: z.string(),
data: z.array(accountPlanSchema),
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(),
}),
});

View File

@@ -0,0 +1,69 @@
import { title } from 'process';
import { z } from 'zod';
export type InventoryTable = z.infer<typeof product>;
export type CreateUser = z.infer<typeof createUser>;
export type UpdateUser = z.infer<typeof updateUser>;
export const product = z.object({
id: z.number().optional(),
title: z.string(),
description: z.string(),
// price: z.number(),
// quantity: z.number(),
// category: z.string(),
// image: z.string().optional(),
stock: z.number(),
price: z.string(),
urlImg: z.string(),
userId: z.number().optional(),
});
export const createUser = z.object({
id: z.number().optional(),
username: z.string().min(5, { message: "Debe de tener 5 o más caracteres" }),
password: z.string().min(8, { message: "Debe de tener 8 o más caracteres" }),
email: z.string().email({ message: "Correo no válido" }),
fullname: z.string(),
phone: z.string(),
confirmPassword: z.string(),
role: z.number()
})
.refine((data) => data.password === data.confirmPassword, {
message: 'La contraseña no coincide',
path: ['confirmPassword'],
})
export const updateUser = z.object({
id: z.number(),
username: z.string().min(5, { message: "Debe de tener 5 o más caracteres" }).or(z.literal('')),
password: z.string().min(6, { message: "Debe de tener 6 o más caracteres" }).or(z.literal('')),
email: z.string().email({ message: "Correo no válido" }).or(z.literal('')),
fullname: z.string().optional(),
phone: z.string().optional(),
role: z.number().optional(),
isActive: z.boolean().optional(),
state: z.number().optional().nullable(),
municipality: z.number().optional().nullable(),
parish: z.number().optional().nullable(),
})
export const surveysApiResponseSchema = z.object({
message: z.string(),
data: z.array(product),
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 productMutate = z.object({
message: z.string(),
data: product,
})

View File

@@ -0,0 +1,6 @@
export const PUBLISHED_TYPES = {
published: 'Publicada',
draft: 'Borrador',
} as const;
export type PublishedType = keyof typeof PUBLISHED_TYPES;

View File

@@ -0,0 +1,11 @@
export function formatDate(date: Date): string {
return new Intl.DateTimeFormat('es-ES', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
}).format(date);
}

View File

@@ -0,0 +1,16 @@
import {
createSearchParamsCache,
createSerializer,
parseAsInteger,
parseAsString,
} from 'nuqs/server';
export const searchParams = {
page: parseAsInteger.withDefault(1),
limit: parseAsInteger.withDefault(10),
q: parseAsString,
type: parseAsString,
};
export const searchParamsCache = createSearchParamsCache(searchParams);
export const serialize = createSerializer(searchParams);