diff --git a/scripts/health/assets/health-assets/dashboard-v5-capture.js b/scripts/health/assets/health-assets/dashboard-v5-capture.js index 78e0179..5ff2f7f 100644 --- a/scripts/health/assets/health-assets/dashboard-v5-capture.js +++ b/scripts/health/assets/health-assets/dashboard-v5-capture.js @@ -124,11 +124,12 @@ function closeCurrent(fromHistory = false) { if (hub.open) hub.close(); if (dialog.open) dialog.close(); state.files.forEach(item => URL.revokeObjectURL(item.url)); state.files = []; if (!fromHistory && new URL(location.href).searchParams.has('capture')) history.back(); } const launcher=document.querySelector('#open-capture-hub');if(launcher&&window.matchMedia('(max-width: 680px)').matches)document.body.append(launcher);launcher?.addEventListener('click', () => openHub()); document.querySelector('[data-day-capture]')?.addEventListener('click', () => openHub()); document.querySelectorAll('[data-close-dialog]').forEach(button => button.addEventListener('click', () => closeCurrent())); - hub.querySelectorAll('[data-capture-type]').forEach(button => button.addEventListener('click', async () => {if(['medication','supplement'].includes(button.dataset.captureType))await ensurePlans();openType(button.dataset.captureType)})); + function openMedicationRecord() { hub.close(); updateUrl(null,{replace:true}); if(typeof window.healthRecordOpenTab==='function')window.healthRecordOpenTab('medications'); else status.textContent='Die Medikationsakte ist technisch nicht verfügbar.'; } + hub.querySelectorAll('[data-capture-type]').forEach(button => button.addEventListener('click', async () => {if(button.dataset.captureType==='medication'){openMedicationRecord();return;}if(button.dataset.captureType==='supplement')await ensurePlans();openType(button.dataset.captureType)})); hub.querySelector('[data-capture-document]')?.addEventListener('click',()=>{hub.close();updateUrl(null,{replace:true});window.healthRecordOpenDocumentUpload?.();}); hub.querySelector('[data-open-full-checkin]')?.addEventListener('click', () => { hub.close(); const checkin=document.querySelector('#checkin-dialog'); if(checkin){document.body.append(checkin);checkin.showModal();} updateUrl(null, {replace:true}); }); - window.addEventListener('popstate', async () => { const capture = new URL(location.href).searchParams.get('capture'); closeCurrent(true); if (capture === 'menu') openHub(false); else if (labels[capture]) {if(['medication','supplement'].includes(capture))await ensurePlans();openType(capture, false);} }); - const initial = new URL(location.href).searchParams.get('capture'); if (initial === 'menu') openHub(false); else if (labels[initial]) {if(['medication','supplement'].includes(initial))ensurePlans().then(()=>openType(initial,false));else openType(initial, false);} + window.addEventListener('popstate', async () => { const capture = new URL(location.href).searchParams.get('capture'); closeCurrent(true); if (capture === 'menu') openHub(false); else if(capture==='medication')openMedicationRecord(); else if (labels[capture]) {if(capture==='supplement')await ensurePlans();openType(capture, false);} }); + const initial = new URL(location.href).searchParams.get('capture'); if (initial === 'menu') openHub(false); else if(initial==='medication')openMedicationRecord(); else if (labels[initial]) {if(initial==='supplement')ensurePlans().then(()=>openType(initial,false));else openType(initial, false);} function addSelectedFiles(input) { warning.textContent = ''; diff --git a/scripts/health/assets/health-assets/dashboard-v5-day-controller.js b/scripts/health/assets/health-assets/dashboard-v5-day-controller.js index f7fb734..005f9af 100644 --- a/scripts/health/assets/health-assets/dashboard-v5-day-controller.js +++ b/scripts/health/assets/health-assets/dashboard-v5-day-controller.js @@ -265,7 +265,11 @@ q('[data-day-next]').addEventListener('click',()=>openDay({date:shiftDay(currentDay,1),source:'next'})); q('[data-day-calendar]').addEventListener('click',()=>{ selectView('calendar'); ensureCalendar(); calendar.gotoDate(currentDay); }); q('[data-day-compare]').addEventListener('click',()=>{ window.healthRange?.set({from:shiftDay(currentDay,-6),to:currentDay,preset:'custom'}); selectView('explorer'); }); - q('[data-day-symptom]').addEventListener('click',()=>q('#open-checkin')?.click()); + q('[data-day-symptom]').addEventListener('click',()=>{ + const date=currentDay; + if(typeof window.healthOpenSymptomCheckin==='function')window.healthOpenSymptomCheckin(date); + else q('[data-day-status]').textContent='Der bestehende Symptomdialog ist technisch nicht verfügbar.'; + }); q('[data-day-header-open]')?.addEventListener('click',()=>openDay({date:q('[data-day-header-date]').value,source:'header'})); q('[data-day-today]')?.addEventListener('click',()=>openDay({date:today,source:'today'})); q('[data-today-day-open]')?.addEventListener('click',()=>openDay({date:today,source:'today_header'})); diff --git a/scripts/health/assets/health-assets/dashboard-v5-record.js b/scripts/health/assets/health-assets/dashboard-v5-record.js index 4ab3635..86cc284 100644 --- a/scripts/health/assets/health-assets/dashboard-v5-record.js +++ b/scripts/health/assets/health-assets/dashboard-v5-record.js @@ -738,8 +738,10 @@ } const medicationDose = item => { - const actual = [item.actual_dose_value, item.actual_dose_unit].filter(Boolean).join(' '); - const planned = [item.planned_dose_value, item.planned_dose_unit].filter(Boolean).join(' '); + const actualStructured = [item.actual_quantity_value,item.actual_dosage_form].filter(Boolean).join(' ')+(item.actual_strength?` · ${item.actual_strength}`:''); + const plannedStructured = [item.planned_quantity_value,item.planned_dosage_form].filter(Boolean).join(' ')+(item.planned_strength?` · ${item.planned_strength}`:''); + const actual = actualStructured || [item.actual_dose_value, item.actual_dose_unit].filter(Boolean).join(' '); + const planned = plannedStructured || [item.planned_dose_value, item.planned_dose_unit].filter(Boolean).join(' '); if(item.status==='administered') return actual || (item.legacy_dose ? `Legacy-Freitext (nicht als tatsächliche Dosis strukturiert): ${item.legacy_dose}` : 'Tatsächliche Dosis nicht dokumentiert'); if(item.status==='planned') return planned || (item.legacy_dose ? `Legacy-Freitext (nicht strukturiert): ${item.legacy_dose}` : 'Geplante Dosis nicht dokumentiert'); if(item.status==='corrected') { @@ -811,19 +813,76 @@ dialog.addEventListener('close',()=>dialog.remove(),{once:true});dialog.showModal();when.focus(); } + function administrationCaptureDialog(prescription, eventItem = null, captureMode = 'historical') { + const trigger=document.activeElement; + const plannedMode=captureMode==='planned'; + if(plannedMode&&!eventItem)return; + const baseline=plannedMode?eventItem?.administration_preset:prescription.administration_preset; + const dialog=document.createElement('dialog');dialog.className='medication-action-dialog';dialog.setAttribute('aria-labelledby','medication-administration-title'); + const form=document.createElement('form');form.method='dialog'; + const title=element('h2',plannedMode?'Geplanten Termin dokumentieren':'Historische Einnahme/Gabe erfassen');title.id='medication-administration-title'; + form.append(title,element('p','Manuelle Dokumentation ohne Änderung des Verordnungsstatus.','v5-meta')); + if(prescription.status!=='active')form.append(element('p',`Verordnungsstatus bleibt: ${prescription.status==='unknown'?'unbekannt':prescription.status}.`,'v5-meta')); + const when=document.createElement('input');when.type='datetime-local';when.required=true; + const quantity=document.createElement('input');quantity.maxLength=40;quantity.required=true;quantity.inputMode='decimal';quantity.value=baseline?.quantity_value||''; + const dosageForm=document.createElement('input');dosageForm.maxLength=40;dosageForm.required=true;dosageForm.value=baseline?.dosage_form||''; + const strength=document.createElement('input');strength.maxLength=80;strength.required=true;strength.value=baseline?.strength||''; + const route=document.createElement('select');[['unknown','Nicht belegt'],['oral','Oral'],['subcutaneous','Subkutan'],['intravenous','Intravenös'],['intramuscular','Intramuskulär'],['topical','Äußerlich'],['inhaled','Inhalativ'],['other','Andere dokumentierte Route']].forEach(([value,label])=>{const option=element('option',label);option.value=value;route.append(option);});route.value=baseline?.route_normalized||'unknown'; + const routeOriginal=document.createElement('input');routeOriginal.maxLength=60;routeOriginal.value=baseline?.route_original||''; + const region=document.createElement('input');region.maxLength=80;const side=document.createElement('select');[['','Nicht angegeben'],['left','Links'],['right','Rechts'],['unspecified','Seite nicht angegeben']].forEach(([value,label])=>{const option=element('option',label);option.value=value;side.append(option);});const injectionDetail=document.createElement('input');injectionDetail.maxLength=120; + const note=document.createElement('textarea');note.maxLength=300;const deviation=document.createElement('input');deviation.type='checkbox';const duplicate=document.createElement('input');duplicate.type='checkbox'; + form.append(formRow('Medikament',element('strong',prescription.name)),formRow('Zeitpunkt (Europe/Zurich)',when),formRow('Menge',quantity),formRow('Darreichungsform',dosageForm),formRow('Wirkstoffstärke',strength),formRow('Applikationsweg',route),formRow('Applikationsweg – dokumentierte Originalangabe',routeOriginal),formRow('Injektionsregion (optional)',region),formRow('Seite (optional)',side),formRow('Injektionsstelle (optional)',injectionDetail),formRow('Notiz (optional)',note)); + const deviationRow=formRow('Abweichung von der verifizierten Vorauswahl bewusst bestätigen',deviation);deviationRow.hidden=true;form.append(deviationRow,formRow('Möglichen identischen Doppeleintrag nach Prüfung zulassen',duplicate)); + const fieldError=element('p','', 'capture-inline-status');fieldError.setAttribute('role','alert');form.append(fieldError); + const preview=element('section',undefined,'medication-action-preview');preview.hidden=true;preview.setAttribute('aria-live','polite');form.append(preview); + const controls=element('div',undefined,'record-actions');const cancel=button('Abbrechen',()=>dialog.close());const submit=button('Vorschau prüfen',()=>form.requestSubmit());controls.append(cancel,submit);form.append(controls);dialog.append(form);document.body.append(dialog); + const injectionRoutes=new Set(['subcutaneous','intravenous','intramuscular','other']); + const syncInjection=()=>{const enabled=injectionRoutes.has(route.value);[region,side,injectionDetail].forEach(input=>{input.disabled=!enabled;if(!enabled)input.value='';});}; + const differs=()=>Boolean(baseline)&&[quantity.value.trim()!==baseline.quantity_value,dosageForm.value.trim()!==baseline.dosage_form,strength.value.trim()!==baseline.strength,route.value!==(baseline.route_normalized||'unknown')].some(Boolean); + const syncDeviation=()=>{deviationRow.hidden=!differs();if(deviationRow.hidden)deviation.checked=false;}; + [quantity,dosageForm,strength,route].forEach(input=>input.addEventListener('input',syncDeviation));route.addEventListener('change',()=>{routeOriginal.value=baseline&&route.value===baseline.route_normalized?(baseline.route_original||''):'';syncInjection();syncDeviation();});syncInjection();syncDeviation(); + let frozen=null;let frozenCsrf=''; + form.addEventListener('input',()=>{fieldError.textContent='';if(frozen){frozen=null;frozenCsrf='';preview.hidden=true;submit.textContent='Vorschau prüfen';}}); + form.addEventListener('submit',async event=>{event.preventDefault();fieldError.textContent='';if(!frozen){ + if(differs()&&!deviation.checked){fieldError.textContent='Die Angaben weichen von der verifizierten Vorauswahl ab. Bitte Abweichung bestätigen.';deviation.focus();return;} + const data={contract:'health.medication_action.v2',mode:captureMode,status:'administered',medication_ref:prescription.id,planned_event_ref:plannedMode?eventItem.id:'',name:prescription.name,quantity_value:quantity.value.trim(),dosage_form:dosageForm.value.trim(),strength:strength.value.trim(),route_original:routeOriginal.value.trim(),route_normalized:route.value,injection_region:region.value.trim(),injection_side:side.value,injection_detail:injectionDetail.value.trim(),note:note.value.trim(),preset_revision:baseline?.preset_revision||'',preview_revision:'',deviation_confirmed:deviation.checked,duplicate_confirmed:duplicate.checked}; + const draft={version:1,action:'capture_entry',capture_type:'medication',request_version:1,idempotency_key:[...crypto.getRandomValues(new Uint8Array(16))].map(value=>value.toString(16).padStart(2,'0')).join(''),occurred_at:when.value,ended_at:null,data,attachments:[],corrects_entry_id:null,withdraws_entry_id:null}; + submit.disabled=true; + try{ + const csrfResponse=await fetch('/api/v1/capture/csrf',{credentials:'same-origin',headers:{'Accept':'application/json'}});if(!csrfResponse.ok)throw new Error('csrf');const csrfResult=await csrfResponse.json();const captureCsrf=String(csrfResult.csrf_token||'');if(!/^[A-Za-z0-9_-]{24,128}$/.test(captureCsrf))throw new Error('csrf'); + const body=new URLSearchParams({csrf_token:captureCsrf,return_to:'v5',payload:JSON.stringify(draft)});const response=await fetch('/health-actions/medication-preview',{method:'POST',credentials:'same-origin',headers:{'Accept':'application/json','Content-Type':'application/x-www-form-urlencoded;charset=UTF-8'},body:body.toString()});const result=await response.json().catch(()=>({}));if(!response.ok){fieldError.textContent=result?.error?.message||'Die Angaben sind ungültig. Bitte markierte Felder prüfen.';const field=result?.error?.field;const fieldInputs={medication:quantity,quantity_value:quantity,dosage_form:dosageForm,strength,route_original:routeOriginal,injection_region:region,injection_detail:injectionDetail,note};fieldInputs[field]?.focus();return;}if(!/^[a-f0-9]{64}$/.test(String(result.preview_revision||'')))throw new Error('preview');draft.data.preview_revision=result.preview_revision;frozen=draft;frozenCsrf=captureCsrf; + }catch(_error){fieldError.textContent='Die Erfassung ist technisch nicht verfügbar. Es wurde nichts vorgemerkt.';return;}finally{submit.disabled=false;} + preview.replaceChildren(element('h3','Vollständige Vorschau'),element('p',`${when.value} · ${prescription.name}`),element('p',`${quantity.value} ${dosageForm.value} · ${strength.value}`),element('p',`Applikationsweg: ${route.options[route.selectedIndex].text}${region.value||side.value||injectionDetail.value?` · ${[region.value,side.options[side.selectedIndex]?.text,injectionDetail.value].filter(Boolean).join(' · ')}`:''}`),element('p',note.value?`Notiz: ${note.value}`:'Keine Notiz.','v5-meta'),element('p',plannedMode?'Mit vorhandenem Plantermin verknüpft.':'Historische manuelle Gabe ohne Plantermin.','v5-meta'));preview.hidden=false;submit.textContent='Verbindlich vormerken';return; + } + submit.disabled=true; + try{ + const body=new URLSearchParams({csrf_token:frozenCsrf,return_to:'v5',payload:JSON.stringify(frozen)}); + const response=await fetch('/health-actions/capture',{method:'POST',credentials:'same-origin',headers:{'Accept':'application/json','Content-Type':'application/x-www-form-urlencoded;charset=UTF-8'},body:body.toString()}); + const result=await response.json().catch(()=>({})); + if(!response.ok||result.status!=='queued')throw new Error('capture_unavailable'); + fieldError.textContent='Vorgemerkt. Die lokale Verarbeitung steht noch aus.';submit.textContent='Vorgemerkt'; + const statusKey=String(result.status_key||''); + if(/^[a-f0-9]{32}$/.test(statusKey)){ + let attempts=0;const poll=async()=>{attempts+=1;try{const check=await fetch(`/api/v1/capture/status/${statusKey}`,{credentials:'same-origin',headers:{'Accept':'application/json'}});const state=await check.json();if(state.status==='processed'){fieldError.textContent='Gespeichert.';setTimeout(()=>{dialog.close();render('medications',{push:false});},300);return;}if(state.status==='rejected'){fieldError.textContent='Die Verarbeitung wurde wegen eines geänderten Datenstands oder eines möglichen Duplikats abgelehnt. Eingaben bleiben erhalten; bitte Vorschau erneut prüfen.';frozen=null;frozenCsrf='';preview.hidden=true;submit.disabled=false;submit.textContent='Vorschau erneut prüfen';return;}}catch(_error){}if(attempts<8)setTimeout(poll,750);};setTimeout(poll,750); + } + }catch(_error){fieldError.textContent='Die Erfassung ist technisch nicht verfügbar. Eingaben bleiben erhalten; bitte Vorschau erneut prüfen.';frozen=null;frozenCsrf='';preview.hidden=true;submit.disabled=false;submit.textContent='Vorschau erneut prüfen';} + }); + dialog.addEventListener('close',()=>{dialog.remove();if(trigger instanceof HTMLElement)trigger.focus();},{once:true});dialog.showModal();when.focus(); + } + async function renderMedications(filters = {}, current) { const node = card('Medikamente'); const safeFilters=sanitizeFilters('medications',filters); const data = await request(`/api/v1/medications?${query(safeFilters)}`, current); const form=element('form',undefined,'record-filter-form');const from=document.createElement('input');from.type='date';from.value=safeFilters.from||'';const to=document.createElement('input');to.type='date';to.value=safeFilters.to||'';const medication=document.createElement('select');[['','Alle Medikamente'],...(data.prescriptions||[]).map(item=>[item.id,item.name])].forEach(([value,label])=>{const option=element('option',label);option.value=value;option.selected=safeFilters.medication===value;medication.append(option);});const eventStatus=document.createElement('select');[['','Alle Status'],['planned','Geplant'],['administered','Verabreicht / eingenommen'],['missed','Ausgelassen'],['corrected','Korrigiert'],['unknown','Unbekannt']].forEach(([value,label])=>{const option=element('option',label);option.value=value;option.selected=safeFilters.status===value;eventStatus.append(option);});const source=document.createElement('select');[['','Alle Quellen'],...(data.sources||[]).map(value=>[value,sourceLabel(value)])].forEach(([value,label])=>{const option=element('option',label);option.value=value;option.selected=safeFilters.source===value;source.append(option);});form.append(formRow('Von',from),formRow('Bis',to),formRow('Medikament',medication),formRow('Status',eventStatus),formRow('Quelle',source),button('Filter anwenden',()=>form.requestSubmit()));form.addEventListener('submit',event=>{event.preventDefault();render('medications',{push:true,filters:{from:from.value,to:to.value,medication:medication.value,status:eventStatus.value,source:source.value}});});node.append(form); - const currentSection=element('section',undefined,'medication-current');currentSection.append(element('h4','Aktive Verordnungen'));(data.current_prescriptions||[]).forEach(item=>{const article=element('article',undefined,'medication-prescription-row');article.append(element('strong',item.name),element('span','Aktiv','status-chip'),element('p',item.documented_dose||'Dosis nicht strukturiert dokumentiert','v5-meta'));article.append(button('Jetzt als eingenommen / verabreicht dokumentieren',()=>medicationActionDialog(item)));currentSection.append(article);});if(!(data.current_prescriptions||[]).length)appendEmpty(currentSection);node.append(currentSection); - const otherSection=element('section',undefined,'medication-other-prescriptions');otherSection.append(element('h4','Pausierte, beendete oder unklare Verordnungen'));(data.other_prescriptions||[]).forEach(item=>{const article=element('article',undefined,'medication-prescription-row');const label=item.status==='paused'?'Pausiert':item.status==='ended'?'Beendet':'Status unbekannt';article.append(element('strong',item.name),element('span',label,'status-chip'),element('p','Keine Darstellung als aktuell aktive Verordnung.','v5-meta'));otherSection.append(article);});if(!(data.other_prescriptions||[]).length)appendEmpty(otherSection);node.append(otherSection,element('h4','Verlauf')); + const currentSection=element('section',undefined,'medication-current');currentSection.append(element('h4','Aktive Verordnungen'));(data.current_prescriptions||[]).forEach(item=>{const article=element('article',undefined,'medication-prescription-row');article.append(element('strong',item.name),element('span','Aktiv','status-chip'),element('p',item.documented_dose||'Dosis nicht strukturiert dokumentiert','v5-meta'));article.append(button('Historische Einnahme/Gabe erfassen',()=>administrationCaptureDialog(item,null,'historical')));currentSection.append(article);});if(!(data.current_prescriptions||[]).length)appendEmpty(currentSection);node.append(currentSection); + const otherSection=element('section',undefined,'medication-other-prescriptions');otherSection.append(element('h4','Pausierte, beendete oder unklare Verordnungen'));(data.other_prescriptions||[]).forEach(item=>{const article=element('article',undefined,'medication-prescription-row');const label=item.status==='paused'?'Pausiert':item.status==='ended'?'Beendet':'Status unbekannt';article.append(element('strong',item.name),element('span',label,'status-chip'),element('p','Keine Darstellung als aktuell aktive Verordnung. Historische Dokumentation ändert diesen Status nicht.','v5-meta'),button('Historische Einnahme/Gabe erfassen',()=>administrationCaptureDialog(item,null,'historical')));otherSection.append(article);});if(!(data.other_prescriptions||[]).length)appendEmpty(otherSection);node.append(otherSection,element('h4','Verlauf')); const prescriptionsById=new Map((data.prescriptions||[]).map(item=>[item.id,item])); ['planned', 'administered', 'missed', 'corrected', 'unknown'].forEach(kind => { const section = element('section'); section.append(element('h5', ({ planned: 'Geplant', administered: 'Tatsächlich verabreicht / eingenommen', missed: 'Ausgelassen', corrected: 'Korrigiert',unknown:'Status unbekannt' })[kind])); const rows = data[kind] || []; if(!rows.length){section.append(element('p','Keine dokumentierten Angaben.'));node.append(section);return;} - const list=element('div',undefined,'medication-history-list');rows.forEach(item=>{const article=element('article',undefined,'medication-history-row');article.append(element('div',`${item.date} · ${item.name} · ${medicationDose(item)} · ${sourceLabel(item.source)}`));if(item.superseded_by_correction)article.append(element('span','Durch Korrektur ersetzt','status-chip'));const details=document.createElement('details');details.append(element('summary','Details'));const facts=element('dl');[['Status',item.status],['Dokumentierter Folgetermin',item.scheduled_next_date||'Nicht angegeben'],['Geplante Dosis',[item.planned_dose_value,item.planned_dose_unit].filter(Boolean).join(' ')||'Nicht strukturiert dokumentiert'],['Tatsächliche Dosis',[item.actual_dose_value,item.actual_dose_unit].filter(Boolean).join(' ')||'Nicht strukturiert dokumentiert'],['Legacy-Dosisfreitext (uninterpretiert)',item.legacy_dose||'Nicht vorhanden'],['Applikationsweg (Original)',item.route_original||'Nicht angegeben'],['Applikationsweg (Anzeige)',item.route_normalized||'Unbekannt'],['Injektionsstelle',[item.injection_region,item.injection_side,item.injection_detail].filter(Boolean).join(' · ')||'Nicht angegeben'],['Charge',item.lot_number||'Nicht angegeben'],['Notiz',item.note||'Nicht angegeben'],['Korrekturbegründung',item.correction_reason||'Nicht angegeben'],['Quelle',sourceLabel(item.source)]].forEach(([label,value])=>facts.append(element('dt',label),element('dd',value)));details.append(facts);article.append(details);const prescription=prescriptionsById.get(item.prescription_id);if(prescription&&kind==='planned'&&!item.plan_consumed){article.append(button('Als eingenommen / verabreicht dokumentieren',()=>medicationActionDialog(prescription,item,'administered')),button('Auslassung dokumentieren',()=>medicationActionDialog(prescription,item,'missed')));}if(prescription&&!item.superseded_by_correction&&item.correction_preview_revision)article.append(button('Eintrag korrigieren',()=>medicationActionDialog(prescription,item,'corrected')));list.append(article);});section.append(list);node.append(section); + const list=element('div',undefined,'medication-history-list');rows.forEach(item=>{const article=element('article',undefined,'medication-history-row');article.append(element('div',`${item.date} · ${item.name} · ${medicationDose(item)} · ${sourceLabel(item.source)}`));if(item.superseded_by_correction)article.append(element('span','Durch Korrektur ersetzt','status-chip'));const details=document.createElement('details');details.append(element('summary','Details'));const facts=element('dl');[['Status',item.status],['Dokumentierter Folgetermin',item.scheduled_next_date||'Nicht angegeben'],['Geplante Menge',item.planned_quantity_value||item.planned_dose_value||'Nicht dokumentiert'],['Geplante Darreichungsform',item.planned_dosage_form||item.planned_dose_unit||'Nicht dokumentiert'],['Geplante Wirkstoffstärke',item.planned_strength||'Nicht dokumentiert'],['Tatsächliche Menge',item.actual_quantity_value||item.actual_dose_value||'Nicht dokumentiert'],['Tatsächliche Darreichungsform',item.actual_dosage_form||item.actual_dose_unit||'Nicht dokumentiert'],['Tatsächliche Wirkstoffstärke',item.actual_strength||'Nicht dokumentiert'],['Legacy-Dosisfreitext (uninterpretiert)',item.legacy_dose||'Nicht vorhanden'],['Applikationsweg (Original)',item.route_original||'Nicht angegeben'],['Applikationsweg (Anzeige)',item.route_normalized||'Unbekannt'],['Injektionsstelle',[item.injection_region,item.injection_side,item.injection_detail].filter(Boolean).join(' · ')||'Nicht angegeben'],['Charge',item.lot_number||'Nicht angegeben'],['Notiz',item.note||'Nicht angegeben'],['Korrekturbegründung',item.correction_reason||'Nicht angegeben'],['Quelle',sourceLabel(item.source)]].forEach(([label,value])=>facts.append(element('dt',label),element('dd',value)));details.append(facts);article.append(details);const prescription=prescriptionsById.get(item.prescription_id);if(prescription&&kind==='planned'&&!item.plan_consumed){if(item.administration_preset)article.append(button('Geplanten Termin dokumentieren',()=>administrationCaptureDialog(prescription,item,'planned')));article.append(button('Auslassung dokumentieren',()=>medicationActionDialog(prescription,item,'missed')));}if(prescription&&!item.superseded_by_correction&&item.correction_preview_revision)article.append(button('Eintrag korrigieren',()=>medicationActionDialog(prescription,item,'corrected')));list.append(article);});section.append(list);node.append(section); }); node.append(element('p', `Datenvollständigkeit: ${data.truncated ? `gekürzt (${data.truncated_sections.join(', ')})` : 'vollständige Seite'} · Fehlende Legacy-Felder bleiben unbekannt.`, 'v5-meta')); return node; diff --git a/scripts/health/assets/health-assets/dashboard-v5.js b/scripts/health/assets/health-assets/dashboard-v5.js index 0adfe94..dca1d01 100644 --- a/scripts/health/assets/health-assets/dashboard-v5.js +++ b/scripts/health/assets/health-assets/dashboard-v5.js @@ -518,17 +518,27 @@ if (status) status.textContent = message; } - function openSymptomCheckin() { + function openSymptomCheckin(date = bundle.today) { const dialog = q("#checkin-dialog"); if (!dialog) { setTaskStatus("Das Symptomformular ist in dieser Ansicht nicht verfügbar."); return false; } + const safeDate = /^\d{4}-\d{2}-\d{2}$/.test(String(date || "")) ? String(date) : bundle.today; + if (dialog.parentElement !== document.body) document.body.append(dialog); + const dateInput = q("input[name=date]", dialog); + const dateLabel = q("#capture-date", dialog); + if (dateInput) dateInput.value = safeDate; + if (dateLabel) { + dateLabel.dateTime = safeDate; + dateLabel.textContent = new Intl.DateTimeFormat("de-CH", { dateStyle: "long", timeZone: "Europe/Zurich" }).format(new Date(`${safeDate}T12:00:00Z`)); + } if (!dialog.open) dialog.showModal(); q("select:not(:disabled), input:not([type=hidden]):not(:disabled), textarea:not(:disabled), button:not(:disabled)", dialog)?.focus(); - setTaskStatus("Symptomerfassung geöffnet."); + setTaskStatus(`Symptomerfassung für ${safeDate} geöffnet.`); return true; } + window.healthOpenSymptomCheckin = openSymptomCheckin; async function dispatchTaskAction(action, queueCode = '') { if (action === "checkin") { __HERMES_CWD_8d46a20096ed__/home/agent/.hermes/repos/HealthManager__HERMES_CWD_8d46a20096ed__