myCRM/docs/design/INVOICE_FORM_UX_DESIGN.md
olli 82b022ba3b Add comprehensive styles for invoice form components and utilities
- Introduced CSS custom properties for spacing, typography, colors, and shadows.
- Developed styles for form sections, grids, invoice items, and summary components.
- Implemented responsive design adjustments for various screen sizes.
- Added utility classes for text, spacing, and flex layouts.
- Included dark mode and high contrast mode support.
- Established loading states and validation/error styles.
- Enhanced accessibility features with focus styles and screen reader utilities.
2025-12-13 10:02:30 +01:00

50 KiB
Raw Blame History

Invoice Form UX/UI Design Konzept

myCRM Billing Module - Invoice Management

Erstellt: 2025-12-12 Projekt: myCRM - Symfony 7.1 LTS + Vue.js 3 + PrimeVue Fokus: Rechnungsformular (InvoiceForm.vue) und zugehörige Komponenten


Executive Summary

Dieses Dokument analysiert das aktuelle Invoice-Formular und präsentiert ein umfassendes UX/UI-Design-Konzept für eine optimierte, professionelle Rechnungsverwaltung. Der Fokus liegt auf verbesserter visueller Hierarchie, optimierter Formularstruktur und konsistenter Nutzererfahrung.


1. Analyse des Ist-Zustands

1.1 Bestehende Komponenten

Hauptkomponenten:

  • InvoiceManagement.vue - Rechnungsübersicht mit DataTable
  • InvoiceForm.vue - Rechnungserstellung/-bearbeitung (Hauptformular)
  • PaymentForm.vue - Zahlungserfassung
  • PDFUploadForm.vue - PDF-Upload (MVP-Platzhalter)

1.2 Identifizierte UX-Probleme

Schwerwiegende Probleme (P0)

  1. Fehlende visuelle Hierarchie im Hauptformular

    • Alle Felder auf gleicher Ebene ohne logische Gruppierung
    • Rechnungspositionen-Tabelle verliert sich im Formular
    • Keine klare Trennung zwischen Stammdaten und Positionen
  2. Unklare Validierungs-Feedback

    • Validierung nur beim Submit sichtbar
    • Keine Echtzeit-Validierung während Eingabe
    • Fehler-Messages zu klein und leicht zu übersehen
  3. Schlechte Rechnungspositions-Verwaltung

    • DataTable in Formular wirkt überladen
    • Inline-Editing schwer zu bedienen
    • Fehlende Summenberechnung in Echtzeit
    • Keine visuelle Bestätigung beim Hinzufügen/Entfernen
  4. Inkonsistente Spacing und Layouts

    • Inkonsistente Abstände zwischen Formulargruppen
    • Dialog-Größe (70vw) nicht optimal für verschiedene Bildschirme
    • Fehlende responsive Breakpoints für Tablet-Ansicht

Mittelschwere Probleme (P1)

  1. Fehlende Benutzerführung

    • Keine Hilfe-Tooltips für komplexe Felder
    • Fehlende Feldkontexte (z.B. Steuerberechnung)
    • Keine Keyboard-Shortcuts
  2. Unvollständige Status-Kommunikation

    • Loading-States nur beim Speichern
    • Keine Progress-Indication beim Laden von Kontakten
    • Fehlende Empty-States
  3. Schwache Datenvisualisierung

    • Keine Gesamtsummen-Anzeige
    • Fehlende Berechnungsübersicht (Netto, Steuer, Brutto)
    • Kein Currency-Formatting in Tabelle

Niedrige Priorität (P2)

  1. Fehlende Micro-Interactions

    • Keine Animationen beim Hinzufügen/Entfernen von Positionen
    • Fehlende Hover-States auf interaktiven Elementen
    • Keine visuellen Bestätigungen
  2. Accessibility-Mängel

    • Fehlende ARIA-Labels für Screen Reader
    • Keine Keyboard-Navigation in Positionen-Tabelle
    • Kontrast könnte besser sein

2. Design-Prinzipien

2.1 Core Principles

  1. Progressive Disclosure - Zeige nur relevante Informationen zum richtigen Zeitpunkt
  2. Visual Hierarchy - Klare Strukturierung durch Größe, Farbe, Spacing
  3. Immediate Feedback - Sofortige Rückmeldung auf Benutzeraktionen
  4. Error Prevention - Validierung bevor Fehler entstehen
  5. Consistency - Einheitliche PrimeVue-Komponenten und Patterns

2.2 Design System Integration

Verwendung bestehender Assets:

  • PrimeVue Aura Theme (Dark Mode Support)
  • Tailwind CSS v4 mit PrimeUI Plugin
  • Sakai Layout System
  • Bestehende Farbpalette und Spacing-System

3. Design-Konzept: Invoice Form

3.1 Layout-Struktur (Desktop)

