add admin navbar and brand/platform management components

This commit is contained in:
Vincent Guillet
2025-10-31 18:32:24 +01:00
parent 6dc9f4ffea
commit 7531ea9453
50 changed files with 842 additions and 227 deletions

View File

@@ -0,0 +1,9 @@
<mat-tab-group>
<mat-tab label="Marques">
<app-brands-list></app-brands-list>
</mat-tab>
<mat-tab label="Plateformes">
<app-platforms-list></app-platforms-list>
</mat-tab>
<mat-tab label="Catégories">Catégories</mat-tab>
</mat-tab-group>

View File

@@ -0,0 +1,28 @@
import { Component } from '@angular/core';
import {MatAnchor, MatButton} from "@angular/material/button";
import {MatIcon} from '@angular/material/icon';
import {RouterLink, RouterLinkActive} from '@angular/router';
import {MatTab, MatTabGroup} from '@angular/material/tabs';
import {BrandsListComponent} from '../brands-list/brands-list.component';
import {PlatformsListComponent} from '../platforms-list/platforms-list.component';
@Component({
selector: 'app-admin-navbar',
standalone: true,
imports: [
MatButton,
MatIcon,
RouterLinkActive,
RouterLink,
MatAnchor,
MatTabGroup,
MatTab,
BrandsListComponent,
PlatformsListComponent
],
templateUrl: './admin-navbar.component.html',
styleUrl: './admin-navbar.component.css'
})
export class AdminNavbarComponent {
}

View File

@@ -0,0 +1,13 @@
<h2 mat-dialog-title>{{ brandExists ? 'Modifier la marque' : 'Nouvelle marque' }}</h2>
<mat-dialog-content>
<mat-form-field appearance="fill" style="width:100%;">
<mat-label>Nom</mat-label>
<input matInput [(ngModel)]="brand.name" />
</mat-form-field>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-button (click)="cancel()">Annuler</button>
<button mat-flat-button color="primary" (click)="save()">Enregistrer</button>
</mat-dialog-actions>

View File

@@ -0,0 +1,51 @@
import { Component, Inject } from '@angular/core';
import {
MatDialogRef,
MAT_DIALOG_DATA,
MatDialogTitle,
MatDialogContent,
MatDialogActions
} from '@angular/material/dialog';
import { Brand } from '../../interfaces/brand';
import {MatFormField, MatLabel} from '@angular/material/form-field';
import {MatInput} from '@angular/material/input';
import {FormsModule} from '@angular/forms';
import {MatButton} from '@angular/material/button';
@Component({
selector: 'app-brand-dialog',
standalone: true,
imports: [
MatDialogTitle,
MatDialogContent,
MatFormField,
MatLabel,
MatInput,
FormsModule,
MatDialogActions,
MatButton
],
templateUrl: './brand-dialog.component.html'
})
export class BrandDialogComponent {
brand: Brand = { id: '', name: '' }
constructor(
private readonly dialogRef: MatDialogRef<BrandDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: { brand: Brand }
) {
this.brand = { ...(data?.brand || { id: '', name: '' }) };
}
get brandExists(): boolean {
return !!this.data?.brand?.id;
}
save() {
this.dialogRef.close(this.brand);
}
cancel() {
this.dialogRef.close();
}
}

View File

@@ -0,0 +1,65 @@
:host {
display: block;
box-sizing: border-box;
width: 100%;
}
.container {
max-width: 900px;
margin: 0 auto;
width: 100%;
}
.toolbar {
display: flex;
gap: 12px;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
}
.filter {
max-width: 240px;
width: 100%;
}
table {
width: 100%;
overflow: auto;
}
td, th {
word-break: break-word;
white-space: normal;
}
.actions-cell {
display: flex;
gap: 8px;
justify-content: flex-end;
min-width: 120px;
}
button.mat-icon-button {
width: 40px;
height: 40px;
}
.no-brands {
text-align: center;
margin-top: 16px;
color: rgba(0,0,0,0.6);
padding: 8px 12px;
}
@media (max-width: 600px) {
.toolbar {
flex-direction: column;
align-items: stretch;
gap: 8px;
}
.actions-cell {
min-width: 0;
}
}

View File

