Zum Inhalt

ERP Entwicklung – Developer Guide

Dieser Guide richtet sich an Entwickler, die an Kundenprojekten auf Basis von easySale arbeiten. Er beschreibt alle wiederverwendbaren Controls, das Plugin-System, das Datenmodell-Pattern sowie den Aufbau von neuen Connectors und Jobs.


Inhaltsverzeichnis

  1. Shared Controls (shared-Paket)
  2. ERP-spezifische Controls
  3. Plugin-System (ClientConfig)
  4. Daten & Models
  5. Neuen Connector erstellen
  6. Neuen Job erstellen

1. Shared Controls

Alle Widgets aus core/shared/lib/widgets/ stehen in jedem Kundenprojekt über das shared-Paket zur Verfügung:

import 'package:shared/widgets/controls/buttons/es_action_button.dart';
import 'package:shared/widgets/controls/input/es_text_field.dart';
// ...

1.1 Buttons

EsActionButton

Universeller Action-Button mit drei Varianten. Wichtigster Button im System.

Parameter Typ Standard Beschreibung
onTap VoidCallback? required null = disabled
icon IconData required Icon links
label String? null Optional: Text neben Icon
color Color? Theme-Farbe Akzentfarbe
isPrimary bool false Stärkere Hintergrund-Tinte
isFilled bool false Solid-Background (CTA)
centered bool false Inhalt zentrieren (Full-Width)
isLoading bool false Zeigt Spinner
iconSize double 20 Icon-Größe
padding double 10 Innenabstand
borderRadius double 12 Eckenradius
// Icon-only Button (Default)
EsActionButton(
  onTap: () => _handleEdit(),
  icon: CupertinoIcons.pencil,
  color: Colors.blue,
)

// Mit Label (Primary)
EsActionButton(
  onTap: () => _save(),
  icon: CupertinoIcons.checkmark_circle_fill,
  label: 'Speichern',
  isPrimary: true,
  color: Colors.green,
)

// CTA-Button (Filled)
EsActionButton(
  onTap: () => _create(),
  icon: CupertinoIcons.plus,
  label: 'Neuen Kunden erstellen',
  isFilled: true,
  centered: true,
  color: primaryColor,
)

// Disabled
EsActionButton(
  onTap: null,
  icon: CupertinoIcons.trash,
  color: Colors.red,
)

// Loading State
EsActionButton(
  onTap: _handleSave,
  icon: CupertinoIcons.cloud_upload,
  label: 'Speichern',
  isLoading: _isSaving,
  isFilled: true,
  color: primaryColor,
)

Spezialisierte Buttons

Diese Wrapper um EsActionButton für Standard-Aktionen:

import 'package:shared/widgets/controls/buttons/es_add_button.dart';
import 'package:shared/widgets/controls/buttons/es_edit_button.dart';
import 'package:shared/widgets/controls/buttons/es_delete_button.dart';
import 'package:shared/widgets/controls/buttons/es_copy_button.dart';
import 'package:shared/widgets/controls/buttons/es_action_buttons_container.dart';

// Vordefinierte Action-Buttons
EsAddButton(onTap: () => _showAddDialog())
EsEditButton(onTap: () => _showEditDialog())
EsDeleteButton(onTap: () => _confirmDelete())
EsCopyButton(onTap: () => _copyToClipboard())

// Gruppe von Action-Buttons (z.B. in einer Listenzeile)
EsActionButtonsContainer(
  children: [
    EsEditButton(onTap: () => _edit(item)),
    EsDeleteButton(onTap: () => _delete(item)),
  ],
)

1.2 Input

EsTextField

Standardisiertes Texteingabefeld mit integrierter Validierung.

Parameter Typ Standard Beschreibung
controller TextEditingController required Text-Controller
label String? null Label über dem Feld
type EsTextFieldType text Feldtyp mit Validierung
isMandatory bool false Pflichtfeld-Validierung
maxLines int? 1 Textarea bei > 1
isReadonly bool false Nur-Lesen
validator FormFieldValidator<String>? null Custom Validator
suffixIcon Widget? null Icon am rechten Rand
maxLength int? null Maximale Zeichen
obscureText bool false Passwort-Feld
import 'package:shared/widgets/controls/input/es_text_field.dart';

// Einfaches Textfeld
EsTextField(
  label: 'Name',
  controller: _nameController,
  isMandatory: true,
)

// E-Mail mit integrierter Validierung
EsTextField(
  label: 'E-Mail',
  controller: _emailController,
  type: EsTextFieldType.email,
  isMandatory: true,
)

// Dezimalzahl
EsTextField(
  label: 'Preis',
  controller: _priceController,
  type: EsTextFieldType.decimal,
)

// Mehrzeiliges Textfeld
EsTextField(
  label: 'Beschreibung',
  controller: _descController,
  maxLines: 5,
)

// Mit Custom-Validator
EsTextField(
  label: 'Kundennummer',
  controller: _numberController,
  isMandatory: true,
  validator: (v) => v!.length < 3 ? 'Mindestens 3 Zeichen' : null,
)

Verfügbare Typen (EsTextFieldType): - text – Freitext - email – E-Mail-Validierung - phone – Telefonnummer-Validierung - homepage – URL-Validierung - decimal – Dezimalzahl - int – Ganzzahl - custom – Eigenes Regex-Pattern via customValidationPattern

EsDropdown<T>

Generisches Dropdown für beliebige Enum- oder Objekt-Listen.

import 'package:shared/widgets/controls/input/es_dropdown.dart';

EsDropdown<MyStatusEnum>(
  label: 'Status',
  value: _selectedStatus,
  items: MyStatusEnum.values,
  labelBuilder: (s) => s.labelDE,
  onChanged: (v) => setState(() => _selectedStatus = v!),
  isMandatory: true,
)

// Mit führendem Icon pro Item
EsDropdown<CustomerUserType>(
  label: 'Benutzertyp',
  value: _userType,
  items: CustomerUserType.values,
  labelBuilder: (t) => t.titleDE,
  leadingIconBuilder: (t) => Icon(t.icon, size: 18),
  onChanged: (v) => setState(() => _userType = v!),
)

EsDatePicker / EsDateSpanPicker

import 'package:shared/widgets/controls/input/es_date_picker.dart';
import 'package:shared/widgets/controls/input/es_date_span_picker.dart';

// Einzelnes Datum
EsDatePicker(
  label: 'Lieferdatum',
  selectedDate: _date,
  onDateSelected: (d) => setState(() => _date = d),
  isMandatory: true,
)

// Datumsbereich (Von / Bis)
EsDateSpanPicker(
  label: 'Zeitraum',
  startDate: _start,
  endDate: _end,
  onStartDateChanged: (d) => setState(() => _start = d),
  onEndDateChanged: (d) => setState(() => _end = d),
)

EsSearchField

import 'package:shared/widgets/controls/input/es_search_field.dart';

EsSearchField(
  onChanged: (query) => setState(() => _searchQuery = query),
  hintText: 'Kunden suchen...',
)

EsCountryPicker / EsLanguagePicker

import 'package:shared/widgets/controls/input/es_country_picker.dart';
import 'package:shared/widgets/controls/input/es_language_picker.dart';

EsCountryPicker(
  label: 'Land',
  value: _selectedCountry,
  onChanged: (c) => setState(() => _selectedCountry = c!),
)

EsLanguagePicker(
  label: 'Sprache',
  value: _selectedLanguage,
  onChanged: (l) => setState(() => _selectedLanguage = l!),
)

1.3 Display

EsInfoBadge

Icon + Label in farbigem Badge-Style. Für Status, Kategorien und sonstige Info-Labels.

import 'package:shared/widgets/controls/display/es_info_badge.dart';

EsInfoBadge(
  icon: CupertinoIcons.person_2_fill,
  label: 'Kunden',
  color: Colors.blue,
)

// Ohne explizite Farbe (Theme-Farbe)
EsInfoBadge(
  icon: CupertinoIcons.cube_box,
  label: 'Artikel',
)

// Kompakt (kleinere Schrift + Padding)
EsInfoBadge(
  icon: CupertinoIcons.checkmark_circle_fill,
  label: 'Aktiv',
  color: Colors.green,
  fontSize: 11,
  iconSize: 11,
  horizontalPadding: 6,
  verticalPadding: 3,
)

EsStatusBadge

Speziell für Status-Anzeigen mit vordefinierten Farb-Zuständen.

import 'package:shared/widgets/controls/display/es_status_badge.dart';

EsCountBadge

Kleines numerisches Badge (z.B. für Zähler an Icons).

import 'package:shared/widgets/controls/display/es_count_badge.dart';

EsEmptyState

Standardisierter Leer-Zustand für Listen und Seiten. Zwei Varianten:

import 'package:shared/widgets/controls/display/es_empty_state.dart';

// Einfach (für Suchergebnisse, gefilterte Listen)
EsEmptyState(
  icon: Icons.search_off,
  message: 'Keine Ergebnisse',
  subtitle: 'Versuche eine andere Suche',
)

// Settings-Style (mit Card + Action-Button)
EsEmptyState.settings(
  icon: CupertinoIcons.time,
  title: 'Keine Jobs konfiguriert',
  description: 'Erstelle deinen ersten Job über den Button unten.',
  actionLabel: 'Job erstellen',
  onActionPressed: () => _showAddDialog(),
  primaryColor: primaryColor,
)

EsListView / EsListViewItem

import 'package:shared/widgets/controls/display/es_list_view.dart';
import 'package:shared/widgets/controls/display/es_list_view_item.dart';

EsListView(
  items: items,
  itemBuilder: (context, item) => EsListViewItem(
    title: item.name,
    subtitle: item.description,
    leading: Icon(item.icon),
    trailing: EsActionButtonsContainer(
      children: [
        EsEditButton(onTap: () => _edit(item)),
        EsDeleteButton(onTap: () => _delete(item)),
      ],
    ),
    onTap: () => _openDetail(item),
  ),
)

EsSectionHeader

Überschrift für Abschnitte (mit optionaler Trennlinie).

import 'package:shared/widgets/controls/display/es_section_header.dart';

EsSectionHeader(title: 'Kontaktdaten')

EsLabelValueBox

Kompakte Anzeige von Label/Wert-Paaren in einer Box.

import 'package:shared/widgets/controls/display/es_label_value_box.dart';

EsLabelValueBox(label: 'Kunden-ID', value: customer.id)

EsFormLabel

Beschriftung für Formularfelder (konsistentes Styling).

import 'package:shared/widgets/controls/display/es_form_label.dart';

EsFormLabel(label: 'Rechnungsadresse', isMandatory: true)

EsGradientIconContainer / EsDialogIconContainer

Styled Icon-Container für Seiten-Header, Dialoge usw.

import 'package:shared/widgets/controls/display/es_gradient_icon_container.dart';
import 'package:shared/widgets/controls/display/es_dialog_icon_container.dart';

EsGradientIconContainer(
  icon: CupertinoIcons.cube_box,
  color: primaryColor,
)

EsDialogIconContainer(
  icon: CupertinoIcons.person_add,
  color: Colors.green,
)

1.4 Wizard

Für mehrstufige Erstellungs-Dialoge:

import 'package:shared/widgets/controls/wizard/es_wizard_header.dart';
import 'package:shared/widgets/controls/wizard/es_wizard_progress_indicator.dart';
import 'package:shared/widgets/controls/wizard/es_wizard_navigation_buttons.dart';

// Header mit Titel und Schritt-Info
EsWizardHeader(
  title: 'Neuen Kunden erstellen',
  currentStep: _currentStep,
  totalSteps: _steps.length,
)

// Fortschrittsanzeige
EsWizardProgressIndicator(
  currentStep: _currentStep,
  totalSteps: _steps.length,
  stepLabels: ['Stammdaten', 'Kontakt', 'Einstellungen'],
)

// Navigations-Buttons (Zurück / Weiter / Fertig)
EsWizardNavigationButtons(
  currentStep: _currentStep,
  totalSteps: _steps.length,
  onNext: _nextStep,
  onBack: _previousStep,
  onFinish: _save,
  isLoading: _isSaving,
)

1.5 Dialoge

EsDialogHeader

import 'package:shared/widgets/dialogs/es_dialog_header.dart';

showDialog(
  context: context,
  builder: (ctx) => Dialog(
    child: Column(
      children: [
        EsDialogHeader(
          title: 'Kategorie erstellen',
          icon: CupertinoIcons.tag,
          color: primaryColor,
          onClose: () => Navigator.of(ctx).pop(),
        ),
        // Dialog-Inhalt
      ],
    ),
  ),
)

1.6 EsBlocBuilder

Typsicherer BLoC-Builder mit Loading/Error-Handling:

import 'package:shared/widgets/es_bloc_builder.dart';

EsBlocBuilder<MyBloc, MyState>(
  builder: (context, state) {
    return MyWidget(data: state.data);
  },
  loadingBuilder: (context) => const CircularProgressIndicator(),
  errorBuilder: (context, error) => Text('Fehler: $error'),
)

1.7 Charts

import 'package:shared/widgets/charts/es_line_chart.dart';

EsLineChart(
  data: _chartData,
  color: primaryColor,
)

2. ERP-spezifische Controls

Diese Controls liegen in core/apps/erp_system/lib/pages/widgets/controls/ und sind im ERP und in ERP-basierten Kundenprojekten verfügbar.

Buttons

Widget Import Beschreibung
EsDialogActionButtons controls/buttons/ OK/Abbrechen-Buttons für Dialoge
EsDropdownActionButton controls/buttons/ Dropdown-Button mit Aktionsliste
EsFilterButton controls/buttons/ Filter-Toggle-Button
EsTranslateButton controls/buttons/ Übersetzungs-Aktion
EsRoundNavBarButton controls/buttons/ Runder Button für NavBar

Input

Widget Import Beschreibung
EsCheckbox / EsCheckboxItem controls/input/ Checkbox mit Label
EsColorPicker controls/input/ Farbauswahl
EsDatePicker (ERP) controls/input/ ERP-spezifischer Datepicker
EsDateTextField / EsTimeTextField controls/input/ Text + Datums-Combo
EsEnumPicker controls/input/ Enum-Auswahl
EsGenericSearchSelector controls/input/ Such-Dropdown
EsIconPicker controls/input/ Icon-Auswahl
EsLanguageTextFields controls/input/ Mehrsprachige Textfelder
EsListPicker controls/input/ Liste mit Mehrfachauswahl
EsMultiWeekDayPicker controls/input/ Wochentage-Auswahl
EsUserRolePicker controls/input/ Benutzerrollen-Auswahl

