diff --git a/backend/src/pdf/agreementHtml.js b/backend/src/pdf/agreementHtml.js
index 46a611c..d8f2eb6 100644
--- a/backend/src/pdf/agreementHtml.js
+++ b/backend/src/pdf/agreementHtml.js
@@ -2,20 +2,13 @@ import { readFileSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
-const ROOT = path.dirname(fileURLToPath(import.meta.url));
-const AGREEMENT_PATH = path.resolve(ROOT, '../../../files/docs/agreement.md');
-const LOGO_PATH = path.resolve(ROOT, '../../../frontend/src/images/CREST_logo.jpg');
+const AGREEMENT_PATH = path.resolve(
+ path.dirname(fileURLToPath(import.meta.url)),
+ '../../../files/docs/agreement.md',
+);
const sourceText = readFileSync(AGREEMENT_PATH, 'utf8');
-/** @type {string} */
-let crestLogoDataUrl = '';
-try {
- crestLogoDataUrl = `data:image/jpeg;base64,${readFileSync(LOGO_PATH).toString('base64')}`;
-} catch {
- crestLogoDataUrl = '';
-}
-
const MONTHS = [
'January',
'February',
@@ -87,18 +80,6 @@ function physicianSignatureHtml(signature) {
return '___________________';
}
-/**
- * @param {string} signatureMarkup
- * @param {{ withLogo?: boolean }} [options]
- */
-function signatureRow(signatureMarkup, options = {}) {
- const logo =
- options.withLogo && crestLogoDataUrl
- ? ``
- : '';
- return `Signature: ${signatureMarkup}${logo}`;
-}
-
/**
* @param {{ name: string, designation: string, signatureDataUrl?: string } | null} signatory
* @param {string} dateLabel
@@ -115,7 +96,7 @@ function crestBlock(signatory, dateLabel) {
return [
`Name: ${filled(signatory.name)}`,
`Designation: ${filled(signatory.designation)}`,
- signatureRow(image, { withLogo: true }),
+ `Signature: ${image}`,
`Date: ${filled(dateLabel)}`,
].join('\n\n');
}
@@ -306,10 +287,7 @@ export function agreementHtml(options) {
.filled { font-weight: 700; text-decoration: underline; text-underline-offset: 0.15em; }
.watermark { position: fixed; top: 45%; left: -10%; width: 120%; text-align: center; font-size: 42pt; font-weight: 700; letter-spacing: 0.04em; color: rgba(185, 28, 28, 0.18); transform: rotate(-32deg); z-index: 10; }
.typed-signature { font-family: "Segoe Script", "Apple Chancery", cursive; font-size: 20pt; }
- .signature-row { display: flex; justify-content: space-between; align-items: center; gap: 1rem; width: 100%; }
- .signature-field { display: inline-flex; align-items: center; gap: 0.5rem; min-height: 64px; }
img.signature { height: 64px; max-width: 240px; width: auto; vertical-align: middle; background: #fff; }
- img.crest-logo { height: 52px; width: auto; flex: none; }
{error}
:Loading settings…
; + return loadError ?{loadError}
:Loading settings…
; } return ( @@ -115,6 +147,7 @@ export default function SettingsPage() { +{error}
: null} - {status ?{status}
: null} ); } + +/** + * @param {{ message?: CardMessage }} props + */ +function CardStatus({ message }) { + if (!message) { + return null; + } + + return ( ++ {message.message} +
+ ); +} diff --git a/frontend/src/index.css b/frontend/src/index.css index d177817..78b1d1c 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -773,6 +773,56 @@ a:focus-visible { color: #1c1917; } +.survey-field { + display: grid; + gap: 0; +} + +.survey-field-errors { + margin: 0.15rem 0 0.35rem; +} + +.survey-field-errors .field-error { + margin: 0.15rem 0 0; +} + +.survey-toast { + position: fixed; + right: 1.25rem; + /* Sit above the site footer */ + bottom: 5.5rem; + z-index: 1000; + max-width: min(22rem, calc(100vw - 2rem)); + padding: 0.65rem 0.9rem; + border-radius: 0.55rem; + box-shadow: var(--shadow); + font-size: 0.9rem; + font-weight: 500; + animation: survey-toast-in 0.2s ease-out; +} + +.survey-toast-success { + background: rgba(15, 118, 110, 0.9); + color: #f0fdfa; +} + +.survey-toast-error { + background: rgb(185, 28, 28, 0.9); + color: #fff; +} + +@keyframes survey-toast-in { + from { + opacity: 0; + transform: translateY(0.4rem); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + .survey-dialog { position: fixed; inset: 0; @@ -1018,19 +1068,33 @@ a:focus-visible { .user-step-list { display: grid; + grid-template-columns: 1fr; gap: 0.75rem; margin: 1.25rem 0; } +@media (min-width: 48rem) { + .user-step-list { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } +} + .user-step { display: flex; - flex-wrap: wrap; - justify-content: space-between; - gap: 0.5rem; - padding: 0.85rem 1rem; + flex-direction: column; + align-items: flex-start; + gap: 0.55rem; + padding: 0.95rem 1rem; border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--surface-muted); + min-width: 0; +} + +.user-step strong { + font-size: 0.98rem; + line-height: 1.3; + color: var(--text); } .user-step.done { @@ -1038,9 +1102,35 @@ a:focus-visible { background: #f0fdfa; } -.user-step span { - color: var(--text-muted); - font-size: 0.9rem; +.user-step-status { + display: inline-flex; + align-items: center; + padding: 0.2rem 0.55rem; + border-radius: 999px; + font-size: 0.72rem; + font-weight: 700; + letter-spacing: 0.03em; + text-transform: uppercase; +} + +.user-step-status-completed { + background: #dcfce7; + color: #14532d; +} + +.user-step-status-in-process { + background: #fef3c7; + color: #92400e; +} + +.user-step-status-not-started { + background: #fecaca; + color: #9f1233; +} + +.user-step-status-rejected { + background: #fee2e2; + color: #991b1b; } .user-next-hint { diff --git a/frontend/src/pages/SurveyPage.jsx b/frontend/src/pages/SurveyPage.jsx index 1cec5c9..56e6533 100644 --- a/frontend/src/pages/SurveyPage.jsx +++ b/frontend/src/pages/SurveyPage.jsx @@ -1,4 +1,5 @@ import { useEffect, useRef, useState } from 'react'; +import { createPortal } from 'react-dom'; import { Link, useParams } from 'react-router-dom'; import { validateAnswers } from '../../../backend/src/survey/validate.js'; import { surveyApi } from '../surveyApi.js'; @@ -70,10 +71,44 @@ function formatAnswer(question, value) { } /** - * @param {{ question: Record+ * {message} +
+ ))} +{question.text}
; @@ -86,204 +121,220 @@ function QuestionField({ question, value, invalid = false, hideLabel = false, on if (question.type === 'short_text' || question.type === 'long_text') { const Input = question.type === 'long_text' ? 'textarea' : 'input'; return ( - +