┌─────────────────────────────────────────────────────────────────┐
│ HEADER: Neue Rechnung / Rechnung bearbeiten          [×] Schließen │
├─────────────────────────────────────────────────────────────────┤
│                                                                   │
│ [INFO-BANNER] Phase 1 MVP - wird erweitert (nur bei Neuerstellung) │
│                                                                   │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ SEKTION 1: STAMMDATEN                                      │ │
│ │ ┌──────────────────┐  ┌──────────────────┐               │ │
│ │ │ Rechnungsnr. *   │  │ Status           │               │ │
│ │ └──────────────────┘  └──────────────────┘               │ │
│ │ ┌────────────────────────────────────────────────────┐   │ │
│ │ │ Kunde (Dropdown mit Search) *                      │   │ │
│ │ └────────────────────────────────────────────────────┘   │ │
│ │ ┌──────────────────┐  ┌──────────────────┐               │ │
│ │ │ Rechnungsdatum * │  │ Fälligkeitsdatum*│               │ │
│ │ └──────────────────┘  └──────────────────┘               │ │
│ └───────────────────────────────────────────────────────────┘ │
│                                                                   │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ SEKTION 2: RECHNUNGSPOSITIONEN          [+ Position]      │ │
│ │                                                             │ │
│ │ ┌─────────────────────────────────────────────────────┐ │ │
│ │ │ Position 1                                [Löschen] │ │ │
│ │ │ ┌──────────────────────────────────────────────┐    │ │ │
│ │ │ │ Beschreibung                                  │    │ │ │
│ │ │ └──────────────────────────────────────────────┘    │ │ │
│ │ │ [Menge: 1.00] [Einzelpreis: 0,00 €] [MwSt: 19%] │ │ │
│ │ │ → Zwischensumme: 0,00 €                           │ │ │
│ │ └─────────────────────────────────────────────────────┘ │ │
│ │                                                             │ │
│ │ [+ Position hinzufügen]                                   │ │
│ │                                                             │ │
│ │ ┌───────────────────────────────────────────────────┐   │ │
│ │ │ ZUSAMMENFASSUNG                                     │   │ │
│ │ │ Nettobetrag:                         1.000,00 €    │   │ │
│ │ │ MwSt (19%):                            190,00 €    │   │ │
│ │ │ MwSt (7%):                              14,00 €    │   │ │
│ │ │ ─────────────────────────────────────────────────  │   │ │
│ │ │ GESAMTBETRAG:                        1.204,00 €    │   │ │
│ │ └───────────────────────────────────────────────────┘   │ │
│ └───────────────────────────────────────────────────────────┘ │
│                                                                   │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ SEKTION 3: ZUSÄTZLICHE INFORMATIONEN (Optional)            │ │
│ │ ┌────────────────────────────────────────────────────┐   │ │
│ │ │ Notizen (z.B. Zahlungsbedingungen)                  │   │ │
│ │ │                                                      │   │ │
│ │ └────────────────────────────────────────────────────┘   │ │
│ └───────────────────────────────────────────────────────────┘ │
│                                                                   │
├─────────────────────────────────────────────────────────────────┤
│ FOOTER: [Abbrechen]          [Entwurf speichern] [Speichern]   │
└─────────────────────────────────────────────────────────────────┘

3.2 Visual Design Specifications

Spacing System

/* Card/Section Spacing */
--section-gap: 2rem;          /* 32px - Abstand zwischen Sektionen */
--section-padding: 1.5rem;    /* 24px - Innen-Padding der Sektionen */
--field-gap: 1rem;            /* 16px - Abstand zwischen Feldern */
--inline-field-gap: 0.75rem;  /* 12px - Abstand zwischen Inline-Feldern */

/* Component Spacing */
--form-element-height: 2.75rem; /* 44px - Touch-friendly */
--label-margin: 0.5rem;         /* 8px - Label zu Input */

Typography Hierarchy

/* Heading Levels */
--h2-dialog-title: 1.5rem;    /* 24px - Dialog Titel */
--h3-section-title: 1.25rem;  /* 20px - Sektion Überschrift */
--h4-subsection: 1rem;        /* 16px - Subsection */

/* Body Text */
--body-normal: 0.875rem;      /* 14px - Standard Text */
--body-small: 0.75rem;        /* 12px - Helper Text, Labels */
--body-large: 1rem;           /* 16px - Wichtige Werte */

/* Font Weights */
--weight-normal: 400;
--weight-medium: 500;
--weight-semibold: 600;
--weight-bold: 700;

Color Palette (Semantic Colors)

/* Status Colors (PrimeVue Aura Theme) */
--color-primary: #3B82F6;      /* Blue - Primary Actions */
--color-success: #22C55E;      /* Green - Success, Paid */
--color-warning: #F59E0B;      /* Orange - Partial, Warning */
--color-danger: #EF4444;       /* Red - Overdue, Delete */
--color-info: #06B6D4;         /* Cyan - Info Messages */
--color-secondary: #6B7280;    /* Gray - Secondary Actions */

/* Surface Colors */
--surface-0: #FFFFFF;          /* Base Background */
--surface-50: #F9FAFB;         /* Light Background */
--surface-100: #F3F4F6;        /* Section Background */
--surface-200: #E5E7EB;        /* Border Color */

/* Text Colors */
--text-primary: #111827;       /* Primary Text */
--text-secondary: #6B7280;     /* Secondary Text */
--text-muted: #9CA3AF;         /* Muted Text */

Component Styling

Card/Section Style:

.form-section {
  background: var(--surface-0);
  border: 1px solid var(--surface-200);
  border-radius: 0.75rem; /* 12px */
  padding: var(--section-padding);
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}

.form-section-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 1.5rem;
  padding-bottom: 0.75rem;
  border-bottom: 1px solid var(--surface-200);
}

.form-section-title {
  font-size: var(--h3-section-title);
  font-weight: var(--weight-semibold);
  color: var(--text-primary);
}

Invoice Item Card:

.invoice-item-card {
  background: var(--surface-50);
  border: 1px solid var(--surface-200);
  border-radius: 0.5rem; /* 8px */
  padding: 1rem;
  margin-bottom: 0.75rem;
  transition: all 0.2s ease;
}

.invoice-item-card:hover {
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
  border-color: var(--color-primary);
}

.invoice-item-card--new {
  animation: slideIn 0.3s ease-out;
}

