myBusiness — Developer Documentation

MAUI 10 · .NET 10 · Firebase Firestore (REST, online-only) · MVVM CommunityToolkit
Stand: April 2026 · v2.5 · Architektur: Online-Only (kein SQLite-Sync)

ℹ️
Architektur-Migration abgeschlossen Diese Version arbeitet vollständig online-only. Alle Daten kommen direkt aus Firestore REST API. DatabaseService + SyncService sind deaktiviert (aufgehoben für spätere Offline-Erweiterung).

Architektur-Übersicht

Schichten

Views (XAML + Code-behind)
↑ Bindings / Commands ↓
ViewModels (MVVM CommunityToolkit)
↑ Daten / Task ↓
DataService
SettingsService
PdfService
↑ HTTP / JSON ↓
FirebaseRestService
FirebaseAuthService
↑ REST API ↓
Firebase Firestore REST API
Firebase Auth REST API

🔒 Auth-Strategie

Email/Password via Firebase Auth REST API. Token wird in Preferences (verschlüsselt) gespeichert. Auto-Refresh 5 Min vor Ablauf.

companyId === userId (Firebase Auth UID) — MVP single-user.

☁️ Daten-Strategie

100% online-only. Kein lokaler Cache. Jede Navigation lädt frisch aus Firestore.

Vorteil: immer aktuelle Daten, kein Sync-Konflikt. Nachteil: kein Offline.

📦 DI-Lebenszyklus

Singleton: HttpClient, FirebaseAuthService, FirebaseRestService, DataService, SettingsService, PdfService

Transient: alle ViewModels und Views

Datenfluss: Beispiel "Kundenliste laden"

  1. Page.AppearingEventToCommandBehavior feuert LoadCommand
  2. CustomersViewModel.LoadAsync()DataService.CheckPrerequisites()
  3. CheckPrerequisites() prüft: Connectivity.NetworkAccess != None + IsLoggedIn + CompanyId != null
  4. DataService.GetCustomersAsync()LoadCollectionAsync("customers", MapCustomer)
  5. FirebaseRestService.GetCollectionAsync(companyId, "customers")
  6. FirebaseAuthService.GetValidTokenAsync() → Token aus Preferences oder Refresh
  7. HTTP GET → https://firestore.googleapis.com/v1/projects/mytime-7c3cd/databases/(default)/documents/companies/{uid}/customers
  8. Response: FirestoreListResponse { documents: [...] }
  9. Jedes Dokument → MapCustomer(doc, companyId)LocalCustomer
  10. ViewModel: Customers = new ObservableCollection<LocalCustomer>(list) → UI aktualisiert

Firestore URL-Struktur

Base URL

https://firestore.googleapis.com/v1/projects/mytime-7c3cd/databases/(default)/documents

Collection (Liste)

{BaseUrl}/companies/{companyId}/{collection}
// z.B.: .../documents/companies/abc123uid/customers

Dokument (Einzeln)

{BaseUrl}/companies/{companyId}/{collection}/{docId}
// z.B.: .../documents/companies/abc123uid/customers/k1a2b3c4

Alle Collections

C#-KonstanteFirestore-Pfad
Collections.Customerscompanies/{uid}/customers
Collections.Projectscompanies/{uid}/projects
Collections.TimeEntriescompanies/{uid}/timeEntries
Collections.Invoicescompanies/{uid}/invoices
Collections.Quotescompanies/{uid}/quotes
Collections.Paymentscompanies/{uid}/payments
Collections.MaintenanceContractscompanies/{uid}/maintenanceContracts
Collections.ActivityReportscompanies/{uid}/activityReports
Collections.ServiceCatalogcompanies/{uid}/serviceCatalog
Collections.Settingscompanies/{uid}/settings
⚠️
Wichtig: Web App muss gleiche Pfade nutzen! Die MAUI App setzt voraus, dass die Web App Daten unter companies/{uid}/... speichert, wobei uid die Firebase Auth UID ist. Wenn die Web App eine andere Struktur hat, kommen keine Daten an.

FirebaseAuthService

Aufgabe

Verwaltet Login, Token-Lifecycle und CompanyId. Alles in Preferences gespeichert.

Properties

PropertyTypWas
CurrentUserIdstring?Firebase UID aus Preferences
CurrentCompanyIdstring?Manuell gesetzt per SetCompanyId() — aktuell = UserId
IsLoggedInboolUserId != null

Token-Flow

GetValidTokenAsync()
  → TokenExpiry in Preferences > UtcNow + 5min?
    → ja: return IdToken aus Preferences
    → nein: RefreshTokenAsync()
      → POST securetoken.googleapis.com/v1/token
      → neues IdToken + RefreshToken → Preferences
      → return neues IdToken

Methoden

MethodeBeschreibung
SignInAsync(email, pw)POST identitytoolkit signInWithPassword
GetValidTokenAsync()Gibt valides Token zurück, refresht automatisch
RefreshTokenAsync()Tauscht RefreshToken gegen neues IdToken
SetCompanyId(id)Schreibt CompanyId in Preferences
SignOut()Löscht alle Token + IDs aus Preferences
⚠️
CompanyId wird beim Login gesetztLoginViewModel: _auth.SetCompanyId(_auth.CurrentUserId!) direkt nach erfolgreichem Login. Bei App-Neustart ist die ID aus Preferences vorhanden.

FirebaseRestService

Aufgabe

Low-level HTTP-Wrapper für Firestore REST API. Kein Business-Logic. Alle Methoden brauchen ein valides Token (→ wird intern per GetValidTokenAsync() geholt).

Methoden

MethodeHTTPWas
GetCollectionAsync(cId, coll)GETAlle Docs einer Sub-Collection, mit Pagination
GetDocumentAsync(cId, coll, docId)GETEinzelnes Dokument
UpsertDocumentAsync(cId, coll, docId, fields)PATCHDokument anlegen oder überschreiben
DeleteDocumentAsync(cId, coll, docId)DELETEDokument löschen
GetCompanyDocAsync(cId)GETFirmen-Root-Dokument
ResolveCompanyIdAsync(userId)GETCompanyId aus /users/{userId} auflösen

Fehlerverhalten

Bei HTTP-Fehler: loggt Warning + Response-Body, gibt leere Liste / null / false zurück. Keine Exception nach oben.

DataService

Aufgabe

Zentrale Datenschicht. Einzige Abhängigkeit der ViewModels für Datenzugriff. Enthält alle Firestore-Mapper.

CheckPrerequisites()

Gibt null zurück wenn alles OK. Gibt Fehlermeldung (string) zurück bei:
NetworkAccess == None oder Unknown → "Keine Internetverbindung"
!IsLoggedIn oder CompanyId == null → "Du bist nicht angemeldet"

Alle List-ViewModels rufen CheckPrerequisites() als erstes auf. Bei Fehler → Toast + früher Return.

Typed Getters (Collections)

MethodeFirestore-CollectionReturn
GetCustomersAsync()customersList<LocalCustomer>
GetProjectsAsync()projectsList<LocalProject>
GetTimeEntriesAsync()timeEntriesList<LocalTimeEntry>
GetInvoicesAsync()invoicesList<LocalInvoice>
GetQuotesAsync()quotesList<LocalQuote>
GetPaymentsAsync()paymentsList<LocalPayment>
GetContractsAsync()maintenanceContractsList<LocalContract>
GetActivityReportsAsync()activityReportsList<LocalActivityReport>
GetServiceCatalogAsync()serviceCatalogList<LocalServiceEntry>
GetSettingsAsync()settingsLocalCompanySettings

