base con autenticacion, registro, modulo encuestas

This commit is contained in:
2025-06-16 12:02:22 -04:00
commit 475e0754df
411 changed files with 26265 additions and 0 deletions

View File

@@ -0,0 +1,17 @@
'use server';
import { safeFetchApi } from '@/lib';
import { loginResponseSchema, UserFormValue } from '../schemas/login';
export const SignInAction = async (payload: UserFormValue) => {
const [error, data] = await safeFetchApi(
loginResponseSchema,
'/auth/sign-in',
'POST',
payload,
);
if (error) {
return error;
} else {
return data;
}
};

View File

@@ -0,0 +1,20 @@
'use server';
import { safeFetchApi } from '@/lib';
import {
RefreshTokenResponseSchema,
RefreshTokenValue,
} from '../schemas/refreshToken';
export const resfreshTokenAction = async (refreshToken: RefreshTokenValue) => {
const [error, data] = await safeFetchApi(
RefreshTokenResponseSchema,
'/auth/refreshToken',
'POST',
refreshToken,
);
if (error) {
console.error('Error:', error);
} else {
return data;
}
};

View File

@@ -0,0 +1,27 @@
'use server';
import { safeFetchApi } from '@/lib/fetch.api';
import { createUserValue, UsersMutate } from '../schemas/register';
export const registerUserAction = async (payload: createUserValue) => {
const { confirmPassword, ...payloadWithoutId } = payload;
const [error, data] = await safeFetchApi(
UsersMutate,
'/auth/sing-up',
'POST',
payloadWithoutId,
);
if (error) {
// console.error(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');
}
throw new Error('Error al crear el usuario');
}
return payloadWithoutId;
};

View File

@@ -0,0 +1,35 @@
import {
Card,
CardContent,
} from '@repo/shadcn/card';
import { cn } from '@repo/shadcn/lib/utils';
import UserAuthForm from './user-auth-form';
export function LoginForm({
className,
...props
}: React.ComponentPropsWithoutRef<'div'>) {
return (
<div className={cn("flex flex-col gap-6", className)} {...props}>
<Card className="overflow-hidden">
<CardContent className="grid p-0 md:grid-cols-2">
<UserAuthForm />
<div className="relative hidden bg-muted md:block">
<img
src="logo.png"
alt="Image"
className="absolute inset-0 p-10 h-full w-full object-cover "
/>
</div>
</CardContent>
</Card>
{/* <div className="text-balance text-center text-xs text-muted-foreground [&_a]:underline [&_a]:underline-offset-4 hover:[&_a]:text-primary">
By clicking continue, you agree to our <a href="#">Terms of Service</a>{" "}
and <a href="#">Privacy Policy</a>.
</div> */}
</div>
)
}

View File

@@ -0,0 +1,32 @@
import {
Card,
CardContent,
} from '@repo/shadcn/card';
import { cn } from '@repo/shadcn/lib/utils';
// import UserAuthForm from './user-auth-form';
import UserAuthForm from './user-register-form';
export function LoginForm({
className,
...props
}: React.ComponentPropsWithoutRef<'div'>) {
return (
<div className={cn("flex flex-col gap-6", className)} {...props}>
<Card className="overflow-hidden">
<CardContent className="grid p-0">
<UserAuthForm />
{/* <div className="relative hidden bg-muted md:block">
<img
src="logo.png"
alt="Image"
className="absolute inset-0 p-10 h-full w-full object-cover "
/>
</div> */}
</CardContent>
</Card>
</div>
)
}

View File