@keyframes slideIn {
  from {
    opacity: 0;
    transform: translateY(-10px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

Summary Box:

.invoice-summary {
  background: var(--surface-100);
  border: 2px solid var(--color-primary);
  border-radius: 0.75rem;
  padding: 1.5rem;
  margin-top: 1.5rem;
}

.invoice-summary-row {
  display: flex;
  justify-content: space-between;
  padding: 0.5rem 0;
  font-size: 0.875rem;
  color: var(--text-secondary);
}

.invoice-summary-total {
  display: flex;
  justify-content: space-between;
  padding: 1rem 0 0;
  margin-top: 0.75rem;
  border-top: 2px solid var(--surface-200);
  font-size: 1.25rem;
  font-weight: var(--weight-bold);
  color: var(--text-primary);
}

3.3 Responsive Breakpoints

/* Desktop First Approach */

/* Large Desktop (1920px+) */
.invoice-dialog--large {
  width: 1200px;
  max-width: 90vw;
}

/* Desktop (1200px - 1920px) */
@media (max-width: 1920px) {
  .invoice-dialog {
    width: 1000px;
  }
}

/* Tablet Landscape (992px - 1200px) */
@media (max-width: 1200px) {
  .invoice-dialog {
    width: 90vw;
  }

  .form-grid {
    grid-template-columns: 1fr 1fr; /* 2 Spalten */
  }
}

/* Tablet Portrait (768px - 992px) */
@media (max-width: 992px) {
  .invoice-dialog {
    width: 95vw;
    max-height: 90vh;
  }

  .form-grid {
    grid-template-columns: 1fr; /* 1 Spalte */
  }

  .invoice-item-fields {
    flex-direction: column;
  }
}

/* Mobile (576px - 768px) */
@media (max-width: 768px) {
  .invoice-dialog {
    width: 100vw;
    height: 100vh;
    border-radius: 0;
    margin: 0;
  }

  .form-section {
    padding: 1rem;
  }

  .invoice-summary {
    position: sticky;
    bottom: 0;
    z-index: 100;
  }
}

/* Small Mobile (< 576px) */
@media (max-width: 576px) {
  .form-section-title {
    font-size: 1rem;
  }

  .invoice-item-card {
    padding: 0.75rem;
  }
}

4. Komponentenweise Design-Spezifikationen

4.1 InvoiceForm - Hauptformular

Sektion 1: Stammdaten

Layout:

  • 2-spaltig auf Desktop (Rechnungsnr + Status)
  • 1-spaltig auf Mobile
  • Kunde als volle Breite für bessere Lesbarkeit
  • Datum-Felder wieder 2-spaltig

Verbesserungen:

<div class="form-section">
  <div class="form-section-header">
    <h3 class="form-section-title">Rechnungsinformationen</h3>
    <Tag :value="getStatusLabel(form.status)" :severity="getStatusSeverity(form.status)" />
  </div>

  <div class="form-grid">
    <FloatLabel>
      <InputText id="invoiceNumber" v-model="form.invoiceNumber"
                 :invalid="submitted && !form.invoiceNumber" />
      <label for="invoiceNumber">Rechnungsnummer *</label>
    </FloatLabel>

    <FloatLabel>
      <Dropdown id="status" v-model="form.status" :options="statusOptions"
                option-label="label" option-value="value" />
      <label for="status">Status</label>
    </FloatLabel>
  </div>

  <div class="form-field-full">
    <FloatLabel>
      <Select id="contact" v-model="form.contactId"
              :options="contacts"
              option-label="companyName"
              option-value="id"
              filter
              :filter-fields="['companyName', 'email']"
              placeholder="Kunde suchen..."
              :invalid="submitted && !form.contactId"
              :loading="loadingContacts">
        <template #value="slotProps">
          <div v-if="slotProps.value" class="flex align-items-center gap-2">
            <i class="pi pi-building"></i>
            <span>{{ getContactName(slotProps.value) }}</span>
          </div>
        </template>
        <template #option="slotProps">
          <div class="flex flex-column">
            <span class="font-semibold">{{ slotProps.option.companyName }}</span>
            <span class="text-sm text-secondary">{{ slotProps.option.email }}</span>
          </div>
        </template>
      </Select>
      <label for="contact">Kunde *</label>
    </FloatLabel>
  </div>

  <div class="form-grid">
    <FloatLabel>
      <DatePicker id="invoiceDate" v-model="form.invoiceDate"
                  date-format="dd.mm.yy"
                  :show-icon="true"
                  icon="pi pi-calendar" />
      <label for="invoiceDate">Rechnungsdatum *</label>
    </FloatLabel>

    <FloatLabel>
      <DatePicker id="dueDate" v-model="form.dueDate"
                  date-format="dd.mm.yy"
                  :show-icon="true"
                  icon="pi pi-calendar"
                  :min-date="form.invoiceDate" />
      <label for="dueDate">Fälligkeitsdatum *</label>
    </FloatLabel>
  </div>
</div>

Design-Rationale:

  • FloatLabel für moderneres Erscheinungsbild
  • Status-Tag im Header für sofortige Sichtbarkeit
  • Verbesserter Kunden-Dropdown mit Icons und Mehrfachanzeige
  • Min-Date beim Fälligkeitsdatum verhindert ungültige Daten

Sektion 2: Rechnungspositionen

Problem: Aktuelle DataTable-Lösung ist zu komplex für Inline-Editing

Lösung: Card-basierte Positionen mit optimierter UX

<div class="form-section">
  <div class="form-section-header">
    <h3 class="form-section-title">
      Rechnungspositionen
      <span class="text-secondary text-sm">({{ form.items.length }})</span>
    </h3>
    <Button label="Position hinzufügen"
            icon="pi pi-plus"
            size="small"
            @click="addItem" />
  </div>

  <!-- Empty State -->
  <div v-if="form.items.length === 0" class="empty-state">
    <i class="pi pi-inbox empty-state-icon"></i>
    <p class="empty-state-text">Noch keine Positionen hinzugefügt</p>
    <Button label="Erste Position hinzufügen"
            icon="pi pi-plus"
            outlined
            @click="addItem" />
  </div>

  <!-- Invoice Items -->
  <TransitionGroup name="list" tag="div" class="invoice-items-list">
    <div v-for="(item, index) in form.items"
         :key="item.id || `new-${index}`"
         class="invoice-item-card">

      <!-- Item Header -->
      <div class="invoice-item-header">
        <span class="invoice-item-number">Position {{ index + 1 }}</span>
        <div class="invoice-item-actions">
          <Button icon="pi pi-trash"
                  text
                  rounded
                  severity="danger"
                  size="small"
                  @click="removeItem(index)"
                  v-tooltip.top="'Position entfernen'" />
        </div>
      </div>

      <!-- Description -->
      <div class="form-field-full mb-3">
        <FloatLabel>
          <Textarea id="`description-${index}`"
                    v-model="item.description"
                    rows="2"
                    :invalid="submitted && !item.description"
                    @input="calculateItemTotal(index)" />
          <label :for="`description-${index}`">Beschreibung *</label>
        </FloatLabel>
      </div>

      <!-- Quantity, Price, Tax in responsive grid -->
      <div class="invoice-item-fields">
        <div class="invoice-item-field">
          <FloatLabel>
            <InputNumber :id="`quantity-${index}`"
                         v-model="item.quantity"
                         :min-fraction-digits="2"
                         :max-fraction-digits="2"
                         :min="0.01"
                         @input="calculateItemTotal(index)" />
            <label :for="`quantity-${index}`">Menge</label>
          </FloatLabel>
        </div>

        <div class="invoice-item-field">
          <FloatLabel>
            <InputNumber :id="`unitPrice-${index}`"
                         v-model="item.unitPrice"
                         mode="currency"
                         currency="EUR"
                         locale="de-DE"
                         @input="calculateItemTotal(index)" />
            <label :for="`unitPrice-${index}`">Einzelpreis</label>
          </FloatLabel>
        </div>

        <div class="invoice-item-field invoice-item-field--small">
          <FloatLabel>
            <Select :id="`taxRate-${index}`"
                    v-model="item.taxRate"
                    :options="taxRates"
                    @change="calculateItemTotal(index)">
              <template #value="slotProps">
                {{ slotProps.value }}%
              </template>
              <template #option="slotProps">
                {{ slotProps.option }}% MwSt.
              </template>
            </Select>
            <label :for="`taxRate-${index}`">MwSt.</label>
          </FloatLabel>
        </div>
      </div>

      <!-- Item Subtotal -->
      <div class="invoice-item-subtotal">
        <span class="text-secondary">Zwischensumme:</span>
        <span class="font-semibold">{{ formatCurrency(item.subtotal) }}</span>
      </div>
    </div>
  </TransitionGroup>

  <!-- Add Button (bottom) -->
  <Button label="Weitere Position hinzufügen"
          icon="pi pi-plus"
          outlined
          class="w-full mt-3"
          @click="addItem" />

  <!-- Summary Box -->
  <div class="invoice-summary">
    <div class="invoice-summary-header">
      <h4>Zusammenfassung</h4>
    </div>

    <div class="invoice-summary-body">
      <div class="invoice-summary-row">
        <span>Nettobetrag:</span>
        <span>{{ formatCurrency(totals.net) }}</span>
      </div>

      <div v-for="(amount, rate) in totals.taxBreakdown"
           :key="rate"
           class="invoice-summary-row">
        <span>MwSt. ({{ rate }}%):</span>
        <span>{{ formatCurrency(amount) }}</span>
      </div>

      <Divider />

      <div class="invoice-summary-total">
        <span>Gesamtbetrag:</span>
        <span>{{ formatCurrency(totals.gross) }}</span>
      </div>
    </div>
  </div>
</div>

CSS für Transitions:

/* List transitions */
.list-enter-active,
.list-leave-active {
  transition: all 0.3s ease;
}

.list-enter-from {
  opacity: 0;
  transform: translateY(-20px);
}

.list-leave-to {
  opacity: 0;
  transform: translateX(20px);
}

.list-move {
  transition: transform 0.3s ease;
}

/* Empty State */
.empty-state {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 3rem 1rem;
  text-align: center;
}

.empty-state-icon {
  font-size: 3rem;
  color: var(--text-muted);
  margin-bottom: 1rem;
}

.empty-state-text {
  color: var(--text-secondary);
  margin-bottom: 1.5rem;
}

/* Invoice Item Fields */
.invoice-item-fields {
  display: grid;
  grid-template-columns: 1fr 1fr auto;
  gap: 1rem;
  align-items: start;
}

.invoice-item-field--small {
  max-width: 120px;
}

@media (max-width: 768px) {
  .invoice-item-fields {
    grid-template-columns: 1fr;
  }

  .invoice-item-field--small {
    max-width: 100%;
  }
}

Sektion 3: Notizen

Kollapsierbar für bessere Übersicht:

<div class="form-section">
  <Panel header="Zusätzliche Informationen"
         toggleable
         :collapsed="!form.notes">
    <FloatLabel>
      <Textarea id="notes"
                v-model="form.notes"
                rows="4"
                placeholder="z.B. Zahlungsbedingungen, Lieferbedingungen..." />
      <label for="notes">Notizen</label>
    </FloatLabel>
  </Panel>
</div>

4.2 PaymentForm - Zahlungserfassung

Aktuelles Problem: Zu minimalistisch, fehlt Kontext

Verbesserungen:

<div class="payment-form">
  <!-- Context Card -->
  <div class="context-card">
    <div class="context-card-header">
      <h4>Rechnungsdetails</h4>
    </div>
    <div class="context-card-body">
      <div class="context-row">
        <span class="context-label">Rechnung:</span>
        <span class="context-value font-semibold">{{ invoice?.invoiceNumber }}</span>
      </div>
      <div class="context-row">
        <span class="context-label">Kunde:</span>
        <span class="context-value">{{ invoice?.contact?.companyName }}</span>
      </div>
      <div class="context-row">
        <span class="context-label">Rechnungsbetrag:</span>
        <span class="context-value">{{ formatCurrency(invoice?.total) }}</span>
      </div>
      <div class="context-row">
        <span class="context-label">Bereits gezahlt:</span>
        <span class="context-value text-success">
          {{ formatCurrency((invoice?.total || 0) - (invoice?.openAmount || 0)) }}
        </span>
      </div>
      <Divider />
      <div class="context-row context-row--highlight">
        <span class="context-label font-semibold">Offener Betrag:</span>
        <span class="context-value font-bold text-xl text-primary">
          {{ formatCurrency(invoice?.openAmount) }}
        </span>
      </div>
    </div>
  </div>

  <!-- Payment Form -->
  <div class="form-section mt-4">
    <div class="form-grid">
      <FloatLabel>
        <DatePicker id="paymentDate"
                    v-model="form.paymentDate"
                    date-format="dd.mm.yy"
                    :show-icon="true"
                    icon="pi pi-calendar"
                    :max-date="new Date()" />
        <label for="paymentDate">Zahlungsdatum *</label>
      </FloatLabel>

      <FloatLabel>
        <InputNumber id="amount"
                     v-model="form.amount"
                     mode="currency"
                     currency="EUR"
                     locale="de-DE"
                     :max="parseFloat(invoice?.openAmount || 0)"
                     :invalid="form.amount > parseFloat(invoice?.openAmount || 0)" />
        <label for="amount">Betrag *</label>
      </FloatLabel>
    </div>

    <div class="form-field-full mt-3">
      <FloatLabel>
        <Select id="paymentMethod"
                v-model="form.paymentMethod"
                :options="paymentMethods"
                option-label="label"
                option-value="value">
          <template #value="slotProps">
            <div class="flex align-items-center gap-2">
              <i :class="getPaymentMethodIcon(slotProps.value)"></i>
              <span>{{ getPaymentMethodLabel(slotProps.value) }}</span>
            </div>
          </template>
          <template #option="slotProps">
            <div class="flex align-items-center gap-2">
              <i :class="getPaymentMethodIcon(slotProps.option.value)"></i>
              <span>{{ slotProps.option.label }}</span>
            </div>
          </template>
        </Select>
        <label for="paymentMethod">Zahlungsart</label>
      </FloatLabel>
    </div>

    <div class="form-field-full mt-3">
      <FloatLabel>
        <Textarea id="notes"
                  v-model="form.notes"
                  rows="3"
                  placeholder="Optionale Notizen zur Zahlung..." />
        <label for="notes">Notizen</label>
      </FloatLabel>
    </div>
  </div>

  <!-- Quick Amount Buttons -->
  <div class="quick-amounts mt-3">
    <span class="text-sm text-secondary mr-2">Schnellauswahl:</span>
    <Button :label="formatCurrency(invoice?.openAmount)"
            size="small"
            outlined
            @click="form.amount = parseFloat(invoice?.openAmount || 0)" />
    <Button :label="formatCurrency(invoice?.openAmount / 2)"
            size="small"
            outlined
            @click="form.amount = parseFloat(invoice?.openAmount || 0) / 2" />
  </div>

  <!-- Action Buttons -->
  <div class="flex gap-2 mt-4 justify-content-end">
    <Button label="Abbrechen"
            icon="pi pi-times"
            severity="secondary"
            text
            @click="cancel" />
    <Button label="Zahlung erfassen"
            icon="pi pi-check"
            :loading="saving"
            :disabled="!isValid"
            @click="save" />
  </div>
</div>

Styling:

.context-card {
  background: linear-gradient(135deg, var(--primary-50) 0%, var(--primary-100) 100%);
  border: 1px solid var(--primary-200);
  border-radius: 0.75rem;
  padding: 1.5rem;
}

.context-row {
  display: flex;
  justify-content: space-between;
  padding: 0.5rem 0;
}

.context-row--highlight {
  padding: 1rem 0 0;
  margin-top: 0.5rem;
}

.context-label {
  color: var(--text-secondary);
}

.context-value {
  text-align: right;
}

.quick-amounts {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
}

4.3 PDFUploadForm - Verbesserungen

Problem: Zu primitiv, fehlt Drag & Drop

Lösung:

<div class="pdf-upload-form">
  <Message v-if="!invoice?.pdfPath" severity="info">
    Laden Sie die PDF-Rechnung hoch für eine vollständige Dokumentation
  </Message>

  <!-- Upload Area -->
  <div class="upload-area"
       :class="{ 'upload-area--dragover': isDragging }"
       @drop.prevent="handleDrop"
       @dragover.prevent="isDragging = true"
       @dragleave.prevent="isDragging = false">

    <div v-if="!selectedFile" class="upload-placeholder">
      <i class="pi pi-cloud-upload upload-icon"></i>
      <h4>PDF-Datei hochladen</h4>
      <p class="text-secondary">
        Datei hierher ziehen oder klicken zum Auswählen
      </p>
      <Button label="Datei auswählen"
              icon="pi pi-folder-open"
              outlined
              @click="$refs.fileInput.click()" />
      <p class="upload-hint">Nur PDF-Dateien, max. 10 MB</p>
    </div>

    <div v-else class="upload-preview">
      <div class="file-info">
        <i class="pi pi-file-pdf file-icon"></i>
        <div class="file-details">
          <p class="file-name">{{ selectedFile.name }}</p>
          <p class="file-size">{{ formatFileSize(selectedFile.size) }}</p>
        </div>
        <Button icon="pi pi-times"
                rounded
                text
                severity="danger"
                @click="clearFile" />
      </div>

      <!-- Progress Bar during upload -->
      <ProgressBar v-if="uploading"
                   :value="uploadProgress"
                   :show-value="true" />
    </div>

    <input ref="fileInput"
           type="file"
           accept="application/pdf"
           style="display: none"
           @change="handleFileChange" />
  </div>

  <!-- Existing PDF (if edit mode) -->
  <div v-if="invoice?.pdfPath" class="existing-pdf mt-4">
    <div class="existing-pdf-card">
      <i class="pi pi-file-pdf text-danger text-4xl"></i>
      <div class="flex-1">
        <p class="font-semibold">Aktuelle PDF-Datei</p>
        <p class="text-sm text-secondary">{{ invoice.pdfPath }}</p>
      </div>
      <Button label="Anzeigen"
              icon="pi pi-external-link"
              outlined
              @click="viewExistingPDF" />
    </div>
  </div>

  <!-- Actions -->
  <div class="flex gap-2 mt-4 justify-content-end">
    <Button label="Abbrechen"
            icon="pi pi-times"
            severity="secondary"
            text
            @click="cancel" />
    <Button label="Hochladen"
            icon="pi pi-upload"
            :loading="uploading"
            :disabled="!selectedFile"
            @click="upload" />
  </div>
</div>

Styling:

.upload-area {
  border: 2px dashed var(--surface-200);
  border-radius: 0.75rem;
  padding: 3rem 2rem;
  text-align: center;
  transition: all 0.3s ease;
  cursor: pointer;
}

.upload-area:hover {
  border-color: var(--primary-color);
  background: var(--primary-50);
}

.upload-area--dragover {
  border-color: var(--primary-color);
  background: var(--primary-100);
  border-width: 3px;
}

.upload-icon {
  font-size: 4rem;
  color: var(--primary-color);
  margin-bottom: 1rem;
}

.upload-hint {
  font-size: 0.75rem;
  color: var(--text-muted);
  margin-top: 1rem;
}

.file-info {
  display: flex;
  align-items: center;
  gap: 1rem;
  padding: 1rem;
  background: var(--surface-50);
  border-radius: 0.5rem;
}

.file-icon {
  font-size: 2.5rem;
  color: var(--danger-color);
}

.file-details {
  flex: 1;
  text-align: left;
}

.file-name {
  font-weight: 600;
  margin-bottom: 0.25rem;
}

.file-size {
  font-size: 0.875rem;
  color: var(--text-secondary);
}

.existing-pdf-card {
  display: flex;
  align-items: center;
  gap: 1rem;
  padding: 1.5rem;
  background: var(--surface-50);
  border: 1px solid var(--surface-200);
  border-radius: 0.75rem;
}

5. Interaction Design

5.1 Validierung & Fehlerbehandlung

Echtzeit-Validierung

// Composable: useFormValidation.js
import { ref, computed } from 'vue'

export function useFormValidation() {
  const errors = ref({})
  const touched = ref({})

  const validateField = (fieldName, value, rules) => {
    const fieldErrors = []

    if (rules.required && !value) {
      fieldErrors.push('Dieses Feld ist erforderlich')
    }

    if (rules.minLength && value?.length < rules.minLength) {
      fieldErrors.push(`Mindestens ${rules.minLength} Zeichen erforderlich`)
    }

    if (rules.pattern && !rules.pattern.test(value)) {
      fieldErrors.push('Ungültiges Format')
    }

    if (rules.custom) {
      const customError = rules.custom(value)
      if (customError) fieldErrors.push(customError)
    }

    errors.value[fieldName] = fieldErrors
    return fieldErrors.length === 0
  }

  const touchField = (fieldName) => {
    touched.value[fieldName] = true
  }

  const hasError = (fieldName) => {
    return touched.value[fieldName] && errors.value[fieldName]?.length > 0
  }

  const getError = (fieldName) => {
    return hasError(fieldName) ? errors.value[fieldName][0] : null
  }

  return {
    errors,
    touched,
    validateField,
    touchField,
    hasError,
    getError
  }
}

Verwendung im Template:

<FloatLabel>
  <InputText id="invoiceNumber"
             v-model="form.invoiceNumber"
             :invalid="hasError('invoiceNumber')"
             @blur="touchField('invoiceNumber')"
             @input="validateField('invoiceNumber', form.invoiceNumber, {
               required: true,
               minLength: 3,
               pattern: /^[A-Z0-9-]+$/
             })" />
  <label for="invoiceNumber">Rechnungsnummer *</label>
</FloatLabel>
<small v-if="hasError('invoiceNumber')" class="p-error">
  {{ getError('invoiceNumber') }}
</small>

Toast-Benachrichtigungen Strategie

// Toast Guidelines
const toastConfig = {
  success: {
    severity: 'success',
    life: 3000,
    closable: true
  },
  error: {
    severity: 'error',
    life: 5000,
    closable: true
  },
  warning: {
    severity: 'warn',
    life: 4000,
    closable: true
  },
  info: {
    severity: 'info',
    life: 4000,
    closable: true
  }
}

// Verwendung
toast.add({
  ...toastConfig.success,
  summary: 'Rechnung erstellt',
  detail: `Rechnung ${invoice.invoiceNumber} wurde erfolgreich erstellt`
})

5.2 Loading States

Skeleton Screens statt Spinner:

<template>
  <!-- Während Kontakte laden -->
  <div v-if="loadingContacts" class="contact-skeleton">
    <Skeleton height="3rem" class="mb-2"></Skeleton>
  </div>
  <Select v-else ...>

  <!-- Während Form lädt -->
  <div v-if="loadingInvoice" class="form-skeleton">
    <Skeleton height="2.5rem" class="mb-3"></Skeleton>
    <Skeleton height="2.5rem" class="mb-3"></Skeleton>
    <Skeleton height="10rem"></Skeleton>
  </div>
</template>

Button Loading States:

<Button label="Speichern"
        :loading="saving"
        :disabled="!isFormValid || saving">
  <template #loadingicon>
    <i class="pi pi-spin pi-spinner"></i>
  </template>
</Button>

5.3 Keyboard Shortcuts

// useKeyboardShortcuts.js composable
import { onMounted, onUnmounted } from 'vue'

export function useKeyboardShortcuts(shortcuts) {
  const handleKeydown = (event) => {
    const key = event.key.toLowerCase()
    const ctrl = event.ctrlKey || event.metaKey
    const shift = event.shiftKey

    const shortcutKey = [
      ctrl && 'ctrl',
      shift && 'shift',
      key
    ].filter(Boolean).join('+')

    const action = shortcuts[shortcutKey]
    if (action) {
      event.preventDefault()
      action()
    }
  }

  onMounted(() => {
    window.addEventListener('keydown', handleKeydown)
  })

  onUnmounted(() => {
    window.removeEventListener('keydown', handleKeydown)
  })
}

// Verwendung im InvoiceForm
useKeyboardShortcuts({
  'ctrl+s': save,
  'ctrl+shift+p': addItem,
  'escape': cancel
})

Keyboard Hints in UI:

<Button label="Speichern" icon="pi pi-check">
  <template #default>
    Speichern <kbd class="ml-2">Ctrl+S</kbd>
  </template>
</Button>

6. Accessibility (WCAG 2.1 AA/AAA)

6.1 ARIA Labels & Roles

<div class="invoice-form" role="form" aria-label="Rechnungsformular">

  <!-- Section with proper heading hierarchy -->
  <section aria-labelledby="invoice-info-heading">
    <h3 id="invoice-info-heading">Rechnungsinformationen</h3>
    ...
  </section>

  <!-- Required field indication -->
  <label for="invoiceNumber">
    Rechnungsnummer
    <abbr title="Pflichtfeld" aria-label="Pflichtfeld">*</abbr>
  </label>
  <InputText id="invoiceNumber"
             aria-required="true"
             aria-invalid="submitted && !form.invoiceNumber"
             aria-describedby="invoiceNumber-error" />
  <small id="invoiceNumber-error"
         class="p-error"
         role="alert"
         v-if="submitted && !form.invoiceNumber">
    Rechnungsnummer ist erforderlich
  </small>

  <!-- Invoice items list -->
  <div role="list" aria-label="Rechnungspositionen">
    <div v-for="(item, index) in form.items"
         :key="index"
         role="listitem"
         :aria-label="`Position ${index + 1}`">
      ...
    </div>
  </div>

  <!-- Action buttons -->
  <div role="group" aria-label="Formularaktionen">
    <Button label="Abbrechen"
            aria-label="Formular abbrechen" />
    <Button label="Speichern"
            aria-label="Rechnung speichern" />
  </div>
</div>

6.2 Farbkontrast

Mindestanforderungen:

  • Normaler Text (< 18pt): 4.5:1 (AA), 7:1 (AAA)
  • Großer Text (≥ 18pt): 3:1 (AA), 4.5:1 (AAA)
  • UI-Komponenten: 3:1

Farb-Validierung:

/* Ensure sufficient contrast */
.p-error {
  color: #DC2626; /* Contrast ratio 4.5:1 on white */
}

.text-secondary {
  color: #6B7280; /* Contrast ratio 4.6:1 on white */
}

/* Never rely on color alone */
.required-field::after {
  content: ' *';
  color: var(--danger-color);
  font-weight: bold;
}

.invoice-status--overdue {
  color: var(--danger-color);
  font-weight: 600;
  text-decoration: underline; /* Visual indicator beyond color */
}

6.3 Focus Management

/* Custom focus styles */
*:focus-visible {
  outline: 2px solid var(--primary-color);
  outline-offset: 2px;
  border-radius: 4px;
}

/* Skip to main content link */
.skip-link {
  position: absolute;
  top: -40px;
  left: 0;
  background: var(--primary-color);
  color: white;
  padding: 8px;
  text-decoration: none;
  z-index: 100;
}

.skip-link:focus {
  top: 0;
}

Focus Trap in Dialog:

// useFocusTrap.js
import { onMounted, onUnmounted } from 'vue'

export function useFocusTrap(containerRef) {
  const focusableElements = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'

  const trapFocus = (e) => {
    if (e.key !== 'Tab' || !containerRef.value) return

    const focusable = containerRef.value.querySelectorAll(focusableElements)
    const firstFocusable = focusable[0]
    const lastFocusable = focusable[focusable.length - 1]

    if (e.shiftKey) {
      if (document.activeElement === firstFocusable) {
        lastFocusable.focus()
        e.preventDefault()
      }
    } else {
      if (document.activeElement === lastFocusable) {
        firstFocusable.focus()
        e.preventDefault()
      }
    }
  }

  onMounted(() => {
    document.addEventListener('keydown', trapFocus)
    // Focus first element
    const firstFocusable = containerRef.value?.querySelector(focusableElements)
    firstFocusable?.focus()
  })

  onUnmounted(() => {
    document.removeEventListener('keydown', trapFocus)
  })
}

6.4 Screen Reader Support

<!-- Live region for dynamic updates -->
<div aria-live="polite" aria-atomic="true" class="sr-only">
  <span v-if="saving">Rechnung wird gespeichert...</span>
  <span v-if="saved">Rechnung wurde gespeichert</span>
</div>

<!-- Descriptive button labels -->
<Button icon="pi pi-trash"
        aria-label="`Position ${index + 1} entfernen`"
        @click="removeItem(index)" />

<!-- Progress indication -->
<ProgressBar :value="uploadProgress"
             role="progressbar"
             :aria-valuenow="uploadProgress"
             aria-valuemin="0"
             aria-valuemax="100"
             aria-label="Upload-Fortschritt" />

<!-- Screen reader only class -->
<style>
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border-width: 0;
}
</style>

