init
This commit is contained in:
@@ -5,7 +5,7 @@ import { NotFoundComponent } from './core/components/not-found/not-found.compone
|
||||
|
||||
import { DefaultLayoutComponent } from './core/components/default-layout/default-layout.component';
|
||||
|
||||
import { DashboardPageComponent } from './features/dashboard/pages/dashboard-page/dashboard-page.component';
|
||||
import { DashboardPageComponent } from './features/dashboard/components/dashboard-page/dashboard-page.component';
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
|
||||
@@ -34,11 +34,13 @@ app-page-header {
|
||||
}
|
||||
app-sidebar {
|
||||
margin: 1rem;
|
||||
margin-top:0;
|
||||
grid-area: 2 / 1 / 6 / 2;
|
||||
}
|
||||
.main-content {
|
||||
background-color: var(--color-body-bg);
|
||||
margin: 1rem;
|
||||
border-radius: var(--border-radius-md) 0 0 0;
|
||||
background-color: var(--color-body-bg-lighter);
|
||||
padding: 1rem;
|
||||
grid-area: 2 / 2 / 6 / 6;
|
||||
overflow-y: auto; /* Fügt Scrollen hinzu, falls der Inhalt den Bereich übersteigt */
|
||||
}
|
||||
|
||||
9
src/app/core/models/dashboard.ts
Normal file
9
src/app/core/models/dashboard.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
|
||||
import { KpiColor } from "../types/dashboard";
|
||||
|
||||
export interface Kpi {
|
||||
value: string;
|
||||
label: string;
|
||||
color: KpiColor; // <-- Hier verwenden wir den importierten Typ
|
||||
iconName: string;
|
||||
}
|
||||
19
src/app/core/models/order.ts
Normal file
19
src/app/core/models/order.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { OrderStatus } from "../types/order";
|
||||
|
||||
export interface OrderUser {
|
||||
name: string;
|
||||
email: string;
|
||||
avatarUrl: string;
|
||||
}
|
||||
|
||||
export interface Order {
|
||||
id: string;
|
||||
user: OrderUser;
|
||||
amount: string;
|
||||
status: OrderStatus;
|
||||
}
|
||||
|
||||
export interface StatusOption {
|
||||
value: OrderStatus | 'all';
|
||||
label: string;
|
||||
}
|
||||
1
src/app/core/types/dashboard.ts
Normal file
1
src/app/core/types/dashboard.ts
Normal file
@@ -0,0 +1 @@
|
||||
export type KpiColor = 'blue' | 'green' | 'orange' | 'purple';
|
||||
1
src/app/core/types/order.ts
Normal file
1
src/app/core/types/order.ts
Normal file
@@ -0,0 +1 @@
|
||||
export type OrderStatus = 'completed' | 'processing' | 'cancelled' | 'info';
|
||||
1
src/app/core/types/status-pill.ts
Normal file
1
src/app/core/types/status-pill.ts
Normal file
@@ -0,0 +1 @@
|
||||
export type PillStatus = 'success' | 'warning' | 'danger' | 'info';
|
||||
@@ -0,0 +1,75 @@
|
||||
<main>
|
||||
<div class="grid">
|
||||
<app-kpi-card
|
||||
*ngFor="let kpi of kpiData"
|
||||
[value]="kpi.value"
|
||||
[label]="kpi.label"
|
||||
[color]="kpi.color"
|
||||
[iconName]="kpi.iconName"
|
||||
>
|
||||
</app-kpi-card>
|
||||
</div>
|
||||
<div>
|
||||
<app-orders-table
|
||||
[data]="mockOrders"
|
||||
[itemsPerPage]="5"
|
||||
(view)="handleViewDetails($event)"
|
||||
(edit)="handleEditOrder($event)"
|
||||
(delete)="handleDeleteOrder($event)"
|
||||
>
|
||||
</app-orders-table>
|
||||
</div>
|
||||
</main>
|
||||
Exzellente Frage! Ein gutes Dashboard ist der Schlüssel zu einem effizienten
|
||||
Admin-Panel. Es sollte auf einen Blick die wichtigsten Fragen beantworten: "Wie
|
||||
läuft mein Geschäft?" und "Was muss ich jetzt tun?". Basierend auf den
|
||||
Endpunkten und Datenmodellen, die in Ihrer Swagger-Datei verfügbar sind
|
||||
(insbesondere AdminAnalytics, AdminOrders etc.), hier eine Gliederung der
|
||||
Kerninformationen, von der höchsten zur niedrigsten Priorität: Kategorie 1:
|
||||
Handlungsbedarf – "Was muss ich jetzt tun?" Diese Sektion ist die wichtigste,
|
||||
denn sie treibt die tägliche Arbeit an. Sie sollte prominent platziert sein.
|
||||
Offene Bestellungen: Eine Zählung und eine Liste der letzten 3-5 Bestellungen
|
||||
mit dem Status Pending oder Processing. Jede Zeile sollte direkt zur Detailseite
|
||||
der Bestellung verlinken. Warum? Dies ist die primäre Aufgabe des
|
||||
Shop-Betreibers: Bestellungen bearbeiten und versenden. Datenquelle: GET
|
||||
/api/v1/admin/AdminOrders (clientseitig filtern). Niedriger Lagerbestand: Eine
|
||||
Warnung und eine Liste der Produkte, deren stockQuantity unter einen kritischen
|
||||
Schwellenwert gefallen ist. Warum? Verhindert "Out of Stock"-Situationen und
|
||||
Umsatzverluste. Ermöglicht rechtzeitige Nachbestellungen. Datenquelle: GET
|
||||
/api/v1/admin/AdminAnalytics -> inventoryStatus.productsWithLowStock. Zu
|
||||
genehmigende Bewertungen: Eine Zählung der Kundenbewertungen, die auf Freigabe
|
||||
warten. Warum? Fördert die Interaktion mit Kunden und stellt die Qualität der
|
||||
Inhalte sicher. Datenquelle: GET /api/v1/admin/AdminReviews (clientseitig nach
|
||||
isApproved === false filtern). Kategorie 2: Key Performance Indicators (KPIs) –
|
||||
"Wie läuft mein Geschäft?" Dies ist der "Auf einen Blick"-Überblick über die
|
||||
Gesundheit des Shops. Ideal als große, klare Zahlen am oberen Rand des
|
||||
Dashboards. Umsatz (letzte 30 Tage): Die wichtigste Kennzahl. Datenquelle: GET
|
||||
/api/v1/admin/AdminAnalytics -> kpiSummary.totalRevenue. Anzahl Bestellungen
|
||||
(letzte 30 Tage): Zeigt die allgemeine Aktivität. Datenquelle: GET
|
||||
/api/v1/admin/AdminAnalytics -> kpiSummary.totalOrders. Durchschnittlicher
|
||||
Bestellwert: Gibt Aufschluss über das Kaufverhalten. Datenquelle: GET
|
||||
/api/v1/admin/AdminAnalytics -> kpiSummary.averageOrderValue. Neukunden (letzte
|
||||
30 Tage): Eine wichtige Metrik für das Wachstum. Datenquelle: GET
|
||||
/api/v1/admin/AdminAnalytics -> kpiSummary.newCustomersThisPeriod. Kategorie 3:
|
||||
Jüngste Aktivitäten & Trends – "Was passiert gerade?" Diese Sektion gibt ein
|
||||
Gefühl für die aktuelle Dynamik und hilft, Muster zu erkennen. Umsatzverlauf
|
||||
(Diagramm): Ein einfaches Linien- oder Balkendiagramm, das den Umsatz der
|
||||
letzten 7 oder 30 Tage anzeigt. Warum? Visualisiert Trends, Hochs und Tiefs
|
||||
(z.B. an Wochenenden). Datenquelle: GET /api/v1/admin/AdminAnalytics ->
|
||||
salesOverTime. Letzte Bestellungen (Feed): Eine kurze, scrollbare Liste der
|
||||
letzten 10 Bestellungen (unabhängig vom Status) mit Kundenname und Bestellwert.
|
||||
Warum? Gibt dem Dashboard ein "lebendiges" Gefühl. Datenquelle: GET
|
||||
/api/v1/admin/AdminOrders. Kategorie 4: Strategische Einblicke – "Was
|
||||
funktioniert gut?" Diese Informationen helfen bei Marketing- und
|
||||
Bestandsentscheidungen. Top 5 Produkte: Eine einfache Liste der meistverkauften
|
||||
Produkte im aktuellen Zeitraum (nach Umsatz oder verkauften Einheiten). Warum?
|
||||
Zeigt, welche Produkte beworben werden sollten und wo der Lagerbestand immer
|
||||
hoch sein muss. Datenquelle: GET /api/v1/admin/AdminAnalytics ->
|
||||
topPerformingProducts. Vorschlag für ein Dashboard-Layout: Ein bewährtes Layout
|
||||
könnte so aussehen: Obere Reihe: Die 4 großen KPI-Karten (Umsatz, Bestellungen,
|
||||
Ø-Wert, Neukunden). Linke (breitere) Spalte: Ganz oben das
|
||||
Umsatzverlauf-Diagramm. Darunter die Liste der "Letzten Bestellungen". Rechte
|
||||
(schmalere) Spalte: Ganz oben die "Handlungsbedarf"-Box mit den offenen
|
||||
Bestellungen und dem niedrigen Lagerbestand. Darunter die "Top 5
|
||||
Produkte"-Liste. Mit dieser Auswahl an Informationen hat der Admin alles
|
||||
Wichtige im Blick und kann direkt die notwendigen Aktionen ausführen.
|
||||
@@ -0,0 +1,85 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { KpiCardComponent } from '../kpi-card/kpi-card.component';
|
||||
import { OrdersTableComponent } from '../../../../shared/components/data-display/orders-table/orders-table.component';
|
||||
import { Kpi } from '../../../../core/models/dashboard';
|
||||
import { Order } from '../../../../core/models/order';
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'app-dashboard-page',
|
||||
imports: [CommonModule,KpiCardComponent,OrdersTableComponent],
|
||||
templateUrl: './dashboard-page.component.html',
|
||||
styleUrl: './dashboard-page.component.css'
|
||||
})
|
||||
export class DashboardPageComponent {
|
||||
|
||||
|
||||
kpiData: Kpi[] = [
|
||||
{
|
||||
value: '€ 14.750',
|
||||
label: 'Umsatz',
|
||||
color: 'green',
|
||||
iconName: 'placeholder',
|
||||
},
|
||||
{
|
||||
value: '1.284',
|
||||
label: 'Neue Nutzer',
|
||||
color: 'blue',
|
||||
iconName: 'placeholder',
|
||||
},
|
||||
{
|
||||
value: '312',
|
||||
label: 'Bestellungen',
|
||||
color: 'orange',
|
||||
iconName: 'placeholder',
|
||||
},
|
||||
{
|
||||
value: '99.8%',
|
||||
label: 'Verfügbarkeit',
|
||||
color: 'purple',
|
||||
iconName: 'placeholder',
|
||||
},
|
||||
];
|
||||
|
||||
mockOrders: Order[] = [
|
||||
{
|
||||
id: 'a2d4b',
|
||||
user: { name: 'Max Mustermann', email: 'max@test.de', avatarUrl: 'https://i.pravatar.cc/150?u=max' },
|
||||
amount: '€129.99',
|
||||
status: 'completed', // NEU: Sprechender Status
|
||||
},
|
||||
{
|
||||
id: 'f8e9c',
|
||||
user: { name: 'Erika Musterfrau', email: 'erika@test.de', avatarUrl: 'https://i.pravatar.cc/150?u=erika' },
|
||||
amount: '€49.50',
|
||||
status: 'processing', // NEU: Sprechender Status
|
||||
},
|
||||
{
|
||||
id: 'h1g3j',
|
||||
user: { name: 'John Doe', email: 'john.d@test.com', avatarUrl: 'https://i.pravatar.cc/150?u=john' },
|
||||
amount: '€87.00',
|
||||
status: 'cancelled', // NEU: Sprechender Status
|
||||
},
|
||||
{
|
||||
id: 'h1g3j',
|
||||
user: { name: 'John Doe', email: 'john.d@test.com', avatarUrl: 'https://i.pravatar.cc/150?u=john' },
|
||||
amount: '€87.00',
|
||||
status: 'info', // NEU: Sprechender Status
|
||||
},
|
||||
];
|
||||
|
||||
handleDeleteOrder(orderId: string): void {
|
||||
console.log('Lösche Bestellung mit ID:', orderId);
|
||||
// Hier könnten Sie z.B. einen Bestätigungs-Dialog öffnen
|
||||
}
|
||||
handleViewDetails(orderId: string): void {
|
||||
console.log('View Bestellung mit ID:', orderId);
|
||||
// Hier könnten Sie z.B. einen Bestätigungs-Dialog öffnen
|
||||
}
|
||||
handleEditOrder(orderId: string): void {
|
||||
console.log('Edit Bestellung mit ID:', orderId);
|
||||
// Hier könnten Sie z.B. einen Bestätigungs-Dialog öffnen
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { IconComponent } from '../../ui/icon/icon.component';
|
||||
import { CardComponent } from '../../ui/card/card.component';
|
||||
import { IconComponent } from '../../../../shared/components/ui/icon/icon.component';
|
||||
import { CardComponent } from '../../../../shared/components/ui/card/card.component';
|
||||
|
||||
export type KpiColor = 'blue' | 'green' | 'orange' | 'purple';
|
||||
import { KpiColor } from '../../../../core/types/dashboard';
|
||||
|
||||
@Component({
|
||||
selector: 'app-kpi-card',
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Routes } from '@angular/router';
|
||||
import { DashboardPageComponent } from './pages/dashboard-page/dashboard-page.component';
|
||||
import { DashboardPageComponent } from './components/dashboard-page/dashboard-page.component';
|
||||
// Importiere dein spezielles Layout für Auth-Seiten und alle Komponenten
|
||||
|
||||
export const DASHBOARD_ROUTES: Routes = [
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
<p>dashboard-page works!</p>
|
||||
<p>
|
||||
Key Performance Indicators (KPIs) – Das "Big Picture" Dies sind die
|
||||
wichtigsten Kennzahlen, die den Geschäftserfolg auf einen Blick zeigen. Sie
|
||||
sollten prominent ganz oben platziert werden. Alle diese Daten stammen direkt
|
||||
aus dem KpiSummaryDto, das Sie vom /api/v1/admin/AdminAnalytics-Endpunkt
|
||||
bekommen. Gesamtumsatz (totalRevenue): Die wichtigste Kennzahl. Zeigt, wie
|
||||
viel Geld der Shop einnimmt. Anzahl der Bestellungen (totalOrders): Zeigt die
|
||||
Aktivität im Shop. Durchschnittlicher Bestellwert (averageOrderValue): Hilft
|
||||
zu beurteilen, ob Kunden tendenziell mehr oder weniger pro Einkauf ausgeben.
|
||||
Anzahl der Neukunden (newCustomersThisPeriod): Wichtig für das Wachstum.
|
||||
Zeigt, wie viele neue Kunden im gewählten Zeitraum registriert wurden.
|
||||
Darstellung: Am besten als einzelne, große "KPI-Karten", wie wir es im
|
||||
vorherigen Beispiel hatten.
|
||||
</p>
|
||||
|
||||
<br />
|
||||
<p>
|
||||
Handlungsrelevante Aufgaben – "Was muss ich jetzt tun?" Das ist der vielleicht
|
||||
wichtigste Teil des Dashboards. Er sollte dem Admin direkt zeigen, wo seine
|
||||
Aufmerksamkeit benötigt wird. Bestellungen zur Bearbeitung: Eine Zählung und
|
||||
eine Liste der letzten Bestellungen mit dem Status Pending oder Processing.
|
||||
Dies signalisiert "Hier muss etwas verpackt und versendet werden!".
|
||||
API-Endpunkt: GET /api/v1/admin/AdminOrders – Sie müssten die zurückgegebene
|
||||
Liste clientseitig nach dem Status filtern. Produkte mit niedrigem
|
||||
Lagerbestand: Eine Warnung oder eine Liste von Produkten, deren stockQuantity
|
||||
unter einen bestimmten Schwellenwert fällt. API-Endpunkt: Das
|
||||
InventoryStatusDto aus dem AdminAnalytics-Endpunkt liefert Ihnen direkt die
|
||||
Anzahl (productsWithLowStock). Zu genehmigende Bewertungen (Reviews): Falls
|
||||
Sie ein Freigabeverfahren für Kundenbewertungen haben, ist eine Anzeige wie "3
|
||||
neue Bewertungen warten auf Freigabe" extrem hilfreich. API-Endpunkt: GET
|
||||
/api/v1/admin/AdminReviews – Sie müssten hier eine Zählung der noch nicht
|
||||
genehmigten Reviews durchführen. Darstellung: Als auffällige
|
||||
Benachrichtigungs-Boxen oder "To-Do"-Listen.
|
||||
</p>
|
||||
<br />
|
||||
<p>
|
||||
Jüngste Aktivitäten – "Was passiert gerade im Shop?" Diese Sektion gibt dem
|
||||
Admin ein Gefühl für die aktuelle Dynamik im Shop. Letzte Bestellungen: Eine
|
||||
kurze Liste der 5-10 neuesten Bestellungen, unabhängig vom Status. Zeigt den
|
||||
Namen des Kunden, den Bestellwert und das Datum. API-Endpunkt: GET
|
||||
/api/v1/admin/AdminOrders. Umsatzverlauf der letzten 30 Tage: Ein einfaches
|
||||
Linien- oder Balkendiagramm, das den täglichen Umsatz anzeigt. API-Endpunkt:
|
||||
Die salesOverTime-Daten aus dem AdminAnalytics-Endpunkt sind perfekt dafür
|
||||
gemacht.
|
||||
</p>
|
||||
<br />
|
||||
<p>
|
||||
Top-Listen – "Was läuft am besten?" Diese Informationen sind strategisch
|
||||
wertvoll, um zu wissen, welche Produkte populär sind. Top 5 meistverkaufte
|
||||
Produkte: Eine einfache Tabelle mit den Produkten, die den meisten Umsatz
|
||||
generieren oder am häufigsten verkauft wurden. API-Endpunkt: Die Liste
|
||||
topPerformingProducts aus dem AdminAnalytics-Endpunkt liefert Ihnen genau
|
||||
diese Daten.
|
||||
</p>
|
||||
<br />
|
||||
@@ -1,11 +0,0 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-dashboard-page',
|
||||
imports: [],
|
||||
templateUrl: './dashboard-page.component.html',
|
||||
styleUrl: './dashboard-page.component.css'
|
||||
})
|
||||
export class DashboardPageComponent {
|
||||
|
||||
}
|
||||
@@ -9,13 +9,14 @@ import { DOCUMENT } from '@angular/common';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CardComponent } from '../../../../shared/components/ui/card/card.component';
|
||||
// Wir müssen KpiColor hier importieren, um es als Typ verwenden zu können
|
||||
import {
|
||||
KpiCardComponent,
|
||||
KpiColor,
|
||||
} from '../../../../shared/components/data-display/kpi-card/kpi-card.component';
|
||||
import { KpiCardComponent } from '../../../dashboard/components/kpi-card/kpi-card.component';
|
||||
|
||||
import { KpiColor } from '../../../../core/types/dashboard';
|
||||
|
||||
|
||||
import {
|
||||
OrdersTableComponent,
|
||||
Order,
|
||||
|
||||
} from '../../../../shared/components/data-display/orders-table/orders-table.component';
|
||||
import { FormFieldComponent } from '../../../../shared/components/form/form-field/form-field.component';
|
||||
import { FormGroupComponent } from '../../../../shared/components/form/form-group/form-group.component';
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Routes } from '@angular/router';
|
||||
|
||||
import { Demo1Component } from './components/demo1/demo1.component';
|
||||
import { Demo2Component } from './components/demo2/demo2.component';
|
||||
// import { Demo2Component } from './components/demo2/demo2.component';
|
||||
|
||||
export const DEMO_ROUTES: Routes = [
|
||||
{
|
||||
@@ -17,12 +17,12 @@ export const DEMO_ROUTES: Routes = [
|
||||
component: Demo1Component,
|
||||
title: 'Demo1',
|
||||
},
|
||||
{
|
||||
// Diese Route passt auf '/demo/1' und lädt die Komponente genau einmal.
|
||||
path: '2',
|
||||
component: Demo2Component,
|
||||
title: 'Demo2',
|
||||
},
|
||||
// {
|
||||
// // Diese Route passt auf '/demo/1' und lädt die Komponente genau einmal.
|
||||
// path: '2',
|
||||
// component: Demo2Component,
|
||||
// title: 'Demo2',
|
||||
// },
|
||||
// Hier könntest du weitere Routen wie '2', '3' etc. hinzufügen,
|
||||
// die andere Komponenten laden.
|
||||
// { path: '2', component: AnotherDemoComponent },
|
||||
|
||||
@@ -88,7 +88,7 @@
|
||||
}
|
||||
|
||||
.mono {
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
font-family: "Courier New", Courier, monospace;
|
||||
}
|
||||
|
||||
.actions-cell {
|
||||
@@ -102,3 +102,39 @@
|
||||
padding: 2rem;
|
||||
color: var(--color-text-light);
|
||||
}
|
||||
|
||||
.table-header {
|
||||
/* display: flex; */
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.table-header app-search-bar {
|
||||
flex-grow: 1; /* Lässt die Suchleiste den verfügbaren Platz einnehmen */
|
||||
/* max-width: 400px; Verhindert, dass sie zu breit wird */
|
||||
}
|
||||
|
||||
.order-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
|
||||
.order-searchbar {
|
||||
}
|
||||
|
||||
.order-filter {
|
||||
margin-left: auto;
|
||||
grid-area: 1 / 2 / 2 / 5;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.order-searchbar {
|
||||
grid-area: 1 / 1 / 2 / 3;
|
||||
}
|
||||
|
||||
.order-filter {
|
||||
margin-left: auto;
|
||||
grid-area: 1 / 3 / 2 / 5;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,19 @@
|
||||
<div class="order-grid table-header">
|
||||
<app-search-bar class="order-searchbar"
|
||||
placeholder="Kunde oder Bestell-ID..."
|
||||
(search)="onSearchChange($event)"
|
||||
></app-search-bar>
|
||||
<div class="order-filter">
|
||||
<app-status-pill
|
||||
*ngFor="let option of statusOptions"
|
||||
[class.active]="selectedStatus === 'all'"
|
||||
(click)="onStatusChange(option.value)"
|
||||
>
|
||||
{{ option.label }}
|
||||
</app-status-pill>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-container">
|
||||
<table class="modern-table">
|
||||
<thead>
|
||||
@@ -28,9 +44,9 @@
|
||||
<span class="mono">#{{ order.id }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<app-status-pill [status]="order.status">{{
|
||||
order.statusText
|
||||
}}</app-status-pill>
|
||||
<app-status-pill [status]="order.status">
|
||||
{{ order.status }}</app-status-pill
|
||||
>
|
||||
</td>
|
||||
<td class="amount">{{ order.amount }}</td>
|
||||
<td class="actions-cell text-right">
|
||||
@@ -66,7 +82,7 @@
|
||||
<div>
|
||||
<app-paginator
|
||||
[currentPage]="currentPage"
|
||||
[totalItems]="data.length"
|
||||
[totalItems]="filteredData.length"
|
||||
[itemsPerPage]="itemsPerPage"
|
||||
(pageChange)="onPageChange($event)"
|
||||
>
|
||||
|
||||
@@ -1,23 +1,19 @@
|
||||
import { Component, Input, Output, EventEmitter, SimpleChanges } from '@angular/core';
|
||||
import {
|
||||
Component,
|
||||
Input,
|
||||
Output,
|
||||
EventEmitter,
|
||||
SimpleChanges,
|
||||
} from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { StatusPillComponent } from '../../ui/status-pill/status-pill.component';
|
||||
import { ButtonComponent } from '../../ui/button/button.component';
|
||||
import { PaginatorComponent } from '../paginator/paginator.component';
|
||||
|
||||
// Interfaces für die Datenstruktur
|
||||
export interface OrderUser {
|
||||
name: string;
|
||||
email: string;
|
||||
avatarUrl: string;
|
||||
}
|
||||
|
||||
export interface Order {
|
||||
id: string;
|
||||
user: OrderUser;
|
||||
amount: string;
|
||||
status: 'success' | 'warning' | 'danger';
|
||||
statusText: string;
|
||||
}
|
||||
import { Order } from '../../../../core/models/order';
|
||||
import { StatusOption } from '../../../../core/models/order';
|
||||
import { OrderStatus } from '../../../../core/types/order';
|
||||
import { SearchBarComponent } from '../../layout/search-bar/search-bar.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-orders-table',
|
||||
@@ -26,42 +22,100 @@ export interface Order {
|
||||
CommonModule,
|
||||
StatusPillComponent,
|
||||
ButtonComponent,
|
||||
PaginatorComponent
|
||||
PaginatorComponent,
|
||||
SearchBarComponent,
|
||||
],
|
||||
templateUrl: './orders-table.component.html',
|
||||
styleUrl: './orders-table.component.css'
|
||||
styleUrl: './orders-table.component.css',
|
||||
})
|
||||
export class OrdersTableComponent {
|
||||
// Nimmt die anzuzeigenden Bestelldaten entgegen
|
||||
@Input() data: Order[] = [];
|
||||
@Input() itemsPerPage = 5;
|
||||
|
||||
currentPage = 1;
|
||||
displayedOrders: Order[] = [];
|
||||
|
||||
// Gibt Events für die Aktionen aus, mit der ID der betroffenen Bestellung
|
||||
@Output() view = new EventEmitter<string>();
|
||||
@Output() edit = new EventEmitter<string>();
|
||||
@Output() delete = new EventEmitter<string>();
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
// Wenn sich die Eingabedaten (allOrders) ändern, aktualisieren wir die Ansicht
|
||||
public searchTerm = '';
|
||||
public selectedStatus: OrderStatus | 'all' = 'all';
|
||||
|
||||
public statusOptions: StatusOption[] = [
|
||||
{ value: 'all', label: 'Alle' },
|
||||
{ value: 'info', label: 'Info' },
|
||||
{ value: 'completed', label: 'Abgeschlossen' },
|
||||
{ value: 'processing', label: 'In Bearbeitung' },
|
||||
{ value: 'cancelled', label: 'Storniert' },
|
||||
];
|
||||
|
||||
public filteredData: Order[] = [];
|
||||
public displayedOrders: Order[] = [];
|
||||
public currentPage = 1;
|
||||
|
||||
private statusTextMap = new Map<OrderStatus, string>([
|
||||
['completed', 'Abgeschlossen'],
|
||||
['processing', 'In Bearbeitung'],
|
||||
['cancelled', 'Storniert'],
|
||||
['info', 'Info'],
|
||||
]);
|
||||
|
||||
ngOnInit(): void {
|
||||
this.applyFiltersAndPagination();
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (changes['data']) {
|
||||
this.updateDisplayedOrders();
|
||||
this.applyFiltersAndPagination();
|
||||
}
|
||||
}
|
||||
|
||||
// Wird aufgerufen, wenn der Paginator die Seite wechselt
|
||||
// Called on input from the search field
|
||||
onSearchChange(term: string): void {
|
||||
this.searchTerm = term;
|
||||
this.applyFiltersAndPagination();
|
||||
}
|
||||
|
||||
// Called when a status pill is clicked
|
||||
onStatusChange(status: OrderStatus | 'all'): void {
|
||||
this.selectedStatus = status;
|
||||
this.applyFiltersAndPagination();
|
||||
}
|
||||
|
||||
onPageChange(newPage: number): void {
|
||||
this.currentPage = newPage;
|
||||
this.updateDisplayedOrders();
|
||||
this.applyFiltersAndPagination();
|
||||
}
|
||||
|
||||
// Diese Methode berechnet, welcher Teil der Daten angezeigt werden soll
|
||||
private updateDisplayedOrders(): void {
|
||||
private applyFiltersAndPagination(): void {
|
||||
let processedData = [...this.data];
|
||||
|
||||
if (this.selectedStatus !== 'all') {
|
||||
processedData = processedData.filter(
|
||||
(order) => order.status === this.selectedStatus
|
||||
);
|
||||
}
|
||||
|
||||
if (this.searchTerm) {
|
||||
const lowerCaseSearchTerm = this.searchTerm.toLowerCase();
|
||||
processedData = processedData.filter((order) => {
|
||||
// Holt den deutschen Status-Text für die aktuelle Bestellung
|
||||
const statusText = this.statusTextMap.get(order.status) || '';
|
||||
|
||||
return (
|
||||
order.user.name.toLowerCase().includes(lowerCaseSearchTerm) ||
|
||||
order.user.email.toLowerCase().includes(lowerCaseSearchTerm) ||
|
||||
order.id.toLowerCase().includes(lowerCaseSearchTerm) ||
|
||||
// === NEU: Suche im Status-Text ===
|
||||
statusText.toLowerCase().includes(lowerCaseSearchTerm)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
this.filteredData = processedData;
|
||||
|
||||
const startIndex = (this.currentPage - 1) * this.itemsPerPage;
|
||||
const endIndex = startIndex + this.itemsPerPage;
|
||||
this.displayedOrders = this.data.slice(startIndex, endIndex);
|
||||
this.displayedOrders = this.filteredData.slice(startIndex, endIndex);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
AfterViewInit,
|
||||
} from '@angular/core';
|
||||
import { CommonModule, DOCUMENT, isPlatformBrowser } from '@angular/common';
|
||||
import { SearchBarComponent } from '../search-bar/search-bar.component';
|
||||
import { UserProfileComponent } from '../user-profile/user-profile.component';
|
||||
import { SlideToggleComponent } from '../../form/slide-toggle/slide-toggle.component';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
@@ -17,7 +16,6 @@ import { FormsModule } from '@angular/forms';
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
SearchBarComponent,
|
||||
UserProfileComponent,
|
||||
SlideToggleComponent,
|
||||
FormsModule,
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
.search-bar {
|
||||
position: relative;
|
||||
width: 300px;
|
||||
width: 100%
|
||||
}
|
||||
.search-bar svg {
|
||||
position: absolute;
|
||||
|
||||
@@ -3,5 +3,5 @@
|
||||
<circle cx="11" cy="11" r="8"></circle>
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
||||
</svg>
|
||||
<input type="text" placeholder="Dashboard durchsuchen..." (input)="onSearch($event)" />
|
||||
<input type="text" placeholder="Durchsuchen..." (input)="onSearch($event)" />
|
||||
</div>
|
||||
@@ -1,6 +1,6 @@
|
||||
/* Globale Variablen für dieses Bauteil */
|
||||
:host {
|
||||
--sidebar-width-expanded: 280px;
|
||||
--sidebar-width-expanded: 200px;
|
||||
--sidebar-width-collapsed: 96px;
|
||||
--sidebar-padding: 1rem;
|
||||
--sidebar-margin: 1rem;
|
||||
@@ -15,9 +15,11 @@
|
||||
flex-direction: column;
|
||||
gap: 1.5rem; /* Reduzierter Abstand */
|
||||
width: var(--sidebar-width-expanded);
|
||||
height: calc(100vh - 2rem - 46px);
|
||||
height: calc(100vh - 4rem - 46px);
|
||||
|
||||
padding: var(--sidebar-padding);
|
||||
/* border-right: 1px solid var(--color-border); */
|
||||
padding-top:0;
|
||||
/* border: 1px solid var(--color-border); */
|
||||
/* border-radius: var(--border-radius-md); */
|
||||
transition: width var(--transition-speed) var(--transition-ease);
|
||||
/* background-color: var(--color-surface); */
|
||||
@@ -49,12 +51,13 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
.nav-item:hover {
|
||||
background-color: var(--color-body-bg-hover);
|
||||
background-color: var(--color-surface);
|
||||
color: var(--color-text);
|
||||
}
|
||||
.nav-item.active {
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
background: var(--color-body-bg-active);
|
||||
color: var(--color-text);
|
||||
|
||||
box-shadow: var(--box-shadow-sm);
|
||||
}
|
||||
.nav-item app-icon {
|
||||
@@ -94,8 +97,8 @@
|
||||
|
||||
/* 4. Zustandsabhängige Hintergrundfarben für das Icon */
|
||||
.nav-item:hover app-icon {
|
||||
background-color: var(--color-body-bg-hover);
|
||||
background: transparent;
|
||||
}
|
||||
.nav-item.active app-icon {
|
||||
background: var(--color-primary);
|
||||
background: transparent;
|
||||
}
|
||||
@@ -28,6 +28,4 @@
|
||||
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
|
||||
</aside>
|
||||
|
||||
@@ -1,29 +1,66 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Component, Inject, PLATFORM_ID, OnInit } from '@angular/core';
|
||||
import { CommonModule, isPlatformBrowser } from '@angular/common';
|
||||
import { IconComponent } from '../../ui/icon/icon.component';
|
||||
import { ButtonComponent } from '../../ui/button/button.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-sidebar',
|
||||
standalone: true,
|
||||
imports: [CommonModule, IconComponent,ButtonComponent],
|
||||
imports: [CommonModule, IconComponent],
|
||||
templateUrl: './sidebar.component.html',
|
||||
styleUrl: './sidebar.component.css'
|
||||
})
|
||||
export class SidebarComponent {
|
||||
// Dummy-Eigenschaft für die aktive Route, damit der Code funktioniert
|
||||
export class SidebarComponent implements OnInit { // 1. OnInit implementieren
|
||||
// Key für localStorage, genau wie beim Dark Mode
|
||||
private readonly sidebarCollapsedKey = 'app-sidebar-collapsed-setting';
|
||||
|
||||
// Dummy-Eigenschaft für die aktive Route
|
||||
activeRoute = 'dashboard';
|
||||
|
||||
// NEU: Eigenschaft, um den Zustand der Sidebar zu speichern
|
||||
// Der Standardwert ist 'false' (aufgeklappt), wird aber sofort überschrieben
|
||||
public isCollapsed = false;
|
||||
|
||||
// Dummy-Methode, damit der Code funktioniert
|
||||
// 2. PLATFORM_ID injizieren, um localStorage sicher zu verwenden
|
||||
constructor(@Inject(PLATFORM_ID) private platformId: Object) {}
|
||||
|
||||
// 3. Beim Start der Komponente den gespeicherten Zustand laden
|
||||
ngOnInit(): void {
|
||||
this.loadCollapsedState();
|
||||
}
|
||||
|
||||
// Dummy-Methode
|
||||
setActive(route: string): void {
|
||||
this.activeRoute = route;
|
||||
}
|
||||
|
||||
// NEU: Methode, um den Zustand umzuschalten
|
||||
// 4. Die Umschalt-Methode aktualisieren, damit sie den neuen Zustand speichert
|
||||
toggleSidebar(): void {
|
||||
// Zuerst den Zustand ändern
|
||||
this.isCollapsed = !this.isCollapsed;
|
||||
// Dann den neuen Zustand speichern
|
||||
this.saveCollapsedState();
|
||||
}
|
||||
|
||||
// 5. Methode zum Laden des Zustands (kopiert vom Dark-Mode-Muster)
|
||||
private loadCollapsedState(): void {
|
||||
if (isPlatformBrowser(this.platformId)) {
|
||||
try {
|
||||
const storedValue = localStorage.getItem(this.sidebarCollapsedKey);
|
||||
// Setze den Zustand der Komponente basierend auf dem gespeicherten Wert
|
||||
this.isCollapsed = storedValue === 'true';
|
||||
} catch (e) {
|
||||
console.error('Could not access localStorage for sidebar state:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Methode zum Speichern des Zustands (kopiert vom Dark-Mode-Muster)
|
||||
private saveCollapsedState(): void {
|
||||
if (isPlatformBrowser(this.platformId)) {
|
||||
try {
|
||||
localStorage.setItem(this.sidebarCollapsedKey, String(this.isCollapsed));
|
||||
} catch (e) {
|
||||
console.error('Could not write to localStorage for sidebar state:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,3 @@
|
||||
<span
|
||||
class="status-pill"
|
||||
[ngClass]="{
|
||||
'pill-success': status === 'success',
|
||||
'pill-warning': status === 'warning',
|
||||
'pill-danger': status === 'danger',
|
||||
'pill-info': status === 'info'
|
||||
}">
|
||||
<!-- Der Text für die Pille wird von außen über ng-content eingefügt -->
|
||||
<ng-content></ng-content>
|
||||
</span>
|
||||
<div class="status-pill" [ngClass]="cssClass">
|
||||
{{ displayText }}
|
||||
</div>
|
||||
@@ -1,16 +1,34 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
// Definiert die erlaubten Status-Typen für Typsicherheit
|
||||
type PillStatus = 'success' | 'warning' | 'danger' | 'info';
|
||||
import { Component, Input, OnChanges } from '@angular/core';
|
||||
import { CommonModule, NgClass } from '@angular/common';
|
||||
import { OrderStatus } from '../../../../core/types/order';
|
||||
|
||||
@Component({
|
||||
selector: 'app-status-pill',
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
imports: [CommonModule, NgClass],
|
||||
templateUrl: './status-pill.component.html',
|
||||
styleUrl: './status-pill.component.css'
|
||||
})
|
||||
export class StatusPillComponent {
|
||||
@Input() status: PillStatus = 'info';
|
||||
export class StatusPillComponent implements OnChanges {
|
||||
// Nimmt jetzt den neuen, sprechenden Status entgegen
|
||||
@Input() status: OrderStatus = 'info';
|
||||
|
||||
// Diese Eigenschaften werden vom Template verwendet
|
||||
public displayText = '';
|
||||
public cssClass = '';
|
||||
|
||||
// Eine Map, die Statusnamen auf Text und CSS-Klasse abbildet
|
||||
private statusMap = new Map<OrderStatus, { text: string, css: string }>([
|
||||
['completed', { text: 'Abgeschlossen', css: 'pill-success' }],
|
||||
['processing', { text: 'In Bearbeitung', css: 'pill-warning' }],
|
||||
['cancelled', { text: 'Storniert', css: 'pill-danger' }],
|
||||
['info', { text: 'Info', css: 'pill-info' }]
|
||||
]);
|
||||
|
||||
ngOnChanges(): void {
|
||||
// Wenn sich der Input-Status ändert, aktualisieren wir Text und Klasse
|
||||
const details = this.statusMap.get(this.status) || this.statusMap.get('info')!;
|
||||
this.displayText = details.text;
|
||||
this.cssClass = details.css;
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,8 @@
|
||||
/* Neutrale Farben (Light Mode) */
|
||||
--color-text: #2c3e50;
|
||||
--color-text-light: #7f8c8d;
|
||||
--color-body-bg: #f4f7fa;
|
||||
--color-body-bg: rgb(243, 243, 243);
|
||||
--color-body-bg-lighter: #ffffff;
|
||||
--color-body-bg-active: rgb(238, 238, 240);
|
||||
--color-body-bg-hover: #f2f3f5;
|
||||
--color-surface: #ffffff;
|
||||
@@ -41,11 +42,32 @@
|
||||
}
|
||||
|
||||
body.dark-theme {
|
||||
/* Textfarben werden heller */
|
||||
--color-text: #ecf0f1;
|
||||
--color-text-light: #95a5a6;
|
||||
|
||||
/*
|
||||
* Die Hierarchie der Hintergrundfarben wird umgekehrt:
|
||||
* Was im Light Mode hell war, ist jetzt dunkel.
|
||||
* Was leicht abgedunkelt war (hover, active), wird jetzt leicht aufgehellt.
|
||||
*/
|
||||
|
||||
/* Der Hintergrund für den Body (am dunkelsten) */
|
||||
--color-body-bg: #1a202c;
|
||||
--color-body-bg-hover: rgb(34, 41, 56);
|
||||
|
||||
/* Der Hintergrund für Elemente, die darauf liegen (etwas heller) */
|
||||
--color-surface: #2d3748;
|
||||
|
||||
/* NEU: Die "hellste" Hintergrundfarbe aus dem Light Mode wird zur dunkelsten */
|
||||
--color-body-bg-lighter: #131720; /* Ein sehr dunkler Ton für besondere Fälle */
|
||||
|
||||
/* Die Hover-Farbe ist jetzt heller als der Standard-Hintergrund */
|
||||
--color-body-bg-hover: #374151; /* ÜBERARBEITET für besseren Kontrast */
|
||||
|
||||
/* NEU: Die "active"-Farbe ist noch einen Schritt heller als die Hover-Farbe */
|
||||
--color-body-bg-active: #4a5568;
|
||||
|
||||
/* Die Rahmenfarbe ist heller als die Oberfläche, um sichtbar zu sein */
|
||||
--color-border: #4a5568;
|
||||
}
|
||||
|
||||
@@ -85,16 +107,16 @@ body.no-scroll {
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
/* .dashboard-grid {
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 1.5rem;
|
||||
} */
|
||||
}
|
||||
|
||||
main {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
/* padding: 1rem; */
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
@@ -142,12 +164,12 @@ main {
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.dashboard-grid {
|
||||
.grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.dashboard-grid {
|
||||
.grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user