first commit
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
import ApiRequest from "@services/Api.Service";
|
||||
import * as Yup from "yup";
|
||||
import { ApiError } from "@core/Models/Server.Error.model";
|
||||
import {
|
||||
AppointmentAdminByClientView,
|
||||
DeleteAppointmentParams,
|
||||
FindAppointmentsByUserParams,
|
||||
FindAppointmentsByCollaboratorParams,
|
||||
FindAppointmentsByUserPaginatedParams,
|
||||
FindAppointmentsByCollaboratorPaginatedParams,
|
||||
PaginatedAppointmentEventByClientResult,
|
||||
} from "@core/Models/Appointments.model";
|
||||
import { UserAppointmentView, UserAppointmentViewParams } from "@core/Models/Views.model";
|
||||
import { ApiVoidResult } from "@core/Models/Api.VoidResult.type";
|
||||
|
||||
const schemaCancellAppointment = Yup.object().shape({
|
||||
id: Yup.string().required("El turno no ha sido especificado."),
|
||||
validation: Yup.boolean().oneOf([true], "No es posible cancelar la reserva."),
|
||||
sessionUser: Yup.string().required("Para cancelar un turno debe iniciar sesión."),
|
||||
});
|
||||
|
||||
export const deleteAppointment = async (data: DeleteAppointmentParams): Promise<ApiVoidResult> => {
|
||||
return new Promise<ApiVoidResult>((resolve, reject) => {
|
||||
schemaCancellAppointment
|
||||
.validate(data, { abortEarly: true })
|
||||
.then(() => {
|
||||
resolve(ApiRequest.post<ApiVoidResult>("appointments/delete", data));
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(new ApiError(500, error.message));
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const schemaFindAppointmentByUser = Yup.object().shape({
|
||||
sessionUser: Yup.string().required("Para crear un turno debe iniciar sesión."),
|
||||
});
|
||||
|
||||
export const findAppointmentByUser = async (
|
||||
data: FindAppointmentsByUserParams
|
||||
): Promise<AppointmentAdminByClientView> => {
|
||||
return new Promise<AppointmentAdminByClientView>((resolve, reject) => {
|
||||
schemaFindAppointmentByUser
|
||||
.validate(data, { abortEarly: true })
|
||||
.then(() => {
|
||||
resolve(
|
||||
ApiRequest.post<AppointmentAdminByClientView>("appointments/find-by-user", data)
|
||||
);
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(new ApiError(500, error.message));
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const findAppointmentByCollaborator = async (
|
||||
data: FindAppointmentsByCollaboratorParams
|
||||
): Promise<AppointmentAdminByClientView> => {
|
||||
return new Promise<AppointmentAdminByClientView>((resolve, reject) => {
|
||||
schemaFindAppointmentByUser
|
||||
.validate(data, { abortEarly: true })
|
||||
.then(() => {
|
||||
resolve(
|
||||
ApiRequest.post<AppointmentAdminByClientView>("appointments/find-by-collaborator", data)
|
||||
);
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(new ApiError(500, error.message));
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const schemaFindAppointmentByUserPaginated = Yup.object().shape({
|
||||
sessionUser: Yup.string().required("Para crear un turno debe iniciar sesión."),
|
||||
page: Yup.number().required(),
|
||||
limit: Yup.number().required(),
|
||||
});
|
||||
|
||||
export const findAppointmentByUserPaginated = async (
|
||||
data: FindAppointmentsByUserPaginatedParams
|
||||
): Promise<PaginatedAppointmentEventByClientResult> => {
|
||||
return new Promise<PaginatedAppointmentEventByClientResult>((resolve, reject) => {
|
||||
schemaFindAppointmentByUserPaginated
|
||||
.validate(data, { abortEarly: true })
|
||||
.then(() => {
|
||||
resolve(ApiRequest.post<PaginatedAppointmentEventByClientResult>("appointments/find-by-user-paginated", data));
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(new ApiError(500, error.message));
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const schemaFindAppointmentByCollaboratorPaginated = Yup.object().shape({
|
||||
sessionUser: Yup.string().required("Para crear un turno debe iniciar sesión."),
|
||||
page: Yup.number().required(),
|
||||
limit: Yup.number().required(),
|
||||
});
|
||||
|
||||
export const findAppointmentByCollaboratorPaginated = async (
|
||||
data: FindAppointmentsByCollaboratorPaginatedParams
|
||||
): Promise<PaginatedAppointmentEventByClientResult> => {
|
||||
return new Promise<PaginatedAppointmentEventByClientResult>((resolve, reject) => {
|
||||
schemaFindAppointmentByCollaboratorPaginated
|
||||
.validate(data, { abortEarly: true })
|
||||
.then(() => {
|
||||
resolve(ApiRequest.post<PaginatedAppointmentEventByClientResult>("appointments/find-by-collaborator-paginated", data));
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(new ApiError(500, error.message));
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const schemaViewAppointment = Yup.object().shape({
|
||||
appointmentId: Yup.string().required("No se ha proporcionado ningun turno."),
|
||||
sessionUser: Yup.string().required("Para visuzlizar el turno debe iniciar sesión."),
|
||||
});
|
||||
|
||||
export const viewAppointment = async (
|
||||
data: UserAppointmentViewParams
|
||||
): Promise<UserAppointmentView> => {
|
||||
return new Promise<UserAppointmentView>((resolve, reject) => {
|
||||
schemaViewAppointment
|
||||
.validate(data, { abortEarly: true })
|
||||
.then(() => {
|
||||
resolve(ApiRequest.post<UserAppointmentView>("views/appointment", data));
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(new ApiError(500, error.message));
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,154 @@
|
||||
.organizationContent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 30px;
|
||||
margin: 30px;
|
||||
}
|
||||
|
||||
@media (min-width: 801px) {
|
||||
.organizationContent {
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
.organizationServices {
|
||||
flex: 1.5;
|
||||
background: #ffffff;
|
||||
border-radius: 16px;
|
||||
padding: 30px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.04);
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.organizationServices h1 {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.introText {
|
||||
font-size: 15px;
|
||||
color: #64748b;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.infoGrid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
@media (min-width: 600px) {
|
||||
.infoGrid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.infoCard {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 15px;
|
||||
background: #f8fafc;
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.infoCard:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.infoIconWrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background: #ffffff;
|
||||
border-radius: 50%;
|
||||
color: var(--primary);
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
.infoContent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.infoLabel {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #94a3b8;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.infoValue {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
.infoSubValue {
|
||||
font-size: 13px;
|
||||
color: #64748b;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.organizationMap {
|
||||
flex: 1;
|
||||
background: #ffffff;
|
||||
border-radius: 16px;
|
||||
padding: 30px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.04);
|
||||
border: 1px solid #e2e8f0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.organizationMap h2 {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.mapWrapper {
|
||||
width: 100%;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
/* Extra specific border/radius for child maps if needed */
|
||||
}
|
||||
|
||||
.contactInfo {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.contactRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.contactRow svg {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.actionButtons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
"use client";
|
||||
import AnimatedContainer from "@components/AnimatedConainer/AnimatedContainer";
|
||||
import WhatsAppIcon from "@mui/icons-material/WhatsApp";
|
||||
import LocationOnIcon from "@mui/icons-material/LocationOn";
|
||||
import Button from "@core/app/components/Button/Button";
|
||||
import LocationMap from "@core/app/components/LocationMap/LocationMap";
|
||||
import EventIcon from "@mui/icons-material/Event";
|
||||
import LocalOfferIcon from "@mui/icons-material/LocalOffer";
|
||||
import PersonIcon from "@mui/icons-material/Person";
|
||||
import AttachMoneyIcon from "@mui/icons-material/AttachMoney";
|
||||
import { PublicOrganizationView } from "@core/Models/Company.model";
|
||||
import OrganizationHeader from "@core/app/components/Home/OrganizationHeader/OrganizationHeader";
|
||||
import { UserAppointmentView, UserAppointmentViewParams } from "@core/Models/Views.model";
|
||||
import { useEffect, useState } from "react";
|
||||
import { deleteAppointment, viewAppointment } from "../Appointments.Service";
|
||||
import { useAlert } from "@core/Store/Alert.Store";
|
||||
import { useParams } from "next/navigation";
|
||||
import { EVENT_TYPES, useEventHandlerStore } from "@core/Store/EventHandler.Store";
|
||||
import { useSessionStore } from "@core/Store/Sesion.Store";
|
||||
import style from "./page.module.css";
|
||||
import dayjs from "dayjs";
|
||||
import "dayjs/locale/es";
|
||||
import { formatPrice } from "@core/app/helpers/Numbers";
|
||||
import { DeleteAppointmentParams } from "@core/Models/Appointments.model";
|
||||
import { useConfirmStore } from "@core/Store/Confirm.Store";
|
||||
import { useNavigation } from "@core/app/hooks/goto";
|
||||
import { ScrollToTop } from "@core/app/components/ScrollTop";
|
||||
|
||||
dayjs.locale("es");
|
||||
|
||||
type UrlData = {
|
||||
aid: string;
|
||||
};
|
||||
|
||||
type PositionType = [number, number];
|
||||
|
||||
export default function OrganizationPublicProfile() {
|
||||
const data = useParams<UrlData>();
|
||||
const aid = data.aid;
|
||||
const [appointmentInfo, setAppointmentInfo] = useState<UserAppointmentView | null>(null);
|
||||
const [orgData, setOrgData] = useState<PublicOrganizationView | null>(null);
|
||||
const [position, setPosition] = useState<PositionType>([0, 0]);
|
||||
|
||||
const alert = useAlert();
|
||||
const confirm = useConfirmStore();
|
||||
const eventHandler = useEventHandlerStore();
|
||||
const SessionInfo = useSessionStore();
|
||||
const { goTo } = useNavigation();
|
||||
|
||||
const loadAppointmentData = async () => {
|
||||
const data: UserAppointmentViewParams = {
|
||||
appointmentId: aid,
|
||||
sessionUser: SessionInfo.userId,
|
||||
};
|
||||
|
||||
eventHandler.setEventType(EVENT_TYPES.LOADING);
|
||||
|
||||
viewAppointment(data)
|
||||
.then((response) => {
|
||||
setAppointmentInfo(response);
|
||||
setOrgData(response.organization);
|
||||
setPosition([response.organization.latitude, response.organization.longitude]);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
alert.showError(error.format());
|
||||
})
|
||||
.finally(() => {
|
||||
eventHandler.setEventType(EVENT_TYPES.SLEEP);
|
||||
});
|
||||
};
|
||||
|
||||
const handleCancellAppointment = () => {
|
||||
confirm.show(
|
||||
"¿Está seguro que desea cancelar el turno?",
|
||||
() => {
|
||||
const data: DeleteAppointmentParams = {
|
||||
id: aid,
|
||||
validation: true,
|
||||
sessionUser: SessionInfo.userId,
|
||||
};
|
||||
eventHandler.setEventType(EVENT_TYPES.LOADING);
|
||||
deleteAppointment(data)
|
||||
.then(() => {
|
||||
alert.showSuccess("Turno cancelado correctamente.");
|
||||
setTimeout(() => {
|
||||
goTo("/landing/my-appointments");
|
||||
}, 2000);
|
||||
})
|
||||
.catch((error) => {
|
||||
alert.showError(error.format());
|
||||
})
|
||||
.finally(() => {
|
||||
eventHandler.setEventType(EVENT_TYPES.SLEEP);
|
||||
});
|
||||
},
|
||||
() => {}
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!SessionInfo.userId) {
|
||||
eventHandler.setEventType(EVENT_TYPES.SLEEP);
|
||||
return;
|
||||
}
|
||||
loadAppointmentData();
|
||||
}, [SessionInfo.userId]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{appointmentInfo && orgData && (
|
||||
<AnimatedContainer
|
||||
color={orgData.headerColor}
|
||||
minSize={150}
|
||||
maxSize={500}
|
||||
items={10}
|
||||
style={{
|
||||
background: `linear-gradient(180deg, ${orgData.headerColor} 0%, var(--white) 50%)`,
|
||||
}}
|
||||
>
|
||||
<ScrollToTop />
|
||||
<div className="homeCentered">
|
||||
<OrganizationHeader organization={orgData} />
|
||||
<div className={style.organizationContent}>
|
||||
<div className={style.organizationServices}>
|
||||
<h1>Detalles de tu Reserva</h1>
|
||||
<p className={style.introText}>
|
||||
Bienvenido a los detalles de tu turno. Aquí encontrarás toda la
|
||||
información sobre tu reserva, así como opciones para cancelarla
|
||||
o ponerte en contacto directo con nosotros.
|
||||
</p>
|
||||
|
||||
<div className={style.infoGrid}>
|
||||
<div className={style.infoCard}>
|
||||
<div className={style.infoIconWrapper}>
|
||||
<EventIcon />
|
||||
</div>
|
||||
<div className={style.infoContent}>
|
||||
<span className={style.infoLabel}>Fecha y Hora</span>
|
||||
<span className={style.infoValue}>
|
||||
{dayjs(appointmentInfo.appointment.appointmentDate)
|
||||
.format("D [de] MMMM [de] YYYY - HH:mm [hs]")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={style.infoCard}>
|
||||
<div className={style.infoIconWrapper}>
|
||||
<LocalOfferIcon />
|
||||
</div>
|
||||
<div className={style.infoContent}>
|
||||
<span className={style.infoLabel}>Servicio</span>
|
||||
<span className={style.infoValue}>
|
||||
{appointmentInfo.appointment.serviceName}
|
||||
</span>
|
||||
<span className={style.infoSubValue}>
|
||||
{appointmentInfo.appointment.serviceDescription}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={style.infoCard}>
|
||||
<div className={style.infoIconWrapper}>
|
||||
<PersonIcon />
|
||||
</div>
|
||||
<div className={style.infoContent}>
|
||||
<span className={style.infoLabel}>Profesional</span>
|
||||
<span className={style.infoValue}>
|
||||
{appointmentInfo.appointment.collaboratorName}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={style.infoCard}>
|
||||
<div className={style.infoIconWrapper}>
|
||||
<AttachMoneyIcon />
|
||||
</div>
|
||||
<div className={style.infoContent}>
|
||||
<span className={style.infoLabel}>Precio</span>
|
||||
<span className={style.infoValue}>
|
||||
{formatPrice(appointmentInfo.appointment.price)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={style.organizationMap}>
|
||||
<h2>Información Útil</h2>
|
||||
<div className={style.mapWrapper}>
|
||||
<LocationMap position={position} orgData={orgData} />
|
||||
</div>
|
||||
<div className={style.contactInfo}>
|
||||
<div className={style.contactRow}>
|
||||
<LocationOnIcon />
|
||||
<span>{orgData.address}</span>
|
||||
</div>
|
||||
<div className={style.contactRow}>
|
||||
<WhatsAppIcon />
|
||||
<span>{orgData.phone}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className={style.actionButtons}>
|
||||
<Button
|
||||
color="primary"
|
||||
text="Cancelar turno"
|
||||
width="100%"
|
||||
onClick={handleCancellAppointment}
|
||||
/>
|
||||
<Button
|
||||
color="secondary"
|
||||
text="Enviar mensaje"
|
||||
width="100%"
|
||||
onClick={() => {
|
||||
goTo(`/landing/org/${orgData.id}/send-message`);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AnimatedContainer>
|
||||
)}
|
||||
{appointmentInfo === null && <h1>Cargando...</h1>}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,399 @@
|
||||
"use client";
|
||||
import AnimatedContainer from "@components/AnimatedConainer/AnimatedContainer";
|
||||
import { useNavigation } from "@core/app/hooks/goto";
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
findAppointmentByUserPaginated,
|
||||
findAppointmentByCollaboratorPaginated,
|
||||
} from "./Appointments.Service";
|
||||
import { EVENT_TYPES, useEventHandlerStore } from "@core/Store/EventHandler.Store";
|
||||
import { useAlert } from "@core/Store/Alert.Store";
|
||||
import { AppointmentEventByClient } from "@core/Models/Appointments.model";
|
||||
import { useSessionStore } from "@core/Store/Sesion.Store";
|
||||
import dayjs from "dayjs";
|
||||
import "dayjs/locale/es";
|
||||
import ExploreCategories from "@core/app/components/ExploreCategories/ExploreCategories";
|
||||
import useContainerHeight from "@core/app/hooks/ContainerHeight";
|
||||
import { ScrollToTop } from "@core/app/components/ScrollTop";
|
||||
import PendingRepeatsCard from "@core/app/components/PendingRepeatsCard/PendingRepeatsCard";
|
||||
import HeaderConfProvider from "@core/app/components/HeaderConfProvider/HeaderConfProvider";
|
||||
import { Button, Tabs, Tab, Box } from "@mui/material";
|
||||
import { ThemeProvider } from "@emotion/react";
|
||||
import turnosXpressTheme from "@core/app/theme/turnosXpress";
|
||||
|
||||
dayjs.locale("es");
|
||||
|
||||
interface TabPanelProps {
|
||||
children?: React.ReactNode;
|
||||
index: number;
|
||||
value: number;
|
||||
}
|
||||
|
||||
function CustomTabPanel(props: TabPanelProps) {
|
||||
const { children, value, index, ...other } = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
role="tabpanel"
|
||||
hidden={value !== index}
|
||||
id={`appointments-tabpanel-${index}`}
|
||||
aria-labelledby={`appointments-tab-${index}`}
|
||||
style={{ width: "100%" }}
|
||||
{...other}
|
||||
>
|
||||
{value === index && <Box sx={{ p: 3, width: "100%" }}>{children}</Box>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function a11yProps(index: number) {
|
||||
return {
|
||||
id: `appointments-tab-${index}`,
|
||||
"aria-controls": `appointments-tabpanel-${index}`,
|
||||
};
|
||||
}
|
||||
|
||||
const AppointmentList = ({ appointments, containerHeight, emptyMessage, goTo, isProfessionalView }: { appointments: any, containerHeight: any, emptyMessage: string, goTo: any, isProfessionalView: boolean }) => {
|
||||
if (appointments.length === 0) {
|
||||
return <ExploreCategories containerHeight={containerHeight} message={emptyMessage} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "flex-start",
|
||||
alignItems: "flex-start",
|
||||
marginTop: "20px",
|
||||
width: "100%",
|
||||
gap: "20px",
|
||||
marginBottom: "40px",
|
||||
minHeight: "50vh",
|
||||
}}
|
||||
>
|
||||
{appointments.map((item: AppointmentEventByClient, index: number) => (
|
||||
<div
|
||||
key={index}
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
justifyContent: "flex-start",
|
||||
alignItems: "flex-start",
|
||||
width: "100%",
|
||||
gap: "20px",
|
||||
borderBottom: "solid 1px var(--gray-light)",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: "120px",
|
||||
minWidth: "120px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
borderRight: "solid 1px var(--gray-light)",
|
||||
color: "var(--black)",
|
||||
marginBottom: "20px",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontWeight: "bold",
|
||||
fontSize: "60px",
|
||||
color: "var(--wine-red)",
|
||||
}}
|
||||
>
|
||||
{dayjs(item.appointmentDate).format("D")}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontWeight: "bold",
|
||||
fontSize: "20px",
|
||||
textTransform: "uppercase",
|
||||
color: "var(--wine-red)",
|
||||
}}
|
||||
>
|
||||
{dayjs(item.appointmentDate).format("MMMM").substring(0, 3)}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
paddingTop: "15px",
|
||||
color: "var(--black)",
|
||||
cursor: "pointer",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<h1 style={{ fontSize: "24px" }}>{item.serviceName}</h1>
|
||||
<p>{item.serviceDescription}</p>
|
||||
<p
|
||||
style={{
|
||||
color: "var(--gray-dark)",
|
||||
fontSize: "14px",
|
||||
marginTop: "4px",
|
||||
marginBottom: "4px",
|
||||
}}
|
||||
>
|
||||
{isProfessionalView ? (
|
||||
<>
|
||||
Cliente:{" "}
|
||||
<strong style={{ color: "var(--black)" }}>{item.clientName}</strong>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Profesional:{" "}
|
||||
<strong style={{ color: "var(--black)" }}>{item.collaboratorName}</strong>
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
<p style={{ color: "var(--gray-dark)", fontWeight: "bold" }}>
|
||||
{dayjs(item.appointmentDate).format("HH:mm")} hs.
|
||||
</p>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "flex-end",
|
||||
marginBottom: "20px",
|
||||
}}
|
||||
>
|
||||
<ThemeProvider theme={turnosXpressTheme}>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
if (isProfessionalView) {
|
||||
goTo(
|
||||
`/admin/org/${item.companyId}/client/${item.clientId}/appointments/${item.id}`,
|
||||
);
|
||||
} else {
|
||||
goTo(`/landing/my-appointments/${item.id}`);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Ver turno
|
||||
</Button>
|
||||
</ThemeProvider>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default function MyAppointmentsPage() {
|
||||
const [clientAppointments, setClientAppointments] = useState<AppointmentEventByClient[]>([]);
|
||||
const [collaboratorAppointments, setCollaboratorAppointments] = useState<AppointmentEventByClient[]>([]);
|
||||
const [clientPage, setClientPage] = useState(1);
|
||||
const [clientHasMore, setClientHasMore] = useState(false);
|
||||
const [collaboratorPage, setCollaboratorPage] = useState(1);
|
||||
const [collaboratorHasMore, setCollaboratorHasMore] = useState(false);
|
||||
const [tabValue, setTabValue] = useState(0);
|
||||
const containerHeight = useContainerHeight();
|
||||
const eventHandler = useEventHandlerStore();
|
||||
const SessionInfo = useSessionStore();
|
||||
const alert = useAlert();
|
||||
const { goTo } = useNavigation();
|
||||
|
||||
const handleTabChange = (event: React.SyntheticEvent, newValue: number) => {
|
||||
setTabValue(newValue);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!SessionInfo.userId) {
|
||||
eventHandler.setEventType(EVENT_TYPES.SLEEP);
|
||||
return;
|
||||
}
|
||||
eventHandler.setEventType(EVENT_TYPES.LOADING);
|
||||
|
||||
Promise.all([
|
||||
findAppointmentByUserPaginated({ sessionUser: SessionInfo.userId, page: 1, limit: 10 }),
|
||||
findAppointmentByCollaboratorPaginated({ sessionUser: SessionInfo.userId, page: 1, limit: 10 }),
|
||||
])
|
||||
.then(([resUser, resCollaborator]) => {
|
||||
setClientAppointments(resUser.data);
|
||||
setClientHasMore(resUser.page < resUser.pages);
|
||||
|
||||
setCollaboratorAppointments(resCollaborator.data);
|
||||
setCollaboratorHasMore(resCollaborator.page < resCollaborator.pages);
|
||||
|
||||
if (resUser.data.length === 0 && resCollaborator.data.length > 0) {
|
||||
setTabValue(1);
|
||||
}
|
||||
})
|
||||
.catch((error: Error | any) => {
|
||||
if (error.format) {
|
||||
alert.showError(error.format());
|
||||
} else {
|
||||
alert.showError(error.message);
|
||||
}
|
||||
console.error(error);
|
||||
})
|
||||
.finally(() => {
|
||||
eventHandler.setEventType(EVENT_TYPES.SLEEP);
|
||||
});
|
||||
}, [SessionInfo.userId]);
|
||||
|
||||
const handleLoadMoreClient = () => {
|
||||
const nextPage = clientPage + 1;
|
||||
eventHandler.setEventType(EVENT_TYPES.LOADING);
|
||||
findAppointmentByUserPaginated({ sessionUser: SessionInfo.userId, page: nextPage, limit: 10 })
|
||||
.then((res) => {
|
||||
setClientAppointments((prev) => [...prev, ...res.data]);
|
||||
setClientPage(nextPage);
|
||||
setClientHasMore(res.page < res.pages);
|
||||
})
|
||||
.catch((error) => alert.showError(error.message))
|
||||
.finally(() => eventHandler.setEventType(EVENT_TYPES.SLEEP));
|
||||
};
|
||||
|
||||
const handleLoadMoreCollaborator = () => {
|
||||
const nextPage = collaboratorPage + 1;
|
||||
eventHandler.setEventType(EVENT_TYPES.LOADING);
|
||||
findAppointmentByCollaboratorPaginated({ sessionUser: SessionInfo.userId, page: nextPage, limit: 10 })
|
||||
.then((res) => {
|
||||
setCollaboratorAppointments((prev) => [...prev, ...res.data]);
|
||||
setCollaboratorPage(nextPage);
|
||||
setCollaboratorHasMore(res.page < res.pages);
|
||||
})
|
||||
.catch((error) => alert.showError(error.message))
|
||||
.finally(() => eventHandler.setEventType(EVENT_TYPES.SLEEP));
|
||||
};
|
||||
|
||||
return (
|
||||
<AnimatedContainer
|
||||
color="#aeaeae"
|
||||
minSize={150}
|
||||
maxSize={500}
|
||||
items={10}
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<ScrollToTop />
|
||||
<HeaderConfProvider />
|
||||
<div className="homeCentered" style={{ width: "100%", maxWidth: "1200px" }}>
|
||||
<h1 className="homeTitleHeader" style={{ textAlign: "center", width: "100%" }}>
|
||||
Mis Turnos
|
||||
</h1>
|
||||
|
||||
<Box sx={{ width: "100%", mt: 4 }}>
|
||||
{collaboratorAppointments.length > 0 ? (
|
||||
<>
|
||||
<Box sx={{ borderBottom: 1, borderColor: "divider" }}>
|
||||
<ThemeProvider theme={turnosXpressTheme}>
|
||||
<Tabs
|
||||
value={tabValue}
|
||||
onChange={handleTabChange}
|
||||
aria-label="appointments tabs"
|
||||
centered
|
||||
>
|
||||
<Tab label="Como Cliente" {...a11yProps(0)} />
|
||||
<Tab label="Como Profesional" {...a11yProps(1)} />
|
||||
</Tabs>
|
||||
</ThemeProvider>
|
||||
</Box>
|
||||
<CustomTabPanel value={tabValue} index={0}>
|
||||
<AppointmentList
|
||||
appointments={clientAppointments}
|
||||
containerHeight={containerHeight}
|
||||
emptyMessage="Aun no tienes ninguna reserva hecha como cliente."
|
||||
goTo={goTo}
|
||||
isProfessionalView={false}
|
||||
/>
|
||||
{clientHasMore && (
|
||||
<Box sx={{ display: "flex", justifyContent: "center", mt: 3, mb: 3 }}>
|
||||
<ThemeProvider theme={turnosXpressTheme}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
onClick={handleLoadMoreClient}
|
||||
>
|
||||
Cargar Más
|
||||
</Button>
|
||||
</ThemeProvider>
|
||||
</Box>
|
||||
)}
|
||||
</CustomTabPanel>
|
||||
<CustomTabPanel value={tabValue} index={1}>
|
||||
<Box sx={{ display: "flex", justifyContent: "flex-end", mb: 2 }}>
|
||||
<ThemeProvider theme={turnosXpressTheme}>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => goTo("/landing/dashboard")}
|
||||
sx={{
|
||||
borderRadius: "8px",
|
||||
textTransform: "none",
|
||||
fontWeight: 600,
|
||||
px: 3,
|
||||
py: 1,
|
||||
boxShadow: "0 4px 12px rgba(225, 29, 72, 0.2)",
|
||||
"&:hover": {
|
||||
boxShadow: "0 6px 16px rgba(225, 29, 72, 0.3)",
|
||||
transform: "translateY(-2px)",
|
||||
},
|
||||
transition: "all 0.2s ease-in-out",
|
||||
}}
|
||||
>
|
||||
Ir a mi Dashboard
|
||||
</Button>
|
||||
</ThemeProvider>
|
||||
</Box>
|
||||
<PendingRepeatsCard />
|
||||
<AppointmentList
|
||||
appointments={collaboratorAppointments}
|
||||
containerHeight={containerHeight}
|
||||
emptyMessage="No tienes turnos asignados como profesional."
|
||||
goTo={goTo}
|
||||
isProfessionalView={true}
|
||||
/>
|
||||
{collaboratorHasMore && (
|
||||
<Box sx={{ display: "flex", justifyContent: "center", mt: 3, mb: 3 }}>
|
||||
<ThemeProvider theme={turnosXpressTheme}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
onClick={handleLoadMoreCollaborator}
|
||||
>
|
||||
Cargar Más
|
||||
</Button>
|
||||
</ThemeProvider>
|
||||
</Box>
|
||||
)}
|
||||
</CustomTabPanel>
|
||||
</>
|
||||
) : (
|
||||
<Box sx={{ p: 3, width: "100%" }}>
|
||||
<AppointmentList
|
||||
appointments={clientAppointments}
|
||||
containerHeight={containerHeight}
|
||||
emptyMessage="Aun no tienes ninguna reserva hecha."
|
||||
goTo={goTo}
|
||||
isProfessionalView={false}
|
||||
/>
|
||||
{clientHasMore && (
|
||||
<Box sx={{ display: "flex", justifyContent: "center", mt: 3, mb: 3 }}>
|
||||
<ThemeProvider theme={turnosXpressTheme}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
onClick={handleLoadMoreClient}
|
||||
>
|
||||
Cargar Más
|
||||
</Button>
|
||||
</ThemeProvider>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</div>
|
||||
</AnimatedContainer>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user