Cards

Widget Import Beschreibung
EsCard controls/cards/ Standard Content-Card
EsCardSelector / EsCardSelectorWithIcon controls/cards/ Auswählbare Card
EsSelectableOptionCard controls/cards/ Option-Card mit Checkbox
EsSectionCard controls/cards/ Abschnitt mit Card-Wrapper
EsSettingsCard controls/cards/ Settings-Eintrag (Icon + Titel + Chevron)

Display (ERP)

Widget Import Beschreibung
EsCircularProgressIndicator controls/display/ Styled Loading-Spinner
EsContentContainer controls/display/ Zentrierter Content-Container
EsCustomVerticalStepper controls/display/ Vertikaler Schritt-Anzeiger
EsDetailPageHeader controls/display/ Header für Detailseiten
EsDetailHeaderInfo controls/display/ Info-Zeile im Detail-Header
EsDetailHeaderImage controls/display/ Bild im Detail-Header
EsFilterableTable controls/display/ Tabelle mit Filter
EsIconContainer controls/display/ Icon in farbigem Container
EsIconHeadline controls/display/ Überschrift mit Icon
EsIconTextHeader controls/display/ Header mit Icon + Text
EsImagePlaceholder controls/display/ Platzhalter für fehlende Bilder
EsImagePreviewCard controls/display/ Bild-Vorschau-Karte
EsInfiniteListView controls/display/ Infinite-Scroll-Liste
EsInfoBanner controls/display/ Info/Warn/Error-Banner
EsInfoChip controls/display/ Kleiner Info-Chip
EsInfoRow controls/display/ Label + Value in einer Zeile
EsLanguageHeader controls/display/ Sprach-Header für i18n-Felder
EsLoadingContainer controls/display/ Scaffold mit Loading-State
EsLoadingScaffold controls/display/ Vollseiten-Loading-Scaffold
EsMenuBar controls/display/ Horizontale Menüleiste
EsNetworkImage controls/display/ Netzwerkbild mit Fallback
EsSearchBar controls/display/ Suchleiste
EsSingleImageViewer controls/display/ Vollansicht eines Bildes
EsSubPageHeaderRow controls/display/ Header-Zeile für Unterseiten
EsTabMenu controls/display/ Tab-Menü

Sonstige

Widget Import Beschreibung
EsOverlayPopup controls/misc/ Kontext-Popup (Overlay)
EsSelectDate / EsSelectTime controls/misc/ Datum/Uhrzeit-Picker
EsStatItem controls/misc/ Statistik-Wert-Anzeige
EsSwitch controls/misc/ Toggle-Switch
EsUserAvatar controls/misc/ Benutzer-Avatar
EsVerticalSeparator controls/misc/ Vertikale Trennlinie

3. Plugin-System

Kundenprojekte erweitern das ERP/Shop über die ClientConfig-Klasse, die in core/shared/lib/config/client_config.dart definiert ist. Jedes Kundenprojekt implementiert DefaultClientConfig und überschreibt nur das, was benötigt wird.

Einstiegspunkt: main.dart

// erp/lib/main.dart im Kundenprojekt
import 'package:easy_sale_erp/main.dart' as erp_main;
import 'package:shared/shared.dart';
import 'config/my_client_config.dart';

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  GetIt.instance.registerLazySingleton<ClientConfig>(() => MyClientConfig());
  erp_main.main();
}

ClientConfig Übersicht

// erp/lib/config/my_client_config.dart
class MyClientConfig extends DefaultClientConfig {
  @override String get clientId => 'my_client';
  @override String get clientName => 'Mein Kunde GmbH';
  @override String? get logoPath => 'assets/logos/logo.png';

  @override
  Map<String, bool> get features => {
    'enableDashboard': true,
    'enableCustomer': true,
    'enableArticle': true,
    'enableOrder': true,
    'enableMyCustomFeature': true,
  };

  @override
  Map<String, dynamic> get themeOverrides => {
    'primaryColor': 0xFF1565C0,
    'accentColor': 0xFFFF6F00,
    'backgroundColor': 0xFFF5F7FA,
  };
}

3.1 ERP Plugins – Neue Nav-Bar-Seiten

Komplette neue Seiten in der ERP-Navigation hinzufügen:

@override
List<ErpPlugin> get erpPlugins => [
  ErpPlugin(
    key: 'my_page',
    navTitle: 'Meine Seite',
    navIcon: CupertinoIcons.chart_bar,
    widgetFactory: () => const MyPage(),
    position: PluginNavPosition.afterDashboard,
    featureFlag: 'enableMyCustomFeature', // Optional
  ),
];

PluginNavPosition Werte: - afterDashboard - afterCustomers - afterArticles - afterOrders - beforeSettings

Die zugehörige Page ist ein normales StatelessWidget / StatefulWidget:

// erp/lib/plugins/my_page.dart
class MyPage extends StatelessWidget {
  const MyPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(child: Text('Meine kundenspezifische Seite')),
    );
  }
}

3.2 Settings-Plugins – Neue Einstellungs-Einträge

@override
List<ErpSettingsPlugin> get erpSettingsPlugins => [
  ErpSettingsPlugin(
    key: 'my_settings',
    title: 'Eigene Einstellungen',
    subtitle: 'Kundenspezifische Konfiguration',
    icon: CupertinoIcons.gear_alt,
    widgetFactory: () => const MySettingsPage(),
  ),
];

3.3 Detail-Tabs – Artikel & Kunden erweitern

Zusätzliche Tabs in Artikel- oder Kundendetailseiten:

import 'package:easy_sale_erp/tabs/article_tab_registry.dart';
import 'package:easy_sale_erp/tabs/customer_tab_registry.dart';

@override
List<TabDefinition<Article>>? get articleTabs => [
  ...ArticleTabRegistry.defaults(), // Standard-Tabs beibehalten
  TabDefinition<Article>(
    key: 'my_article_tab',
    label: 'Zusatzdaten',
    icon: CupertinoIcons.doc_text,
    contentBuilder: (ctx, article) => MyArticleTab(article: article),
  ),
];

@override
List<TabDefinition<Customer>>? get customerTabs => [
  ...CustomerTabRegistry.defaults(),
  TabDefinition<Customer>(
    key: 'my_customer_tab',
    label: 'Kundenspezifisch',
    icon: CupertinoIcons.person_crop_square,
    contentBuilder: (ctx, customer) => MyCustomerTab(customer: customer),
  ),
];

Tab-Konstanten (ArticleDetailTab):

Konstante Index Beschreibung
ArticleDetailTab.coreData 0 Stammdaten
ArticleDetailTab.descriptions 1 Beschreibungen
ArticleDetailTab.variants 2 Varianten
ArticleDetailTab.customerAssignment 3 Kundenzuweisung
ArticleDetailTab.images 4 Bilder
ArticleDetailTab.documents 5 Dokumente

Inhalte einem bestehenden Tab hinzufügen (ohne den Tab zu ersetzen):

@override
List<Widget> getArticleDetailTabExtraSections(
  BuildContext context,
  Article article,
  int tabIndex,
) {
  if (tabIndex == ArticleDetailTab.coreData) {
    return [MyExtraSection(article: article)];
  }
  return [];
}

3.4 Erstellungs-Wizard erweitern

Zusätzliche Schritte im Artikel- oder Kunden-Erstellungsdialog:

@override
List<CreateStepDefinition>? get articleCreateSteps => [
  CreateStepDefinition(
    key: 'my_extra_step',
    title: 'Pharma-Pflichtfelder',
    icon: Icons.science,
    contentWidget: MyCreateStepWidget.new,
    validator: (data) =>
        data['chargennummer']?.isEmpty ?? true ? 'Pflichtfeld' : null,
  ),
];

// Daten vor dem Speichern anreichern
@override
Article enrichArticleBeforeCreate(Article article, Map<String, dynamic> extraData) {
  return article.copyWith(
    customData: {'chargennummer': extraData['chargennummer']},
  );
}

3.5 Shop-Plugins

Dieselbe Logik gilt für das Shop-System. Verfügbare Plugin-Typen:

Plugin Methode Beschreibung
ShopMobileBottomNavBarPlugin shopMobileBottomNavBarPlugins Neuer Tab im mobilen Bottom-NavBar
ShopWebTopBarPlugin shopWebTopBarPlugins Icon-Button in der Web-TopBar
ShopProfilePlugin shopProfilePlugins Eintrag im Profil-Menü
ShopFeedPlugin shopFeedPlugins Widget in der Feed-Seite
ShopArticlesPagePlugin shopArticlesPagePlugins Widget auf der Artikel-Seite
ShopArticleDetailPlugin shopArticleDetailPlugins Widget in der Artikel-Detailseite
ShopCartPlugin shopCartPlugins Widget in der Warenkorb-Seite
@override
List<ShopMobileBottomNavBarPlugin> get shopMobileBottomNavBarPlugins => [
  ShopMobileBottomNavBarPlugin(
    key: 'my_recipes',
    navIcon: CupertinoIcons.doc_text,
    navLabel: 'Rezepte',
    widgetFactory: () => const MyRecipesPage(),
    position: ShopMobileBottomNavBarPosition.afterOrders,
  ),
];

4. Daten & Models

4.1 Model erstellen

Alle Models erben von EntityBase (aus package:shared/models/base/entity_base.dart) und implementieren fromMap + toMap + copyWith:

import 'package:shared/models/base/entity_base.dart';

class MyExtensionData extends EntityBase {
  final String customField;
  final int quantity;
  final bool isActive;

  MyExtensionData({
    super.id = '',
    required this.customField,
    required this.quantity,
    this.isActive = true,
    super.createdAt,
    super.modifiedAt,
    super.createdBy,
    super.modifiedBy,
  });

  /// Aus Firestore/JSON laden
  factory MyExtensionData.fromMap(Map<String, dynamic> json, String id) {
    return MyExtensionData(
      id: id,
      customField: json['customField'] ?? '',
      quantity: json['quantity'] ?? 0,
      isActive: json['isActive'] ?? true,
      createdAt: json['createdAt']?.toDate(),
      modifiedAt: json['modifiedAt']?.toDate(),
      createdBy: json['createdBy'],
      modifiedBy: json['modifiedBy'],
    );
  }

  /// Für Firestore/JSON speichern
  Map<String, dynamic> toMap() {
    return {
      'customField': customField,
      'quantity': quantity,
      'isActive': isActive,
      'createdAt': createdAt?.toIso8601String(),
      'modifiedAt': modifiedAt?.toIso8601String(),
      'createdBy': createdBy,
      'modifiedBy': modifiedBy,
    };
  }

  /// Unveränderliche Kopie mit Änderungen
  MyExtensionData copyWith({
    String? id,
    String? customField,
    int? quantity,
    bool? isActive,
  }) {
    return MyExtensionData(
      id: id ?? this.id,
      customField: customField ?? this.customField,
      quantity: quantity ?? this.quantity,
      isActive: isActive ?? this.isActive,
      createdAt: createdAt,
      modifiedAt: modifiedAt,
    );
  }
}

4.2 Firebase Service erstellen

Alle Firebase-Zugriffe erben von BaseFirebaseService. Der Service stellt firestore, auth und storage bereit und behandelt Multi-Tenant sicher:

import 'package:easy_sale_erp/services/firebase_services/base_firebase_service.dart';
import 'package:cloud_firestore/cloud_firestore.dart';

class MyExtensionFirebaseService extends BaseFirebaseService {
  /// Pfad: customers/{customerId}/myExtensionData/{docId}
  static const _subCollection = 'myExtensionData';

  CollectionReference<Map<String, dynamic>> _collection(String customerId) {
    return firestore
        .collection('customers')
        .doc(customerId)
        .collection(_subCollection);
  }

  /// Alle Einträge laden
  Future<List<MyExtensionData>> getAll(String customerId) async {
    final snapshot = await _collection(customerId).get();
    return snapshot.docs
        .map((doc) => MyExtensionData.fromMap(doc.data(), doc.id))
        .toList();
  }

  /// Realtime Stream
  Stream<List<MyExtensionData>> stream(String customerId) {
    return _collection(customerId).snapshots().map((snapshot) =>
        snapshot.docs
            .map((doc) => MyExtensionData.fromMap(doc.data(), doc.id))
            .toList());
  }

  /// Einzelnen Eintrag laden
  Future<MyExtensionData?> getById(String customerId, String id) async {
    final doc = await _collection(customerId).doc(id).get();
    if (!doc.exists) return null;
    return MyExtensionData.fromMap(doc.data()!, doc.id);
  }

  /// Erstellen (auto-generated ID)
  Future<String> create(String customerId, MyExtensionData data) async {
    final ref = await _collection(customerId).add(data.toMap());
    return ref.id;
  }

  /// Aktualisieren
  Future<void> update(String customerId, MyExtensionData data) async {
    await _collection(customerId).doc(data.id).set(data.toMap());
  }

  /// Löschen
  Future<void> delete(String customerId, String id) async {
    await _collection(customerId).doc(id).delete();
  }

  /// Mit Filter abfragen
  Future<List<MyExtensionData>> getActive(String customerId) async {
    final snapshot = await _collection(customerId)
        .where('isActive', isEqualTo: true)
        .orderBy('customField')
        .get();
    return snapshot.docs
        .map((doc) => MyExtensionData.fromMap(doc.data(), doc.id))
        .toList();
  }

  /// Batch-Schreiben (für große Mengen)
  Future<void> batchCreate(
    String customerId,
    List<MyExtensionData> items,
  ) async {
    const batchSize = 500; // Firestore-Limit
    for (var i = 0; i < items.length; i += batchSize) {
      final batch = firestore.batch();
      final chunk = items.skip(i).take(batchSize);
      for (final item in chunk) {
        final ref = _collection(customerId).doc();
        batch.set(ref, item.toMap());
      }
      await batch.commit();
    }
  }
}

Firestore-Regeln für eigene Sub-Collections nicht vergessen! Die bestehenden Regeln in core/firestore.rules müssen um die neue Collection erweitert werden.


