PPPAIMT-Crest/frontend/src/admin/ApplicationDetailPage.jsx

228 lines
7.1 KiB
JavaScript

import { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import { adminApi } from './adminApi.js';
import Modal from './Modal.jsx';
import StatusBadge from './StatusBadge.jsx';
const FIELDS = [
['full_name', 'Name'],
['email', 'Email'],
['agreement_date', 'Agreement date'],
['age', 'Age'],
['address', 'Address'],
['registration_no', 'Registration number'],
['medical_council', 'Medical council'],
['pan', 'PAN'],
['gstin', 'GSTIN'],
['bank_name', 'Bank'],
['account_holder', 'Account holder'],
['account_number', 'Account number'],
['ifsc', 'IFSC'],
['rejection_reason', 'Rejection reason'],
];
export default function ApplicationDetailPage() {
const { id } = useParams();
const [application, setApplication] = useState(null);
const [error, setError] = useState('');
const [confirming, setConfirming] = useState(false);
const [rejecting, setRejecting] = useState(false);
const [reason, setReason] = useState('');
const [busy, setBusy] = useState(false);
useEffect(() => {
adminApi(`/api/admin/applications/${id}`)
.then(setApplication)
.catch((loadError) => setError(loadError.message));
}, [id]);
async function resendSurveyLink() {
setBusy(true);
setError('');
try {
setApplication(
await adminApi(`/api/admin/applications/${id}/resend-survey-link`, { method: 'POST' }),
);
} catch (resendError) {
setError(resendError.message);
} finally {
setBusy(false);
}
}
async function approve() {
setBusy(true);
setError('');
try {
setApplication(await adminApi(`/api/admin/applications/${id}/approve`, { method: 'POST' }));
setConfirming(false);
} catch (approveError) {
setError(approveError.message);
} finally {
setBusy(false);
}
}
async function reject(event) {
event.preventDefault();
setBusy(true);
setError('');
try {
setApplication(
await adminApi(`/api/admin/applications/${id}/reject`, {
method: 'POST',
body: { reason },
}),
);
setRejecting(false);
setReason('');
} catch (rejectError) {
setError(rejectError.message);
} finally {
setBusy(false);
}
}
if (!application && !error) {
return <p>Loading application…</p>;
}
if (!application) {
return <p className="text-red-700">{error}</p>;
}
const canDecide = application.status === 'SUBMITTED';
return (
<div>
<div className="mb-4 flex flex-wrap items-center gap-3">
<h1 className="admin-title admin-title-inline">{application.full_name || 'Application'}</h1>
<StatusBadge status={application.status} />
</div>
{error ? <p className="mb-3 text-red-700">{error}</p> : null}
<div className="grid gap-4 lg:grid-cols-2">
<div className="admin-card">
<dl className="grid gap-2 text-sm">
{FIELDS.map(([key, label]) => (
<div key={key}>
<dt className="text-stone-500">{label}</dt>
<dd className="m-0">{application[key] || '—'}</dd>
</div>
))}
</dl>
<h2 className="mt-6 font-semibold text-[#134e4a]">Signature</h2>
{application.signature ? (
<p className="text-sm">
{application.signature.type} at {application.signature.signedAt}
<br />
IP {application.signature.ip || '—'}
<br />
{application.signature.userAgent || 'Device not recorded'}
</p>
) : (
<p className="text-sm">No signature</p>
)}
{canDecide ? (
<div className="mt-4 flex gap-2">
<button
type="button"
className="admin-btn admin-btn-primary"
onClick={() => setConfirming(true)}
>
Approve
</button>
<button
type="button"
className="admin-btn admin-btn-danger"
onClick={() => setRejecting(true)}
>
Reject
</button>
</div>
) : null}
<h2 className="mt-6 font-semibold text-[#134e4a]">Events</h2>
<ol className="mt-2 list-disc pl-5 text-sm">
{application.audit.map((event) => (
<li key={event.id}>
{event.action} · {event.createdAt}
</li>
))}
</ol>
<h2 className="mt-6 font-semibold text-[#134e4a]">Emails</h2>
{application.emails.some(
(email) => email.status === 'bounced' || email.status === 'dropped',
) ? (
<p className="mt-2 rounded bg-amber-100 px-3 py-2 text-sm" role="status">
An email bounced or was dropped. Check the address before sending again.
</p>
) : null}
<ol className="mt-2 list-disc pl-5 text-sm">
{application.emails.map((email) => (
<li key={email.id}>
{email.template} to {email.to_email} · {email.status} · {email.created_at}
</li>
))}
</ol>
{application.status === 'APPROVED' ? (
<button
type="button"
className="admin-btn admin-btn-primary mt-3 text-sm"
disabled={busy}
onClick={resendSurveyLink}
>
{busy ? 'Sending…' : 'Resend survey link'}
</button>
) : null}
</div>
<div className="admin-card min-h-[70vh] p-2">
{application.pdfUrl ? (
<iframe title="Agreement PDF" className="h-[70vh] w-full" src={application.pdfUrl} />
) : (
<p className="p-4 text-sm">No PDF yet</p>
)}
</div>
</div>
{confirming ? (
<Modal title="Approve this agreement?" onClose={() => setConfirming(false)}>
<p className="mb-3 text-sm">This adds the CREST signature and emails the survey link.</p>
<button
type="button"
className="admin-btn admin-btn-primary"
disabled={busy}
onClick={approve}
>
{busy ? 'Approving…' : 'Approve'}
</button>
</Modal>
) : null}
{rejecting ? (
<Modal title="Reject this agreement" onClose={() => setRejecting(false)}>
<form onSubmit={reject}>
<label className="mb-3 grid gap-1 text-sm" htmlFor="reject-reason">
Reason
<textarea
id="reject-reason"
required
minLength={10}
className="admin-input min-h-24"
value={reason}
onChange={(event) => setReason(event.target.value)}
/>
</label>
<button
type="submit"
className="admin-btn admin-btn-danger"
disabled={busy}
>
{busy ? 'Rejecting…' : 'Reject'}
</button>
</form>
</Modal>
) : null}
</div>
);
}