7. Performance Optimierungen

7.1 Lazy Loading

// Lazy load forms in InvoiceManagement.vue
const InvoiceForm = defineAsyncComponent(() =>
  import('./InvoiceForm.vue')
)
const PaymentForm = defineAsyncComponent(() =>
  import('./PaymentForm.vue')
)
const PDFUploadForm = defineAsyncComponent(() =>
  import('./PDFUploadForm.vue')
)

7.2 Debouncing

// Debounce expensive calculations
import { debounce } from 'lodash-es'

const calculateTotals = debounce(() => {
  // Berechnung der Gesamtsummen
  totals.value = {
    net: 0,
    taxBreakdown: {},
    gross: 0
  }

  form.value.items.forEach(item => {
    const net = item.quantity * item.unitPrice
    const tax = net * (item.taxRate / 100)

    totals.value.net += net
    totals.value.taxBreakdown[item.taxRate] =
      (totals.value.taxBreakdown[item.taxRate] || 0) + tax
    totals.value.gross += net + tax
  })
}, 300)

7.3 Virtual Scrolling (für lange Listen)

<!-- Wenn viele Kontakte -->
<VirtualScroller :items="contacts"
                 :item-size="50"
                 class="contact-list">
  <template v-slot:item="{ item }">
    <div class="contact-option">
      {{ item.companyName }}
    </div>
  </template>
