PPPAIMT-Crest/frontend/src/pages/UserDashboardPage.jsx

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>
);
}