@@ -0,0 +1,44 @@
<div class="container" style="padding:16px;">
<div class="toolbar">
<button mat-flat-button color="accent" (click)="onAdd()">
<mat-icon>add</mat-icon>&nbsp;Ajouter
</button>
<mat-form-field class="filter">
<input matInput placeholder="Rechercher" (input)="applyFilter($any($event.target).value)">
</mat-form-field>
</div>
<table mat-table [dataSource]="dataSource" class="mat-elevation-z1" matSort>
<!-- Name Column -->
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef>Nom</th>
<td mat-cell *matCellDef="let brand">{{ brand.name }}</td>
</ng-container>
<!-- Actions Column -->
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let brand" class="actions-cell">
<button mat-icon-button (click)="onEdit(brand)">
<mat-icon>edit</mat-icon>
</button>
<button mat-icon-button color="warn" (click)="onDelete(brand)">
<mat-icon>delete</mat-icon>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
<mat-paginator [pageSize]="10" [pageSizeOptions]="[5,10,25]" showFirstLastButtons></mat-paginator>
@if (!brands || brands.length === 0) {
<div class="no-brands">
Aucune marque trouvée.
</div>
}
</div>

View File

@@ -0,0 +1,138 @@
import {
Component,
Input,
Output,
EventEmitter,
ViewChild,
AfterViewInit,
OnChanges,
SimpleChanges,
OnInit,
inject
} from '@angular/core';
import {
MatCell, MatCellDef,
MatColumnDef,
MatHeaderCell,
MatHeaderCellDef, MatHeaderRow, MatHeaderRowDef, MatRow, MatRowDef,
MatTable,
MatTableDataSource
} from '@angular/material/table';
import {MatPaginator} from '@angular/material/paginator';
import {MatSort} from '@angular/material/sort';
import {Brand} from '../../interfaces/brand';
import {MatButton, MatIconButton} from '@angular/material/button';
import {MatIcon} from '@angular/material/icon';
import {MatFormField} from '@angular/material/form-field';
import {MatInput} from '@angular/material/input';
import {BrandService} from '../../services/brand/brand.service';
import {MatDialog} from '@angular/material/dialog';
import { BrandDialogComponent } from '../brand-dialog/brand-dialog.component';
@Component({
selector: 'app-brands-list',
templateUrl: './brands-list.component.html',
standalone: true,
imports: [
MatButton,
MatIcon,
MatFormField,
MatInput,
MatTable,
MatColumnDef,
MatHeaderCell,
MatCell,
MatHeaderCellDef,
MatCellDef,
MatSort,
MatIconButton,
MatHeaderRow,
MatRow,
MatHeaderRowDef,
MatRowDef,
MatPaginator
],
styleUrls: ['./brands-list.component.css']
})
export class BrandsListComponent implements OnInit, AfterViewInit, OnChanges {
@Input() brands: Brand[] = [];
@Output() add = new EventEmitter<Brand>();
@Output() edit = new EventEmitter<Brand>();
@Output() delete = new EventEmitter<Brand>();
displayedColumns: string[] = ['name', 'actions'];
dataSource = new MatTableDataSource<Brand>([]);
@ViewChild(MatPaginator) paginator!: MatPaginator;
@ViewChild(MatSort) sort!: MatSort;
private readonly brandService: BrandService = inject(BrandService);
private readonly dialog = inject(MatDialog);
ngOnInit(): void {
if (!this.brands || this.brands.length === 0) {
this.loadBrands();
} else {
this.dataSource.data = this.brands;
}
}
ngOnChanges(changes: SimpleChanges): void {
if (changes['brands']) {
this.dataSource.data = this.brands || [];
}
}
ngAfterViewInit(): void {
this.dataSource.paginator = this.paginator;
this.dataSource.sort = this.sort;
}
loadBrands() {
this.brandService.getBrands().subscribe({
next: (brands:Brand[]) => {
this.brands = brands || []
this.dataSource.data = this.brands;
},
error: () => this.brands = []
});
}
onAdd(): void {
const ref = this.dialog.open(BrandDialogComponent, {
data: { brand: { id: '', name: '' } },
width: '420px'
});
ref.afterClosed().subscribe((result?: Brand) => {
if (result) {
this.add.emit(result);
this.brandService.addBrand(result).subscribe(() => this.loadBrands());
}
});
}
onEdit(brand: Brand): void {
const ref = this.dialog.open(BrandDialogComponent, {
data: { brand: { ...brand } },
width: '420px'
});
ref.afterClosed().subscribe((result?: Brand) => {
if (result) {
this.edit.emit(result);
this.brandService.updateBrand((brand as any).id, result).subscribe(() => this.loadBrands());
}
});
}
onDelete(brand: Brand): void {
this.delete.emit(brand);
this.brandService.deleteBrand((brand as any).id).subscribe(() => this.loadBrands());
}
applyFilter(value: string): void {
this.dataSource.filter = (value || '').trim().toLowerCase();
}
}