</VirtualScroller>

8. Testing Strategy

8.1 Unit Tests

// InvoiceForm.spec.js
import { mount } from '@vue/test-utils'
import InvoiceForm from './InvoiceForm.vue'
import PrimeVue from 'primevue/config'

describe('InvoiceForm', () => {
  it('validates required fields', async () => {
    const wrapper = mount(InvoiceForm, {
      global: {
        plugins: [PrimeVue]
      }
    })

    await wrapper.find('button[type="submit"]').trigger('click')

    expect(wrapper.text()).toContain('Rechnungsnummer ist erforderlich')
    expect(wrapper.text()).toContain('Bitte wählen Sie einen Kunden aus')
  })

  it('calculates totals correctly', async () => {
    const wrapper = mount(InvoiceForm)

    await wrapper.vm.addItem()
    wrapper.vm.form.items[0] = {
      quantity: 2,
      unitPrice: 100,
      taxRate: 19
    }

    await wrapper.vm.$nextTick()

    expect(wrapper.vm.totals.net).toBe(200)
    expect(wrapper.vm.totals.gross).toBe(238)
  })
})

8.2 E2E Tests (Cypress/Playwright)

// invoice.spec.js
describe('Invoice Management', () => {
  it('creates new invoice', () => {
    cy.login('admin@mycrm.local', 'admin123')
    cy.visit('/billing/invoices')

    cy.contains('Neue Rechnung').click()

    // Fill form
    cy.get('#invoiceNumber').type('INV-2025-001')
    cy.get('#contact').click()
    cy.contains('ACME Corp').click()

    // Add item
    cy.contains('Position hinzufügen').click()
    cy.get('[id^="description-"]').first().type('Consulting Services')
    cy.get('[id^="quantity-"]').first().clear().type('10')
    cy.get('[id^="unitPrice-"]').first().clear().type('100')

    // Verify total
    cy.contains('Gesamtbetrag:').parent().should('contain', '1.190,00 €')

    // Save
    cy.contains('button', 'Speichern').click()

    // Verify success
    cy.contains('Rechnung wurde erstellt')
    cy.contains('INV-2025-001')
  })
})

