This commit is contained in:
Tizian.Breuch
2025-09-17 21:17:27 +02:00
parent b8b0e167af
commit c066476cc3
31 changed files with 480 additions and 179 deletions

View File

@@ -1,56 +0,0 @@
:host {
display: block;
}
/* Die .kpi-card-Regel wird jetzt auf den Host angewendet, da das Template nur den Inhalt enthält */
:host .kpi-card-content {
display: flex;
align-items: center;
gap: 1rem;
padding: 1.5rem;
}
.kpi-icon {
width: 50px;
height: 50px;
border-radius: 50%;
display: grid;
place-items: center;
flex-shrink: 0;
}
.kpi-icon app-icon {
width: 24px;
height: 24px;
color: #fff; /* Standardfarbe für das Icon (currentColor) */
}
/* Farbvarianten für den Hintergrund des Icons */
.icon-blue {
background: linear-gradient(135deg, #3498db, #2980b9);
}
.icon-green {
background: linear-gradient(135deg, #2ecc71, #27ae60);
}
.icon-orange {
background: linear-gradient(135deg, #f39c12, #f1c40f);
}
.icon-purple {
background: linear-gradient(135deg, #9b59b6, #8e44ad);
}
.kpi-content {
display: flex;
flex-direction: column;
}
.kpi-value {
font-size: 1.75rem;
font-weight: 700;
color: var(--color-text);
}
.kpi-label {
font-size: 0.9rem;
color: var(--color-text-light);
}

View File

@@ -1,19 +0,0 @@
<app-card>
<div
class="kpi-icon"
[class.icon-blue]="color === 'blue'"
[class.icon-green]="color === 'green'"
[class.icon-orange]="color === 'orange'"
[class.icon-purple]="color === 'purple'"
>
<app-icon
*ngIf="iconName"
[iconName]="iconName"
[svgColor]="svgColor"
></app-icon>
</div>
<div class="kpi-content">
<span class="kpi-value">{{ value }}</span>
<span class="kpi-label">{{ label }}</span>
</div>
</app-card>

View File

@@ -1,21 +0,0 @@
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';
export type KpiColor = 'blue' | 'green' | 'orange' | 'purple';
@Component({
selector: 'app-kpi-card',
standalone: true,
imports: [CommonModule, IconComponent,CardComponent],
templateUrl: './kpi-card.component.html',
styleUrl: './kpi-card.component.css',
})
export class KpiCardComponent {
@Input() value: string = '';
@Input() label: string = '';
@Input() color: KpiColor = 'blue';
@Input() iconName: string | null = null;
@Input() svgColor: string | null = null;
}

View File

@@ -88,7 +88,7 @@
}
.mono {
font-family: 'Courier New', Courier, monospace;
font-family: "Courier New", Courier, monospace;
}
.actions-cell {
@@ -101,4 +101,40 @@
text-align: center;
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;
}
}

View File

@@ -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)"
>

View File

@@ -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
onPageChange(newPage: number): void {
this.currentPage = newPage;
this.updateDisplayedOrders();
// Called on input from the search field
onSearchChange(term: string): void {
this.searchTerm = term;
this.applyFiltersAndPagination();
}
// Diese Methode berechnet, welcher Teil der Daten angezeigt werden soll
private updateDisplayedOrders(): void {
// 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.applyFiltersAndPagination();
}
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);
}
}
}

View File

@@ -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,

View File

@@ -5,7 +5,7 @@
.search-bar {
position: relative;
width: 300px;
width: 100%
}
.search-bar svg {
position: absolute;

View File

@@ -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>

View File

@@ -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); */
@@ -45,16 +47,17 @@
white-space: nowrap;
transition: background-color 0.2s, color 0.2s;
overflow: hidden;
position: relative;
position: relative;
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 {
@@ -64,7 +67,7 @@
position: relative;
z-index: 2;
background-color: transparent;
transition: background-color 0.2s;
transition: background-color 0.2s;
}
.nav-item span {
white-space: nowrap;
@@ -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;
}

View File

@@ -28,6 +28,4 @@
</div>
</nav>
</aside>

View File

@@ -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);
}
}
}
}

View File

@@ -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>

View File

@@ -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;
}
}