Added 'No show' status. Edit user profile

This commit is contained in:
Jose Selesan
2026-04-09 23:01:09 -03:00
parent 4dcaae7136
commit 2d86881f94
7 changed files with 440 additions and 45 deletions

View File

@@ -1,19 +1,91 @@
import { Link } from '@tanstack/react-router'
import { useQuery } from '@tanstack/react-query'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'
import type { UserProfileResponse } from '@repo/api-contract'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import { Button } from '@/components/ui/button'
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form'
import { Input } from '@/components/ui/input'
import { apiClient } from '@/lib/api-client'
import { useAuth } from '@/lib/auth'
const updateProfileSchema = z.object({
fullName: z.string().min(1, 'El nombre es requerido'),
})
const updatePasswordSchema = z.object({
currentPassword: z.string().min(1, 'La contraseña actual es requerida'),
newPassword: z.string().min(6, 'La nueva contraseña debe tener al menos 6 caracteres'),
confirmPassword: z.string().min(1, 'La confirmación de contraseña es requerida'),
}).refine((data) => data.newPassword === data.confirmPassword, {
message: 'Las contraseñas no coinciden',
path: ['confirmPassword'],
})
type UpdateProfileForm = z.infer<typeof updateProfileSchema>
type UpdatePasswordForm = z.infer<typeof updatePasswordSchema>
export function ProfilePage() {
const { user, displayName, initials, avatarUrl, isAuthenticated } = useAuth()
const { user, displayName, initials, avatarUrl, isAuthenticated, updateProfile, updatePassword } = useAuth()
const queryClient = useQueryClient()
const profileQuery = useQuery({
queryKey: ['user-profile'],
enabled: isAuthenticated,
queryFn: (): Promise<UserProfileResponse> => apiClient.user.getProfile(),
})
const updateProfileForm = useForm<UpdateProfileForm>({
resolver: zodResolver(updateProfileSchema),
defaultValues: {
fullName: displayName,
},
})
const updatePasswordForm = useForm<UpdatePasswordForm>({
resolver: zodResolver(updatePasswordSchema),
defaultValues: {
currentPassword: '',
newPassword: '',
confirmPassword: '',
},
})
const updateProfileMutation = useMutation({
mutationFn: updateProfile,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['user-profile'] })
updateProfileForm.reset()
},
})
const updatePasswordMutation = useMutation({
mutationFn: async (data: UpdatePasswordForm) => {
// Note: Supabase doesn't require current password for password update
// The user needs to be recently authenticated
await updatePassword({ password: data.newPassword })
},
onSuccess: () => {
updatePasswordForm.reset()
},
})
const onUpdateProfile = (data: UpdateProfileForm) => {
updateProfileMutation.mutate(data)
}
const onUpdatePassword = (data: UpdatePasswordForm) => {
updatePasswordMutation.mutate(data)
}
if (!isAuthenticated) {
return (
<main className="mx-auto flex min-h-[60vh] w-full max-w-6xl items-center justify-center px-6">
@@ -31,41 +103,143 @@ export function ProfilePage() {
}
return (
<main className="mx-auto flex min-h-[60vh] w-full max-w-6xl items-center justify-center px-6">
<section className="w-full max-w-3xl rounded-xl border bg-card p-6 text-card-foreground shadow-sm">
<div className="flex items-center gap-3">
<Avatar size="lg">
<AvatarImage
src={profileQuery.data?.avatarUrl ?? avatarUrl ?? undefined}
alt={displayName}
/>
<AvatarFallback>{initials}</AvatarFallback>
</Avatar>
<div>
<h1 className="text-2xl font-semibold">
{profileQuery.data?.fullName ?? displayName}
</h1>
<p className="text-sm text-muted-foreground">
{profileQuery.data?.email ?? user?.email}
</p>
{profileQuery.data?.role && (
<main className="mx-auto flex min-h-[60vh] w-full max-w-6xl items-center justify-center px-6 py-8">
<div className="w-full max-w-3xl space-y-6">
<section className="rounded-xl border bg-card p-6 text-card-foreground shadow-sm">
<div className="flex items-center gap-3">
<Avatar size="lg">
<AvatarImage
src={profileQuery.data?.avatarUrl ?? avatarUrl ?? undefined}
alt={displayName}
/>
<AvatarFallback>{initials}</AvatarFallback>
</Avatar>
<div>
<h1 className="text-2xl font-semibold">
{profileQuery.data?.fullName ?? displayName}
</h1>
<p className="text-sm text-muted-foreground">
Rol: {profileQuery.data.role}
{profileQuery.data?.email ?? user?.email}
</p>
)}
{profileQuery.data?.role && (
<p className="text-sm text-muted-foreground">
Rol: {profileQuery.data.role}
</p>
)}
</div>
</div>
</div>
{profileQuery.isLoading && (
<p className="mt-4 text-sm text-muted-foreground">Cargando perfil...</p>
)}
{profileQuery.isLoading && (
<p className="mt-4 text-sm text-muted-foreground">Cargando perfil...</p>
)}
{profileQuery.isError && (
<p className="mt-4 text-sm text-destructive">
No se pudo cargar el perfil desde el backend.
</p>
)}
</section>
{profileQuery.isError && (
<p className="mt-4 text-sm text-destructive">
No se pudo cargar el perfil desde el backend.
</p>
)}
</section>
<section className="rounded-xl border bg-card p-6 text-card-foreground shadow-sm">
<h2 className="text-xl font-semibold mb-4">Editar Perfil</h2>
<Form {...updateProfileForm}>
<form onSubmit={updateProfileForm.handleSubmit(onUpdateProfile)} className="space-y-4">
<FormField
control={updateProfileForm.control}
name="fullName"
render={({ field }) => (
<FormItem>
<FormLabel>Nombre completo</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button
type="submit"
disabled={updateProfileMutation.isPending}
>
{updateProfileMutation.isPending ? 'Guardando...' : 'Guardar cambios'}
</Button>
</form>
</Form>
{updateProfileMutation.isError && (
<p className="mt-4 text-sm text-destructive">
Error al actualizar el perfil: {updateProfileMutation.error?.message}
</p>
)}
{updateProfileMutation.isSuccess && (
<p className="mt-4 text-sm text-green-600">
Perfil actualizado exitosamente.
</p>
)}
</section>
<section className="rounded-xl border bg-card p-6 text-card-foreground shadow-sm">
<h2 className="text-xl font-semibold mb-4">Cambiar Contraseña</h2>
<Form {...updatePasswordForm}>
<form onSubmit={updatePasswordForm.handleSubmit(onUpdatePassword)} className="space-y-4">
<FormField
control={updatePasswordForm.control}
name="currentPassword"
render={({ field }) => (
<FormItem>
<FormLabel>Contraseña actual</FormLabel>
<FormControl>
<Input type="password" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={updatePasswordForm.control}
name="newPassword"
render={({ field }) => (
<FormItem>
<FormLabel>Nueva contraseña</FormLabel>
<FormControl>
<Input type="password" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={updatePasswordForm.control}
name="confirmPassword"
render={({ field }) => (
<FormItem>
<FormLabel>Confirmar nueva contraseña</FormLabel>
<FormControl>
<Input type="password" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button
type="submit"
disabled={updatePasswordMutation.isPending}
>
{updatePasswordMutation.isPending ? 'Cambiando...' : 'Cambiar contraseña'}
</Button>
</form>
</Form>
{updatePasswordMutation.isError && (
<p className="mt-4 text-sm text-destructive">
Error al cambiar la contraseña: {updatePasswordMutation.error?.message}
</p>
)}
{updatePasswordMutation.isSuccess && (
<p className="mt-4 text-sm text-green-600">
Contraseña cambiada exitosamente.
</p>
)}
</section>
</div>
</main>
)
}