9. Implementierungs-Roadmap

Phase 1: Foundation (Woche 1-2)

  • Styling-System aufsetzen (CSS Variables, Utilities)
  • Formular-Layout refactoren (Grid System, Sections)
  • FloatLabel für alle Inputs implementieren
  • Validation Composable erstellen

Phase 2: Invoice Items Redesign (Woche 3)

  • Card-basierte Positions-Ansicht implementieren
  • Echtzeit-Summenberechnung
  • Transitions für Add/Remove
  • Empty State Design

Phase 3: Enhanced Forms (Woche 4)

  • PaymentForm Context Card
  • PDF Upload Drag & Drop
  • Quick Amount Buttons
  • Keyboard Shortcuts

Phase 4: UX Polish (Woche 5)

  • Loading States & Skeletons
  • Micro-Interactions & Animations
  • Toast-Benachrichtigungen harmonisieren
  • Error Handling verbessern

Phase 5: Accessibility (Woche 6)

  • ARIA Labels hinzufügen
  • Focus Management
  • Keyboard Navigation
  • Screen Reader Testing

Phase 6: Testing & Optimization (Woche 7)

  • Unit Tests schreiben
  • E2E Tests implementieren
  • Performance Profiling
  • Code Splitting optimieren

10. Design-System Komponenten

