#!/usr/bin/env node import { promises as fs } from 'node:fs' import path from 'node:path' const ROOT = process.cwd() const FRONTEND_PAGES_DIR = path.join(ROOT, 'frontend', 'pages') const OUT_FILE = path.join(ROOT, 'docs', 'bedienung', 'frontend', 'alle-seiten-und-felder.md') function normalizePosix(p) { return p.split(path.sep).join('/') } async function walkFiles(dir, extension) { const result = [] async function walk(current) { const entries = await fs.readdir(current, { withFileTypes: true }) for (const entry of entries) { const full = path.join(current, entry.name) if (entry.isDirectory()) { await walk(full) } else if (entry.isFile() && full.endsWith(extension)) { result.push(full) } } } await walk(dir) return result.sort() } function filePathToNuxtRoute(filePath, baseDir) { const relative = normalizePosix(path.relative(baseDir, filePath)) let route = relative.replace(/\.vue$/, '') route = route .replace(/\.client$/, '') .replace(/\.server$/, '') .replace(/\[\[\.\.\.(.+?)\]\]/g, ':$1*?') .replace(/\[\.\.\.(.+?)\]/g, ':$1*') .replace(/\[\[(.+?)\]\]/g, ':$1?') .replace(/\[(.+?)\]/g, ':$1') route = route.replace(/\/index$/g, '') if (route === 'index') route = '' if (!route.startsWith('/')) route = `/${route}` return route || '/' } function pickAttr(tag, attr) { const doubleQuoted = new RegExp(`${attr}\\s*=\\s*"([^"]+)"`) const singleQuoted = new RegExp(`${attr}\\s*=\\s*'([^']+)'`) return tag.match(doubleQuoted)?.[1] || tag.match(singleQuoted)?.[1] || null } function detectFields(source) { const fields = [] const seen = new Set() const formFieldRegex = //g let match while ((match = formFieldRegex.exec(source)) !== null) { const attrs = match[1] || '' const label = pickAttr(attrs, 'label') const key = `form:${label || ''}` if (label && !seen.has(key)) { seen.add(key) fields.push({ name: label, component: 'UFormField', description: `Eingabebereich für „${label}“.` }) } } const inputRegex = /<(UInput|UTextarea|USelectMenu|USelect|USwitch|UCheckbox|URadioGroup|UInputNumber|UDatePicker)\b([\s\S]*?)>/g while ((match = inputRegex.exec(source)) !== null) { const component = match[1] const attrs = match[2] || '' const label = pickAttr(attrs, 'label') const placeholder = pickAttr(attrs, 'placeholder') const model = pickAttr(attrs, 'v-model') || pickAttr(attrs, 'v-model:model-value') const name = label || placeholder || model || '(ohne Bezeichnung)' const key = `${component}:${name}` if (seen.has(key)) continue seen.add(key) let description = 'Eingabefeld in dieser Seite.' if (label) description = `Eingabefeld für „${label}“.` else if (placeholder) description = `Eingabefeld mit Platzhalter „${placeholder}“.` else if (model) description = `Eingabefeld für den internen Wert „${model}“.` fields.push({ name, component, description }) } return fields } function getPagePurpose(route) { if (route.startsWith('/createDocument')) return 'Dokumentenerstellung und Verwaltung von Ausgangsbelegen.' if (route.startsWith('/support') || route.startsWith('/helpdesk')) return 'Support- und Kommunikationsprozesse.' if (route.startsWith('/staff')) return 'Mitarbeiterbezogene Verwaltung und Zeiterfassung.' if (route.startsWith('/settings')) return 'Konfiguration und Stammdatenpflege.' if (route.startsWith('/accounting') || route.startsWith('/banking')) return 'Finanz- und Buchhaltungsfunktionen.' return 'Funktionsseite im FEDEO-Frontend.' } async function main() { const files = await walkFiles(FRONTEND_PAGES_DIR, '.vue') let output = '# Frontend-Seiten und Eingabefelder\n\n' output += 'Diese Übersicht dient als Nutzer-Bedienung für alle Seiten des Web-Frontends.\n' output += 'Die Felder wurden technisch aus den Seiten erkannt und nutzerorientiert beschrieben.\n\n' for (const file of files) { const source = await fs.readFile(file, 'utf-8') const route = filePathToNuxtRoute(file, FRONTEND_PAGES_DIR) const fields = detectFields(source) const relativeFile = normalizePosix(path.relative(ROOT, file)) output += `## ${route}\n\n` output += `- Datei: \`${relativeFile}\`\n` output += `- Seitenfunktion: ${getPagePurpose(route)}\n\n` output += '### Felder\n\n' if (fields.length === 0) { output += 'Auf dieser Seite wurden keine direkten Eingabefelder erkannt.\n\n' continue } output += '| Feld | Komponente | Erklärung |\n' output += '|---|---|---|\n' for (const field of fields) { output += `| ${field.name.replace(/\|/g, '\\|')} | ${field.component} | ${field.description.replace(/\|/g, '\\|')} |\n` } output += '\n' } await fs.writeFile(OUT_FILE, output, 'utf-8') console.log(`Frontend-Bedienungsübersicht erzeugt: ${OUT_FILE}`) } main().catch((err) => { console.error('Fehler beim Erzeugen der Frontend-Bedienungsübersicht', err) process.exit(1) })