feat: support editing plan details and deleting drafts with backend endpoints

This commit is contained in:
Luis Gabriel Ramos Robles 2026-06-14 02:22:04 +00:00
parent bff2c3974e
commit 08fe3954a2
5 changed files with 141 additions and 18 deletions

View file

@ -149,3 +149,54 @@ export const PUT = withAuth(async (req, { session, prisma, params }) => {
return NextResponse.json({ plan: updatedPlan, versioned: false });
}
}, ['admin', 'director']);
// DELETE: Delete a plan (restricted to admin and director, and only allowed for DRAFT plans)
export const DELETE = withAuth(async (req, { session, prisma, params }) => {
const unwrappedParams = await params;
const id = parseInt(unwrappedParams.id);
if (isNaN(id)) {
return NextResponse.json({ error: 'ID no válido.' }, { status: 400 });
}
const plan = await prisma.compensationPlan.findUnique({
where: { id },
include: { settlements: true }
});
if (!plan) {
return NextResponse.json({ error: 'Plan no encontrado.' }, { status: 404 });
}
if (plan.status !== 'DRAFT') {
return NextResponse.json(
{ error: 'Solo se pueden eliminar planes en estado BORRADOR (DRAFT).' },
{ status: 400 }
);
}
if (plan.settlements && plan.settlements.length > 0) {
return NextResponse.json(
{ error: 'No se puede eliminar el plan porque tiene liquidaciones asociadas.' },
{ status: 400 }
);
}
await prisma.$transaction(async (tx: any) => {
await createAuditLog(tx, {
userId: session.userId,
action: 'DELETE',
targetTable: 'compensation_plans',
targetId: id,
previousValue: plan,
newValue: null,
ipAddress: req.headers.get('x-forwarded-for') || '127.0.0.1'
});
await tx.compensationPlan.delete({
where: { id }
});
});
return NextResponse.json({ success: true });
}, ['admin', 'director']);

View file

@ -230,6 +230,7 @@
.cardFooter {
display: flex;
flex-wrap: wrap;
gap: var(--space-2);
margin-top: auto;
}

View file

