diff --git a/mock-server/src/dals/user/user.repository.ts b/mock-server/src/dals/user/user.repository.ts index 71de253..5c50d93 100644 --- a/mock-server/src/dals/user/user.repository.ts +++ b/mock-server/src/dals/user/user.repository.ts @@ -27,4 +27,11 @@ export const userRepository = { } return index !== -1; }, + resetPassword: async (id: string, nuevaContraseña: string): Promise => { + const index = db.users.findIndex(user => user._id.toHexString() === id); + if (index !== -1) { + db.users[index].contraseña = nuevaContraseña; + } + return index !== -1; + }, }; diff --git a/mock-server/src/pods/user/user.rest-api.ts b/mock-server/src/pods/user/user.rest-api.ts index d03c788..aa784b3 100644 --- a/mock-server/src/pods/user/user.rest-api.ts +++ b/mock-server/src/pods/user/user.rest-api.ts @@ -63,4 +63,29 @@ userApi } catch (error) { next(error); } + }) + // TODO: Implementar la lógica para actualizar el password + .put('/resetPassword/:id', async (req, res, next) => { + try { + //const { id } = req.params; + //const { contraseña } = req.body; + // comporabar si existe el usuario sino mandamos un erro un 404 + //const estaActualizado = await userRepository.resetPassword(id, contraseña); + // if (usaarioexiste) { + // await userRepository.resetPassword(id, password); + // } else { + // res.sendStatus(404); + // } + //res.status(201).send(estaActualizado); + const { id } = req.params; + const { contraseña } = req.body; + const contraseñaEstaActualizada = await userRepository.resetPassword(id, contraseña); + if (contraseñaEstaActualizada) { + res.status(200).send(contraseñaEstaActualizada); + } else { + res.sendStatus(404); + } + } catch (error) { + next(error); + } }); diff --git a/src/common/components/index.ts b/src/common/components/index.ts index 829e726..b2fbd6d 100644 --- a/src/common/components/index.ts +++ b/src/common/components/index.ts @@ -4,3 +4,4 @@ export * from './sidebar-menu'; export * from './navigation-button'; export * from './spinner'; export * from './form'; +export * from './snackbar'; diff --git a/src/common/components/snackbar/index.ts b/src/common/components/snackbar/index.ts new file mode 100644 index 0000000..52b0843 --- /dev/null +++ b/src/common/components/snackbar/index.ts @@ -0,0 +1,4 @@ +export * from './snackbar.component'; +export { SnackbarProvider } from './snackbar.context'; +export { useSnackbarContext } from './snackbar.hook'; +export * from './snackbar.vm'; diff --git a/src/common/components/snackbar/snackbar.component.tsx b/src/common/components/snackbar/snackbar.component.tsx new file mode 100644 index 0000000..69e7b4e --- /dev/null +++ b/src/common/components/snackbar/snackbar.component.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import Snackbar, { SnackbarOrigin } from '@mui/material/Snackbar'; +import SnackbarContent from '@mui/material/SnackbarContent'; +import IconButton from '@mui/material/IconButton'; +import CloseIcon from '@mui/icons-material/Close'; +import { useSnackbar } from './snackbar.context'; +import * as classes from './snackbar.styles'; + +interface Props { + autoHideDuration?: number; + position?: SnackbarOrigin; +} + +export const SnackbarComponent: React.FunctionComponent = props => { + const { position = { horizontal: 'right', vertical: 'top' }, autoHideDuration = 3000 } = props; + const { open, onClose, options } = useSnackbar(); + + return ( + + + + , + ]} + /> + + ); +}; diff --git a/src/common/components/snackbar/snackbar.context.tsx b/src/common/components/snackbar/snackbar.context.tsx new file mode 100644 index 0000000..7459a1b --- /dev/null +++ b/src/common/components/snackbar/snackbar.context.tsx @@ -0,0 +1,51 @@ +import React, { PropsWithChildren } from 'react'; +import { SnackbarOptions } from './snackbar.vm'; +import { SnackbarCloseReason } from '@mui/material'; + +interface Context { + open: boolean; + setOpen: (open: boolean) => void; + onClose: (event?: React.SyntheticEvent | Event, reason?: SnackbarCloseReason) => void; + options: SnackbarOptions; + setOptions: (options: SnackbarOptions) => void; +} + +export const SnackbarContext = React.createContext(null); + +export const SnackbarProvider: React.FC = props => { + const { children } = props; + const [open, setOpen] = React.useState(false); + const [options, setOptions] = React.useState({ + message: '', + variant: 'success', + }); + + const handleClose = (_?: React.SyntheticEvent | Event, reason?: SnackbarCloseReason) => { + if (reason === 'clickaway') { + return; + } + setOpen(false); + }; + + return ( + + {children} + + ); +}; + +export const useSnackbar = () => { + const context = React.useContext(SnackbarContext); + if (!context) { + throw new Error('useSnackbar must be used within a SnackbarProvider'); + } + return context; +}; diff --git a/src/common/components/snackbar/snackbar.hook.tsx b/src/common/components/snackbar/snackbar.hook.tsx new file mode 100644 index 0000000..48faede --- /dev/null +++ b/src/common/components/snackbar/snackbar.hook.tsx @@ -0,0 +1,13 @@ +import { useSnackbar } from './snackbar.context'; +import { Variant } from './snackbar.vm'; + +export const useSnackbarContext = () => { + const { setOptions, setOpen } = useSnackbar(); + + return { + showMessage: (message: string, variant: Variant) => { + setOptions({ message, variant }); + setOpen(true); + }, + }; +}; diff --git a/src/common/components/snackbar/snackbar.styles.ts b/src/common/components/snackbar/snackbar.styles.ts new file mode 100644 index 0000000..feb79d0 --- /dev/null +++ b/src/common/components/snackbar/snackbar.styles.ts @@ -0,0 +1,35 @@ +import { css } from '@emotion/css'; +import { theme } from '#core/theme'; + +export const success = css` + &.MuiSnackbarContent-root { + background-color: ${theme.palette.success.main}; + } +`; + +export const error = css` + &.MuiSnackbarContent-root { + background-color: ${theme.palette.error.dark}; + } +`; + +export const info = css` + &.MuiSnackbarContent-root { + background-color: ${theme.palette.info.main}; + } +`; + +export const warning = css` + &.MuiSnackbarContent-root { + background-color: ${theme.palette.warning.main}; + } +`; + +export const snackbarContent = css` + align-items: flex-start; +`; + +export const message = css` + align-self: center; + white-space: pre; +`; diff --git a/src/common/components/snackbar/snackbar.vm.ts b/src/common/components/snackbar/snackbar.vm.ts new file mode 100644 index 0000000..5adf446 --- /dev/null +++ b/src/common/components/snackbar/snackbar.vm.ts @@ -0,0 +1,6 @@ +export type Variant = 'success' | 'info' | 'warning' | 'error'; + +export interface SnackbarOptions { + message: string; + variant: Variant; +} diff --git a/src/core/notification/index.ts b/src/core/notification/index.ts new file mode 100644 index 0000000..527f422 --- /dev/null +++ b/src/core/notification/index.ts @@ -0,0 +1,2 @@ +export * from './notification.provider'; +export * from './notification.hooks'; diff --git a/src/core/notification/notification.hooks.ts b/src/core/notification/notification.hooks.ts new file mode 100644 index 0000000..f633e18 --- /dev/null +++ b/src/core/notification/notification.hooks.ts @@ -0,0 +1,16 @@ +import { Variant, useSnackbarContext } from '#common/components'; + +export type Notify = { + notify: (message: string, variant?: Variant) => void; +}; + +export const useNotification = (): Notify => { + const { showMessage } = useSnackbarContext(); + const notify = (message: string, variant: Variant = 'error') => { + showMessage(message, variant); + }; + + return { + notify, + }; +}; diff --git a/src/core/notification/notification.provider.tsx b/src/core/notification/notification.provider.tsx new file mode 100644 index 0000000..a4df3a8 --- /dev/null +++ b/src/core/notification/notification.provider.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import { SnackbarComponent, SnackbarProvider } from '#common/components'; + +interface Props { + children: React.ReactNode; +} + +export const NotificationProvider: React.FC = props => { + const { children } = props; + + return ( + + + {children} + + ); +}; diff --git a/src/main.tsx b/src/main.tsx index f824cb0..cb7060f 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -6,6 +6,7 @@ import { RouterProvider } from '@tanstack/react-router'; import { queryClient } from './core/react-query'; import { router } from './core/router'; import { ThemeProvider } from './core/theme'; +import { NotificationProvider } from './core/notification'; const App = () => { return ; @@ -15,8 +16,10 @@ createRoot(document.getElementById('root')!).render( - - + + + + diff --git a/src/modules/users/edit-reset-password/api/edit-reset-password.api.ts b/src/modules/users/edit-reset-password/api/edit-reset-password.api.ts new file mode 100644 index 0000000..7e37acb --- /dev/null +++ b/src/modules/users/edit-reset-password/api/edit-reset-password.api.ts @@ -0,0 +1,7 @@ +import axios from 'axios'; + +export const updatePassword = async (password: string, id: string): Promise => { + const response = await axios.put(`/api/user/resetPassword/${id}`, { contraseña: password }); + + return response.data; +}; diff --git a/src/modules/users/edit-reset-password/api/index.ts b/src/modules/users/edit-reset-password/api/index.ts new file mode 100644 index 0000000..8da65fa --- /dev/null +++ b/src/modules/users/edit-reset-password/api/index.ts @@ -0,0 +1 @@ +export * from './edit-reset-password.api'; diff --git a/src/modules/users/edit-reset-password/components/confirm-reset-dialog.component.tsx b/src/modules/users/edit-reset-password/components/confirm-reset-dialog.component.tsx index 0a0aaa7..b9615d5 100644 --- a/src/modules/users/edit-reset-password/components/confirm-reset-dialog.component.tsx +++ b/src/modules/users/edit-reset-password/components/confirm-reset-dialog.component.tsx @@ -5,33 +5,39 @@ import DialogActions from '@mui/material/DialogActions'; import DialogContent from '@mui/material/DialogContent'; import DialogContentText from '@mui/material/DialogContentText'; import DialogTitle from '@mui/material/DialogTitle'; +import { Spinner } from '#common/components/spinner/spinner.component.tsx'; interface Props { open: boolean; handleClose: () => void; + onConfirm: () => void; + isLoading: boolean; } export const ConfirmResetDialog: React.FC = props => { - const { open, handleClose } = props; + const { open, handleClose, onConfirm, isLoading } = props; return ( - - - - {'¿Está seguro que desea resetear la contraseña de este usuario?'} - - - - Si está seguro de cambiar la contraseña presiona CONFIRMAR, de lo contrario presiona CANCELAR - - - - - - - - + <> + + + + + {'¿Está seguro que desea resetear la contraseña de este usuario?'} + + + + Si está seguro de cambiar la contraseña presiona CONFIRMAR, de lo contrario presiona CANCELAR + + + + + + + + + ); }; diff --git a/src/modules/users/edit-reset-password/edit-reset-password.component.tsx b/src/modules/users/edit-reset-password/edit-reset-password.component.tsx index 4212189..45e1861 100644 --- a/src/modules/users/edit-reset-password/edit-reset-password.component.tsx +++ b/src/modules/users/edit-reset-password/edit-reset-password.component.tsx @@ -4,17 +4,26 @@ import { Button, IconButton } from '@mui/material'; import { Visibility, VisibilityOff, ContentCopy } from '@mui/icons-material'; import { TextFieldForm } from '#common/components'; import { useToggle } from '#common/hooks'; +import { ConfirmResetDialog } from './components'; import { formValidation } from './validations'; import { usePassword } from '../create/use-password.hook'; -import { ConfirmResetDialog } from './components'; import { handleCopyPassword } from './edit-reset-password.business'; import { createEmptyInitialResetPassword } from './edit-reset-password.vm'; +import { useUpdateUserPasswordMutation } from './edit-reset-password.query.hook'; import * as classes from './edit-reset-password.styles'; -export const EditResetPasswordComponent: React.FC = () => { +interface Props { + userId: string; +} + +export const EditResetPasswordComponent: React.FC = (props: Props) => { + const { userId } = props; const { showPassword, toggleShowPassword } = usePassword(); + const { isOpen: isOpenDialog, onToggle: onToggleDialog } = useToggle(false); + const { savePassword, isPending } = useUpdateUserPasswordMutation(onToggleDialog); - const { isOpen, onToggle } = useToggle(false); + const handleConfirmPassword = (newPassword: string, userId: string) => + savePassword({ password: newPassword, id: userId }); return (
@@ -23,45 +32,54 @@ export const EditResetPasswordComponent: React.FC = () => { initialValues={createEmptyInitialResetPassword()} enableReinitialize={true} validate={formValidation.validateForm} - onSubmit={() => {}} + onSubmit={onToggleDialog} > - {({ values, isValid, dirty }) => ( -
-
- - {showPassword ? : } - - ), - }, - }} - /> - { - handleCopyPassword(values.contraseña || ''); - }} - className={classes.icon} - > - - -
+ {({ values, isValid, dirty, resetForm }) => ( + <> + +
+ + {showPassword ? : } + + ), + }, + }} + /> + { + handleCopyPassword(values.contraseña || ''); + }} + className={classes.icon} + > + + +
-
- -
-
+
+ +
+ + { + handleConfirmPassword(values.contraseña, userId); + resetForm(); + }} + /> + )} - -
); diff --git a/src/modules/users/edit-reset-password/edit-reset-password.pod.tsx b/src/modules/users/edit-reset-password/edit-reset-password.pod.tsx index 1669ab7..d7a2942 100644 --- a/src/modules/users/edit-reset-password/edit-reset-password.pod.tsx +++ b/src/modules/users/edit-reset-password/edit-reset-password.pod.tsx @@ -1,16 +1,11 @@ import React from 'react'; import { EditResetPasswordComponent } from './edit-reset-password.component'; -// TO DO: prop id se usará cuando el endpoint de reseteo de clave esté listo interface Props { id: string; } -export const EditResetPassword: React.FC = () => { - // const { id } = props; - return ( - <> - - - ); +export const EditResetPassword: React.FC = props => { + const { id } = props; + return ; }; diff --git a/src/modules/users/edit-reset-password/edit-reset-password.query.hook.ts b/src/modules/users/edit-reset-password/edit-reset-password.query.hook.ts new file mode 100644 index 0000000..80fc783 --- /dev/null +++ b/src/modules/users/edit-reset-password/edit-reset-password.query.hook.ts @@ -0,0 +1,33 @@ +import { useMutation } from '@tanstack/react-query'; +import { useNotification } from '#core/notification'; +import { updatePassword } from './api'; + +interface ParamsMutationPassword { + password: string; + id: string; +} + +interface UseSavePasswordMutationResult { + savePassword: (params: ParamsMutationPassword) => void; + isPending: boolean; +} + +export const useUpdateUserPasswordMutation = (onToggleDialog: () => void): UseSavePasswordMutationResult => { + const { notify } = useNotification(); + const { mutate: savePassword, isPending } = useMutation({ + mutationFn: ({ password, id }: ParamsMutationPassword) => updatePassword(password, id), + onSuccess: () => { + onToggleDialog(); + notify('Contraseña actualizada correctamente', 'success'); + }, + onError: () => { + onToggleDialog(); + notify('Error al actualizar la contraseña', 'error'); + }, + }); + + return { + savePassword, + isPending, + }; +};