feat: add manual subscription finalization functionality and support for expired plan notifications
This commit is contained in:
@@ -109,6 +109,23 @@ export const extendUserSubscription = async (data: ExtendUserSubscriptionPayload
|
||||
return res.data;
|
||||
};
|
||||
|
||||
export type FinalizeUserSubscriptionPayload = {
|
||||
userId: string;
|
||||
reason?: string;
|
||||
};
|
||||
|
||||
export type FinalizeUserSubscriptionResult = {
|
||||
userId: string;
|
||||
subscriptionId: string;
|
||||
previousEndDate: string;
|
||||
newEndDate: string;
|
||||
};
|
||||
|
||||
export const finalizeUserSubscription = async (data: FinalizeUserSubscriptionPayload): Promise<FinalizeUserSubscriptionResult> => {
|
||||
const res = await sysadminApi.post('/users/finalize-subscription', data);
|
||||
return res.data;
|
||||
};
|
||||
|
||||
export type Plan = {
|
||||
id?: string;
|
||||
_id?: string;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { CreditCard, X } from 'lucide-react';
|
||||
import { extendUserSubscription, getUserSubscriptionDetails, type UserSubscriptionDetails } from '../../api/sysadmin';
|
||||
import { extendUserSubscription, finalizeUserSubscription, getUserSubscriptionDetails, type UserSubscriptionDetails } from '../../api/sysadmin';
|
||||
import { formatDateTime } from '../../utils/formatters';
|
||||
|
||||
type Props = {
|
||||
@@ -17,6 +17,7 @@ export const SubscriptionDetailsModal = ({ user, onClose }: Props) => {
|
||||
const [extensionLoading, setExtensionLoading] = useState(false);
|
||||
const [extensionMessage, setExtensionMessage] = useState('');
|
||||
const [extensionError, setExtensionError] = useState('');
|
||||
const [finalizeLoading, setFinalizeLoading] = useState(false);
|
||||
|
||||
const formatAmount = (value?: number) => {
|
||||
if (typeof value !== 'number') return '-';
|
||||
@@ -61,6 +62,29 @@ export const SubscriptionDetailsModal = ({ user, onClose }: Props) => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleFinalize = async () => {
|
||||
if (!details?.currentSubscription) return;
|
||||
|
||||
const confirmed = window.confirm('Esto establecerá el vencimiento de la suscripción a ayer para forzar el comportamiento de plan vencido. No elimina datos ni cambia el plan directamente. ¿Continuar?');
|
||||
if (!confirmed) return;
|
||||
|
||||
setFinalizeLoading(true);
|
||||
setExtensionError('');
|
||||
setExtensionMessage('');
|
||||
try {
|
||||
const result = await finalizeUserSubscription({
|
||||
userId: user._id,
|
||||
reason: 'Finalización manual desde sysadmin-cli para probar expiración de plan',
|
||||
});
|
||||
setExtensionMessage(`Período finalizado. El vencimiento quedó en ${formatDateTime(result.newEndDate)} para forzar la expiración en la próxima lectura.`);
|
||||
await loadDetails();
|
||||
} catch (e: any) {
|
||||
setExtensionError(e.response?.data?.message || e.message || 'Error finalizando período');
|
||||
} finally {
|
||||
setFinalizeLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const subscription = details?.currentSubscription;
|
||||
|
||||
return (
|
||||
@@ -118,10 +142,10 @@ export const SubscriptionDetailsModal = ({ user, onClose }: Props) => {
|
||||
Extiende la vigencia sin pago y registra un pago de bonificación por $0 asociado a esta suscripción.
|
||||
</p>
|
||||
<div className="extension-actions">
|
||||
<button className="btn btn-secondary" disabled={extensionLoading} onClick={() => handleExtend({ mode: 'add_months', months: 1 })}>
|
||||
<button className="btn btn-secondary" disabled={extensionLoading || finalizeLoading} onClick={() => handleExtend({ mode: 'add_months', months: 1 })}>
|
||||
Extender 1 mes
|
||||
</button>
|
||||
<button className="btn btn-secondary" disabled={extensionLoading} onClick={() => handleExtend({ mode: 'add_months', months: 2 })}>
|
||||
<button className="btn btn-secondary" disabled={extensionLoading || finalizeLoading} onClick={() => handleExtend({ mode: 'add_months', months: 2 })}>
|
||||
Extender 2 meses
|
||||
</button>
|
||||
<div className="extension-date-action">
|
||||
@@ -129,18 +153,27 @@ export const SubscriptionDetailsModal = ({ user, onClose }: Props) => {
|
||||
type="date"
|
||||
value={extensionDate}
|
||||
onChange={(event) => setExtensionDate(event.target.value)}
|
||||
disabled={extensionLoading}
|
||||
disabled={extensionLoading || finalizeLoading}
|
||||
/>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
disabled={extensionLoading || !extensionDate}
|
||||
disabled={extensionLoading || finalizeLoading || !extensionDate}
|
||||
onClick={() => handleExtend({ mode: 'set_end_date', endDate: extensionDate })}
|
||||
>
|
||||
Establecer vencimiento
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{extensionLoading && <p style={{ color: 'var(--text-muted)', marginTop: '0.75rem' }}>Aplicando ajuste...</p>}
|
||||
<div className="finalize-action">
|
||||
<div>
|
||||
<h4>Finalizar período</h4>
|
||||
<p>Establece el vencimiento en ayer para forzar el comportamiento de suscripción vencida en la próxima lectura. No elimina datos ni cambia el plan directamente.</p>
|
||||
</div>
|
||||
<button className="btn btn-danger" disabled={extensionLoading || finalizeLoading} onClick={handleFinalize}>
|
||||
Finalizar período
|
||||
</button>
|
||||
</div>
|
||||
{(extensionLoading || finalizeLoading) && <p style={{ color: 'var(--text-muted)', marginTop: '0.75rem' }}>Aplicando ajuste...</p>}
|
||||
{extensionMessage && <p style={{ color: 'var(--success)', marginTop: '0.75rem' }}>{extensionMessage}</p>}
|
||||
{extensionError && <p style={{ color: 'var(--danger)', marginTop: '0.75rem' }}>{extensionError}</p>}
|
||||
</section>
|
||||
@@ -226,6 +259,30 @@ export const SubscriptionDetailsModal = ({ user, onClose }: Props) => {
|
||||
color: var(--text-main);
|
||||
padding: 0.65rem 0.75rem;
|
||||
}
|
||||
.finalize-action {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
margin-top: 1rem;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid var(--glass-border);
|
||||
}
|
||||
.finalize-action h4 {
|
||||
color: var(--danger);
|
||||
font-size: 1rem;
|
||||
margin-bottom: 0.35rem;
|
||||
}
|
||||
.finalize-action p {
|
||||
color: var(--text-muted);
|
||||
margin: 0;
|
||||
}
|
||||
@media (max-width: 640px) {
|
||||
.finalize-action {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
`}} />
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user