228 lines
7.1 KiB
JavaScript
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>
|
|
);
|
|
}
|