Client Override System - Vollständiger Leitfaden¶
📦 Update März 2026 — Multi-Repo-Architektur
Client-Projekte liegen jetzt in eigenen Git-Repos (
easysale-client-<slug>) neben dem Core-Repo, nicht mehr unterclients/im Core. Die Dependency auf Core erfolgt per Git-Dependency inpubspec.yaml:Für lokale Entwicklung:dependencies: erp_system: git: url: git@github.com:Tech-Schuppen/easysale-core.git path: core/apps/erp_system ref: mainpubspec_overrides.yamlmitpath:verwenden. Siehe README im jeweiligen Client-Repo für Details.
📋 Inhaltsverzeichnis¶
Übersicht¶
Das Client Override System ermöglicht es, kundespezifische Anpassungen am easySale ERP/Shop-System vorzunehmen, ohne den Basis-Code zu verändern.
✅ Was kann überschrieben werden?¶
- Models - Erweitern mit Custom Fields via Extensions
- BLoCs - Eigene Business Logic durch Vererbung
- Services - Zusätzliche Funktionalität
- Pages/Widgets - Custom UI-Komponenten
- Configuration - Feature-Flags, Themes, Settings
🎯 Vorteile¶
- ✅ Keine Breaking Changes - Apps bleiben unverändert
- ✅ Evolutionärer Ansatz - Schrittweise Migration möglich
- ✅ Team-freundlich - Paralleles Arbeiten ohne Konflikte
- ✅ Type-Safe - Volle Dart-Typsicherheit
- ✅ Testbar - Jeder Client isoliert testbar
Architektur-Prinzipien¶
1. Single-Tenant Multi-Instance¶
Jeder Kunde bekommt: - Eigene Firebase-Instanz - Eigenen Deployment - Eigenes Git-Repository mit Anpassungen
~/Development/
├── easysale-core/ ← Core-Repo (read-only für Clients)
│ ├── core/apps/erp_system/ ← ERP Base App
│ ├── core/apps/shop_system/ ← Shop Base App
│ └── core/shared/ ← Shared Code mit Extension-Support
│
├── easysale-client-pharma/ ← Kundenrepo 1 (eigenes Git-Repo)
│ ├── erp/ ← Flutter-App mit Git-Dependency auf Core
│ └── firebase/ ← Firebase-Konfiguration
│
└── easysale-client-baumarkt/ ← Kundenrepo 2 (eigenes Git-Repo)
└── ...
2. Composition über Inheritance¶
Statt Basis-Code zu ändern:
- CustomDataMixin für flexible Model-Erweiterungen
- ClientConfig für Feature-Steuerung
- Extensions für typsichere Custom-Fields
3. Configuration-Driven¶
class DemoPharmaConfig implements ClientConfig {
@override
Map<String, bool> get features => {
'pharma_tracking': true,
'temperature_monitoring': true,
// ...
};
}
Komponenten¶
1. CustomDataMixin¶
Zweck: Fügt customData-Feld zu jedem Model hinzu
Location: packages/shared/lib/models/mixins/custom_data_mixin.dart
// Mixin zur Klasse hinzufügen
class Article extends EntityBase with CustomDataMixin {
@override
final Map<String, dynamic>? customData;
// ...
}
Methoden:
- getCustom<T>(String key) - Typsicherer Zugriff
- hasCustom(String key) - Prüft Existenz
- mergeCustomData(Map data) - Daten zusammenführen
- deepMergeCustomData(Map data) - Verschachtelte Daten mergen
- removeCustomData(String key) - Daten entfernen
2. ClientConfig¶
Zweck: Zentrale Konfiguration für Client-Anpassungen
Location: packages/shared/lib/config/client_config.dart
abstract class ClientConfig {
String get clientId;
String get clientName;
String get logoPath;
Map<String, bool> get features;
Map<String, dynamic> get themeOverrides;
Map<String, dynamic> get customConfig;
// Helper
bool isFeatureEnabled(String feature);
T? getConfig<T>(String key);
T? getConfigPath<T>(String path, {T? defaultValue});
}
3. Extensions (Typsichere Custom Fields)¶
Zweck: Typsichere Wrapper für customData
Beispiel: packages/shared/lib/extensions/pharma/article_pharma_extension.dart
class PharmaArticleData {
final String chargennummer;
final DateTime verfallsdatum;
final bool rezeptpflichtig;
// ...
}
extension ArticlePharmaExtension on Article {
PharmaArticleData? get pharmaData { /* ... */ }
Article withPharmaData(PharmaArticleData data) { /* ... */ }
bool get isExpired { /* ... */ }
bool isExpiringSoon(int days) { /* ... */ }
}
Verwendung:
// Pharma-Daten hinzufügen
final article = Article(/* ... */)
.withPharmaData(PharmaArticleData(
chargennummer: 'PH-2024-12345',
verfallsdatum: DateTime(2027, 12, 31),
));
// Typsicherer Zugriff
if (article.hasPharmaData) {
print(article.chargennummer);
if (article.isExpired) { /* ... */ }
}
Verwendung¶
Schritt 1: Client-Repo erstellen¶
# Automatisch per Onboarding-Script:
./onboarding/create_client.sh
# Ergebnis: Eigenes Repo easysale-client-demo_pharma/
easysale-client-demo_pharma/
├── erp/
│ ├── pubspec.yaml
│ ├── lib/
│ │ ├── main.dart
│ │ ├── config/
│ │ │ └── demo_pharma_config.dart
│ │ ├── blocs/
│ │ ├── services/
│ │ └── pages/
│ └── assets/
├── firebase/
│ ├── .firebaserc
│ └── firebase.json
└── .code-workspace ← Multi-Root Workspace (Core read-only)
Schritt 2: pubspec.yaml konfigurieren¶
name: demo_pharma_erp
dependencies:
flutter:
sdk: flutter
# Abhängigkeit vom Core-ERP (per Git-Dependency)
erp_system:
git:
url: git@github.com:Tech-Schuppen/easysale-core.git
path: core/apps/erp_system
ref: main # oder v1, v2, etc.
# Shared Package
shared:
git:
url: git@github.com:Tech-Schuppen/easysale-core.git
path: core/shared
ref: main
flutter_bloc: ^8.1.6
get_it: ^7.7.0
Lokale Entwicklung: Erstelle
pubspec_overrides.yaml(gitignored):
Schritt 3: ClientConfig implementieren¶
class DemoPharmaConfig implements ClientConfig {
@override
String get clientId => 'demo_pharma';
@override
Map<String, bool> get features => {
'pharma_tracking': true,
'temperature_monitoring': true,
};
@override
Map<String, dynamic> get customConfig => {
'pharma': {
'expiryWarningDays': {
'critical': 30,
'warning': 90,
},
},
};
}
Schritt 4: Extension für Custom Fields¶
// Typsichere Klasse
class PharmaArticleData {
final String chargennummer;
final DateTime verfallsdatum;
// ...
}
// Extension auf Model
extension ArticlePharmaExtension on Article {
PharmaArticleData? get pharmaData { /* ... */ }
Article withPharmaData(PharmaArticleData data) { /* ... */ }
}
Schritt 5: BLoC überschreiben (Optional)¶
class PharmaArticlesBloc extends ArticlesBloc {
PharmaArticlesBloc({
required IArticleRepository articleFirebaseService,
required ICustomerRepository customerFirebaseService,
required ClientConfig config,
}) : super(/* ... */) {
// Registriere zusätzliche Events
on<FilterByExpiryDate>(_onFilterByExpiryDate);
on<LoadExpiringArticles>(_onLoadExpiringArticles);
}
// Pharma-spezifische Event-Handler
}
Schritt 6: Service erstellen (Optional)¶
class PharmaArticleService {
final ClientConfig _config;
ArticleValidationResult validateArticleForSale(Article article) {
// Pharma-spezifische Validierung
}
ExpiryReport generateExpiryReport(List<Article> articles) {
// Ablaufdatum-Report
}
}
Schritt 7: Custom Page/Widget (Optional)¶
class PharmaArticleDetailPage extends StatelessWidget {
final Article article;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(article.name)),
body: Column(
children: [
// Standard Info
_buildStandardInfo(),
// Pharma-spezifische Info
if (article.hasPharmaData)
_buildPharmaInfo(),
],
),
);
}
}
Schritt 8: DI Setup in main.dart¶
void main() {
WidgetsFlutterBinding.ensureInitialized();
// Config registrieren
GetIt.instance.registerLazySingleton<ClientConfig>(
() => DemoPharmaConfig(),
);
// Services registrieren
GetIt.instance.registerLazySingleton<PharmaArticleService>(
() => PharmaArticleService(config: GetIt.instance<ClientConfig>()),
);
// BLoCs überschreiben
if (GetIt.instance.isRegistered<ArticlesBloc>()) {
GetIt.instance.unregister<ArticlesBloc>();
}
GetIt.instance.registerLazySingleton<ArticlesBloc>(
() => PharmaArticlesBloc(/* ... */),
);
// Starte Basis-App
erp_main.main();
}
Beispiele¶
Beispiel 1: Artikel mit Pharma-Daten¶
// 1. Artikel erstellen
final article = Article(
number: 'MED-001',
name: 'Aspirin 500mg',
isAvailable: true,
);
// 2. Pharma-Daten hinzufügen
final pharmaData = PharmaArticleData(
chargennummer: 'PH-2024-12345',
verfallsdatum: DateTime(2027, 12, 31),
rezeptpflichtig: false,
wirkstoff: 'Acetylsalicylsäure',
dosierung: '500mg',
);
final articleWithPharma = article.withPharmaData(pharmaData);
// 3. Typsicherer Zugriff
if (articleWithPharma.hasPharmaData) {
print('Charge: ${articleWithPharma.chargennummer}');
print('Verfallsdatum: ${articleWithPharma.verfallsdatum}');
if (articleWithPharma.isExpired) {
print('⛔ Abgelaufen!');
} else if (articleWithPharma.isExpiringSoon(90)) {
print('⚠️ Läuft in ${articleWithPharma.daysUntilExpiry} Tagen ab');
}
}
// 4. Speichern (customData wird automatisch serialisiert)
await repository.saveArticle(articleWithPharma);
Beispiel 2: Config-gesteuerte Features¶
final config = GetIt.instance<ClientConfig>();
// Feature-Checks
if (config.isFeatureEnabled('pharma_tracking')) {
// Zeige Chargen-Verwaltung
}
if (config.isFeatureEnabled('temperature_monitoring')) {
// Zeige Temperatur-Überwachung
}
// Config-Werte abrufen
final warningDays = config.getConfigPath<int>(
'pharma.expiryWarningDays.critical',
defaultValue: 30,
);
// Theme-Overrides
final primaryColor = config.getTheme<int>('primaryColor');
Beispiel 3: BLoC Events¶
final articlesBloc = GetIt.instance<ArticlesBloc>() as PharmaArticlesBloc;
// Standard Events funktionieren weiterhin
articlesBloc.add(LoadArticles());
// Pharma-spezifische Events
articlesBloc.add(LoadExpiringArticles());
articlesBloc.add(FilterByBatchNumber('PH-2024'));
articlesBloc.add(FilterByExpiryDate(
showExpired: true,
showExpiringSoon: true,
));
Beispiel 4: Service-Nutzung¶
final pharmaService = GetIt.instance<PharmaArticleService>();
// Validierung
final validation = pharmaService.validateArticleForSale(article);
if (!validation.isValid) {
showDialog(/* ... */);
}
// Report generieren
final report = pharmaService.generateExpiryReport(articles);
print(report.summary);
print('Abgelaufen: ${report.expiredArticles.length}');
print('Kritisch: ${report.criticalArticles.length}');
Best Practices¶
✅ DO's¶
-
Extensions für Type-Safety nutzen
-
ClientConfig für Features
-
BLoC Inheritance sparsam
-
Services kapseln Custom Logic
❌ DON'Ts¶
- Keine Basis-Code-Änderungen
- ❌ Core-Code NICHT ändern (read-only im Multi-Root Workspace)
-
✅ Overrides im eigenen Client-Repo erstellen
-
Kein Client-Check in Shared Code
-
Keine Deep customData-Strukturen
Häufige Fragen¶
Q: Muss ich für jeden Kunden alle Komponenten überschreiben?¶
A: Nein! Das ist der Vorteil des Systems. Überschreibe nur was nötig ist: - Nur custom fields? → Nur Extension - Nur UI anpassen? → Nur Pages/Widgets - Neue Business Logic? → BLoC + Service
Q: Kann ich mehrere Clients gleichzeitig entwickeln?¶
A: Ja! Jeder Client ist komplett isoliert in einem eigenen Git-Repo.
~/Development/
├── easysale-client-pharma/erp/ ← Team 1 arbeitet hier
├── easysale-client-baumarkt/erp/ ← Team 2 arbeitet hier
└── easysale-client-textil/erp/ ← Team 3 arbeitet hier
Q: Was passiert mit Bug-Fixes im Basis-System?¶
A: Sie werden automatisch an alle Clients vererbt, da diese die Basis-App als Git-Dependency haben:
dependencies:
erp_system:
git:
url: git@github.com:Tech-Schuppen/easysale-core.git
path: core/apps/erp_system
ref: main # ← Bug-Fix im Core → flutter pub upgrade holt ihn
Bei konfigurierten Client-Repos wird ein Core-Push automatisch ein Re-Deployment getriggert.
Q: Wie teste ich Client-spezifische Features?¶
A: Standard Flutter-Tests, isoliert pro Client:
// clients/demo_pharma/erp/test/pharma_service_test.dart
void main() {
test('validateArticleForSale rejects expired articles', () {
final service = PharmaArticleService(config: MockConfig());
final article = Article(/* ... */).withPharmaData(/* expired */);
final result = service.validateArticleForSale(article);
expect(result.isValid, false);
expect(result.errors, contains('Artikel ist abgelaufen'));
});
}
Q: Kann ich später zum Core/Template-Ansatz wechseln?¶
A: Ja! Das ist der evolutionäre Ansatz:
Phase 1 (jetzt): Override-System
- Apps bleiben unverändert
- Client-Overrides in clients/
Phase 2 (später, optional): Core/Template-Extraktion
- Gemeinsamen Code nach packages/erp_core/
- Templates nach templates/erp_template/
- Clients bleiben kompatibel
Q: Wie deploye ich einen Client?¶
A: Jeder Client ist standalone in seinem eigenen Repo:
cd easysale-client-demo_pharma/erp
flutter build web
cd ../firebase
firebase deploy --project demo-pharma-prod
Oder automatisch per GitHub Actions (empfohlen):
Push auf main im Client-Repo triggert den auto-deploy.yml Workflow.
Q: Kann ich customData auch für andere Zwecke nutzen?¶
A: Ja! Beispiele: - Baumarkt: Maße, Gewicht, Regalplatz - Mode: Größentabellen, Materialien, Pflegehinweise - Lebensmittel: Allergene, Nährwerte, Bio-Siegel
// Extension für Baumarkt
extension ArticleBaumarktExtension on Article {
BaumarktData? get baumarktData { /* ... */ }
}
class BaumarktData {
final double laenge;
final double breite;
final double hoehe;
final double gewicht;
final String regalplatz;
}
Migration Guide (für bestehende Kunden)¶
Falls Sie bereits einen Kunden haben, der direkt in apps/ entwickelt wurde:
Schritt 1: Client-Code extrahieren¶
# Erstelle Client-Struktur
mkdir -p clients/KUNDE/erp/lib
# Kopiere client-spezifischen Code
cp -r apps/erp_system/lib/custom_kunde clients/KUNDE/erp/lib/
Schritt 2: pubspec.yaml erstellen¶
dependencies:
erp_system:
git:
url: git@github.com:Tech-Schuppen/easysale-core.git
path: core/apps/erp_system
ref: main
Schritt 3: main.dart anpassen¶
Siehe Beispiel oben (DI Setup).
Schritt 4: CustomData Migration¶
// Vorher: Direkt in Model (Breaking Change!)
class Article extends EntityBase {
final String? pharmaCharge; // ← Alle Kunden müssen das Feld haben
}
// Nachher: Via customData (Non-Breaking!)
class Article extends EntityBase with CustomDataMixin {
@override
final Map<String, dynamic>? customData;
}
// Extension nur für Pharma-Kunde
extension ArticlePharmaExtension on Article {
String? get pharmaCharge => getCustom<String>('pharma.charge');
}
Zusammenfassung¶
Das Client Override System ermöglicht:
✅ Kundespezifische Anpassungen ohne Basis-Code zu ändern
✅ Evolutionärer Ansatz - Start mit Overrides, später optional Core-Extraktion
✅ Type-Safe - Extensions für typsichere Custom Fields
✅ Config-Driven - Features via ClientConfig steuern
✅ Testbar - Jeder Client isoliert testbar
✅ Team-freundlich - Paralleles Arbeiten ohne Konflikte
Nächste Schritte: 1. Demo Pharma Client testen 2. Eigenen Client nach diesem Muster erstellen 3. Bei Fragen: Dokumentation aktualisieren
Erstellt: 2024
Version: 1.0
Autor: easySale Team