4.3 Collection-Namen

Neue Collections in FirebaseCollectionNames eintragen (bei ERP-Core-Erweiterungen):

core/apps/erp_system/lib/constants/firebase_collection_names.dart

Für Client-spezifische Sub-Collections kann der Name direkt als String-Konstante im Service definiert werden.


4.4 BLoC für den Service

Standard-Muster für State-Management mit BLoC:

// Events
abstract class MyDataEvent {}
class LoadMyData extends MyDataEvent { final String customerId; }
class CreateMyData extends MyDataEvent {
  final String customerId;
  final MyExtensionData data;
}
class DeleteMyData extends MyDataEvent {
  final String customerId;
  final String id;
}

// States
abstract class MyDataState {}
class MyDataInitial extends MyDataState {}
class MyDataLoading extends MyDataState {}
class MyDataLoaded extends MyDataState {
  final List<MyExtensionData> items;
  MyDataLoaded(this.items);
}
class MyDataError extends MyDataState {
  final String message;
  MyDataError(this.message);
}

// BLoC
class MyDataBloc extends Bloc<MyDataEvent, MyDataState> {
  final MyExtensionFirebaseService _service;

  MyDataBloc(this._service) : super(MyDataInitial()) {
    on<LoadMyData>(_onLoad);
    on<CreateMyData>(_onCreate);
    on<DeleteMyData>(_onDelete);
  }

  Future<void> _onLoad(LoadMyData event, Emitter<MyDataState> emit) async {
    emit(MyDataLoading());
    try {
      final items = await _service.getAll(event.customerId);
      emit(MyDataLoaded(items));
    } catch (e) {
      emit(MyDataError(e.toString()));
    }
  }

  Future<void> _onCreate(CreateMyData event, Emitter<MyDataState> emit) async {
    await _service.create(event.customerId, event.data);
    add(LoadMyData(customerId: event.customerId));
  }

  Future<void> _onDelete(DeleteMyData event, Emitter<MyDataState> emit) async {
    await _service.delete(event.customerId, event.id);
    add(LoadMyData(customerId: event.customerId));
  }
}

5. Neuen Connector erstellen

Connectors importieren oder exportieren Daten zwischen dem ERP und externen Systemen (ERP, FTP, REST-API etc.).

5.1 Naming-Convention

core/functions/src/connectors/instances/handler_<beschreibung>.js

Beispiele: - handler_articles_import.js – Artikel-Import - handler_orders_export.js – Auftrags-Export - handler_customers_import.js – Kunden-Import

5.2 Handler-Struktur

// core/functions/src/connectors/instances/handler_my_import.js
'use strict';

const admin = require('firebase-admin');
const axios = require('axios');

/**
 * Connector-Handler: Beschreibung
 *
 * Erforderliche Connector-Einstellungen (connector.settings):
 *   baseUrl  - Basis-URL der API
 *   apiKey   - API-Schlüssel
 */

const db = admin.firestore();

/**
 * Haupt-Handler (wird von executeConnectorImport aufgerufen)
 *
 * @param {Object} connector - Connector-Konfiguration aus Firestore
 * @param {string} customerId - Customer-ID
 * @param {Object} logger - Logger-Instanz
 */
exports.execute = async (connector, customerId, logger) => {
  const { baseUrl, apiKey } = connector.settings;

  logger.log('info', `Starte Import von ${baseUrl}`);

  // 1. Externe Daten laden
  const response = await axios.get(`${baseUrl}/api/data`, {
    headers: { 'X-API-Key': apiKey },
    timeout: 30_000,
  });
  const items = response.data.data;

  logger.log('info', `${items.length} Einträge empfangen`);

  // 2. In Firestore schreiben (Batch)
  const batchSize = 500;
  for (let i = 0; i < items.length; i += batchSize) {
    const batch = db.batch();
    const chunk = items.slice(i, i + batchSize);

    for (const item of chunk) {
      const ref = db
        .collection('customers').doc(customerId)
        .collection('myCollection').doc(item.id);

      batch.set(ref, {
        ...item,
        syncedAt: admin.firestore.FieldValue.serverTimestamp(),
      }, { merge: true });
    }

    await batch.commit();
  }

  logger.log('success', `Import abgeschlossen: ${items.length} Einträge`);
  return { recordsProcessed: items.length };
};

5.3 Connector in der UI anlegen

  1. Im ERP unter Einstellungen → Connectors einen neuen Connector erstellen
  2. Als Handler-Key den Dateinamen ohne .js angeben (z.B. handler_my_import)
  3. Die settings-Felder (apiKey, baseUrl etc.) konfigurieren
  4. Optional: Cron-Schedule für automatische Ausführung

5.4 Verfügbare Templates

In core/functions/src/connectors/templates/ gibt es fertige Vorlagen:

Template Datei Beschreibung
REST API rest_api_template.js Standard HTTP REST-API
FTP/Excel ftp_excel_template.js FTP-Zugriff auf Excel-Dateien
Business Central business_central_template.js Microsoft BC Integration
SQL sql_connector_template.js Direkter Datenbankzugriff

5.5 Deployment

firebase deploy --only functions:executeConnectorImport,functions:executeConnectorHttp --project <project-id>

6. Neuen Job erstellen

Jobs sind serverseite Aufgaben, die manuell oder nach Zeitplan ausgeführt werden (DSGVO-Bereinigung, Statistiken, Benachrichtigungen, etc.).

6.1 Naming-Convention

core/functions/src/jobs/instances/job_<jobId>.js

Die <jobId> entspricht der Job-ID aus der Firestore-Datenbank. Bei System-Jobs ist es der Enum-Name (z.B. dsgvoDeleteCustomers). Bei manuellen Jobs ist es die in der UI generierte ID.

Ablauf: 1. Job im ERP unter Einstellungen → Jobs erstellen → ID notieren (z.B. abc123xyz) 2. Datei anlegen: job_abc123xyz.js 3. Datei nach functions/src/jobs/instances/ kopieren 4. Deployen

6.2 Template verwenden

Die Datei _TEMPLATE_custom_job.js als Startpunkt kopieren:

// core/functions/src/jobs/instances/job_<jobId>.js
const admin = require('firebase-admin');

/**
 * @param {Object} job          - Job-Konfiguration (id, name, parameters)
 * @param {Object} credentials  - Secret Manager Credentials (optional)
 * @param {Object} logger       - JobLogger-Instanz
 * @param {string} customerId   - Customer-ID
 * @returns {Object} { message, recordsProcessed, affectedRecords }
 */
exports.execute = async (job, credentials, logger, customerId) => {
  logger.log('info', `Starte Job: ${job.name}`);

  try {
    const db = admin.firestore();

    // Parameter aus der Job-Konfiguration lesen
    const maxDays = job.parameters?.maxDays || 365;
    const dryRun = job.parameters?.dryRun ?? false;

    // Daten abfragen
    const cutoffDate = new Date();
    cutoffDate.setDate(cutoffDate.getDate() - maxDays);

    const snapshot = await db
      .collection('customers').doc(customerId)
      .collection('myCollection')
      .where('createdAt', '<', cutoffDate)
      .get();

    logger.log('info', `${snapshot.size} Einträge gefunden`);

    if (dryRun) {
      logger.log('info', 'Dry-Run: keine Änderungen');
      return { message: `Dry-Run: ${snapshot.size} würden gelöscht`, recordsProcessed: 0 };
    }

    // Batch-Delete
    const batch = db.batch();
    snapshot.forEach(doc => batch.delete(doc.ref));
    await batch.commit();

    logger.log('success', `${snapshot.size} Einträge gelöscht`);
    return {
      message: `${snapshot.size} Einträge erfolgreich gelöscht`,
      recordsProcessed: snapshot.size,
      affectedRecords: snapshot.size,
    };

  } catch (error) {
    logger.log('error', `Fehler: ${error.message}`);
    throw error;
  }
};

6.3 Logger-Level

logger.log('debug', '...');    // Detailinfos (nur im Debug-Modus)
logger.log('info', '...');     // Normaler Fortschritt
logger.log('warn', '...');     // Warnung, Ausführung läuft weiter
logger.log('error', '...');    // Fehler (wird im Job-Log angezeigt)
logger.log('success', '...');  // Erfolgsmeldung am Ende

6.4 Job-Parameter

Parameter werden in der UI beim Erstellen oder Bearbeiten eines Jobs konfiguriert und über job.parameters im Handler ausgelesen:

const limit = job.parameters?.limit || 100;
const email = job.parameters?.email || '';
const enabled = job.parameters?.enabled ?? true;

6.5 Credentials (Secret Manager)

Für Jobs, die externe Zugänge benötigen, werden Credentials verschlüsselt im Google Secret Manager gespeichert und über den credentials-Parameter übergeben:

const apiKey = credentials?.apiKey;
const password = credentials?.password;

if (!apiKey) {
  logger.log('warn', 'Kein API-Key – überspringe externen Aufruf');
  return { message: 'Kein API-Key', recordsProcessed: 0 };
}

6.6 Deployment

firebase deploy --only functions:executeJob,functions:executeJobHttp --project <project-id>

Referenz-Dokumentation

Die folgenden Abschnitte enthalten die detaillierte Implementierungsdokumentation der einzelnen Widgets und Features.

EsIconActionButton Widget

Ein wiederverwendbares Icon Action Button Widget für konsistente Action-Buttons in Settings und Listen.

Features

  • ✅ Konsistenter Style: Background mit 0.1 Alpha + Border mit 0.3 Alpha
  • ✅ Touch-Feedback mit InkWell und BorderRadius
  • ✅ Tooltip Support für alle Buttons
  • ✅ Loading State mit CircularProgressIndicator
  • ✅ Disabled State mit reduzierter Opacity und grauer Farbe
  • ✅ Konfigurierbare Icon-Größe, Padding und BorderRadius

Verwendung

import 'package:easy_sale_erp/pages/widgets/controls/es_icon_action_button.dart';

// Basic Button
EsIconActionButton(
  icon: CupertinoIcons.pencil_circle_fill,
  color: primaryColor,
  onPressed: () => _handleEdit(),
  tooltip: 'Bearbeiten',
)

// Button mit Loading State
EsIconActionButton(
  icon: CupertinoIcons.bolt_fill,
  color: Colors.green.shade600,
  onPressed: _handleExecute,
  tooltip: 'Ausführen',
  isLoading: _isExecuting,
)

// Disabled Button
EsIconActionButton(
  icon: CupertinoIcons.trash_circle_fill,
  color: Colors.red.shade600,
  onPressed: _handleDelete,
  tooltip: 'Löschen',
  isDisabled: !canDelete,
)

// Custom Größe und Padding
EsIconActionButton(
  icon: CupertinoIcons.plus,
  color: Colors.blue,
  onPressed: _handleAdd,
  tooltip: 'Hinzufügen',
  iconSize: 20,
  padding: 12,
  borderRadius: 10,
)

Parameter

Parameter Typ Standard Beschreibung
icon IconData required Das anzuzeigende Icon
color Color required Farbe für Icon, Border und Background
onPressed VoidCallback required Callback beim Button-Klick
tooltip String required Tooltip-Text
isLoading bool false Zeigt CircularProgressIndicator statt Icon
isDisabled bool false Deaktiviert Button mit grauer Farbe und 50% Opacity
iconSize double 16 Größe des Icons
padding double 10 Padding um Icon/Spinner
borderRadius double 8 Border-Radius des Buttons

Migrierte Widgets

Folgende Widgets/Methoden wurden durch EsIconActionButton ersetzt:

Settings Card Widgets

  • customer_category_card_widget.dart - _buildIconActionButton
  • article_category_card_widget.dart - _buildIconActionButton
  • package_size_card_widget.dart - _buildIconActionButton
  • document_type_card_widget.dart - _buildIconActionButton
  • notification_group_card_widget.dart - _buildIconActionButton
  • user_card_widget.dart - _buildIconActionButton
  • legal_info_card_widget.dart - Inline Button
  • job_card_widget.dart - _buildIconActionButton
  • connector_card_widget.dart - ConnectorActionButtonWidget

Gelöschte Dateien

  • connector_action_button_widget.dart (ersetzt durch EsIconActionButton)

Vorteile der Zentralisierung

  1. Konsistenz: Einheitlicher Button-Style in allen Settings
  2. Wartbarkeit: Änderungen müssen nur an einer Stelle gemacht werden
  3. Weniger Code: ~40 Zeilen pro Card Widget entfernt
  4. Features: Loading und Disabled States eingebaut
  5. Flexibilität: Anpassbare Parameter für verschiedene Use-Cases

Style-Spezifikation

Container(
  padding: EdgeInsets.all(padding), // default: 10
  decoration: BoxDecoration(
    color: color.withValues(alpha: 0.1),
    borderRadius: BorderRadius.circular(borderRadius), // default: 8
    border: Border.all(
      color: color.withValues(alpha: 0.3),
    ),
  ),
  child: Icon(icon, size: iconSize, color: color), // default iconSize: 16
)

Unterschied zu EsActionIconButton

  • EsIconActionButton: Einfacher Style (Background + Border), für Settings geeignet
  • EsActionIconButton: Gradient + Shadow Style, für hervorgehobene Actions

Beide Widgets koexistieren für unterschiedliche Design-Anforderungen.

EsInfoBadge Widget

Ein wiederverwendbares Badge Widget für Icon + Text Kombinationen in Cards und Listen.

Features

  • ✅ Icon + Label Kombination
  • ✅ Automatische Farb-Shading für MaterialColor und reguläre Color
  • ✅ Konfigurierbare Größen (Icon, Text, Padding, Border)
  • ✅ Konsistentes Design in allen Settings

Verwendung

import 'package:easy_sale_erp/pages/widgets/controls/es_info_badge.dart';

// Basic Badge mit MaterialColor
EsInfoBadge(
  icon: CupertinoIcons.person_2_fill,
  label: 'Kunden',
  color: Colors.blue, // MaterialColor wird automatisch zu .shade50/.shade200/.shade700
)

// Badge mit regulärer Color
EsInfoBadge(
  icon: CupertinoIcons.shield_fill,
  label: 'System',
  color: primaryColor, // Wird mit alpha 0.1/0.3 verwendet
)