Typed Single-Item Getters

Für jede Collection: GetXxxAsync(string id) → liefert einzelnes Objekt oder null.

Beispiel: GetCustomerAsync(id), GetProjectAsync(id), ...

CRUD

MethodeWas
SaveAsync(collection, docId, fields)PATCH zu Firestore → true/false
DeleteAsync(collection, docId)DELETE in Firestore → true/false
SaveSettingsAsync(settings)Speichert Firmeneinstellungen unter "settings/settings"

SettingsService

Thin wrapper um DataService für Firmeneinstellungen. Wird von Detail-VMs (Invoice, Quote) und SettingsViewModel genutzt.

public Task<LocalCompanySettings> GetAsync()
    => _data.GetSettingsAsync();

public Task<bool> SaveAsync(LocalCompanySettings settings)
    => _data.SaveSettingsAsync(settings);

Wenn noch keine Settings in Firestore: GetSettingsAsync() gibt Fallback-Defaults zurück (TaxRate=20, Currency=EUR, etc.).

PdfService

Generiert PDFs mit Syncfusion.PDF. Abhängigkeiten: SettingsService (Firmeninfos) + DataService (Kundendaten für Anschrift).

MethodeWas
GenerateInvoicePdfAsync(LocalInvoice)PDF mit Rechnungsdetails, speichert in CacheDirectory
GenerateQuotePdfAsync(LocalQuote)PDF mit Angebotsdetails, speichert in CacheDirectory

Gibt string? filePath zurück → wird via Share.RequestAsync() geteilt.

LocalModels

Alle C#-Klassen, die Firestore-Daten repräsentieren. Kein SQLite mehr nötig — die Attribute bleiben aber kompatibel für spätere Offline-Erweiterung.

Basis-Klasse LocalBase

public class LocalBase {
  public string Id        { get; set; }  // Firestore Document ID
  public string CompanyId { get; set; }  // = Firebase Auth UID
  public string? CreatedAt { get; set; }
  public string? UpdatedAt { get; set; }
  public SyncState SyncState { get; set; }  // für künftigen Offline-Support
}

Models Übersicht

KlasseWichtige Felder
LocalCustomerName, Email, Phone, Type (company/person), Street, PostalCode, City, Country, ContactsJson
LocalProjectTitle, CustomerId, Status, BudgetHours?, FixedPrice?, ServiceIdsJson (JSON-Array der zugewiesenen Tätigkeits-IDs)
LocalTimeEntryStartedAt (ISO-String!), EndedAt, DurationMinutes?, HourlyRate?, IsRunning, ProjectId, CustomerId, ServiceId
LocalInvoiceNumber, CustomerId, Date, DueDate, Status, TaxRate, TotalNet, TotalGross, LineItemsJson
LocalQuoteNumber, CustomerId, Date, ValidUntil, Status, TaxRate, TotalNet, TotalGross, LineItemsJson
LocalPaymentDate, Amount, Currency, PaymentMethod, CategoryId
LocalContractTitle, CustomerId, Interval (monthly/quarterly/biannual/annual), Status, TotalNet, TaxRate
LocalActivityReportTitle, CustomerId, ProjectId, Date, SignedAt, InvoiceId, TimeEntryIdsJson, PositionsJson
LocalServiceEntryName, DefaultHourlyRate, Unit (Stunde/Tag/Pauschal/Stück)
LocalCompanySettingsName, TaxRate, Currency, InvoicePrefix, QuotePrefix, NextInvoiceNumber, NextQuoteNumber, PaymentDueDays, Language

FirestoreModels — JSON-Besonderheiten

FirestoreValue Union-Typ

Firestore REST gibt Werte in einem Union-Objekt zurück. Nur eine Property ist gesetzt:

// Firestore REST Response (JSON)
{
  "fields": {
    "name":      { "stringValue":    "Acme GmbH" },
    "taxRate":   { "doubleValue":    20.0 },
    "count":     { "integerValue":  "42" },  // ← String! nicht Number!
    "active":    { "booleanValue":  true },
    "createdAt": { "timestampValue": "2024-01-15T10:00:00Z" },
    "deleted":   { "nullValue":     "NULL_VALUE" }
  }
}
🐛
Bug behoben: integerValue als String
Firestore gibt integerValue als JSON-String zurück ("42"), nicht als Number. System.Text.Json kann das standardmäßig nicht deserialisieren.

Fix: [JsonNumberHandling(AllowReadingFromString | WriteAsString)] auf long? integerValue gesetzt.

Getter-Extension-Methoden (auf Dictionary)

MethodeWas
GetString(key)stringValue ?? timestampValue, null wenn nicht vorhanden
GetDouble(key)doubleValue ?? integerValue, 0 wenn nicht vorhanden
GetBool(key)booleanValue, false wenn nicht vorhanden
GetStringArray(key)arrayValue.values als List<string>

DocumentId Extraction

public string DocumentId => name?.Split('/').LastOrDefault() ?? "";
// name = ".../documents/companies/uid/customers/abc123"
// → DocumentId = "abc123"

ViewModels — Übersicht

ViewModelInjected ServicesDataService-Methoden
LoginViewModelFirebaseAuthService— (nur Auth)
DashboardViewModelDataServiceGetInvoicesAsync, GetProjectsAsync, GetContractsAsync, GetSettingsAsync
CustomersViewModelDataServiceGetCustomersAsync
CustomerDetailViewModelDataServiceGetCustomerAsync, GetInvoicesAsync, GetQuotesAsync (für Offene Posten Tab), SaveAsync, DeleteAsync
ProjectsViewModelDataServiceGetProjectsAsync, GetCustomersAsync (für Kunden-Filter)
ProjectDetailViewModelDataServiceGetCustomersAsync, GetProjectAsync, SaveAsync, DeleteAsync
TimeTrackingViewModelDataServiceGetProjectsAsync, GetCustomersAsync, GetServiceCatalogAsync (→ gefiltert per Projekt), GetTimeEntriesAsync, SaveAsync
TimeEntryDetailViewModelDataServiceGetProjectsAsync, GetCustomersAsync, GetServiceCatalogAsync, GetTimeEntryAsync, SaveAsync, DeleteAsync
InvoicesViewModelDataServiceGetInvoicesAsync, SaveAsync (status change)
InvoiceDetailViewModelDataService, SettingsService, PdfServiceGetCustomersAsync, GetProjectsAsync, GetServiceCatalogAsync, GetTimeEntriesAsync (für Zeiten-Import), GetInvoiceAsync, SaveAsync, DeleteAsync
QuotesViewModelDataServiceGetQuotesAsync, SaveAsync (status change)
QuoteDetailViewModelDataService, SettingsService, PdfServiceGetCustomersAsync, GetProjectsAsync, GetQuoteAsync, SaveAsync, DeleteAsync
PaymentsViewModelDataServiceGetPaymentsAsync
PaymentDetailViewModelDataServiceGetPaymentAsync, SaveAsync, DeleteAsync
ContractsViewModelDataServiceGetContractsAsync
ContractDetailViewModelDataServiceGetCustomersAsync, GetContractAsync, SaveAsync, DeleteAsync
ActivityReportsViewModelDataServiceGetActivityReportsAsync
ActivityReportDetailViewModelDataServiceGetCustomersAsync, GetProjectsAsync, GetActivityReportAsync, SaveAsync, DeleteAsync
ServiceCatalogViewModelDataServiceGetServiceCatalogAsync
ServiceCatalogDetailViewModelDataServiceGetServiceEntryAsync, SaveAsync, DeleteAsync
SettingsViewModelSettingsService, FirebaseAuthService(via SettingsService)