10.1 Wiederverwendbare Komponenten

FormSection.vue - Wrapper für Formular-Sektionen

<template>
  <div class="form-section" :class="{'form-section--collapsible': collapsible}">
    <div v-if="title" class="form-section-header">
      <h3 class="form-section-title">
        <slot name="title">{{ title }}</slot>
      </h3>
      <div class="form-section-actions">
        <slot name="actions"></slot>
      </div>
    </div>
    <div class="form-section-body">
      <slot></slot>
    </div>
  </div>
</template>

<script setup>
defineProps({
  title: String,
  collapsible: Boolean
})
</script>

CurrencyDisplay.vue - Formatierte Währungsanzeige

<template>
  <span class="currency-display" :class="[`currency-display--${size}`, { 'currency-display--highlight': highlight }]">
    {{ formatted }}
  </span>
</template>

<script setup>
import { computed } from 'vue'

const props = defineProps({
  amount: [Number, String],
  currency: { type: String, default: 'EUR' },
  locale: { type: String, default: 'de-DE' },
  size: { type: String, default: 'normal' }, // small, normal, large
  highlight: Boolean
})

const formatted = computed(() => {
  const num = parseFloat(props.amount || 0)
  return new Intl.NumberFormat(props.locale, {
    style: 'currency',
    currency: props.currency
  }).format(num)
})
</script>