// Custom Größen
EsInfoBadge(
  icon: CupertinoIcons.checkmark_circle_fill,
  label: 'Aktiv',
  color: Colors.green,
  iconSize: 14,
  fontSize: 11,
  horizontalPadding: 10,
  verticalPadding: 6,
  borderRadius: 8,
)

Parameter

Parameter Typ Standard Beschreibung
icon IconData required Icon neben dem Label
label String required Anzuzeigender Text
color Color required Farbe für Icon, Text, Border und Background
iconSize double 13 Größe des Icons
fontSize double 12 Schriftgröße des Labels
horizontalPadding double 8 Horizontaler Innenabstand
verticalPadding double 4 Vertikaler Innenabstand
borderRadius double 6 Border-Radius des Badges
borderWidth double 1 Breite des Borders

Farb-Logik

MaterialColor (z.B. Colors.blue, Colors.green)

Background: color.shade50
Border: color.shade200
Icon & Text: color.shade700

Reguläre Color (z.B. primaryColor)

Background: color.withValues(alpha: 0.1)
Border: color.withValues(alpha: 0.3)
Icon & Text: color (original)

Migrierte Komponenten

connector_card_widget.dart

  • _buildDataTypeBadgeEsInfoBadge (Kunden/Artikel/Aufträge)

Weitere Verwendungen (potentiell)

  • System-Badges in Job-Cards
  • Status-Badges in verschiedenen Cards
  • Datentyp-Badges in Import/Export

Beispiele

Datentyp-Badges

// Connector Card - Datentypen
Wrap(
  spacing: 6,
  runSpacing: 6,
  children: [
    if (connector.importsCustomers)
      EsInfoBadge(
        icon: CupertinoIcons.person_2_fill,
        label: 'Kunden',
        color: Colors.blue,
      ),
    if (connector.importsArticles)
      EsInfoBadge(
        icon: CupertinoIcons.cube_box_fill,
        label: 'Artikel',
        color: Colors.green,
      ),
  ],
)

System Badge

// Job Handler Card
if (handler.isSystemHandler)
  EsInfoBadge(
    icon: CupertinoIcons.shield_lefthalf_fill,
    label: 'System',
    color: primaryColor,
  )

Status Badge

// Status Anzeige
EsInfoBadge(
  icon: isActive 
    ? CupertinoIcons.play_circle_fill 
    : CupertinoIcons.pause_circle_fill,
  label: isActive ? 'Aktiv' : 'Pausiert',
  color: isActive ? Colors.green : Colors.orange,
)

Vorteile der Zentralisierung

  1. Konsistenz: Einheitliches Badge-Design in allen Settings
  2. Wartbarkeit: Änderungen nur an einer Stelle
  3. Flexibilität: Unterstützt MaterialColor und reguläre Color
  4. Weniger Code: ~35 Zeilen pro Badge-Implementierung gespart
  5. Type-Safe: Klare API mit benannten Parametern

Unterschied zu anderen Badge-Widgets

  • EsInfoBadge: Icon + Text für Info-Anzeige
  • EsStatusBadge: Nur Icon, rund mit Gradient/Shadow
  • EsCountBadge: Für Zahlen/Zähler

Alle Widgets koexistieren für unterschiedliche Use-Cases.

ES Widgets Implementation - Zusammenfassung

✅ Erstellte Widgets

1. EsSpacing - Spacing Constants

  • Datei: lib/core/constants/es_spacing.dart
  • Verwendung: Konsistente Abstände im gesamten Projekt
  • Varianten: xs(4), s(8), m(12), l(16), xl(20), xxl(24), xxxl(32)
  • Widgets: hGap4-32, wGap4-32
  • Paddings: pagePadding, cardPadding, dialogPadding, sectionPadding

2. SnackBar Helpers - Success/Error/Info/Warning

  • Datei: lib/core/utils/es_snackbar_helpers.dart
  • Functions:
  • showSuccessSnackBar(context, message)
  • showErrorSnackBar(context, message)
  • showInfoSnackBar(context, message)
  • showWarningSnackBar(context, message)

3. EsGradientButton - Gradient Add/Create Buttons

  • Datei: lib/pages/widgets/controls/es_gradient_button.dart
  • Properties: onPressed, label, icon, color, height, padding, borderRadius, isLoading, isDisabled
  • Verwendung: Standardisierte Add/Create Buttons mit Gradient-Style

4. EsCopyButton - Clipboard mit SnackBar

  • Datei: lib/pages/widgets/controls/es_copy_button.dart
  • Properties: value, label, color, icon, iconSize, padding, successMessage
  • Integration: Automatisches Clipboard.setData + showSuccessSnackBar

5. EsVerticalSeparator - Vertikaler Trenner

  • Datei: lib/pages/widgets/controls/es_vertical_separator.dart
  • Properties: height(30), width(1), color(grey.shade300)
  • Verwendung: Trenner zwischen Stat-Items in Rows

6. EsStatItem - Statistik-Anzeige

  • Datei: lib/pages/widgets/controls/es_stat_item.dart
  • Properties: icon, label, value, color, iconSize, labelFontSize, valueFontSize, padding
  • Verwendung: Icon + Label + Value in vertikaler Anordnung

7. EsSearchBar - Search + Add Button Pattern

  • Datei: lib/pages/widgets/controls/es_search_bar.dart
  • Properties:
  • Search: hintText, searchController, onSearchChanged
  • Add (optional): addButtonLabel, addButtonIcon, onAddPressed, primaryColor
  • Layout: horizontalPadding, bottomSpacing
  • Integration: Kombiniert EsSearchField + EsGradientButton

8. EsEmptyState - Empty State Widget (erweitert)

  • Datei: lib/pages/widgets/controls/es_empty_state.dart
  • Varianten:
  • Simple: EsEmptyState(icon, message, subtitle) - für Filter/Suche
  • Settings: EsEmptyState.settings(icon, title, description, actionLabel, onActionPressed, primaryColor, withCard) - für Settings mit Action Button
  • Features: Optionaler Card-Style, Action Button, anpassbare Icons

📊 Implementierungs-Status

✅ Phase 1: Widgets erstellt (KOMPLETT)

  • [x] EsSpacing
  • [x] SnackBar Helpers
  • [x] EsGradientButton
  • [x] EsCopyButton
  • [x] EsVerticalSeparator
  • [x] EsStatItem
  • [x] EsSearchBar
  • [x] EsEmptyState (erweitert)

🔄 Phase 2: Implementation in Settings Pages (IN PROGRESS)

🟢 EsSearchBar - Implementiert in:

  • [x] customer_lists_settings_page.dart

⏳ EsSearchBar - Noch zu implementieren in:

  • [ ] customer_categories_page.dart
  • [ ] article_categories_page.dart
  • [ ] package_sizes_page.dart
  • [ ] user_setting_page.dart
  • [ ] push_notification_settings_page.dart
  • [ ] article_document_type_settings_page.dart
  • [ ] country_settings_page.dart
  • [ ] language_settings_page.dart

⏳ EsCopyButton - Zu ersetzen in:

  • [ ] job_card_widget.dart (Job ID copy)
  • [ ] connector_card_widget.dart (Connector ID copy)
  • [ ] customer_category_card_widget.dart (Identifier copy)
  • [ ] article_category_card_widget.dart (Identifier copy)
  • [ ] package_size_card_widget.dart (Identifier copy)
  • [ ] user_card_widget.dart (User ID copy)
  • [ ] document_type_card_widget.dart (Key copy)
  • [ ] notification_group_card_widget.dart (ID copy)

⏳ EsStatItem + EsVerticalSeparator - Zu ersetzen in:

  • [ ] job_card_widget.dart (_buildStatItem method)
  • [ ] connector_card_widget.dart (Stats Row mit Separatoren)

⏳ EsEmptyState.settings - Zu ersetzen:

  • [ ] job_empty_state_widget.dart (komplettes Widget ersetzen)
  • [ ] connector_empty_state_widget.dart (komplettes Widget ersetzen)

⏳ SnackBar Helpers - Zu ersetzen in:

  • [ ] Alle Settings Pages (28+ ScaffoldMessenger.showSnackBar)
  • [ ] Erfolgs-Meldungen (grün)
  • [ ] Fehler-Meldungen (rot)

📈 Code-Reduktion (Geschätzt)

Bereits implementiert:

  • EsIconActionButton: ~400 Zeilen ✅
  • EsInfoBadge: ~80 Zeilen ✅
  • EsIconContainer: ~60 Zeilen ✅
  • EsSearchBar (1/9 Pages): ~60 Zeilen ✅
  • Zwischensumme: ~600 Zeilen

Noch zu implementieren:

  • EsSearchBar (8 Pages): ~480 Zeilen
  • EsCopyButton (8 Cards): ~120 Zeilen
  • EsStatItem + EsVerticalSeparator (2 Cards): ~130 Zeilen
  • EsEmptyState (2 Widgets): ~180 Zeilen
  • SnackBar Helpers (28+ Stellen): ~258 Zeilen
  • Zwischensumme: ~1.168 Zeilen

🎯 Gesamtpotential: ~1.768 Zeilen Code-Reduktion


🚀 Nächste Schritte

Priorität 1: EsSearchBar (8 verbleibende Pages)

Größter Impact, einfache Implementation

Priorität 2: EsCopyButton (8 Card Widgets)

Häufig verwendet, mit SnackBar Helper bereits teilweise abgedeckt

Priorität 3: EsStatItem + EsVerticalSeparator

Verbessert Konsistenz in Cards

Priorität 4: EsEmptyState.settings

Ersetzt 2 komplette Widget-Files

Priorität 5: SnackBar Helpers

Viele kleine Änderungen, große Gesamt-Wirkung


📝 Verwendungsbeispiele

EsSearchBar

EsSearchBar(
  hintText: 'Jobs suchen...',
  searchController: _searchController,
  onSearchChanged: (value) => setState(() {}),
  addButtonLabel: 'Hinzufügen',
  onAddPressed: () => _showAddDialog(),
  primaryColor: primaryColor,
)

EsCopyButton

EsCopyButton(
  value: job.id,
  label: 'Job-ID',
)

EsStatItem + EsVerticalSeparator

Row(
  children: [
    Expanded(
      child: EsStatItem(
        icon: CupertinoIcons.clock,
        label: 'Schedule',
        value: job.schedule.displayName,
      ),
    ),
    const EsVerticalSeparator(),
    Expanded(
      child: EsStatItem(
        icon: CupertinoIcons.checkmark_circle,
        label: 'Status',
        value: 'Aktiv',
        color: Colors.green,
      ),
    ),
  ],
)

EsEmptyState.settings

EsEmptyState.settings(
  icon: CupertinoIcons.time,
  title: 'Keine Jobs konfiguriert',
  description: 'Erstelle deinen ersten Job...',
  actionLabel: 'Job erstellen',
  onActionPressed: () => _showDialog(),
  primaryColor: primaryColor,
  withCard: true,
)

SnackBar Helpers

// Vorher:
ScaffoldMessenger.of(context).showSnackBar(
  SnackBar(
    content: Text('Erfolgreich gespeichert'),
    duration: const Duration(seconds: 2),
    backgroundColor: Colors.green.shade600,
  ),
);

// Nachher:
showSuccessSnackBar(context, 'Erfolgreich gespeichert');

🎨 Design-Standards

Alle Widgets folgen diesen Standards: - Spacing: EsSpacing Constants (4, 8, 12, 16, 20, 24, 32) - Border Radius: 8-16px (Buttons: 8-12, Cards: 16-20) - Colors: UsersBloc.primaryColor für primäre Actions - Shadows: BoxShadow mit alpha: 0.04-0.1 - Padding: Horizontal 24px für Page-Content - Animations: InkWell für touch feedback - Icons: CupertinoIcons bevorzugt - Fonts: Theme-based mit custom weights


⚠️ Breaking Changes

Keine - alle neuen Widgets sind additiv und kompatibel mit bestehendem Code.


📚 Dokumentation

Jedes Widget hat: - ✅ Dartdoc-Kommentare - ✅ Verwendungsbeispiele im Kommentar - ✅ Named Parameters mit Defaults - ✅ Assert für Validation - ✅ Konsistente API

Settings Pages - Gemeinsame Patterns & ES-Widgets

Identifizierte Patterns

1. ✅ Card Container Pattern (IMPLEMENTIERT: EsCard)

Verwendung in: - Alle *_card_widget.dart Dateien (10+) - job_card_widget.dart - connector_card_widget.dart - customer_category_card_widget.dart - article_category_card_widget.dart - package_size_card_widget.dart - user_card_widget.dart - legal_info_card_widget.dart - etc.

Gemeinsames Pattern:

Container(
  margin: const EdgeInsets.only(bottom: 16),
  decoration: BoxDecoration(
    color: Colors.white,
    borderRadius: BorderRadius.circular(16),
    border: Border.all(color: Colors.grey.shade200, width: 1),
    boxShadow: [BoxShadow(color: Colors.black.withValues(alpha: 0.04), blurRadius: 16, offset: Offset(0, 2))],
  ),
  child: ClipRRect(
    borderRadius: BorderRadius.circular(16),
    child: ...,
  ),
)

Ersetzt durch: EsCard Widget (bereits erstellt)


2. ✅ Icon Container Pattern (IMPLEMENTIERT: EsIconContainer)

Verwendung in: - job_card_widget.dart (Type Icon) - connector_card_widget.dart (Emoji Container) - legal_info_card_widget.dart (Icon Container) - customer_category_card_widget.dart (Category Icon) - article_category_card_widget.dart (Category Icon) - package_size_card_widget.dart (Icon) - user_card_widget.dart (User Avatar/Icon)

Gemeinsames Pattern:

Container(
  padding: const EdgeInsets.all(12-14),
  decoration: BoxDecoration(
    color: color.withValues(alpha: 0.1),
    borderRadius: BorderRadius.circular(12),
    border: Border.all(color: color.withValues(alpha: 0.2)),
  ),
  child: Icon(icon, color: color, size: 24),
)

Ersetzt durch: EsIconContainer / EsTextContainer (bereits erstellt)


3. ✅ Action Buttons Pattern (IMPLEMENTIERT: EsIconActionButton)

Verwendung in: - Alle Card Widgets mit Edit/Delete/Copy Aktionen - 30+ Button-Instanzen

Gemeinsames Pattern:

InkWell + Container mit:
- padding: EdgeInsets.all(10)
- background: color.withValues(alpha: 0.1)
- border: color.withValues(alpha: 0.3)
- Icon + Tooltip

Ersetzt durch: EsIconActionButton (bereits implementiert in 9 Dateien)


4. ✅ Info Badge Pattern (IMPLEMENTIERT: EsInfoBadge)

Verwendung in: - connector_card_widget.dart (Aktiv/Pausiert, Datentyp-Badges) - job_card_widget.dart (System Badge) - job_handler_card.dart (System Badge) - Potentiell in anderen Cards

Gemeinsames Pattern:

Container(
  padding: EdgeInsets.symmetric(horizontal: 8-10, vertical: 4-6),
  decoration: BoxDecoration(
    color: color.shade50,
    border: Border.all(color: color.shade200/300),
    borderRadius: BorderRadius.circular(6-8),
  ),
  child: Row([Icon, SizedBox, Text]),
)

Ersetzt durch: EsInfoBadge (bereits implementiert)


5. 🔄 Divider Pattern (TEILWEISE STANDARDISIERT)

Verwendung in: - Fast alle Card Widgets zwischen Sections - job_card_widget.dart - connector_card_widget.dart - etc.

Gemeinsames Pattern:

Divider(height: 1, thickness: 1, color: Colors.grey.shade200)
// oder
const Divider(height: 1, thickness: 1)

Empfehlung: EsDivider Widget mit Standard-Styling


6. 🔄 Vertical Separator Pattern

Verwendung in: - job_card_widget.dart (Stats Row) - connector_card_widget.dart (Stats Row) - Zwischen Stat-Items

Gemeinsames Pattern:

Container(
  width: 1,
  height: 40,
  color: Colors.grey.shade200,
)

Empfehlung: EsVerticalSeparator Widget


7. 🔄 Copy-To-Clipboard Button Pattern

Verwendung in: - job_card_widget.dart (Job ID kopieren) - connector_card_widget.dart (Connector ID kopieren) - customer_category_card_widget.dart (Identifier kopieren) - article_category_card_widget.dart (Identifier kopieren) - package_size_card_widget.dart (Identifier kopieren) - user_card_widget.dart (User ID kopieren)

Gemeinsames Pattern:

EsIconActionButton(
  icon: CupertinoIcons.doc_on_doc_fill,
  color: Colors.grey.shade700,
  onPressed: () {
    Clipboard.setData(ClipboardData(text: value));
    ScaffoldMessenger.of(context).showSnackBar(...);
  },
  tooltip: 'XX kopieren',
)

Empfehlung: EsCopyButton Widget mit automatischem Snackbar


8. 🔄 Translation Count Badge Pattern

Verwendung in: - customer_category_card_widget.dart - article_category_card_widget.dart - package_size_card_widget.dart

Gemeinsames Pattern:

Row(
  children: [
    Icon(CupertinoIcons.globe, size: 14, color: Colors.blue.shade600),
    SizedBox(width: 6),
    Text(
      '${item.languages.length} Übersetzungen',
      style: TextStyle(color: Colors.blue.shade600, fontSize: 12, fontWeight: FontWeight.w500),
    ),
  ],
)

Empfehlung: EsTranslationCountBadge Widget


9. 🔄 Stats Item Pattern

Verwendung in: - job_card_widget.dart (_buildStatItem) - connector_card_widget.dart (ConnectorStatWidget)

Gemeinsames Pattern:

Column(
  children: [
    Icon(icon, size: 18, color: color),
    SizedBox(height: 8),
    Text(label, style: small_grey),
    SizedBox(height: 4),
    Text(value, style: bold_dark),
  ],
)

Status: connector_card_widget verwendet bereits ConnectorStatWidget Empfehlung: Generisches EsStatItem Widget


10. 🔄 Empty State Pattern

Verwendung in: - customer_lists_settings_page.dart - Verschiedene Listen wenn leer

Gemeinsames Pattern:

Center(
  child: Column(
    children: [
      Icon(icon, size: 64, color: grey),
      SizedBox(height: 16),
      Text('Title', style: bold),
      SizedBox(height: 8),
      Text('Description', style: grey),
    ],
  ),
)

Empfehlung: EsEmptyState Widget


Implementierungs-Priorität

✅ Fertig Implementiert (Heute)

  1. EsIconActionButton - 30+ Verwendungen ersetzt
  2. EsInfoBadge - 4+ Verwendungen ersetzt
  3. EsIconContainer / EsTextContainer - 3+ Verwendungen ersetzt
  4. EsCard - Erstellt (Implementierung in Cards pending)

🔥 Hohe Priorität

  1. EsCopyButton - ~10 Verwendungen
  2. EsVerticalSeparator - ~6 Verwendungen
  3. EsStatItem - ~6 Verwendungen

📊 Mittlere Priorität

  1. EsTranslationCountBadge - ~4 Verwendungen
  2. EsEmptyState - ~3 Verwendungen
  3. EsDivider - Viele Verwendungen (niedriger Impact)

Geschätzte Code-Reduktion

  • EsIconActionButton: ~400 Zeilen gespart ✅
  • EsInfoBadge: ~80 Zeilen gespart ✅
  • EsIconContainer: ~60 Zeilen gespart ✅
  • EsCard: ~500 Zeilen potentiell (bei vollständiger Implementierung)
  • EsCopyButton: ~150 Zeilen potentiell
  • EsVerticalSeparator: ~30 Zeilen potentiell
  • EsStatItem: ~100 Zeilen potentiell

Gesamt bisher: ~540 Zeilen entfernt Potential gesamt: ~1.320 Zeilen


Nächste Schritte

  1. EsCard vollständig implementieren in allen Card Widgets
  2. EsCopyButton erstellen und überall ersetzen
  3. EsVerticalSeparator für Stats-Rows
  4. EsStatItem für konsistente Stat-Anzeigen
  5. Weitere Pattern bei Bedarf

Hinweise

  • Alle ES-Widgets sollten in lib/pages/widgets/controls/ liegen
  • Dokumentation für jedes Widget in docs/ES_*_README.md
  • Konsistente API: required + optional Parameter
  • Support für Theming (primaryColor, etc.)
  • Touch-Feedback wo nötig (InkWell)

Settings Pages - Erweiterte Pattern-Analyse

Neue identifizierte Patterns

11. 🔄 Search + Add Button Row Pattern (SEHR HÄUFIG)

Verwendung in: - customer_lists_settings_page.dart - customer_categories_page.dart - article_categories_page.dart - package_sizes_page.dart - user_setting_page.dart - push_notification_settings_page.dart - article_document_type_settings_page.dart - country_settings_page.dart - language_settings_page.dart

Gemeinsames Pattern:

Padding(
  padding: const EdgeInsets.symmetric(horizontal: 24),
  child: Row(
    children: [
      Expanded(
        child: EsSearchField(
          hintText: 'XX suchen...',
          controller: _searchController,
          onChanged: (value) => setState(() {}),
        ),
      ),
      const SizedBox(width: 12),
      Material/InkWell + Container mit Gradient-Button (Add)
    ],
  ),
),
const SizedBox(height: 12),

Empfehlung: EsSearchBar Widget mit optionalem Add-Button - Enthält: Search Field + optional Action Button - Auto-Padding (24px horizontal) - Auto-Spacing (12px unten)

Code-Reduktion: ~30 Zeilen × 9 Pages = ~270 Zeilen


12. 🔄 Add/Create Button Pattern (Gradient Button)

Verwendung in: - Fast alle List-Pages mit "Hinzufügen" Funktion - Immer mit Gradient-Style

Gemeinsames Pattern:

Material(
  color: Colors.transparent,
  child: InkWell(
    onTap: onTap,
    borderRadius: BorderRadius.circular(12),
    child: Container(
      height: 45,
      padding: EdgeInsets.symmetric(horizontal: 16),
      decoration: BoxDecoration(
        gradient: LinearGradient(
          colors: [primaryColor.withValues(alpha: 0.8), primaryColor],
          begin: Alignment.topLeft,
          end: Alignment.bottomRight,
        ),
        borderRadius: BorderRadius.circular(12),
      ),
      child: Row([Icon, SizedBox, Text]),
    ),
  ),
)

Empfehlung: EsGradientButton Widget

Code-Reduktion: ~20 Zeilen × 10+ Verwendungen = ~200 Zeilen


13. 🔄 Copy-to-Clipboard with SnackBar Pattern

Verwendung in: - job_card_widget.dart (Job ID) - connector_card_widget.dart (Connector ID) - customer_category_card_widget.dart (Identifier) - article_category_card_widget.dart (Identifier) - package_size_card_widget.dart (Identifier) - user_card_widget.dart (User ID) - document_type_card_widget.dart (Key) - notification_group_card_widget.dart (ID)

Gemeinsames Pattern:

EsIconActionButton(
  icon: CupertinoIcons.doc_on_doc_fill,
  color: Colors.grey.shade700,
  onPressed: () {
    Clipboard.setData(ClipboardData(text: value));
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(
        content: Text('XX kopiert: $value'),
        duration: const Duration(seconds: 2),
        backgroundColor: Colors.green.shade600,
      ),
    );
  },
  tooltip: 'XX kopieren',
)

Empfehlung: EsCopyButton Widget - Automatisches Clipboard + SnackBar - Parameter: value, label (für Snackbar)

Code-Reduktion: ~15 Zeilen × 8+ Verwendungen = ~120 Zeilen


14. 🔄 Empty State Pattern (2 Varianten)

Verwendung in: - job_empty_state_widget.dart (mit Card Container) - connector_empty_state_widget.dart (ohne Card) - customer_lists_settings_page.dart (inline)

Gemeinsames Pattern - Variante A (mit Card):

Center(
  child: Container(
    margin: EdgeInsets.all(32),
    padding: EdgeInsets.all(48),
    constraints: BoxConstraints(maxWidth: 500),
    decoration: BoxDecoration(
      color: Colors.white,
      borderRadius: BorderRadius.circular(20),
      border: Border.all(color: Colors.grey.shade200),
      boxShadow: [...],
    ),
    child: Column([
      CircleIcon,
      Title,
      Description,
      ActionButton,
    ]),
  ),
)

Gemeinsames Pattern - Variante B (ohne Card):

Center(
  child: Column([
    CircleIcon,
    Title,
    Description,
    ActionButton,
  ]),
)

Empfehlung: EsEmptyState Widget mit optional Card-Style

Code-Reduktion: ~60 Zeilen × 3+ Verwendungen = ~180 Zeilen


15. 🔄 Page Layout Pattern (UNIVERSAL)

Verwendung in: - ALLE Settings List Pages

Gemeinsames Pattern:

Column(
  crossAxisAlignment: CrossAxisAlignment.start,
  children: [
    // Search + Add Button Row
    Padding(
      padding: const EdgeInsets.symmetric(horizontal: 24),
      child: Row([EsSearchField, AddButton]),
    ),
    const SizedBox(height: 12),

    // List View
    Expanded(
      child: EsListView<T>(
        padding: EdgeInsets.symmetric(horizontal: 24),
        items: items,
        itemBuilder: (item) => CardWidget(item),
      ),
    ),
  ],
)

Empfehlung: EsSettingsListPage Widget oder Template

Code-Reduktion: ~25 Zeilen × 10 Pages = ~250 Zeilen


16. ✅ Progress Indicator Pattern (TEILWEISE STANDARDISIERT)

Verwendung in: - job_settings_page.dart (EsCircularProgressIndicator) - connector_settings_page.dart (EsCircularProgressIndicator) - job_execution_history_dialog.dart (EsCircularProgressIndicator) - import_running_dialog.dart (EsCircularProgressIndicator) - ABER: Viele noch mit Standard CircularProgressIndicator

Status: EsCircularProgressIndicator existiert bereits!

Aktion: Alle Standard-CircularProgressIndicator ersetzen

Code-Reduktion: Minimal (meist schon standardisiert)


17. 🔄 Success SnackBar Pattern

Verwendung in: - ~28 Stellen in Settings

Gemeinsames Pattern:

ScaffoldMessenger.of(context).showSnackBar(
  SnackBar(
    content: Text('Erfolgsmeldung'),
    duration: const Duration(seconds: 2),
    backgroundColor: Colors.green.shade600,
  ),
)

Empfehlung: Helper Function showSuccessSnackBar(context, message)

Code-Reduktion: ~6 Zeilen × 28 = ~168 Zeilen


18. 🔄 Error SnackBar Pattern

Verwendung in: - ~15 Stellen in Settings

Gemeinsames Pattern:

ScaffoldMessenger.of(context).showSnackBar(
  SnackBar(
    content: Text('Fehlermeldung'),
    duration: const Duration(seconds: 3),
    backgroundColor: Colors.red.shade600,
  ),
)

Empfehlung: Helper Function showErrorSnackBar(context, message)

Code-Reduktion: ~6 Zeilen × 15 = ~90 Zeilen


19. 🔄 Spacing Constants

Verwendung: ÜBERALL

Patterns: - const SizedBox(height: 12) - ~50+ Verwendungen - const SizedBox(height: 8) - ~40+ Verwendungen - const SizedBox(height: 4) - ~30+ Verwendungen - const SizedBox(width: 8) - ~30+ Verwendungen - const SizedBox(width: 12) - ~20+ Verwendungen - const EdgeInsets.symmetric(horizontal: 24) - ~15+ Verwendungen

Empfehlung: Spacing Constants Class

class EsSpacing {
  static const xs = 4.0;
  static const s = 8.0;
  static const m = 12.0;
  static const l = 16.0;
  static const xl = 20.0;
  static const xxl = 24.0;

  static const hGap4 = SizedBox(height: 4);
  static const hGap8 = SizedBox(height: 8);
  static const hGap12 = SizedBox(height: 12);
  static const hGap16 = SizedBox(height: 16);
  static const hGap20 = SizedBox(height: 20);
  static const hGap24 = SizedBox(height: 24);

  static const wGap4 = SizedBox(width: 4);
  static const wGap8 = SizedBox(width: 8);
  static const wGap12 = SizedBox(width: 12);
  static const wGap16 = SizedBox(width: 16);

  static const pagePadding = EdgeInsets.symmetric(horizontal: 24);
  static const cardPadding = EdgeInsets.all(20);
}