View File

@@ -0,0 +1,21 @@
<h2 mat-dialog-title>{{ platformExists ? 'Modifier la plateforme' : 'Nouvelle plateforme' }}</h2>
<mat-dialog-content>
<mat-form-field appearance="fill" style="width:100%;">
<mat-label>Nom</mat-label>
<input matInput [(ngModel)]="platform.name" />
</mat-form-field>
<mat-form-field appearance="fill" style="width:100%;">
<mat-label>Marque</mat-label>
<mat-select [(ngModel)]="platform.brand" disableRipple>
@for (brand of brands; track brand.id) {
<mat-option [value]="brand">{{ brand.name }}</mat-option>
}
</mat-select>
</mat-form-field>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-button (click)="cancel()">Annuler</button>
<button mat-flat-button color="primary" (click)="save()">Enregistrer</button>
</mat-dialog-actions>

View File

@@ -0,0 +1,74 @@
import {Component, inject, Inject, OnInit} from '@angular/core';
import {MatButton} from "@angular/material/button";
import {
MAT_DIALOG_DATA,
MatDialogActions,
MatDialogContent,
MatDialogRef,
MatDialogTitle
} from "@angular/material/dialog";
import {MatFormField, MatLabel} from "@angular/material/form-field";
import {MatInput} from "@angular/material/input";
import {FormsModule, ReactiveFormsModule} from "@angular/forms";
import {Brand} from '../../interfaces/brand';
import {Platform} from '../../interfaces/platform';
import {MatOption} from '@angular/material/core';
import {MatSelect} from '@angular/material/select';
import {BrandService} from '../../services/brand/brand.service';
@Component({
selector: 'app-platform-dialog',
standalone: true,
imports: [
MatButton,
MatDialogActions,
MatDialogContent,
MatDialogTitle,
MatFormField,
MatInput,
MatLabel,
ReactiveFormsModule,
FormsModule,
MatOption,
MatSelect
],
templateUrl: './platform-dialog.component.html',
styleUrl: './platform-dialog.component.css'
})
export class PlatformDialogComponent implements OnInit {
private readonly brandService: BrandService = inject(BrandService);
platform: Platform = { id: '', name: '', brand: undefined };
brands: Brand[] = [];
constructor(
private readonly dialogRef: MatDialogRef<PlatformDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: { platform: Platform }
) {
this.platform = { ...data.platform };
}
ngOnInit(): void {
this.loadBrands();
}
get platformExists(): boolean {
return !!this.data?.platform?.id;
}
loadBrands() {
this.brandService.getBrands().subscribe({
next: (brands:Brand[]) => this.brands = brands || [],
error: () => this.brands = []
});
}
save() {
this.dialogRef.close(this.platform);
}
cancel() {
this.dialogRef.close();
}
}

View File

@@ -0,0 +1,65 @@
:host {
display: block;
box-sizing: border-box;
width: 100%;
}
.container {
max-width: 900px;
margin: 0 auto;
width: 100%;
}
.toolbar {
display: flex;
gap: 12px;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
}
.filter {
max-width: 240px;
width: 100%;
}
table {
width: 100%;
overflow: auto;
}
td, th {
word-break: break-word;
white-space: normal;
}
.actions-cell {
display: flex;
gap: 8px;
justify-content: flex-end;
min-width: 120px;
}
button.mat-icon-button {
width: 40px;
height: 40px;
}
.no-brands {
text-align: center;
margin-top: 16px;
color: rgba(0,0,0,0.6);
padding: 8px 12px;
}
@media (max-width: 600px) {
.toolbar {
flex-direction: column;
align-items: stretch;
gap: 8px;
}
.actions-cell {
min-width: 0;
}
}

View File

@@ -0,0 +1,50 @@
<div class="container" style="padding:16px;">
<div class="toolbar">
<button mat-flat-button color="accent" (click)="onAdd()">
<mat-icon>add</mat-icon>&nbsp;Ajouter
</button>
<mat-form-field class="filter">
<input matInput placeholder="Rechercher" (input)="applyFilter($any($event.target).value)">
</mat-form-field>
</div>
<table mat-table [dataSource]="dataSource" class="mat-elevation-z1" matSort>
<!-- Name Column -->
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef>Nom</th>
<td mat-cell *matCellDef="let platform">{{ platform.name }}</td>
</ng-container>
<!-- Brand Column -->
<ng-container matColumnDef="brand">
<th mat-header-cell *matHeaderCellDef>Marque</th>
<td mat-cell *matCellDef="let platform">{{ platform.brand.name }}</td>
</ng-container>
<!-- Actions Column -->
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let platform" class="actions-cell">
<button mat-icon-button (click)="onEdit(platform)">
<mat-icon>edit</mat-icon>
</button>
<button mat-icon-button color="warn" (click)="onDelete(platform)">
<mat-icon>delete</mat-icon>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
<mat-paginator [pageSize]="10" [pageSizeOptions]="[5,10,25]" showFirstLastButtons></mat-paginator>
@if (!platforms || platforms.length === 0) {
<div class="no-platforms">
Aucune plateforme trouvée.
</div>
}
</div>