@ -43,6 +43,22 @@ export default function PlansPage() {
const [maxCap, setMaxCap] = useState('');
const [status, setStatus] = useState('DRAFT');
const [error, setError] = useState<string | null>(null);
const [editPlanId, setEditPlanId] = useState<number | null>(null);
const handleCloseModal = () => {
setName('');
setCode('');
setValidityStart('');
setValidityEnd('');
setType('PERCENTAGE');
setMetaAmount('');
setPercentageRate('');
setMaxCap('');
setStatus('DRAFT');
setEditPlanId(null);
setError(null);
setShowModal(false);
};
const fetchPlans = async () => {
try {
@ -80,8 +96,10 @@ export default function PlansPage() {
}
try {
const res = await fetch('/api/plans', {
method: 'POST',
const url = editPlanId ? `/api/plans/${editPlanId}` : '/api/plans';
const method = editPlanId ? 'PUT' : 'POST';
const res = await fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name,
@ -99,27 +117,53 @@ export default function PlansPage() {
const data = await res.json();
if (!res.ok) {
setError(data.error || 'Failed to create plan');
setError(data.error || 'Failed to submit plan');
return;
}
// Reset form & reload
setName('');
setCode('');
setValidityStart('');
setValidityEnd('');
setType('PERCENTAGE');
setMetaAmount('');
setPercentageRate('');
setMaxCap('');
setStatus('DRAFT');
setShowModal(false);
handleCloseModal();
fetchPlans();
} catch (err) {
setError('An error occurred during submission.');
}
};
const handleEditClick = (plan: Plan) => {
setName(plan.name);
setCode(plan.code);
setValidityStart(plan.validityStart ? plan.validityStart.substring(0, 10) : '');
setValidityEnd(plan.validityEnd ? plan.validityEnd.substring(0, 10) : '');
setType(plan.type);
setMetaAmount(plan.metaAmount ? plan.metaAmount.toString() : '');
setPercentageRate(plan.percentageRate ? plan.percentageRate.toString() : '');
setMaxCap(plan.maxCap ? plan.maxCap.toString() : '');
setStatus(plan.status);
setEditPlanId(plan.id);
setShowModal(true);
};
const handleDeletePlan = async (planId: number) => {
if (!confirm(t('plans.confirmDelete'))) {
return;
}
try {
const res = await fetch(`/api/plans/${planId}`, {
method: 'DELETE'
});
if (!res.ok) {
const data = await res.json();
alert(data.error || 'Failed to delete plan');
return;
}
fetchPlans();
} catch (err) {
alert('An error occurred while deleting the plan.');
}
};
const handleToggleStatus = async (plan: Plan) => {
const nextStatus = plan.status === 'ACTIVE' ? 'INACTIVE' : 'ACTIVE';
try {
@ -207,6 +251,23 @@ export default function PlansPage() {
>
{plan.status === 'ACTIVE' ? t('plans.deactivateVersion') : t('plans.activate')}
</button>
<button
onClick={() => handleEditClick(plan)}
className={styles.btnSecondary}
id={`btn-edit-${plan.id}`}
>
{t('plans.editPlan')}
</button>
{plan.status === 'DRAFT' && (
<button
onClick={() => handleDeletePlan(plan.id)}
className={styles.btnSecondary}
style={{ borderColor: 'hsl(0, 75%, 60%)', color: 'hsl(0, 75%, 60%)' }}
id={`btn-delete-${plan.id}`}
>
{t('plans.deletePlan')}
</button>
)}
</>
) : (
<Link href={`/plans/${plan.id}/rules`} className={styles.btnSecondary} id={`btn-rules-${plan.id}`}>
@ -224,7 +285,7 @@ export default function PlansPage() {
{showModal && (
<div className={styles.modalOverlay} role="dialog">
<div className={styles.modal}>
<h2 className={styles.modalTitle}>Crear {t('plans.newPlan')}</h2>
<h2 className={styles.modalTitle}>{editPlanId ? t('plans.modalEditTitle') : t('plans.modalCreateTitle')}</h2>
<form onSubmit={handleCreatePlan} className={styles.form}>
{error && <div className={styles.errorText}>{error}</div>}
@ -332,7 +393,7 @@ export default function PlansPage() {
</div>
<div className={styles.formActions}>
<button type="button" onClick={() => setShowModal(false)} className={styles.btnSecondary}>
<button type="button" onClick={handleCloseModal} className={styles.btnSecondary}>
{t('plans.modalCancel')}
</button>
<button type="submit" className={styles.btnPrimary} id="btn-save-plan">

View file

@ -149,7 +149,12 @@
},
"errorFind": "Could not find the plan.",
"errorLoad": "Error loading plan information.",
"modalValidityEnd": "End Validity (Optional)"
"modalValidityEnd": "End Validity (Optional)",
"editPlan": "Edit Details",
"deletePlan": "Delete Plan",
"confirmDelete": "Are you sure you want to delete this plan? This action cannot be undone.",
"modalEditTitle": "Edit Plan",
"modalCreateTitle": "Create New Plan"
},
"rules": {
"back": "Back to Plans",

View file

@ -149,7 +149,12 @@
},
"errorFind": "No se pudo encontrar el plan.",
"errorLoad": "Error al cargar la información del plan.",
"modalValidityEnd": "Fin de Vigencia (Opcional)"
"modalValidityEnd": "Fin de Vigencia (Opcional)",
"editPlan": "Editar Detalles",
"deletePlan": "Eliminar Plan",
"confirmDelete": "¿Está seguro de que desea eliminar este plan? Esta acción no se puede deshacer.",
"modalEditTitle": "Editar Plan",
"modalCreateTitle": "Crear Nuevo Plan"
},
"rules": {
"back": "Volver a Planes",