Code-Reduktion: Bessere Wartbarkeit, konsistente Abstände


20. 🔄 Dialog Pattern

Verwendung: - Alle Editor-Dialogs

Gemeinsames Pattern:

showLocalizedDialog(
  context: context,
  builder: (context) => EditorPage(...),
)

Status: Bereits gut standardisiert mit showLocalizedDialog


Prioritäts-Ranking (Aktualisiert)

🔥 HÖCHSTE Priorität (>200 Zeilen Reduktion)

  1. EsSearchBar (Search + Add Button Row) - ~270 Zeilen
  2. EsSettingsListPage Template - ~250 Zeilen
  3. EsGradientButton (Add/Create Buttons) - ~200 Zeilen
  4. EsEmptyState - ~180 Zeilen
  5. showSuccessSnackBar / showErrorSnackBar - ~258 Zeilen

🎯 HOHE Priorität (100-200 Zeilen)

  1. EsCopyButton - ~120 Zeilen (bereits in Analyse)
  2. EsVerticalSeparator - ~30 Zeilen
  3. EsStatItem - ~100 Zeilen

📊 MITTLERE Priorität (<100 Zeilen)

  1. EsTranslationCountBadge - ~60 Zeilen
  2. EsSpacing Constants - Wartbarkeit
  3. Replace CircularProgressIndicator - Minimal (meist done)

Geschätzte Gesamt-Code-Reduktion

Bereits implementiert (heute):

  • EsIconActionButton: ~400 Zeilen ✅
  • EsInfoBadge: ~80 Zeilen ✅
  • EsIconContainer: ~60 Zeilen ✅
  • Zwischensumme: ~540 Zeilen

Neue High-Priority Patterns:

  • EsSearchBar: ~270 Zeilen
  • EsSettingsListPage: ~250 Zeilen
  • EsGradientButton: ~200 Zeilen
  • EsEmptyState: ~180 Zeilen
  • SnackBar Helpers: ~258 Zeilen
  • EsCopyButton: ~120 Zeilen
  • EsVerticalSeparator: ~30 Zeilen
  • EsStatItem: ~100 Zeilen
  • EsTranslationCountBadge: ~60 Zeilen
  • Zwischensumme: ~1.468 Zeilen

🎯 GESAMTPOTENTIAL: ~2.008 Zeilen Code-Reduktion


Sofortige Implementierungs-Empfehlung

Reihenfolge: 1. ✅ EsCard (erstellt, muss implementiert werden) 2. EsSearchBar - Maximaler Impact, 9 Pages betroffen 3. EsGradientButton - Für Add-Buttons überall 4. SnackBar Helpers - Simple Utils, große Wirkung 5. EsCopyButton - Häufig verwendet 6. EsEmptyState - Saubere Empty-States 7. EsSettingsListPage - Ultimate Standardisierung


Zusätzliche Findings

Controller Pattern

  • Fast alle Pages: TextEditingController _searchController
  • Könnte in EsSearchBar integriert werden

Animation Pattern

  • Alle Listen: flutter_staggered_animations
  • Bereits in EsListView integriert ✅

Bloc Pattern

  • Alle Pages: context.read<UsersBloc>().primaryColor
  • Konsistent verwendet ✅

Dialog Pattern

  • Alle Dialogs: showLocalizedDialog
  • Bereits standardisiert ✅

Server-Side Filtering Implementation

Overview

Implemented server-side filtering for the Orders page to handle large datasets efficiently. Instead of loading all orders and filtering client-side, the application now sends filter criteria to Firestore and receives only the matching results.

Key Changes

1. Firebase Service (order_firebase_service.dart)

  • Added streamOrdersWithFilters(): Streams filtered orders from Firestore
  • Filters: customer number, order number, status, date range
  • Server-side filtering using Firestore where() clauses
  • Pagination with limit()
  • Sorted by orderDate descending

  • Added loadMoreOrders(): Loads next page of results

  • Cursor-based pagination using startAfterDocument()
  • Maintains current filter criteria
  • Prevents loading duplicate data

2. Orders Bloc (orders_bloc.dart)

  • Removed client-side filtering logic (_allOrders list)
  • Added server-side filter state (_currentFilters map)
  • New event handler _onApplyFilters():
  • Stores filter criteria
  • Triggers new filtered query via LoadOrders
  • Updated _onLoadOrders():
  • Calls streamOrdersWithFilters() instead of streamOrders()
  • Passes filter parameters to Firestore
  • Updated _onLoadMoreOrders():
  • Fetches last document snapshot
  • Loads more with same filters applied

3. Orders Events (order_events.dart)

  • Extended LoadOrders: Added filters parameter
  • Added ApplyFilters: New event to update filter criteria

4. Orders States (order_states.dart)

  • Extended OrdersLoaded:
  • hasMore: Indicates if more pages available
  • isLoadingMore: Shows loading state during pagination
  • totalCount: Number of loaded orders (not total in DB)

5. Filterable Table Widget (es_filterable_table.dart)

  • Added useServerSideFiltering flag: Disables client-side filtering
  • Added onFilterChanged callback: Notifies parent of filter changes
  • Implemented debouncing (500ms): Prevents excessive server queries
  • Updated _filteredData: Returns all data when server-side filtering enabled

6. Orders Page (orders_page.dart)

  • Enabled server-side filtering: useServerSideFiltering: true
  • Added onFilterChanged handler:
  • Maps UI filter keys to server field names
  • Extracts customer number from display format ("123 - Company Name")
  • Converts OrderStatus enum to string
  • Dispatches ApplyFilters event to bloc

Firestore Indexes

Required Composite Indexes

The following indexes are defined in firestore.indexes.json:

  1. Status + Date: orderStatus (ASC) + orderDate (DESC)
  2. Customer + Date: customerNumber (ASC) + orderDate (DESC)
  3. Order Number + Date: orderNumber (ASC) + orderDate (DESC)
  4. Status + Customer + Date: orderStatus (ASC) + customerNumber (ASC) + orderDate (DESC)

Deploying Indexes

# Deploy Firestore indexes to production
firebase deploy --only firestore:indexes

# Or use the specific deployment script
cd deployment
./deploy_flutter_firebase.sh

Note: Index creation can take several minutes for large collections. Monitor progress in Firebase Console → Firestore → Indexes.

Performance Benefits

Before (Client-Side Filtering)

  • ❌ Loads ALL orders from Firestore (1000s of documents)
  • ❌ Transfers large amount of data over network
  • ❌ Filters in-memory on client
  • ❌ Slow for large datasets
  • ❌ High bandwidth usage

After (Server-Side Filtering)

  • ✅ Loads only filtered results (100 documents per page)
  • ✅ Minimal data transfer
  • ✅ Filters using Firestore indexes (fast!)
  • ✅ Scales to millions of orders
  • ✅ Low bandwidth usage
  • ✅ Debounced filter inputs (500ms delay)

Usage Example

Filtering by Customer

  1. User types "123" in customer filter
  2. After 500ms debounce, onFilterChanged fires
  3. ApplyFilters event dispatched with {customer: "123"}
  4. Bloc calls streamOrdersWithFilters(customerFilter: "123")
  5. Firestore query: where('customerNumber', isGreaterThanOrEqualTo: "123").where('customerNumber', isLessThan: "124")
  6. Only matching orders returned

Filtering by Status

  1. User selects "pending" status
  2. onFilterChanged fires immediately (enum change)
  3. ApplyFilters event dispatched with {status: "pending"}
  4. Firestore query: where('orderStatus', isEqualTo: "pending")
  5. Only pending orders returned

Pagination

  1. User scrolls to bottom of table
  2. onLoadMore fires
  3. LoadMoreOrders event dispatched
  4. Bloc fetches last document snapshot
  5. Calls loadMoreOrders(lastDocument: snapshot, filters: {...})
  6. Firestore query: startAfterDocument(snapshot).limit(100)
  7. Next 100 orders appended to list

Testing

Manual Testing Steps

  1. Navigate to Orders page
  2. Verify initial 100 orders load
  3. Apply customer filter → Should see filtered results
  4. Apply status filter → Should see filtered results
  5. Scroll to bottom → Should load more (if hasMore = true)
  6. Clear filters → Should show all orders again

Performance Testing

  • Test with 1000+ orders
  • Measure query time in Firebase Console → Firestore → Usage
  • Verify indexes are being used (not showing "missing index" errors)

Future Improvements

  • [ ] Add date range filter UI
  • [ ] Show total count from server (requires separate count query)
  • [ ] Add filter chips to show active filters
  • [ ] Export filtered results only
  • [ ] Cache filter state in URL query parameters

Notes

  • Firestore has limit of 100 inequality filters per query
  • Customer number filter uses range query (startsWith simulation)
  • Order number filter requires exact match (parseInt)
  • Multiple filters combined with AND logic
  • Date filters use >= and <= operators

🏗️ EasySale ERP — Konzept: Deployment, Testing & Stabilität

Autor: Senior Dev / Product Manager Perspektive
Datum: Februar 2026
Status: Konzeptphase — keine Umsetzung
Ziel: Sicheres Ausrollen pro Kunde, stabile Weiterentwicklung, Schutz vor unkontrollierten Regressionen (insbesondere bei KI-gestützter Entwicklung)


📑 Inhaltsverzeichnis

  1. Ist-Analyse & Problemstellung
  2. Architektur-Übersicht: Single-Tenant pro Firebase-Projekt
  3. Environment-Strategie (Dev → Staging → Production)
  4. CI/CD Pipeline
  5. Test-Strategie (Pyramide)
  6. Regressions-Schutz bei KI-gestützter Entwicklung
  7. Deployment-Prozess pro Kunde
  8. Monitoring & Alerting
  9. Rollback-Strategie
  10. Zusammenfassung & Priorisierte Roadmap

1. Ist-Analyse & Problemstellung

Was existiert heute

Bereich Status Bewertung
Architektur Single-Tenant pro Firebase-Projekt ✅ Solide Datenisolierung
Deployment Manuelle Shell-Skripte (deploy_flutter_firebase.sh, multideploy) ⚠️ Fehleranfällig, nicht reproduzierbar
Onboarding Vollautomatisiertes Skript (814 Zeilen) ✅ Gut, aber ohne Validierung danach
CI/CD Nicht vorhanden 🔴 Kritisch
Tests 351 Tests (280 Unit, 43 Widget, 18 Integration, 10 Service) ⚠️ Gute Basis, aber 13 BLoCs ungetestet
Code-Analyse flutter_lints Standard — keine Custom-Rules ⚠️ Zu wenig
Feature Flags JSON-basiert pro Deployment ✅ Gut, aber nicht runtime-steuerbar
Monitoring Kein zentrales Monitoring 🔴 Kritisch
Rollback Manuell via Firebase Hosting Rollback ⚠️ Undokumentiert

Kernprobleme

  1. Kein Sicherheitsnetz: Ohne CI/CD und ausreichende Tests kann jedes Deployment (manuell oder KI-generiert) unbemerkt Features brechen.
  2. KI-Risiko: Wenn KI-Tools (Copilot, Cursor, etc.) Code ändern, gibt es keinen automatischen Gate-Keeper der prüft ob alles noch funktioniert.
  3. Multi-Customer-Deployment: Bei N Kunden muss jedes Release N-mal korrekt deployed werden — manuell ist das ein Albtraum.
  4. Keine Sichtbarkeit: Niemand weiß ob ein Kunden-Environment gesund ist oder Probleme hat.

2. Architektur-Übersicht: Single-Tenant pro Firebase-Projekt

Bestehende Architektur (beibehalten ✅)

┌─────────────────────────────────────────────────────────────────┐
│                    Git Repository (Monorepo)                     │
│                                                                  │
│  ┌──────────────┐  ┌──────────────┐  ┌────────────────────────┐ │
│  │  Flutter App  │  │  Cloud Funcs │  │ Security Rules + Config│ │
│  │  (lib/)       │  │  (functions/)│  │ (firestore/storage)    │ │
│  └──────┬───────┘  └──────┬───────┘  └──────────┬─────────────┘ │
│         │                  │                      │               │
└─────────┼──────────────────┼──────────────────────┼───────────────┘
          │                  │                      │
          ▼                  ▼                      ▼
   ┌──────────────────────────────────────────────────────────┐
   │              Firebase Projekt: Kunde A                    │
   │  ┌─────────┐  ┌──────────┐  ┌────────┐  ┌────────────┐  │
   │  │Hosting  │  │Firestore │  │Storage │  │Cloud Funcs │  │
   │  │(Web App)│  │(Daten)   │  │(Files) │  │(Backend)   │  │
   │  └─────────┘  └──────────┘  └────────┘  └────────────┘  │
   └──────────────────────────────────────────────────────────┘
   ┌──────────────────────────────────────────────────────────┐
   │              Firebase Projekt: Kunde B                    │
   │  ┌─────────┐  ┌──────────┐  ┌────────┐  ┌────────────┐  │
   │  │Hosting  │  │Firestore │  │Storage │  │Cloud Funcs │  │
   │  └─────────┘  └──────────┘  └────────┘  └────────────┘  │
   └──────────────────────────────────────────────────────────┘
   ┌──────────────────────────────────────────────────────────┐
   │              Firebase Projekt: Kunde C ...                │
   └──────────────────────────────────────────────────────────┘

Warum beibehalten: - Vollständige Datenisolierung auf Infrastrukturebene (nicht nur auf App-Ebene) - DSGVO-konform: Löschung eines Kunden = Löschung des gesamten Firebase-Projekts - Unabhängige Skalierung, unabhängige Backups - Kein Risiko von Cross-Tenant-Datenlecks

Neue Ergänzung: Zentrales Management-Projekt

┌─────────────────────────────────────────────────────┐
│           Firebase Projekt: "easysale-management"    │
│                                                      │
│  ┌───────────────┐  ┌────────────────────────────┐  │
│  │ Tenant-Registry│  │ Deployment-Metadata        │  │
│  │ - projectId    │  │ - lastDeployedVersion      │  │
│  │ - displayName  │  │ - lastDeployedAt           │  │
│  │ - environment  │  │ - deployedFeatureFlags     │  │
│  │ - featureFlags │  │ - healthCheckStatus        │  │
│  │ - createdAt    │  │ - lastHealthCheck          │  │
│  │ - activeUsers  │  │ - cloudFunctionVersions    │  │
│  └───────────────┘  └────────────────────────────┘  │
│                                                      │
│  ┌────────────────────────────────────────────────┐  │
│  │ Deployment-Logs (Audit Trail)                   │  │
│  │ - who deployed, when, what version, to which    │  │
│  │   project, success/failure                      │  │
│  └────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────┘

