"use client"; import ApiRequest from "@services/Api.Service"; import AnimatedContainer from "@core/app/components/AnimatedConainer/AnimatedContainer"; import HeaderConfProvider from "@core/app/components/HeaderConfProvider/HeaderConfProvider"; import Secure from "@components/Secure/Secure"; import SuspenseLoading from "@core/app/components/SuspendeLoading/SuspenseLoading"; import { ScrollToTop } from "@core/app/components/ScrollTop"; import { useNavigation } from "@core/app/hooks/goto"; import { loadOrganizationsByUser } from "@core/app/admin/org/Org.Service"; import { DashboardMetrics, getCompanyMetrics } from "../dashboard/Dashboard.Service"; import { MyOranizationsView } from "@core/Models/Company.model"; import { IPlan } from "@core/Models/Plans.model"; import { CreateMPPreApprovalParams, ISubscriptionInfo, MP_PRE_APPROVAL_STATUS, MPPreApprovalResponse, MP_SUBS_STATUS, } from "@core/Models/Subscription.Model"; import { useAlert } from "@core/Store/Alert.Store"; import { useSessionStore } from "@core/Store/Sesion.Store"; import { ThemeProvider } from "@emotion/react"; import { formatPrice } from "@helpers/Numbers"; import turnosXpressTheme from "@core/app/theme/turnosXpress"; import dayjs from "dayjs"; import { Suspense, useEffect, useMemo, useState } from "react"; import { useSearchParams } from "next/navigation"; import styles from "./page.module.css"; const billingOptions = [ { months: 1, label: "1 mes" }, { months: 3, label: "3 meses" }, { months: 6, label: "6 meses" }, { months: 12, label: "12 meses" }, ]; const DAY_MS = 24 * 60 * 60 * 1000; const formatDate = (date?: Date) => (date ? dayjs(date).format("DD/MM/YYYY") : "No disponible"); const getDiscount = (plan: IPlan, months: number) => { if (months === 3) return plan.discount3Months || 0; if (months === 6) return plan.discount6Months || 0; if (months === 12) return plan.discount12Months || 0; return 0; }; const getPlanTotal = (plan: IPlan, months: number) => { const discount = getDiscount(plan, months); const total = plan.price * months; return total - total * (discount / 100); }; const roundPrice = (value: number) => Math.round(value * 100) / 100; const getRemainingDays = (date?: Date) => { if (!date) return 0; return Math.max(0, Math.ceil((new Date(date).getTime() - new Date().getTime()) / DAY_MS)); }; function UpgradePlanContent() { const searchParams = useSearchParams(); const requestedOrgId = searchParams.get("org") || ""; const { goTo } = useNavigation(); const alert = useAlert(); const SessionInfo = useSessionStore(); const [organizations, setOrganizations] = useState([]); const [selectedOrgId, setSelectedOrgId] = useState(""); const [subscription, setSubscription] = useState(null); const [metrics, setMetrics] = useState(null); const [isOwner, setIsOwner] = useState(false); const [plans, setPlans] = useState([]); const [selectedPlanId, setSelectedPlanId] = useState(""); const [selectedMonths, setSelectedMonths] = useState(1); const [loading, setLoading] = useState(true); const [plansLoading, setPlansLoading] = useState(true); const [submitting, setSubmitting] = useState(false); const [pageError, setPageError] = useState(""); const [plansError, setPlansError] = useState(""); useEffect(() => { if (!SessionInfo.userId) return; setLoading(true); loadOrganizationsByUser(SessionInfo.userId) .then((orgs) => { setOrganizations(orgs); const requestedOrg = orgs.find((org) => org.id === requestedOrgId); setSelectedOrgId(requestedOrg?.id || orgs[0]?.id || ""); setPageError(orgs.length === 0 ? "No pertenecés a ninguna organización." : ""); }) .catch((e) => { const message = e.format ? e.format() : e.message; setPageError(message || "No pudimos cargar tus organizaciones."); }) .finally(() => setLoading(false)); }, [SessionInfo.userId, requestedOrgId]); useEffect(() => { setPlansLoading(true); ApiRequest.post("plans/find", {}) .then((res) => { setPlans(res); setPlansError(""); }) .catch((e) => { const message = e.format ? e.format() : e.message; setPlansError(message || "No pudimos cargar los planes disponibles."); }) .finally(() => setPlansLoading(false)); }, []); useEffect(() => { if (!selectedOrgId || !SessionInfo.userId) return; setLoading(true); setSubscription(null); setMetrics(null); getCompanyMetrics({ sessionUser: SessionInfo.userId, companyId: selectedOrgId }) .then((res) => { setSubscription(res.subscription); setMetrics(res.metrics); setIsOwner(res.isOwner); setPageError(""); }) .catch((e) => { const message = e.format ? e.format() : e.message; setPageError(message || "No pudimos cargar el detalle del plan actual."); setIsOwner(false); }) .finally(() => setLoading(false)); }, [selectedOrgId, SessionInfo.userId]); const currentPlan = subscription?.plan; const isCurrentPaid = Boolean(currentPlan && currentPlan.price > 0); const availablePlans = useMemo(() => { if (!currentPlan) return []; return [...plans] .filter((plan) => plan.active && plan.price > 0 && plan.price > currentPlan.price) .sort((a, b) => a.price - b.price); }, [currentPlan, plans]); useEffect(() => { const hasSelectedPlan = availablePlans.some((plan) => plan.id === selectedPlanId); if (!hasSelectedPlan) setSelectedPlanId(availablePlans[0]?.id || ""); }, [availablePlans, selectedPlanId]); const selectedPlan = availablePlans.find((plan) => plan.id === selectedPlanId) || null; const selectedDiscount = selectedPlan ? getDiscount(selectedPlan, selectedMonths) : 0; const selectedSubtotal = selectedPlan ? selectedPlan.price * selectedMonths : 0; const selectedDiscountAmount = roundPrice(selectedSubtotal * (selectedDiscount / 100)); const selectedTotal = selectedPlan ? roundPrice(getPlanTotal(selectedPlan, selectedMonths)) : 0; const remainingDays = isCurrentPaid ? getRemainingDays(subscription?.endDate) : 0; const monthlyDifference = selectedPlan && currentPlan ? selectedPlan.price - currentPlan.price : 0; const proratedAmount = isCurrentPaid ? roundPrice((monthlyDifference / 30) * remainingDays) : 0; const canSubmit = Boolean( selectedPlan?.id && SessionInfo.userId && isOwner && subscription?.isActive && subscription?.mpStatus !== MP_SUBS_STATUS.PENDING && !submitting ); const effectiveMonths = isCurrentPaid ? 1 : selectedMonths; const error = plansError || pageError; const handleSubmit = () => { if (!canSubmit || !selectedPlan?.id) { if (!isOwner) alert.showWarning("Solo el dueño de la organización puede cambiar el plan."); else if (subscription?.mpStatus === MP_SUBS_STATUS.PENDING) alert.showWarning("Ya tenés un pago pendiente. Finalizalo antes de generar otro cambio de plan."); else alert.showWarning("Elegí un plan superior para continuar."); return; } const preApprovalData: CreateMPPreApprovalParams = { planId: selectedPlan.id, sessionUser: SessionInfo.userId, months: effectiveMonths, }; setSubmitting(true); ApiRequest.post("suscriptions/preapproval", preApprovalData) .then((response) => { if (response.status === MP_PRE_APPROVAL_STATUS.READY_TO_START) { goTo(response.init_point); } }) .catch((e) => { alert.showError(e.format ? e.format() : "No pudimos iniciar el pago del cambio de plan."); }) .finally(() => setSubmitting(false)); }; return (
Cambiar plan

Elegí un plan superior

Solo te mostramos planes pagos con mayor capacidad que tu plan actual.

{organizations.length > 1 && ( )}
{(loading || plansLoading) &&

Cargando opciones de cambio...

} {error && !loading && !plansLoading &&

{error}

} {subscription && currentPlan && !loading && !plansLoading && !error && ( <>
Plan actual {currentPlan.name} {currentPlan.price > 0 ? `${formatPrice(currentPlan.price)} por mes` : "Plan gratis"}
Vencimiento {formatDate(subscription.endDate)} {metrics ? `${metrics.clientsCount} clientes cargados` : "Organización activa"}
{isCurrentPaid && (
Upgrade prorrateado

Si subís a un plan superior, hoy pagás solo la diferencia proporcional por los días restantes. El vencimiento se mantiene y los nuevos límites se aplican cuando el pago quede aprobado.

)} {!isOwner &&

Solo el dueño de la organización puede cambiar el plan.

} {subscription.mpStatus === MP_SUBS_STATUS.PENDING &&

Ya tenés un pago pendiente. Finalizalo antes de generar otro cambio.

} {availablePlans.length === 0 ? (

No hay planes superiores disponibles

Tu plan actual ya está en el nivel más alto disponible para contratar.

) : (
{availablePlans.map((plan) => { const isSelected = selectedPlanId === plan.id; return ( ); })}
)} {selectedPlan && !isCurrentPaid && (
Duración del plan
{billingOptions.map((option) => { const discount = getDiscount(selectedPlan, option.months); return ( ); })}
)} {selectedPlan && (
Detalle del importe

Resumen de facturación

{isCurrentPaid ? "El importe se calcula sobre la diferencia entre planes y los días restantes del ciclo actual." : "Importe final según el plan, duración y descuento seleccionados."}

{isCurrentPaid ? ( <>
Contexto del cambio
Plan actual {currentPlan.name}

{formatPrice(currentPlan.price)} / mes

Plan seleccionado {selectedPlan.name}

{formatPrice(selectedPlan.price)} / mes

Cálculo proporcional
Diferencia mensual entre planes {formatPrice(monthlyDifference)} / mes
Días restantes hasta el {formatDate(subscription.endDate)} {remainingDays} día{remainingDays === 1 ? "" : "s"}
Importe proporcional {formatPrice(proratedAmount)}

Se toma la diferencia mensual ({formatPrice(selectedPlan.price)} - {formatPrice(currentPlan.price)}) y se prorratea por 30 días × {remainingDays} día{remainingDays === 1 ? "" : "s"}.

TurnosXpress calcula este importe según las reglas de tu plan. El vencimiento no cambia: seguís con fecha {formatDate(subscription.endDate)}. Los nuevos límites se aplican cuando el pago quede aprobado.

) : ( <>
Contexto del cambio
Plan actual {currentPlan.name}

Plan gratis

Plan seleccionado {selectedPlan.name}

{formatPrice(selectedPlan.price)} / mes

Detalle de compra
Precio mensual {formatPrice(selectedPlan.price)}
Duración seleccionada {selectedMonths} mes{selectedMonths === 1 ? "" : "es"}
Subtotal {formatPrice(selectedSubtotal)}
{selectedDiscount > 0 ? `Descuento ${selectedDiscount}%` : "Descuento"} {selectedDiscount > 0 ? `-${formatPrice(selectedDiscountAmount)}` : "No aplica"}

Este es el importe final definido por TurnosXpress. {selectedDiscount > 0 ? `Incluye ${selectedDiscount}% de descuento por contratar ${selectedMonths} meses.` : "No hay descuentos aplicados para esta duración."} El plan se activa cuando el pago quede aprobado.

)}
)}
{isCurrentPaid ? "Total a pagar hoy" : "Total a pagar"} {selectedPlan ? isCurrentPaid ? formatPrice(proratedAmount) : formatPrice(selectedTotal) : "Seleccioná un plan"} {isCurrentPaid ? "TurnosXpress cobra solo la diferencia proporcional restante." : "Precio final según duración y descuentos disponibles."}
)}
); } export default function UpgradePlanPage() { return ( }> ); }