Zum Inhalt

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 unter clients/ im Core. Die Dependency auf Core erfolgt per Git-Dependency in pubspec.yaml:

dependencies:
  erp_system:
    git:
      url: git@github.com:Tech-Schuppen/easysale-core.git
      path: core/apps/erp_system
      ref: main
Für lokale Entwicklung: pubspec_overrides.yaml mit path: verwenden. Siehe README im jeweiligen Client-Repo für Details.

📋 Inhaltsverzeichnis

  1. Übersicht
  2. Architektur-Prinzipien
  3. Komponenten
  4. Verwendung
  5. Beispiele
  6. Best Practices
  7. Häufige Fragen

Ü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):

dependency_overrides:
  erp_system:
    path: ../../easysale-core/core/apps/erp_system
  shared:
    path: ../../easysale-core/core/shared

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

  1. Extensions für Type-Safety nutzen

    // ✅ Gut: Typsicher
    article.pharmaData?.chargennummer
    
    // ❌ Schlecht: Nicht typsicher
    article.customData?['pharma']['chargennummer']
    

  2. ClientConfig für Features

    // ✅ Gut: Config-gesteuert
    if (config.isFeatureEnabled('pharma_tracking')) { /* ... */ }
    
    // ❌ Schlecht: Hardcoded
    if (clientId == 'demo_pharma') { /* ... */ }
    

  3. BLoC Inheritance sparsam

    // ✅ Gut: Nur bei echter Business Logic
    class PharmaArticlesBloc extends ArticlesBloc { /* ... */ }
    
    // ❌ Schlecht: Für simple Filter
    // Besser: Event im Standard-BLoC verwenden
    

  4. Services kapseln Custom Logic

    // ✅ Gut: Service-Klasse
    class PharmaArticleService {
      ExpiryReport generateReport() { /* ... */ }
    }
    
    // ❌ Schlecht: Logic in Widget
    class Widget {
      build() {
        // viel Business Logic hier...
      }
    }
    

❌ DON'Ts

  1. Keine Basis-Code-Änderungen
  2. ❌ Core-Code NICHT ändern (read-only im Multi-Root Workspace)
  3. ✅ Overrides im eigenen Client-Repo erstellen

  4. Kein Client-Check in Shared Code

    // ❌ NIEMALS in shared/ oder apps/
    if (clientId == 'demo_pharma') { /* ... */ }
    
    // ✅ Stattdessen: Feature-Flag
    if (config.isFeatureEnabled('pharma_tracking')) { /* ... */ }
    

  5. Keine Deep customData-Strukturen

    // ❌ Schlecht: Zu verschachtelt
    customData: {
      'pharma': {
        'data': {
          'fields': {
            'charge': { /* ... */ }
          }
        }
      }
    }
    
    // ✅ Gut: Flache Struktur + Extension
    customData: {
      'pharma': {
        'chargennummer': 'PH-123',
        'verfallsdatum': '2027-12-31'
      }
    }
    


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