Files
turnosxpress/txclient/src/app/landing/dashboard/page.tsx
T
horacio 57c5deb3bd Add QR code images for transaction assistant, login, and plans
- Added `qrcode-tx-assistant.png` for transaction assistant functionality.
- Added `qrcode-tx-login.png` for user login process.
- Added `qrcode-tx-plans.png` for displaying available plans.
2026-09-04 21:05:20 -03:00

717 lines
40 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import AnimatedContainer from "@core/app/components/AnimatedConainer/AnimatedContainer";
import Button from "@core/app/components/Button/Button";
import { useNavigation } from "@core/app/hooks/goto";
import turnosXpressTheme from "@core/app/theme/turnosXpress";
import { useConfirmStore } from "@core/Store/Confirm.Store";
import { useSessionStore } from "@core/Store/Sesion.Store";
import { ThemeProvider } from "@emotion/react";
import SuspenseLoading from "@core/app/components/SuspendeLoading/SuspenseLoading";
import Secure from "@components/Secure/Secure";
import { useEffect, useRef, useState, Suspense } from "react";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import {
cancellSubscription,
getSuscriptionInitPoint,
getDashboardSummary,
DashboardSummaryResult,
getCompanyMetrics,
DashboardMetrics,
verifyPendingPlanPayment
} from "./Dashboard.Service";
import { EVENT_TYPES, useEventHandlerStore } from "@core/Store/EventHandler.Store";
import { useAlert } from "@core/Store/Alert.Store";
import { loadOrganizationsByUser } from "@core/app/admin/org/Org.Service";
import { MyOranizationsView } from "@core/Models/Company.model";
import {
getSubStatusText,
ISubscriptionInfo,
MP_SUBS_STATUS,
} from "@core/Models/Subscription.Model";
import dayjs from "dayjs";
import style from "./style.module.css";
import { blurElementsOnLoad } from "@core/helpers/blur";
import HeaderConfProvider from "@core/app/components/HeaderConfProvider/HeaderConfProvider";
import classNames from "classnames";
import { ScrollToTop } from "@core/app/components/ScrollTop";
import StorefrontOutlinedIcon from "@mui/icons-material/StorefrontOutlined";
import NotificationsNoneOutlinedIcon from "@mui/icons-material/NotificationsNoneOutlined";
import ChatBubbleOutlineOutlinedIcon from "@mui/icons-material/ChatBubbleOutlineOutlined";
import AddCircleOutlineOutlinedIcon from "@mui/icons-material/AddCircleOutlineOutlined";
import AutoAwesomeIcon from "@mui/icons-material/AutoAwesome";
import HelpOutlineIcon from "@mui/icons-material/HelpOutline";
import FinancialWidget from "@components/FinancialWidget/FinancialWidget";
import PlanMetricsWidget from "@components/PlanMetricsWidget/PlanMetricsWidget";
import PendingRepeatsCard from "@core/app/components/PendingRepeatsCard/PendingRepeatsCard";
import PendingRatingsBanner from "@components/PendingRatingsBanner/PendingRatingsBanner";
function DashboardContent() {
const { goTo } = useNavigation();
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const SessionInfo = useSessionStore();
const eventHandler = useEventHandlerStore();
const alert = useAlert();
const confirm = useConfirmStore();
const infoEmail = process.env.NEXT_PUBLIC_INFO_EMAIL || "info@turnosxpress.com.ar";
const [showOnlyMine, setShowOnlyMine] = useState(false);
const [isAgendaCollapsed, setIsAgendaCollapsed] = useState(true);
const [subscription, setSubscription] = useState<ISubscriptionInfo>({
plan: {
name: "Free",
limitOrganizations: 1,
limitEmployees: 1,
limitServices: 1,
limitAppointments: 1,
limitClients: 1,
limitRepeats: 1,
description: "",
features: [],
code: "",
price: 0,
annualPrice: 0,
id: "",
mailNotifications: false,
smsNotifications: false,
wapNotifications: false,
bot: false,
active: false,
dateLimit: false,
payments: false,
},
id: "",
startDate: undefined,
endDate: undefined,
isActive: false,
autoRenew: false,
mpStatus: "",
mpDateCreated: undefined,
});
const [dashboardData, setDashboardData] = useState<DashboardSummaryResult | null>(null);
const [organizations, setOrganizations] = useState<MyOranizationsView[]>([]);
const [selectedOrgId, setSelectedOrgId] = useState<string>("");
// Per-company states
const [companyMetrics, setCompanyMetrics] = useState<DashboardMetrics | null>(null);
const [companySubscription, setCompanySubscription] = useState<ISubscriptionInfo | null>(null);
const [isCompanyOwner, setIsCompanyOwner] = useState<boolean>(false);
const [isVerifyingPayment, setIsVerifyingPayment] = useState(false);
const verifiedPaymentReturnKey = useRef<string | null>(null);
const getErrorMessage = (error: unknown): string => {
if (error && typeof error === "object" && "format" in error && typeof error.format === "function") {
return error.format();
}
if (error instanceof Error) {
return error.message;
}
return "Ha ocurrido un error inesperado";
};
useEffect(() => {
if (SessionInfo.userId && !SessionInfo.subscription) {
goTo("/");
}
if (SessionInfo.subscription?.plan?.id) {
setSubscription(SessionInfo.subscription);
}
blurElementsOnLoad();
if (SessionInfo.userId) {
loadDashboardData();
}
}, [SessionInfo.userId]);
const loadDashboardData = async () => {
try {
const data = await getDashboardSummary({ sessionUser: SessionInfo.userId });
setDashboardData(data);
const orgs = await loadOrganizationsByUser(SessionInfo.userId);
setOrganizations(orgs);
if (orgs.length > 0) {
setSelectedOrgId(orgs[0].id);
}
} catch (e) {
console.error("Error loading dashboard data", e);
}
};
useEffect(() => {
if (selectedOrgId && SessionInfo.userId) {
loadCompanyMetrics(selectedOrgId);
}
}, [selectedOrgId, SessionInfo.userId]);
const loadCompanyMetrics = async (companyId: string) => {
setCompanySubscription(null);
setCompanyMetrics(null);
setIsCompanyOwner(false);
try {
const res = await getCompanyMetrics({ sessionUser: SessionInfo.userId, companyId });
setCompanyMetrics(res.metrics);
setCompanySubscription(res.subscription);
setSubscription(res.subscription);
setIsCompanyOwner(res.isOwner);
if (res.isOwner) {
SessionInfo.setSubscription(res.subscription);
}
} catch (e) {
const errMsg = getErrorMessage(e);
console.warn("Acceso denegado a métricas de la empresa: ", errMsg);
setCompanyMetrics(null);
setCompanySubscription(null);
setIsCompanyOwner(false);
}
};
const refreshDashboardSubscriptionData = async () => {
await loadDashboardData();
if (selectedOrgId) {
await loadCompanyMetrics(selectedOrgId);
}
};
const getPaymentLink = () => {
const pendingPaymentInitPoint = displayedSubscription?.pendingPaymentInitPoint;
if (pendingPaymentInitPoint) {
goTo(pendingPaymentInitPoint);
return;
}
eventHandler.setEventType(EVENT_TYPES.LOADING);
getSuscriptionInitPoint({
sessionUser: SessionInfo.userId,
})
.then((response) => {
goTo(response.init_point);
})
.catch((error) => {
alert.showError(error.format());
})
.finally(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
});
};
const handleVerifyPendingPayment = async () => {
setIsVerifyingPayment(true);
eventHandler.setEventType(EVENT_TYPES.LOADING);
try {
const response = await verifyPendingPlanPayment({ sessionUser: SessionInfo.userId });
if (response.approved) {
alert.showSuccess(response.message);
await refreshDashboardSubscriptionData();
return;
}
if (["rejected", "failed", "cancelled"].includes(response.status)) {
alert.showWarning(response.message);
await refreshDashboardSubscriptionData();
return;
}
alert.showWarning(response.message);
} catch (error) {
alert.showError(getErrorMessage(error));
} finally {
setIsVerifyingPayment(false);
eventHandler.setEventType(EVENT_TYPES.SLEEP);
}
};
useEffect(() => {
if (!SessionInfo.userId) return;
const hasPaymentReturnParams = [
"mp_return",
"status",
"collection_status",
"payment_id",
"preference_id",
"merchant_order_id",
].some((param) => searchParams.has(param));
if (!hasPaymentReturnParams) return;
const paymentReturnKey = searchParams.toString();
if (verifiedPaymentReturnKey.current === paymentReturnKey) return;
verifiedPaymentReturnKey.current = paymentReturnKey;
handleVerifyPendingPayment().finally(() => {
router.replace(pathname, { scroll: false });
});
}, [SessionInfo.userId, searchParams, pathname, router]);
const cancelarSubscription = () => {
confirm.show(
"Estas a punto de cancelar tu suscripción. Al realizar esta acción dejaras de tener acceso a la información de tus organizaciones cuando se termine el periodo contratado. Puedes volver a suscribirte a cualquier otro plan y recuperarlo en el futuro. Estas seguro que quieres cancelar tu suscripcion?",
() => {
eventHandler.setEventType(EVENT_TYPES.LOADING);
cancellSubscription({
sessionUser: SessionInfo.userId,
})
.then(() => {
SessionInfo.clearSubscription();
setSubscription({
...SessionInfo.subscription,
mpStatus: MP_SUBS_STATUS.CANCELLED,
});
alert.showSuccess("La suscripción ha sido cancelada con éxito");
})
.catch((error) => {
alert.showWarning(error.format());
})
.finally(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
});
},
() => {
alert.showWarning("No se ha cancelado la suscripción");
}
);
};
const handleNewAppointmentClick = () => {
if (organizations.length === 0) {
alert.showWarning("No perteneces a ninguna organización");
return;
}
if (organizations.length === 1) {
const todayStr = dayjs().startOf('day').toISOString();
goTo(`/admin/org/profile/${organizations[0].id}/appointments/${todayStr}/create`);
return;
}
// Mostrar listado de organizaciones para elegir usando alert / modal
// Simplificado con un confirm adaptado o directamente yendo a mis organizaciones si no hay modal para esto.
// Dado que turnosXpress usa confirm.show para preguntas, la mejor UI aquí sin crear un modal nuevo
// es ir a /admin/org y pedirle que elija desde allí, o crear un modal custom.
// Como pidió que se despliegue un listado, si tienen un componente modal, podríamos usarlo.
// Por practicidad y reuso, voy a redirigirlo a Mis Organizaciones si son múltiples, pero en el comentario decia "desplegar un listado".
// Voy a intentar redirigir a /admin/org y que elija ahí.
alert.showWarning("Perteneces a múltiples organizaciones, selecciona una para crear el turno.");
goTo("/admin/org");
};
const isUsingSessionSubscriptionFallback = !companySubscription && Boolean(subscription.plan.id);
const selectedOrganization = organizations.find((organization) => organization.id === selectedOrgId);
const displayedSubscription = companySubscription ?? (subscription.plan.id ? subscription : null);
const displayedPlan = displayedSubscription?.plan;
const displayedPlanStatusText = displayedPlan?.price === 0 ? "ACTIVO" : getSubStatusText(displayedSubscription?.mpStatus);
const hasPendingPaymentMetadata = Boolean(displayedSubscription?.pendingPaymentPreferenceId);
const hasRecentFailedPayment = Boolean(
displayedSubscription?.lastPaymentStatus &&
["rejected", "failed", "cancelled"].includes(displayedSubscription.lastPaymentStatus) &&
displayedSubscription.lastPaymentAt &&
dayjs().diff(dayjs(displayedSubscription.lastPaymentAt), "day") <= 14
);
const selectedCompanyHasPendingPayment = Boolean(
companySubscription &&
(companySubscription.mpStatus === MP_SUBS_STATUS.PENDING || companySubscription.pendingPaymentPreferenceId)
);
const showPaymentAction = selectedOrgId
? Boolean(selectedCompanyHasPendingPayment && isCompanyOwner)
: Boolean(
(displayedSubscription?.mpStatus === MP_SUBS_STATUS.PENDING || hasPendingPaymentMetadata) &&
isUsingSessionSubscriptionFallback
);
const showFailedPaymentCard = Boolean(hasRecentFailedPayment && !showPaymentAction);
const showExpiredDowngradeCard = Boolean(
displayedSubscription?.downgradeReason === "expired" &&
displayedSubscription.downgradedAt &&
isCompanyOwner
);
const selectedCompanyPlanSupportsRepeats = Boolean(
companySubscription?.plan &&
(companySubscription.plan.limitRepeats === -1 || companySubscription.plan.limitRepeats > 0)
);
const canLoadPendingRepeats = Boolean(
selectedCompanyPlanSupportsRepeats &&
(isCompanyOwner || selectedOrganization?.isAdmin)
);
const showNoOrganizationStarterContent = Boolean(displayedPlan && organizations.length === 0);
const agendaItems = dashboardData?.personalAgenda ?? [];
const filteredAgendaItems = agendaItems.filter(item => !showOnlyMine || item.collaboratorUserId === SessionInfo.userId);
return (
<ThemeProvider theme={turnosXpressTheme}>
<ScrollToTop />
<HeaderConfProvider />
<Suspense fallback={<SuspenseLoading />}>
<Secure>
<AnimatedContainer
color="#aeaeae"
minSize={150}
maxSize={500}
items={10}
style={{
display: "flex",
flexDirection: "column",
justifyContent: "flex-start",
alignItems: "center",
minHeight: "calc(100vh - 60px)",
}}
>
{SessionInfo.userId != "" && !displayedPlan && (
<div className="homeCentered">No tiene una suscripción activa.</div>
)}
{displayedPlan && <PendingRatingsBanner variant="banner" />}
{displayedPlan && (
<div className={style.dashboardGrid}>
{/* Welcome Header */}
<div className={style.welcomeCard}>
{organizations.length > 1 && (
<div style={{ position: "absolute", top: "20px", right: "20px", display: "flex", alignItems: "center", gap: "10px" }}>
<select
value={selectedOrgId}
onChange={e => {
setCompanySubscription(null);
setCompanyMetrics(null);
setIsCompanyOwner(false);
setSelectedOrgId(e.target.value);
}}
style={{ padding: "8px 12px", borderRadius: "8px", border: "1px solid #e5e7eb", fontSize: "14px", fontWeight: "500", backgroundColor: "#f9fafb", cursor: "pointer", outline: "none" }}
>
{organizations.map(org => (
<option key={org.id} value={org.id}>{org.name}</option>
))}
</select>
</div>
)}
<div className={style.welcomeContainer}>
<div style={{display: 'flex', flexDirection: 'column', gap: '5px'}}>
<h3>Bienvenido, {SessionInfo.userFullName}</h3>
<div style={{ display: 'flex', alignItems: 'center', gap: '5px' }}>
<span style={{ fontWeight: 'bold' }}>{displayedPlan.name}</span>
{displayedSubscription && (
<span
style={{
backgroundColor:
displayedPlanStatusText === "ACTIVO"
? "var(--sub-status-active)"
: "var(--sub-status-inactive)",
borderRadius: "12px",
padding: "4px 10px",
fontSize: "12px",
color: "white",
fontWeight: "bold",
}}
>
{displayedPlanStatusText}
</span>
)}
{displayedSubscription && isCompanyOwner && (
<Button
color="link"
text="Ver detalles del plan"
width="custom"
style={{ margin: 0, padding: 0, fontSize: "14px", fontWeight: "700", color: "var(--wine-red)", minHeight: "auto", width: "fit-content" }}
onClick={() => goTo("/landing/current-plan")}
/>
)}
</div>
</div>
</div>
<div className={style.quickActions}>
<div className={classNames(style.actionButton, style.buttonOrganizations)} onClick={() => goTo("/admin/org")}>
<StorefrontOutlinedIcon /> Mis Organizaciones
</div>
<div className={classNames(style.actionButton, style.buttonNotification)} onClick={() => goTo("/user/profile/notifications")}>
<NotificationsNoneOutlinedIcon /> Notificaciones
</div>
<div className={classNames(style.actionButton, style.buttonMessenger)} onClick={() => goTo("/messenger")}>
<ChatBubbleOutlineOutlinedIcon /> Mensajes
</div>
<div className={classNames(style.actionButton, style.buttonOrganizations)} onClick={() => goTo("/admin/assistant")} style={{backgroundColor: "#800020", color: "white"}}>
<AutoAwesomeIcon /> Asistente
</div>
<div className={classNames(style.actionButton)} style={{backgroundColor: "var(--wine-red)"}} onClick={handleNewAppointmentClick}>
<AddCircleOutlineOutlinedIcon /> Nuevo Turno
</div>
<div className={classNames(style.actionButton, style.buttonHelp)} onClick={() => goTo("/landing/help")}>
<HelpOutlineIcon /> Obtener ayuda
</div>
</div>
</div>
{showPaymentAction && (
<div className={style.pendingPaymentCard}>
<div className={style.pendingPaymentContent}>
<span className={style.pendingPaymentEyebrow}>Pago pendiente</span>
<h2>Tu plan está esperando confirmación de MercadoPago</h2>
<p>
Si ya pagaste, puede tardar unos minutos en actualizarse porque dependemos de la notificación de MercadoPago.
No te preocupes: podés volver al checkout si quedó incompleto o pedirnos que verifiquemos el pago ahora mismo.
{" "}Si el problema continúa, escribinos a <a className={style.pendingPaymentLink} href={`mailto:${infoEmail}`}>{infoEmail}</a> y te ayudamos a revisarlo.
</p>
</div>
<div className={style.pendingPaymentActions}>
<Button
text="Ir a pagar"
color="plain"
width="custom"
style={{
margin: 0,
width: "220px",
backgroundColor: "#9a4b00",
color: "white",
border: "1px solid #9a4b00",
}}
onClick={getPaymentLink}
/>
<Button
text={isVerifyingPayment ? "Verificando..." : "Verificar pago"}
color="plain"
width="custom"
style={{
margin: 0,
width: "220px",
backgroundColor: "#fff3d6",
color: "#9a4b00",
border: "1px solid rgba(154, 75, 0, 0.28)",
}}
onClick={handleVerifyPendingPayment}
disabled={isVerifyingPayment}
/>
</div>
</div>
)}
{showFailedPaymentCard && (
<div className={style.failedPaymentCard}>
<div className={style.pendingPaymentContent}>
<span className={style.failedPaymentEyebrow}>Pago no aprobado</span>
<h2>Tu pago no se aprobó y tu plan sigue igual</h2>
<p>
El intento de pago fue rechazado, falló o se canceló. No cambiamos tu plan actual ni la fecha de finalización.
Cuando quieras, podés volver a intentarlo desde los detalles del plan.
</p>
</div>
</div>
)}
{showExpiredDowngradeCard && (
<div className={style.expiredDowngradeCard}>
<div className={style.pendingPaymentContent}>
<span className={style.expiredDowngradeEyebrow}>Plan vencido</span>
<h2>Tu plan pago venció y pasaste al plan básico</h2>
<p>
No borramos tu organización ni tus datos. Desde ahora las nuevas acciones usan los límites del plan básico.
{displayedSubscription?.downgradedFromPlanName && ` Tu plan anterior era ${displayedSubscription.downgradedFromPlanName}.`}
</p>
</div>
<button
className={style.expiredDowngradeButton}
onClick={() => goTo(selectedOrgId ? `/landing/upgrade-plan?org=${selectedOrgId}` : "/landing/pricing")}
>
Volver a contratar un plan
</button>
</div>
)}
{showNoOrganizationStarterContent && (
<div className={style.starterCard}>
<div className={style.starterContent}>
<span className={style.starterEyebrow}>Tu plan {displayedPlan.name} ya está activo</span>
<h2>Creá tu primera organización para empezar a trabajar</h2>
<p>
Todavía no pertenecés a ninguna organización. Podés seguir el flujo guiado con el asistente
o hacerlo manualmente desde Mis Organizaciones.
</p>
</div>
<div className={style.starterActions}>
<button className={style.starterPrimaryButton} onClick={() => goTo("/admin/assistant?flow=onboarding-org&action=new-org")}>
Usar el asistente
</button>
<button className={style.starterSecondaryButton} onClick={() => goTo("/admin/org")}>
Crear manualmente
</button>
</div>
</div>
)}
{showNoOrganizationStarterContent && (
<div className={style.starterGuideGrid}>
<section className={style.starterGuideCard}>
<span className={style.starterGuideNumber}>1</span>
<h3>Creá tu espacio de trabajo</h3>
<p>
El asistente te guía paso a paso para cargar tu negocio, servicios, horarios y colaboradores.
Es el camino recomendado si querés empezar a recibir reservas.
</p>
</section>
<section className={style.starterGuideCard}>
<span className={style.starterGuideNumber}>2</span>
<h3>Compartí tu enlace público</h3>
<p>
Cuando termines la configuración vas a tener una página pública para que tus clientes reserven
turnos sin escribirte por WhatsApp a cada rato.
</p>
</section>
<section className={style.starterGuideCard}>
<span className={style.starterGuideNumber}>3</span>
<h3>También podés sumarte a un equipo</h3>
<p>
Si trabajás para una organización existente, ahora pueden invitarte y vas a poder gestionar tus
turnos desde esta misma cuenta.
</p>
</section>
</div>
)}
{showNoOrganizationStarterContent && (
<div className={style.starterNextStepsPanel}>
<div>
<span className={style.starterEyebrow}>Qué podés hacer con tu plan</span>
<h3>Tu cuenta ya está lista para operar</h3>
<p>
Usá este primer paso para preparar la base de tu negocio. Cuando tengas tu organización creada,
vas a poder administrar servicios, horarios, reservas e invitaciones desde el dashboard.
</p>
</div>
<ul className={style.starterChecklist}>
<li>Crear una organización para tu negocio o actividad profesional.</li>
<li>Configurar servicios y horarios disponibles.</li>
<li>Recibir invitaciones de otras organizaciones y formar parte de su equipo.</li>
<li>Seguir usando Mis turnos para tus reservas como cliente.</li>
</ul>
</div>
)}
{/* Left Column (Sidebar) */}
{!showNoOrganizationStarterContent && <div style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
{/* Agenda del Día */}
{dashboardData && dashboardData.role !== "NONE" && (
<div className={style.agendaCard}>
<button
className={style.agendaHeader}
type="button"
onClick={() => setIsAgendaCollapsed(prev => !prev)}
>
<div className={style.agendaHeaderTitleWrap}>
<h3 style={{ margin: 0 }}>
{showOnlyMine ? "Mi Agenda de Hoy" : "Agenda General de Hoy"}
</h3>
<span className={style.agendaCounter}>{filteredAgendaItems.length}</span>
</div>
<span className={style.agendaToggleIcon}>{isAgendaCollapsed ? "+" : ""}</span>
</button>
{!isAgendaCollapsed && (
<div style={{ display: 'flex', justifyContent: 'flex-end', alignItems: 'center', marginBottom: '15px' }}>
<label
style={{ fontSize: '13px', display: 'flex', alignItems: 'center', gap: '5px', cursor: 'pointer', color: '#666' }}
onClick={e => e.stopPropagation()}
>
<input
type="checkbox"
checked={showOnlyMine}
onChange={(e) => setShowOnlyMine(e.target.checked)}
/>
Solo mis turnos
</label>
</div>
)}
{!isAgendaCollapsed && (
<div className={style.agendaList}>
{filteredAgendaItems.length === 0 && (
<div style={{color: '#666', marginTop: '10px'}}>
No hay turnos programados para hoy con estos filtros.
</div>
)}
{filteredAgendaItems.map(item => (
<div key={item.id} className={style.agendaItem}>
<span style={{fontWeight: 'bold'}}>{dayjs(item.appointmentDate).format("HH:mm")} - {item.clientName}</span>
<span style={{fontSize: '14px', color: '#666'}}>{item.serviceName}</span>
<span style={{fontSize: '12px', color: '#999', marginTop: '5px'}}>{item.collaboratorName}</span>
</div>
))}
</div>
)}
</div>
)}
{/* Plan Metrics (Only for ADMIN of the selected org) */}
{companySubscription && companyMetrics && (
<PlanMetricsWidget
subscription={companySubscription}
metrics={companyMetrics}
isOwner={isCompanyOwner}
onUpgrade={() => {
const freePlan = companySubscription.plan.price === 0;
if (freePlan) {
goTo("/landing/pricing");
return;
}
if (
companySubscription.mpStatus !==
MP_SUBS_STATUS.CANCELLED
) {
confirm.show(
"Para cambiarte a un plan superior, debes cancelar primero el plan actual.",
() => {},
() => {}
);
return;
}
goTo("/landing/pricing");
}}
onRenew={() => goTo(`/landing/subscription/${companySubscription.plan.id}?renew=1`)}
onViewDetails={() => goTo("/landing/current-plan")}
onCancel={cancelarSubscription}
/>
)}
</div>}
{/* Right Column (Main Content) */}
{!showNoOrganizationStarterContent && <div style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
{/* Rendimiento Financiero (Only for ADMIN) */}
{dashboardData && dashboardData.role === "ADMIN" && (
<FinancialWidget
sessionUser={SessionInfo.userId}
selectedOrgId={selectedOrgId}
organizationName={organizations.find(o => o.id === selectedOrgId)?.name}
/>
)}
{/* Repeats Pending Card */}
{dashboardData && selectedOrgId && canLoadPendingRepeats && (
<PendingRepeatsCard
companyId={selectedOrgId}
allowEmployeeFilter={true}
/>
)}
</div>}
</div>
)}
</AnimatedContainer>
</Secure>
</Suspense>
</ThemeProvider>
);
}
export default function DashboardPage() {
return (
<Suspense fallback={<SuspenseLoading />}>
<DashboardContent />
</Suspense>
);
}