@@ -0,0 +1,139 @@
'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 { signIn } from 'next-auth/react';
import { useRouter, useSearchParams } from 'next/navigation';
import { useState, useTransition } from 'react';
import { useForm } from 'react-hook-form';
import { toast } from 'sonner';
import { UserFormValue, formSchema } from '../schemas/login';
export default function UserAuthForm() {
const router = useRouter();
const searchParams = useSearchParams();
const callbackUrl = searchParams.get('callbackUrl');
const [loading, startTransition] = useTransition();
const [error, SetError] = useState<string | null>(null);
const defaultValues = {
username: '',
password: '',
};
const form = useForm<UserFormValue>({
resolver: zodResolver(formSchema),
defaultValues,
});
const onSubmit = async (data: UserFormValue) => {
SetError(null); // Limpia cualquier error previo al intentar iniciar sesión
startTransition(async () => {
try {
const login = await signIn('credentials', {
username: data.username,
password: data.password,
redirect: false, // No queremos una redirección automática aquí
});
if (login?.error) {
const errorMessage =
login.error === 'CredentialsSignin'
? 'Usuario o contraseña incorrectos'
: 'Contacte al Administrador';
SetError(errorMessage);
toast.error(errorMessage);
}
// Si la autenticación es exitosa y `redirect: false`, necesitamos redirigir manualmente
if (login?.ok && !login?.error) {
toast.success('Ingreso Exitoso!');
router.push(callbackUrl ?? '/dashboard');
}
} catch (error) {
console.error('Error durante el inicio de sesión:', error);
toast.error('Hubo un error inesperado');
}
});
};
return (
<>
<Form {...form}>
<form className="p-6 md:p-8" onSubmit={form.handleSubmit(onSubmit)}>
<div className="flex flex-col gap-6">
<div className="flex flex-col items-center text-center">
<h1 className="text-2xl font-bold">Sistema Integral Fondemi</h1>
<p className="text-balance text-muted-foreground">
Ingresa tus datos
</p>
</div>
<div className="grid gap-2">
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Usuario</FormLabel>
<FormControl>
<Input
type="text"
placeholder="ingrese su usuario..."
disabled={loading}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="grid gap-2">
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Contraseña</FormLabel>
<FormControl>
<Input
type="password"
placeholder="*************"
disabled={loading}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
{error && (
<FormMessage className="text-red-500">{error}</FormMessage>
)}{' '}
<Button disabled={loading} type="submit" className="w-full">
Ingresar
</Button>
<div className="text-center text-sm">
No tienes una cuenta?{" "}
<a href="/register" className="underline underline-offset-4">
Registrate
</a>
</div>
</div>
</form>
</Form>
</>
);
}

View File