<style scoped>
.currency-display--small { font-size: 0.875rem; }
.currency-display--normal { font-size: 1rem; }
.currency-display--large { font-size: 1.25rem; font-weight: 600; }
.currency-display--highlight { color: var(--primary-color); }
</style>

StatusBadge.vue - Einheitliche Status-Anzeige

<template>
  <Tag :value="label"
       :severity="severity"
       :icon="icon"
       class="status-badge">
    <template #default>
      <i v-if="icon" :class="icon" class="mr-1"></i>
      {{ label }}
    </template>
  </Tag>
</template>

<script setup>
import { computed } from 'vue'
import Tag from 'primevue/tag'

const props = defineProps({
  status: String,
  type: { type: String, default: 'invoice' } // invoice, payment, etc.
})

const statusConfig = {
  invoice: {
    draft: { label: 'Entwurf', severity: 'secondary', icon: 'pi pi-file-edit' },
    open: { label: 'Offen', severity: 'info', icon: 'pi pi-clock' },
    paid: { label: 'Bezahlt', severity: 'success', icon: 'pi pi-check-circle' },
    partial: { label: 'Teilweise bezahlt', severity: 'warning', icon: 'pi pi-exclamation-triangle' },
    overdue: { label: 'Überfällig', severity: 'danger', icon: 'pi pi-times-circle' },
    cancelled: { label: 'Storniert', severity: 'secondary', icon: 'pi pi-ban' }
  }
}

const config = computed(() => statusConfig[props.type]?.[props.status] || {})
const label = computed(() => config.value.label || props.status)
const severity = computed(() => config.value.severity || 'info')
const icon = computed(() => config.value.icon)
</script>

11. Anhang

11.1 Checkliste für Entwickler

Vor der Implementierung:

  • Design-System Variablen verstanden
  • PrimeVue Komponenten-Dokumentation gelesen
  • Accessibility Guidelines durchgegangen
  • Responsive Breakpoints definiert

Während der Implementierung:

  • Semantisches HTML verwenden
  • ARIA-Attribute hinzufügen
  • Keyboard-Navigation testen
  • Validierung implementieren
  • Loading States einbauen
  • Error Handling abdecken

Nach der Implementierung:

  • Mit Screen Reader testen
  • Auf verschiedenen Bildschirmgrößen testen
  • Performance messen
  • Unit Tests schreiben
  • Code Review durchführen

11.2 Browser-Kompatibilität

Mindestanforderungen:

  • Chrome/Edge 90+
  • Firefox 88+
  • Safari 14+
  • Mobile Safari 14+
  • Chrome Android 90+

Polyfills für:

  • Intl.NumberFormat (bereits vorhanden)
  • CSS Custom Properties (bereits unterstützt)

11.3 Design-Assets

Icons: PrimeIcons (pi-*) Fonts: System Font Stack

font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
             'Helvetica Neue', Arial, sans-serif;

Illustrations: Optional für Empty States (z.B. unDraw)


12. Zusammenfassung & Nächste Schritte

Key Takeaways

  1. Visuelle Hierarchie durch klare Sektion-Trennung und konsistentes Spacing
  2. Verbesserte Formular-UX mit FloatLabels, Echtzeit-Validierung und besseren Fehler-Messages
  3. Card-basierte Rechnungspositionen statt komplexer Inline-DataTable
  4. Echtzeit-Summenberechnung für sofortiges Feedback
  5. Accessibility-First Ansatz mit ARIA-Labels und Keyboard-Support
  6. Responsive Design für alle Geräte optimiert

Quick Wins (sofort umsetzbar)

  1. FloatLabel für alle Formular-Felder
  2. Status-Tag in Formular-Header
  3. Verbesserte Kunden-Dropdown mit Icons
  4. Echtzeit-Summenberechnung
  5. Quick Amount Buttons im PaymentForm

Langfristige Verbesserungen

  1. Komplett neues Positions-Management mit Cards
  2. Drag & Drop PDF Upload
  3. Umfassendes Keyboard-Shortcut System
  4. Vollständige WCAG AAA Konformität
  5. Progressive Web App Features

Dokument-Version: 1.0 Letztes Update: 2025-12-12 Autor: Claude (UX Designer Agent) Projekt: myCRM Billing Module