init
This commit is contained in:
@@ -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);
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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); */
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user