Zweck: Zentrale Übersicht über alle Kunden-Deployments, Versionen, Feature-Flags und Health-Status. Keine Kundendaten — nur Metadaten.


3. Environment-Strategie (Dev → Staging → Production)

Drei-Stufen-Modell

┌─────────────────┐     ┌─────────────────┐     ┌─────────────────────┐
│   DEVELOPMENT    │────▶│    STAGING       │────▶│    PRODUCTION       │
│                  │     │                  │     │                     │
│ • Lokaler Code   │     │ • Eigenes Firebase│    │ • Kunden-Projekte   │
│ • Emulators      │     │   Projekt        │     │ • Echte Daten       │
│ • Feature Branch │     │ • Testdaten      │     │ • Monitored         │
│ • Schnelles      │     │ • Vollständiges  │     │ • Rollback-fähig    │
│   Iterieren      │     │   E2E-Testing    │     │                     │
│ • KI-Entwicklung │     │ • Release Candidate│   │                     │
│   erlaubt        │     │ • Manuelles QA   │     │                     │
└─────────────────┘     └─────────────────┘     └─────────────────────┘
      │                        │                        │
      │ Automatisch            │ Manuelles              │ Automatisch
      │ bei Push               │ Approval               │ nach Approval
      │ auf Branch             │ erforderlich            │ (Multi-Deploy)
      ▼                        ▼                        ▼
  CI: Tests +              CI: Full Suite           CD: Sequenzielles
  Lint + Build             + Deploy auf              Deployment auf
                           Staging-Projekt           alle Kunden

Branch-Strategie

main (production-ready)
 ├── develop (integration branch)
 │    ├── feature/TICKET-123-neue-funktion
 │    ├── feature/TICKET-456-bugfix
 │    └── feature/TICKET-789-ki-generiert   ← KI-Änderungen gleich behandelt
 └── release/v2.5.0 (release candidate)
      └── hotfix/v2.5.1 (Notfall-Fix)

Regeln: - main ist immer deploybar — kein direkter Push möglich - develop ist der Integrations-Branch — Merge nur via Pull Request - Feature-Branches werden gegen develop gemerged - Release-Branch wird von develop abgezweigt → nach Staging deployed → nach QA in main gemerged - Hotfix direkt von main abzweigen → nach Fix in main UND develop zurückmergen


4. CI/CD Pipeline

Übersicht

┌─────────────────────────────────────────────────────────────────────┐
│                        GitHub Actions Pipeline                       │
│                                                                      │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌────────┐  ┌────────┐ │
│  │  Lint &   │─▶│  Unit    │─▶│ Widget + │─▶│ Build  │─▶│ Deploy │ │
│  │  Analyze  │  │  Tests   │  │ Integ.   │  │  Web   │  │        │ │
│  │           │  │          │  │  Tests   │  │        │  │        │ │
│  └──────────┘  └──────────┘  └──────────┘  └────────┘  └────────┘ │
│       │              │              │             │           │      │
│    ❌ Fail?       ❌ Fail?      ❌ Fail?      ❌ Fail?    ✅ Done │
│    → Block PR     → Block PR   → Block PR   → Block PR             │
└─────────────────────────────────────────────────────────────────────┘

Phase 1: Pull Request Pipeline (auf jeden PR gegen develop oder main)

# Konzept — nicht die finale Implementierung
name: PR Quality Gate

triggers: [pull_request → develop, main]

jobs:
  quality-gate:
    steps:
      # 1. Code-Analyse
      - flutter analyze --fatal-infos --fatal-warnings
      - dart format --set-exit-if-changed .

      # 2. Dependency-Check
      - flutter pub get
      - flutter pub outdated  # Nur Info, kein Blocker

      # 3. Unit Tests (schnell, <2 min)
      - flutter test test/unit/ --coverage

      # 4. Widget Tests
      - flutter test test/widget/

      # 5. Integration Tests
      - flutter test test/integration/

      # 6. Coverage-Gate
      - Mindestens 70% Gesamtcoverage (Ziel: 85%)
      - Neue Dateien müssen ≥80% Coverage haben

      # 7. Build-Validierung
      - flutter build web --no-tree-shake-icons

      # 8. Cloud Functions Tests (wenn functions/ geändert)
      - cd functions && npm test

Blockierend: Wenn einer dieser Steps fehlschlägt, kann der PR nicht gemerged werden.

