diff --git a/frontend/src/components/MobileOrders.css b/frontend/src/components/MobileOrders.css index bed15cd..ad6fee0 100644 --- a/frontend/src/components/MobileOrders.css +++ b/frontend/src/components/MobileOrders.css @@ -244,6 +244,79 @@ margin-bottom: 13px; } +.mobile-panel-heading { + align-items: center; + display: flex; + justify-content: space-between; + gap: 12px; + margin-bottom: 10px; +} + +.mobile-panel-heading h3 { + margin-bottom: 2px; +} + +.mobile-panel-heading span { + color: #64748b; + font-size: 0.84rem; + font-weight: 700; +} + +.mobile-panel-heading > strong { + border-radius: 999px; + font-size: 0.82rem; + padding: 6px 10px; +} + +.mobile-panel-heading > strong.ready { + background: #ecfdf5; + color: #166534; +} + +.mobile-panel-heading > strong.missing { + background: #fff7ed; + color: #9a3412; +} + +.mobile-progress { + background: #e2e8f0; + border-radius: 999px; + height: 8px; + margin-bottom: 12px; + overflow: hidden; +} + +.mobile-progress span { + background: #2563eb; + display: block; + height: 100%; + transition: width 160ms ease; +} + +.mobile-readiness-box { + background: #fff7ed; + border: 1px solid #fed7aa; + border-radius: 7px; + color: #7c2d12; + display: grid; + gap: 7px; + margin-bottom: 13px; + padding: 10px 12px; +} + +.mobile-readiness-box.warning { + background: #f8fafc; + border-color: #cbd5e1; + color: #334155; +} + +.mobile-readiness-box ul { + display: grid; + gap: 4px; + margin: 0; + padding-left: 18px; +} + .mobile-question-list, .mobile-history-list { display: grid; diff --git a/frontend/src/components/MobileOrders.js b/frontend/src/components/MobileOrders.js index 857f902..6fb7946 100644 --- a/frontend/src/components/MobileOrders.js +++ b/frontend/src/components/MobileOrders.js @@ -16,10 +16,56 @@ const formatNumber = (value, digits = 0) => { }; const buildInitialAnswers = (questions = []) => questions.reduce((acc, question) => { - acc[question.key] = question.inputType === 'boolean' ? false : ''; + acc[question.key] = question.inputType === 'boolean' && question.required ? null : ''; return acc; }, {}); +const isPositiveNumber = (value) => { + const number = Number(String(value ?? '').replace(',', '.')); + return Number.isFinite(number) && number > 0; +}; + +const isQuestionAnswered = (question, value) => { + if (!question.required) return true; + if (question.inputType === 'boolean') return value === true || value === false; + if (question.inputType === 'number') return isPositiveNumber(value); + return String(value ?? '').trim().length > 0; +}; + +export const buildMobileReadiness = ({ questions = [], answers = {}, measuredData = {}, attachments = [] }) => { + const requiredQuestions = questions.filter((question) => question.required); + const missingRequired = requiredQuestions + .filter((question) => !isQuestionAnswered(question, answers[question.key])) + .map((question) => question.label); + const missing = [...missingRequired]; + const suggestions = []; + + if (!isPositiveNumber(measuredData.area)) { + missing.push('Areal'); + } + + if (!String(measuredData.accessNotes || '').trim()) { + suggestions.push('Adgangsnoter'); + } + + if (!attachments.length) { + suggestions.push('Billeder eller dokumentation'); + } + + const completedRequired = requiredQuestions.length - missingRequired.length; + const totalRequired = requiredQuestions.length + 1; + const completed = completedRequired + (isPositiveNumber(measuredData.area) ? 1 : 0); + + return { + ready: missing.length === 0, + missing, + suggestions, + completed, + total: totalRequired, + percent: totalRequired > 0 ? Math.round((completed / totalRequired) * 100) : 100 + }; +}; + const MobileOrders = ({ apiBaseUrl = '' }) => { const [orders, setOrders] = useState([]); const [selectedCaseNumber, setSelectedCaseNumber] = useState(null); @@ -44,6 +90,13 @@ const MobileOrders = ({ apiBaseUrl = '' }) => { [orders, selectedCaseNumber, detail] ); + const readiness = useMemo(() => buildMobileReadiness({ + questions: detail?.checklist?.questions || [], + answers, + measuredData, + attachments: detail?.attachments || [] + }), [answers, detail, measuredData]); + const apiPath = (path) => `${apiBaseUrl}${path}`; const loadOrders = async () => { @@ -194,6 +247,11 @@ const MobileOrders = ({ apiBaseUrl = '' }) => { }; const createDraft = async () => { + if (!readiness.ready) { + setError(`Mangler før desktop-review: ${readiness.missing.join(', ')}`); + return; + } + let intake = latestIntakeId ? { id: latestIntakeId } : null; if (!intake) { intake = await saveIntake('ready_for_review'); @@ -227,6 +285,27 @@ const MobileOrders = ({ apiBaseUrl = '' }) => { const renderQuestion = (question) => { const value = answers[question.key]; if (question.inputType === 'boolean') { + if (question.required) { + return ( + + ); + } + return (