first commit
This commit is contained in:
@@ -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;
|
||||
}
|
||||
@@ -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> </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> </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> </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" }}> </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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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" }}> </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);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user