From 1ddc84b68a654469a442af2b3c39204ebd4a67fd Mon Sep 17 00:00:00 2001 From: nfloser <102595695+nfloser@users.noreply.github.com> Date: Sat, 10 Oct 2026 22:07:12 +0200 Subject: [PATCH 1/6] Add system-aware medical blue dark mode --- docs/development/DEMO_ABNAHME.de.md | 10 ++++++ frontend/e2e/clinical-workspace.spec.mjs | 2 +- frontend/e2e/theme.spec.mjs | 35 ++++++++++++++++++ frontend/src/ThemeControl.tsx | 14 ++++++++ frontend/src/WorkspaceMenu.tsx | 3 +- frontend/src/de.json | 7 ++++ frontend/src/style.css | 45 ++++++++++++++++++++++++ frontend/tests/contrast.test.mjs | 11 ++++++ 8 files changed, 125 insertions(+), 2 deletions(-) create mode 100644 frontend/e2e/theme.spec.mjs create mode 100644 frontend/src/ThemeControl.tsx diff --git a/docs/development/DEMO_ABNAHME.de.md b/docs/development/DEMO_ABNAHME.de.md index c0ec7db..49fbd02 100644 --- a/docs/development/DEMO_ABNAHME.de.md +++ b/docs/development/DEMO_ABNAHME.de.md @@ -99,3 +99,13 @@ zu prüfen; Konfliktverhalten bleibt durch die vorhandenen API-Tests abgedeckt. Fallbezeichnungen verwenden in jeder Tabellenzeile einen gleich breiten Lesezeichenplatz. Das Setzen oder Entfernen des Sterns verschiebt weder den Textanfang noch die Spalten; optionale Notizen beginnen auf derselben Textkante. + +### Helle und dunkle Darstellung + +„Farbschema“ rechts im Menüband bietet System, Hell und Dunkel. System reagiert +auf die Betriebssystemeinstellung; eine explizite Auswahl hat Vorrang. Die Auswahl +bleibt beim Wechsel des Arbeitsbereichs in dieser Sitzung erhalten. Sie wird +nicht in localStorage, sessionStorage oder Cookies gespeichert; dauerhafte +benutzergebundene Ansichten bleiben Aufgabe #281. PDF-/Scanseiten behalten ihre +Originalfarben. Die umgebende Dokumentenansicht, Tabellen, Fallakten, Kontextmenüs, +Dialoge und Extras verwenden die dunklen medizinisch-blauen Farben. diff --git a/frontend/e2e/clinical-workspace.spec.mjs b/frontend/e2e/clinical-workspace.spec.mjs index 9d72407..c3daefb 100644 --- a/frontend/e2e/clinical-workspace.spec.mjs +++ b/frontend/e2e/clinical-workspace.spec.mjs @@ -37,7 +37,7 @@ for(const viewport of [{width:1920,height:1080},{width:1366,height:768}]){ await expect(explorer.locator('.page-stage')).toHaveAttribute('data-zoom-mode','width'); await expect(explorer.locator('.viewer-page')).toHaveCount(2); await expect(explorer.getByLabel('Seitenzahl')).toHaveValue('2'); - const image=await explorer.locator('.pdf-page-image').first().boundingBox();expect(image.width).toBeGreaterThan(600); + await expect.poll(async()=> (await explorer.locator('.pdf-page-image').first().boundingBox())?.width??0).toBeGreaterThan(600); await expect(explorer.getByLabel('Dokument auswählen')).toHaveCount(0); await page.screenshot({path:`test-results/clinical-documents-${viewport.width}.png`}); await explorer.getByLabel('Zoommodus').selectOption('150'); diff --git a/frontend/e2e/theme.spec.mjs b/frontend/e2e/theme.spec.mjs new file mode 100644 index 0000000..4e2c3a8 --- /dev/null +++ b/frontend/e2e/theme.spec.mjs @@ -0,0 +1,35 @@ +import {test,expect} from '@playwright/test'; +test('theme follows the system until explicitly changed and survives workspace navigation',async({page})=>{ + await page.emulateMedia({colorScheme:'dark'});await page.goto('/#work-queues'); + const scheme=page.getByLabel('Farbschema');await expect(scheme).toHaveValue('system'); + await expect(page.locator('html')).toHaveAttribute('data-theme','dark'); + await scheme.selectOption('light');await page.emulateMedia({colorScheme:'dark'}); + await expect(page.locator('html')).toHaveAttribute('data-theme','light'); + await scheme.selectOption('system');await expect(page.locator('html')).toHaveAttribute('data-theme','dark'); + await page.emulateMedia({colorScheme:'light'});await expect(page.locator('html')).toHaveAttribute('data-theme','light'); + await scheme.selectOption('dark');await page.getByRole('navigation',{name:'Arbeitsbereiche'}).getByText('Extras',{exact:true}).click();await page.locator('.workspace-extras-list a[href="#workbench"]').click(); + await expect(scheme).toHaveValue('dark');await expect(page.locator('html')).toHaveAttribute('data-theme','dark'); + expect(await page.evaluate(()=>[localStorage.length,sessionStorage.length])).toEqual([0,0]); + await page.screenshot({path:'test-results/dark-workbench.png'}); +}); +for(const width of [1366,1920])test(`dark case file, sources and dialogs remain readable at ${width}`,async({page})=>{ + await page.setViewportSize({width,height:1080});await page.goto('/#work-queues'); + await page.getByLabel('Farbschema').selectOption('dark'); + const explorer=page.getByRole('region',{name:'Fallverwaltung'}); + await expect(explorer.locator('.explorer-tree')).toHaveCSS('background-color','rgb(15, 27, 40)'); + await page.getByLabel('Fälle suchen').fill('reference-care-complete'); + await explorer.locator('tbody tr').dblclick(); + await expect(explorer.getByRole('heading',{name:'Prüfergebnis: Voraussetzungen erfüllt',exact:true})).toBeVisible(); + await page.screenshot({path:`test-results/dark-case-${width}.png`}); + await explorer.getByRole('button',{name:'Dokumente',exact:true}).click(); + await expect(explorer.locator('.pdf-page-image').first()).toHaveCSS('filter','none'); + await page.screenshot({path:`test-results/dark-documents-${width}.png`}); + await explorer.getByRole('button',{name:'Neuer Ordner',exact:true}).click(); + await expect(page.getByRole('dialog')).toHaveCSS('background-color','rgb(23, 38, 53)'); + await page.screenshot({path:`test-results/dark-dialog-${width}.png`}); + await page.getByRole('dialog').getByRole('button',{name:'Abbrechen'}).click(); + await explorer.getByRole('button',{name:'← Liste',exact:true}).click(); + await explorer.locator('tbody tr').click({button:'right'}); + await expect(page.getByRole('menu',{name:'Fallaktionen'})).toHaveCSS('background-color','rgb(23, 38, 53)'); + await page.screenshot({path:`test-results/dark-context-${width}.png`}); +}); diff --git a/frontend/src/ThemeControl.tsx b/frontend/src/ThemeControl.tsx new file mode 100644 index 0000000..66ec1ca --- /dev/null +++ b/frontend/src/ThemeControl.tsx @@ -0,0 +1,14 @@ +import {useEffect,useState} from 'react'; +import de from './de.json'; +type Mode='system'|'light'|'dark'; +/** Session-only display preference: no case data or browser persistence. */ +export function ThemeControl(){ + const [mode,setMode]=useState('system'); + useEffect(()=>{ + const media=window.matchMedia('(prefers-color-scheme: dark)'); + const apply=()=>{document.documentElement.dataset.theme=mode==='system'?(media.matches?'dark':'light'):mode;}; + apply();media.addEventListener('change',apply); + return()=>media.removeEventListener('change',apply); + },[mode]); + return ; +} diff --git a/frontend/src/WorkspaceMenu.tsx b/frontend/src/WorkspaceMenu.tsx index 6653ef0..0b4edda 100644 --- a/frontend/src/WorkspaceMenu.tsx +++ b/frontend/src/WorkspaceMenu.tsx @@ -1,5 +1,6 @@ import {useEffect,useState} from 'react'; import de from './de.json'; +import {ThemeControl} from './ThemeControl'; const ui=de.clinicalWorkspace; export function workspaceAction(action:string){window.dispatchEvent(new CustomEvent('normacase-workspace-action',{detail:action}));} export function WorkspaceMenu({view}:{view:string}){ @@ -12,5 +13,5 @@ export function WorkspaceMenu({view}:{view:string}){ [ui.view,[['detail',ui.detail],['orientation',ui.orientation],['navigation',ui.navigation],['density',ui.density],['columns',ui.columns],['reset',ui.reset],['refresh',ui.refresh]]], [ui.cases,[['folder',ui.newFolder],['move',ui.move],['bookmark',ui.bookmark],['confirm',ui.confirm],['dispatch',ui.dispatch]]], ] as const; - return ; + return ; } diff --git a/frontend/src/de.json b/frontend/src/de.json index 61c591e..fc76adf 100644 --- a/frontend/src/de.json +++ b/frontend/src/de.json @@ -648,5 +648,12 @@ "dispatchHelp": "Erstellt und lädt ein lokales Versandpaket herunter. Anschließend erscheint der Fall in „Versand simuliert“. Es wird nichts extern übermittelt.", "confirmedNext": "Versandpaket erstellen", "completeNext": "Lokales Versandpaket erstellt" + }, + "theme": { + "label": "Farbschema", + "system": "System", + "light": "Hell", + "dark": "Dunkel", + "help": "Farbschema für diese Sitzung. System folgt der Darstellungseinstellung des Betriebssystems." } } diff --git a/frontend/src/style.css b/frontend/src/style.css index d1159df..a6282e1 100644 --- a/frontend/src/style.css +++ b/frontend/src/style.css @@ -531,3 +531,48 @@ h2,h3,h4,h5{font-size:14px;font-weight:600}strong{font-weight:600}main p{line-he .case-bookmark-slot{display:inline-flex;align-items:center;justify-content:center;width:16px;min-width:16px;flex:0 0 16px} .case-label{display:block;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} .case-explorer .explorer-table td .case-note{padding-left:20px} + +/* Medical blue themes share geometry and leave document pixels unchanged. */ +:root{color-scheme:light} +:root[data-theme=dark]{ + color-scheme:dark; + --nc-primary:#b5d9ff;--nc-action:#81baff;--nc-light:#20384f;--nc-background:#0f1b28;--nc-surface:#172635;--nc-line:#3c5268;--nc-button-border:#61778f;--nc-text:#e1ebf5;--nc-muted:#acbbcc;--nc-selected:#284d70;--nc-viewer:#0a1520; + --nc-shadow:0 4px 12px rgba(0,0,0,.45); +} +.theme-control{display:flex;align-items:center;flex-shrink:0;margin-left:8px} +.theme-control select{width:85px;height:24px;min-height:24px;font-size:var(--nc-small);background:var(--nc-surface);color:var(--nc-text)} +html[data-theme=dark] :is(.card,.opened-case,.case-result-view,.explorer-table-scroll,.document-case-file,.case-context-menu,.context-submenu-panel,.explorer-dialog,.workspace-extras-list,.workspace-help,.case-analysis-panel,.document-text,.workflow-tools,.reviewed-work-queues){background:var(--nc-surface);color:var(--nc-text);border-color:var(--nc-line)} +html[data-theme=dark] :is(input,select,textarea){background:var(--nc-background);color:var(--nc-text);border-color:var(--nc-button-border);accent-color:var(--nc-action)} +html[data-theme=dark] :is(p,dt,dd,small,summary,.field,.source,.source>span,.workflow-help,.section-head p,.eyebrow,.case-note,.queue-badge,.reader-help,.review-notice,.local){color:var(--nc-muted)} +html[data-theme=dark] :is(h1,h2,h3,h4,h5,strong,button.case-title-button){color:inherit} +html[data-theme=dark] :is(a,.text-button,.source-page-link){color:var(--nc-action)} +html[data-theme=dark] :is(.secondary,.case-context-menu button,.context-submenu-panel button){background:transparent;color:var(--nc-primary);border-color:var(--nc-button-border)} +html[data-theme=dark] :is(.case-context-menu button,.context-submenu-panel button,.secondary):hover:not(:disabled){background:var(--nc-light)} +html[data-theme=dark] .primary{background:#256da8;color:#fff} +html[data-theme=dark] .primary:hover:not(:disabled){background:#1d5687} +html[data-theme=dark] :is(pre,.notice,.explorer-notice,.opened-case-status,.queue-card,.queue-case,.workflow-history,.source){background:var(--nc-light);color:var(--nc-text);border-color:var(--nc-line)} +html[data-theme=dark] :is(.missing,.outcome.incomplete,.outcome.human_review){background:#403621;color:#ffebbf;border-color:#a08348} +html[data-theme=dark] :is(.error,.outcome.not_supported){background:#462730;color:#ffd9d1;border-color:#ac7280} +html[data-theme=dark] .outcome{background:#183e38;color:var(--nc-text);border-color:#4b867a} +html[data-theme=dark] :is(.section-head,.source,.document-observations td,.recorded-criteria td){border-color:var(--nc-line)} +html[data-theme=dark] .case-explorer .explorer-table th{background:var(--nc-light)} +html[data-theme=dark] .case-explorer .explorer-table tbody tr{background:var(--nc-surface);color:var(--nc-text)} +html[data-theme=dark] .case-explorer .explorer-table td{border-color:var(--nc-line)} +html[data-theme=dark] .case-explorer .explorer-table.status-row-colors tbody tr:is([data-status=approval],[data-status=complete]){background:#183e38} +html[data-theme=dark] .case-explorer .explorer-table.status-row-colors tbody tr[data-status=clarification]{background:#403621} +html[data-theme=dark] .case-explorer .explorer-table.status-row-colors tbody tr[data-status=review]{background:#493126} +html[data-theme=dark] .case-explorer .explorer-table.status-row-colors tbody tr[data-status=technical]{background:#462730} +html[data-theme=dark] .case-explorer .explorer-table.status-row-colors tbody tr[data-status=documents]{background:#343048} +html[data-theme=dark] .case-explorer .explorer-table tbody tr[aria-selected=true]{background:#256da8!important;color:#fff} +html[data-theme=dark] .case-explorer .explorer-table tbody tr[aria-selected=true] :is(.case-title-button,.case-identifier,.queue-badge){color:#fff} +html[data-theme=dark] .case-explorer .explorer-footer{background:var(--nc-background)} +html[data-theme=dark] .brandmark{color:var(--nc-background)} +html[data-theme=dark] dialog::backdrop{background:rgba(0,0,0,.6)} +html[data-theme=dark] :is(.pdf-page-image,.document-reader img){filter:none} + +html[data-theme=dark] :is(.batch-review,.workload-summary,.identity-list li,.batch-warning){background:var(--nc-light);color:var(--nc-text);border-color:var(--nc-line)} +html[data-theme=dark] :is(.batch-choice,.batch-result span,.workload-total span,.workload-counts span,.queue-more){color:var(--nc-muted)} +html[data-theme=dark] .workload-total strong{color:var(--nc-primary)} +html[data-theme=dark] input::placeholder{color:var(--nc-muted)} + +html[data-theme=dark] .workspace-extras-list a:hover{background:var(--nc-light)} diff --git a/frontend/tests/contrast.test.mjs b/frontend/tests/contrast.test.mjs index b002de8..87d2a51 100644 --- a/frontend/tests/contrast.test.mjs +++ b/frontend/tests/contrast.test.mjs @@ -15,3 +15,14 @@ test('reading, help and action colors meet WCAG AA on their actual surfaces',()= assert.ok(contrast('ffffff','256da8')>=4.5); assert.ok(contrast('123c63','eaf3f9')>=4.5); }); + +test('dark theme reading and status colors meet AA contrast',()=>{ + const block=css.match(/:root\[data-theme=dark\]\{([^}]+)\}/)[1]; + const token=name=>block.match(new RegExp('--nc-'+name+':#([0-9a-f]{6})'))[1]; + for(const bg of [token('surface'),token('background'),token('light'),'183e38','403621','493126','462730','343048']){ + assert.ok(contrast(token('text'),bg)>=4.5); + assert.ok(contrast(token('muted'),bg)>=4.5); + } + assert.ok(contrast(token('action'),token('surface'))>=4.5); + assert.ok(contrast(token('button-border'),token('background'))>=3); +}); From 9fafe68d1a2d639c480b83ddf2051c8189078612 Mon Sep 17 00:00:00 2001 From: nfloser <102595695+nfloser@users.noreply.github.com> Date: Sat, 10 Oct 2026 22:08:28 +0200 Subject: [PATCH 2/6] Remove filed cases from the All cases inbox --- docs/development/DEMO_ABNAHME.de.md | 9 +++++++++ frontend/e2e/case-explorer.spec.mjs | 10 ++++++++++ frontend/e2e/folder-inbox.spec.mjs | 23 +++++++++++++++++++++++ frontend/src/CaseExplorer.tsx | 8 +++++--- frontend/src/de.json | 2 +- 5 files changed, 48 insertions(+), 4 deletions(-) create mode 100644 frontend/e2e/folder-inbox.spec.mjs diff --git a/docs/development/DEMO_ABNAHME.de.md b/docs/development/DEMO_ABNAHME.de.md index c0ec7db..972fa30 100644 --- a/docs/development/DEMO_ABNAHME.de.md +++ b/docs/development/DEMO_ABNAHME.de.md @@ -99,3 +99,12 @@ zu prüfen; Konfliktverhalten bleibt durch die vorhandenen API-Tests abgedeckt. Fallbezeichnungen verwenden in jeder Tabellenzeile einen gleich breiten Lesezeichenplatz. Das Setzen oder Entfernen des Sterns verschiebt weder den Textanfang noch die Spalten; optionale Notizen beginnen auf derselben Textkante. + +### Ablage aus dem Eingang + +„Alle Fälle“ enthält gemäß der gewünschten Bedienung nur Fälle ohne eigenen +Ordner. Nach „Verschieben nach“ verschwindet der Fall dort, einschließlich +Auswahl und eingeblendeter Detailakte. Im Zielordner und seinen fachlichen +Arbeits-/Statuslisten bleibt er zugänglich. „Aus Ordner entfernen“ bringt ihn +wieder in den Eingang. Zähler folgen der tatsächlichen Zuordnung; Prüfung, +Bestätigung und Versandstatus ändern sich dadurch nicht. diff --git a/frontend/e2e/case-explorer.spec.mjs b/frontend/e2e/case-explorer.spec.mjs index 29eeb48..66c7f4d 100644 --- a/frontend/e2e/case-explorer.spec.mjs +++ b/frontend/e2e/case-explorer.spec.mjs @@ -14,11 +14,17 @@ test('explorer organizes colored folders and bookmarks without changing the resu await page.getByRole('dialog').getByLabel('Zielordner').selectOption({label:'Für die Besprechung'}); await page.getByRole('dialog').getByRole('button',{name:'Verschieben',exact:true}).click(); await page.getByRole('navigation',{name:'Arbeitsbereiche'}).getByText('Ansicht',{exact:true}).click();await page.getByRole('button',{name:'Ordnerspalte ein/aus',exact:true}).click(); + await expect(row).toHaveCount(0); + await explorer.locator('.custom-folder .tree-filter').filter({hasText:'Für die Besprechung'}).click(); await expect(row).toContainText('Für die Besprechung'); await row.click({button:'right'});await page.getByRole('menuitem',{name:'Fall öffnen',exact:true}).click(); await expect(explorer.getByRole('heading',{name:'Prüfergebnis: Voraussetzungen erfüllt',exact:true})).toBeVisible(); await explorer.getByRole('button',{name:'← Liste',exact:true}).click(); await page.reload();await expect(explorer.getByText('Für die Besprechung').first()).toBeVisible(); + await explorer.locator('.custom-folder .tree-filter').filter({hasText:'Für die Besprechung'}).click(); + await row.click({button:'right'});await page.getByRole('menuitem',{name:'Aus Ordner entfernen',exact:true}).click(); + await explorer.locator('.tree-filter').filter({hasText:'Alle Fälle'}).click(); + await expect(row).toHaveCount(1); await page.screenshot({path:'test-results/medical-blue-explorer.png'}); }); @@ -49,7 +55,11 @@ test('keyboard menu returns focus and removes only the folder assignment',async( await row.getByRole('checkbox').check();await explorer.getByRole('button',{name:/Auswahl verschieben/}).click(); await page.getByRole('dialog').getByLabel('Zielordner').selectOption({label:'Tastaturablage'}); await page.getByRole('dialog').getByRole('button',{name:'Verschieben',exact:true}).click(); + await explorer.locator('.custom-folder .tree-filter').filter({hasText:'Tastaturablage'}).click(); await row.click({button:'right'});await expect(page.getByRole('menuitem',{name:'Aus Ordner entfernen',exact:true})).toBeVisible();await page.getByRole('menuitem',{name:'Aus Ordner entfernen',exact:true}).click(); + await expect(row).toHaveCount(0); + await explorer.locator('.tree-filter').filter({hasText:'Alle Fälle'}).click(); + await expect(row).toHaveCount(1); await expect(row).not.toContainText('Tastaturablage'); }); diff --git a/frontend/e2e/folder-inbox.spec.mjs b/frontend/e2e/folder-inbox.spec.mjs new file mode 100644 index 0000000..e85a115 --- /dev/null +++ b/frontend/e2e/folder-inbox.spec.mjs @@ -0,0 +1,23 @@ +import {test,expect} from '@playwright/test'; +test('moving a case removes it from All cases and clearing its folder returns it',async({page})=>{ + await page.goto('/#work-queues');const explorer=page.getByRole('region',{name:'Fallverwaltung'}); + await page.getByLabel('Fälle suchen').fill('reference-care-out-of-range'); + const row=explorer.locator('.explorer-table tbody tr');await expect(row).toHaveCount(1); + const result=await row.locator('td').nth(6).innerText(); + await row.locator('td').nth(2).click(); + await explorer.getByRole('button',{name:'Neuer Ordner',exact:true}).click(); + const dialog=page.getByRole('dialog');await dialog.getByLabel('Ordnername').fill('Abgelegte Vorgänge'); + await dialog.getByRole('button',{name:'Ordner speichern'}).click(); + await explorer.getByRole('button',{name:/Auswahl verschieben/}).click(); + await dialog.getByLabel('Zielordner').selectOption({label:'Abgelegte Vorgänge'}); + await dialog.getByRole('button',{name:'Verschieben',exact:true}).click(); + await expect(row).toHaveCount(0); + await expect(explorer.locator('.explorer-detail-heading')).toHaveCount(0); + await expect(explorer.getByRole('button',{name:'Auswahl bestätigen',exact:true})).toBeDisabled(); + await explorer.locator('.custom-folder .tree-filter').filter({hasText:'Abgelegte Vorgänge'}).click(); + await expect(row).toHaveCount(1);await expect(row.locator('td').nth(6)).toHaveText(result); + await row.click({button:'right'});await page.getByRole('menuitem',{name:'Aus Ordner entfernen',exact:true}).click(); + await expect(row).toHaveCount(0); + await explorer.locator('.tree-filter').filter({hasText:'Alle Fälle'}).click(); + await expect(row).toHaveCount(1);await expect(row.locator('td').nth(6)).toHaveText(result); +}); diff --git a/frontend/src/CaseExplorer.tsx b/frontend/src/CaseExplorer.tsx index 758b3b1..dfc6750 100644 --- a/frontend/src/CaseExplorer.tsx +++ b/frontend/src/CaseExplorer.tsx @@ -80,7 +80,9 @@ export function CaseExplorer({host=browserWorkspaceHost}:{host?:WorkspaceHost}={ if(!workspace||busy)return;setBusy(true);setError('');setNotice('');const controller=new AbortController();pending.current=controller; try{const response=await fetch('/api/demo-workspace/commands',{method:'POST',signal:controller.signal,headers:{'Content-Type':'application/json'},body:JSON.stringify({action,caseIds:ids,expectedRevision:workspace.revision,operationId:crypto.randomUUID(),...extra})}); if(!response.ok){let message=de.networkError;try{message=(await response.json()).message??message;}catch{}setError(message);await refresh(controller.signal);return;} - const result=await response.json();if(controller.signal.aborted)return;setWorkspace(result.workspace);setNotice(action==='DISPATCH'?t.dispatchedNotice:t.saved);if(dialog)closeDialog(); + const result=await response.json();if(controller.signal.aborted)return;setWorkspace(result.workspace); + if(action==='MOVE'){const remaining=new Set(result.workspace.cases.filter((item:Case)=>matches(item)).map((item:Case)=>item.caseId));setSelection(previous=>previous.filter(id=>remaining.has(id)));if(activeCase&&!remaining.has(activeCase))setActiveCase('');if(opened&&!remaining.has(opened))setOpened('');} + setNotice(action==='DISPATCH'?t.dispatchedNotice:t.saved);if(dialog)closeDialog(); if(result.packageJson)host.saveJson('normacase-versand-demo-'+result.workspace.revision+'.json',result.packageJson); }catch{if(!controller.signal.aborted)setError(t.completionUnknown);}finally{if(!controller.signal.aborted)setBusy(false);} } @@ -95,7 +97,7 @@ export function CaseExplorer({host=browserWorkspaceHost}:{host?:WorkspaceHost}={ function resultLabel(c:Case){const record=summaries[c.caseId];return record?record.assessmentJson?(outcomes[(parse(record.assessmentJson) as {assessment:{outcome:string}}).assessment.outcome]??de.unknown):ui.noTechnical:de.unknown;} function nextStep(c:Case){return c.organization.dispatched?processing.completeNext:c.organization.confirmed?processing.confirmedNext:(ui.nextSteps as Record)[c.queueId]??de.unknown;} function extraText(c:Case,key:string){return key==='number'?caseIdentity(c.caseId,c.title).number:key==='question'?catalog[c.caseId]?.context?.request??ui.noTechnical:key==='next'?nextStep(c):key==='result'?resultLabel(c):key==='documents'?String(summaries[c.caseId]?.documents.length??''):folders.find(f=>f.id===c.organization.folderId)?.label??'';} - function matches(c:Case){return Object.entries(extraFilters).every(([key,value])=>extraText(c,key).toLocaleLowerCase('de-DE').includes(value.toLocaleLowerCase('de-DE')))&& (platformCases||c.caseId.startsWith('reference-'))&&(!statusFilter||c.queueId===statusFilter)&&caseIdentity(c.caseId,c.title).subject.toLocaleLowerCase('de-DE').includes(columnQuery.toLocaleLowerCase('de-DE'))&&(filter==='all'||filter==='bookmarks'&&c.organization.bookmark||filter==='confirmed'&&c.organization.confirmed&&!c.organization.dispatched||filter==='dispatched'&&c.organization.dispatched||filter==='unfiled'&&!c.organization.folderId||filter===c.queueId||filter===c.organization.folderId)&&(c.caseId+' '+c.title+' '+c.organization.note).toLocaleLowerCase('de-DE').includes(query.toLocaleLowerCase('de-DE'));} + function matches(c:Case){return Object.entries(extraFilters).every(([key,value])=>extraText(c,key).toLocaleLowerCase('de-DE').includes(value.toLocaleLowerCase('de-DE')))&& (platformCases||c.caseId.startsWith('reference-'))&&(!statusFilter||c.queueId===statusFilter)&&caseIdentity(c.caseId,c.title).subject.toLocaleLowerCase('de-DE').includes(columnQuery.toLocaleLowerCase('de-DE'))&&(filter==='all'&&!c.organization.folderId||filter==='bookmarks'&&c.organization.bookmark||filter==='confirmed'&&c.organization.confirmed&&!c.organization.dispatched||filter==='dispatched'&&c.organization.dispatched||filter==='unfiled'&&!c.organization.folderId||filter===c.queueId||filter===c.organization.folderId)&&(c.caseId+' '+c.title+' '+c.organization.note).toLocaleLowerCase('de-DE').includes(query.toLocaleLowerCase('de-DE'));} const visible=cases.filter(matches).sort((a,b)=>{const left=caseIdentity(a.caseId,a.title),right=caseIdentity(b.caseId,b.title);const compared=(sort==='title'?left.subject:sort==='status'?a.queueId:left.number).localeCompare(sort==='title'?right.subject:sort==='status'?b.queueId:right.number,'de-DE',{numeric:true});return (descending?-compared:compared)||a.caseId.localeCompare(b.caseId);}); function selectRow(id:string,range:boolean,toggle:boolean){ if(busy)return; @@ -192,7 +194,7 @@ export function CaseExplorer({host=browserWorkspaceHost}:{host?:WorkspaceHost}={ {toolbar} {error&&

{error}

}{notice&&

{notice}

}
{if(e.key==='ArrowLeft'||e.key==='ArrowRight'){e.preventDefault();e.currentTarget.parentElement?.style.setProperty('--nav-width',Math.max(200,Math.min(360,(parseFloat(getComputedStyle(e.currentTarget.parentElement!).getPropertyValue('--nav-width'))||240)+(e.key==='ArrowRight'?10:-10)))+'px');}}} onPointerDown={e=>e.currentTarget.setPointerCapture(e.pointerId)} onPointerMove={e=>{if(e.currentTarget.hasPointerCapture(e.pointerId))e.currentTarget.parentElement?.style.setProperty('--nav-width',Math.max(200,Math.min(360,e.clientX))+'px');}}/>