Phase 2: Staging Deployment (auf Merge in release/*)

name: Deploy to Staging

triggers: [push → release/*]

jobs:
  deploy-staging:
    steps:
      - Komplette Quality Gate (wie oben)
      - flutter build web --dart-define=ENVIRONMENT=staging
      - firebase deploy --project easysale-staging --only hosting,firestore:rules,storage,functions
      - Smoke-Test gegen Staging URL
      - Slack/Teams Notification: "Release X.Y.Z auf Staging bereit für QA"

Phase 3: Production Deployment (auf Merge in main)

name: Deploy to Production

triggers: [push → main]
requires: manual-approval  # ← Manueller Gate-Keeper!

jobs:
  deploy-production:
    strategy: sequential  # Nicht parallel — Kunde für Kunde
    steps:
      - Lese Tenant-Registry aus Management-Projekt
      - Für jeden Kunden:
          - firebase deploy --project {kundenProjektId}
          - Health-Check nach Deploy
          - Bei Fehler: Automatischer Rollback für diesen Kunden
          - Logging in Deployment-Audit-Trail
      - Zusammenfassung an Team senden

5. Test-Strategie (Pyramide)

Test-Pyramide für EasySale

                    ╱╲
                   ╱  ╲
                  ╱ E2E ╲          ← 5-10 kritische User-Journeys
                 ╱________╲           (Login → Kunde erstellen → Bestellung → Logout)
                ╱          ╲
               ╱ Integration ╲     ← 30-50 Tests: BLoC-Service-Firestore Zusammenspiel
              ╱________________╲      (Order-Workflow, Import-Pipeline, Connector-Sync)
             ╱                  ╲
            ╱   Widget Tests     ╲  ← 100+ Tests: Jedes es_* Widget isoliert testen
           ╱______________________╲    (EsFilterableTable, EsTextField, EsActionButton...)
          ╱                        ╲
         ╱      Unit Tests          ╲ ← 500+ Tests: Models, BLoCs, Services, Helpers
        ╱____________________________╲   (Serialisierung, State Transitions, Berechnungen)

Bestehende Lücken & Priorisierung

🔴 Kritisch — Sofort schließen

Bereich Was fehlt Auswirkung
13 ungetestete BLoCs ImportBloc, ExportBloc, SystemSettingsBloc, NavBarBloc, etc. Regressions bei KI-Änderungen bleiben unentdeckt
Cloud Functions Tests Keine Tests für functions/ vorhanden Connector-/Job-Fehler erst in Production sichtbar
Security Rules Tests Firestore-Rules nicht getestet Berechtigungsfehler oder Datenlecks möglich

🟡 Wichtig — Innerhalb von 4 Wochen

Bereich Was fehlt Auswirkung
Widget Tests für es_* Komponenten 80+ Shared Widgets haben nur 43 Tests UI-Regressions unentdeckt
Firebase Service Tests 30 Services, nur 10 Tests Datenbank-Operationen nicht abgesichert
Feature Flag Tests Keine Tests dass Feature Flags korrekt greifen Features könnten bei falscher Flag-Config sichtbar sein

🟢 Nice-to-have — Kontinuierlich aufbauen

Bereich Was fehlt
E2E Browser Tests Volle User-Journeys mit Flutter Integration Tests
Performance Tests Ladezeiten, Firestore-Abfrage-Performance
Accessibility Tests Screen-Reader-Kompatibilität

Neue Test-Kategorien einführen

A) Contract Tests für Models (Schutz vor KI-Serialisierungsfehler)

Konzept: Jedes Model hat einen "Vertrag" mit Firestore
- Fixture-JSON (bereits vorhanden in test/fixtures/) wird geladen
- fromJson() → toJson() Roundtrip muss identisch sein
- Neue Felder müssen in Fixtures hinzugefügt werden
- Fehlende Felder müssen graceful handeln (nullable oder default)

Warum: Wenn KI ein Feld in einem Model umbenennt oder entfernt, bricht der Contract Test sofort.

B) Golden Tests für kritische UI-Komponenten

Konzept: Screenshot-Vergleich der wichtigsten Widgets
- Dashboard, Kundenliste, Bestellübersicht, Settings
- Bei jeder Änderung wird das gerenderte Widget mit dem "Golden" verglichen
- Abweichungen müssen manuell approved werden

Warum: KI ändert versehentlich Styling oder Layout — Golden Tests fangen das ab.

C) Firestore Security Rules Tests

Konzept: Mit @firebase/rules-unit-testing
- Test pro Collection: Was darf welche Rolle?
- Negative Tests: User darf NICHT Orders löschen (wenn Permission fehlt)
- Besonders wichtig: Catch-all Rule `/{document=**}` testen

Warum: Die bestehende Catch-all Rule allow read, write: if isAuthenticated() ist ein Sicherheitsrisiko. Tests erzwingen bewusstes Design.


6. Regressions-Schutz bei KI-gestützter Entwicklung

Das Problem

KI (Copilot/Cursor/ChatGPT) ändert Datei A
  → Datei B hängt von A ab
    → Feature C nutzt B
      → Feature C ist kaputt
        → Niemand merkt es bis der Kunde sich beschwert

Die Lösung: Mehrschichtiges Sicherheitsnetz

Schicht 1: Pre-Commit Hooks (lokal, sofort)

Vor jedem Commit automatisch ausgeführt:
├── flutter analyze              # Statische Analyse
├── dart format --set-exit-if-changed  # Formatierung
├── flutter test test/unit/      # Schnelle Unit Tests (<60 Sek)
└── Commit-Message Validierung   # Conventional Commits erzwingen

Effekt: Offensichtliche Fehler werden sofort gefangen, bevor sie ins Repository kommen.

Schicht 2: Pull Request Quality Gate (CI, 3-5 min)

Automatisch bei jedem PR:
├── Vollständige Test-Suite (Unit + Widget + Integration)
├── Code Coverage Report (Diff-basiert)
├── flutter analyze (strikte Regeln)
├── Dependency Audit
├── Build-Validierung
└── KI-Änderungs-Detektion (siehe unten)

Schicht 3: KI-Änderungs-Detektion (neu!)

Konzept: Ein CI-Step der spezifisch KI-generierte Risiken erkennt

Prüfungen:
1. MODEL-INTEGRITÄT
   - Wurde ein Model verändert? → Prüfe ob zugehöriger Contract Test existiert und passt
   - Wurde ein Feld umbenannt? → Prüfe ob alle Firestore-Referenzen aktualisiert wurden
   - Wurde ein Enum erweitert? → Prüfe ob alle switch-Statements updated sind

2. BLOC-STATE-KONSISTENZ  
   - Wurde ein BLoC Event/State hinzugefügt? → Prüfe ob Tests dafür existieren
   - Wurde ein BLoC Handler geändert? → Prüfe ob bestehende Tests noch passen

3. SERVICE-INTERFACE-STABILITÄT
   - Wurde eine Service-Methode umbenannt? → Prüfe ob alle Caller aktualisiert sind
   - Wurde eine Signatur geändert? → Prüfe ob Mocks aktualisiert sind

4. WIDGET-REGRESSIONS-CHECK
   - Wurde ein es_* Widget geändert? → Prüfe ob Golden Test aktualisiert wurde
   - Wurde ein Page-Widget geändert? → Prüfe ob Widget Test existiert

5. SECURITY-RULES-DIVERGENZ
   - Wurden firestore.rules geändert? → Security Rules Tests MÜSSEN passen
   - Wurde ein neues Collection-Feld hinzugefügt? → Rules müssen reviewed werden

Schicht 4: Mandatory Review Policy

Pull Request Review Regeln:
├── Mindestens 1 menschlicher Reviewer (kein Auto-Merge!)
├── KI-generierte PRs brauchen EXTRA Aufmerksamkeit:
│   ├── Reviewer muss explizit bestätigen: "Seiteneffekte geprüft"
│   ├── Reviewer muss testen: Betroffene Features manuell durchklicken
│   └── Bei Model/Service-Änderungen: Zweiter Reviewer erforderlich
└── Autor muss in PR-Beschreibung angeben:
    ├── Was wurde geändert?
    ├── Welche Features sind betroffen?
    ├── Wie wurde getestet?
    └── War KI an der Erstellung beteiligt? (Ja/Nein + welches Tool)

Schicht 5: Staging Validation (manuell + automatisiert)

Nach Deploy auf Staging:
├── Automatisierte Smoke Tests (Login, Dashboard laden, CRUD pro Entity)
├── Manueller Durchlauf der Checkliste:
│   ├── ☐ Dashboard lädt korrekt
│   ├── ☐ Kunde anlegen/bearbeiten/löschen
│   ├── ☐ Artikel anlegen/bearbeiten/löschen
│   ├── ☐ Bestellung durchführen (komplett)
│   ├── ☐ Connector-Sync testen
│   ├── ☐ Benachrichtigungen versenden
│   ├── ☐ Settings ändern und speichern
│   ├── ☐ Import/Export funktioniert
│   ├── ☐ Berechtigungen: User/Admin/SuperAdmin korrekt
│   └── ☐ Feature Flags: Deaktivierte Features unsichtbar
└── Freigabe durch Product Manager ODER Senior Dev

KI-Entwicklungs-Regeln (Team-Policy)

📋 KI-Nutzungs-Richtlinie für EasySale-Entwicklung

ERLAUBT:
✅ KI für neue Features in isolierten Feature-Branches
✅ KI für Unit Tests und Dokumentation
✅ KI für Refactoring MIT vollständiger Test-Abdeckung
✅ KI für Bug-Analyse und Lösungsvorschläge

EINGESCHRÄNKT:
⚠️ KI-Änderungen an Models → Nur mit Contract Test Update
⚠️ KI-Änderungen an Security Rules → Nur mit Security Test Update
⚠️ KI-Änderungen an Cloud Functions → Nur mit Function Test Update
⚠️ KI-Änderungen an >5 Dateien → Zweiter Reviewer Pflicht

VERBOTEN:
❌ KI-generierter Code direkt auf develop/main pushen
❌ KI Auto-Merge ohne menschlichen Review
❌ KI-Änderungen an Deployment-Skripten ohne manuellen Test
❌ KI-Löschung von Tests (auch wenn sie "unnötig" erscheinen)

7. Deployment-Prozess pro Kunde

Neukunde: Onboarding (bestehend, erweitert)

┌─────────────────────────────────────────────────────────────┐
│                  Kunden-Onboarding Pipeline                  │
│                                                              │
│  1. ┌──────────────────────────────────────────────────────┐│
│     │ Vorbereitung                                         ││
│     │ • Kundenname, gewünschte Features, Branding klären   ││
│     │ • Feature-Flag-Konfiguration erstellen               ││
│     │ • Firebase Projekt-ID festlegen                      ││
│     └──────────────────────────────────────────────────────┘│
│                           │                                  │
│  2. ┌──────────────────────────────────────────────────────┐│
│     │ Automatisiertes Onboarding (onboard_new_customer.sh) ││
│     │ • Firebase Projekt erstellen                         ││
│     │ • Firestore, Storage, Auth konfigurieren             ││
│     │ • Security Rules deployen                            ││
│     │ • Cloud Functions deployen                           ││
│     │ • Feature Flags schreiben                            ││
│     │ • Initial-User anlegen                               ││
│     │ • Flutter Web Build deployen                         ││
│     └──────────────────────────────────────────────────────┘│
│                           │                                  │
│  3. ┌──────────────────────────────────────────────────────┐│
│     │ Post-Onboarding Validierung  (NEU!)                  ││
│     │ • Health-Check: Hosting erreichbar?                  ││
│     │ • Login mit Test-User möglich?                       ││
│     │ • Feature Flags korrekt? (Dashboard sichtbar? etc.)  ││
│     │ • Cloud Functions erreichbar?                        ││
│     │ • Firestore Rules korrekt? (Test-Schreibzugriff)     ││
│     └──────────────────────────────────────────────────────┘│
│                           │                                  │
│  4. ┌──────────────────────────────────────────────────────┐│
│     │ Registrierung in Management-Projekt                  ││
│     │ • Tenant-Registry Eintrag                            ││
│     │ • Deployment-Metadata Initial-Eintrag                ││
│     │ • Team-Notification: "Neuer Kunde live"              ││
│     └──────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────┘

Bestandskunde: Update-Deployment

┌─────────────────────────────────────────────────────────────┐
│              Update-Deployment (CI/CD gesteuert)             │
│                                                              │
│  Trigger: Merge in main (nach Staging-Freigabe)             │
│                                                              │
│  Für JEDEN Kunden sequenziell:                              │
│                                                              │
│  ┌────────────────────────┐                                  │
│  │ 1. Pre-Deploy Check    │                                  │
│  │ • Kunde aktiv?         │──── Nein ──→ Überspringen       │
│  │ • Spezielle Version?   │──── Ja ───→ Skip (Pinned)      │
│  │ • Maintenance Window?  │──── Nein ──→ Warten/Skip       │
│  └────────┬───────────────┘                                  │
│           │ OK                                               │
│  ┌────────▼───────────────┐                                  │
│  │ 2. Deploy              │                                  │
│  │ • Hosting (Flutter Web)│                                  │
│  │ • Firestore Rules      │                                  │
│  │ • Storage Rules        │                                  │
│  │ • Cloud Functions      │                                  │
│  │ • Firestore Indexes    │                                  │
│  └────────┬───────────────┘                                  │
│           │                                                  │
│  ┌────────▼───────────────┐                                  │
│  │ 3. Post-Deploy Check   │                                  │
│  │ • HTTP 200 auf URL?    │──── Fail ──→ Auto-Rollback     │
│  │ • Login möglich?       │──── Fail ──→ Auto-Rollback     │
│  │ • API Health Endpoint? │──── Fail ──→ Auto-Rollback     │
│  └────────┬───────────────┘                                  │
│           │ OK                                               │
│  ┌────────▼───────────────┐                                  │
│  │ 4. Update Registry     │                                  │
│  │ • Version aktualisieren│                                  │
│  │ • Timestamp setzen     │                                  │
│  │ • Status: ✅ healthy   │                                  │
│  └────────────────────────┘                                  │
│                                                              │
│  Am Ende: Zusammenfassung                                    │
│  ├── ✅ Kunde A: v2.5.0 deployed                            │
│  ├── ✅ Kunde B: v2.5.0 deployed                            │
│  ├── ⏭️ Kunde C: Übersprungen (pinned auf v2.4.0)          │
│  └── 🔴 Kunde D: Rollback auf v2.4.0 (Health Check failed) │
└─────────────────────────────────────────────────────────────┘

Version-Pinning (Sonderfall)

Manche Kunden haben spezielle Anforderungen:
- Kunde möchte Update erst nach eigenem Test
- Kunde hat Customizing das noch nicht mit neuer Version kompatibel ist
- Regulatorische Gründe (Audit-Zeitraum)

Lösung: "pinned_version" Flag in Tenant-Registry
- Wenn gesetzt: Pipeline überspringt diesen Kunden
- Manuelles Deployment wenn Kunde bereit ist
- Automatische Erinnerung wenn Version >2 Releases hinter main

8. Monitoring & Alerting

Was überwacht werden muss

┌──────────────────────────────────────────────────────────────┐
│                    Monitoring Dashboard                        │
│                                                               │
│  ┌──────────────────┐  ┌──────────────────┐                  │
│  │ PRO KUNDE:       │  │ GLOBAL:          │                  │
│  │                   │  │                   │                  │
│  │ • Hosting Status  │  │ • CI/CD Status    │                  │
│  │   (UP/DOWN)       │  │ • Test Coverage   │                  │
│  │ • Letzte Version  │  │ • Open PRs        │                  │
│  │ • Letztes Deploy  │  │ • Pending Reviews │                  │
│  │ • Auth Errors     │  │ • Dependency      │                  │
│  │ • Firestore Reads │  │   Vulnerabilities │                  │
│  │ • Storage Usage   │  │ • Build Status    │                  │
│  │ • Function Errors │  │                   │                  │
│  │ • Active Users    │  │                   │                  │
│  └──────────────────┘  └──────────────────┘                  │
└──────────────────────────────────────────────────────────────┘

Alerting-Regeln

Alert Bedingung Kanal Priorität
Hosting Down HTTP != 200 für >2 Min Slack + SMS 🔴 P1
Cloud Function Error Error Rate >5% in 5 Min Slack 🔴 P1
Deploy Failed CI/CD Pipeline rot Slack 🟡 P2
Test Coverage Drop Coverage sinkt >3% in einem PR PR-Kommentar 🟡 P2
Version Drift Kunde >2 Versionen hinter main Slack (wöchentlich) 🟢 P3
Firestore Quota >80% Quota in einem Projekt Slack 🟡 P2
Ungetesteter Code Neue Datei ohne Test-Datei PR-Kommentar 🟢 P3

Umsetzung (Tooling)

Empfehlung: Firebase-native + Lightweight-Custom

Monitoring:
├── Firebase Console Alerts (pro Projekt) — kostenlos, built-in
│   ├── Cloud Function Errors
│   ├── Firestore Usage
│   └── Hosting Availability
├── UptimeRobot / BetterUptime (oder ähnlich)
│   └── HTTP-Check auf jede Kunden-URL alle 60 Sek
└── Custom Health-Check Cloud Function (in jedem Kunden-Projekt)
    ├── GET /healthcheck
    ├── Prüft: Firestore erreichbar, Auth aktiv, Version korrekt
    └── Schreibt Status in Management-Projekt

9. Rollback-Strategie

Hosting Rollback (schnellster Rollback)

Firebase Hosting unterstützt nativ Rollbacks:
- Jedes Deployment erstellt eine Version
- firebase hosting:channel:list zeigt alle Versionen
- firebase hosting:clone SOURCE:CHANNEL TARGET:live

Automatisierung im CI:
  → Bei fehlgeschlagenem Health-Check nach Deploy
  → Automatisch vorherige Hosting-Version wiederherstellen
  → Dauer: <30 Sekunden pro Kunde

Cloud Functions Rollback

- Functions werden immer zusammen deployed
- Rollback: Re-Deploy der vorherigen Git-Version
  → git checkout v2.4.0 && cd functions && firebase deploy --only functions
- Alternative: Functions versioniert halten (Tag-basiert)

Firestore Rules Rollback

⚠️ Firebase hat KEINEN nativen Rules-Rollback!

Lösung:
- Rules werden im Git versioniert (bereits der Fall ✅)
- Bei Rollback: Vorherige Rules-Version aus Git deployen
- Automatisierung: CI speichert Rules-Hash nach Deploy
  → Bei Rollback: Vorherige Version via Git-Tag deployen

Daten-Rollback (Worst Case)

Firestore hat Point-in-Time Recovery (PITR):
- Aktivieren für jedes Kunden-Projekt
- Erlaubt Wiederherstellung auf jeden Zeitpunkt der letzten 7 Tage
- Zusätzlich: Tägliche Firestore-Exports in Cloud Storage Bucket
  → Erlaubt Wiederherstellung auch nach 7 Tagen

10. Zusammenfassung & Priorisierte Roadmap

Phase 1: Fundament (Wochen 1-2) — MUST HAVE

┌─────────────────────────────────────────────────────┐
│  1.1  CI/CD Pipeline aufsetzen (GitHub Actions)     │
│       → PR Quality Gate mit Tests + Analyze + Build │
│                                                      │
│  1.2  Pre-Commit Hooks einrichten                    │
│       → flutter analyze + format + schnelle Tests    │
│                                                      │
│  1.3  Branch Protection Rules aktivieren             │
│       → Kein direkter Push auf main/develop          │
│       → PR Reviews mandatory                         │
│                                                      │
│  1.4  Analyse-Rules verschärfen                      │
│       → Custom lint rules für Projekt-Konventionen   │
└─────────────────────────────────────────────────────┘

Phase 2: Test-Abdeckung (Wochen 3-6) — MUST HAVE

┌─────────────────────────────────────────────────────┐
│  2.1  13 fehlende BLoC-Tests schreiben              │
│       → ImportBloc, ExportBloc, SystemSettingsBloc...│
│                                                      │
│  2.2  Contract Tests für alle Models                 │
│       → JSON Roundtrip-Tests als KI-Schutz           │
│                                                      │
│  2.3  Cloud Functions Tests aufbauen                 │
│       → Jest + Firebase Emulator für functions/      │
│                                                      │
│  2.4  Firestore Security Rules Tests                 │
│       → @firebase/rules-unit-testing                 │
│                                                      │
│  2.5  Widget Tests für Top-20 es_* Widgets           │
│       → Fokus auf geschäftskritische Komponenten     │
└─────────────────────────────────────────────────────┘

Phase 3: Deployment-Automatisierung (Wochen 7-8) — SHOULD HAVE

┌─────────────────────────────────────────────────────┐
│  3.1  CI/CD: Staging Auto-Deploy                     │
│       → Automatisch bei release/* Branch             │
│                                                      │
│  3.2  CI/CD: Production Multi-Deploy                 │
│       → Sequenziell über alle Kunden mit Health-Check│
│                                                      │
│  3.3  Management-Projekt aufsetzen                   │
│       → Tenant-Registry + Deployment-Logs            │
│                                                      │
│  3.4  Rollback-Automatisierung                       │
│       → Auto-Rollback bei fehlgeschlagenem Health    │
└─────────────────────────────────────────────────────┘

Phase 4: Monitoring & Polishing (Wochen 9-10) — NICE TO HAVE

┌─────────────────────────────────────────────────────┐
│  4.1  Health-Check Cloud Function pro Kunde          │
│       → /healthcheck Endpoint                        │
│                                                      │
│  4.2  Uptime Monitoring für alle Kunden-URLs         │
│       → UptimeRobot oder ähnlich                     │
│                                                      │
│  4.3  Alerting-Rules einrichten                      │
│       → Slack Integration für P1/P2 Alerts           │
│                                                      │
│  4.4  Golden Tests für kritische UI-Screens          │
│       → Dashboard, Kundenliste, Bestellübersicht     │
│                                                      │
│  4.5  E2E Tests (Flutter Integration Tests)          │
│       → 5 kritische User-Journeys automatisiert      │
└─────────────────────────────────────────────────────┘

Visualisierung: Gesamtbild

                     KI schreibt Code
                   Feature Branch (Git)
                     Pre-Commit Hooks
                   ┌───────┴───────┐
                   │ analyze       │
                   │ format        │
                   │ quick tests   │
                   └───────┬───────┘
                           │ ✅
                    Pull Request
               ┌───────────┴───────────┐
               │   CI Quality Gate     │
               │  • Full Test Suite    │
               │  • Coverage Check     │
               │  • Build Validation   │
               │  • KI-Change-Detection│
               └───────────┬───────────┘
                           │ ✅
                   Human Code Review
                  (mandatory, min 1 reviewer)
                           │ ✅
                   Merge → develop
                   Release Branch
               ┌───────────────────────┐
               │   Deploy → Staging    │
               │   Manuelles QA        │
               │   Staging Smoke Tests │
               └───────────┬───────────┘
                           │ ✅ Freigabe
                   Merge → main
               ┌───────────────────────┐
               │ Deploy → Production   │
               │ Pro Kunde sequenziell │
               │ + Health Check        │
               │ + Auto-Rollback       │
               └───────────┬───────────┘
               ┌───────────────────────┐
               │    Monitoring &       │
               │    Alerting           │
               │    (kontinuierlich)   │
               └───────────────────────┘

Fazit: Das Konzept setzt auf Defense in Depth — nicht eine einzelne Maßnahme schützt, sondern jede Schicht fängt ab, was die vorherige durchgelassen hat. Selbst wenn KI fehlerhaften Code produziert: Pre-Commit Hooks → CI Tests → Human Review → Staging QA → Health Checks → Monitoring bilden zusammen ein robustes Sicherheitsnetz.