Reliefo Therapy App Client
- Tests auf iPad und Android
- Google Termin Import
- Doku: Wartung, Angular Update, etc.
- Kundenname-Header als Component
- Quittung Detail Ansicht mit Download PDF Button
- Falsche URL -> 404 Seite mit Link zurück zur Startseite
- Statistiken
- Anzahl Kunden
- Anzahl Massagen pro Jahr
- Kunde erstellen
- Kunde bearbeiten
- Kunde löschen
- Termin erstellen
- Termin bearbeiten
- Termin löschen
- Quittung erstellen
- Quittung herunterladen
- Quittung löschen
- Massage erstellen
- Massage bearbeiten
- Massage löschen
- Krankengeschichte Eintrag erstellen
- Krankengeschichte Eintrag bearbeiten
- Krankengeschichte Eintrag löschen
- Statistik anzeigen
- Die App wird allgemein für Therapien entwickelt.
- Im konkreten Anwendungsfall sind das Massagen.
- Aus Gewohnheit werden Rechnungen als Quittungen bezeichnet, da sie dem Kunden nach der sofortigen Bezahlung ausgehändigt werden.
- Für den Benutzer sichtbar wird überall Massagen und Quittungen statt Therapien und Rechnungen verwendet. Im Hintergrund (Code, API) bleiben die neutraleren Begriffe Therapien und Rechnungen bestehen.
| Tabelle | NG Klasse | NG Interface | Bezeichnung (deutsch, für Benutzer sichtbar) |
|---|---|---|---|
| appointments | AppointmentsXy | Appointment | Termine |
| customers | CustomersXy | Customer | Kunden |
| medical_histories | MedicalHistoriesXy | MedicalHistory | Krankengeschichte |
| bills | BillsXy | Bill | Quittungen |
| therapies | TherapiesXy | Therapy | Massagen |
- Kunden
- Termine
- Neuer Termin
- Termin bearbeiten
- Termin löschen
- Quittungen
- Neue Quittung
- Quittung löschen
- Krankengeschichte
- Neuer KG Eintrag
- KG Eintrag bearbeiten
- KG Eintrag löschen
- Termine
- Termine
- Termin bearbeiten
- Termin löschen
- Quittungen
- Quittung löschen
- Massagen
- Neue Massage
- Neue Version
- Massage bearbeiten
- Massage löschen
/login /kunden /massagen /quittungen /start /termine
| Bereich | Farbe | HEX-Code | Beschreibung / Anwendung |
|---|---|---|---|
| Primär | Kupfer-Gold | #C28E58 | Hauptfarbe für Buttons, das Logo und wichtige Highlights. |
| Sekundär | Salbei-Grün | #A8C2B0 | Für die fließenden Linien, Statusanzeigen oder sanfte Akzente. |
| Hintergrund | Creme-Weiß | #F9F7F2 | Haupt-Hintergrund der App (wirkt weicher und edler als Reinweiß). |
| Oberflächen | Leinen-Beige | #E5DCD0 | Für Karten-Elemente (Cards), Trennlinien oder Eingabefelder. |
| Text | Dunkles Taupe | #4A433D | Für Texte; bietet hohen Kontrast bei natürlicherer Anmutung als Schwarz. |
Design-Empfehlungen für die Umsetzung:
- Primärfarbe (Brand): Ich empfehle den Kupfer-Gold-Ton (#C59467) deines Logos als Primärfarbe für wichtige Schaltflächen zu verwenden.
- Weißraum: Nutze das Warme Elfenbein großzügig. Es spiegelt die hellen Handtücher und das Licht im Bild wider und lässt die App „atmen".
- Abgerundete Ecken: Passend zum verspielten Logo und der weichen Polsterung der Liege sollten alle UI-Elemente (Buttons, Bilder) stark abgerundete Ecken (z. B. 16px oder mehr) haben.
- Schriftart: Eine serifenlose, leicht „puffy" wirkende Schriftart würde den verspielten Charakter des Logos im Interface fortführen.
This project was generated using Angular CLI version 21.1.2.
To start a local development server, run:
ng serveOnce the server is running, open your browser and navigate to http://localhost:4200/. The application will automatically reload whenever you modify any of the source files.
If ng serve produces a page with broken layout or styling, it's likely a cache issue. Try these steps:
-
Hard refresh the browser: Ctrl+Shift+R (Windows/Linux) or Cmd+Shift+R (Mac)
-
Clear Angular CLI cache:
rm -rf .angular
-
Restart
ng serve
Angular CLI includes powerful code scaffolding tools. To generate a new component, run:
ng generate component component-name
ng generate component component-name --skip-testsFor a complete list of available schematics (such as components, directives, or pipes), run:
ng generate --helpTo build the project run:
ng buildThis will compile your project and store the build artifacts in the dist/ directory. By default, the production build optimizes your application for performance and speed.
To execute unit tests with the Vitest test runner, use the following command:
ng testFor end-to-end (e2e) testing, run:
ng e2eAngular CLI does not come with an end-to-end testing framework by default. You can choose one that suits your needs.
For more information on using the Angular CLI, including detailed command references, visit the Angular CLI Overview and Command Reference page.
ng build --configuration production
rm -rf ../reliefo-api/wwwroot/*
cp -r dist/reliefo-client/browser/* ../reliefo-api/wwwroot/dateFormat attribute, the format specifier yy actually means a 4-digit year (e.g., 2026), not a 2-digit year as might be expected from other frameworks. This is counterintuitive but confirmed in PrimeNG's implementation.
dd.mm.yy→27.06.2026(4-digit year)- Input masks are also added to all datepickers for better UX:
- Date:
pInputMask="99.99.9999"(dd.mm.yyyy format) - Time:
pInputMask="99:99"(HH:mm format)
- Date:
export class CustomersList {
private readonly customerService = inject(CustomerService);
constructor() {
this.customerService.getAll().pipe(takeUntilDestroyed()).subscribe((data) => this.customers.set(data));
}
}
export class CustomersForm implements OnInit {
private readonly customerService = inject(CustomerService);
private readonly route = inject(ActivatedRoute);
ngOnInit(): void {
const idParam = this.route.snapshot.paramMap.get('id');
const id = idParam ? Number(idParam) : null;
if (id) {
this.customerId = id;
this.isNew.set(false);
this.customerService.getById(id).subscribe((customer) => {
this.form.patchValue(customer);
});
}
}
}export class CustomersDetail {
private readonly route = inject(ActivatedRoute);
private readonly customerService = inject(CustomerService);
customer = toSignal(
this.route.params.pipe(
switchMap(params => this.customerService.getById(+params['id']))
)
);
}export class AppointmentsList {
private readonly appointmentService = inject(AppointmentService);
protected readonly appointments = signal<Appointment[]>([]);
ngOnInit(): void {
this.appointmentService.getAll().subscribe((data) => this.appointments.set(data));
}
}| Ansatz | Wo | Lifecycle | Angular-Stil | Empfehlung |
|---|---|---|---|---|
constructor() |
Klassen-Konstruktor | Vor der Initialisierung | Klassisch | |
implements OnInit + ngOnInit() |
Lifecycle-Hook | Nach der Initialisierung | Klassisch | ✅ OK, aber veraltet |
toSignal() |
Klassenfeld | Reaktiv / deklarativ | Modern | ✅✅ Bevorzugt |
signal<T>([]) + ngOnInit() |
Hybrid | Nach der Initialisierung | Teilweise modern |
constructor() {
this.customerService.getAll().pipe(takeUntilDestroyed()).subscribe(...);
}Was passiert: Der Konstruktor wird aufgerufen, sobald Angular die Klasse instanziiert — noch bevor Inputs oder der DOM verfügbar sind.
takeUntilDestroyed() wird benötigt, weil das Observable sonst nach der Zerstörung der Komponente weiter feuern würde (Memory Leak). Im constructor() ist DestroyRef automatisch verfügbar.
Vorteile:
- Kein Interface (
OnInit) nötig takeUntilDestroyed()funktioniert ohne explizitenDestroyRef
Nachteile:
- Konstruktoren sollten keine Seiteneffekte haben (Best Practice)
- Inputs sind noch nicht gesetzt — gefährlich bei parameterabhängigem Laden
export class CustomersForm implements OnInit {
ngOnInit(): void {
this.customerService.getById(id).subscribe(...);
}
}Was passiert: ngOnInit() ist ein Angular Lifecycle-Hook, der nach dem Setzen der Inputs ausgeführt wird. implements OnInit ist ein TypeScript-Interface, das sicherstellt, dass die Methode korrekt implementiert ist.
implements OnInit ist rein ein TypeScript-Vertrag (Interface). Angular ruft ngOnInit() unabhängig davon auf — aber das Interface hilft dem Compiler, Tippfehler (ngOniNit) zu erkennen.
Vorteile:
- Inputs sind verfügbar
- Klar strukturiert und verständlich
Nachteile:
- Imperativ: man muss manuell abonnieren und den State setzen
- Erfordert manuelle Cleanup-Logik (
takeUntilDestroyed()oderunsubscribe)
export class CustomersDetail {
customer = toSignal(
this.route.params.pipe(
switchMap(params => this.customerService.getById(+params['id']))
)
);
}Was passiert: toSignal() konvertiert ein Observable direkt in ein Signal. Angular verwaltet das Abonnement automatisch — kein manuelles subscribe(), kein unsubscribe(), kein Lifecycle-Hook nötig.
Vorteile:
- Vollständig deklarativ — kein Boilerplate
- Automatisches Cleanup durch Angular
- Reaktiv: aktualisiert sich automatisch bei Route-Änderungen
- Kompatibel mit
ChangeDetectionStrategy.OnPush
Nachteile:
- Muss im Injection-Kontext aufgerufen werden (Klassenfeld oder
constructor) - Returntyp ist
Signal<T | undefined>(sofern keininitialValueangegeben)
protected readonly appointments = signal<Appointment[]>([]);
ngOnInit(): void {
this.appointmentService.getAll().subscribe((data) => this.appointments.set(data));
}Was passiert: Ein Signal wird manuell mit einem leeren Array initialisiert. In ngOnInit() wird das Observable abonniert und das Signal mit den Daten befüllt.
signal<Appointment[]>([]) erstellt ein beschreibbares Signal mit dem Typ Appointment[] und dem Startwert []. Der Typ wird explizit angegeben, weil TypeScript [] allein nicht als Appointment[] inferieren kann.
Vorteile:
- Template rendert sofort mit leerem Array (kein
undefined) - Vertraut für Entwickler, die Signals kennen aber Observables vermeiden wollen
Nachteile:
- Mischform: imperatives
subscribe()mit reaktivem Signal - Kein automatisches Cleanup — erfordert
takeUntilDestroyed() toSignal()macht dasselbe eleganter
In modernem Angular (v17+) ist toSignal() der bevorzugte Ansatz:
// Empfohlen: deklarativ, kein Boilerplate, automatisches Cleanup
readonly data = toSignal(this.myService.getAll(), { initialValue: [] });ngOnInit() bleibt sinnvoll, wenn Inputs vor dem Laden benötigt werden und toSignal() nicht passt. constructor() sollte für Datenladen vermieden werden.