first commit

This commit is contained in:
2026-07-16 20:48:43 -03:00
commit 5696cee264
1111 changed files with 322270 additions and 0 deletions
+20
View File
@@ -0,0 +1,20 @@
.logoText {
width: 180px;
height: 25px;
margin: 40px;
}
.backgroundContent {
position: fixed;
overflow: hidden;
z-index: 0;
top: 0px;
left: 0px;
right: 0px;
bottom: 0px;
background: var(--wine-darkest);
/*background: linear-gradient(180deg, var(--wine-darkest) 0%, var(--wine-dark) 100%);*/
color: var(--foreground);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
+42
View File
@@ -0,0 +1,42 @@
import type { Metadata } from "next";
import "@core/app/globals.css";
import Header from "@components/Header/Header";
import Footer from "@components/Footer/Footer";
import UserMenu from "@components/UserMenu/UserMenu";
import Loading from "@components/Loading/Loading";
import Alert from "@components/Alert/Alert";
import Confirm from "@components/Confirm/Confirm";
import TimePicker from "@components/TimePicker/TimePicker";
import TextObjectModal from "@components/TextObjectFilter/TextObjectModal";
import Filter from "@components/Filter/Filter";
import style from "./layout.module.css";
import HeaderConfProvider from "../components/HeaderConfProvider/HeaderConfProvider";
import CheckPendingInvitations from "../components/CheckPendingInvitations/CheckPendingInvitations";
export const metadata: Metadata = {
title: "TurnosXpress",
description: process.env.NEXT_PUBLIC_SLOGAN,
};
export default function AdminLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<div className={style.backgroundContent}>
<Header />
<HeaderConfProvider />
{children}
<Footer />
<UserMenu />
<Filter />
<Loading />
<Alert />
<Confirm />
<TimePicker />
<TextObjectModal />
<CheckPendingInvitations />
</div>
);
}
@@ -0,0 +1,6 @@
import { ApiVoidResult } from "@models/Api.VoidResult.type";
import ApiRequest from "@services/Api.Service";
export const uploadAvatar = async (data: FormData): Promise<ApiVoidResult> => {
return ApiRequest.postFile<ApiVoidResult>("users/upload-avatar", data);
};
@@ -0,0 +1,42 @@
.userProfileHeader {
position: absolute;
top: -70px;
margin-left: 30px;
display: flex;
}
.userProfileHeader > div {
margin-left: 10px;
}
.userProfileHeader > div h1 {
padding-top: 8px;
font-size: 22px;
color: var(--white);
}
.userProfileHeader > div span {
font-size: 14px;
color: var(--white-dark);
}
.userProfileOptionsContainer {
overflow-y: auto;
overflow-x: hidden;
scrollbar-width: none;
position: absolute;
top: 50px;
left: 40px;
right: 40px;
bottom: 0px;
display: flex;
flex-direction: column;
}
.userProfileOptionsContainer::-webkit-scrollbar {
display: none;
}
.userProfileOptionsContainer a {
margin-top: 10px;
}
@@ -0,0 +1,15 @@
import { IUserView, UpdateUserParams } from "@models/User.model";
import { ApiVoidResult } from "@models/Api.VoidResult.type";
import ApiRequest from "@services/Api.Service";
export const loadUserAddress = async (userId: string): Promise<IUserView> => {
return ApiRequest.post<IUserView>("users/get", {
sessionUser: userId,
});
};
export const updateUserAddress = async (data: UpdateUserParams): Promise<ApiVoidResult> => {
return ApiRequest.post<ApiVoidResult>("users/update", {
...data,
});
};
@@ -0,0 +1,10 @@
"use client";
import Saveable from "@core/app/components/Saveable/Saveable";
export default function UserEditLayout({ children }: { children: React.ReactNode }) {
return (
<>
<Saveable>{children}</Saveable>
</>
);
}
@@ -0,0 +1,366 @@
"use client";
import { useState, useEffect } from "react";
import Textbox from "@components/Textbox/Textbox";
import { useSessionStore } from "@store/Sesion.Store";
import { EVENT_TYPES, useEventHandlerStore } from "@store/EventHandler.Store";
import { loadUserAddress, updateUserAddress } from "./UserAddress.Service";
import { IUserView } from "@models/User.model";
import { useAlert } from "@store/Alert.Store";
import { ApiError } from "@models/Server.Error.model";
import { Grid2 } from "@mui/material";
import Location from "@components/Location/Location";
import { useLocationStore } from "@store/Location.Store";
import AccountCircleOutlinedIcon from "@mui/icons-material/AccountCircleOutlined";
import { TextObjectModalContent } from "@core/Store/TextObjectModal.Store";
import { TextObjectFilterResult } from "@core/Models/TextObjectFilter.model";
import TextObjectFilterItem from "@components/TextObjectFilterItem/TextObjectFilterItem";
import filterObjects from "@components/TextObjectFilter/Filter.Service";
import {
FindLocalidadesParams,
FindPartidosParams,
FindProvinciasParams,
} from "@core/Models/Location.model";
import TextObjectFilter from "@core/app/components/TextObjectFilter/TextObjectFilter";
export default function EditProfile() {
const alert = useAlert();
const SessionInfo = useSessionStore();
const eventHandler = useEventHandlerStore();
const [street, setStreet] = useState("");
const [streetNumber, setStreetNumber] = useState("");
const [builingFloor, setBuilingFloor] = useState("");
const [buildingApartament, setBuildingApartament] = useState("");
const [country, setCountry] = useState("");
const [zipCode, setZipCode] = useState("");
const location = useLocationStore();
const [state, setState] = useState("");
const [city, setCity] = useState("");
const [block, setBlock] = useState("");
/**
* FILTRO DE PROVINCIA
* ===================
* */
const [provinciaFilterContent] = useState<TextObjectModalContent>({
header: (
<div>
<div style={{ fontWeight: "700", fontSize: "14px", paddingTop: "7px" }}>
Seleccionar Provincia:
</div>
<p style={{ fontWeight: "500", fontSize: "13px", paddingTop: "2px" }}>
Ingrese el nombre de la provincia para buscarla.
</p>
</div>
),
bottom: <div>&nbsp;</div>,
renderResult: (result: TextObjectFilterResult, index: number) => (
<TextObjectFilterItem
key={index}
data={result}
style={{ textTransform: "capitalize" }}
/>
),
});
const [provinciaFilterData, setProvinciaFilterData] = useState<TextObjectFilterResult[]>([]);
const [selectedProvincia, setSelectedProvincia] = useState<string>("");
/**
* FILTRO DE PARTIDO
* =================
* */
const [partidoFilterContent] = useState<TextObjectModalContent>({
header: (
<div>
<div style={{ fontWeight: "700", fontSize: "14px", paddingTop: "7px" }}>
Seleccionar Partido:
</div>
<p style={{ fontWeight: "500", fontSize: "13px", paddingTop: "2px" }}>
Ingrese el nombre del partido para buscarlo.
</p>
</div>
),
bottom: <div>&nbsp;</div>,
renderResult: (result: TextObjectFilterResult, index: number) => (
<TextObjectFilterItem
key={index}
data={result}
style={{ textTransform: "capitalize" }}
/>
),
});
const [partidosFilterData, setPartidosFilterData] = useState<TextObjectFilterResult[]>([]);
const [selectedPartido, setSelectedPartido] = useState<string>("");
/**
* FILTRO DE LOCALIDAD
* ===================
* */
const [localidadFilterContent] = useState<TextObjectModalContent>({
header: (
<div>
<div style={{ fontWeight: "700", fontSize: "14px", paddingTop: "7px" }}>
Seleccionar Localidad:
</div>
<p style={{ fontWeight: "500", fontSize: "13px", paddingTop: "2px" }}>
Ingrese el nombre de la localidad para buscarla.
</p>
</div>
),
bottom: <div>&nbsp;</div>,
renderResult: (result: TextObjectFilterResult, index: number) => (
<TextObjectFilterItem
key={index}
data={result}
style={{ textTransform: "capitalize" }}
/>
),
});
const [localidadesFilterData, setLocalidadesFilterData] = useState<TextObjectFilterResult[]>(
[]
);
const [selectedLocalidad, setSelectedLocalidad] = useState<string>("");
const cargarProvincias = () => {
filterObjects<FindProvinciasParams>("location/provincias-filter-all", {}).then((res) => {
setProvinciaFilterData(res);
});
};
const cargarPartidos = (provincia: string) => {
filterObjects<FindPartidosParams>("location/partidos-filter-all", {
provincia,
}).then((res) => {
setPartidosFilterData(res);
});
};
const cargarLocalidades = (provincia: string, partido: string) => {
filterObjects<FindLocalidadesParams>("location/localidades-filter-all", {
provincia,
partido,
}).then((res) => {
setLocalidadesFilterData(res);
});
};
useEffect(() => {
setSelectedProvincia(state);
}, [provinciaFilterData]);
useEffect(() => {
setSelectedPartido(city);
}, [partidosFilterData]);
useEffect(() => {
setSelectedLocalidad(block);
}, [localidadesFilterData]);
useEffect(() => {
location.clear();
location.add({
url: "/user/profile",
title: "Perfil de usuario",
icon: <AccountCircleOutlinedIcon sx={{ fontSize: "32px", mr: "10px" }} />,
});
location.add({
url: "/user/profile/address",
title: "Editar domicilio",
icon: <AccountCircleOutlinedIcon sx={{ fontSize: "32px", mr: "10px" }} />,
});
}, []);
useEffect(() => {
if (!SessionInfo.userId) return;
loadUserAddress(SessionInfo.userId)
.then((response: IUserView) => {
setStreet(response.street);
setStreetNumber(response.streetNumber);
setBuilingFloor(response.builingFloor);
setBuildingApartament(response.buildingApartament);
setState(response.state.toUpperCase());
setCity(response.city.toUpperCase());
setBlock(response.block.toUpperCase());
setCountry(response.country);
setZipCode(response.zipCode);
cargarProvincias();
cargarPartidos(response.state.toUpperCase());
cargarLocalidades(response.state.toUpperCase(), response.city.toUpperCase());
})
.catch(() => {});
}, [SessionInfo.userId]);
useEffect(() => {
if (eventHandler.eventType === EVENT_TYPES.SAVE) {
updateUserAddress({
id: SessionInfo.userId,
street: street,
streetNumber: streetNumber,
builingFloor: builingFloor,
buildingApartament: buildingApartament,
block: block,
city: city,
state: state,
country: country,
zipCode: zipCode,
sessionUser: SessionInfo.userId,
})
.then(() => {
alert.showSuccess("Dirección actualizada!");
})
.catch((error: ApiError) => {
alert.showError(error.format());
})
.finally(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
});
}
}, [eventHandler.eventType]);
return (
<>
<Location />
<Grid2 container sx={{ marginTop: "10px" }}>
<Grid2 size={6}>
<Textbox
name="street"
placeholder="Calle"
type="text"
value={street}
width="100%"
onChange={(e) => {
setStreet(e.target.value);
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
}}
/>
</Grid2>
<Grid2 size={1}></Grid2>
<Grid2 size={5}>
<Textbox
name="streetNumber"
placeholder="Nro."
type="text"
value={streetNumber}
width="100%"
onChange={(e) => {
setStreetNumber(e.target.value);
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
}}
/>
</Grid2>
</Grid2>
<Grid2 container>
<Grid2 size={6}>
<Textbox
name="buildingApartament"
placeholder="Apartamento"
type="text"
value={buildingApartament}
width="100%"
onChange={(e) => {
setBuildingApartament(e.target.value);
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
}}
/>
</Grid2>
<Grid2 size={1}></Grid2>
<Grid2 size={5}>
<Textbox
name="builingFloor"
placeholder="Piso"
type="text"
value={builingFloor}
width="100%"
onChange={(e) => {
setBuilingFloor(e.target.value);
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
}}
/>
</Grid2>
</Grid2>
{/* <Textbox
name="country"
placeholder="Pais"
type="text"
value={country}
width="100%"
onChange={(e) => {
setCountry(e.target.value);
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
}}
/> */}
<TextObjectFilter
placeholder="Provincia"
content={provinciaFilterContent}
data={provinciaFilterData}
selectedId={selectedProvincia}
onChange={(id) => {
setState(id);
if (id !== "") {
cargarPartidos(id);
} else {
setPartidosFilterData([]);
}
setCity("");
setLocalidadesFilterData([]);
setBlock("");
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
}}
/>
<TextObjectFilter
placeholder="Partido"
content={partidoFilterContent}
data={partidosFilterData}
selectedId={selectedPartido}
onChange={(id) => {
setCity(id);
if (id !== "") {
cargarLocalidades(state, id);
} else {
setLocalidadesFilterData([]);
}
setBlock(" ");
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
}}
/>
<TextObjectFilter
placeholder="Localidad"
content={localidadFilterContent}
data={localidadesFilterData}
selectedId={selectedLocalidad}
onChange={(id) => {
setBlock(id);
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
}}
/>
<Textbox
name="zipCode"
placeholder="C.P."
type="text"
value={zipCode}
width="100%"
onChange={(e) => {
setZipCode(e.target.value);
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
}}
/>
<div style={{ marginBottom: "20px" }}>&nbsp;</div>
</>
);
}
@@ -0,0 +1,7 @@
import { DeleteUserParams } from "@core/Models/User.model";
import { ApiVoidResult } from "@models/Api.VoidResult.type";
import ApiRequest from "@services/Api.Service";
export const deleteUser = async (data: DeleteUserParams): Promise<ApiVoidResult> => {
return ApiRequest.post<ApiVoidResult>("users/delete", data);
};
@@ -0,0 +1,5 @@
"use client";
export default function UserEditLayout({ children }: { children: React.ReactNode }) {
return <>{children}</>;
}
@@ -0,0 +1,109 @@
"use client";
import { useState, useEffect } from "react";
import Textbox from "@components/Textbox/Textbox";
import { useSessionStore, useSessionTokenStore } from "@store/Sesion.Store";
import { EVENT_TYPES, useEventHandlerStore } from "@store/EventHandler.Store";
import { useAlert } from "@store/Alert.Store";
import Location from "@components/Location/Location";
import { useLocationStore } from "@store/Location.Store";
import AccountCircleOutlinedIcon from "@mui/icons-material/AccountCircleOutlined";
import MaterialAlert from "@mui/material/Alert";
import Button from "@core/app/components/Button/Button";
import { useConfirmStore } from "@core/Store/Confirm.Store";
import { deleteUser } from "./Advanced.Service";
import { useNavigation } from "@core/app/hooks/goto";
export default function EditProfile() {
const alert = useAlert();
const confirm = useConfirmStore();
const SessionInfo = useSessionStore();
const SessionToken = useSessionTokenStore();
const eventHandler = useEventHandlerStore();
const [verification, setVerification] = useState("");
const { goTo } = useNavigation();
const location = useLocationStore();
useEffect(() => {
location.clear();
location.add({
url: "/user/profile",
title: "Perfil de usuario",
icon: <AccountCircleOutlinedIcon sx={{ fontSize: "32px", mr: "10px" }} />,
});
location.add({
url: "/user/profile/edit",
title: "Configuración avanzada",
icon: <AccountCircleOutlinedIcon sx={{ fontSize: "32px", mr: "10px" }} />,
});
}, []);
useEffect(() => {
if (!SessionInfo.userId) {
return;
}
}, [SessionInfo.userId]);
const eliminarHandler = () => {
if (!SessionInfo.userId) {
return;
}
if (verification === "eliminar-usuario") {
confirm.show(
"Si eliminas tu usuario se perderá toda la información asociada. ¿Estas seguro?",
() => {
eventHandler.setEventType(EVENT_TYPES.LOADING);
deleteUser({ sessionUser: SessionInfo.userId })
.then(() => {
SessionInfo.clear();
SessionToken.setToken("");
goTo("/landing/login");
alert.showSuccess("Usuario eliminado");
})
.catch((error) => {
alert.showError(error.format());
})
.finally(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
});
},
() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
}
);
} else {
alert.showError("La confirmación no es correcta");
}
};
return (
<>
<Location />
<MaterialAlert severity="info" sx={{ marginTop: "20px", width: "100%" }}>
<strong>Atención:</strong> para darte de baja escribe{" "}
<strong>eliminar-usuario</strong> en la casilla de confirmación.
</MaterialAlert>
<Textbox
name="verification"
placeholder="Confirmación"
type="text"
value={verification}
width="100%"
onChange={(e) => {
setVerification(e.target.value);
}}
/>
<Button
text="Eliminar mi usuario"
width="100%"
color="error"
style={{ height: "42px", marginTop: "10px" }}
onClick={eliminarHandler}
/>
</>
);
}
@@ -0,0 +1,15 @@
import { IUserView, UpdateUserParams } from "@models/User.model";
import { ApiVoidResult } from "@models/Api.VoidResult.type";
import ApiRequest from "@services/Api.Service";
export const loadUserInfo = async (userId: string): Promise<IUserView> => {
return ApiRequest.post<IUserView>("users/get", {
sessionUser: userId,
});
};
export const updateUserInfo = async (data: UpdateUserParams): Promise<ApiVoidResult> => {
return ApiRequest.post<ApiVoidResult>("users/update", {
...data,
});
};
@@ -0,0 +1,10 @@
"use client";
import Saveable from "@components/Saveable/Saveable";
export default function UserEditLayout({ children }: { children: React.ReactNode }) {
return (
<>
<Saveable>{children}</Saveable>
</>
);
}
+137
View File
@@ -0,0 +1,137 @@
"use client";
import { useState, useEffect } from "react";
import Textbox from "@components/Textbox/Textbox";
import { useSessionStore } from "@store/Sesion.Store";
import { EVENT_TYPES, useEventHandlerStore } from "@store/EventHandler.Store";
import TextPhone from "@components/TextPhone/TextPhone";
import { loadUserInfo, updateUserInfo } from "./UserEdit.Service";
import { IUserView } from "@models/User.model";
import { useAlert } from "@store/Alert.Store";
import { ApiError } from "@models/Server.Error.model";
import Location from "@components/Location/Location";
import { useLocationStore } from "@store/Location.Store";
import AccountCircleOutlinedIcon from "@mui/icons-material/AccountCircleOutlined";
import { useConfirmStore } from "@core/Store/Confirm.Store";
import { validatePhone } from "@core/helpers/validatePhone";
export default function EditProfile() {
const alert = useAlert();
const confirm = useConfirmStore();
const SessionInfo = useSessionStore();
const eventHandler = useEventHandlerStore();
const [firstName, setFirstName] = useState("");
const [lastName, setLastName] = useState("");
const [areaCode, setAreaCode] = useState("");
const [phone, setPhone] = useState("");
const location = useLocationStore();
useEffect(() => {
location.clear();
location.add({
url: "/user/profile",
title: "Perfil de usuario",
icon: <AccountCircleOutlinedIcon sx={{ fontSize: "32px", mr: "10px" }} />,
});
location.add({
url: "/user/profile/edit",
title: "Editar perfil",
icon: <AccountCircleOutlinedIcon sx={{ fontSize: "32px", mr: "10px" }} />,
});
}, []);
useEffect(() => {
if (!SessionInfo.userId) {
return;
}
loadUserInfo(SessionInfo.userId)
.then((response: IUserView) => {
setFirstName(response.firstName);
setLastName(response.lastName);
setAreaCode(response.phoneAreaCode);
setPhone(response.phoneNumber);
eventHandler.setEventType(EVENT_TYPES.SLEEP);
})
.catch(() => {});
}, [SessionInfo.userId]);
useEffect(() => {
if (eventHandler.eventType === EVENT_TYPES.SAVE) {
const validationPhoneResult = validatePhone(areaCode, phone);
if (!validationPhoneResult.isValid) {
confirm.show(
validationPhoneResult.message,
() => {},
() => {}
);
eventHandler.setEventType(EVENT_TYPES.SLEEP);
return;
}
updateUserInfo({
id: SessionInfo.userId,
firstName: firstName,
lastName: lastName,
phoneNumber: phone,
phoneAreaCode: areaCode,
phoneCountryCode: "549",
sessionUser: SessionInfo.userId,
})
.then(() => {
alert.showSuccess("Perfil actualizado!");
})
.catch((error: ApiError) => {
alert.showError(error.format());
})
.finally(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
});
}
}, [eventHandler.eventType]);
return (
<>
<Location />
<Textbox
name="firstName"
placeholder="Nombre"
type="text"
value={firstName}
width="100%"
onChange={(e) => {
setFirstName(e.target.value);
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
}}
/>
<Textbox
name="lastName"
placeholder="Apellido"
type="text"
value={lastName}
width="100%"
onChange={(e) => {
setLastName(e.target.value);
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
}}
/>
<TextPhone
label="Teléfono"
areaCode={areaCode}
number={phone}
width="100%"
onChangeArea={(e) => {
setAreaCode(e.target.value);
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
}}
onChangePhone={(e) => {
setPhone(e.target.value);
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
}}
/>
</>
);
}
+131
View File
@@ -0,0 +1,131 @@
"use client";
import style from "./UserProfile.module.css";
import Avatar from "@components/Avatar/Avatar";
import { useSessionStore } from "@store/Sesion.Store";
import SectionContainer from "@components/SectionContainer/SectionContainer";
import { uploadAvatar } from "./UserAvatar.Service";
import { useAlert } from "@store/Alert.Store";
import { EVENT_TYPES, useEventHandlerStore } from "@store/EventHandler.Store";
import { ApiError } from "@models/Server.Error.model";
import SettingsIcon from "@mui/icons-material/Settings";
import IconButton from "@mui/material/IconButton";
import { Badge, BadgeProps } from "@mui/material";
import styled from "@emotion/styled";
import Secure from "@components/Secure/Secure";
import Content from "@core/app/components/Content/Content";
import { Suspense, useEffect, useState } from "react";
import { SystemNotificationsProvider } from "@core/app/components/SystemNotificationsProvider/SystemNotificationsProvider";
import SuspenseLoading from "@core/app/components/SuspendeLoading/SuspenseLoading";
import { useMenuStore } from "@core/Store/Menu.Store";
import Menu from "@core/app/components/Menu/Menu";
const StyledBadge = styled(Badge)<BadgeProps>(() => ({
"& .MuiBadge-badge": {
right: 5,
top: 13,
padding: "0 4px",
},
}));
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
const SessionInfo = useSessionStore();
const alert = useAlert();
const eventHandler = useEventHandlerStore();
const menu = useMenuStore();
const [userAvatar, setUserAvatar] = useState("");
const handleAvatarChange = (e: React.ChangeEvent<HTMLInputElement>) => {
e.preventDefault();
const file = e.target.files?.[0];
if (!file) {
return;
}
const formData = new FormData();
formData.append("sessionUser", SessionInfo.userId); // Añade el ID del usuario
formData.append("file", file);
uploadAvatar(formData)
.then((res) => {
if (res.success) {
alert.showSuccess("Avatar actualizado");
}
})
.catch((error: ApiError) => {
alert.showError(error.format());
})
.finally(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
});
};
useEffect(() => {
if (!SessionInfo.userId) {
return;
}
setUserAvatar(SessionInfo.avatar);
}, [SessionInfo.userId]);
return (
<>
<SystemNotificationsProvider />
<Content>
<Suspense fallback={<SuspenseLoading />}>
<Secure>
<SectionContainer style={{ top: "70px" }}>
<div className={style.userProfileHeader}>
<Avatar
name="userProfileAvatar"
src={userAvatar}
alt="Avatar"
readOnly={false}
onChange={handleAvatarChange}
/>
<div>
<h1>{SessionInfo.userFullName}</h1>
<span>{SessionInfo.userEmail}</span>
</div>
</div>
<div className={style.userProfileOptionsContainer}>{children}</div>
{menu.length() > 0 && (
<div style={{ position: "absolute", top: "-40px", right: "40px" }}>
<div className={style.buttonMenu}>
<StyledBadge
color="warning"
badgeContent="1"
variant="dot"
sx={{
top: "-10px",
}}
>
<IconButton
sx={{ color: "var(--white)" }}
id="button-menu-appointment"
aria-controls={menu.visible ? "tx-menu" : undefined}
aria-expanded={menu.visible ? "true" : undefined}
aria-haspopup="true"
onClick={() => {
menu.setAnchorEl(
document.getElementById(
"button-menu-appointment"
)!
);
menu.show();
}}
>
<SettingsIcon />
</IconButton>
</StyledBadge>
</div>
</div>
)}
<Menu />
</SectionContainer>
</Secure>
</Suspense>
</Content>
</>
);
}
@@ -0,0 +1,48 @@
import { ApiVoidResult } from "@core/Models/Api.VoidResult.type";
import {
DeleteNotificationsByDateParams,
DeleteSystemNotificationParams,
FindSystemNotificationsParams,
PaginateSystemNotificationsParams,
PaginateSystemNotificationsResults,
ToggleStateSystemNotificationsParams,
UnreadNotificationResults,
UpdateSystemNotificationParams,
} from "@core/Models/SystemNotifications.model";
import ApiRequest from "@services/Api.Service";
export const findSystemNotifications = async (
data: PaginateSystemNotificationsParams
): Promise<PaginateSystemNotificationsResults> => {
return ApiRequest.post<PaginateSystemNotificationsResults>("systemnotifications/paginate", data);
};
export const unreadSystemNotifications = async (
data: FindSystemNotificationsParams
): Promise<UnreadNotificationResults> => {
return ApiRequest.post<UnreadNotificationResults>("systemnotifications/unread", data);
};
export const deleteSystemNotification = async (
data: DeleteSystemNotificationParams
): Promise<ApiVoidResult> => {
return ApiRequest.post<ApiVoidResult>("systemnotifications/delete", data);
};
export const updateSystemNotification = async (
data: UpdateSystemNotificationParams
): Promise<ApiVoidResult> => {
return ApiRequest.post<ApiVoidResult>("systemnotifications/update", data);
};
export const setStateNotificationByUser = async (
data: ToggleStateSystemNotificationsParams
): Promise<ApiVoidResult> => {
return ApiRequest.post<ApiVoidResult>("systemnotifications/set-state", data);
};
export const deleteNotificationsByDate = async (
data: DeleteNotificationsByDateParams
): Promise<ApiVoidResult> => {
return ApiRequest.post<ApiVoidResult>("systemnotifications/delete-by-date", data);
};
@@ -0,0 +1,575 @@
"use client";
import { useState, useEffect, useRef } from "react";
import { useSessionStore } from "@store/Sesion.Store";
import { EVENT_TYPES, useEventHandlerStore } from "@store/EventHandler.Store";
import { useAlert } from "@store/Alert.Store";
import Location from "@components/Location/Location";
import { useLocationStore } from "@store/Location.Store";
import AccountCircleOutlinedIcon from "@mui/icons-material/AccountCircleOutlined";
import {
DeleteSystemNotificationParams,
ISystemNotification,
PaginateSystemNotificationsParams,
PaginateSystemNotificationsResults,
UpdateSystemNotificationParams,
NotificationType,
} from "@core/Models/SystemNotifications.model";
import API from "@services/Api.Service";
import { useRouter } from "next/navigation";
import {
deleteNotificationsByDate,
deleteSystemNotification,
findSystemNotifications,
setStateNotificationByUser,
updateSystemNotification,
} from "./Notifications.Service";
import DeleteIcon from "@mui/icons-material/DeleteOutlined";
import MarkReadIcon from "@mui/icons-material/MarkChatReadOutlined";
import MarkUnReadIcon from "@mui/icons-material/MarkUnreadChatAltOutlined";
import ReloadIcon from "@mui/icons-material/Autorenew";
import DoneAllIcon from "@mui/icons-material/DoneAll";
import NotificationsActiveIcon from "@mui/icons-material/NotificationsActive";
import InboxIcon from "@mui/icons-material/Inbox";
import IconButton from "@mui/material/IconButton";
import Pagination from "@mui/material/Pagination";
import { useConfirmStore } from "@core/Store/Confirm.Store";
import HeaderConfProvider from "@core/app/components/HeaderConfProvider/HeaderConfProvider";
import { useMenuStore } from "@core/Store/Menu.Store";
import dayjs from "dayjs";
import "dayjs/locale/es";
import { motion, AnimatePresence } from "motion/react";
export type SystemNotificationRow = ISystemNotification & { _id: string };
export default function EditProfile() {
const alert = useAlert();
const confirm = useConfirmStore();
const SessionInfo = useSessionStore();
const eventHandler = useEventHandlerStore();
const location = useLocationStore();
const menu = useMenuStore();
const router = useRouter();
const [filterType, setFilterType] = useState<string>("ALL");
const [pageSize] = useState<number>(
parseInt(
process.env.NEXT_PUBLIC_DATAGRID_PAGE_SIZE ? process.env.NEXT_PUBLIC_DATAGRID_PAGE_SIZE : "10"
)
);
const [notifications, setNotifications] = useState<PaginateSystemNotificationsResults>();
const notificationsRef = useRef(notifications);
const handleMarkRead = (notif: SystemNotificationRow) => {
const data: UpdateSystemNotificationParams = {
id: notif._id,
readed: !notif.readed,
sessionUser: SessionInfo.userId,
};
eventHandler.setEventType(EVENT_TYPES.LOADING);
const loadPage = notificationsRef.current?.page;
updateSystemNotification(data)
.then(() => {
loadNotifications(loadPage, pageSize);
})
.catch((err) => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
alert.showError(err.desc);
});
};
const handleDelete = (notif: SystemNotificationRow) => {
confirm.show(
"¿Está seguro de que desea eliminar esta notificación?",
() => {
eventHandler.setEventType(EVENT_TYPES.LOADING);
const data: DeleteSystemNotificationParams = {
id: notif._id,
sessionUser: SessionInfo.userId,
};
let loadPage = 1;
if (notificationsRef.current) {
loadPage = notificationsRef.current.page;
}
deleteSystemNotification(data)
.then(() => {
loadNotifications(loadPage, pageSize);
})
.catch((err) => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
alert.showError(err.desc);
});
},
() => {
// Do nothing
}
);
};
const setNotiState = (state: boolean) => {
eventHandler.setEventType(EVENT_TYPES.LOADING);
setStateNotificationByUser({
state: state,
sessionUser: SessionInfo.userId,
})
.then(() => {
loadNotifications(1, pageSize);
alert.showSuccess("Las notificaciones han sido actualizadas");
})
.catch((error) => {
alert.showError(error.format());
})
.finally(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
});
};
const handleNotificationClick = async (notif: SystemNotificationRow) => {
if (!notif.readed) {
handleMarkRead(notif);
}
if (!notif.type || !notif.code) return;
eventHandler.setEventType(EVENT_TYPES.LOADING);
try {
let companyId = notif.companyId;
switch (notif.type) {
case NotificationType.APPOINTMENT:
// Fetch appointment to get clientId and companyId
const aptRes = await API.post<any>("appointments/get-event", {
appointmentId: notif.code,
sessionUser: SessionInfo.userId,
});
if (aptRes && aptRes.clientId) {
companyId = companyId || aptRes.companyId;
if (companyId) {
router.push(`/admin/org/${companyId}/client/${aptRes.clientId}/appointments/${notif.code}`);
}
}
break;
case NotificationType.COMPANY:
router.push(`/admin/org/profile/${notif.code}`);
break;
case NotificationType.EMPLOYEE:
case NotificationType.SCHEDULE:
case NotificationType.SCHEDULE_EXCEPTION:
case NotificationType.SCHEDULE_RESTRICTION:
if (!companyId) {
const empRes = await API.post<any>("employees/get-by-id", {
id: notif.code,
sessionUser: SessionInfo.userId,
});
companyId = companyId || empRes?.companyId;
}
if (companyId) {
if (notif.type === NotificationType.EMPLOYEE || notif.type === NotificationType.SCHEDULE) {
router.push(`/admin/org/profile/${companyId}/collaborators/${notif.code}`);
} else if (notif.type === NotificationType.SCHEDULE_EXCEPTION) {
router.push(`/admin/org/profile/${companyId}/collaborators/${notif.code}/exceptions`);
} else {
router.push(`/admin/org/profile/${companyId}/collaborators/${notif.code}/restrictions`);
}
}
break;
case NotificationType.BILLING:
companyId = companyId || Object.keys(SessionInfo.organizationSubscriptions || {})[0];
if (companyId) {
router.push(`/admin/org/profile/${companyId}/dashboard`);
}
break;
case NotificationType.MESSAGE:
if (notif.conversationId) {
router.push(`/messages?conversation=${notif.conversationId}`);
}
break;
}
} catch (e: any) {
console.error("Error navigating from notification", e);
alert.showError(e.format ? e.format() : "Error al navegar: " + (e.message || "Desconocido"));
} finally {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
}
};
const handleDeleteByDate = (dateFrom: Date | undefined, dateTo: Date | undefined) => {
if (!SessionInfo.userId) {
return;
}
eventHandler.setEventType(EVENT_TYPES.LOADING);
deleteNotificationsByDate({
userId: SessionInfo.userId,
dateFrom: dateFrom,
dateTo: dateTo,
sessionUser: SessionInfo.userId,
})
.then(() => {
loadNotifications(1, pageSize);
alert.showSuccess("Las notificaciones han sido eliminadas");
})
.catch((error) => {
alert.showError(error.format());
})
.finally(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
});
};
const createMenu = () => {
menu.clear();
menu.add({
text: "Marcar todas como leidas",
onClick: () => {
setNotiState(true);
},
icon: <MarkReadIcon />,
});
menu.add({
text: "Marcar todas como no leidas",
onClick: () => {
setNotiState(false);
},
icon: <MarkUnReadIcon />,
});
menu.divider();
menu.add({
text: "Eliminar las notificaciones de hoy",
onClick: () => {
confirm.show(
"¿Está seguro que desea eliminar las notificaciones de hoy?",
() => {
const filterFrom = dayjs().startOf("day").toDate();
const filterTo = dayjs().endOf("day").toDate();
handleDeleteByDate(filterFrom, filterTo);
},
() => {}
);
},
icon: <DeleteIcon />,
});
menu.add({
text: "Eliminar las notificaciones de la semana",
onClick: () => {
confirm.show(
"¿Está seguro que desea eliminar las notificaciones de la semana?",
() => {
const filterFrom = dayjs().startOf("week").toDate();
const filterTo = dayjs().endOf("week").toDate();
handleDeleteByDate(filterFrom, filterTo);
},
() => {}
);
},
icon: <DeleteIcon />,
});
menu.add({
text: "Eliminar las notificaciones del mes",
onClick: () => {
confirm.show(
"¿Está seguro que desea eliminar las notificaciones del mes?",
() => {
const filterFrom = dayjs().startOf("month").toDate();
const filterTo = dayjs().endOf("month").toDate();
handleDeleteByDate(filterFrom, filterTo);
},
() => {}
);
},
icon: <DeleteIcon />,
});
menu.add({
text: "Eliminar las notificaciones del año",
onClick: () => {
confirm.show(
"¿Está seguro que desea eliminar las notificaciones del año?",
() => {
const filterFrom = dayjs().startOf("year").toDate();
const filterTo = dayjs().endOf("year").toDate();
handleDeleteByDate(filterFrom, filterTo);
},
() => {}
);
},
icon: <DeleteIcon />,
});
menu.add({
text: "Eliminar todas las notificaciones",
onClick: () => {
confirm.show(
"¿Está seguro que desea eliminar todas las notificaciones?",
() => {
handleDeleteByDate(undefined, undefined);
},
() => {}
);
},
icon: <DeleteIcon />,
});
};
useEffect(() => {
location.clear();
location.add({
url: "/user/profile",
title: "Perfil de usuario",
icon: <AccountCircleOutlinedIcon sx={{ fontSize: "32px", mr: "10px" }} />,
});
location.add({
url: "/user/profile/edit",
title: "Notificaciones",
icon: <AccountCircleOutlinedIcon sx={{ fontSize: "32px", mr: "10px" }} />,
});
createMenu();
return () => {
menu.clear();
};
}, []);
useEffect(() => {
if (!SessionInfo.userId) {
return;
}
loadNotifications(1, pageSize);
}, [SessionInfo.userId]);
useEffect(() => {
notificationsRef.current = notifications;
}, [notifications]);
const loadNotifications = (page: number = 1, pageSize: number = 30) => {
eventHandler.setEventType(EVENT_TYPES.LOADING);
const filterData: PaginateSystemNotificationsParams = {
userId: SessionInfo.userId,
sessionUser: SessionInfo.userId,
page: page,
limit: pageSize,
};
findSystemNotifications(filterData)
.then((res) => {
if (res.page > res.pages && res.page > 1) {
loadNotifications(res.page - 1, pageSize);
return;
}
setNotifications(res);
})
.finally(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
});
};
// Derived states for UI gamification
const allDataList = (notifications?.data || []) as SystemNotificationRow[];
// Apply type filter
const dataList = allDataList.filter(n => {
if (filterType === "ALL") return true;
if (filterType === "APPOINTMENT") return n.type === NotificationType.APPOINTMENT;
if (filterType === "COMPANY") return n.type === NotificationType.COMPANY || n.type === NotificationType.EMPLOYEE || n.type === NotificationType.SCHEDULE || n.type === NotificationType.SCHEDULE_EXCEPTION || n.type === NotificationType.SCHEDULE_RESTRICTION;
if (filterType === "BILLING") return n.type === NotificationType.BILLING;
if (filterType === "MESSAGE") return n.type === NotificationType.MESSAGE;
if (filterType === "SYSTEM") return n.type === NotificationType.SYSTEM || !n.type;
return true;
});
const unreadCount = allDataList.filter(n => !n.readed).length;
const isInboxZero = allDataList.length > 0 && unreadCount === 0;
return (
<>
<Location />
<HeaderConfProvider />
<div style={{ padding: "20px 40px", maxWidth: "900px", margin: "0 auto", paddingBottom: "100px" }}>
{/* Header and Controls */}
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "30px", borderBottom: "1px solid #eaeaea", paddingBottom: "20px" }}>
<div>
<h1 style={{ margin: 0, fontSize: "28px", color: "var(--black-light)", display: "flex", alignItems: "center", gap: "10px" }}>
<NotificationsActiveIcon sx={{ color: "var(--wine-dark)" }} />
Bandeja de Entrada
</h1>
<p style={{ margin: "5px 0 0 0", color: "#666", fontSize: "14px" }}>
{unreadCount > 0 ? `Tienes ${unreadCount} notificación(es) sin leer.` : "No tienes notificaciones pendientes."}
</p>
</div>
<div style={{ display: "flex", gap: "10px" }}>
{unreadCount > 0 && (
<button
onClick={() => setNotiState(true)}
style={{
backgroundColor: "var(--wine-lighterX2)",
color: "var(--wine-darkest)",
border: "none",
padding: "8px 16px",
borderRadius: "20px",
cursor: "pointer",
fontWeight: "bold",
display: "flex",
alignItems: "center",
gap: "5px",
transition: "all 0.2s"
}}
onMouseOver={e => e.currentTarget.style.backgroundColor = "var(--wine-light)"}
onMouseOut={e => e.currentTarget.style.backgroundColor = "var(--wine-lighterX2)"}
>
<DoneAllIcon fontSize="small" /> Marcar leídas
</button>
)}
<IconButton
onClick={() => loadNotifications(notifications?.page, pageSize)}
color="secondary"
style={{ backgroundColor: "#f5f5f5" }}
>
<ReloadIcon sx={{ color: "var(--black)" }} />
</IconButton>
</div>
</div>
{/* Filters */}
{allDataList.length > 0 && (
<div style={{ display: "flex", gap: "10px", marginBottom: "25px", flexWrap: "wrap" }}>
{[
{ id: "ALL", label: "Todas" },
{ id: "APPOINTMENT", label: "Turnos" },
{ id: "MESSAGE", label: "Mensajes" },
{ id: "COMPANY", label: "Negocio y Equipo" },
{ id: "BILLING", label: "Suscripciones y Pagos" },
{ id: "SYSTEM", label: "Sistema" }
].map(f => (
<button
key={f.id}
onClick={() => setFilterType(f.id)}
style={{
padding: "8px 16px",
borderRadius: "20px",
border: "1px solid",
borderColor: filterType === f.id ? "var(--wine-dark)" : "#eaeaea",
backgroundColor: filterType === f.id ? "var(--wine-dark)" : "#fff",
color: filterType === f.id ? "#fff" : "#666",
cursor: "pointer",
fontWeight: filterType === f.id ? "bold" : "normal",
transition: "all 0.2s"
}}
>
{f.label}
</button>
))}
</div>
)}
{/* Inbox Zero State */}
{isInboxZero && dataList.length > 0 && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
style={{ textAlign: "center", padding: "40px 20px", backgroundColor: "#f9fafc", borderRadius: "15px", marginBottom: "30px" }}
>
<InboxIcon sx={{ fontSize: "64px", color: "var(--wine-light)", marginBottom: "15px" }} />
<h2 style={{ margin: 0, color: "var(--wine-darkest)" }}>¡Estás al día! 🌟</h2>
<p style={{ color: "#666" }}>Has gestionado todas tus notificaciones recientes.</p>
</motion.div>
)}
{dataList.length === 0 && (
<div style={{ textAlign: "center", padding: "80px 20px" }}>
<InboxIcon sx={{ fontSize: "64px", color: "#ccc", marginBottom: "15px" }} />
<h2 style={{ margin: 0, color: "#999" }}>Bandeja Vacía</h2>
<p style={{ color: "#aaa" }}>Aún no has recibido ninguna notificación.</p>
</div>
)}
{/* Notifications List */}
<div style={{ display: "flex", flexDirection: "column", gap: "12px" }}>
<AnimatePresence>
{dataList.map((notif) => (
<motion.div
key={notif._id}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, scale: 0.95 }}
transition={{ duration: 0.2 }}
style={{
backgroundColor: notif.readed ? "#ffffff" : "var(--wine-lighterX2)",
border: notif.readed ? "1px solid #eaeaea" : "1px solid var(--wine-light)",
borderRadius: "12px",
padding: "16px 20px",
display: "flex",
alignItems: "flex-start",
justifyContent: "space-between",
boxShadow: notif.readed ? "none" : "0 4px 12px rgba(127, 42, 255, 0.1)",
transition: "all 0.2s ease",
cursor: notif.type && notif.code ? "pointer" : "default"
}}
onClick={() => handleNotificationClick(notif)}
onMouseOver={(e) => { e.currentTarget.style.transform = "translateY(-2px)"; e.currentTarget.style.boxShadow = "0 6px 16px rgba(0,0,0,0.08)"; }}
onMouseOut={(e) => { e.currentTarget.style.transform = "none"; e.currentTarget.style.boxShadow = notif.readed ? "none" : "0 4px 12px rgba(127, 42, 255, 0.1)"; }}
>
<div style={{ display: "flex", gap: "15px", alignItems: "flex-start", flex: 1 }}>
<div style={{ marginTop: "4px" }}>
{notif.readed ?
<MarkReadIcon sx={{ color: "#ccc" }} /> :
<MarkUnReadIcon sx={{ color: "var(--wine-dark)" }} />
}
</div>
<div style={{ flex: 1 }}>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "4px" }}>
<h3 style={{ margin: 0, fontSize: "16px", fontWeight: notif.readed ? "normal" : "bold", color: notif.readed ? "#444" : "var(--wine-darkest)" }}>
{notif.subject}
</h3>
</div>
<p style={{ margin: 0, fontSize: "14px", color: notif.readed ? "#777" : "#333", lineHeight: "1.5" }}>
{notif.message}
</p>
</div>
</div>
<div style={{ display: "flex", gap: "5px", opacity: 0.8 }} onClick={(e) => e.stopPropagation()}>
<IconButton size="small" onClick={() => handleMarkRead(notif)} title={notif.readed ? "Marcar como no leída" : "Marcar como leída"}>
{notif.readed ? <MarkUnReadIcon fontSize="small" /> : <MarkReadIcon fontSize="small" color="primary" />}
</IconButton>
<IconButton size="small" onClick={() => handleDelete(notif)} title="Eliminar">
<DeleteIcon fontSize="small" sx={{ color: "#d32f2f" }} />
</IconButton>
</div>
</motion.div>
))}
</AnimatePresence>
</div>
{/* Pagination */}
{notifications && notifications.pages > 1 && (
<div style={{ display: "flex", justifyContent: "center", marginTop: "40px" }}>
<Pagination
count={notifications.pages}
page={notifications.page}
onChange={(e, value) => loadNotifications(value, pageSize)}
color="secondary"
/>
</div>
)}
</div>
</>
);
}
+62
View File
@@ -0,0 +1,62 @@
"use client";
import ButtonOption from "@components/ButtonOption/ButtonOption";
import { useRouter } from "next/navigation";
import Location from "@components/Location/Location";
import { useLocationStore } from "@store/Location.Store";
import { useEffect } from "react";
import AccountCircleOutlinedIcon from "@mui/icons-material/AccountCircleOutlined";
import { useBottomToolbarStore } from "@core/Store/BottomToolbar.Store";
export default function Profile() {
const router = useRouter();
const location = useLocationStore();
const bottomToolbar = useBottomToolbarStore();
const goTo = (path: string) => {
router.push(path);
};
useEffect(() => {
location.clear();
location.add({
url: "/user/profile",
title: "Perfil de usuario",
icon: <AccountCircleOutlinedIcon sx={{ fontSize: "32px", mr: "10px" }} />,
});
bottomToolbar.set(<></>);
}, []);
return (
<>
<Location />
<div style={{ lineHeight: "20px" }}>&nbsp;</div>
<ButtonOption
text="Editar perfil"
icon="/profile-icon.svg"
onClick={() => goTo("/user/profile/edit")}
/>
<ButtonOption
text="Seguridad"
icon="/security-icon.svg"
onClick={() => goTo("/user/profile/security")}
/>
<ButtonOption
text="Editar domicilio"
icon="/location-icon.svg"
onClick={() => goTo("/user/profile/address")}
/>
<ButtonOption
text="Notificaciones"
icon="/notifications-icon.svg"
onClick={() => goTo("/user/profile/notifications")}
/>
<ButtonOption
text="Avanzado"
icon="/services-icon.svg"
onClick={() => goTo("/user/profile/advanced")}
/>
</>
);
}
@@ -0,0 +1,9 @@
import { ChangePasswordParams } from "@models/User.model";
import { ApiVoidResult } from "@models/Api.VoidResult.type";
import ApiRequest from "@services/Api.Service";
export const changePassword = async (data: ChangePasswordParams): Promise<ApiVoidResult> => {
return ApiRequest.post<ApiVoidResult>("users/change-password", {
...data,
});
};
@@ -0,0 +1,29 @@
"use client";
import { EVENT_TYPES, useEventHandlerStore } from "@store/EventHandler.Store";
import { useConfirmStore } from "@store/Confirm.Store";
import Saveable from "@components/Saveable/Saveable";
export default function UserEditLayout({ children }: { children: React.ReactNode }) {
const confirm = useConfirmStore();
const eventHandler = useEventHandlerStore();
return (
<>
<Saveable
onClick={() => {
confirm.show(
"Estas seguro que queres cambiar la contraseña? Esta acción no se puede deshacer. Será necesario volver a iniciar sesión.",
() => {
eventHandler.setEventType(EVENT_TYPES.SAVE);
},
() => {
eventHandler.setEventType(EVENT_TYPES.CLEAR);
}
);
}}
>
{children}
</Saveable>
</>
);
}
@@ -0,0 +1,145 @@
"use client";
import { useRouter } from "next/navigation";
import { useState, useEffect } from "react";
import Textbox from "@components/Textbox/Textbox";
import { useSessionStore, useSessionTokenStore } from "@store/Sesion.Store";
import { EVENT_TYPES, useEventHandlerStore } from "@store/EventHandler.Store";
import { changePassword } from "./UserSecurity.Service";
import { useAlert } from "@store/Alert.Store";
import { ApiError } from "@models/Server.Error.model";
import * as Yup from "yup";
import Location from "@components/Location/Location";
import { useLocationStore } from "@store/Location.Store";
import AccountCircleOutlinedIcon from "@mui/icons-material/AccountCircleOutlined";
export default function EditProfile() {
const router = useRouter();
const alert = useAlert();
const SessionInfo = useSessionStore();
const SessionToken = useSessionTokenStore();
const eventHandler = useEventHandlerStore();
const [oldPassword, setOldPassword] = useState("");
const [newPassword1, setNewPassword1] = useState("");
const [newPassword2, setNewPassword2] = useState("");
const location = useLocationStore();
const logOut = () => {
SessionInfo.clear();
SessionToken.setToken("");
goTo("/");
};
const goTo = (path: string) => {
router.push(path);
};
useEffect(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
}, []);
useEffect(() => {
location.clear();
location.add({
url: "/user/profile",
title: "Perfil de usuario",
icon: <AccountCircleOutlinedIcon sx={{ fontSize: "32px", mr: "10px" }} />,
});
location.add({
url: "/user/profile/security",
title: "Cambiar contraseña",
icon: <AccountCircleOutlinedIcon sx={{ fontSize: "32px", mr: "10px" }} />,
});
}, []);
useEffect(() => {
if (eventHandler.eventType === EVENT_TYPES.SAVE) {
const schema = Yup.object().shape({
oldPassword: Yup.string().required("Debe introducir la contraseña anterior."),
newPassword1: Yup.string()
.min(8, "La nueva contraseña debe tener al menos 8 caracteres.")
.required("Debe introducir una nueva contraseña."),
newPassword2: Yup.string()
.min(
8,
"La verificación de la nueva contraseña debe tener al menos 8 caracteres."
)
.required("Debe introducir la verificación de la nueva contraseña."),
});
schema
.validate({ oldPassword, newPassword1, newPassword2 }, { abortEarly: true })
.then(() => {
if (newPassword1 !== newPassword2) {
alert.showError("Las contraseñas no coinciden");
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
return;
}
changePassword({
oldPassword: oldPassword,
newPassword: newPassword1,
sessionUser: SessionInfo.userId,
})
.then(() => {
alert.showSuccess(
"Contraseña actualizada!. Cerrando sesión en 3 segundos."
);
setTimeout(() => {
logOut();
}, 3000);
})
.catch((error: ApiError) => {
alert.showError(error.format());
})
.finally(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
});
})
.catch((error: Yup.ValidationError) => {
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
alert.showError(error.message);
});
}
}, [eventHandler.eventType]);
return (
<>
<Location />
<Textbox
name="oldPassword"
placeholder="Contraseña anterior"
type="password"
value={oldPassword}
width="100%"
onChange={(e) => {
setOldPassword(e.target.value);
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
}}
/>
<Textbox
name="newPassword1"
placeholder="Contraaseña nueva"
type="password"
value={newPassword1}
width="100%"
onChange={(e) => {
setNewPassword1(e.target.value);
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
}}
/>
<Textbox
name="newPassword2"
placeholder="Repetir contraaseña nueva"
type="password"
value={newPassword2}
width="100%"
onChange={(e) => {
setNewPassword2(e.target.value);
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
}}
/>
</>
);
}