340 lines
9.6 KiB
JavaScript
340 lines
9.6 KiB
JavaScript
import { useEffect, useState } from 'react';
|
|
import { Link, useOutletContext } from 'react-router-dom';
|
|
import { api } from '../api.js';
|
|
import { pathForNextStep, userApi } from '../userApi.js';
|
|
|
|
/**
|
|
* @param {unknown} value
|
|
*/
|
|
function formatWhen(value) {
|
|
if (!value) {
|
|
return null;
|
|
}
|
|
|
|
const date = new Date(String(value));
|
|
return Number.isNaN(date.getTime()) ? String(value) : date.toLocaleString();
|
|
}
|
|
|
|
/**
|
|
* @param {string | null | undefined} status
|
|
*/
|
|
function agreementStatusLabel(status) {
|
|
switch (status) {
|
|
case 'APPROVED':
|
|
return 'Approved';
|
|
case 'SUBMITTED':
|
|
return 'Submitted — awaiting approval';
|
|
case 'REJECTED':
|
|
return 'Rejected';
|
|
case 'DRAFT':
|
|
return 'Draft';
|
|
default:
|
|
return 'Not started';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {{ status?: string | null, done?: boolean }} surveyStep
|
|
* @param {{ status?: string | null } | null} survey
|
|
*/
|
|
function surveyStatusLabel(surveyStep, survey) {
|
|
if (surveyStep?.done || survey?.status === 'COMPLETED') {
|
|
return 'Completed';
|
|
}
|
|
|
|
if (survey?.status === 'IN_PROGRESS' || surveyStep?.status === 'IN_PROGRESS') {
|
|
return 'In progress';
|
|
}
|
|
|
|
return 'Not started';
|
|
}
|
|
|
|
/**
|
|
* @param {string} label
|
|
* @returns {'completed' | 'in-process' | 'not-started' | 'rejected'}
|
|
*/
|
|
function stepStatusTone(label) {
|
|
const normalized = label.toLowerCase();
|
|
|
|
if (normalized === 'completed' || normalized === 'approved') {
|
|
return 'completed';
|
|
}
|
|
|
|
if (normalized === 'rejected') {
|
|
return 'rejected';
|
|
}
|
|
|
|
if (
|
|
normalized === 'not started' ||
|
|
normalized === 'not-started' ||
|
|
normalized === ''
|
|
) {
|
|
return 'not-started';
|
|
}
|
|
|
|
return 'in-process';
|
|
}
|
|
|
|
/**
|
|
* @param {{ label: string }} props
|
|
*/
|
|
function StepStatusBadge({ label }) {
|
|
return (
|
|
<span className={`user-step-status user-step-status-${stepStatusTone(label)}`}>
|
|
{label}
|
|
</span>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @param {{ status: string }} props
|
|
*/
|
|
function StatusBadge({ status }) {
|
|
const tone = status.toLowerCase();
|
|
return <span className={`user-status-badge user-status-${tone}`}>{status}</span>;
|
|
}
|
|
|
|
/**
|
|
* @param {{
|
|
* application: Record<string, unknown> | null | undefined,
|
|
* previewUrl: string,
|
|
* finalPdfUrl: string,
|
|
* chequeUrl: string,
|
|
* previewError: string,
|
|
* }} props
|
|
*/
|
|
function AgreementDetails({ application, previewUrl, finalPdfUrl, chequeUrl, previewError }) {
|
|
if (!application) {
|
|
return <p className="user-detail-empty">No agreement details yet.</p>;
|
|
}
|
|
|
|
const rows = [
|
|
['Agreement date', application.agreement_date],
|
|
['Full name', application.full_name],
|
|
['Age', application.age],
|
|
['Address', application.address],
|
|
['Registration no.', application.registration_no],
|
|
['Medical council', application.medical_council],
|
|
['PAN', application.pan],
|
|
['GSTIN', application.gstin || '—'],
|
|
['Bank name', application.bank_name],
|
|
['Account holder', application.account_holder],
|
|
['Account number', application.account_number],
|
|
['IFSC', application.ifsc],
|
|
];
|
|
|
|
return (
|
|
<div className="user-agreement-layout">
|
|
<div className="user-detail-card user-agreement-meta">
|
|
<div className="user-agreement-heading">
|
|
<h2 id="agreement-details-heading">Agreement details</h2>
|
|
{application.status ? <StatusBadge status={String(application.status)} /> : null}
|
|
</div>
|
|
<dl className="user-detail-grid">
|
|
{rows.map(([label, value]) =>
|
|
value === undefined || value === null || value === '' ? null : (
|
|
<div key={label}>
|
|
<dt>{label}</dt>
|
|
<dd>{String(value)}</dd>
|
|
</div>
|
|
),
|
|
)}
|
|
</dl>
|
|
{chequeUrl ? (
|
|
<div className="user-cheque-block">
|
|
<h3>Cancelled cheque</h3>
|
|
<img src={chequeUrl} alt="Cancelled cheque" className="user-cheque-image" />
|
|
</div>
|
|
) : null}
|
|
{finalPdfUrl ? (
|
|
<p className="user-detail-links">
|
|
<a href={finalPdfUrl} className="button" target="_blank" rel="noopener noreferrer">
|
|
Download final agreement PDF
|
|
</a>
|
|
</p>
|
|
) : null}
|
|
</div>
|
|
<div className="user-detail-card user-agreement-preview">
|
|
{previewError ? (
|
|
<p className="error" role="alert">
|
|
{previewError}
|
|
</p>
|
|
) : previewUrl ? (
|
|
<iframe className="preview-frame" title="Agreement PDF" src={previewUrl} />
|
|
) : (
|
|
<p className="user-detail-empty">No agreement PDF yet.</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @param {{
|
|
* survey: { status?: string | null, startedAt?: unknown, completedAt?: unknown } | null,
|
|
* surveyStep: { done?: boolean, status?: string | null },
|
|
* }} props
|
|
*/
|
|
function SurveyDetails({ survey, surveyStep }) {
|
|
const status = surveyStatusLabel(surveyStep, survey);
|
|
const started = formatWhen(survey?.startedAt);
|
|
const completed = formatWhen(survey?.completedAt);
|
|
|
|
return (
|
|
<dl className="user-detail-grid">
|
|
<div>
|
|
<dt>Status</dt>
|
|
<dd>
|
|
<StepStatusBadge label={status} />
|
|
</dd>
|
|
</div>
|
|
{started ? (
|
|
<div>
|
|
<dt>Started</dt>
|
|
<dd>{started}</dd>
|
|
</div>
|
|
) : null}
|
|
{completed ? (
|
|
<div>
|
|
<dt>Completed</dt>
|
|
<dd>{completed}</dd>
|
|
</div>
|
|
) : null}
|
|
</dl>
|
|
);
|
|
}
|
|
|
|
export default function UserDashboardPage() {
|
|
const me = useOutletContext();
|
|
const [surveyError, setSurveyError] = useState('');
|
|
const [surveyBusy, setSurveyBusy] = useState(false);
|
|
const [previewUrl, setPreviewUrl] = useState('');
|
|
const [finalPdfUrl, setFinalPdfUrl] = useState('');
|
|
const [chequeUrl, setChequeUrl] = useState('');
|
|
const [previewError, setPreviewError] = useState('');
|
|
const [application, setApplication] = useState(me.application);
|
|
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
|
|
// Fresh PDF/cheque URLs — layout-cached /api/me URLs expire after a short TTL.
|
|
Promise.all([userApi('/api/me'), api('/api/applications/me').catch(() => null)])
|
|
.then(([freshMe, appDocs]) => {
|
|
if (cancelled) {
|
|
return;
|
|
}
|
|
|
|
setApplication(freshMe.application || me.application);
|
|
const preview =
|
|
appDocs?.pdfUrl ||
|
|
appDocs?.finalPdfUrl ||
|
|
appDocs?.signedPdfUrl ||
|
|
freshMe.application?.finalPdfUrl ||
|
|
freshMe.application?.signedPdfUrl ||
|
|
'';
|
|
setPreviewUrl(preview ? String(preview) : '');
|
|
setFinalPdfUrl(
|
|
String(appDocs?.finalPdfUrl || freshMe.application?.finalPdfUrl || ''),
|
|
);
|
|
setChequeUrl(
|
|
String(appDocs?.cancelledChequeUrl || freshMe.application?.cancelledChequeUrl || ''),
|
|
);
|
|
setPreviewError('');
|
|
})
|
|
.catch((error) => {
|
|
if (!cancelled) {
|
|
setPreviewError(error.message);
|
|
}
|
|
});
|
|
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, [me.application]);
|
|
|
|
async function openSurvey() {
|
|
setSurveyBusy(true);
|
|
setSurveyError('');
|
|
|
|
try {
|
|
const link = await userApi('/api/survey/me/link', { method: 'POST' });
|
|
window.location.assign(link.url);
|
|
} catch (error) {
|
|
setSurveyError(error.message);
|
|
setSurveyBusy(false);
|
|
}
|
|
}
|
|
|
|
const agreementLabel = me.steps.agreement.done
|
|
? 'Completed'
|
|
: agreementStatusLabel(application?.status || me.application?.status);
|
|
|
|
const surveyLabel = surveyStatusLabel(me.steps.survey, me.survey);
|
|
|
|
return (
|
|
<section className="user-dashboard">
|
|
<h1>Dashboard</h1>
|
|
<p className="page-lead">Signed in as {me.user.email}</p>
|
|
|
|
<div className="user-step-list">
|
|
<div className={`user-step ${me.steps.consent.done ? 'done' : 'pending'}`}>
|
|
<strong>1. Consent (PIS / ICF)</strong>
|
|
<StepStatusBadge label={me.steps.consent.done ? 'Completed' : 'Not started'} />
|
|
</div>
|
|
<div className={`user-step ${me.steps.agreement.done ? 'done' : 'pending'}`}>
|
|
<strong>2. Physician agreement</strong>
|
|
<StepStatusBadge label={agreementLabel} />
|
|
</div>
|
|
<div className={`user-step ${me.steps.survey.done ? 'done' : 'pending'}`}>
|
|
<strong>3. Study questionnaire</strong>
|
|
<StepStatusBadge label={surveyLabel} />
|
|
</div>
|
|
</div>
|
|
|
|
<section aria-labelledby="agreement-details-heading">
|
|
<AgreementDetails
|
|
application={application || me.application}
|
|
previewUrl={previewUrl}
|
|
finalPdfUrl={finalPdfUrl}
|
|
chequeUrl={chequeUrl}
|
|
previewError={previewError}
|
|
/>
|
|
</section>
|
|
|
|
<section className="user-detail-card" aria-labelledby="survey-details-heading">
|
|
<h2 id="survey-details-heading">Survey details</h2>
|
|
<SurveyDetails survey={me.survey} surveyStep={me.steps.survey} />
|
|
</section>
|
|
|
|
{surveyError ? (
|
|
<p className="error" role="alert">
|
|
{surveyError}
|
|
</p>
|
|
) : null}
|
|
|
|
{me.nextStep === 'consent' || me.nextStep === 'agreement' || me.nextStep === 'survey' ? (
|
|
<>
|
|
<p className="user-next-hint">
|
|
Next step: <strong>{me.nextStep}</strong>
|
|
</p>
|
|
{me.nextStep === 'survey' ? (
|
|
<p>
|
|
<button type="button" onClick={openSurvey} disabled={surveyBusy}>
|
|
{surveyBusy ? 'Opening survey…' : 'Continue to survey'}
|
|
</button>
|
|
</p>
|
|
) : (
|
|
<p>
|
|
<Link to={pathForNextStep(me.nextStep)}>Continue to {me.nextStep}</Link>
|
|
</p>
|
|
)}
|
|
</>
|
|
) : (
|
|
<p className="banner" role="status">
|
|
All steps are complete. Thank you for participating.
|
|
</p>
|
|
)}
|
|
</section>
|
|
);
|
|
}
|