"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({ 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(null); const [organizations, setOrganizations] = useState([]); const [selectedOrgId, setSelectedOrgId] = useState(""); // Per-company states const [companyMetrics, setCompanyMetrics] = useState(null); const [companySubscription, setCompanySubscription] = useState(null); const [isCompanyOwner, setIsCompanyOwner] = useState(false); const [isVerifyingPayment, setIsVerifyingPayment] = useState(false); const verifiedPaymentReturnKey = useRef(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 ( }> {SessionInfo.userId != "" && !displayedPlan && (
No tiene una suscripción activa.
)} {displayedPlan && } {displayedPlan && (
{/* Welcome Header */}
{organizations.length > 1 && (
)}

Bienvenido, {SessionInfo.userFullName}

{displayedPlan.name} {displayedSubscription && ( {displayedPlanStatusText} )} {displayedSubscription && isCompanyOwner && (
goTo("/admin/org")}> Mis Organizaciones
goTo("/user/profile/notifications")}> Notificaciones
goTo("/messenger")}> Mensajes
goTo("/admin/assistant")} style={{backgroundColor: "#800020", color: "white"}}> Asistente
Nuevo Turno
goTo("/landing/help")}> Obtener ayuda
{showPaymentAction && (
Pago pendiente

Tu plan está esperando confirmación de MercadoPago

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 {infoEmail} y te ayudamos a revisarlo.

)} {showFailedPaymentCard && (
Pago no aprobado

Tu pago no se aprobó y tu plan sigue igual

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.

)} {showExpiredDowngradeCard && (
Plan vencido

Tu plan pago venció y pasaste al plan básico

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}.`}

)} {showNoOrganizationStarterContent && (
Tu plan {displayedPlan.name} ya está activo

Creá tu primera organización para empezar a trabajar

Todavía no pertenecés a ninguna organización. Podés seguir el flujo guiado con el asistente o hacerlo manualmente desde Mis Organizaciones.

)} {showNoOrganizationStarterContent && (
1

Creá tu espacio de trabajo

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.

2

Compartí tu enlace público

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.

3

También podés sumarte a un equipo

Si trabajás para una organización existente, ahora pueden invitarte y vas a poder gestionar tus turnos desde esta misma cuenta.

)} {showNoOrganizationStarterContent && (
Qué podés hacer con tu plan

Tu cuenta ya está lista para operar

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.

  • Crear una organización para tu negocio o actividad profesional.
  • Configurar servicios y horarios disponibles.
  • Recibir invitaciones de otras organizaciones y formar parte de su equipo.
  • Seguir usando “Mis turnos” para tus reservas como cliente.
)} {/* Left Column (Sidebar) */} {!showNoOrganizationStarterContent &&
{/* Agenda del Día */} {dashboardData && dashboardData.role !== "NONE" && (
{!isAgendaCollapsed && (
)} {!isAgendaCollapsed && (
{filteredAgendaItems.length === 0 && (
No hay turnos programados para hoy con estos filtros.
)} {filteredAgendaItems.map(item => (
{dayjs(item.appointmentDate).format("HH:mm")} - {item.clientName} {item.serviceName} {item.collaboratorName}
))}
)}
)} {/* Plan Metrics (Only for ADMIN of the selected org) */} {companySubscription && companyMetrics && ( { 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} /> )}
} {/* Right Column (Main Content) */} {!showNoOrganizationStarterContent &&
{/* Rendimiento Financiero (Only for ADMIN) */} {dashboardData && dashboardData.role === "ADMIN" && ( o.id === selectedOrgId)?.name} /> )} {/* Repeats Pending Card */} {dashboardData && selectedOrgId && canLoadPendingRepeats && ( )}
}
)}
); } export default function DashboardPage() { return ( }> ); }