feat: add manual subscription finalization functionality and support for expired plan notifications
This commit is contained in:
+2
-1
@@ -11,4 +11,5 @@ NEXT_PUBLIC_DATAGRID_PAGE_SIZE = 10
|
||||
|
||||
NEXT_PUBLIC_SLOGAN = Tu tiempo en orden.
|
||||
|
||||
NEXT_PUBLIC_BASE_URL = https://turnosxpress.com.ar
|
||||
NEXT_PUBLIC_BASE_URL = https://turnosxpress.com.ar
|
||||
NEXT_PUBLIC_INFO_EMAIL = info@turnosxpress.com.ar
|
||||
+2
-1
@@ -11,4 +11,5 @@ NEXT_PUBLIC_DATAGRID_PAGE_SIZE = 10
|
||||
|
||||
NEXT_PUBLIC_SLOGAN = Tu tiempo en orden.
|
||||
|
||||
NEXT_PUBLIC_BASE_URL = https://turnosxpress.com.ar
|
||||
NEXT_PUBLIC_BASE_URL = https://turnosxpress.com.ar
|
||||
NEXT_PUBLIC_INFO_EMAIL = info@turnosxpress.com.ar
|
||||
@@ -63,6 +63,7 @@ export type VerifyPendingPlanPaymentResponse = {
|
||||
|
||||
export type LastPlanPaymentStatus = "rejected" | "failed" | "cancelled";
|
||||
export type LastPlanPaymentType = "new" | "extension" | "upgrade";
|
||||
export type DowngradeReason = "expired";
|
||||
|
||||
export interface GetSuscriptionInitPointResponse {
|
||||
init_point: string;
|
||||
@@ -84,6 +85,11 @@ export interface ISubscriptionInfo {
|
||||
lastPaymentPreferenceId?: string;
|
||||
lastPaymentAt?: Date;
|
||||
lastPaymentType?: LastPlanPaymentType;
|
||||
downgradedFromPlanId?: string;
|
||||
downgradedFromPlanName?: string;
|
||||
downgradedFromPlanCode?: string;
|
||||
downgradedAt?: Date;
|
||||
downgradeReason?: DowngradeReason;
|
||||
}
|
||||
export const DefaultSubscriptionModel: ISubscriptionInfo = {
|
||||
id: "",
|
||||
|
||||
@@ -278,6 +278,43 @@
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.expiredDowngradeNotice {
|
||||
margin: 0;
|
||||
padding: 18px 20px;
|
||||
border: 1px solid rgba(194, 65, 12, 0.24);
|
||||
border-radius: 18px;
|
||||
background: #fff7ed;
|
||||
color: #7c2d12;
|
||||
box-shadow: 0 12px 28px rgba(194, 65, 12, 0.08);
|
||||
}
|
||||
|
||||
.expiredDowngradeNotice span {
|
||||
display: inline-flex;
|
||||
width: fit-content;
|
||||
margin-bottom: 8px;
|
||||
padding: 5px 11px;
|
||||
border-radius: 999px;
|
||||
background: rgba(194, 65, 12, 0.12);
|
||||
color: #9a3412;
|
||||
font-size: 12px;
|
||||
font-weight: 900;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.expiredDowngradeNotice strong {
|
||||
display: block;
|
||||
color: #7c2d12;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.expiredDowngradeNotice p {
|
||||
margin: 8px 0 0;
|
||||
color: #9a3412;
|
||||
line-height: 1.55;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -107,6 +107,11 @@ export default function CurrentPlanPage() {
|
||||
const plan = subscription?.plan;
|
||||
const isPaidPlan = Boolean(plan && plan.price > 0);
|
||||
const statusText = isPaidPlan ? getSubStatusText(subscription?.mpStatus) : "ACTIVO";
|
||||
const showExpiredDowngradeNotice = Boolean(
|
||||
isOwner &&
|
||||
subscription?.downgradeReason === "expired" &&
|
||||
subscription.downgradedAt
|
||||
);
|
||||
const canRenew = Boolean(
|
||||
isOwner &&
|
||||
isPaidPlan &&
|
||||
@@ -216,6 +221,17 @@ export default function CurrentPlanPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showExpiredDowngradeNotice && (
|
||||
<div className={styles.expiredDowngradeNotice}>
|
||||
<span>Plan vencido</span>
|
||||
<strong>Tu plan pago venció y esta organización pasó al plan básico.</strong>
|
||||
<p>
|
||||
No borramos tu organización ni tus datos. Los límites cambiaron y las nuevas acciones se validan con el plan básico.
|
||||
{subscription.downgradedFromPlanName && ` Tu plan anterior era ${subscription.downgradedFromPlanName}.`}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isPaidPlan && (
|
||||
<p className={styles.notice}>Estás usando el plan gratis. No tiene vencimiento pago.</p>
|
||||
)}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
"use client";
|
||||
import MetricGraph from "@components/MetricGraph/MetricGraph";
|
||||
import AnimatedContainer from "@core/app/components/AnimatedConainer/AnimatedContainer";
|
||||
import Button from "@core/app/components/Button/Button";
|
||||
import { useNavigation } from "@core/app/hooks/goto";
|
||||
@@ -36,7 +35,6 @@ 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 { isNull } from "@core/app/helpers/IsNull";
|
||||
|
||||
import StorefrontOutlinedIcon from "@mui/icons-material/StorefrontOutlined";
|
||||
import NotificationsNoneOutlinedIcon from "@mui/icons-material/NotificationsNoneOutlined";
|
||||
@@ -56,16 +54,8 @@ function DashboardContent() {
|
||||
const eventHandler = useEventHandlerStore();
|
||||
const alert = useAlert();
|
||||
const confirm = useConfirmStore();
|
||||
const infoEmail = process.env.NEXT_PUBLIC_INFO_EMAIL || "info@turnosxpress.com.ar";
|
||||
|
||||
const [metrics, setMetrics] = useState({
|
||||
organizationsCount: 0,
|
||||
employeesCount: 0,
|
||||
clientsCount: 0,
|
||||
appointmentsCount: 0,
|
||||
servicesCount: 0,
|
||||
repeatsCount: 0,
|
||||
});
|
||||
|
||||
const [showOnlyMine, setShowOnlyMine] = useState(false);
|
||||
const [isAgendaCollapsed, setIsAgendaCollapsed] = useState(true);
|
||||
|
||||
@@ -164,6 +154,7 @@ function DashboardContent() {
|
||||
const loadCompanyMetrics = async (companyId: string) => {
|
||||
setCompanySubscription(null);
|
||||
setCompanyMetrics(null);
|
||||
setIsCompanyOwner(false);
|
||||
try {
|
||||
const res = await getCompanyMetrics({ sessionUser: SessionInfo.userId, companyId });
|
||||
setCompanyMetrics(res.metrics);
|
||||
@@ -315,6 +306,7 @@ function DashboardContent() {
|
||||
};
|
||||
|
||||
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);
|
||||
@@ -325,11 +317,30 @@ function DashboardContent() {
|
||||
displayedSubscription.lastPaymentAt &&
|
||||
dayjs().diff(dayjs(displayedSubscription.lastPaymentAt), "day") <= 14
|
||||
);
|
||||
const showPaymentAction = Boolean(
|
||||
(displayedSubscription?.mpStatus === MP_SUBS_STATUS.PENDING || hasPendingPaymentMetadata) &&
|
||||
(isCompanyOwner || isUsingSessionSubscriptionFallback)
|
||||
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 showFreePlanStarterContent = Boolean(displayedPlan && displayedPlan.price === 0 && organizations.length === 0);
|
||||
const agendaItems = dashboardData?.personalAgenda ?? [];
|
||||
const filteredAgendaItems = agendaItems.filter(item => !showOnlyMine || item.collaboratorId === SessionInfo.userId);
|
||||
@@ -365,7 +376,12 @@ function DashboardContent() {
|
||||
<div style={{ position: "absolute", top: "20px", right: "20px", display: "flex", alignItems: "center", gap: "10px" }}>
|
||||
<select
|
||||
value={selectedOrgId}
|
||||
onChange={e => setSelectedOrgId(e.target.value)}
|
||||
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 => (
|
||||
@@ -435,6 +451,7 @@ function DashboardContent() {
|
||||
<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}>
|
||||
@@ -482,6 +499,25 @@ function DashboardContent() {
|
||||
</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>
|
||||
)}
|
||||
|
||||
{showFreePlanStarterContent && (
|
||||
<div className={style.starterCard}>
|
||||
<div className={style.starterContent}>
|
||||
@@ -649,7 +685,7 @@ function DashboardContent() {
|
||||
)}
|
||||
|
||||
{/* Repeats Pending Card */}
|
||||
{dashboardData && selectedOrgId && (
|
||||
{dashboardData && selectedOrgId && canLoadPendingRepeats && (
|
||||
<PendingRepeatsCard
|
||||
companyId={selectedOrgId}
|
||||
allowEmployeeFilter={true}
|
||||
|
||||
@@ -185,6 +185,28 @@
|
||||
box-shadow: 0 12px 28px rgba(15, 23, 42, 0.06);
|
||||
}
|
||||
|
||||
.expiredDowngradeCard {
|
||||
grid-column: 1 / -1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 24px;
|
||||
padding: 26px 30px;
|
||||
border-radius: 18px;
|
||||
background: #fff7ed;
|
||||
border: 1px solid rgba(194, 65, 12, 0.24);
|
||||
box-shadow: 0 12px 28px rgba(194, 65, 12, 0.08);
|
||||
color: #7c2d12;
|
||||
}
|
||||
|
||||
.expiredDowngradeCard .pendingPaymentContent h2 {
|
||||
color: #7c2d12;
|
||||
}
|
||||
|
||||
.expiredDowngradeCard .pendingPaymentContent p {
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
.pendingPaymentContent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -216,6 +238,35 @@
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.expiredDowngradeEyebrow {
|
||||
width: fit-content;
|
||||
padding: 5px 11px;
|
||||
border-radius: 999px;
|
||||
background: rgba(194, 65, 12, 0.12);
|
||||
color: #9a3412;
|
||||
font-size: 12px;
|
||||
font-weight: 900;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.expiredDowngradeButton {
|
||||
flex: 0 0 auto;
|
||||
border: 1px solid rgba(124, 45, 18, 0.22);
|
||||
border-radius: 14px;
|
||||
background: #7c2d12;
|
||||
color: #fff7ed;
|
||||
cursor: pointer;
|
||||
font-weight: 900;
|
||||
padding: 14px 18px;
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.expiredDowngradeButton:hover {
|
||||
box-shadow: 0 10px 24px rgba(124, 45, 18, 0.18);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.pendingPaymentContent h2 {
|
||||
margin: 0;
|
||||
color: var(--black);
|
||||
@@ -228,6 +279,13 @@
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.pendingPaymentLink {
|
||||
color: #9a4b00;
|
||||
font-weight: 800;
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 3px;
|
||||
}
|
||||
|
||||
.pendingPaymentActions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -433,6 +491,16 @@
|
||||
padding: 22px;
|
||||
}
|
||||
|
||||
.expiredDowngradeCard {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
padding: 22px;
|
||||
}
|
||||
|
||||
.expiredDowngradeButton {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.pendingPaymentActions {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
Reference in New Issue
Block a user