BaseViewModel

Properties & Helpers

MemberWas
IsBusy / IsNotBusyLoading-State für Spinner und CanExecute
TitleSeitentitel für AppBar
ShowErrorAsync(msg)Toast (Long-Duration) auf Main Thread
ShowSuccessAsync(msg)Toast (Default) auf Main Thread
GoToAsync(route)Shell Navigation auf Main Thread
GoToAsync(route, params)Shell Navigation mit Query-Parametern
GoBackAsync()Shell.GoToAsync("..")

Pattern in jedem LoadAsync

[RelayCommand]
private async Task LoadAsync()
{
    if (IsBusy) return;                              // 1. double-fire guard
    var err = _data.CheckPrerequisites();
    if (err is not null) { await ShowErrorAsync(err); return; } // 2. network/auth guard
    IsBusy = true;
    try   { /* Firestore-Laden */ }
    finally { IsBusy = false; }                       // 3. immer freigeben
}

Behobene Bugs (diese Session)

🐛
Bug #1: integerValue-Deserialisierung (KRITISCH)
Problem: Firestore REST gibt integerValue als JSON-String zurück ("42"). System.Text.Json kann das nicht in long? deserialisieren → stille null → alle Integer-Felder (DurationMinutes, NextInvoiceNumber, ...) waren immer 0 oder null.
Fix: [JsonNumberHandling(AllowReadingFromString | WriteAsString)] auf long? integerValue in FirestoreModels.cs
🐛
Bug #2: Connectivity-Check zu strikt
Problem: Connectivity.NetworkAccess != NetworkAccess.Internet schlägt auf Emulatoren und manchen Mobilfunkverbindungen fehl (liefern ConstrainedInternet), obwohl HTTP-Requests funktionieren → CheckPrerequisites() lieferte Fehler → keine Daten geladen.
Fix: Nur None und Unknown als "kein Internet" behandeln. ConstrainedInternet wird jetzt akzeptiert.
🐛
Bug #3: SyncCompletedMessage nicht gefunden
Problem: SyncService.cs (aufgehoben für später) referenzierte SyncCompletedMessage, die beim Refactoring entfernt wurde → Compile-Fehler.
Fix: SyncCompletedMessage : ValueChangedMessage<bool> direkt in SyncService.cs definiert.
🐛
Bug #4: startedAt als Firestore Timestamp → Web-App-Crash (v2.1)
Problem: FirestoreFieldBuilder.ForTimeEntry() speicherte startedAt als Fv.Ts() (Firestore timestampValue). Das Firebase Web SDK gibt timestampValue-Felder als Timestamp-Objekt zurück — nicht als String. Code wie e.startedAt.startsWith(today) im Dashboard crashte mit "startedAt.startsWith is not a function".

Fix MAUI: ForTimeEntry(): ["startedAt"] = Fv.Str(t.StartedAt) — jetzt als ISO-String gespeichert.
Fix Web: useCollection.ts: normalizeDoc() wandelt alle Timestamp-Objekte in ISO-Strings um, bevor Daten an React übergeben werden. Damit funktioniert auch zukünftiger Code robust.
// useCollection.ts — normalizeDoc()
snapshot.docs.map((d) => normalizeDoc<T>({ id: d.id, ...d.data() }))

// Wandelt Firestore Timestamp → ISO-String:
v instanceof Timestamp ? v.toDate().toISOString() : v

