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¶
- Shared Controls (shared-Paket)
- ERP-spezifische Controls
- Plugin-System (ClientConfig)
- Daten & Models
- Neuen Connector erstellen
- 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.
EsCountBadge¶
Kleines numerisches Badge (z.B. für Zähler an Icons).
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):
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¶
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¶
- Im ERP unter Einstellungen → Connectors einen neuen Connector erstellen
- Als Handler-Key den Dateinamen ohne
.jsangeben (z.B.handler_my_import) - Die
settings-Felder (apiKey, baseUrl etc.) konfigurieren - 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¶
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¶
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¶
- Konsistenz: Einheitlicher Button-Style in allen Settings
- Wartbarkeit: Änderungen müssen nur an einer Stelle gemacht werden
- Weniger Code: ~40 Zeilen pro Card Widget entfernt
- Features: Loading und Disabled States eingebaut
- 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 geeignetEsActionIconButton: 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)¶
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¶
- ✅
_buildDataTypeBadge→EsInfoBadge(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¶
- Konsistenz: Einheitliches Badge-Design in allen Settings
- Wartbarkeit: Änderungen nur an einer Stelle
- Flexibilität: Unterstützt MaterialColor und reguläre Color
- Weniger Code: ~35 Zeilen pro Badge-Implementierung gespart
- Type-Safe: Klare API mit benannten Parametern
Unterschied zu anderen Badge-Widgets¶
EsInfoBadge: Icon + Text für Info-AnzeigeEsStatusBadge: Nur Icon, rund mit Gradient/ShadowEsCountBadge: 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¶
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:
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)¶
- EsIconActionButton - 30+ Verwendungen ersetzt
- EsInfoBadge - 4+ Verwendungen ersetzt
- EsIconContainer / EsTextContainer - 3+ Verwendungen ersetzt
- EsCard - Erstellt (Implementierung in Cards pending)
🔥 Hohe Priorität¶
- EsCopyButton - ~10 Verwendungen
- EsVerticalSeparator - ~6 Verwendungen
- EsStatItem - ~6 Verwendungen
📊 Mittlere Priorität¶
- EsTranslationCountBadge - ~4 Verwendungen
- EsEmptyState - ~3 Verwendungen
- 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¶
- EsCard vollständig implementieren in allen Card Widgets
- EsCopyButton erstellen und überall ersetzen
- EsVerticalSeparator für Stats-Rows
- EsStatItem für konsistente Stat-Anzeigen
- 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):
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:
Status: Bereits gut standardisiert mit showLocalizedDialog
Prioritäts-Ranking (Aktualisiert)¶
🔥 HÖCHSTE Priorität (>200 Zeilen Reduktion)¶
- EsSearchBar (Search + Add Button Row) - ~270 Zeilen
- EsSettingsListPage Template - ~250 Zeilen
- EsGradientButton (Add/Create Buttons) - ~200 Zeilen
- EsEmptyState - ~180 Zeilen
- showSuccessSnackBar / showErrorSnackBar - ~258 Zeilen
🎯 HOHE Priorität (100-200 Zeilen)¶
- EsCopyButton - ~120 Zeilen (bereits in Analyse)
- EsVerticalSeparator - ~30 Zeilen
- EsStatItem - ~100 Zeilen
📊 MITTLERE Priorität (<100 Zeilen)¶
- EsTranslationCountBadge - ~60 Zeilen
- EsSpacing Constants - Wartbarkeit
- 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
orderDatedescending -
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 (
_allOrderslist) - Added server-side filter state (
_currentFiltersmap) - New event handler
_onApplyFilters(): - Stores filter criteria
- Triggers new filtered query via
LoadOrders - Updated
_onLoadOrders(): - Calls
streamOrdersWithFilters()instead ofstreamOrders() - 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: Addedfiltersparameter - Added
ApplyFilters: New event to update filter criteria
4. Orders States (order_states.dart)¶
- Extended
OrdersLoaded: hasMore: Indicates if more pages availableisLoadingMore: Shows loading state during paginationtotalCount: Number of loaded orders (not total in DB)
5. Filterable Table Widget (es_filterable_table.dart)¶
- Added
useServerSideFilteringflag: Disables client-side filtering - Added
onFilterChangedcallback: 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
onFilterChangedhandler: - Maps UI filter keys to server field names
- Extracts customer number from display format ("123 - Company Name")
- Converts OrderStatus enum to string
- Dispatches
ApplyFiltersevent to bloc
Firestore Indexes¶
Required Composite Indexes¶
The following indexes are defined in firestore.indexes.json:
- Status + Date:
orderStatus(ASC) +orderDate(DESC) - Customer + Date:
customerNumber(ASC) +orderDate(DESC) - Order Number + Date:
orderNumber(ASC) +orderDate(DESC) - 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¶
- User types "123" in customer filter
- After 500ms debounce,
onFilterChangedfires ApplyFiltersevent dispatched with{customer: "123"}- Bloc calls
streamOrdersWithFilters(customerFilter: "123") - Firestore query:
where('customerNumber', isGreaterThanOrEqualTo: "123").where('customerNumber', isLessThan: "124") - Only matching orders returned
Filtering by Status¶
- User selects "pending" status
onFilterChangedfires immediately (enum change)ApplyFiltersevent dispatched with{status: "pending"}- Firestore query:
where('orderStatus', isEqualTo: "pending") - Only pending orders returned
Pagination¶
- User scrolls to bottom of table
onLoadMorefiresLoadMoreOrdersevent dispatched- Bloc fetches last document snapshot
- Calls
loadMoreOrders(lastDocument: snapshot, filters: {...}) - Firestore query:
startAfterDocument(snapshot).limit(100) - Next 100 orders appended to list
Testing¶
Manual Testing Steps¶
- Navigate to Orders page
- Verify initial 100 orders load
- Apply customer filter → Should see filtered results
- Apply status filter → Should see filtered results
- Scroll to bottom → Should load more (if hasMore = true)
- 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¶
- Ist-Analyse & Problemstellung
- Architektur-Übersicht: Single-Tenant pro Firebase-Projekt
- Environment-Strategie (Dev → Staging → Production)
- CI/CD Pipeline
- Test-Strategie (Pyramide)
- Regressions-Schutz bei KI-gestützter Entwicklung
- Deployment-Prozess pro Kunde
- Monitoring & Alerting
- Rollback-Strategie
- 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¶
- Kein Sicherheitsnetz: Ohne CI/CD und ausreichende Tests kann jedes Deployment (manuell oder KI-generiert) unbemerkt Features brechen.
- KI-Risiko: Wenn KI-Tools (Copilot, Cursor, etc.) Code ändern, gibt es keinen automatischen Gate-Keeper der prüft ob alles noch funktioniert.
- Multi-Customer-Deployment: Bei N Kunden muss jedes Release N-mal korrekt deployed werden — manuell ist das ein Albtraum.
- 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.