Kundenansicht neu P2: Hilfen, Testdaten und Quelltext-Regeln
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
385844192d
commit
91e5bca1c5
4 files changed
+537
No files matched your search
@@ -0,0 +1,165 @@
|
||||
/* Kundenansicht neu (P2): gemeinsame Hilfen der Ansichten. Formate, Bausteinklassen, Navigation, Ereignisse und kleine Merker.
|
||||
Kein DOM-Zugriff beim Import; document und window werden erst beim Aufruf gelesen. */
|
||||
|
||||
import { api } from './api.js'
|
||||
import { geheZu } from './router.js'
|
||||
import { hashFuerAnsicht } from './routen.js'
|
||||
import { safeUrl } from '../fn-util.js'
|
||||
|
||||
/** Klassen der Bausteine aus basis.css. */
|
||||
export const K = Object.freeze({
|
||||
knopf: 'kn-knopf',
|
||||
primaer: 'kn-knopf-primaer',
|
||||
zweit: 'kn-knopf-zweit',
|
||||
textknopf: 'kn-textknopf',
|
||||
feld: 'kn-feld',
|
||||
input: 'kn-input',
|
||||
select: 'kn-select',
|
||||
textarea: 'kn-textarea',
|
||||
check: 'kn-check',
|
||||
liste: 'kn-liste',
|
||||
zeile: 'kn-zeile',
|
||||
abschnitt: 'kn-abschnitt',
|
||||
meldung: 'kn-meldung',
|
||||
fehler: 'kn-fehler',
|
||||
leer: 'kn-leer',
|
||||
versteckt: 'hidden',
|
||||
pille: Object.freeze({
|
||||
grau: 'kn-pille kn-pille-grau',
|
||||
blau: 'kn-pille kn-pille-blau',
|
||||
orange: 'kn-pille kn-pille-orange',
|
||||
violett: 'kn-pille kn-pille-violett',
|
||||
gruen: 'kn-pille kn-pille-gruen',
|
||||
rot: 'kn-pille kn-pille-rot',
|
||||
}),
|
||||
})
|
||||
|
||||
export const ABO_NICHT_GELADEN = 'Deine Verträge konnten gerade nicht geladen werden. Bitte versuche es später noch einmal.'
|
||||
|
||||
/* ---------- Formate ---------- */
|
||||
|
||||
const ZAHL_2 = new Intl.NumberFormat('de-DE', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
||||
const ZAHL_1 = new Intl.NumberFormat('de-DE', { minimumFractionDigits: 1, maximumFractionDigits: 1 })
|
||||
const ZAHL_STD = new Intl.NumberFormat('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 2 })
|
||||
|
||||
/** Teile eines Zeitpunkts in Berliner Zeit; ungültige Werte ergeben null. */
|
||||
function berlinTeile(wert, extra = {}) {
|
||||
const d = wert instanceof Date ? wert : new Date(wert)
|
||||
if (!wert || Number.isNaN(d.getTime())) return null
|
||||
const teile = new Intl.DateTimeFormat('de-DE', { timeZone: 'Europe/Berlin', hourCycle: 'h23', ...extra }).formatToParts(d)
|
||||
return Object.fromEntries(teile.map((t) => [t.type, t.value]))
|
||||
}
|
||||
|
||||
/** 50 → '50,00 €' (geschütztes Leerzeichen vor dem Euro-Zeichen). */
|
||||
export const fmtEuro = (euro) => `${ZAHL_2.format(Number(euro) || 0)} €`
|
||||
|
||||
/** 'TT.MM.JJJJ' in Berliner Zeit; ein Wert, der schon so aussieht, bleibt; leer oder ungültig '–'. */
|
||||
export function fmtDatum(wert) {
|
||||
if (typeof wert === 'string' && /^\d{2}\.\d{2}\.\d{4}$/.test(wert)) return wert
|
||||
const t = berlinTeile(wert, { day: '2-digit', month: '2-digit', year: 'numeric' })
|
||||
return t ? `${t.day}.${t.month}.${t.year}` : '–'
|
||||
}
|
||||
|
||||
/** 'TT.MM.JJ, HH:MM' in Berliner Zeit. */
|
||||
export function fmtDatumZeit(wert) {
|
||||
const t = berlinTeile(wert, { day: '2-digit', month: '2-digit', year: '2-digit', hour: '2-digit', minute: '2-digit' })
|
||||
return t ? `${t.day}.${t.month}.${t.year}, ${t.hour}:${t.minute}` : '–'
|
||||
}
|
||||
|
||||
/** 'TT.MM.' in Berliner Zeit; ungültig ''. */
|
||||
export function fmtTagMonat(wert) {
|
||||
const t = berlinTeile(wert, { day: '2-digit', month: '2-digit' })
|
||||
return t ? `${t.day}.${t.month}.` : ''
|
||||
}
|
||||
|
||||
/** Minuten → '2,5 Std.' | '2 Std.' | '0,75 Std.' (Komma, höchstens zwei Stellen). */
|
||||
export const fmtStd = (minuten) => `${ZAHL_STD.format(Math.round(((Number(minuten) || 0) / 60) * 100) / 100)} Std.`
|
||||
|
||||
/** Minuten → '' | '45 min' | '2 h' | '2 h 30 min'. */
|
||||
export function fmtMinuten(minuten) {
|
||||
const m = Math.round(Number(minuten) || 0)
|
||||
if (m <= 0) return ''
|
||||
const h = Math.floor(m / 60)
|
||||
const rest = m % 60
|
||||
if (!h) return `${rest} min`
|
||||
return rest ? `${h} h ${rest} min` : `${h} h`
|
||||
}
|
||||
|
||||
/** Bytes → '12 B' | '1,5 KB' | '5,0 MB'. */
|
||||
export function fmtBytes(bytes) {
|
||||
const b = Number(bytes) || 0
|
||||
if (b < 1024) return `${Math.round(b)} B`
|
||||
if (b < 1024 * 1024) return `${ZAHL_1.format(b / 1024)} KB`
|
||||
return `${ZAHL_1.format(b / (1024 * 1024))} MB`
|
||||
}
|
||||
|
||||
/* ---------- Alt-Module, Navigation, Ereignisse ---------- */
|
||||
|
||||
/** Adapter für Alt-Module (auftrag-portal.js), die den Body als JSON-String schicken: das neue api() kodiert Objekte selbst. */
|
||||
export function legacyRequest(apiFn) {
|
||||
return (pfad, opts = {}) => {
|
||||
const { body } = opts
|
||||
if (typeof body !== 'string') return apiFn(pfad, { ...opts })
|
||||
let objekt = body
|
||||
try { objekt = JSON.parse(body) } catch { /* kein JSON: unverändert weitergeben */ }
|
||||
return apiFn(pfad, { ...opts, body: objekt })
|
||||
}
|
||||
}
|
||||
|
||||
/** Programmatisch navigieren (nur über den Router); wirft der Router, setzt der Hash die Adresse. */
|
||||
export function navigiere(hash, { ersetzen = false } = {}) {
|
||||
try {
|
||||
geheZu(hash, { ersetzen })
|
||||
} catch {
|
||||
window.location.hash = hash
|
||||
}
|
||||
}
|
||||
|
||||
/** Verknüpfung zu einer Ansicht für die Brücke der Hülle: href plus data-view (und data-project). */
|
||||
export function zuAnsicht(name, projektId = '') {
|
||||
const basis = hashFuerAnsicht(name) ?? '#'
|
||||
if (!projektId) return { href: basis, 'data-view': name }
|
||||
return { href: `${basis}${basis.includes('?') ? '&' : '?'}projekt=${encodeURIComponent(projektId)}`, 'data-view': name, 'data-project': projektId }
|
||||
}
|
||||
|
||||
/** Titel der Etage melden (die Hülle zeigt ihn im Kopf). */
|
||||
export function meldeTitel(el, text) {
|
||||
el.dispatchEvent(new CustomEvent('kn:titel', { bubbles: true, detail: { text } }))
|
||||
}
|
||||
|
||||
/** Zähler neu laden lassen: was = 'chat' | 'vertraege' | 'alle'. Ohne Dokument tut es nichts. */
|
||||
export function meldeZaehlerNeu(was) {
|
||||
if (typeof document === 'undefined' || typeof document.dispatchEvent !== 'function' || typeof CustomEvent === 'undefined') return
|
||||
document.dispatchEvent(new CustomEvent('kn:zaehler-neu', { bubbles: true, detail: { was } }))
|
||||
}
|
||||
|
||||
/* ---------- Merker zwischen Ansichten (einmalig) ---------- */
|
||||
|
||||
let hinweis = ''
|
||||
export function merkeHinweis(text) { hinweis = String(text ?? '') }
|
||||
export function nimmHinweis() { const t = hinweis; hinweis = ''; return t }
|
||||
|
||||
let chatVorgabe = ''
|
||||
export function setzeChatVorgabe(text) { chatVorgabe = String(text ?? '') }
|
||||
export function nimmChatVorgabe() { const t = chatVorgabe; chatVorgabe = ''; return t }
|
||||
|
||||
/* ---------- Abo ---------- */
|
||||
|
||||
/** GET /api/abo → Objekt oder null (jeder Fehler). */
|
||||
export async function ladeAbo() {
|
||||
try {
|
||||
const antwort = await api('/api/abo')
|
||||
return antwort && typeof antwort === 'object' ? antwort : null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/** Gemerkte Funktion eines Wechsels (V11): nur wenn key und projectId nichtleere Texte sind. */
|
||||
export function wechselFunktionAus(sub) {
|
||||
const f = sub?.wechselFunktion
|
||||
if (!f || typeof f.key !== 'string' || !f.key || typeof f.projectId !== 'string' || !f.projectId) return null
|
||||
return { key: f.key, projectId: f.projectId }
|
||||
}
|
||||
|
||||
export { safeUrl }
|
||||
Reference in new issue
Block a user