Files
nat-as-server/website/js/app-ui-screens.js
Tom Hempel 154535edc2
Some checks failed
PHPUnit / test (push) Has been cancelled
started working on app ui page
2026-07-18 12:58:06 +02:00

1763 lines
90 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* App UI editor: screen → keys mapping and tablet mocks matching current Android layouts.
* Screens with multiple states expose `variants` (chip picker in the editor).
*/
/** @typedef {{ id: string, label: string, focusKeys?: string[], mock: () => string }} AppUiVariant */
/** @typedef {{ id: string, label: string, hint?: string, keys: string[], mock?: () => string, variants?: AppUiVariant[] }} AppUiScreen */
function f(key, cls = '', tag = 'input') {
const safe = String(key).replace(/"/g, '');
if (tag === 'textarea') {
return `<textarea class="aq-field ${cls}" data-appui-key="${safe}" rows="2" spellcheck="true"></textarea>`;
}
return `<input type="text" class="aq-field ${cls}" data-appui-key="${safe}" spellcheck="true">`;
}
function iconHelp() {
return `<span class="aq-icon-btn" aria-hidden="true"><svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="9"/><path d="M9.5 9a2.5 2.5 0 115 1c0 1.5-2.5 2-2.5 3.5"/><circle cx="12" cy="17" r=".8" fill="currentColor" stroke="none"/></svg></span>`;
}
function iconRefresh() {
return `<span class="aq-icon-btn" aria-hidden="true"><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12a9 9 0 11-3-6.7"/><polyline points="21 3 21 9 15 9"/></svg></span>`;
}
function iconPerson() {
return `<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>`;
}
function iconLock() {
return `<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2"><rect x="5" y="11" width="14" height="10" rx="2"/><path d="M8 11V8a4 4 0 018 0v3"/></svg>`;
}
function iconBack() {
return `<span class="aq-icon-btn aq-icon-btn-lg" aria-hidden="true"><svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2"><polyline points="15 18 9 12 15 6"/></svg></span>`;
}
function iconChevron() {
return `<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>`;
}
function langChip() {
return `<span class="aq-lang-chip">Deutsch ${iconChevron()}</span>`;
}
function toastBanner(key, cls = '') {
return `<div class="aq-toast ${cls}">${f(key, 'aq-toast-field')}</div>`;
}
/** Client note row as on opening_screen (ClientNoteUi.bindRow). */
function clientNoteRow({ hasNote = false, pending = false } = {}) {
const preview = hasNote
? `<span class="aq-note-preview">Beispielnotiz…</span>`
: f('client_note_add', 'aq-muted aq-sm');
const actionLabel = hasNote ? 'client_note_edit' : 'client_note_add_action';
const badge = pending
? `<span class="aq-note-pending">${f('client_note_pending', 'aq-xs')}</span>`
: '';
return `
<div class="aq-note-row">
<div class="aq-note-icon-well" aria-hidden="true">
<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="#fff" stroke-width="2"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg>
</div>
<div class="aq-note-main">
<div class="aq-note-label-row">
${f('client_note_for_client', 'aq-note-label')}
${badge}
</div>
${preview}
</div>
<div class="aq-btn-pill aq-btn-primary aq-note-cta-pill">${f(actionLabel, 'aq-field-on-primary aq-xs')}</div>
</div>`;
}
/** Settings bottom sheet (HandlerOpeningScreen.showSettingsSheet — handle + logout only, no title). */
function settingsSheet() {
return `
<div class="aq-sheet-scrim">
<div class="aq-bottom-sheet">
<div class="aq-sheet-handle" aria-hidden="true"></div>
<div class="aq-btn-pill aq-btn-danger aq-sheet-logout">${f('logout', 'aq-field-on-primary')}</div>
</div>
</div>`;
}
/** Client note editor bottom sheet (ClientNoteUi.showEditor — bottom_sheet_client_note.xml). */
function clientNoteEditorSheet({ errorKey = null } = {}) {
const err = errorKey
? `<div class="aq-field-error">${f(errorKey, 'aq-sm')}</div>`
: '';
return `
<div class="aq-sheet-scrim">
<div class="aq-bottom-sheet">
<div class="aq-sheet-handle" aria-hidden="true"></div>
<div class="aq-note-editor-header">
<div class="aq-note-icon-well aq-note-icon-well-sm" aria-hidden="true">
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="#fff" stroke-width="2"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg>
</div>
<div>
${f('client_note_title', 'aq-sheet-title')}
${f('client_note_for_client', 'aq-muted aq-sm')}
</div>
</div>
<div class="aq-outlined aq-mt">${f('client_note_hint', 'aq-outlined-input aq-note-input', 'textarea')}</div>
${err}
<div class="aq-btn-pill aq-btn-primary aq-mt">${f('save', 'aq-field-on-primary')}</div>
<div class="aq-btn-outline aq-note-cancel aq-mt">${f('cancel', 'aq-sm')}</div>
</div>
</div>`;
}
function homeHeaderChrome({ status = 'online', note = 'empty' } = {}) {
const statusChip = status === 'offline'
? `<div class="aq-status-chip offline">
<span class="aq-dot"></span>
${f('offline', 'aq-status-text')}
</div>`
: `<div class="aq-status-chip online">
<span class="aq-dot"></span>
${f('online', 'aq-status-text')}
</div>`;
const noteRow = note === 'none'
? ''
: clientNoteRow({
hasNote: note === 'filled' || note === 'pending',
pending: note === 'pending',
});
return `
<div class="aq-card aq-header-card">
<div class="aq-header-row">
<div class="aq-header-titles">
${f('hub_welcome_back', 'aq-muted aq-sm')}
<div class="aq-coach-name">demoCounselor</div>
</div>
${statusChip}
${iconHelp()}
${iconRefresh()}
</div>
<div class="aq-divider"></div>
<div class="aq-dual-chips">
<div class="aq-chip-col">
${f('language', 'aq-label-xs')}
<div class="aq-control-chip">Deutsch ${iconChevron()}</div>
</div>
<div class="aq-chip-col">
${f('active_client', 'aq-label-xs')}
<div class="aq-control-chip aq-control-chip-person">
<span class="aq-chip-lead">${iconPerson()}</span>
<span class="aq-chip-value">demoClient</span>
${iconChevron()}
</div>
</div>
</div>
${noteRow}
</div>`;
}
function homeDock() {
const iconReview = `<svg class="aq-dock-icon" viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/></svg>`;
const iconUpload = `<svg class="aq-dock-icon" viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><polyline points="16 16 12 12 8 16"/><line x1="12" y1="12" x2="12" y2="21"/><path d="M20.39 18.39A5 5 0 0018 9h-1.26A8 8 0 103 16.3"/></svg>`;
return `
<div class="aq-hub-dock">
<div class="aq-dock-btn review">${iconReview}${f('review_scores', 'aq-field-on-primary aq-xs')}</div>
<div class="aq-dock-btn upload">${iconUpload}${f('upload', 'aq-field-on-primary aq-xs')}</div>
</div>`;
}
function homeNextStep(titleKey, bodyKey, actionKey) {
return `
<div class="aq-next-step">
<div class="aq-eyebrow">${f('next_step_label', 'aq-eyebrow-field')}</div>
<div class="aq-next-step-body">
<div class="aq-next-step-copy">
${f(titleKey, 'aq-bold')}
${f(bodyKey, 'aq-muted aq-sm')}
</div>
${actionKey ? `<div class="aq-btn-pill aq-btn-primary aq-btn-compact">${f(actionKey, 'aq-field-on-primary aq-xs')}</div>` : ''}
</div>
</div>`;
}
/** In-group Ampelkategorie / Punkteprüfung badge (view_category_review_badge.xml). */
function categoryReviewBadge({ band = 'yellow' } = {}) {
const lamp = band === 'green'
? '<span class="lamp green"></span><span class="lamp off"></span><span class="lamp off"></span>'
: band === 'red'
? '<span class="lamp off"></span><span class="lamp off"></span><span class="lamp red"></span>'
: '<span class="lamp off"></span><span class="lamp yellow"></span><span class="lamp off"></span>';
return `
<div class="aq-category-review-badge">
<div class="aq-traffic aq-traffic-compact">${lamp}</div>
<div class="aq-category-review-copy">
${f('next_step_review_title', 'aq-category-review-title')}
${f('next_step_review_body', 'aq-category-review-body', 'textarea')}
</div>
</div>`;
}
/** Process-complete / Ampelkategorie panel (view_process_complete_panel.xml). */
function processCompleteBadge({
mode = 'complete',
showUpload = false,
} = {}) {
const review = mode === 'review';
const titleKey = review ? 'next_step_review_title' : 'process_complete_title';
const bodyKey = review ? 'next_step_review_body' : 'process_complete_hub_body';
const actionKey = review ? 'process_complete_set_category' : (
mode === 'edit' ? 'process_complete_edit_category' : 'process_complete_set_category'
);
const tone = review ? 'review' : 'complete';
const icon = review
? `<div class="aq-traffic aq-traffic-compact"><span class="lamp off"></span><span class="lamp yellow"></span><span class="lamp off"></span></div>`
: `<div class="aq-process-complete-icon-well" aria-hidden="true">✓</div>`;
const upload = showUpload
? `<div class="aq-btn-complete-outline aq-mt">${f('upload', 'aq-xs')}</div>`
: '';
return `
<div class="aq-process-complete aq-process-${tone}">
<div class="aq-process-complete-top">
${icon}
<div class="aq-process-complete-copy">
${f(titleKey, `aq-bold aq-process-complete-title`)}
${f(bodyKey, 'aq-sm aq-process-complete-body', 'textarea')}
</div>
</div>
<div class="aq-btn-complete aq-mt">${f(actionKey, 'aq-field-on-primary aq-xs')}</div>
${upload}
</div>`;
}
/** Save-only questionnaire footer (questionnaire_all_in_one has only btnSave, no previous). */
function qnFooter({ progressPct = 0, showProgress = true } = {}) {
const progress = showProgress
? `<div class="aq-progress-row">
${f('questionnaire_progress_answered', 'aq-muted aq-xs aq-progress-label-end')}
<div class="aq-progress-track"><div class="aq-progress-fill" style="width:${progressPct}%"></div></div>
</div>`
: '';
return `
<div class="aq-qn-footer">
${progress}
<div class="aq-btn-pill aq-btn-primary aq-mt">${f('save', 'aq-field-on-primary')}</div>
</div>`;
}
function helpShell(topicTitleKey, introKey, sections) {
const sectionHtml = sections.map((s, i) => `
<div class="aq-help-section${i ? ' aq-mt' : ''}">
<div class="aq-help-section-header">
<span class="aq-step">${i + 1}</span>
${f(s.titleKey, 'aq-bold aq-sm aq-help-section-title')}
</div>
<div class="aq-help-screenshot-box" aria-hidden="true"></div>
${f(s.bodyKey, 'aq-muted aq-xs aq-mt', 'textarea')}
</div>`).join('<div class="aq-divider aq-mt"></div>');
return `
<div class="aq-screen aq-help">
<div class="aq-toolbar">
<span class="aq-toolbar-back"></span>
${f('help_title', 'aq-screen-title')}
</div>
<div class="aq-help-body">
<div class="aq-card aq-picker-row">
${f('language', 'aq-muted aq-sm aq-picker-label')}
${langChip()}
</div>
<div class="aq-card aq-picker-row">
${f('help_topic', 'aq-muted aq-sm aq-picker-label')}
<div class="aq-control-chip">${f(topicTitleKey, 'aq-sm')} ${iconChevron()}</div>
</div>
<div class="aq-help-current">
<span class="aq-zone-pill">${f('help_this_page', 'aq-xs')}</span>
${f(topicTitleKey, 'aq-bold aq-mt')}
${f(introKey, 'aq-muted aq-sm', 'textarea')}
<div class="aq-divider"></div>
${sectionHtml}
</div>
</div>
</div>`;
}
function syncPhaseMock(statusKey, subtitleKey, progressPct, finished = false) {
const stage = finished
? `<div class="aq-sync-stage aq-sync-stage-done"><span class="aq-sync-check" aria-hidden="true">✓</span></div>`
: `<div class="aq-sync-stage"><div class="aq-sync-ring"></div></div>`;
// 7 checkpoint phases: preparing(10%), questionnaires(25%), client_info(40%), answers(55%), processing(70%), saving(85%), finished(100%)
const phases = [10, 25, 40, 55, 70, 85, 100];
const dots = phases.map(p => `<div class="aq-sync-dot${p <= progressPct ? ' done' : ''}"></div>`).join('');
return `
<div class="aq-screen aq-sync">
${f('sync_title', 'aq-muted aq-sync-title')}
${stage}
${f(statusKey, 'aq-sync-status')}
${f(subtitleKey, 'aq-muted aq-sm aq-sync-sub', 'textarea')}
<div class="aq-sync-bar"><div class="aq-sync-bar-fill" style="width:${progressPct}%"></div></div>
<div class="aq-sync-checkpoints" aria-hidden="true">${dots}</div>
</div>`;
}
/**
* Branded app dialog (dialog_app_message.xml).
* @param {'info'|'warning'|'logout'|'danger'} tone
*/
function dialogMock({
titleKey,
messageKey,
primaryKey,
secondaryKey = null,
detailsKeys = null,
tone = 'info',
}) {
const details = detailsKeys?.length
? `<div class="aq-dialog-details">${detailsKeys.map(k => f(k, 'aq-sm aq-dialog-detail-line')).join('')}</div>`
: '';
const secondary = secondaryKey
? `<div class="aq-btn-text">${f(secondaryKey, 'aq-muted')}</div>`
: '';
const primaryCls = tone === 'logout' || tone === 'danger'
? 'aq-btn-pill aq-btn-danger'
: 'aq-btn-pill aq-btn-primary';
return `
<div class="aq-screen aq-dialogs">
<div class="aq-dialog aq-dialog-${tone}">
<div class="aq-dialog-icon ${tone}" aria-hidden="true"></div>
${f(titleKey, 'aq-dialog-title')}
${f(messageKey, 'aq-muted aq-sm aq-dialog-msg', 'textarea')}
${details}
<div class="aq-dialog-actions-col">
<div class="${primaryCls}">${f(primaryKey, 'aq-field-on-primary')}</div>
${secondary}
</div>
</div>
</div>`;
}
function scoreCalculatedMock() {
return `
<div class="aq-screen aq-score-notice">
<div class="aq-score-hero">
<div class="aq-score-icon" aria-hidden="true">✓</div>
${f('score_calculated_eyebrow', 'aq-score-eyebrow')}
${f('score_calculated_title', 'aq-score-title')}
</div>
<div class="aq-score-body">
<div class="aq-card aq-score-card">
${f('score_calculated_result_label', 'aq-muted aq-xs')}
<div class="aq-bold aq-mt">RHS Index · 27.0</div>
<div class="aq-band mid aq-mt">${f('category_yellow', 'aq-xs')}</div>
${f('score_calculated_band_hint', 'aq-muted aq-xs aq-mt')}
</div>
<div class="aq-card aq-score-card">
${f('score_calculated_next_label', 'aq-muted aq-xs')}
${f('score_calculated_next_title', 'aq-bold aq-mt')}
${f('score_calculated_body', 'aq-muted aq-sm', 'textarea')}
</div>
<div class="aq-btn-pill aq-btn-primary">${f('score_calculated_continue', 'aq-field-on-primary')}</div>
</div>
</div>`;
}
/** @type {AppUiScreen[]} */
export const APP_UI_SCREENS = [
{
id: 'auth',
label: 'Anmeldung',
hint: 'activity_auth.xml',
keys: [
'auth_title', 'auth_subtitle_login', 'auth_subtitle_change_password',
'auth_footer_accounts', 'username_hint', 'password_hint', 'login_btn', 'exit_btn',
'new_password_hint', 'confirm_password_hint', 'save_password_btn',
'password_too_short', 'passwords_dont_match', 'please_username_password',
'password_needs_prefix', 'password_rule_min_length', 'password_rule_uppercase',
'password_rule_number', 'password_rule_special', 'login_failed_with_reason',
'language', 'help_title',
],
variants: [
{
id: 'login',
label: 'Login',
focusKeys: [
'auth_title', 'auth_subtitle_login', 'username_hint', 'password_hint',
'login_btn', 'exit_btn', 'auth_footer_accounts', 'language', 'help_title',
],
mock: () => `
<div class="aq-screen aq-auth">
<div class="aq-auth-top">
<div class="aq-logo-well" aria-hidden="true"></div>
<div class="aq-auth-top-right">${iconHelp()}${langChip()}</div>
</div>
${f('auth_title', 'aq-auth-title')}
${f('auth_subtitle_login', 'aq-muted aq-auth-sub')}
<div class="aq-card aq-card-header">
<div class="aq-outlined">
<span class="aq-outlined-icon">${iconPerson()}</span>
${f('username_hint', 'aq-outlined-input')}
</div>
<div class="aq-outlined">
<span class="aq-outlined-icon">${iconLock()}</span>
${f('password_hint', 'aq-outlined-input')}
</div>
<div class="aq-btn-pill aq-btn-primary">${f('login_btn', 'aq-field-on-primary')}</div>
<div class="aq-btn-text">${f('exit_btn', 'aq-muted')}</div>
</div>
<div class="aq-auth-footer">${f('auth_footer_accounts', 'aq-muted aq-sm')}</div>
</div>`,
},
{
id: 'login_error',
label: 'Login Fehler',
focusKeys: [
'please_username_password', 'login_failed_with_reason',
'username_hint', 'password_hint', 'login_btn', 'exit_btn',
],
mock: () => `
<div class="aq-screen aq-auth">
<div class="aq-auth-top">
<div class="aq-logo-well" aria-hidden="true"></div>
<div class="aq-auth-top-right">${iconHelp()}${langChip()}</div>
</div>
${f('auth_title', 'aq-auth-title')}
${f('auth_subtitle_login', 'aq-muted aq-auth-sub')}
<div class="aq-card aq-card-header">
<div class="aq-outlined">
<span class="aq-outlined-icon">${iconPerson()}</span>
${f('username_hint', 'aq-outlined-input')}
</div>
<div class="aq-outlined">
<span class="aq-outlined-icon">${iconLock()}</span>
${f('password_hint', 'aq-outlined-input')}
</div>
<div class="aq-btn-pill aq-btn-primary">${f('login_btn', 'aq-field-on-primary')}</div>
<div class="aq-btn-text">${f('exit_btn', 'aq-muted')}</div>
</div>
<div class="aq-auth-footer">${f('auth_footer_accounts', 'aq-muted aq-sm')}</div>
${toastBanner('please_username_password')}
</div>`,
},
{
id: 'login_failed',
label: 'Login fehlgeschlagen',
focusKeys: ['login_failed_with_reason', 'username_hint', 'password_hint', 'login_btn'],
mock: () => `
<div class="aq-screen aq-auth">
<div class="aq-auth-top">
<div class="aq-logo-well" aria-hidden="true"></div>
<div class="aq-auth-top-right">${iconHelp()}${langChip()}</div>
</div>
${f('auth_title', 'aq-auth-title')}
${f('auth_subtitle_login', 'aq-muted aq-auth-sub')}
<div class="aq-card aq-card-header">
<div class="aq-outlined">
<span class="aq-outlined-icon">${iconPerson()}</span>
${f('username_hint', 'aq-outlined-input')}
</div>
<div class="aq-outlined">
<span class="aq-outlined-icon">${iconLock()}</span>
${f('password_hint', 'aq-outlined-input')}
</div>
<div class="aq-btn-pill aq-btn-primary">${f('login_btn', 'aq-field-on-primary')}</div>
<div class="aq-btn-text">${f('exit_btn', 'aq-muted')}</div>
</div>
<div class="aq-auth-footer">${f('auth_footer_accounts', 'aq-muted aq-sm')}</div>
${toastBanner('login_failed_with_reason')}
</div>`,
},
{
id: 'password',
label: 'Passwort ändern',
focusKeys: [
'auth_title', 'auth_subtitle_change_password', 'new_password_hint',
'confirm_password_hint', 'save_password_btn', 'auth_footer_accounts',
'password_needs_prefix', 'password_rule_min_length', 'password_rule_uppercase',
'password_rule_number', 'password_rule_special',
],
mock: () => `
<div class="aq-screen aq-auth">
<div class="aq-auth-top">
<div class="aq-logo-well" aria-hidden="true"></div>
<div class="aq-auth-top-right">${iconHelp()}${langChip()}</div>
</div>
${f('auth_title', 'aq-auth-title')}
${f('auth_subtitle_change_password', 'aq-muted aq-auth-sub')}
<div class="aq-card aq-card-header">
<div class="aq-outlined">
<span class="aq-outlined-icon">${iconLock()}</span>
${f('new_password_hint', 'aq-outlined-input')}
</div>
<div class="aq-pw-reqs">
${f('password_needs_prefix', 'aq-xs')}
${f('password_rule_min_length', 'aq-xs')}
${f('password_rule_uppercase', 'aq-xs')}
${f('password_rule_number', 'aq-xs')}
${f('password_rule_special', 'aq-xs')}
</div>
<div class="aq-outlined">
<span class="aq-outlined-icon">${iconLock()}</span>
${f('confirm_password_hint', 'aq-outlined-input')}
</div>
<div class="aq-btn-pill aq-btn-primary">${f('save_password_btn', 'aq-field-on-primary')}</div>
</div>
<div class="aq-auth-footer">${f('auth_footer_accounts', 'aq-muted aq-sm')}</div>
</div>`,
},
{
id: 'password_error',
label: 'Passwort Fehler',
focusKeys: ['password_too_short', 'passwords_dont_match', 'new_password_hint', 'confirm_password_hint'],
mock: () => `
<div class="aq-screen aq-auth">
<div class="aq-auth-top">
<div class="aq-logo-well" aria-hidden="true"></div>
<div class="aq-auth-top-right">${iconHelp()}${langChip()}</div>
</div>
${f('auth_title', 'aq-auth-title')}
${f('auth_subtitle_change_password', 'aq-muted aq-auth-sub')}
<div class="aq-card aq-card-header">
<div class="aq-outlined">
<span class="aq-outlined-icon">${iconLock()}</span>
${f('new_password_hint', 'aq-outlined-input')}
</div>
<div class="aq-pw-reqs">
${f('password_needs_prefix', 'aq-xs')}
${f('password_rule_min_length', 'aq-xs')}
${f('password_rule_uppercase', 'aq-xs')}
${f('password_rule_number', 'aq-xs')}
${f('password_rule_special', 'aq-xs')}
</div>
<div class="aq-outlined">
<span class="aq-outlined-icon">${iconLock()}</span>
${f('confirm_password_hint', 'aq-outlined-input')}
</div>
<div class="aq-btn-pill aq-btn-primary">${f('save_password_btn', 'aq-field-on-primary')}</div>
</div>
<div class="aq-auth-footer">${f('auth_footer_accounts', 'aq-muted aq-sm')}</div>
${toastBanner('password_too_short')}
</div>`,
},
],
},
{
id: 'home',
label: 'Startseite',
hint: 'opening_screen.xml + hub dock',
keys: [
'hub_welcome_back', 'language', 'active_client', 'online', 'offline',
'settings', 'logout', 'next_step_label', 'next_step_questionnaire_title',
'next_step_questionnaire_body', 'next_step_review_title', 'next_step_review_body',
'next_step_upload_title', 'next_step_upload_body', 'next_step_completed_title',
'next_step_completed_body', 'next_step_loading_title', 'next_step_loading_body',
'next_step_show_questionnaire', 'questionnaire_title', 'start', 'locked',
'questionnaire_chip_edit', 'questionnaire_chip_edit_pending',
'process_complete_title', 'process_complete_set_category', 'process_complete_edit_category',
'upload', 'review_scores', 'refresh_complete',
'help_title', 'client_note_for_client', 'client_note_add', 'client_note_edit',
'client_note_add_action', 'client_note_pending', 'process_complete_hub_body',
'questionnaire_unlock_2_rhs', 'questionnaire_unlock_3_integration_index',
'questionnaire_unlock_4_consultation_results', 'consultation_unlock_requires_rhs',
'review_required_before_continue', 'download_failed_use_offline', 'no_profile',
'cancel', 'save', 'client_note_title', 'client_note_hint', 'client_note_too_long',
'client_note_saved', 'client_note_saved_pending',
],
variants: [
{
id: 'questionnaire',
label: 'Nächster Fragebogen',
focusKeys: [
'hub_welcome_back', 'language', 'active_client', 'online', 'next_step_label',
'next_step_questionnaire_title', 'next_step_questionnaire_body',
'next_step_show_questionnaire', 'questionnaire_title', 'start', 'locked',
'questionnaire_unlock_2_rhs', 'questionnaire_unlock_3_integration_index',
'consultation_unlock_requires_rhs', 'review_scores', 'upload',
'client_note_for_client', 'client_note_add', 'client_note_add_action',
],
mock: () => `
<div class="aq-screen aq-home">
<div class="aq-home-scroll">
${homeHeaderChrome()}
${homeNextStep('next_step_questionnaire_title', 'next_step_questionnaire_body', 'next_step_show_questionnaire')}
<div class="aq-section-title">${f('questionnaire_title', 'aq-section-title-field')}</div>
<div class="aq-qn-card open">
<span class="aq-qn-name">Demografie</span>
<span class="aq-chip-start">${f('start', 'aq-chip-field')}</span>
</div>
<div class="aq-qn-card locked">
<div>
<div class="aq-qn-name">RHS</div>
${f('questionnaire_unlock_2_rhs', 'aq-qn-lock-hint')}
</div>
<span class="aq-chip-locked">${f('locked', 'aq-chip-field')}</span>
</div>
<div class="aq-qn-card locked">
<div>
<div class="aq-qn-name">Integrationsindex</div>
${f('questionnaire_unlock_3_integration_index', 'aq-qn-lock-hint')}
</div>
<span class="aq-chip-locked">${f('locked', 'aq-chip-field')}</span>
</div>
<div class="aq-qn-card locked">
<div>
<div class="aq-qn-name">Beratung</div>
${f('consultation_unlock_requires_rhs', 'aq-qn-lock-hint')}
</div>
<span class="aq-chip-locked">${f('locked', 'aq-chip-field')}</span>
</div>
</div>
${homeDock()}
</div>`,
},
{
id: 'review',
label: 'Ampelkategorie',
focusKeys: [
'next_step_label', 'next_step_review_title', 'next_step_review_body',
'process_complete_set_category', 'review_scores', 'upload',
'questionnaire_chip_edit', 'questionnaire_title',
'review_required_before_continue',
],
mock: () => `
<div class="aq-screen aq-home">
<div class="aq-home-scroll">
${homeHeaderChrome({ note: 'filled' })}
${homeNextStep('next_step_review_title', 'next_step_review_body', 'process_complete_set_category')}
<div class="aq-section-title">${f('questionnaire_title', 'aq-section-title-field')}</div>
<div class="aq-qn-group">
<div class="aq-qn-card open">
<span class="aq-qn-name">Demografie</span>
<span class="aq-chip-edit">${f('questionnaire_chip_edit', 'aq-chip-field')}</span>
</div>
<div class="aq-qn-card open">
<span class="aq-qn-name">RHS</span>
<span class="aq-chip-edit">${f('questionnaire_chip_edit', 'aq-chip-field')}</span>
</div>
${categoryReviewBadge({ band: 'yellow' })}
</div>
</div>
${homeDock()}
${toastBanner('review_required_before_continue')}
</div>`,
},
{
id: 'upload',
label: 'Upload bereit',
focusKeys: [
'next_step_label', 'next_step_upload_title', 'next_step_upload_body',
'upload', 'review_scores', 'questionnaire_chip_edit_pending',
'client_note_pending', 'client_note_edit',
],
mock: () => `
<div class="aq-screen aq-home">
<div class="aq-home-scroll">
${homeHeaderChrome({ note: 'pending' })}
${homeNextStep('next_step_upload_title', 'next_step_upload_body', 'upload')}
<div class="aq-section-title">${f('questionnaire_title', 'aq-section-title-field')}</div>
<div class="aq-qn-card open">
<span class="aq-qn-name">Demografie</span>
<span class="aq-chip-edit-pending">${f('questionnaire_chip_edit_pending', 'aq-chip-field')}</span>
</div>
</div>
${homeDock()}
</div>`,
},
{
id: 'complete',
label: 'Abgeschlossen',
focusKeys: [
'process_complete_title', 'process_complete_hub_body',
'process_complete_set_category', 'upload',
],
mock: () => `
<div class="aq-screen aq-home">
<div class="aq-home-scroll">
${homeHeaderChrome()}
${processCompleteBadge({ mode: 'complete', showUpload: true })}
</div>
${homeDock()}
</div>`,
},
{
id: 'complete_edit',
label: 'Abgeschlossen · Kategorie',
focusKeys: [
'process_complete_title', 'process_complete_hub_body',
'process_complete_edit_category',
],
mock: () => `
<div class="aq-screen aq-home">
<div class="aq-home-scroll">
${homeHeaderChrome()}
${processCompleteBadge({ mode: 'edit' })}
</div>
${homeDock()}
</div>`,
},
{
id: 'set_category',
label: 'Ampelkategorie setzen',
focusKeys: [
'next_step_review_title', 'next_step_review_body',
'process_complete_set_category',
],
mock: () => `
<div class="aq-screen aq-home">
<div class="aq-home-scroll">
${homeHeaderChrome()}
${processCompleteBadge({ mode: 'review' })}
</div>
${homeDock()}
</div>`,
},
{
id: 'completed_banner',
label: 'Nächster Schritt · Fertig',
focusKeys: [
'next_step_label', 'next_step_completed_title', 'next_step_completed_body',
'review_scores', 'upload', 'questionnaire_chip_edit',
],
mock: () => `
<div class="aq-screen aq-home">
<div class="aq-home-scroll">
${homeHeaderChrome()}
${homeNextStep('next_step_completed_title', 'next_step_completed_body', null)}
<div class="aq-section-title">${f('questionnaire_title', 'aq-section-title-field')}</div>
<div class="aq-qn-card open">
<span class="aq-qn-name">Demografie</span>
<span class="aq-chip-edit">${f('questionnaire_chip_edit', 'aq-chip-field')}</span>
</div>
</div>
${homeDock()}
</div>`,
},
{
id: 'loading',
label: 'Laden',
focusKeys: [
'next_step_loading_title', 'next_step_loading_body', 'next_step_label',
],
mock: () => `
<div class="aq-screen aq-home">
<div class="aq-home-scroll">
${homeHeaderChrome()}
<div class="aq-next-step">
<div class="aq-eyebrow">${f('next_step_label', 'aq-eyebrow-field')}</div>
<div class="aq-next-step-body">
<div class="aq-next-step-copy">
${f('next_step_loading_title', 'aq-bold')}
${f('next_step_loading_body', 'aq-muted aq-sm')}
</div>
</div>
</div>
</div>
${homeDock()}
</div>`,
},
{
id: 'offline',
label: 'Offline',
focusKeys: [
'offline', 'hub_welcome_back', 'active_client', 'download_failed_use_offline',
],
mock: () => `
<div class="aq-screen aq-home">
<div class="aq-home-scroll">
${homeHeaderChrome({ status: 'offline' })}
${homeNextStep('next_step_questionnaire_title', 'next_step_questionnaire_body', 'next_step_show_questionnaire')}
</div>
${homeDock()}
${toastBanner('download_failed_use_offline')}
</div>`,
},
{
id: 'settings',
label: 'Einstellungen',
focusKeys: ['settings', 'logout'],
mock: () => `
<div class="aq-screen aq-home">
<div class="aq-home-scroll">
${homeHeaderChrome()}
${homeNextStep('next_step_questionnaire_title', 'next_step_questionnaire_body', 'next_step_show_questionnaire')}
</div>
${homeDock()}
${settingsSheet()}
</div>`,
},
{
id: 'refresh_done',
label: 'Aktualisiert',
focusKeys: ['refresh_complete'],
mock: () => `
<div class="aq-screen aq-home">
<div class="aq-home-scroll">
${homeHeaderChrome()}
${homeNextStep('next_step_questionnaire_title', 'next_step_questionnaire_body', 'next_step_show_questionnaire')}
</div>
${homeDock()}
${toastBanner('refresh_complete')}
</div>`,
},
{
id: 'note_editor',
label: 'Notiz bearbeiten',
focusKeys: [
'client_note_title', 'client_note_for_client', 'client_note_hint',
'client_note_too_long', 'cancel', 'save',
],
mock: () => `
<div class="aq-screen aq-home">
<div class="aq-home-scroll">
${homeHeaderChrome({ note: 'filled' })}
${homeNextStep('next_step_questionnaire_title', 'next_step_questionnaire_body', 'next_step_show_questionnaire')}
</div>
${homeDock()}
${clientNoteEditorSheet({ errorKey: 'client_note_too_long' })}
</div>`,
},
{
id: 'note_saved',
label: 'Notiz gespeichert',
focusKeys: [
'client_note_saved', 'client_note_saved_pending',
'client_note_for_client', 'client_note_edit', 'client_note_pending',
],
mock: () => `
<div class="aq-screen aq-home">
<div class="aq-home-scroll">
${homeHeaderChrome({ note: 'pending' })}
${homeNextStep('next_step_questionnaire_title', 'next_step_questionnaire_body', 'next_step_show_questionnaire')}
</div>
${homeDock()}
${toastBanner('client_note_saved_pending')}
</div>`,
},
],
},
{
id: 'client',
label: 'Klient wählen',
hint: 'view_client_select_gate.xml',
keys: [
'client_select_gate_title', 'client_select_gate_body', 'client_select_gate_hint',
'client_select_gate_count', 'client_picker_title', 'client_picker_subtitle',
'client_picker_search', 'client_picker_current', 'client_picker_recent',
'client_picker_all', 'client_picker_empty', 'client_picker_status_finished',
'client_picker_status_started', 'client_picker_status_new',
'client_picker_status_finished_pending_upload', 'client_picker_status_pending_upload',
'client_picker_status_next_step_category', 'client_picker_status_next',
'client_picker_category_assigned', 'client_picker_category_calculated',
'client_picker_score', 'client_picker_loading_title', 'client_picker_loading_body',
'active_client', 'language', 'no_clients_assigned', 'client', 'client_note',
],
variants: [
{
id: 'gate',
label: 'Gate',
focusKeys: [
'client_select_gate_title', 'client_select_gate_body', 'client_select_gate_hint',
'client_select_gate_count', 'client_picker_search', 'language',
'client_picker_status_finished', 'client_picker_status_started',
'client_picker_status_new',
],
mock: () => `
<div class="aq-screen aq-client-gate">
<div class="aq-card aq-header-card">
<div class="aq-gate-title-row">
<div class="aq-avatar-well">${iconPerson()}</div>
${f('client_select_gate_title', 'aq-screen-title')}
</div>
${f('client_select_gate_body', 'aq-body')}
${f('client_select_gate_hint', 'aq-muted aq-sm')}
${f('client_select_gate_count', 'aq-count')}
${f('language', 'aq-label-xs')}
<div class="aq-control-chip">Deutsch ${iconChevron()}</div>
</div>
<div class="aq-outlined aq-search">
<span class="aq-outlined-icon">${iconPerson()}</span>
${f('client_picker_search', 'aq-outlined-input')}
</div>
<div class="aq-client-card">
<div class="aq-client-stripe"></div>
<div class="aq-client-card-body">
<div class="aq-client-card-top">
<div class="aq-avatar-sm">${iconPerson()}</div>
<strong>demoClient</strong>
<span class="aq-badge-done">${f('client_picker_status_finished', 'aq-xs')}</span>
</div>
</div>
</div>
<div class="aq-client-card">
<div class="aq-client-stripe"></div>
<div class="aq-client-card-body">
<div class="aq-client-card-top">
<div class="aq-avatar-sm">${iconPerson()}</div>
<strong>demoClient2</strong>
</div>
<div class="aq-muted aq-xs">${f('client_picker_status_new', 'aq-xs')}</div>
</div>
</div>
</div>`,
},
{
id: 'list',
label: 'Liste',
focusKeys: [
'client_picker_title', 'client_picker_subtitle', 'client_picker_recent',
'client_picker_all', 'client_picker_current', 'client_picker_score',
'client_picker_category_assigned', 'client_picker_category_calculated',
'client_picker_status_finished_pending_upload', 'client_picker_status_pending_upload',
'client_picker_status_next_step_category', 'client_picker_status_next',
'client', 'client_picker_search',
],
mock: () => `
<div class="aq-screen aq-client-gate">
<div class="aq-card aq-header-card">
${f('client_picker_title', 'aq-screen-title')}
${f('client_picker_subtitle', 'aq-muted aq-sm')}
<div class="aq-outlined aq-search aq-mt">
<span class="aq-outlined-icon">${iconPerson()}</span>
${f('client_picker_search', 'aq-outlined-input')}
</div>
</div>
<div class="aq-label-xs aq-mt">${f('client_picker_current', 'aq-label-xs')}</div>
<div class="aq-client-card">
<div class="aq-client-stripe"></div>
<div class="aq-client-card-body">
<div class="aq-client-card-top">
<div class="aq-avatar-sm">${iconPerson()}</div>
<strong>${f('client', 'aq-xs')} demoA</strong>
</div>
<div class="aq-muted aq-xs">${f('client_picker_status_next', 'aq-xs')}</div>
<div class="aq-muted aq-xs">${f('client_picker_score', 'aq-xs')}: 27.0</div>
<div class="aq-muted aq-xs">${f('client_picker_category_calculated', 'aq-xs')}</div>
</div>
</div>
<div class="aq-label-xs aq-mt">${f('client_picker_recent', 'aq-label-xs')}</div>
<div class="aq-label-xs">${f('client_picker_all', 'aq-label-xs')}</div>
<div class="aq-client-card">
<div class="aq-client-stripe"></div>
<div class="aq-client-card-body">
<div class="aq-client-card-top">
<div class="aq-avatar-sm">${iconPerson()}</div>
<strong>demoB</strong>
<span class="aq-badge-done">${f('client_picker_status_pending_upload', 'aq-xs')}</span>
</div>
<div class="aq-muted aq-xs">${f('client_picker_status_finished_pending_upload', 'aq-xs')}</div>
<div class="aq-muted aq-xs">${f('client_picker_status_next_step_category', 'aq-xs')}</div>
<div class="aq-muted aq-xs">${f('client_picker_category_assigned', 'aq-xs')}</div>
</div>
</div>
</div>`,
},
{
id: 'loading',
label: 'Laden',
focusKeys: ['client_picker_loading_title', 'client_picker_loading_body'],
mock: () => `
<div class="aq-screen aq-client-gate">
<div class="aq-card aq-header-card">
${f('client_picker_loading_title', 'aq-screen-title')}
${f('client_picker_loading_body', 'aq-muted aq-sm', 'textarea')}
</div>
</div>`,
},
{
id: 'empty',
label: 'Leer',
focusKeys: ['client_picker_empty', 'no_clients_assigned'],
mock: () => `
<div class="aq-screen aq-client-gate">
<div class="aq-card aq-header-card">
${f('client_picker_title', 'aq-screen-title')}
${f('client_picker_empty', 'aq-muted aq-sm', 'textarea')}
${f('no_clients_assigned', 'aq-muted aq-sm aq-mt', 'textarea')}
</div>
</div>`,
},
],
},
{
id: 'questionnaire',
label: 'Fragebogen',
hint: 'questionnaire_all_in_one.xml · pick a question type',
keys: [
'coach_code', 'client_code', 'client_note_for_client', 'client_note_add',
'client_note_edit', 'client_note_add_action', 'client_note_pending',
'client_note_title', 'client_note_hint', 'client_note_too_long', 'cancel',
'save', 'previous', 'choose_answer', 'fill_all_fields', 'select_one_answer',
'select_one_answer_per_row', 'choose_more_elements', 'questionnaire_progress_answered',
'no_questions_available', 'questionnaire_missing_options', 'help_title', 'language',
'please_client_code', 'fill_both_fields', 'client_code_exists', 'no_profile',
'other_option', 'other_country', 'january', 'february', 'march', 'april', 'may',
'june', 'july', 'august', 'september', 'october', 'november', 'december',
],
variants: [
{
id: 'codes',
label: 'Codes',
focusKeys: [
'client_code', 'coach_code', 'please_client_code', 'fill_both_fields',
'client_code_exists', 'no_profile', 'previous', 'save',
'questionnaire_progress_answered', 'client_note_for_client',
'client_note_add', 'client_note_add_action',
],
mock: () => `
<div class="aq-screen aq-qn">
<div class="aq-qn-top">
${iconBack()}
<div class="aq-qn-top-right">${iconHelp()}${langChip()}</div>
</div>
<div class="aq-note-inset">${clientNoteRow()}</div>
<div class="aq-qn-scroll">
<div class="aq-card aq-q-card">
<div class="aq-muted aq-sm">${f('client_code', 'aq-sm')}</div>
<div class="aq-outlined aq-mt"><span class="aq-sample">demoClient</span></div>
<div class="aq-muted aq-sm aq-mt">${f('coach_code', 'aq-sm')}</div>
<div class="aq-outlined aq-mt"><span class="aq-sample">demoCounselor</span> ${iconLock()}</div>
</div>
</div>
${qnFooter({ progressPct: 5 })}
${toastBanner('fill_both_fields')}
</div>`,
},
{
id: 'choice',
label: 'Auswahl',
focusKeys: [
'choose_answer', 'other_option', 'other_country', 'select_one_answer',
'choose_more_elements', 'previous', 'save', 'questionnaire_progress_answered',
],
mock: () => `
<div class="aq-screen aq-qn">
<div class="aq-qn-top">
${iconBack()}
<div class="aq-qn-top-right">${iconHelp()}${langChip()}</div>
</div>
<div class="aq-qn-scroll">
<div class="aq-card aq-q-card">
<div class="aq-bold aq-sm">Beispiel-Fragetext</div>
<div class="aq-option-row aq-mt"><span class="aq-radio"></span> Option A</div>
<div class="aq-option-row"><span class="aq-radio"></span> ${f('other_option', 'aq-sm')}</div>
<div class="aq-option-row"><span class="aq-radio"></span> ${f('other_country', 'aq-sm')}</div>
<div class="aq-spinner-prompt aq-mt">${f('choose_answer', 'aq-sm')}</div>
</div>
</div>
${qnFooter({ progressPct: 35 })}
${toastBanner('select_one_answer')}
</div>`,
},
{
id: 'multi',
label: 'Mehrfachauswahl',
focusKeys: [
'choose_more_elements', 'other_option', 'previous', 'save',
'questionnaire_progress_answered',
],
mock: () => `
<div class="aq-screen aq-qn">
<div class="aq-qn-top">
${iconBack()}
<div class="aq-qn-top-right">${iconHelp()}${langChip()}</div>
</div>
<div class="aq-qn-scroll">
<div class="aq-card aq-q-card">
<div class="aq-bold aq-sm">Beispiel-Mehrfachauswahl</div>
<div class="aq-option-row aq-mt"><span class="aq-check">☑</span> Option A</div>
<div class="aq-option-row"><span class="aq-check">☐</span> ${f('other_option', 'aq-sm')}</div>
</div>
</div>
${qnFooter({ progressPct: 40 })}
${toastBanner('choose_more_elements')}
</div>`,
},
{
id: 'date',
label: 'Datum',
focusKeys: [
'choose_answer', 'january', 'february', 'march', 'april', 'may', 'june',
'july', 'august', 'september', 'october', 'november', 'december',
'fill_all_fields', 'previous', 'save',
],
mock: () => `
<div class="aq-screen aq-qn">
<div class="aq-qn-top">
${iconBack()}
<div class="aq-qn-top-right">${iconHelp()}${langChip()}</div>
</div>
<div class="aq-qn-scroll">
<div class="aq-card aq-q-card">
<div class="aq-bold aq-sm">Beispiel-Datumsfrage</div>
<div class="aq-date-spinners aq-mt">
<div class="aq-date-col">
<div class="aq-spinner-field">15</div>
</div>
<div class="aq-date-col aq-date-col-open">
<div class="aq-spinner-field">${f('march', 'aq-sm')}</div>
<div class="aq-spinner-dropdown">
${f('january', 'aq-xs')}
${f('february', 'aq-xs')}
${f('march', 'aq-xs')}
${f('april', 'aq-xs')}
${f('may', 'aq-xs')}
${f('june', 'aq-xs')}
${f('july', 'aq-xs')}
${f('august', 'aq-xs')}
${f('september', 'aq-xs')}
${f('october', 'aq-xs')}
${f('november', 'aq-xs')}
${f('december', 'aq-xs')}
</div>
</div>
<div class="aq-date-col">
<div class="aq-spinner-field">${f('choose_answer', 'aq-xs')}</div>
</div>
</div>
</div>
</div>
${qnFooter({ progressPct: 0, showProgress: false })}
${toastBanner('fill_all_fields')}
</div>`,
},
{
id: 'empty',
label: 'Leer / Fehler',
focusKeys: [
'no_questions_available', 'questionnaire_missing_options', 'previous',
],
mock: () => `
<div class="aq-screen aq-qn">
<div class="aq-qn-top">
${iconBack()}
<div class="aq-qn-top-right">${iconHelp()}${langChip()}</div>
</div>
<div class="aq-qn-scroll">
<div class="aq-card aq-q-card">
${f('no_questions_available', 'aq-muted aq-sm', 'textarea')}
</div>
</div>
${qnFooter({ progressPct: 0, showProgress: false })}
${toastBanner('questionnaire_missing_options')}
</div>`,
},
{
id: 'note',
label: 'Notiz',
focusKeys: [
'client_note_title', 'client_note_for_client', 'client_note_hint',
'client_note_too_long', 'cancel', 'save', 'client_note_edit',
],
mock: () => `
<div class="aq-screen aq-qn">
<div class="aq-qn-top">
${iconBack()}
<div class="aq-qn-top-right">${iconHelp()}${langChip()}</div>
</div>
<div class="aq-note-inset">${clientNoteRow({ hasNote: true })}</div>
<div class="aq-qn-scroll">
<div class="aq-card aq-q-card">
<div class="aq-bold aq-sm">Beispiel-Fragetext</div>
</div>
</div>
${clientNoteEditorSheet({ errorKey: 'client_note_too_long' })}
</div>`,
},
],
},
{
id: 'review',
label: 'Ampelkategorie',
hint: 'review_scores_screen.xml',
keys: [
'review_scores', 'review_scores_subtitle', 'review_scores_agree',
'review_scores_calculated_category', 'review_scores_coach_category',
'review_scores_empty', 'review_scores_pending_upload', 'review_scores_complete_title',
'review_scores_complete_body', 'review_scores_complete_body_uploaded',
'review_scores_done', 'review_scores_save_failed', 'review_scores_saved',
'review_scores_saved_pending', 'review_scores_set_category',
'review_scores_supervisor_notice', 'review_scores_supervisor_confirm_title',
'review_scores_supervisor_confirm_body', 'review_scores_supervisor_confirm_action',
'review_scores_total', 'category_green', 'category_yellow', 'category_red',
'score_calculated_title', 'score_calculated_body', 'score_calculated_eyebrow',
'score_calculated_result_label', 'score_calculated_next_label',
'score_calculated_next_title', 'score_calculated_band_hint', 'score_calculated_continue',
'points', 'help_title', 'not_done', 'previous',
],
variants: [
{
id: 'active',
label: 'Aktiv',
focusKeys: [
'review_scores', 'review_scores_subtitle', 'review_scores_agree',
'review_scores_calculated_category', 'review_scores_coach_category',
'review_scores_total', 'review_scores_set_category', 'category_green',
'category_yellow', 'category_red', 'not_done', 'points', 'previous',
'review_scores_supervisor_notice',
],
mock: () => `
<div class="aq-screen aq-review">
<div class="aq-review-header">
<div>
${f('review_scores', 'aq-screen-title')}
${f('review_scores_subtitle', 'aq-muted aq-sm')}
</div>
<div class="aq-review-header-actions">${langChip()}${iconHelp()}</div>
</div>
<div class="aq-supervisor-notice">${f('review_scores_supervisor_notice', 'aq-sm')}</div>
<div class="aq-card aq-profile-card">
<div class="aq-profile-top">
<div class="aq-traffic">
<span class="lamp off"></span>
<span class="lamp yellow"></span>
<span class="lamp off"></span>
</div>
<div>
<div class="aq-bold">RHS Index</div>
<div class="aq-muted aq-sm">${f('review_scores_total', 'aq-xs')}: 27.0 ${f('points', 'aq-xs')}</div>
</div>
</div>
<div class="aq-band-row">
<span class="aq-muted aq-xs">${f('review_scores_calculated_category', 'aq-xs')}</span>
<span class="aq-band mid">${f('category_yellow', 'aq-xs')}</span>
</div>
<div class="aq-band-row">
<span class="aq-muted aq-xs">${f('review_scores_coach_category', 'aq-xs')}</span>
<span class="aq-band none">${f('not_done', 'aq-xs')}</span>
</div>
<div class="aq-btn-pill aq-btn-primary aq-mt">${f('review_scores_agree', 'aq-field-on-primary aq-xs')}</div>
<div class="aq-label-xs aq-mt">${f('review_scores_set_category', 'aq-label-xs')}</div>
<div class="aq-band-btns">
<span class="aq-band-btn green">${f('category_green', 'aq-xs')}</span>
<span class="aq-band-btn yellow">${f('category_yellow', 'aq-xs')}</span>
<span class="aq-band-btn red">${f('category_red', 'aq-xs')}</span>
</div>
</div>
<div class="aq-btn-outline aq-review-previous">${f('previous', 'aq-sm')}</div>
</div>`,
},
{
id: 'empty',
label: 'Leer',
focusKeys: ['review_scores_empty', 'review_scores'],
mock: () => `
<div class="aq-screen aq-review">
<div class="aq-review-header">
<div>
${f('review_scores', 'aq-screen-title')}
${f('review_scores_empty', 'aq-muted aq-sm', 'textarea')}
</div>
<div class="aq-review-header-actions">${langChip()}${iconHelp()}</div>
</div>
</div>`,
},
{
id: 'complete',
label: 'Abgeschlossen',
focusKeys: [
'review_scores_complete_title', 'review_scores_complete_body',
'review_scores_complete_body_uploaded', 'review_scores_done',
'review_scores_pending_upload', 'review_scores_saved_pending', 'previous',
],
mock: () => `
<div class="aq-screen aq-review">
<div class="aq-review-header">
<div>
${f('review_scores', 'aq-screen-title')}
${f('review_scores_subtitle', 'aq-muted aq-sm')}
</div>
<div class="aq-review-header-actions">${langChip()}${iconHelp()}</div>
</div>
<div class="aq-card aq-profile-card">
${f('review_scores_complete_title', 'aq-bold')}
${f('review_scores_complete_body', 'aq-muted aq-sm', 'textarea')}
<div class="aq-muted aq-sm aq-mt">${f('review_scores_pending_upload', 'aq-sm')}</div>
<div class="aq-btn-pill aq-btn-primary aq-mt">${f('review_scores_done', 'aq-field-on-primary aq-xs')}</div>
</div>
<div class="aq-btn-outline aq-review-previous">${f('previous', 'aq-sm')}</div>
${toastBanner('review_scores_saved_pending')}
</div>`,
},
],
},
{
id: 'upload',
label: 'Hochladen',
hint: 'upload_overview_screen.xml',
keys: [
'upload', 'start_upload', 'upload_pending_subtitle', 'upload_summary',
'upload_nothing_pending', 'upload_prepare_failed', 'upload_failed_title',
'upload_success_title', 'upload_success_message', 'upload_success_questionnaires',
'upload_success_scoring_reviews', 'upload_success_client_notes',
'upload_success_sync_next', 'upload_item_scoring_review', 'upload_scoring_review_summary',
'cancel', 'previous', 'help_title', 'questions_filled', 'done', 'client_code',
'upload_connect_network_first', 'no_questionnaires', 'login_required', 'client_note',
],
variants: [
{
id: 'pending',
label: 'Ausstehend',
focusKeys: [
'upload_pending_subtitle', 'start_upload', 'upload_summary', 'upload',
'cancel', 'previous', 'client_code', 'done', 'questions_filled',
'upload_item_scoring_review', 'upload_scoring_review_summary',
'upload_connect_network_first', 'client_note',
],
mock: () => `
<div class="aq-screen aq-upload">
<div class="aq-card aq-header-card">
<div class="aq-header-row">
<div>
${f('upload_pending_subtitle', 'aq-muted aq-sm')}
${f('start_upload', 'aq-screen-title')}
</div>
<div class="aq-review-header-actions">${langChip()}${iconHelp()}</div>
</div>
<div class="aq-divider"></div>
<div class="aq-control-chip aq-summary-chip">${f('upload_summary', 'aq-sm')}</div>
</div>
<div class="aq-card aq-list-card">
<div class="aq-client-header">${f('client_code', 'aq-xs')}: demoClient</div>
<div class="aq-upload-row">
<div>
<div class="aq-bold">Demografie</div>
<div class="aq-muted aq-xs">✓ ${f('done', 'aq-xs')} · 12 ${f('questions_filled', 'aq-xs')}</div>
</div>
<span class="aq-chevron"></span>
</div>
<div class="aq-upload-row">
<div>
<div class="aq-bold">${f('upload_item_scoring_review', 'aq-sm')}</div>
<div class="aq-muted aq-xs">${f('upload_scoring_review_summary', 'aq-xs')}</div>
</div>
</div>
<div class="aq-upload-row">
<div>
<div class="aq-bold">${f('client_note', 'aq-sm')}</div>
</div>
</div>
</div>
<div class="aq-upload-footer">
<div class="aq-btn-outline">${f('cancel', 'aq-sm')}</div>
<div class="aq-btn-square primary">${f('upload', 'aq-field-on-primary aq-sm')}</div>
</div>
</div>`,
},
{
id: 'empty',
label: 'Leer',
focusKeys: [
'upload_nothing_pending', 'no_questionnaires', 'start_upload',
'login_required', 'upload_connect_network_first',
],
mock: () => `
<div class="aq-screen aq-upload">
<div class="aq-card aq-header-card">
${f('start_upload', 'aq-screen-title')}
${f('upload_nothing_pending', 'aq-muted aq-sm aq-mt', 'textarea')}
${f('no_questionnaires', 'aq-muted aq-sm aq-mt', 'textarea')}
</div>
<div class="aq-upload-footer-hint">
${f('upload_connect_network_first', 'aq-muted aq-sm')}
</div>
${toastBanner('login_required')}
</div>`,
},
],
},
{
id: 'help',
label: 'Hilfe',
hint: 'activity_help.xml · pick a topic below',
keys: [
'help_title', 'help_topic', 'help_this_page', 'language',
'help_auth_title', 'help_auth_intro', 'help_auth_signin_title', 'help_auth_signin_body',
'help_auth_password_title', 'help_auth_password_body',
'help_opening_title', 'help_opening_intro', 'help_opening_pick_client_title',
'help_opening_pick_client_body', 'help_opening_home_title', 'help_opening_home_body',
'help_opening_client_lang_title', 'help_opening_client_lang_body',
'help_opening_questionnaires_title', 'help_opening_questionnaires_body',
'help_opening_complete_title', 'help_opening_complete_body',
'help_questionnaire_title', 'help_questionnaire_intro', 'help_questionnaire_header_title',
'help_questionnaire_header_body', 'help_questionnaire_answer_title',
'help_questionnaire_answer_body', 'help_questionnaire_save_title',
'help_questionnaire_save_body',
'help_category_title', 'help_category_intro', 'help_category_set_title',
'help_category_set_body', 'help_category_change_title', 'help_category_change_body',
'help_upload_title', 'help_upload_intro', 'help_upload_check_title',
'help_upload_check_body', 'help_upload_fix_title', 'help_upload_fix_body',
'help_upload_send_title', 'help_upload_send_body',
],
variants: [
{
id: 'auth',
label: 'Anmeldung',
focusKeys: [
'help_title', 'help_topic', 'help_this_page', 'language',
'help_auth_title', 'help_auth_intro', 'help_auth_signin_title',
'help_auth_signin_body', 'help_auth_password_title', 'help_auth_password_body',
],
mock: () => helpShell('help_auth_title', 'help_auth_intro', [
{ titleKey: 'help_auth_signin_title', bodyKey: 'help_auth_signin_body' },
{ titleKey: 'help_auth_password_title', bodyKey: 'help_auth_password_body' },
]),
},
{
id: 'opening',
label: 'Startseite',
focusKeys: [
'help_title', 'help_topic', 'help_this_page', 'language',
'help_opening_title', 'help_opening_intro', 'help_opening_pick_client_title',
'help_opening_pick_client_body', 'help_opening_home_title', 'help_opening_home_body',
'help_opening_client_lang_title', 'help_opening_client_lang_body',
'help_opening_questionnaires_title', 'help_opening_questionnaires_body',
'help_opening_complete_title', 'help_opening_complete_body',
],
mock: () => helpShell('help_opening_title', 'help_opening_intro', [
{ titleKey: 'help_opening_pick_client_title', bodyKey: 'help_opening_pick_client_body' },
{ titleKey: 'help_opening_home_title', bodyKey: 'help_opening_home_body' },
{ titleKey: 'help_opening_client_lang_title', bodyKey: 'help_opening_client_lang_body' },
{ titleKey: 'help_opening_questionnaires_title', bodyKey: 'help_opening_questionnaires_body' },
{ titleKey: 'help_opening_complete_title', bodyKey: 'help_opening_complete_body' },
]),
},
{
id: 'questionnaire',
label: 'Fragebogen',
focusKeys: [
'help_title', 'help_topic', 'help_this_page', 'language',
'help_questionnaire_title', 'help_questionnaire_intro',
'help_questionnaire_header_title', 'help_questionnaire_header_body',
'help_questionnaire_answer_title', 'help_questionnaire_answer_body',
'help_questionnaire_save_title', 'help_questionnaire_save_body',
],
mock: () => helpShell('help_questionnaire_title', 'help_questionnaire_intro', [
{ titleKey: 'help_questionnaire_header_title', bodyKey: 'help_questionnaire_header_body' },
{ titleKey: 'help_questionnaire_answer_title', bodyKey: 'help_questionnaire_answer_body' },
{ titleKey: 'help_questionnaire_save_title', bodyKey: 'help_questionnaire_save_body' },
]),
},
{
id: 'category',
label: 'Ampelkategorie',
focusKeys: [
'help_title', 'help_topic', 'help_this_page', 'language',
'help_category_title', 'help_category_intro',
'help_category_set_title', 'help_category_set_body',
'help_category_change_title', 'help_category_change_body',
],
mock: () => helpShell('help_category_title', 'help_category_intro', [
{ titleKey: 'help_category_set_title', bodyKey: 'help_category_set_body' },
{ titleKey: 'help_category_change_title', bodyKey: 'help_category_change_body' },
]),
},
{
id: 'upload',
label: 'Hochladen',
focusKeys: [
'help_title', 'help_topic', 'help_this_page', 'language',
'help_upload_title', 'help_upload_intro',
'help_upload_check_title', 'help_upload_check_body',
'help_upload_fix_title', 'help_upload_fix_body',
'help_upload_send_title', 'help_upload_send_body',
],
mock: () => helpShell('help_upload_title', 'help_upload_intro', [
{ titleKey: 'help_upload_check_title', bodyKey: 'help_upload_check_body' },
{ titleKey: 'help_upload_fix_title', bodyKey: 'help_upload_fix_body' },
{ titleKey: 'help_upload_send_title', bodyKey: 'help_upload_send_body' },
]),
},
],
},
{
id: 'sync',
label: 'Synchronisation',
hint: 'view_sync_progress_overlay.xml · pick a phase below',
keys: [
'sync_title',
'sync_status_preparing', 'sync_status_downloading_questionnaires',
'sync_status_downloading_client_info', 'sync_status_downloading_answers',
'sync_status_processing', 'sync_status_securely_saving', 'sync_status_finished',
'sync_subtitle_preparing', 'sync_subtitle_downloading_questionnaires',
'sync_subtitle_downloading_client_info', 'sync_subtitle_downloading_answers',
'sync_subtitle_processing', 'sync_subtitle_securely_saving', 'sync_subtitle_finished',
],
variants: [
{
id: 'preparing',
label: 'Vorbereitung',
focusKeys: ['sync_title', 'sync_status_preparing', 'sync_subtitle_preparing'],
mock: () => syncPhaseMock('sync_status_preparing', 'sync_subtitle_preparing', 10),
},
{
id: 'questionnaires',
label: 'Fragebögen',
focusKeys: [
'sync_title', 'sync_status_downloading_questionnaires',
'sync_subtitle_downloading_questionnaires',
],
mock: () => syncPhaseMock(
'sync_status_downloading_questionnaires',
'sync_subtitle_downloading_questionnaires',
25,
),
},
{
id: 'client_info',
label: 'Klienten',
focusKeys: [
'sync_title', 'sync_status_downloading_client_info',
'sync_subtitle_downloading_client_info',
],
mock: () => syncPhaseMock(
'sync_status_downloading_client_info',
'sync_subtitle_downloading_client_info',
40,
),
},
{
id: 'answers',
label: 'Antworten',
focusKeys: [
'sync_title', 'sync_status_downloading_answers',
'sync_subtitle_downloading_answers',
],
mock: () => syncPhaseMock(
'sync_status_downloading_answers',
'sync_subtitle_downloading_answers',
55,
),
},
{
id: 'processing',
label: 'Verarbeitung',
focusKeys: ['sync_title', 'sync_status_processing', 'sync_subtitle_processing'],
mock: () => syncPhaseMock('sync_status_processing', 'sync_subtitle_processing', 70),
},
{
id: 'saving',
label: 'Speichern',
focusKeys: [
'sync_title', 'sync_status_securely_saving', 'sync_subtitle_securely_saving',
],
mock: () => syncPhaseMock(
'sync_status_securely_saving',
'sync_subtitle_securely_saving',
85,
),
},
{
id: 'finished',
label: 'Fertig',
focusKeys: ['sync_title', 'sync_status_finished', 'sync_subtitle_finished'],
mock: () => syncPhaseMock('sync_status_finished', 'sync_subtitle_finished', 100, true),
},
],
},
{
id: 'dialogs',
label: 'Dialoge',
hint: 'dialog_app_message.xml + notices · pick a dialog below',
keys: [
'logout', 'logout_confirm_title', 'logout_confirm_message', 'logout_confirm_action',
'logout_offline_title', 'logout_offline_message', 'logout_pending_title',
'logout_pending_message', 'logout_pending_detail_questionnaires',
'logout_pending_detail_scoring', 'logout_pending_detail_notes',
'logout_pending_detail_in_progress',
'upload_pending_title', 'upload_pending_review_message', 'upload', 'later',
'review_scores_supervisor_confirm_title', 'review_scores_supervisor_confirm_body',
'review_scores_supervisor_confirm_action', 'cancel', 'ok',
'upload_success_title', 'upload_success_message', 'upload_success_questionnaires',
'upload_success_scoring_reviews', 'upload_success_client_notes',
'upload_success_sync_next', 'upload_failed_title', 'upload_nothing_pending',
'upload_prepare_failed', 'error', 'error_invalid_data', 'error_not_found',
'score_calculated_eyebrow', 'score_calculated_title', 'score_calculated_body',
'score_calculated_result_label', 'score_calculated_next_label',
'score_calculated_next_title', 'score_calculated_band_hint', 'score_calculated_continue',
'category_yellow', 'questionnaire_exit_title', 'questionnaire_exit_message', 'leave',
],
variants: [
{
id: 'logout_confirm',
label: 'Abmelden bestätigen',
focusKeys: [
'logout_confirm_title', 'logout_confirm_message', 'logout_confirm_action',
'cancel', 'logout',
],
mock: () => dialogMock({
titleKey: 'logout_confirm_title',
messageKey: 'logout_confirm_message',
primaryKey: 'logout_confirm_action',
secondaryKey: 'cancel',
tone: 'logout',
}),
},
{
id: 'logout_offline',
label: 'Abmelden offline',
focusKeys: ['logout_offline_title', 'logout_offline_message', 'ok'],
mock: () => dialogMock({
titleKey: 'logout_offline_title',
messageKey: 'logout_offline_message',
primaryKey: 'ok',
tone: 'warning',
}),
},
{
id: 'logout_pending',
label: 'Abmelden blockiert',
focusKeys: [
'logout_pending_title', 'logout_pending_message',
'logout_pending_detail_questionnaires', 'logout_pending_detail_scoring',
'logout_pending_detail_notes', 'logout_pending_detail_in_progress', 'ok',
],
mock: () => dialogMock({
titleKey: 'logout_pending_title',
messageKey: 'logout_pending_message',
primaryKey: 'ok',
detailsKeys: [
'logout_pending_detail_questionnaires',
'logout_pending_detail_scoring',
'logout_pending_detail_notes',
'logout_pending_detail_in_progress',
],
tone: 'warning',
}),
},
{
id: 'upload_pending',
label: 'Upload bereit',
focusKeys: [
'upload_pending_title', 'upload_pending_review_message', 'upload', 'later',
],
mock: () => dialogMock({
titleKey: 'upload_pending_title',
messageKey: 'upload_pending_review_message',
primaryKey: 'upload',
secondaryKey: 'later',
tone: 'info',
}),
},
{
id: 'supervisor',
label: 'Supervision',
focusKeys: [
'review_scores_supervisor_confirm_title',
'review_scores_supervisor_confirm_body',
'review_scores_supervisor_confirm_action', 'cancel',
],
mock: () => dialogMock({
titleKey: 'review_scores_supervisor_confirm_title',
messageKey: 'review_scores_supervisor_confirm_body',
primaryKey: 'review_scores_supervisor_confirm_action',
secondaryKey: 'cancel',
tone: 'warning',
}),
},
{
id: 'upload_success',
label: 'Upload OK',
focusKeys: [
'upload_success_title', 'upload_success_message',
'upload_success_questionnaires', 'upload_success_scoring_reviews',
'upload_success_client_notes', 'upload_success_sync_next', 'ok',
],
mock: () => dialogMock({
titleKey: 'upload_success_title',
messageKey: 'upload_success_message',
primaryKey: 'ok',
detailsKeys: [
'upload_success_questionnaires',
'upload_success_scoring_reviews',
'upload_success_client_notes',
'upload_success_sync_next',
],
tone: 'info',
}),
},
{
id: 'upload_failed',
label: 'Upload Fehler',
focusKeys: [
'upload_failed_title', 'upload_prepare_failed', 'ok', 'cancel',
],
mock: () => dialogMock({
titleKey: 'upload_failed_title',
messageKey: 'upload_prepare_failed',
primaryKey: 'ok',
secondaryKey: 'cancel',
tone: 'danger',
}),
},
{
id: 'exit_questionnaire',
label: 'Fragebogen verlassen',
focusKeys: [
'questionnaire_exit_title', 'questionnaire_exit_message', 'leave', 'cancel',
],
mock: () => `
<div class="aq-screen aq-dialogs">
<div class="aq-dialog aq-dialog-warning">
<div class="aq-dialog-icon warning" aria-hidden="true"></div>
${f('questionnaire_exit_title', 'aq-dialog-title')}
${f('questionnaire_exit_message', 'aq-muted aq-sm aq-dialog-msg', 'textarea')}
<div class="aq-dialog-actions-col">
<div class="aq-btn-pill aq-btn-danger">${f('leave', 'aq-field-on-primary')}</div>
<div class="aq-btn-text">${f('cancel', 'aq-muted')}</div>
</div>
</div>
</div>`,
},
{
id: 'error_invalid',
label: 'Fehler (Daten)',
focusKeys: ['error', 'error_invalid_data', 'ok'],
mock: () => dialogMock({
titleKey: 'error',
messageKey: 'error_invalid_data',
primaryKey: 'ok',
tone: 'danger',
}),
},
{
id: 'error_not_found',
label: 'Fehler (Nicht gefunden)',
focusKeys: ['error', 'error_not_found', 'ok'],
mock: () => dialogMock({
titleKey: 'error',
messageKey: 'error_not_found',
primaryKey: 'ok',
tone: 'danger',
}),
},
{
id: 'score_notice',
label: 'Punktzahl berechnet',
focusKeys: [
'score_calculated_eyebrow', 'score_calculated_title', 'score_calculated_body',
'score_calculated_result_label', 'score_calculated_next_label',
'score_calculated_next_title', 'score_calculated_band_hint',
'score_calculated_continue', 'category_yellow',
],
mock: () => scoreCalculatedMock(),
},
],
},
];
export function screenById(id) {
return APP_UI_SCREENS.find(s => s.id === id) || APP_UI_SCREENS[0];
}
/** @returns {AppUiVariant[]} */
export function getVariants(screen) {
if (screen.variants?.length) return screen.variants;
return [{
id: 'default',
label: screen.label,
focusKeys: screen.keys,
mock: screen.mock || (() => ''),
}];
}
export function variantById(screen, variantId) {
const variants = getVariants(screen);
return variants.find(v => v.id === variantId) || variants[0];
}
export function allScreenKeys() {
const set = new Set();
APP_UI_SCREENS.forEach(s => s.keys.forEach(k => set.add(k)));
return set;
}