View File

@@ -0,0 +1,139 @@
import {
Component,
Input,
Output,
EventEmitter,
ViewChild,
AfterViewInit,
OnChanges,
SimpleChanges,
OnInit,
inject
} from '@angular/core';
import {
MatCell, MatCellDef,
MatColumnDef,
MatHeaderCell,
MatHeaderCellDef, MatHeaderRow, MatHeaderRowDef, MatRow, MatRowDef,
MatTable,
MatTableDataSource
} from '@angular/material/table';
import {MatPaginator} from '@angular/material/paginator';
import {MatSort} from '@angular/material/sort';
import {Platform} from '../../interfaces/platform';
import {MatButton, MatIconButton} from '@angular/material/button';
import {MatIcon} from '@angular/material/icon';
import {MatFormField} from '@angular/material/form-field';
import {MatInput} from '@angular/material/input';
import {PlatformService} from '../../services/platform/platform.service';
import {MatDialog} from '@angular/material/dialog';
import { PlatformDialogComponent } from '../platform-dialog/platform-dialog.component';
@Component({
selector: 'app-platforms-list',
templateUrl: './platforms-list.component.html',
standalone: true,
imports: [
MatButton,
MatIcon,
MatFormField,
MatInput,
MatTable,
MatColumnDef,
MatHeaderCell,
MatCell,
MatHeaderCellDef,
MatCellDef,
MatSort,
MatIconButton,
MatHeaderRow,
MatRow,
MatHeaderRowDef,
MatRowDef,
MatPaginator
],
styleUrls: ['./platforms-list.component.css']
})
export class PlatformsListComponent implements OnInit, AfterViewInit, OnChanges {
@Input() platforms: Platform[] = [];
@Output() add = new EventEmitter<Platform>();
@Output() edit = new EventEmitter<Platform>();
@Output() delete = new EventEmitter<Platform>();
displayedColumns: string[] = ['name', 'brand', 'actions'];
dataSource = new MatTableDataSource<Platform>([]);
@ViewChild(MatPaginator) paginator!: MatPaginator;
@ViewChild(MatSort) sort!: MatSort;
private readonly platformService: PlatformService = inject(PlatformService);
private readonly dialog = inject(MatDialog);
ngOnInit(): void {
if (!this.platforms || this.platforms.length === 0) {
this.loadPlatforms();
} else {
this.dataSource.data = this.platforms;
}
}
ngOnChanges(changes: SimpleChanges): void {
if (changes['platforms']) {
this.dataSource.data = this.platforms || [];
}
}
ngAfterViewInit(): void {
this.dataSource.paginator = this.paginator;
this.dataSource.sort = this.sort;
}
loadPlatforms() {
this.platformService.getPlatforms().subscribe({
next: (platforms:Platform[]) => {
this.platforms = platforms || []
this.dataSource.data = this.platforms;
console.log("Fetched platforms:", this.platforms);
},
error: () => this.platforms = []
});
}
onAdd(): void {
const ref = this.dialog.open(PlatformDialogComponent, {
data: { platform: { id: '', name: '', brand: undefined } },
width: '420px'
});
ref.afterClosed().subscribe((result?: Platform) => {
if (result) {
this.add.emit(result);
this.platformService.addPlatform(result).subscribe(() => this.loadPlatforms());
}
});
}
onEdit(platform: Platform): void {
const ref = this.dialog.open(PlatformDialogComponent, {
data: { platform: { ...platform } },
width: '420px'
});
ref.afterClosed().subscribe((result?: Platform) => {
if (result) {
this.edit.emit(result);
this.platformService.updatePlatform((platform as any).id, result).subscribe(() => this.loadPlatforms());
}
});
}
onDelete(platform: Platform): void {
this.delete.emit(platform);
this.platformService.deletePlatform((platform as any).id).subscribe(() => this.loadPlatforms());
}
applyFilter(value: string): void {
this.dataSource.filter = (value || '').trim().toLowerCase();
}
}