Debug-Checkliste — "Keine Daten"

  • 🔲
    Eingeloggt? Preferences prüfen: user_id und company_id müssen gesetzt sein. Nach frischem Login: SetCompanyId(userId) in LoginViewModel.
  • 🔲
    Firestore-Pfad korrekt? Daten in Firestore müssen unter companies/{uid}/customers etc. liegen. Web-App und MAUI-App müssen dieselbe Struktur verwenden!
  • 🔲
    Token gültig? GetValidTokenAsync() — wenn Token abgelaufen UND kein RefreshToken → gibt null zurück → alle Requests silent fail. Fix: neu einloggen.
  • 🔲
    Firestore Security Rules? Erlauben authenticated User Lesezugriff auf /companies/{uid}/**? Test im Firebase Console → Firestore → Rules.
  • 🔲
    Netzwerk? Debug-Logs: GetCollection customers → 200, 0 docs = Netz OK aber Collection leer. GetCollection customers → 403 = Security Rules. GetCollection customers → 404 = Pfad falsch.
  • 🔲
    Emulator? Auf iOS-Simulator: Connectivity gibt manchmal Unknown zurück. Bug #2 behebt das. Trotzdem auf Device testen.
  • 🔲
    CheckPrerequisites() Fehler sichtbar? Toast-Meldung erscheint kurz bei Fehler. In Konsole nach [Warning] GetCollection suchen.
  • 🔲
    Web-App: Timestamp-Crash? Fehlermeldung startedAt.startsWith is not a function → ein Datumsfeld wird als Firestore Timestamp-Objekt geliefert. Fix: normalizeDoc() in useCollection.ts konvertiert alle Timestamps. MAUI-Seite: startedAt muss als Fv.Str() gespeichert werden (nicht Fv.Ts()).
  • 🔲
    Services/Tätigkeiten leer? LocalProject.ServiceIdsJson muss ein valides JSON-Array enthalten (["id1","id2"]). Leerer String oder null → alle Services werden angezeigt (Fallback). Prüfen in Firestore Console: Feld serviceIdsJson in den Projekt-Dokumenten.
  • 🔲
    Diagnose-Panel verwenden! (v2.2) In den Einstellungen → Sektion "Diagnose & Verbindung" → zeigt UserId, CompanyId, Token-Ablauf, Netzwerkstatus. Button "Verbindung testen" macht einen echten Firestore-Call und zeigt den HTTP-Status. Damit sofort sehen ob Token OK ist.
  • 🔲
    welcome_seen Bug (v2.1 → v2.2 fix): Vor v2.2 wurde die OnboardingSeen-Preference nach dem Login NIE gesetzt (falscher Preference-Key "welcome_seen" statt PrefKeys.OnboardingSeen). Fix: LoginViewModel setzt jetzt Preferences.Set(PrefKeys.OnboardingSeen, true) nach erfolgreichem Login.

Logging — Was wo erscheint

Alle Services verwenden ILogger<T>. In DEBUG: Ausgabe via builder.Logging.AddDebug() im IDE-Output-Window sichtbar.

Wichtige Log-Zeilen

LevelMessageWas
DEBUGGetCollection customers: 5 docsErfolgreicher Request, 5 Dokumente geladen
WARNGetCollection customers → 403: {error}Security Rules blockieren den Zugriff
WARNGetCollection customers → 404: {error}Pfad existiert nicht (leere Collection oder falscher Pfad)
WARNGetCollection customers → 401Token ungültig/abgelaufen
WARNToken refresh failedRefreshToken abgelaufen → neu einloggen
WARNSignIn failed: {error}Login-Fehler mit Firebase-Fehlermeldung
ERRORLoadCollection failed: {Col}Exception beim HTTP-Request
ERRORSave failed: {Col}/{Id}Exception beim Schreiben

Logs anzeigen

// Visual Studio: Debug Output Window (Dropdown: "Debug")
// Rider: Run → Debug Output
// Android: adb logcat -s "myBusiness"
// iOS Simulator: Console.app → Filter "myBusiness"

Neue Features — v2.1

1. Kunden — Offene Posten

CustomerDetailViewModel lädt beim Öffnen eines Kunden parallel Rechnungen und Angebote und filtert auf offene Statuswerte.

PropertyTypWas
OpenInvoicesObservableCollection<LocalInvoice>Status: draft, sent, overdue
OpenQuotesObservableCollection<LocalQuote>Status: draft, sent
OpenItemCountintSumme beider Listen — für Badge im XAML
HasOpenItemsboolFür IsVisible-Binding

XAML: Tab "Offene Posten" mit Zähler-Badge zeigen, wenn OpenItemCount > 0.

2. Projekte — Kunden-Filter

ProjectsViewModel hält die vollständige Liste intern (_allProjects) und exponiert FilteredProjects gefiltert nach SelectedCustomerFilter.

// Filter setzen (XAML: Picker mit Customers-Liste)
SelectedCustomerFilter = customer;   // → Projects wird neu befüllt

// Filter zurücksetzen
ClearFilterCommand.Execute(null);

Kein extra Firestore-Request: Filter funktioniert rein in-memory auf der geladenen Liste.

3. Zeiterfassung — Tätigkeiten (Services) pro Projekt

TimeTrackingViewModel lädt alle Tätigkeiten aus dem Katalog in _allServices. Sobald der User ein Projekt auswählt, wird Services automatisch gefiltert:

// partial void wird automatisch von CommunityToolkit aufgerufen
partial void OnSelectedProjectChanged(LocalProject? value)
{
    SelectedService = null;
    var ids = JsonSerializer.Deserialize<List<string>>(value.ServiceIdsJson);
    Services = new(..._allServices.Where(s => ids.Contains(s.Id)));
}
PropertyWas
ServicesGefilterte Tätigkeit-Liste (nur die dem Projekt zugewiesenen)
HasProjectServicesServices.Count > 0 — für IsVisible des Tätigkeit-Pickers
SelectedServiceGewählte Tätigkeit — wird beim Projekt-Wechsel auf null gesetzt

Beim Stopp des Timers: ServiceId = SelectedService?.Id und HourlyRate = SelectedService?.DefaultHourlyRate werden im TimeEntry gespeichert.

💡
XAML-Tipp: Tätigkeit-Picker mit IsVisible="{Binding HasProjectServices}" nur anzeigen wenn Tätigkeiten verfügbar sind. Picker an SelectedService binden.

4. Rechnungen — Zeiten importieren

InvoiceDetailViewModel lädt beim Öffnen auch Zeiteinträge, Projekte und Tätigkeiten. Sobald ein Kunde ausgewählt ist, werden importierbare Einträge berechnet.

Property / CommandWas
ImportableEntriesAbgeschlossene Zeiteinträge der Kunden-Projekte (isRunning=false)
HasImportableEntriesFür IsVisible-Binding des Import-Buttons
ShowImportPanelToggle für das Import-Panel
ToggleImportPanelCommandÖffnet/schließt das Panel
ImportTimeEntriesCommandGruppiert alle ImportableEntries nach Tätigkeit → fügt als LineItems ein

Gruppirungs-Logik: Einträge werden nach ServiceId (Fallback: ProjectId) gruppiert. Pro Gruppe entsteht eine LineItem:

description = $"{tätigkeitsName} ({Stunden}h {Minuten}m)"
quantity    = Gesamtminuten / 60.0   // z.B. 2.5
unitPrice   = DefaultHourlyRate der Tätigkeit
unit        = "Std."
⚠️
Hinweis: ImportTimeEntriesCommand importiert alle ImportableEntries ohne Selektion (MAUI-vereinfachte Version). Die Web-App bietet Checkbox-Selektion pro Eintrag.

Neue Features — v2.2

1. Tablet-Layout — Persistente Sidebar-Navigation

Ab Fensterbreite ≥ 768 dp wechselt die App automatisch von der Bottom-TabBar (Phone) zur linken Sidebar-Navigation (Tablet/Desktop).

DateiÄnderung
AppShell.xamlShell.FlyoutContent mit 11 Navigations-Items (Border+TapGestureRecognizer), Shell.FlyoutHeader mit Branding, x:Name="MainTabBar" auf dem TabBar-Element
AppShell.xaml.csOnHandlerChanged(): subscribed Window.SizeChangedUpdateAdaptiveLayout() setzt FlyoutBehavior.Locked + MainTabBar.IsVisible=false auf ≥ 768dp
Styles.xamlNeue Styles: SidebarItem (Border) und SidebarItemLabel (Label)

Aktives Element hervorheben

AppShell.Navigated-Event → UpdateActiveSidebarItem(location): Sucht den passenden Border per FindByName() und setzt BackgroundColor auf Primary @12% Opacity (hell) / @18% Opacity (dunkel).

// Breite-Trigger in AppShell.xaml.cs
private void UpdateAdaptiveLayout()
{
    var isTablet = (Window?.Width ?? 0) >= 768;
    FlyoutBehavior       = isTablet ? FlyoutBehavior.Locked : FlyoutBehavior.Disabled;
    MainTabBar.IsVisible = !isTablet;
}
💡
Wichtig: Die 11 Sidebar-Items navigieren per GoToAsync(Routes.XYZ) — gleiche Routen wie MorePage und MoreViewModel. FlyoutBehavior.Locked = permanent sichtbar (kein Hamburger-Button). FlyoutBehavior.Disabled = komplett ausgeblendet.

2. Diagnose-Panel in SettingsPage

SettingsViewModel wurde um Diagnose-Eigenschaften erweitert. Die SettingsPage zeigt jetzt eine "Diagnose & Verbindung" Sektion am Ende.

PropertyTypWas
DiagUserIdstringUserId aus Preferences.Get(PrefKeys.UserId)
DiagCompanyIdstringCompanyId aus Preferences.Get(PrefKeys.CompanyId)
DiagTokenExpirystringAblaufzeit formatiert: "Läuft ab in X min" oder "ABGELAUFEN"
DiagConnectivitystringConnectivity.NetworkAccess.ToString()
DiagApiStatusstringErgebnis des letzten Verbindungstests (✅/⚠️/❌)
DiagTestingboolTrue während Test läuft (deaktiviert Button)
TestConnectionCommandIAsyncRelayCommandPrüft Voraussetzungen → Token → ruft GetSettingsAsync() auf

RefreshDiagnostics() wird automatisch am Ende von LoadAsync() aufgerufen.

Konstruktor: SettingsViewModel(SettingsService, FirebaseAuthService, DataService) — DataService per DI injiziert (bereits als Singleton registriert, kein MauiProgram-Update nötig).

3. Bug-Fix: welcome_seen Preference-Key

In LoginViewModel.LoginAsync(): Der bisher verwendete Hardcode-Key "welcome_seen" wurde durch PrefKeys.OnboardingSeen ersetzt. Außerdem wird nach erfolgreichem Login Preferences.Set(PrefKeys.OnboardingSeen, true) gesetzt, damit der Welcome-Screen nur einmalig erscheint.

Tablet-Layout — Architektur-Details

Adaptive Navigation: Phone vs. Tablet

ModusBreiteFlyoutBehaviorTabBarNavigation
Phone< 768 dpDisabledSichtbar (5 Tabs)Tabs: Dashboard, Kunden, Projekte, Zeit, Mehr
Tablet≥ 768 dpLockedAusgeblendetSidebar: alle 11 Menüpunkte

Shell.FlyoutBehavior-Werte

WertVerhalten
DisabledFlyout komplett deaktiviert — FlyoutContent unsichtbar, kein Hamburger
FlyoutFlyout als Drawer (Hamburger öffnet/schließt es)
LockedFlyout permanent sichtbar — dient als persistente Sidebar ✓

Wichtig: Phone-Navigation beim Tablet-Modus

Wenn MainTabBar.IsVisible = false gesetzt ist, sind die TabBar-Seiten (Dashboard, Kunden, Projekte, Zeit) noch immer per GoToAsync(Routes.XYZ) erreichbar. Die Sidebar-Buttons verwenden dieselben Routes-Konstanten. Die "Mehr"-Seite (MorePage) ist auf Tablet nicht zugänglich — alle Menüpunkte sind direkt in der Sidebar.

Sidebar-Items

// Border mit x:Name für aktive Hervorhebung aus Code-behind
<Border x:Name="SidebarItemDashboard" Style="{StaticResource SidebarItem}">
    <Grid ColumnDefinitions="28,*">
        <Label Text="⊞" .../>
        <Label Grid.Column="1" Text="Dashboard" Style="{StaticResource SidebarItemLabel}"/>
    </Grid>
    <Border.GestureRecognizers>
        <TapGestureRecognizer Tapped="OnSidebarDashboard"/>
    </Border.GestureRecognizers>
</Border>

Aktive Hervorhebung (Code-behind)

private static readonly Dictionary<string, string> RouteToSidebarName = new()
{
    { "dashboard", "SidebarItemDashboard" },
    { "customers", "SidebarItemCustomers"  },
    // ... alle 11 Items
};

private void UpdateActiveSidebarItem(string location)
{
    // Reset vorheriges
    if (_activeSidebarBorder is not null)
        _activeSidebarBorder.BackgroundColor = Colors.Transparent;

    foreach (var (keyword, elementName) in RouteToSidebarName)
    {
        if (!location.Contains(keyword)) continue;
        if (FindByName(elementName) is Border border)
        {
            var isDark = Application.Current?.RequestedTheme == AppTheme.Dark;
            border.BackgroundColor = isDark ? ActiveBgDark : ActiveBgLight;
            _activeSidebarBorder = border;
        }
        break;
    }
}

FirestoreFieldBuilder

Statische Hilfsklasse in Helpers/FirestoreFieldBuilder.cs. Wandelt jedes Local-Model in ein Dictionary<string, FirestoreValue> für den PATCH-Request um.

Wichtige Regel: String vs. Timestamp

⚠️
startedAt MUSS als Fv.Str() gespeichert werden!
Datum-/Zeitfelder die von der Web-App als String gelesen werden, dürfen NICHT als Fv.Ts() gespeichert werden.
Das Firebase Web SDK gibt timestampValue als Timestamp-Objekt zurück (nicht als String) — das bricht alle String-Operationen in der Web-App.

Regel: Fv.Ts() nur für updatedAt/createdAt-Meta-Felder verwenden. Alle app-logischen Datums-Strings (startedAt, endedAt, date, etc.) als Fv.Str() speichern.
MethodeFelder
ForCustomer()type, name, email, phone, notes, street, postalCode, city, country, contactsJson, updatedAt
ForProject()customerId, title, description, status, serviceIdsJson, budgetHours?, fixedPrice?, updatedAt
ForTimeEntry()customerId, projectId, serviceId, startedAt (Str!), endedAt, isRunning, notes, durationMinutes?, hourlyRate?, updatedAt
ForInvoice()number, customerId, projectId, date, dueDate, status, taxRate, totalNet, totalGross, currency, notes, quoteId, lineItemsJson, updatedAt
ForQuote()number, customerId, projectId, date, validUntil, status, taxRate, totalNet, totalGross, currency, notes, invoiceId, lineItemsJson, updatedAt
ForSettings()name, street, postalCode, city, country, logoUrl, taxRate, currency, language, invoicePrefix, quotePrefix, nextInvoiceNumber, nextQuoteNumber, paymentDueDays

Neue Features — v2.3

ℹ️
v2.3 — Feature-Parität Web ↔ MAUI + Dashboard-Erweiterungen
Auth-Fehler werden jetzt korrekt propagiert. Dashboard zeigt Stunden/Monat, offene Angebote und Budget-Warnungen. Projekte und Kunden wurden um gebuchte Stunden, Budget-Fortschritt und nicht abgerechnete Zeiteinträge erweitert. Filter wurden zu Projekten und Kunden hinzugefügt. Alle Dialoge / Popups sind deutlich größer.

1. Kritischer Fix — Auth-Fehler-Propagierung

Bisher wurden Auth-Fehler (abgelaufenes Token, kein Token) still verschluckt. Beide Stellen wurden gefixt:

DateiÄnderung
FirebaseRestService.csGetCollectionAsync() und GetDocumentAsync(): Wenn GetValidTokenAsync() null zurückgibt → wirft jetzt UnauthorizedAccessException("Sitzung abgelaufen...") statt leere Liste/null zurückzugeben
DataService.csÄußeres try/catch in LoadCollectionAsync<T>() entfernt — Exceptions propagieren jetzt zum ViewModel. Inneres per-Dokument try/catch für Mapper-Fehler bleibt erhalten.
DashboardViewModel.cs, ProjectsViewModel.csFangen UnauthorizedAccessException in LoadAsync() und rufen ShowErrorAsync() auf
// FirebaseRestService.cs — vorher
if (token is null) return [];

// FirebaseRestService.cs — nachher
if (token is null)
{
    _log.LogWarning("GetCollection {Col}: kein Token — Sitzung abgelaufen", collection);
    throw new UnauthorizedAccessException("Sitzung abgelaufen. Bitte melde dich neu an.");
}

2. Dashboard — Neue KPIs & Budget-Warnungen (MAUI + Web)

DashboardViewModel wurde um folgende Properties erweitert:

PropertyTypBeschreibung
MonthMinutesintSumme aller Zeiteinträge im laufenden Monat (in Minuten)
MonthHoursDisplaystringFormatiert als "Xh Ym"
OpenQuoteCountintAnzahl Angebote mit Status "Offen"
BudgetWarningsObservableCollection<ProjectBudgetInfo>Projekte mit Budgetauslastung ≥ 80% (max. 5, sortiert nach % absteigend)
HasBudgetWarningsboolBudgetWarnings.Count > 0 — für IsVisible-Binding
public record ProjectBudgetInfo(
    string Title, string CustomerName,
    double BudgetHours, double UsedHours,
    double PercentUsed, bool IsExceeded);

Budget-Warnung wird ausgelöst wenn: UsedHours / BudgetHours >= 0.80. In der DashboardPage.xaml wird IsExceeded per DataTrigger für rote Farbgebung genutzt.

💡
Web-App nutzt denselben 80%-Schwellenwert via budgetCalc.ts. Gleiche Logik, gleicher Schwellenwert auf beiden Plattformen.

3. Projekte — Budget-Fortschritt & Zugewiesene Tätigkeiten (MAUI + Web)

ProjectDetailViewModel lädt jetzt parallel Zeiteinträge und Tätigkeiten (Services) des Projekts:

PropertyTypBeschreibung
BookedHoursdoubleSumme aller Zeiteinträge des Projekts (in Stunden)
BudgetPercentdouble0..100, geclampt auf 100
HasBudgetboolBudgetHours > 0
IsOverBudgetboolBookedHours > BudgetHours
AssignedServicesObservableCollection<LocalService>Tätigkeiten mit ProjectId == CurrentProject.Id
HasAssignedServicesboolFür IsVisible-Binding

Für die Web-App wurde in ProjectsPage.tsx ein Budget-Fortschrittsbalken in der Projekt-Detailansicht ergänzt (grün → amber bei ≥80% → rot bei Überschreitung).

Projekt-Status-Filter

ProjectsViewModel erhielt einen SelectedStatusFilter (string?) mit [NotifyPropertyChangedFor(nameof(FilteredProjects))]. StatusOptions liefert die Liste der wählbaren Status-Werte. ClearAllFiltersCommand setzt beide Filter zurück.

4. Kunden — Nicht abgerechnete Zeiteinträge (MAUI + Web)

CustomerDetailViewModel berechnet "Nicht abgerechnete Zeiteinträge" für den Kunden:

PropertyTypBeschreibung
UnbilledTimeEntriesObservableCollection<LocalTimeEntry>Zeiteinträge ohne InvoiceId, deren ProjectId zu einem Projekt des Kunden gehört
UnbilledHoursdoubleSumme der unbezahlten Stunden
HasUnbilledTimeboolFür IsVisible-Binding der "Nicht abgerechnet"-Karte
💡
Warum über Projekte filtern? Zeiteinträge speichern nur eine projectId, keine direkte customerId. Deshalb wird erst die Liste der Kunden-Projekte ermittelt, dann alle Einträge gefiltert, deren ProjectId in dieser Liste liegt.

Kunden-Typ-Filter

In CustomersViewModel (Web) und CustomersPage.xaml (MAUI) wurde ein Typ-Filter (Alle / Firma / Person) ergänzt.

5. Größere Dialoge / Popups (Web)

In allen Web-Dialog-Komponenten wurde die maximale Breite von sm:max-w-sm/md/lg auf sm:max-w-2xl angehoben:

DateiBetroffene Dialoge
CustomersPage.tsxNeuer Kunde, Kunde bearbeiten
ProjectsPage.tsxNeues Projekt, Projekt bearbeiten
PaymentsPage.tsxNeue Zahlung, Zahlung bearbeiten
TimeTrackingPage.tsxNeuer Zeiteintrag, Zeiteintrag bearbeiten

MAUI: Popup-Größe wird über HeightRequest/WidthRequest auf dem RgPopupPage-Container gesteuert.

Konstanten

FirebaseConfig

ApiKeyAIzaSyBTU10a3_...
ProjectIdmytime-7c3cd
BaseUrlFirestore Docs URL
AuthUrlidentitytoolkit.googleapis.com/v1

PrefKeys

UserId"user_id"
CompanyId"company_id"
IdToken"id_token"
RefreshToken"refresh_token"
TokenExpiry"token_expiry"
Theme"app_theme"
PrimaryColor"primary_color"

Routes

Login"//login"
Dashboard"//main/dashboard"
Customers"//main/customers"
Invoices"//main/invoices"
Settings"//main/settings"

DI-Registrierung (MauiProgram.cs)

Services (Singleton)

s.AddSingleton<HttpClient>();
s.AddSingleton<FirebaseAuthService>();   // auth + token management
s.AddSingleton<FirebaseRestService>();   // HTTP wrapper
s.AddSingleton<DataService>();           // zentrale Datenschicht
s.AddSingleton<SettingsService>();        // thin wrapper für Settings
s.AddSingleton<PdfService>();             // Syncfusion PDF generation
s.AddSingleton<AppShell>();               // Shell als Singleton

ViewModels & Views (Transient)

Alle ViewModels und Views als AddTransient<T>() — neue Instanz bei jeder Navigation.

Auto-resolvierbar (kein explizites Register nötig)

ILogger<T> wird vom MAUI-Hosting-Framework automatisch bereitgestellt, sobald builder.Logging konfiguriert ist.

📝
Deaktiviert (aufgehoben): DatabaseService, SyncService — werden für offline-Erweiterung aufgehoben, sind nicht in DI registriert und werden von keinem ViewModel genutzt.

App-Start-Flow

App.OnStart()
  └─ NavigateInitialAsync()  (200ms Delay, dann Main Thread)
       ├─ IsLoggedIn (UserId in Preferences)?
       │    → ja:  GoToAsync("//main/dashboard")
       │            → DashboardViewModel.LoadAsync() via EventToCommandBehavior
       │            → AppShell.OnHandlerChanged(): UpdateAdaptiveLayout() (Sidebar vs. TabBar)
       │
       └─ nein: PrefKeys.OnboardingSeen in Preferences?
                → ja:  GoToAsync("//login")
                → nein: GoToAsync("//onboarding")

Login (LoginViewModel.LoginAsync):
  SignInAsync() → SetCompanyId(userId) → Preferences.Set(PrefKeys.OnboardingSeen, true)
  → GoToAsync(Dashboard oder Welcome je nach OnboardingSeen)
Auth-Fehler werden jetzt korrekt propagiert (v2.3-Fix): FirebaseRestService.GetCollectionAsync() wirft seit v2.3 eine UnauthorizedAccessException wenn der Token fehlt — anstatt still eine leere Liste zurückzugeben. ViewModels fangen diese Exception und zeigen dem User eine Fehlermeldung an. Diagnose-Panel in Einstellungen → "Verbindung testen" bleibt als zusätzliches Diagnosetool verfügbar.

Neue Features — v2.4

ℹ️
v2.4 — Profil & Avatar, Info-Seite, startedAt Bugfix
Benutzerprofil mit Anzeigename + Profilbild. Avatar-Bubble in Sidebar (Web + MAUI) mit Dropdown-Menü. Info-Seite mit App-Version. Kritischer Timestamp-Bug in Produktion behoben.

1. Benutzerprofil (Web + MAUI)

Profil wird in Firestore unter companies/{companyId}/profile/main gespeichert. Felder: displayName, photoUrl.

PlattformDateiBeschreibung
Webfeatures/profile/useProfile.tsHook: liest/schreibt Profil + Upload zu Firebase Storage
Webfeatures/profile/ProfilePage.tsxFormular: Anzeigename, E-Mail (read-only), Foto-Upload
MAUIViewModels/ProfileViewModel.csLoadAsync / SaveAsync / PickPhotoAsync + Storage-Upload
MAUIViews/ProfilePage.xamlAvatar-Kreis, Name-Entry, E-Mail read-only, Speichern-Button

Foto wird in Firebase Storage unter companies/{companyId}/profile/photo abgelegt. Download-URL wird zurück in Firestore + Preferences gecacht.

2. Avatar-Sidebar + Dropdown-Menü (Web + MAUI)

Unterer Bereich der Sidebar zeigt Avatar (Foto → Initialen → "?") + Name/E-Mail.

PlattformDateiVerhalten
Webapp/AppShell.tsxDropdownMenu mit: Profil, Einstellungen, Info, Abmelden
MAUIAppShell.xaml.cs → RefreshAvatarFromPreferences()Liest Preferences-Cache, zeigt Foto oder Initialen. OnAvatarTapped → DisplayActionSheet
// MAUI — Avatar-Refresh nach Login (LoginViewModel.cs)
Preferences.Set(PrefKeys.UserEmail, Email);
if (Shell.Current is AppShell appShell)
    appShell.RefreshAvatarFromPreferences();

3. Info-Seite + App-Version

Zentrale Versionskonstanten in Constants/AppConstants.cs → AppInfo (MAUI) und shared/lib/appVersion.ts (Web).

KonstanteWert
Version2.5.0
AppNamemyBusiness
DeveloperGellClan Software Products

Web: Info wird als Dialog im AppShell-Dropdown angezeigt. MAUI: eigene Views/InfoPage.xaml (Route: info).

4. Bugfix — startedAt.startsWith Timestamp-Fehler

Firestore gibt Timestamp-Felder manchmal als Objekt {seconds, nanoseconds} zurück statt als ISO-String — wenn MAUI-App geschrieben hat oder nach Chunk-Grenze. Betraf DashboardPage.tsx in Produktion.

Fix in shared/hooks/useCollection.ts: Duck-Typing-Fallback in toIsoIfTimestamp():

function toIsoIfTimestamp(v: unknown): unknown {
  if (v instanceof Timestamp) return v.toDate().toISOString();
  // Duck-type für cross-chunk Szenarien
  if (v !== null && typeof v === 'object' && 'seconds' in v && 'nanoseconds' in v) { ... }
  return v;
}

Zusätzlich: defensive typeof e.startedAt === 'string' Guards in DashboardPage.tsx.

Neue Features — v2.5

ℹ️
v2.5 — Billing-Status für Zeiteinträge + Avatar-Fixes
Zeiteinträge wissen jetzt ob/wann sie verrechnet wurden. Verrechnung passiert automatisch bei Invoice-Erstellung (Import) und bei Tätigkeitsbericht-Transfer. Zeitübersicht zeigt Badge + Filter. MAUI-Initialen-Bug behoben.

1. TimeEntry — neue Billing-Felder

Datenmodell-Erweiterung in shared/types/models.ts (Web) und Models/LocalModels.cs (MAUI):

FeldTypBeschreibung
invoiceId?stringRechnungsnummer wenn über Invoice verrechnet
activityReportId?stringBericht-ID wenn über Tätigkeitsbericht verrechnet
billedAt?string (ISO)Datum der Verrechnung

MAUI: DataService.cs MapTimeEntry und FirestoreFieldBuilder.cs ForTimeEntry wurden entsprechend erweitert.

2. Automatische Markierung bei Verrechnung (Web)

Zwei Stellen setzen die Billing-Felder automatisch per Firestore writeBatch:

WoTriggerFelder gesetzt
InvoicesPage.tsx → handleCreate()Neue Rechnung speichern nach Zeiten-ImportinvoiceId (= Rechnungsnummer), billedAt
ActivityReportsPage.tsx → transferToInvoice()Bericht → Rechnung übertragenactivityReportId (= Report-ID), billedAt

InvoiceForm.tsx verfolgt importierte Entry-IDs in importedEntryIds-State und übergibt sie via erweiterter onSubmit(data, importedTimeEntryIds[])-Signatur. Importierbare Einträge schließen bereits verrechnete Einträge aus (!e.invoiceId && !e.activityReportId).

3. Zeitübersicht — Badge + Filter (Web + MAUI)

Web (TimeTrackingPage.tsx):

  • Filter-Chips: Alle | Offen | Verrechnet — filtern completedEntries in-place
  • Badge "✓ Verrechnet" (grün) auf Einträgen mit invoiceId oder activityReportId

MAUI (TimeTrackingViewModel.cs + TimeTrackingPage.xaml):

  • BillingFilter ObservableProperty ("all" | "open" | "billed")
  • ApplyBillingFilter() filtert _allEntriesEntries
  • SetBillingFilterCommand(string) — gebunden an 3 Buttons in der View
  • Badge "✓ Verrechnet" via IsNotNullOrEmptyConverter auf InvoiceId/ActivityReportId

4. B2-Fix — MAUI ProfilePage Initialen

ProfileViewModel.cs: Neues berechnetes Property AvatarInitials — gibt ersten Buchstaben von DisplayName oder UserEmail zurück:

public string AvatarInitials =>
    !string.IsNullOrEmpty(DisplayName) && DisplayName.Any(char.IsLetterOrDigit)
        ? DisplayName.First(char.IsLetterOrDigit).ToString().ToUpperInvariant()
        : !string.IsNullOrEmpty(UserEmail) && UserEmail.Any(char.IsLetterOrDigit)
            ? UserEmail.First(char.IsLetterOrDigit).ToString().ToUpperInvariant()
            : "?";

ProfilePage.xaml: Label-Binding von IsNullOrEmptyConverter auf AvatarInitials umgestellt. [NotifyPropertyChangedFor]-Attribute auf _displayName und _userEmail hinzugefügt.

Neue Features — v2.5 (P2 – P4)

P2 · P3 · P4 abgeschlossen (April 2026)
Projekt→Angebot/Rechnung Import (Services + Zeiteinträge), Kalender-Ansicht, Payments v2 (Soll/Haben + Kategorien + Berichte) — Web & MAUI.

P2 — Projekt-Import in Angebot & Rechnung (Web + MAUI)

Angebote und Rechnungen können Positionen direkt aus dem Projekt-Kontext importieren:

TabQuelleErgebnis
TätigkeitenServiceCatalog-Einträge des ProjektsJe 1 Zeile mit Menge 1 + defaultHourlyRate
ZeiteinträgeOffene (unberechnete) TimeEntries des KundenGruppiert nach Projekt, Minuten → Stunden

Web: QuoteForm.tsx und InvoiceForm.tsx erweitert mit aufklappbarer "Projektdaten importieren"-Sektion und zwei Tabs. QuotesPage.tsx / InvoicesPage.tsx übergeben timeEntries + services Props.

MAUI: QuoteDetailViewModel.cs + InvoiceDetailViewModel.cs — neue Properties ImportableServices, ImportableEntries, ImportTab, HasImportData. Methoden ImportServicesCommand + ImportTimeEntriesCommand. XAML: Import-Panel mit Service-Zähler und Gruppen-Import-Button. Sichtbarkeit via IsPositiveConverter (neu in Converters.cs).

// IsPositiveConverter — für ObservableCollection.Count-Binding
public class IsPositiveConverter : IValueConverter {
    public object Convert(object? value, ...) => value is int n && n > 0;
}

P3 — Kalender-Ansicht in der Zeiterfassung (Web + MAUI)

Zeiteinträge können in einer Wochenkalender-Ansicht visualisiert werden.

PlattformImplementierungDetails
WebCalendarView.tsx (neu)Pure-React, kein Lib-Dependency. 24h × 48px Grid, absolute positioning nach Startminuten. Projektfarb-Palette (8 Farben). Prev/Next/Today Navigation.
MAUITimeCalendarPage.xaml + TimeCalendarViewModel.cs (neu)Syncfusion SfScheduler v32.1.22, View="Week". SchedulerAppointments mit Projektfarb-Palette.

Route MAUI: Constants.Routes.TimeCalendar = "timeCalendar". Registriert in AppShell.xaml.cs + MauiProgram.cs. Kalender-Button 📅 in TimeTrackingPage.xaml.

Web: Toggle List/Kalender in TimeTrackingPage.tsx Header, viewMode State.

P4 — Payments v2: Soll/Haben (Web + MAUI)

Das Zahlungsmodul wurde auf ein vollständiges Doppelkontomodell (Einnahmen/Ausgaben) erweitert:

KollektionModellZweck
paymentsPayment / LocalPaymentEinnahmen (Haben-Buchungen)
expensesExpense / LocalExpenseAusgaben (Soll-Buchungen)
incomingCategoriesIncomingCategory / LocalIncomingCategoryKategorien für Einnahmen
expenseCategoriesExpenseCategory / LocalExpenseCategoryKategorien für Ausgaben

MAUI — neue/geänderte Dateien

DateiÄnderung
Models/LocalModels.cs+ LocalExpense, LocalExpenseCategory, LocalIncomingCategory
Constants/AppConstants.cs+ Collections: Expenses, ExpenseCategories, IncomingCategories; Routes: ExpenseDetail, CategoryDetail
Services/DataService.cs+ GetExpensesAsync(), GetExpenseCategoriesAsync(), GetIncomingCategoriesAsync() + zugehörige Mapper + Einzel-Getter
Helpers/FirestoreFieldBuilder.cs+ ForExpense(), ForExpenseCategory(), ForIncomingCategory()
ViewModels/PaymentsViewModel.csKomplett neu: 4 Tabs (Einnahmen/Ausgaben/Berichte/Kategorien), Cashflow-Report, alle Navigation-Commands
ViewModels/ExpenseDetailViewModel.csNeu: CRUD für Ausgaben, Kategorie-Picker via SelectedCategory
ViewModels/CategoryDetailViewModel.csNeu: Shared ViewModel für Einnahmen- und Ausgaben-Kategorien (categoryType QueryProperty)
Views/PaymentsPage.xamlKomplett neu: 4-Tab-Layout via IndexEqualsConverter; Einnahmen/Ausgaben-Listen, KPI-Cards, Monats-Tabelle, Kategorie-Verwaltung
Views/ExpenseDetailPage.xamlNeu: Beschreibung, Betrag, Datum, Kategorie-Picker, Methode, Notizen
Views/CategoryDetailPage.xamlNeu: Name + Farbe (Hex) mit Live-BoxView-Preview

Web — geänderte Dateien

DateiÄnderung
shared/types/models.ts+ Expense, ExpenseCategory, IncomingCategory Interfaces
shared/hooks/usePayments.ts+ useExpenses(), useExpenseCategories() Hooks
features/payments/PaymentsPage.tsxKomplett neu: 4 Tabs, CategoryManager<T> generische Komponente, monatlicher Cashflow

Berichte-Logik (MAUI PaymentsViewModel.BuildReport())

// Letzte 6 Monate mit laufendem Saldo
for (int i = 5; i >= 0; i--) {
    var key  = now.AddMonths(-i).ToString("yyyy-MM");
    var inc  = payments.Where(p => p.Date.StartsWith(key)).Sum(p => p.Amount);
    var exp  = expenses.Where(e => e.Date.StartsWith(key)).Sum(e => e.Amount);
    running += inc - exp;
    rows.Add(new MonthlyReportRow(label, inc, exp, running));
}

Neue Features — v2.6 (F2 – F4)

Web-only Release. Alle Änderungen betreffen ausschließlich die React/Firebase Web-App.

F2 — Kunden-Steuerdaten

Kunden können jetzt UID-Nummer (USt-IdNr.) und Steuernummer hinterlegen. Diese erscheinen automatisch in der Adresszeile auf Rechnungen, Angeboten und Tätigkeitsberichten.

DateiÄnderung
shared/types/models.tsCustomer: + vatId?: string, taxNumber?: string
features/customers/CustomerForm.tsxNeuer Abschnitt „Steuerdaten" mit UID-Nummer + Steuernummer
features/invoices/InvoicePDF.tsxAdressblock zeigt customer.vatId und customer.taxNumber
features/quotes/QuotePDF.tsxAnalog InvoicePDF
features/activity-reports/ActivityReportPDF.tsxAnalog InvoicePDF

F3 — Firmen-Stammdaten (Kontakt, Steuer, Bank)

CompanySettings wurde um 9 Felder erweitert. Die Einstellungsseite hat drei neue Karten erhalten.

FeldTypVerwendung
emailstring?Header-Block + PDF-Footer
phonestring?Header-Block + PDF-Footer
websitestring?PDF-Footer
vatIdstring?Header-Block + PDF-Footer: UID: ATU…
taxIdstring?Einstellungen (kein PDF-Druck)
kleinunternehmerboolean?MwSt.-Zeile ersetzt durch Hinweis; § 6-Disclaimer im PDF
closingTextstring?Schlusstext auf allen PDFs nach Notizen
iban / bic / bankNamestring?Zahlungsinfo-Box auf Rechnung; PDF-Footer

Kleinunternehmer-Logik auf PDFs

const isKlein = company.kleinunternehmer === true;
// MwSt.-Zeile zeigt "Keine MwSt. (Kleinunternehmer)" statt Betrag
// Gesamtbetrag = totalNet (nicht totalGross)
// Disclaimer: "Gemäß § 6 Abs. 1 Z 27 UStG wird keine Umsatzsteuer berechnet."

Geändertes PDF-Footer-Format

Footer wird dynamisch aus allen ausgefüllten Feldern zusammengestellt (· getrennt):

Musterfirma GmbH · Musterstraße 1 · 1010 Wien · office@firma.at · +43 1 234 · UID: ATU12345678
DateiÄnderung
shared/types/models.tsCompanySettings + 9 neue Felder
shared/hooks/useSettings.tsDEFAULT_SETTINGS aktualisiert
features/settings/SettingsPage.tsxNeue Cards: Kontaktdaten, Steuer & Rechtliches, Bankverbindung
features/invoices/InvoicePDF.tsxDynamischer Footer, IBAN-Block, Kleinunternehmer-Support
features/quotes/QuotePDF.tsxAnalog InvoicePDF
features/activity-reports/ActivityReportPDF.tsxAnalog InvoicePDF

F4 — PDF Report Designer (Template-Auswahl + Akzentfarbe)

Benutzer können in den Einstellungen ein PDF-Layout und eine Akzentfarbe wählen. Das gewählte Design wird auf alle erzeugten PDFs angewendet.

Templates

TemplateBeschreibung
classic (Standard)Weißer Hintergrund, hellgrauer Tabellenkopf (#f0f0f0), schwarzer Text
modernFarbiger Header-Banner (volle Breite, Akzentfarbe), weißer Text im Header, farbiger Tabellenkopf
minimalKeine Hintergründe, nur Trennlinien unter Tabellenköpfen, sehr clean

Neue Felder in CompanySettings

FeldTypDefault
pdfTemplate'classic' | 'modern' | 'minimal''classic'
pdfAccentColorstring (Hex)'#4f46e5'

Architektur

DateiZweck
shared/lib/pdfTheme.tsZentraler Theme-Helper. getPdfTheme(company) gibt ein PdfTheme-Objekt zurück mit tableHeadBg, tableHeadColor, headerBg, accentLight, dividerColor etc.
features/settings/SettingsPage.tsxNeue Card „PDF Design": 3 Template-Buttons + nativer type="color" Picker
InvoicePDF / QuotePDF / ActivityReportPDFImportieren getPdfTheme(), wenden PdfTheme-Werte auf Styles an; modern rendert negativen Top-Margin Banner

Modern-Banner Implementierung

// Negativer Margin zieht den Banner bis zum Seiten­rand
modernBanner: {
  marginTop:  '-30mm',
  marginLeft: '-20mm',
  marginRight:'-20mm',
  padding:    '10mm 20mm 8mm 20mm',
  backgroundColor: theme.headerBg
}

TypeScript-Hinweis: @react-pdf/renderer akzeptiert kein false in Style-Arrays. Statt condition && {style} immer condition ? {style} : undefined oder ternär verwenden.

myBusiness Developer Documentation · v2.6 · April 2026 · Für internen Entwickler-Einsatz