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
@@ -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>
);
}