@@ -0,0 +1,321 @@
'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 { signIn } from 'next-auth/react';
import { useRouter, useSearchParams } from 'next/navigation';
import { useState, useTransition } from 'react';
import { useForm } from 'react-hook-form';
import { toast } from 'sonner';
import { SelectSearchable } from '@repo/shadcn/select-searchable'
import { createUserValue, createUser } from '../schemas/register';
import { useRegisterUser } from "../hooks/use-mutation-users";
import React from 'react';
import { useStateQuery, useMunicipalityQuery, useParishQuery } from '@/feactures/location/hooks/use-query-location';
export default function UserAuthForm() {
const router = useRouter();
const searchParams = useSearchParams();
const callbackUrl = searchParams.get('callbackUrl');
const [loading, startTransition] = useTransition();
const [error, SetError] = useState<string | null>(null);
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 = dataMunicipality?.data || [{id:0,stateId:0,name:'Sin Municipios'}]
const parishOptions = dataParish?.data || [{id:0,municipalityId:0,name:'Sin Parroquias'}]
const defaultValues = {
username: '',
password: '',
confirmPassword: '',
fullname: '',
lastname: '',
email: '',
phone: '',
role: 5
};
const form = useForm<createUserValue>({
resolver: zodResolver(createUser),
defaultValues,
});
const {
mutate: saveAccountingAccounts,
isPending: isSaving,
isError,
} = useRegisterUser();
const onSubmit = async (data: createUserValue) => {
SetError(null);
const formData = {role: 5, ...data }
saveAccountingAccounts(formData, {
onSuccess: () => {
form.reset();
toast.success('Registro Exitoso!');
router.push(callbackUrl ?? '/');
},
onError: (e) => {
// form.setError('root', {
// type: 'manual',
// message: 'Error al guardar la cuenta contable',
// });
SetError(e.message);
toast.error(e.message);
},
})
}
return (
<>
<Form {...form}>
<form className="p-6 md:p-8" onSubmit={form.handleSubmit(onSubmit)}>
<div className="flex flex-col gap-6">
<div className="items-center text-center">
<h1 className="text-2xl font-bold">Sistema Integral Fondemi</h1>
<p className="text-balance text-muted-foreground">
Ingresa tus datos
</p>
{ error ? (
<p className="text-balance text-muted-foreground">
{error}
</p>
): null }
</div>
<div className='grid md:grid-cols-2 gap-2'>
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Usuario</FormLabel>
<FormControl>
<Input
type="text"
placeholder="ingrese su usuario..."
// disabled={loading}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="fullname"
render={({ field }) => (
<FormItem>
<FormLabel>Nombre Completo</FormLabel>
<FormControl>
<Input
type="text"
placeholder="ingrese su Nombre..."
// disabled={loading}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="phone"
render={({ field }) => (
<FormItem>
<FormLabel>Teléfono</FormLabel>
<FormControl>
<Input
type="text"
placeholder="ingrese su teléfono..."
// disabled={loading}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Correo</FormLabel>
<FormControl>
<Input
type="text"
placeholder="ingrese su correo..."
// disabled={loading}
{...field}
/>
</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>Contraseña</FormLabel>
<FormControl>
<Input
type="password"
placeholder="*************"
// disabled={loading}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="confirmPassword"
render={({ field }) => (
<FormItem>
<FormLabel>Repita la contraseña</FormLabel>
<FormControl>
<Input
type="password"
placeholder="*************"
// disabled={loading}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
{error && (
<FormMessage className="text-red-500">{error}</FormMessage>
)}{' '}
<Button type="submit" className="w-full">
Registrarce
</Button>
<div className="text-center text-sm">
¿Ya tienes una cuenta?{" "}
<a href="/" className="underline underline-offset-4">Inicia Sesión</a>
</div>
</div>
</form>
</Form>
</>
);
}

View File

@@ -0,0 +1,14 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { createUserValue } from "../schemas/register";
import { registerUserAction } from "../actions/register";
// Create mutation
export function useRegisterUser() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (data: createUserValue) => registerUserAction(data),
// onSuccess: () => queryClient.invalidateQueries({ queryKey: ['users'] }),
// onError: (e) =>
})
return mutation
}

View File

@@ -0,0 +1,46 @@
import { z } from 'zod';
// Definir esquema de validación con Zod para el formulario
export const formSchema = z.object({
username: z
.string()
.min(5, { message: 'Usuario debe tener minimo 5 caracteres' }),
password: z
.string()
.min(6, { message: 'La contraseña debe tener al menos 6 caracteres' }),
});
export type UserFormValue = z.infer<typeof formSchema>;
// Esquema para el rol
const rolSchema = z.object({
id: z.number(),
rol: z.string(),
});
// Esquema para el usuario
const userSchema = z.object({
id: z.number(),
username: z.string(),
fullname: z.string(),
email: z.string().email(),
rol: z.array(rolSchema),
});
// Esquema para los tokens
export const tokensSchema = z.object({
access_token: z.string(),
access_expire_in: z.number(),
refresh_token: z.string(),
refresh_expire_in: z.number(),
});
// Esquema final para la respuesta del backend
export const loginResponseSchema = z.object({
message: z.string(),
user: userSchema,
tokens: tokensSchema,
});
// Tipo TypeScript basado en el esquema de Zod
export type LoginResponse = z.infer<typeof loginResponseSchema>;

View File

@@ -0,0 +1,14 @@
import { z } from 'zod';
import { tokensSchema } from './login';
// Esquema para el refresh token
export const refreshTokenSchema = z.object({
token: z.string(),
});
export type RefreshTokenValue = z.infer<typeof refreshTokenSchema>;
// Esquema final para la respuesta del backend
export const RefreshTokenResponseSchema = z.object({
tokens: tokensSchema,
});

View File

@@ -0,0 +1,35 @@
import { z } from 'zod';
// Definir esquema de validación con Zod para el formulario
export const createUser = z.object({
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(),
state: z.number(),
municipality: z.number(),
parish: z.number(),
confirmPassword: z.string(),
})
.refine((data) => data.password === data.confirmPassword, {
message: 'La contraseña no coincide',
path: ['confirmPassword'],
})
export type createUserValue = z.infer<typeof createUser>;
export const user = z.object({
id: z.number().optional(),
username: z.string(),
email: z.string(),
fullname: z.string(),
phone: z.string().nullable(),
isActive: z.boolean(),
role: z.string()
});
export const UsersMutate = z.object({
message: z.string(),
data: user,
})