refactor: rename components and update dialog implementations; add confirm dialog for deletion actions
This commit is contained in:
@@ -1,2 +1,2 @@
|
|||||||
<app-navbar></app-navbar>
|
<app-main-navbar></app-main-navbar>
|
||||||
<router-outlet></router-outlet>
|
<router-outlet></router-outlet>
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
import { TestBed } from '@angular/core/testing';
|
|
||||||
import { AppComponent } from './app.component';
|
|
||||||
|
|
||||||
describe('AppComponent', () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
await TestBed.configureTestingModule({
|
|
||||||
imports: [AppComponent],
|
|
||||||
}).compileComponents();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create the app', () => {
|
|
||||||
const fixture = TestBed.createComponent(AppComponent);
|
|
||||||
const app = fixture.componentInstance;
|
|
||||||
expect(app).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it(`should have the 'client' title`, () => {
|
|
||||||
const fixture = TestBed.createComponent(AppComponent);
|
|
||||||
const app = fixture.componentInstance;
|
|
||||||
expect(app.title).toEqual('client');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should render title', () => {
|
|
||||||
const fixture = TestBed.createComponent(AppComponent);
|
|
||||||
fixture.detectChanges();
|
|
||||||
const compiled = fixture.nativeElement as HTMLElement;
|
|
||||||
expect(compiled.querySelector('h1')?.textContent).toContain('Hello, client');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { RouterOutlet } from '@angular/router';
|
import { RouterOutlet } from '@angular/router';
|
||||||
import {NavbarComponent} from './components/navbar/navbar.component';
|
import {MainNavbarComponent} from './components/navbar/main-navbar/main-navbar.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [RouterOutlet, NavbarComponent],
|
imports: [RouterOutlet, MainNavbarComponent],
|
||||||
templateUrl: './app.component.html',
|
templateUrl: './app.component.html',
|
||||||
styleUrl: './app.component.css'
|
styleUrl: './app.component.css'
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Routes } from '@angular/router';
|
import {Routes} from '@angular/router';
|
||||||
import {HomeComponent} from './pages/home/home.component';
|
import {HomeComponent} from './pages/home/home.component';
|
||||||
import {RegisterComponent} from './pages/register/register.component';
|
import {RegisterComponent} from './pages/register/register.component';
|
||||||
import {LoginComponent} from './pages/login/login.component';
|
import {LoginComponent} from './pages/login/login.component';
|
||||||
@@ -7,19 +7,18 @@ import {guestOnlyCanActivate, guestOnlyCanMatch} from './guards/guest-only.guard
|
|||||||
import {authOnlyCanActivate, authOnlyCanMatch} from './guards/auth-only.guard';
|
import {authOnlyCanActivate, authOnlyCanMatch} from './guards/auth-only.guard';
|
||||||
import {AdminComponent} from './pages/admin/admin.component';
|
import {AdminComponent} from './pages/admin/admin.component';
|
||||||
import {adminOnlyCanActivate, adminOnlyCanMatch} from './guards/admin-only.guard';
|
import {adminOnlyCanActivate, adminOnlyCanMatch} from './guards/admin-only.guard';
|
||||||
import {AddProductComponent} from './pages/add-product/add-product.component';
|
|
||||||
import {ProductsComponent} from './pages/products/products.component';
|
import {ProductsComponent} from './pages/products/products.component';
|
||||||
|
|
||||||
export const routes: Routes = [
|
export const routes: Routes = [
|
||||||
{
|
{
|
||||||
path : '',
|
path: '',
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path : '',
|
path: '',
|
||||||
component: HomeComponent
|
component: HomeComponent
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path : 'home',
|
path: 'home',
|
||||||
component: HomeComponent
|
component: HomeComponent
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -31,38 +30,32 @@ export const routes: Routes = [
|
|||||||
canActivate: [guestOnlyCanActivate]
|
canActivate: [guestOnlyCanActivate]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path : 'login',
|
path: 'login',
|
||||||
component: LoginComponent,
|
component: LoginComponent,
|
||||||
canMatch: [guestOnlyCanMatch],
|
canMatch: [guestOnlyCanMatch],
|
||||||
canActivate: [guestOnlyCanActivate]
|
canActivate: [guestOnlyCanActivate]
|
||||||
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path : 'profile',
|
path: 'profile',
|
||||||
component: ProfileComponent,
|
component: ProfileComponent,
|
||||||
canMatch: [authOnlyCanMatch],
|
canMatch: [authOnlyCanMatch],
|
||||||
canActivate: [authOnlyCanMatch]
|
canActivate: [authOnlyCanMatch]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path : 'admin',
|
path: 'admin',
|
||||||
component: AdminComponent,
|
component: AdminComponent,
|
||||||
canMatch: [adminOnlyCanMatch],
|
canMatch: [adminOnlyCanMatch],
|
||||||
canActivate: [adminOnlyCanActivate]
|
canActivate: [adminOnlyCanActivate]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path : 'products',
|
path: 'products',
|
||||||
component: ProductsComponent,
|
component: ProductsComponent,
|
||||||
canMatch: [authOnlyCanMatch],
|
canMatch: [authOnlyCanMatch],
|
||||||
canActivate: [authOnlyCanActivate]
|
canActivate: [authOnlyCanActivate],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path : 'add-product',
|
path: '**',
|
||||||
component: AddProductComponent,
|
|
||||||
canMatch: [authOnlyCanMatch],
|
|
||||||
canActivate: [authOnlyCanActivate]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path : '**',
|
|
||||||
redirectTo: ''
|
redirectTo: ''
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
<app-generic-list
|
|
||||||
[service]="brandService"
|
|
||||||
[fields]="fields"
|
|
||||||
title="Marques"
|
|
||||||
idKey="id">
|
|
||||||
</app-generic-list>
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
<app-generic-list
|
|
||||||
[service]="categoryService"
|
|
||||||
[fields]="fields"
|
|
||||||
title="Catégories"
|
|
||||||
idKey="id">
|
|
||||||
</app-generic-list>
|
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<h2 mat-dialog-title>{{ data.title ?? 'Confirmation' }}</h2>
|
||||||
|
<mat-dialog-content>
|
||||||
|
<p>{{ data.message ?? 'Êtes-vous sûr·e ?' }}</p>
|
||||||
|
</mat-dialog-content>
|
||||||
|
<mat-dialog-actions align="end">
|
||||||
|
<button mat-button type="button" (click)="close(false)">Annuler</button>
|
||||||
|
<button mat-flat-button color="warn" type="button" (click)="close(true)">Supprimer</button>
|
||||||
|
</mat-dialog-actions>
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { Component, Inject } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { MatDialogModule, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-confirm-dialog',
|
||||||
|
standalone: true,
|
||||||
|
imports: [CommonModule, MatDialogModule, MatButtonModule],
|
||||||
|
templateUrl: './confirm-dialog.component.html',
|
||||||
|
})
|
||||||
|
export class ConfirmDialogComponent {
|
||||||
|
constructor(
|
||||||
|
private readonly dialogRef: MatDialogRef<ConfirmDialogComponent>,
|
||||||
|
@Inject(MAT_DIALOG_DATA) public data: { title?: string; message?: string }
|
||||||
|
) {}
|
||||||
|
|
||||||
|
close(result: boolean) {
|
||||||
|
this.dialogRef.close(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
<mat-label>{{ f.label }}</mat-label>
|
<mat-label>{{ f.label }}</mat-label>
|
||||||
|
|
||||||
<mat-select [formControlName]="f.key">
|
<mat-select [formControlName]="f.key">
|
||||||
@let opts = (f.options$ | async) ?? f.options ?? [];
|
@let opts = (f.options ?? (f.options$ | async) ?? []);
|
||||||
|
|
||||||
@for (opt of opts; track $index) {
|
@for (opt of opts; track $index) {
|
||||||
<mat-option [value]="f.valueKey ? opt?.[f.valueKey] : opt">
|
<mat-option [value]="f.valueKey ? opt?.[f.valueKey] : opt">
|
||||||
@@ -36,12 +36,12 @@ type Field = {
|
|||||||
})
|
})
|
||||||
export class GenericDialogComponent implements OnInit {
|
export class GenericDialogComponent implements OnInit {
|
||||||
form!: FormGroup;
|
form!: FormGroup;
|
||||||
fields: Field[] = [];
|
fields?: Field[];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly fb: FormBuilder,
|
private readonly fb: FormBuilder,
|
||||||
private readonly dialogRef: MatDialogRef<GenericDialogComponent>,
|
private readonly dialogRef: MatDialogRef<GenericDialogComponent>,
|
||||||
@Inject(MAT_DIALOG_DATA) public data: { item?: any; fields?: Field[]; title?: string }
|
@Inject(MAT_DIALOG_DATA) public data?: { item?: any; fields?: Field[]; title?: string }
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<app-generic-list
|
||||||
|
[service]="brandService"
|
||||||
|
[fields]="fields"
|
||||||
|
title="Gestion des marques"
|
||||||
|
addTitle="Ajouter une marque"
|
||||||
|
editTitle="Modifier la marque"
|
||||||
|
deleteTitle="Supprimer la marque"
|
||||||
|
idKey="id">
|
||||||
|
</app-generic-list>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
Component, inject
|
Component, inject
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import {BrandService} from '../../services/app/brand.service';
|
import {BrandService} from '../../../services/app/brand.service';
|
||||||
import {GenericListComponent} from '../generic-list/generic-list.component';
|
import {GenericListComponent} from '../generic-list/generic-list.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<app-generic-list
|
||||||
|
[service]="categoryService"
|
||||||
|
[fields]="fields"
|
||||||
|
title="Gestin des catégories"
|
||||||
|
addTitle="Ajouter une catégorie"
|
||||||
|
editTitle="Modifier la catégorie"
|
||||||
|
deleteTitle="Supprimer la catégorie"
|
||||||
|
idKey="id">
|
||||||
|
</app-generic-list>
|
||||||
@@ -2,7 +2,7 @@ import {
|
|||||||
Component, inject
|
Component, inject
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import {GenericListComponent} from '../generic-list/generic-list.component';
|
import {GenericListComponent} from '../generic-list/generic-list.component';
|
||||||
import {CategoryService} from '../../services/app/category.service';
|
import {CategoryService} from '../../../services/app/category.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-category-list',
|
selector: 'app-category-list',
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
border-bottom: 1px solid rgba(0,0,0,.08);
|
border-bottom: 1px solid rgba(0, 0, 0, .08);
|
||||||
padding-bottom: .75rem;
|
padding-bottom: .75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,12 +27,11 @@
|
|||||||
|
|
||||||
/* ===== Cartes (filtre, tableau, pagination) partagent le même style ===== */
|
/* ===== Cartes (filtre, tableau, pagination) partagent le même style ===== */
|
||||||
.gl-block {
|
.gl-block {
|
||||||
border: 1px solid rgba(0,0,0,.08);
|
border: 1px solid rgba(0, 0, 0, .08);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
background: var(--gl-surface, #fff);
|
background: var(--gl-surface, #fff);
|
||||||
box-shadow:
|
box-shadow: 0 1px 2px rgba(0, 0, 0, .04),
|
||||||
0 1px 2px rgba(0,0,0,.04),
|
0 2px 8px rgba(0, 0, 0, .06);
|
||||||
0 2px 8px rgba(0,0,0,.06);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===== Barre de filtre ===== */
|
/* ===== Barre de filtre ===== */
|
||||||
@@ -66,7 +65,7 @@
|
|||||||
top: 0;
|
top: 0;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
background: inherit;
|
background: inherit;
|
||||||
box-shadow: inset 0 -1px 0 rgba(0,0,0,.08);
|
box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .08);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Cellules */
|
/* Cellules */
|
||||||
@@ -80,8 +79,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Zebra + hover */
|
/* Zebra + hover */
|
||||||
.gl-table tr.mat-mdc-row:nth-child(odd) td[mat-cell] { background: rgba(0,0,0,.015); }
|
.gl-table tr.mat-mdc-row:nth-child(odd) td[mat-cell] {
|
||||||
.gl-table tr.mat-mdc-row:hover td[mat-cell] { background: rgba(0,0,0,.035); }
|
background: rgba(0, 0, 0, .015);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gl-table tr.mat-mdc-row:hover td[mat-cell] {
|
||||||
|
background: rgba(0, 0, 0, .035);
|
||||||
|
}
|
||||||
|
|
||||||
/* Actions */
|
/* Actions */
|
||||||
.actions-cell {
|
.actions-cell {
|
||||||
@@ -90,42 +94,78 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: .4rem;
|
gap: .4rem;
|
||||||
}
|
}
|
||||||
.actions-cell .mat-mdc-icon-button { width: 40px; height: 40px; }
|
|
||||||
|
.actions-cell .mat-mdc-icon-button {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
/* ===== Pagination ===== */
|
/* ===== Pagination ===== */
|
||||||
.gl-paginator-wrap {
|
.gl-paginator-wrap {
|
||||||
padding: .25rem .5rem;
|
padding: .25rem .5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gl-paginator {
|
.gl-paginator {
|
||||||
margin-top: .25rem;
|
margin-top: .25rem;
|
||||||
padding-top: .5rem;
|
padding-top: .5rem;
|
||||||
border-top: 1px solid rgba(0,0,0,.08);
|
border-top: 1px solid rgba(0, 0, 0, .08);
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===== Responsive ===== */
|
/* ===== Responsive ===== */
|
||||||
@media (max-width: 799px) {
|
@media (max-width: 799px) {
|
||||||
.generic-list { padding: 0.75rem 1rem; }
|
.generic-list {
|
||||||
.gl-header { flex-direction: column; align-items: stretch; gap: 0.75rem; }
|
padding: 0.75rem 1rem;
|
||||||
.gl-table { min-width: 0; }
|
}
|
||||||
|
|
||||||
|
.gl-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gl-table {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.gl-table th[mat-header-cell],
|
.gl-table th[mat-header-cell],
|
||||||
.gl-table td[mat-cell] { white-space: normal; padding: 10px 12px; }
|
.gl-table td[mat-cell] {
|
||||||
.actions-cell { justify-content: flex-start; }
|
white-space: normal;
|
||||||
|
padding: 10px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions-cell {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===== Dark mode ===== */
|
/* ===== Dark mode ===== */
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
.gl-block {
|
.gl-block {
|
||||||
background: #1b1b1b;
|
background: #1b1b1b;
|
||||||
border-color: rgba(255,255,255,.08);
|
border-color: rgba(255, 255, 255, .08);
|
||||||
box-shadow:
|
box-shadow: 0 1px 2px rgba(0, 0, 0, .6),
|
||||||
0 1px 2px rgba(0,0,0,.6),
|
0 2px 8px rgba(0, 0, 0, .45);
|
||||||
0 2px 8px rgba(0,0,0,.45);
|
}
|
||||||
|
|
||||||
|
.gl-header {
|
||||||
|
border-bottom-color: rgba(255, 255, 255, .08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gl-table th[mat-header-cell] {
|
||||||
|
box-shadow: inset 0 -1px 0 rgba(255, 255, 255, .08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gl-table tr.mat-mdc-row:nth-child(odd) td[mat-cell] {
|
||||||
|
background: rgba(255, 255, 255, .025);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gl-table tr.mat-mdc-row:hover td[mat-cell] {
|
||||||
|
background: rgba(255, 255, 255, .06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gl-paginator {
|
||||||
|
border-top-color: rgba(255, 255, 255, .08);
|
||||||
}
|
}
|
||||||
.gl-header { border-bottom-color: rgba(255,255,255,.08); }
|
|
||||||
.gl-table th[mat-header-cell] { box-shadow: inset 0 -1px 0 rgba(255,255,255,.08); }
|
|
||||||
.gl-table tr.mat-mdc-row:nth-child(odd) td[mat-cell] { background: rgba(255,255,255,.025); }
|
|
||||||
.gl-table tr.mat-mdc-row:hover td[mat-cell] { background: rgba(255,255,255,.06); }
|
|
||||||
.gl-paginator { border-top-color: rgba(255,255,255,.08); }
|
|
||||||
}
|
}
|
||||||
@@ -4,17 +4,16 @@
|
|||||||
|
|
||||||
<div class="gl-controls">
|
<div class="gl-controls">
|
||||||
<button mat-flat-button color="primary" type="button" (click)="openDialog(null)">
|
<button mat-flat-button color="primary" type="button" (click)="openDialog(null)">
|
||||||
Ajouter
|
{{ addTitle ?? 'Ajouter' }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="gl-filter-bar gl-block">
|
<div class="gl-filter-bar gl-block">
|
||||||
<mat-form-field class="gl-filter" appearance="outline">
|
<mat-form-field class="gl-filter" appearance="outline">
|
||||||
<mat-label>Filtrer</mat-label>
|
<mat-label>Rechercher</mat-label>
|
||||||
<input
|
<input
|
||||||
matInput
|
matInput
|
||||||
placeholder="Tapez pour filtrer…"
|
|
||||||
(input)="applyFilter($any($event.target).value)"
|
(input)="applyFilter($any($event.target).value)"
|
||||||
aria-label="Filtrer le tableau"
|
aria-label="Filtrer le tableau"
|
||||||
/>
|
/>
|
||||||
@@ -57,6 +56,7 @@
|
|||||||
aria-label="Modifier"
|
aria-label="Modifier"
|
||||||
>
|
>
|
||||||
<mat-icon>edit</mat-icon>
|
<mat-icon>edit</mat-icon>
|
||||||
|
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
mat-icon-button
|
mat-icon-button
|
||||||
@@ -5,10 +5,12 @@ import {MatSort, MatSortModule} from '@angular/material/sort';
|
|||||||
import {MatDialog, MatDialogModule} from '@angular/material/dialog';
|
import {MatDialog, MatDialogModule} from '@angular/material/dialog';
|
||||||
import {MatButtonModule} from '@angular/material/button';
|
import {MatButtonModule} from '@angular/material/button';
|
||||||
import {CommonModule} from '@angular/common';
|
import {CommonModule} from '@angular/common';
|
||||||
import {CrudService} from '../../services/crud.service';
|
import {CrudService} from '../../../services/crud.service';
|
||||||
import {GenericDialogComponent} from '../generic-dialog/generic-dialog.component';
|
import {GenericDialogComponent} from '../../dialog/generic-dialog/generic-dialog.component';
|
||||||
import {MatFormField, MatInput, MatLabel} from '@angular/material/input';
|
import {MatFormField, MatInput, MatLabel} from '@angular/material/input';
|
||||||
import {MatIcon} from '@angular/material/icon';
|
import {MatIcon} from '@angular/material/icon';
|
||||||
|
import {ConfirmDialogComponent} from '../../dialog/confirm-dialog/confirm-dialog.component';
|
||||||
|
import {MatChip} from '@angular/material/chips';
|
||||||
|
|
||||||
type Field = {
|
type Field = {
|
||||||
key: string;
|
key: string;
|
||||||
@@ -16,7 +18,6 @@ type Field = {
|
|||||||
sortable?: boolean;
|
sortable?: boolean;
|
||||||
displayKey?: string;
|
displayKey?: string;
|
||||||
displayFn?: (value: any, element?: any) => string;
|
displayFn?: (value: any, element?: any) => string;
|
||||||
// nouveau : clé de tri (peut être chemin 'brand.name') ou fonction personnalisée
|
|
||||||
sortKey?: string | ((item: any) => any);
|
sortKey?: string | ((item: any) => any);
|
||||||
sortFn?: (a: any, b: any) => number;
|
sortFn?: (a: any, b: any) => number;
|
||||||
};
|
};
|
||||||
@@ -24,14 +25,17 @@ type Field = {
|
|||||||
@Component({
|
@Component({
|
||||||
selector: 'app-generic-list',
|
selector: 'app-generic-list',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, MatTableModule, MatPaginatorModule, MatSortModule, MatDialogModule, MatButtonModule, MatInput, MatLabel, MatFormField, MatIcon],
|
imports: [CommonModule, MatTableModule, MatPaginatorModule, MatSortModule, MatDialogModule, MatButtonModule, MatInput, MatLabel, MatFormField, MatIcon, MatChip],
|
||||||
templateUrl: './generic-list.component.html',
|
templateUrl: './generic-list.component.html',
|
||||||
styleUrl: './generic-list.component.css'
|
styleUrl: './generic-list.component.css'
|
||||||
})
|
})
|
||||||
export class GenericListComponent<T> implements OnInit, AfterViewInit {
|
export class GenericListComponent<T> implements OnInit, AfterViewInit {
|
||||||
@Input() service!: CrudService<T>;
|
@Input() service!: CrudService<T>;
|
||||||
@Input() fields: Field[] = [];
|
@Input() fields?: Field[];
|
||||||
@Input() title = '';
|
@Input() title = '';
|
||||||
|
@Input() addTitle?: string;
|
||||||
|
@Input() editTitle?: string;
|
||||||
|
@Input() deleteTitle?: string;
|
||||||
@Input() idKey = 'id';
|
@Input() idKey = 'id';
|
||||||
@Input() dialogComponent: any = GenericDialogComponent;
|
@Input() dialogComponent: any = GenericDialogComponent;
|
||||||
@Output() add = new EventEmitter<T>();
|
@Output() add = new EventEmitter<T>();
|
||||||
@@ -48,32 +52,30 @@ export class GenericListComponent<T> implements OnInit, AfterViewInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
|
this.fields = this.fields ?? [];
|
||||||
this.displayedColumns = this.fields.map(f => f.key).concat(['actions']);
|
this.displayedColumns = this.fields.map(f => f.key).concat(['actions']);
|
||||||
this.load();
|
this.load();
|
||||||
}
|
}
|
||||||
|
|
||||||
ngAfterViewInit() {
|
ngAfterViewInit() {
|
||||||
// configure le sortingDataAccessor avant d'attacher le sort
|
|
||||||
this.dataSource.sortingDataAccessor = (data: any, sortHeaderId: string) => {
|
this.dataSource.sortingDataAccessor = (data: any, sortHeaderId: string) => {
|
||||||
// trouver le Field correspondant (ou utiliser la clé brute)
|
const field = this.fields!.find(f => f.key === sortHeaderId);
|
||||||
const field = this.fields.find(f => f.key === sortHeaderId);
|
|
||||||
if (!field) {
|
if (!field) {
|
||||||
const raw = data?.[sortHeaderId];
|
const raw = getByPath(data, sortHeaderId) ?? data?.[sortHeaderId];
|
||||||
return raw == null ? '' : String(raw);
|
return raw == null ? '' : String(raw);
|
||||||
}
|
}
|
||||||
|
|
||||||
// priorité : sortFn sur le field (géré par sort comparator plus bas)
|
|
||||||
if (field.sortKey) {
|
if (field.sortKey) {
|
||||||
if (typeof field.sortKey === 'function') {
|
if (typeof field.sortKey === 'function') {
|
||||||
const v = field.sortKey(data);
|
const v = field.sortKey(data);
|
||||||
return v == null ? '' : String(v);
|
return v == null ? '' : String(v);
|
||||||
}
|
}
|
||||||
const v = getByPath(data, field.sortKey);
|
const v = getByPath(data, field.sortKey as string);
|
||||||
return v == null ? '' : String(v);
|
return v == null ? '' : String(v);
|
||||||
}
|
}
|
||||||
|
|
||||||
// fallback : valeur simple ou displayKey si object
|
const val = getByPath(data, field.key);
|
||||||
const val = data?.[field.key];
|
|
||||||
if (val == null) return '';
|
if (val == null) return '';
|
||||||
if (typeof val === 'object') {
|
if (typeof val === 'object') {
|
||||||
if (field.displayKey && val[field.displayKey] != null) return String(val[field.displayKey]);
|
if (field.displayKey && val[field.displayKey] != null) return String(val[field.displayKey]);
|
||||||
@@ -90,14 +92,13 @@ export class GenericListComponent<T> implements OnInit, AfterViewInit {
|
|||||||
return String(val);
|
return String(val);
|
||||||
};
|
};
|
||||||
|
|
||||||
// attacher le MatSort
|
|
||||||
this.dataSource.sort = this.sort;
|
this.dataSource.sort = this.sort;
|
||||||
|
this.dataSource.paginator = this.paginator;
|
||||||
|
|
||||||
// si un Field a sortFn définie, adapter le comparator global pour l'utiliser
|
|
||||||
const originalSortData = this.dataSource.sortData;
|
const originalSortData = this.dataSource.sortData;
|
||||||
this.dataSource.sortData = (data: T[], sort: MatSort) => {
|
this.dataSource.sortData = (data: T[], sort: MatSort) => {
|
||||||
if (!sort || !sort.active || sort.direction === '') return originalSortData.call(this.dataSource, data, sort);
|
if (!sort || !sort.active || sort.direction === '') return originalSortData.call(this.dataSource, data, sort);
|
||||||
const field = this.fields.find(f => f.key === sort.active);
|
const field = this.fields!.find(f => f.key === sort.active);
|
||||||
if (field?.sortFn) {
|
if (field?.sortFn) {
|
||||||
const dir = sort.direction === 'asc' ? 1 : -1;
|
const dir = sort.direction === 'asc' ? 1 : -1;
|
||||||
return [...data].sort((a, b) => dir * field.sortFn!(a, b));
|
return [...data].sort((a, b) => dir * field.sortFn!(a, b));
|
||||||
@@ -106,12 +107,11 @@ export class GenericListComponent<T> implements OnInit, AfterViewInit {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// DEBUG + normalisation de l'id pour éviter 'undefined' lors de update
|
|
||||||
load() {
|
load() {
|
||||||
this.service.getAll().subscribe(items => {
|
this.service.getAll().subscribe(items => {
|
||||||
console.debug('Loaded items from service:', items);
|
console.debug('Loaded items from service:', items);
|
||||||
this.dataSource.data = (items as any[]).map(item => {
|
this.dataSource.data = (items as any[]).map(item => {
|
||||||
const normalizedId = item?.[this.idKey] ?? item?.id ?? item?._id ?? item?.platformId ?? null;
|
const normalizedId = getByPath(item, this.idKey) ?? item?.[this.idKey] ?? item?.id ?? item?._id ?? item?.platformId ?? null;
|
||||||
return {...item, [this.idKey]: normalizedId};
|
return {...item, [this.idKey]: normalizedId};
|
||||||
}) as T[];
|
}) as T[];
|
||||||
});
|
});
|
||||||
@@ -122,14 +122,18 @@ export class GenericListComponent<T> implements OnInit, AfterViewInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
openDialog(item: any | null) {
|
openDialog(item: any | null) {
|
||||||
const originalId = item ? (item[this.idKey] ?? item?.id ?? item?._id) : null;
|
const originalId = item ? (getByPath(item, this.idKey) ?? item?.[this.idKey] ?? item?.id ?? item?._id) : null;
|
||||||
|
|
||||||
|
const dialogTitle = item
|
||||||
|
? (this.editTitle ?? 'Modifier')
|
||||||
|
: (this.addTitle ?? 'Ajouter');
|
||||||
|
|
||||||
const dialogRef = this.dialog.open(this.dialogComponent, {
|
const dialogRef = this.dialog.open(this.dialogComponent, {
|
||||||
width: '420px',
|
width: '420px',
|
||||||
data: {
|
data: {
|
||||||
item: item ? {...item} : {},
|
item: item ? {...item} : {},
|
||||||
fields: this.fields,
|
fields: this.fields,
|
||||||
title: item ? 'Modifier' : 'Ajouter',
|
title: dialogTitle,
|
||||||
originalId
|
originalId
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -138,7 +142,7 @@ export class GenericListComponent<T> implements OnInit, AfterViewInit {
|
|||||||
if (!result) return;
|
if (!result) return;
|
||||||
|
|
||||||
if (item) {
|
if (item) {
|
||||||
const idToUpdate = originalId ?? result?.[this.idKey] ?? result?.id ?? result?._id;
|
const idToUpdate = originalId ?? getByPath(result, this.idKey) ?? result?.[this.idKey] ?? result?.id ?? result?._id;
|
||||||
if (idToUpdate == null) {
|
if (idToUpdate == null) {
|
||||||
console.error('Cannot update: id is null/undefined for item', {item, result});
|
console.error('Cannot update: id is null/undefined for item', {item, result});
|
||||||
return;
|
return;
|
||||||
@@ -157,19 +161,31 @@ export class GenericListComponent<T> implements OnInit, AfterViewInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
remove(item: any) {
|
remove(item: any) {
|
||||||
const id = item[this.idKey] ?? item?.id ?? item?._id;
|
const id = getByPath(item, this.idKey) ?? item[this.idKey] ?? item?.id ?? item?._id;
|
||||||
if (id == null) {
|
if (id == null) {
|
||||||
console.error('Cannot delete: id is null/undefined for item', item);
|
console.error('Cannot delete: id is null/undefined for item', item);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const dialogRef = this.dialog.open(ConfirmDialogComponent, {
|
||||||
|
width: '380px',
|
||||||
|
data: {
|
||||||
|
title: this.deleteTitle ?? 'Confirmer la suppression',
|
||||||
|
message: 'Voulez-vous vraiment supprimer cet élément ? Cette action est irréversible.'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
dialogRef.afterClosed().subscribe((confirmed: boolean) => {
|
||||||
|
if (!confirmed) return;
|
||||||
this.service.delete(id).subscribe(() => {
|
this.service.delete(id).subscribe(() => {
|
||||||
this.delete.emit(item);
|
this.delete.emit(item);
|
||||||
this.load();
|
this.load();
|
||||||
});
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
displayValue(element: any, field: Field): string {
|
displayValue(element: any, field: Field): string {
|
||||||
const val = element?.[field.key];
|
const val = getByPath(element, field.key);
|
||||||
if (field.displayFn) {
|
if (field.displayFn) {
|
||||||
try {
|
try {
|
||||||
return String(field.displayFn(val, element) ?? '');
|
return String(field.displayFn(val, element) ?? '');
|
||||||
@@ -193,15 +209,11 @@ export class GenericListComponent<T> implements OnInit, AfterViewInit {
|
|||||||
return String(val);
|
return String(val);
|
||||||
}
|
}
|
||||||
|
|
||||||
trackByField(_index: number, field: Field) {
|
|
||||||
return field?.key ?? _index;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected readonly HTMLInputElement = HTMLInputElement;
|
protected readonly HTMLInputElement = HTMLInputElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Helpers */
|
function getByPath(obj: any, path: string | undefined): any {
|
||||||
function getByPath(obj: any, path: string): any {
|
|
||||||
if (!obj || !path) return undefined;
|
if (!obj || !path) return undefined;
|
||||||
|
if (typeof path !== 'string') return undefined;
|
||||||
return path.split('.').reduce((acc, key) => (acc == null ? undefined : acc[key]), obj);
|
return path.split('.').reduce((acc, key) => (acc == null ? undefined : acc[key]), obj);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<app-generic-list
|
||||||
|
[service]="platformService"
|
||||||
|
[fields]="fields"
|
||||||
|
title="Gestion des plateformes"
|
||||||
|
addTitle="Ajouter une plateforme"
|
||||||
|
editTitle="Modifier la plateforme"
|
||||||
|
deleteTitle="Supprimer la plateforme"
|
||||||
|
idKey="id">
|
||||||
|
</app-generic-list>
|
||||||
@@ -2,9 +2,9 @@ import {
|
|||||||
Component,
|
Component,
|
||||||
inject
|
inject
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import {PlatformService} from '../../services/app/platform.service';
|
import {PlatformService} from '../../../services/app/platform.service';
|
||||||
import {GenericListComponent} from '../generic-list/generic-list.component';
|
import {GenericListComponent} from '../generic-list/generic-list.component';
|
||||||
import {BrandService} from '../../services/app/brand.service';
|
import {BrandService} from '../../../services/app/brand.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-platform-list',
|
selector: 'app-platform-list',
|
||||||
@@ -26,11 +26,10 @@ export class PlatformListComponent {
|
|||||||
key: 'brand',
|
key: 'brand',
|
||||||
label: 'Marque',
|
label: 'Marque',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
options$: this.brandService.getAll(), // transmet les brands dynamiquement
|
options$: this.brandService.getAll(),
|
||||||
displayKey: 'name', // affiche brand.name dans le select
|
displayKey: 'name',
|
||||||
// valueKey: 'id' // uncommenter si backend attend l'id au lieu de l'objet
|
|
||||||
sortable: true,
|
sortable: true,
|
||||||
sortKey: 'brand.name' // permet de trier par brand.name
|
sortKey: 'brand.name'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<app-generic-list
|
||||||
|
[service]="productService"
|
||||||
|
[fields]="fields"
|
||||||
|
title="Liste des produits"
|
||||||
|
addTitle="Ajouter un produit"
|
||||||
|
editTitle="Modifier le produit"
|
||||||
|
deleteTitle="Supprimer le produit"
|
||||||
|
idKey="id">
|
||||||
|
</app-generic-list>
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
import {
|
||||||
|
Component,
|
||||||
|
inject
|
||||||
|
} from '@angular/core';
|
||||||
|
import {GenericListComponent} from '../generic-list/generic-list.component';
|
||||||
|
import {ProductService} from '../../../services/app/product.service';
|
||||||
|
import {BrandService} from '../../../services/app/brand.service';
|
||||||
|
import {PlatformService} from '../../../services/app/platform.service';
|
||||||
|
import {CategoryService} from '../../../services/app/category.service';
|
||||||
|
import {ConditionService} from '../../../services/app/condition.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-product-list',
|
||||||
|
templateUrl: './product-list.component.html',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
GenericListComponent
|
||||||
|
],
|
||||||
|
styleUrls: ['./product-list.component.css']
|
||||||
|
})
|
||||||
|
export class ProductListComponent {
|
||||||
|
|
||||||
|
productService: ProductService = inject(ProductService);
|
||||||
|
categoryService: CategoryService = inject(CategoryService);
|
||||||
|
brandService: BrandService = inject(BrandService);
|
||||||
|
platformService: PlatformService = inject(PlatformService);
|
||||||
|
conditionService: ConditionService = inject(ConditionService);
|
||||||
|
|
||||||
|
fields = [
|
||||||
|
{key: 'title', label: 'Titre', sortable: true},
|
||||||
|
{
|
||||||
|
key: 'description',
|
||||||
|
label: 'Description',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'category',
|
||||||
|
label: 'Catégorie',
|
||||||
|
type: 'select',
|
||||||
|
options$: this.categoryService.getAll(),
|
||||||
|
displayKey: 'name',
|
||||||
|
sortable: true,
|
||||||
|
sortKey: 'category.name'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'platform.brand',
|
||||||
|
label: 'Marque',
|
||||||
|
type: 'select',
|
||||||
|
options$: this.brandService.getAll(),
|
||||||
|
displayKey: 'name',
|
||||||
|
sortable: true,
|
||||||
|
sortKey: 'platform.brand.name'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'platform',
|
||||||
|
label: 'Plateforme',
|
||||||
|
type: 'select',
|
||||||
|
options$: this.platformService.getAll(),
|
||||||
|
displayKey: 'name',
|
||||||
|
sortable: true,
|
||||||
|
sortKey: 'platform.name'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'condition.displayName',
|
||||||
|
label: 'État',
|
||||||
|
type: 'select',
|
||||||
|
options$: this.conditionService.getAll(),
|
||||||
|
displayKey: 'displayName',
|
||||||
|
sortable: true,
|
||||||
|
sortKey: 'condition.displayName'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'complete',
|
||||||
|
label: 'Complet',
|
||||||
|
type: 'checkbox',
|
||||||
|
sortable: true,
|
||||||
|
sortKey: 'complete',
|
||||||
|
displayFn: (value: boolean): string => value ? '✔ Oui' : '✗ Non'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'manualIncluded',
|
||||||
|
label: 'Notice',
|
||||||
|
type: 'checkbox',
|
||||||
|
sortable: true,
|
||||||
|
sortKey: 'manualIncluded',
|
||||||
|
displayFn: (value: boolean): string => value ? '✔ Oui' : '✗ Non'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'price',
|
||||||
|
label: 'Prix',
|
||||||
|
sortable: true,
|
||||||
|
sortKey: 'price',
|
||||||
|
displayFn: (value: string | number): string => {
|
||||||
|
if (value == null || value === '') return '';
|
||||||
|
try {
|
||||||
|
return new Intl
|
||||||
|
.NumberFormat('fr-FR', {style: 'currency', currency: 'EUR', maximumFractionDigits: 2})
|
||||||
|
.format(Number(value));
|
||||||
|
} catch {
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'quantity',
|
||||||
|
label: 'Quantité',
|
||||||
|
sortable: true,
|
||||||
|
sortKey: 'quantity'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import {MatTab, MatTabGroup} from '@angular/material/tabs';
|
import {MatTab, MatTabGroup} from '@angular/material/tabs';
|
||||||
import {PlatformListComponent} from '../platform-list/platform-list.component';
|
import {PlatformListComponent} from '../../list/platform-list/platform-list.component';
|
||||||
import {CategoryListComponent} from '../category-list/category-list.component';
|
import {CategoryListComponent} from '../../list/category-list/category-list.component';
|
||||||
import {BrandListComponent} from '../brand-list/brand-list.component';
|
import {BrandListComponent} from '../../list/brand-list/brand-list.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-admin-navbar',
|
selector: 'app-admin-navbar',
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
<mat-icon>person</mat-icon>
|
<mat-icon>person</mat-icon>
|
||||||
Profil
|
Profil
|
||||||
</button>
|
</button>
|
||||||
<button mat-menu-item (click)="authService.logout().subscribe()">
|
<button mat-menu-item (click)="logout()">
|
||||||
<mat-icon>logout</mat-icon>
|
<mat-icon>logout</mat-icon>
|
||||||
Se déconnecter
|
Se déconnecter
|
||||||
</button>
|
</button>
|
||||||
@@ -2,12 +2,12 @@ import {Component, inject} from '@angular/core';
|
|||||||
import {MatToolbar} from '@angular/material/toolbar';
|
import {MatToolbar} from '@angular/material/toolbar';
|
||||||
import {MatButton} from '@angular/material/button';
|
import {MatButton} from '@angular/material/button';
|
||||||
import {Router, RouterLink} from '@angular/router';
|
import {Router, RouterLink} from '@angular/router';
|
||||||
import {AuthService} from '../../services/auth/auth.service';
|
import {AuthService} from '../../../services/auth/auth.service';
|
||||||
import {MatMenu, MatMenuItem, MatMenuTrigger} from '@angular/material/menu';
|
import {MatMenu, MatMenuItem, MatMenuTrigger} from '@angular/material/menu';
|
||||||
import {MatIcon} from '@angular/material/icon';
|
import {MatIcon} from '@angular/material/icon';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-navbar',
|
selector: 'app-main-navbar',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [
|
imports: [
|
||||||
MatToolbar,
|
MatToolbar,
|
||||||
@@ -18,22 +18,18 @@ import {MatIcon} from '@angular/material/icon';
|
|||||||
MatMenu,
|
MatMenu,
|
||||||
MatMenuItem
|
MatMenuItem
|
||||||
],
|
],
|
||||||
templateUrl: './navbar.component.html',
|
templateUrl: './main-navbar.component.html',
|
||||||
styleUrl: './navbar.component.css'
|
styleUrl: './main-navbar.component.css'
|
||||||
})
|
})
|
||||||
export class NavbarComponent {
|
export class MainNavbarComponent {
|
||||||
|
|
||||||
protected readonly authService = inject(AuthService);
|
protected readonly authService = inject(AuthService);
|
||||||
private readonly router: Router = inject(Router);
|
private readonly router: Router = inject(Router);
|
||||||
|
|
||||||
login() {
|
|
||||||
this.router.navigate(['/login'], {queryParams: {redirect: '/profile'}}).then();
|
|
||||||
}
|
|
||||||
|
|
||||||
logout() {
|
logout() {
|
||||||
this.authService.logout().subscribe({
|
this.authService.logout().subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
this.login();
|
this.router.navigate(['/login'], {queryParams: {redirect: '/profile'}}).then();
|
||||||
},
|
},
|
||||||
error: (err) => {
|
error: (err) => {
|
||||||
console.error('Logout failed:', err);
|
console.error('Logout failed:', err);
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
||||||
|
|
||||||
import { NavbarComponent } from './navbar.component';
|
|
||||||
|
|
||||||
describe('NavbarComponent', () => {
|
|
||||||
let component: NavbarComponent;
|
|
||||||
let fixture: ComponentFixture<NavbarComponent>;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
await TestBed.configureTestingModule({
|
|
||||||
imports: [NavbarComponent]
|
|
||||||
})
|
|
||||||
.compileComponents();
|
|
||||||
|
|
||||||
fixture = TestBed.createComponent(NavbarComponent);
|
|
||||||
component = fixture.componentInstance;
|
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create', () => {
|
|
||||||
expect(component).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
<app-generic-list
|
|
||||||
[service]="platformService"
|
|
||||||
[fields]="fields"
|
|
||||||
title="Plateformes"
|
|
||||||
idKey="id">
|
|
||||||
</app-generic-list>
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<p>product-form works!</p>
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import { Component } from '@angular/core';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-product-form',
|
|
||||||
standalone: true,
|
|
||||||
imports: [],
|
|
||||||
templateUrl: './product-form.component.html',
|
|
||||||
styleUrl: './product-form.component.css'
|
|
||||||
})
|
|
||||||
export class ProductFormComponent {
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
<div class="generic-list">
|
|
||||||
<!-- Header (bouton à droite) -->
|
|
||||||
<div class="gl-header">
|
|
||||||
<div class="gl-controls">
|
|
||||||
<button mat-flat-button color="accent" (click)="onAdd()">
|
|
||||||
<mat-icon>add</mat-icon> Ajouter
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Barre de recherche sous le header -->
|
|
||||||
<div class="gl-filter-bar gl-block">
|
|
||||||
<mat-form-field class="gl-filter" appearance="outline">
|
|
||||||
<mat-label>Rechercher</mat-label>
|
|
||||||
<input
|
|
||||||
matInput
|
|
||||||
placeholder="Tapez pour filtrer…"
|
|
||||||
(input)="applyFilter($any($event.target).value)"
|
|
||||||
aria-label="Filtrer le tableau"
|
|
||||||
/>
|
|
||||||
</mat-form-field>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Tableau -->
|
|
||||||
<div class="gl-table-wrapper gl-block">
|
|
||||||
<table mat-table [dataSource]="dataSource" class="gl-table" matSort>
|
|
||||||
|
|
||||||
<!-- Title Column -->
|
|
||||||
<ng-container matColumnDef="title">
|
|
||||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Nom</th>
|
|
||||||
<td mat-cell *matCellDef="let product">{{ product.title }}</td>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<!-- Description Column -->
|
|
||||||
<ng-container matColumnDef="description">
|
|
||||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Description</th>
|
|
||||||
<td mat-cell *matCellDef="let product">{{ product.description }}</td>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<!-- Category Column -->
|
|
||||||
<ng-container matColumnDef="category">
|
|
||||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Catégorie</th>
|
|
||||||
<td mat-cell *matCellDef="let product">{{ product.category.name }}</td>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<!-- Platform Column -->
|
|
||||||
<ng-container matColumnDef="platform">
|
|
||||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Plateforme</th>
|
|
||||||
<td mat-cell *matCellDef="let product">{{ product.platform.name }}</td>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<!-- Condition Column -->
|
|
||||||
<ng-container matColumnDef="condition">
|
|
||||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>État</th>
|
|
||||||
<td mat-cell *matCellDef="let product">{{ product.condition.displayName }}</td>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<!-- Complete Column -->
|
|
||||||
<ng-container matColumnDef="complete">
|
|
||||||
<th mat-header-cell *matHeaderCellDef>Complet</th>
|
|
||||||
<td mat-cell *matCellDef="let product">
|
|
||||||
@if (product.complete) {
|
|
||||||
<mat-icon color="primary">check_circle</mat-icon>
|
|
||||||
} @else {
|
|
||||||
<mat-icon color="warn">cancel</mat-icon>
|
|
||||||
}
|
|
||||||
</td>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<!-- Manual Column -->
|
|
||||||
<ng-container matColumnDef="manual">
|
|
||||||
<th mat-header-cell *matHeaderCellDef>Notice</th>
|
|
||||||
<td mat-cell *matCellDef="let product">
|
|
||||||
@if (product.manual) {
|
|
||||||
<mat-icon color="primary">check_circle</mat-icon>
|
|
||||||
} @else {
|
|
||||||
<mat-icon color="warn">cancel</mat-icon>
|
|
||||||
}
|
|
||||||
</td>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<!-- Price Column -->
|
|
||||||
<ng-container matColumnDef="price">
|
|
||||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Prix</th>
|
|
||||||
<td mat-cell *matCellDef="let product">{{ product.price | currency:'EUR' }}</td>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<!-- Quantity Column -->
|
|
||||||
<ng-container matColumnDef="quantity">
|
|
||||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Quantité</th>
|
|
||||||
<td mat-cell *matCellDef="let product">{{ product.quantity }}</td>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<!-- Actions Column -->
|
|
||||||
<ng-container matColumnDef="actions">
|
|
||||||
<th mat-header-cell *matHeaderCellDef class="actions-head">Actions</th>
|
|
||||||
<td mat-cell *matCellDef="let product" class="actions-cell">
|
|
||||||
<button mat-icon-button (click)="onEdit(product)" aria-label="Modifier">
|
|
||||||
<mat-icon>edit</mat-icon>
|
|
||||||
</button>
|
|
||||||
<button mat-icon-button color="warn" (click)="onDelete(product)" aria-label="Supprimer">
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Pagination avec même carte -->
|
|
||||||
<div class="gl-paginator-wrap gl-block">
|
|
||||||
<mat-paginator
|
|
||||||
class="gl-paginator"
|
|
||||||
[pageSize]="10"
|
|
||||||
[pageSizeOptions]="[5,10,25]"
|
|
||||||
showFirstLastButtons>
|
|
||||||
</mat-paginator>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@if (!products || products.length === 0) {
|
|
||||||
<div class="no-products gl-block">Aucun produit trouvé.</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
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 {Product} from '../../interfaces/product';
|
|
||||||
import {ProductService} from '../../services/app/product.service';
|
|
||||||
import {MatDialog} from '@angular/material/dialog';
|
|
||||||
import {MatButton, MatIconButton} from '@angular/material/button';
|
|
||||||
import {MatFormField, MatLabel} from '@angular/material/form-field';
|
|
||||||
import {MatIcon} from '@angular/material/icon';
|
|
||||||
import {MatInput} from '@angular/material/input';
|
|
||||||
import {CurrencyPipe} from '@angular/common';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-products-list',
|
|
||||||
templateUrl: './products-list.component.html',
|
|
||||||
standalone: true,
|
|
||||||
imports: [
|
|
||||||
MatButton,
|
|
||||||
MatCell,
|
|
||||||
MatCellDef,
|
|
||||||
MatColumnDef,
|
|
||||||
MatFormField,
|
|
||||||
MatHeaderCell,
|
|
||||||
MatHeaderRow,
|
|
||||||
MatHeaderRowDef,
|
|
||||||
MatIcon,
|
|
||||||
MatIconButton,
|
|
||||||
MatInput,
|
|
||||||
MatPaginator,
|
|
||||||
MatRow,
|
|
||||||
MatRowDef,
|
|
||||||
MatSort,
|
|
||||||
MatTable,
|
|
||||||
MatHeaderCellDef,
|
|
||||||
CurrencyPipe,
|
|
||||||
MatLabel
|
|
||||||
],
|
|
||||||
styleUrls: ['./products-list.component.css']
|
|
||||||
})
|
|
||||||
export class ProductsListComponent implements OnInit, AfterViewInit, OnChanges {
|
|
||||||
|
|
||||||
@Input() products: Product[] = [];
|
|
||||||
@Output() add = new EventEmitter<Product>();
|
|
||||||
@Output() edit = new EventEmitter<Product>();
|
|
||||||
@Output() delete = new EventEmitter<Product>();
|
|
||||||
|
|
||||||
displayedColumns: string[] = ['title', 'description', 'category', 'platform', 'condition', 'complete', 'manual', 'price', 'quantity', 'actions'];
|
|
||||||
dataSource = new MatTableDataSource<Product>([]);
|
|
||||||
|
|
||||||
@ViewChild(MatPaginator) paginator!: MatPaginator;
|
|
||||||
@ViewChild(MatSort) sort!: MatSort;
|
|
||||||
|
|
||||||
private readonly productService: ProductService = inject(ProductService);
|
|
||||||
private readonly dialog = inject(MatDialog);
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
|
||||||
if (!this.products || this.products.length === 0) {
|
|
||||||
this.loadProducts();
|
|
||||||
} else {
|
|
||||||
this.dataSource.data = this.products;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnChanges(changes: SimpleChanges): void {
|
|
||||||
if (changes['products']) {
|
|
||||||
this.dataSource.data = this.products || [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ngAfterViewInit(): void {
|
|
||||||
this.dataSource.paginator = this.paginator;
|
|
||||||
this.dataSource.sort = this.sort;
|
|
||||||
}
|
|
||||||
|
|
||||||
loadProducts() {
|
|
||||||
this.productService.getProducts().subscribe({
|
|
||||||
next: (products: Product[]) => {
|
|
||||||
this.products = products || []
|
|
||||||
this.dataSource.data = this.products;
|
|
||||||
console.log("Fetched products:", this.products);
|
|
||||||
},
|
|
||||||
error: () => this.products = []
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onAdd(): void {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
onEdit(product: Product): void {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
onDelete(product: Product): void {
|
|
||||||
this.delete.emit(product);
|
|
||||||
this.productService.deleteProduct((product as any).id).subscribe(() => this.loadProducts());
|
|
||||||
}
|
|
||||||
|
|
||||||
applyFilter(value: string): void {
|
|
||||||
this.dataSource.filter = (value || '').trim().toLowerCase();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
.auth-wrap {
|
|
||||||
min-height: 100vh;
|
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
padding: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-card {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 520px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-grid {
|
|
||||||
display: grid;
|
|
||||||
gap: 16px;
|
|
||||||
margin-top: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions {
|
|
||||||
display: flex;
|
|
||||||
margin: 8px;
|
|
||||||
|
|
||||||
button {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.ml-8 {
|
|
||||||
margin-left: 8px;
|
|
||||||
}
|
|
||||||
@@ -1,146 +0,0 @@
|
|||||||
<section class="auth-wrap">
|
|
||||||
<mat-card class="auth-card">
|
|
||||||
<mat-card-header>
|
|
||||||
<mat-card-title>Ajouter un produit</mat-card-title>
|
|
||||||
</mat-card-header>
|
|
||||||
|
|
||||||
<mat-card-content>
|
|
||||||
<form [formGroup]="addProductForm" (ngSubmit)="onProductAdd()" class="form-grid">
|
|
||||||
|
|
||||||
<!-- Title -->
|
|
||||||
<mat-form-field appearance="outline">
|
|
||||||
<mat-label>Titre</mat-label>
|
|
||||||
<input matInput
|
|
||||||
id="title"
|
|
||||||
name="title"
|
|
||||||
formControlName="title"
|
|
||||||
type="text"
|
|
||||||
required>
|
|
||||||
@if (isFieldInvalid('title')) {
|
|
||||||
<mat-error>{{ getFieldError('title') }}</mat-error>
|
|
||||||
}
|
|
||||||
</mat-form-field>
|
|
||||||
|
|
||||||
<!-- Description -->
|
|
||||||
<mat-form-field appearance="outline">
|
|
||||||
<mat-label>Description</mat-label>
|
|
||||||
<input matInput
|
|
||||||
id="description"
|
|
||||||
name="description"
|
|
||||||
formControlName="description"
|
|
||||||
type="text"
|
|
||||||
required>
|
|
||||||
@if (isFieldInvalid('description')) {
|
|
||||||
<mat-error>{{ getFieldError('description') }}</mat-error>
|
|
||||||
}
|
|
||||||
</mat-form-field>
|
|
||||||
|
|
||||||
<!-- Category -->
|
|
||||||
<mat-form-field appearance="outline">
|
|
||||||
<mat-label>Catégorie</mat-label>
|
|
||||||
<mat-select formControlName="category" disableRipple>
|
|
||||||
@for (category of categories; track category.id) {
|
|
||||||
<mat-option [value]="category">{{ category.name }}</mat-option>
|
|
||||||
}
|
|
||||||
</mat-select>
|
|
||||||
</mat-form-field>
|
|
||||||
|
|
||||||
<!-- Condition -->
|
|
||||||
<mat-form-field appearance="outline">
|
|
||||||
<mat-label>État</mat-label>
|
|
||||||
<mat-select formControlName="condition" disableRipple>
|
|
||||||
@for (condition of conditions; track condition.id) {
|
|
||||||
<mat-option [value]="condition">{{ condition.displayName }}</mat-option>
|
|
||||||
}
|
|
||||||
</mat-select>
|
|
||||||
</mat-form-field>
|
|
||||||
|
|
||||||
<!-- Brand -->
|
|
||||||
<mat-form-field appearance="outline">
|
|
||||||
<mat-label>Marque</mat-label>
|
|
||||||
<mat-select formControlName="brand" [compareWith]="compareById" disableRipple>
|
|
||||||
@for (brand of filteredBrands; track brand.id) {
|
|
||||||
<mat-option [value]="brand.id">{{ brand.name }}</mat-option>
|
|
||||||
}
|
|
||||||
</mat-select>
|
|
||||||
</mat-form-field>
|
|
||||||
|
|
||||||
<!-- Platform -->
|
|
||||||
<mat-form-field appearance="outline">
|
|
||||||
<mat-label>Plateforme</mat-label>
|
|
||||||
<mat-select formControlName="platform" [compareWith]="compareById" disableRipple>
|
|
||||||
@for (platform of filteredPlatforms; track platform.id) {
|
|
||||||
<mat-option [value]="platform.id">{{ platform.name }}</mat-option>
|
|
||||||
}
|
|
||||||
</mat-select>
|
|
||||||
</mat-form-field>
|
|
||||||
|
|
||||||
<!-- Complete state -->
|
|
||||||
<mat-checkbox formControlName="complete" id="complete">
|
|
||||||
Complet
|
|
||||||
</mat-checkbox>
|
|
||||||
@if (isFieldInvalid('complete')) {
|
|
||||||
<div class="mat-caption mat-error">{{ getFieldError('complete') }}</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
<!-- manual included -->
|
|
||||||
<mat-checkbox formControlName="manual" id="manual">
|
|
||||||
Avec notice
|
|
||||||
</mat-checkbox>
|
|
||||||
@if (isFieldInvalid('manual')) {
|
|
||||||
<div class="mat-caption mat-error">{{ getFieldError('manual') }}</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
<!-- Price -->
|
|
||||||
<mat-form-field appearance="outline">
|
|
||||||
<mat-label>Prix TTC</mat-label>
|
|
||||||
<input matInput
|
|
||||||
id="price"
|
|
||||||
name="price"
|
|
||||||
formControlName="price"
|
|
||||||
type="text"
|
|
||||||
required>
|
|
||||||
@if (isFieldInvalid('price')) {
|
|
||||||
<mat-error>{{ getFieldError('price') }}</mat-error>
|
|
||||||
}
|
|
||||||
</mat-form-field>
|
|
||||||
|
|
||||||
<!-- Quantity -->
|
|
||||||
<mat-form-field appearance="outline">
|
|
||||||
<mat-label>Quantité</mat-label>
|
|
||||||
<input matInput
|
|
||||||
id="quantity"
|
|
||||||
name="quantity"
|
|
||||||
formControlName="quantity"
|
|
||||||
type="text"
|
|
||||||
required>
|
|
||||||
@if (isFieldInvalid('quantity')) {
|
|
||||||
<mat-error>{{ getFieldError('quantity') }}</mat-error>
|
|
||||||
}
|
|
||||||
</mat-form-field>
|
|
||||||
|
|
||||||
<!-- Submit Button -->
|
|
||||||
<div class="actions">
|
|
||||||
<button mat-raised-button color="primary"
|
|
||||||
type="submit"
|
|
||||||
[disabled]="isLoading || addProductForm.invalid">
|
|
||||||
@if (isLoading) {
|
|
||||||
<mat-progress-spinner diameter="16" mode="indeterminate"></mat-progress-spinner>
|
|
||||||
<span class="ml-8">Ajout du produit…</span>
|
|
||||||
} @else {
|
|
||||||
Ajouter le produit
|
|
||||||
}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</mat-card-content>
|
|
||||||
|
|
||||||
<mat-divider></mat-divider>
|
|
||||||
|
|
||||||
<mat-card-actions align="end">
|
|
||||||
<span class="mat-body-small">
|
|
||||||
<a [routerLink]="'/login'">Voir la liste des produits</a>
|
|
||||||
</span>
|
|
||||||
</mat-card-actions>
|
|
||||||
</mat-card>
|
|
||||||
</section>
|
|
||||||
@@ -1,345 +0,0 @@
|
|||||||
import {Component, inject, OnDestroy, OnInit} from '@angular/core';
|
|
||||||
import {
|
|
||||||
AbstractControl,
|
|
||||||
FormBuilder,
|
|
||||||
FormGroup,
|
|
||||||
FormsModule,
|
|
||||||
ReactiveFormsModule, ValidatorFn,
|
|
||||||
Validators
|
|
||||||
} from "@angular/forms";
|
|
||||||
import {MatButton} from "@angular/material/button";
|
|
||||||
import {
|
|
||||||
MatCard,
|
|
||||||
MatCardActions,
|
|
||||||
MatCardContent,
|
|
||||||
MatCardHeader,
|
|
||||||
MatCardTitle
|
|
||||||
} from "@angular/material/card";
|
|
||||||
import {MatCheckbox} from "@angular/material/checkbox";
|
|
||||||
import {MatDivider} from "@angular/material/divider";
|
|
||||||
import {MatError, MatFormField, MatLabel} from "@angular/material/form-field";
|
|
||||||
import {MatInput} from "@angular/material/input";
|
|
||||||
import {MatProgressSpinner} from "@angular/material/progress-spinner";
|
|
||||||
import {MatOption, MatSelect} from '@angular/material/select';
|
|
||||||
import {RouterLink} from '@angular/router';
|
|
||||||
import {Subscription} from 'rxjs';
|
|
||||||
import {BrandService} from '../../services/app/brand.service';
|
|
||||||
import {Brand} from '../../interfaces/brand';
|
|
||||||
import {PlatformService} from '../../services/app/platform.service';
|
|
||||||
import {Platform} from '../../interfaces/platform';
|
|
||||||
import {Category} from '../../interfaces/category';
|
|
||||||
import {CategoryService} from '../../services/app/category.service';
|
|
||||||
import {ConditionService} from '../../services/app/condition.service';
|
|
||||||
import {Condition} from '../../interfaces/condition';
|
|
||||||
import {ProductService} from '../../services/app/product.service';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-add-product',
|
|
||||||
standalone: true,
|
|
||||||
imports: [
|
|
||||||
FormsModule,
|
|
||||||
MatButton,
|
|
||||||
MatCard,
|
|
||||||
MatCardActions,
|
|
||||||
MatCardContent,
|
|
||||||
MatCardHeader,
|
|
||||||
MatCardTitle,
|
|
||||||
MatCheckbox,
|
|
||||||
MatDivider,
|
|
||||||
MatError,
|
|
||||||
MatFormField,
|
|
||||||
MatInput,
|
|
||||||
MatLabel,
|
|
||||||
MatProgressSpinner,
|
|
||||||
ReactiveFormsModule,
|
|
||||||
MatSelect,
|
|
||||||
MatOption,
|
|
||||||
RouterLink
|
|
||||||
],
|
|
||||||
templateUrl: './add-product.component.html',
|
|
||||||
styleUrl: './add-product.component.css'
|
|
||||||
})
|
|
||||||
export class AddProductComponent implements OnInit, OnDestroy {
|
|
||||||
|
|
||||||
addProductForm: FormGroup;
|
|
||||||
isSubmitted = false;
|
|
||||||
isLoading = false;
|
|
||||||
|
|
||||||
brands: Brand[] = [];
|
|
||||||
platforms: Platform[] = [];
|
|
||||||
categories: Category[] = [];
|
|
||||||
conditions: Condition[] = [];
|
|
||||||
|
|
||||||
filteredBrands: Brand[] = [];
|
|
||||||
filteredPlatforms: Platform[] = [];
|
|
||||||
|
|
||||||
private addProductSubscription: Subscription | null = null;
|
|
||||||
|
|
||||||
private brandControlSubscription: Subscription | null = null;
|
|
||||||
private platformControlSubscription: Subscription | null = null;
|
|
||||||
|
|
||||||
private brandSubscription: Subscription | null = null;
|
|
||||||
private platformSubscription: Subscription | null = null;
|
|
||||||
private categorySubscription: Subscription | null = null;
|
|
||||||
private conditionSubscription: Subscription | null = null;
|
|
||||||
|
|
||||||
private readonly brandService: BrandService = inject(BrandService);
|
|
||||||
private readonly platformService = inject(PlatformService);
|
|
||||||
private readonly categoryService = inject(CategoryService);
|
|
||||||
private readonly conditionService = inject(ConditionService);
|
|
||||||
|
|
||||||
private readonly productService = inject(ProductService);
|
|
||||||
|
|
||||||
constructor(private readonly formBuilder: FormBuilder) {
|
|
||||||
this.addProductForm = this.formBuilder.group({
|
|
||||||
title: ['', [
|
|
||||||
Validators.required,
|
|
||||||
Validators.minLength(3),
|
|
||||||
Validators.maxLength(50),
|
|
||||||
Validators.pattern(/^[\p{L}\p{N}\s]+$/u)
|
|
||||||
]],
|
|
||||||
description: ['', [
|
|
||||||
Validators.required,
|
|
||||||
Validators.minLength(10),
|
|
||||||
Validators.maxLength(255),
|
|
||||||
Validators.pattern(/^[\p{L}\p{N}\s]+$/u)
|
|
||||||
]],
|
|
||||||
category: ['', [
|
|
||||||
Validators.required
|
|
||||||
]],
|
|
||||||
condition: ['', [
|
|
||||||
Validators.required
|
|
||||||
]],
|
|
||||||
// stocker des ids (string|number) dans les controls
|
|
||||||
brand: ['', [
|
|
||||||
Validators.required
|
|
||||||
]],
|
|
||||||
platform: ['', [
|
|
||||||
Validators.required
|
|
||||||
]],
|
|
||||||
complete: [true],
|
|
||||||
manual: [true],
|
|
||||||
price: ['', [
|
|
||||||
Validators.required,
|
|
||||||
Validators.pattern(/^\d+([.,]\d{1,2})?$/),
|
|
||||||
this.priceRangeValidator(0, 9999)
|
|
||||||
]],
|
|
||||||
quantity: ['', [
|
|
||||||
Validators.required,
|
|
||||||
Validators.min(1),
|
|
||||||
Validators.max(999),
|
|
||||||
Validators.pattern(/^\d+$/)
|
|
||||||
]]
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private normalizeIds<T extends Record<string, any>>(items: T[] | undefined, idKey = 'id'): T[] {
|
|
||||||
return (items || []).map((it, i) => ({
|
|
||||||
...it,
|
|
||||||
[idKey]: (it[idKey] ?? i)
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
private getPlatformBrandId(platform: any): string | number | undefined {
|
|
||||||
if (!platform) return undefined;
|
|
||||||
const maybe = platform.brand ?? platform['brand_id'] ?? platform['brandId'];
|
|
||||||
if (maybe == null) return undefined;
|
|
||||||
|
|
||||||
if (typeof maybe === 'object') {
|
|
||||||
if (maybe.id != null) return maybe.id;
|
|
||||||
if (maybe.name != null) {
|
|
||||||
const found = this.brands.find(b =>
|
|
||||||
String(b.name).toLowerCase() === String(maybe.name).toLowerCase()
|
|
||||||
|| String(b.id) === String(maybe.name)
|
|
||||||
);
|
|
||||||
return found?.id;
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const asStr = String(maybe);
|
|
||||||
const match = this.brands.find(b =>
|
|
||||||
String(b.id) === asStr || String(b.name).toLowerCase() === asStr.toLowerCase()
|
|
||||||
);
|
|
||||||
return match?.id ?? maybe;
|
|
||||||
}
|
|
||||||
|
|
||||||
private priceRangeValidator(min: number, max: number): ValidatorFn {
|
|
||||||
return (control: AbstractControl) => {
|
|
||||||
const val = control.value;
|
|
||||||
if (val === null || val === undefined || val === '') return null;
|
|
||||||
const normalized = String(val).replace(',', '.').trim();
|
|
||||||
const num = Number.parseFloat(normalized);
|
|
||||||
if (Number.isNaN(num)) return {pattern: true};
|
|
||||||
return (num < min || num > max) ? {range: {min, max, actual: num}} : null;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
|
||||||
|
|
||||||
this.brandSubscription = this.brandService.getAll().subscribe({
|
|
||||||
next: (brands: Brand[]) => {
|
|
||||||
this.brands = this.normalizeIds(brands, 'id');
|
|
||||||
this.filteredBrands = [...this.brands];
|
|
||||||
},
|
|
||||||
error: (error: any) => {
|
|
||||||
console.error('Error fetching brands:', error);
|
|
||||||
},
|
|
||||||
complete: () => {
|
|
||||||
console.log('Finished fetching brands:', this.brands);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.platformSubscription = this.platformService.getAll().subscribe({
|
|
||||||
next: (platforms: Platform[]) => {
|
|
||||||
this.platforms = this.normalizeIds(platforms, 'id');
|
|
||||||
this.filteredPlatforms = [...this.platforms];
|
|
||||||
},
|
|
||||||
error: (error: any) => {
|
|
||||||
console.error('Error fetching platforms:', error);
|
|
||||||
},
|
|
||||||
complete: () => {
|
|
||||||
console.log('Finished fetching platforms:', this.platforms);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.categorySubscription = this.categoryService.getAll().subscribe({
|
|
||||||
next: (categories: Category[]) => {
|
|
||||||
this.categories = this.normalizeIds(categories, 'id');
|
|
||||||
},
|
|
||||||
error: (error: any) => {
|
|
||||||
console.error('Error fetching categories:', error);
|
|
||||||
},
|
|
||||||
complete: () => {
|
|
||||||
console.log('Finished fetching categories:', this.categories);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.conditionSubscription = this.conditionService.getConditions().subscribe({
|
|
||||||
next: (conditions: Condition[]) => {
|
|
||||||
this.conditions = this.normalizeIds(conditions, 'id');
|
|
||||||
},
|
|
||||||
error: (error) => {
|
|
||||||
console.error('Error fetching conditions:', error);
|
|
||||||
},
|
|
||||||
complete: () => {
|
|
||||||
console.log('Finished fetching conditions:', this.conditions);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const brandControl = this.addProductForm.get('brand');
|
|
||||||
const platformControl = this.addProductForm.get('platform');
|
|
||||||
|
|
||||||
this.brandControlSubscription = brandControl?.valueChanges.subscribe((brandId) => {
|
|
||||||
if (brandId != null && brandId !== '') {
|
|
||||||
const brandIdStr = String(brandId);
|
|
||||||
this.filteredPlatforms = this.platforms.filter(p => {
|
|
||||||
const pBid = this.getPlatformBrandId(p);
|
|
||||||
return pBid != null && String(pBid) === brandIdStr;
|
|
||||||
});
|
|
||||||
const curPlatformId = platformControl?.value;
|
|
||||||
if (curPlatformId != null && !this.filteredPlatforms.some(p => String(p.id) === String(curPlatformId))) {
|
|
||||||
platformControl?.setValue(null);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.filteredPlatforms = [...this.platforms];
|
|
||||||
}
|
|
||||||
}) ?? null;
|
|
||||||
|
|
||||||
this.platformControlSubscription = platformControl?.valueChanges.subscribe((platformId) => {
|
|
||||||
if (platformId != null && platformId !== '') {
|
|
||||||
const platformObj = this.platforms.find(p => String(p.id) === String(platformId));
|
|
||||||
const pBrandId = this.getPlatformBrandId(platformObj);
|
|
||||||
if (pBrandId != null) {
|
|
||||||
const pBrandIdStr = String(pBrandId);
|
|
||||||
this.filteredBrands = this.brands.filter(b => String(b.id) === pBrandIdStr);
|
|
||||||
const curBrandId = brandControl?.value;
|
|
||||||
if (curBrandId != null && String(curBrandId) !== pBrandIdStr) {
|
|
||||||
brandControl?.setValue(null);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.filteredBrands = [...this.brands];
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.filteredBrands = [...this.brands];
|
|
||||||
}
|
|
||||||
}) ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
|
||||||
this.addProductSubscription?.unsubscribe();
|
|
||||||
this.brandControlSubscription?.unsubscribe();
|
|
||||||
this.platformControlSubscription?.unsubscribe();
|
|
||||||
this.brandSubscription?.unsubscribe();
|
|
||||||
this.platformSubscription?.unsubscribe();
|
|
||||||
this.categorySubscription?.unsubscribe();
|
|
||||||
this.conditionSubscription?.unsubscribe();
|
|
||||||
}
|
|
||||||
|
|
||||||
onProductAdd() {
|
|
||||||
this.isSubmitted = true;
|
|
||||||
|
|
||||||
if (this.addProductForm.valid) {
|
|
||||||
this.isLoading = true;
|
|
||||||
const raw = this.addProductForm.value;
|
|
||||||
|
|
||||||
const priceStr = raw.price ?? '';
|
|
||||||
const priceNum = Number(String(priceStr).replace(',', '.').trim());
|
|
||||||
if (Number.isNaN(priceNum)) {
|
|
||||||
this.isLoading = false;
|
|
||||||
this.addProductForm.get('price')?.setErrors({pattern: true});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const quantityNum = Number(raw.quantity);
|
|
||||||
|
|
||||||
const payload = {
|
|
||||||
...raw,
|
|
||||||
price: priceNum,
|
|
||||||
quantity: quantityNum
|
|
||||||
};
|
|
||||||
|
|
||||||
this.addProductSubscription = this.productService.addProduct(payload).subscribe({
|
|
||||||
next: (response) => {
|
|
||||||
console.log("Product added successfully:", response);
|
|
||||||
this.addProductForm.reset();
|
|
||||||
this.isSubmitted = false;
|
|
||||||
alert("Produit ajouté avec succès !");
|
|
||||||
},
|
|
||||||
error: (error) => {
|
|
||||||
console.error("Error adding product:", error);
|
|
||||||
alert("Une erreur est survenue lors de l'ajout du produit.");
|
|
||||||
},
|
|
||||||
complete: () => {
|
|
||||||
this.isLoading = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
isFieldInvalid(fieldName: string): boolean {
|
|
||||||
const field = this.addProductForm.get(fieldName);
|
|
||||||
return Boolean(field && field.invalid && (field.dirty || field.touched || this.isSubmitted));
|
|
||||||
}
|
|
||||||
|
|
||||||
getFieldError(fieldName: string): string {
|
|
||||||
const field = this.addProductForm.get(fieldName);
|
|
||||||
|
|
||||||
if (field && field.errors) {
|
|
||||||
if (field.errors['required']) return `Ce champ est obligatoire`;
|
|
||||||
if (field.errors['email']) return `Format d'email invalide`;
|
|
||||||
if (field.errors['minlength']) return `Minimum ${field.errors['minlength'].requiredLength} caractères`;
|
|
||||||
if (field.errors['maxlength']) return `Maximum ${field.errors['maxlength'].requiredLength} caractères`;
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
compareById = (a: any, b: any) => {
|
|
||||||
if (a == null || b == null) return a === b;
|
|
||||||
if (typeof a !== 'object' || typeof b !== 'object') {
|
|
||||||
return String(a) === String(b);
|
|
||||||
}
|
|
||||||
return String(a.id ?? a) === String(b.id ?? b);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import {AdminNavbarComponent} from '../../components/admin-navbar/admin-navbar.component';
|
import {AdminNavbarComponent} from '../../components/navbar/admin-navbar/admin-navbar.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-admin',
|
selector: 'app-admin',
|
||||||
|
|||||||
@@ -3,8 +3,10 @@
|
|||||||
<h1>Bonjour, {{ user.firstName }}!</h1>
|
<h1>Bonjour, {{ user.firstName }}!</h1>
|
||||||
<p>Que souhaitez-vous faire ?</p>
|
<p>Que souhaitez-vous faire ?</p>
|
||||||
<br>
|
<br>
|
||||||
<button mat-flat-button [routerLink]="'/add-product'">Ajouter un nouveau produit</button>
|
<div class="home-actions">
|
||||||
<button mat-raised-button [routerLink]="'/products'">Voir la liste des produits</button>
|
<button mat-flat-button [routerLink]="'/products'">Voir la liste des produits</button>
|
||||||
|
<button mat-raised-button [routerLink]="'/admin'">Gérer la base de données</button>
|
||||||
|
</div>
|
||||||
} @else {
|
} @else {
|
||||||
<h2>Gestion des produits</h2>
|
<h2>Gestion des produits</h2>
|
||||||
<div class="home-actions">
|
<div class="home-actions">
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
||||||
|
|
||||||
import { HomeComponent } from './home.component';
|
|
||||||
|
|
||||||
describe('HomeComponent', () => {
|
|
||||||
let component: HomeComponent;
|
|
||||||
let fixture: ComponentFixture<HomeComponent>;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
await TestBed.configureTestingModule({
|
|
||||||
imports: [HomeComponent]
|
|
||||||
})
|
|
||||||
.compileComponents();
|
|
||||||
|
|
||||||
fixture = TestBed.createComponent(HomeComponent);
|
|
||||||
component = fixture.componentInstance;
|
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create', () => {
|
|
||||||
expect(component).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,16 +1,14 @@
|
|||||||
import {Component, inject} from '@angular/core';
|
import {Component, inject} from '@angular/core';
|
||||||
import {MatButton} from '@angular/material/button';
|
import {MatButton} from '@angular/material/button';
|
||||||
import {AuthService} from '../../services/auth/auth.service';
|
import {AuthService} from '../../services/auth/auth.service';
|
||||||
import {RouterLink} from '@angular/router';
|
import {Router, RouterLink} from '@angular/router';
|
||||||
import {AddProductComponent} from '../add-product/add-product.component';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-home',
|
selector: 'app-home',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [
|
imports: [
|
||||||
MatButton,
|
MatButton,
|
||||||
RouterLink,
|
RouterLink
|
||||||
AddProductComponent
|
|
||||||
],
|
],
|
||||||
templateUrl: './home.component.html',
|
templateUrl: './home.component.html',
|
||||||
styleUrl: './home.component.css'
|
styleUrl: './home.component.css'
|
||||||
@@ -18,6 +16,7 @@ import {AddProductComponent} from '../add-product/add-product.component';
|
|||||||
export class HomeComponent {
|
export class HomeComponent {
|
||||||
|
|
||||||
protected readonly authService: AuthService = inject(AuthService);
|
protected readonly authService: AuthService = inject(AuthService);
|
||||||
|
protected readonly router: Router = inject(Router);
|
||||||
|
|
||||||
getUser() {
|
getUser() {
|
||||||
return this.authService.user();
|
return this.authService.user();
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
||||||
|
|
||||||
import { LoginComponent } from './login.component';
|
|
||||||
|
|
||||||
describe('LoginComponent', () => {
|
|
||||||
let component: LoginComponent;
|
|
||||||
let fixture: ComponentFixture<LoginComponent>;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
await TestBed.configureTestingModule({
|
|
||||||
imports: [LoginComponent]
|
|
||||||
})
|
|
||||||
.compileComponents();
|
|
||||||
|
|
||||||
fixture = TestBed.createComponent(LoginComponent);
|
|
||||||
component = fixture.componentInstance;
|
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create', () => {
|
|
||||||
expect(component).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -50,12 +50,11 @@ export class LoginComponent implements OnDestroy {
|
|||||||
this.loginSubscription = this.authService.login(
|
this.loginSubscription = this.authService.login(
|
||||||
this.loginFormGroup.value as Credentials).subscribe({
|
this.loginFormGroup.value as Credentials).subscribe({
|
||||||
next: (result: User | null | undefined) => {
|
next: (result: User | null | undefined) => {
|
||||||
|
console.log(result);
|
||||||
this.navigateHome();
|
this.navigateHome();
|
||||||
alert('Login successful!');
|
|
||||||
},
|
},
|
||||||
error: (error) => {
|
error: (error) => {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
alert(error.message);
|
|
||||||
this.invalidCredentials = true;
|
this.invalidCredentials = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
||||||
|
|
||||||
import { NotFoundComponent } from './not-found.component';
|
|
||||||
|
|
||||||
describe('NotFoundComponent', () => {
|
|
||||||
let component: NotFoundComponent;
|
|
||||||
let fixture: ComponentFixture<NotFoundComponent>;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
await TestBed.configureTestingModule({
|
|
||||||
imports: [NotFoundComponent]
|
|
||||||
})
|
|
||||||
.compileComponents();
|
|
||||||
|
|
||||||
fixture = TestBed.createComponent(NotFoundComponent);
|
|
||||||
component = fixture.componentInstance;
|
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create', () => {
|
|
||||||
expect(component).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1 +1,7 @@
|
|||||||
<app-products-list></app-products-list>
|
<ng-container>
|
||||||
|
@if (showList) {
|
||||||
|
<app-product-list></app-product-list>
|
||||||
|
}
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<router-outlet></router-outlet>
|
||||||
|
|||||||
@@ -1,17 +1,43 @@
|
|||||||
import {
|
import {
|
||||||
Component,
|
Component, inject,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import {ProductsListComponent} from '../../components/products-list/products-list.component';
|
import {ProductListComponent} from '../../components/list/product-list/product-list.component';
|
||||||
|
import {ActivatedRoute, NavigationEnd, Router, RouterOutlet} from '@angular/router';
|
||||||
|
import {filter, Subscription} from 'rxjs';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-products',
|
selector: 'app-dialog',
|
||||||
templateUrl: './products.component.html',
|
templateUrl: './products.component.html',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [
|
imports: [
|
||||||
ProductsListComponent
|
ProductListComponent,
|
||||||
|
RouterOutlet
|
||||||
],
|
],
|
||||||
styleUrls: ['./products.component.css']
|
styleUrls: ['./products.component.css']
|
||||||
})
|
})
|
||||||
export class ProductsComponent {
|
export class ProductsComponent {
|
||||||
|
|
||||||
|
showList = true;
|
||||||
|
private sub?: Subscription;
|
||||||
|
private readonly router: Router = inject(Router);
|
||||||
|
private readonly route: ActivatedRoute = inject(ActivatedRoute);
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.updateShowList(this.route);
|
||||||
|
this.sub = this.router.events.pipe(
|
||||||
|
filter(evt => evt instanceof NavigationEnd)
|
||||||
|
).subscribe(() => this.updateShowList(this.route));
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateShowList(route: ActivatedRoute): void {
|
||||||
|
let current = route;
|
||||||
|
while (current.firstChild) {
|
||||||
|
current = current.firstChild;
|
||||||
|
}
|
||||||
|
this.showList = current === route;
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.sub?.unsubscribe();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,11 @@
|
|||||||
<mat-icon>account_circle</mat-icon>
|
<mat-icon>account_circle</mat-icon>
|
||||||
</div>
|
</div>
|
||||||
<mat-card-title>{{ user.firstName }} {{ user.lastName }}</mat-card-title>
|
<mat-card-title>{{ user.firstName }} {{ user.lastName }}</mat-card-title>
|
||||||
|
@if (user.role == "Administrator") {
|
||||||
<mat-card-subtitle>{{ user.username }} ({{ user.role }})</mat-card-subtitle>
|
<mat-card-subtitle>{{ user.username }} ({{ user.role }})</mat-card-subtitle>
|
||||||
|
} @else {
|
||||||
|
<mat-card-subtitle>{{ user.username }}</mat-card-subtitle>
|
||||||
|
}
|
||||||
</mat-card-header>
|
</mat-card-header>
|
||||||
<mat-card-content>
|
<mat-card-content>
|
||||||
<p>
|
<p>
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
||||||
|
|
||||||
import { ProfileComponent } from './profile.component';
|
|
||||||
|
|
||||||
describe('ProfileComponent', () => {
|
|
||||||
let component: ProfileComponent;
|
|
||||||
let fixture: ComponentFixture<ProfileComponent>;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
await TestBed.configureTestingModule({
|
|
||||||
imports: [ProfileComponent]
|
|
||||||
})
|
|
||||||
.compileComponents();
|
|
||||||
|
|
||||||
fixture = TestBed.createComponent(ProfileComponent);
|
|
||||||
component = fixture.componentInstance;
|
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create', () => {
|
|
||||||
expect(component).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,31 +1,32 @@
|
|||||||
import {inject, Injectable} from '@angular/core';
|
import {inject, Injectable} from '@angular/core';
|
||||||
import {HttpClient} from '@angular/common/http';
|
import {HttpClient} from '@angular/common/http';
|
||||||
|
import {Observable} from 'rxjs';
|
||||||
import {Condition} from '../../interfaces/condition';
|
import {Condition} from '../../interfaces/condition';
|
||||||
|
import {CrudService} from '../crud.service';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
export class ConditionService {
|
export class ConditionService implements CrudService<Condition> {
|
||||||
|
|
||||||
private readonly http = inject(HttpClient);
|
private readonly http = inject(HttpClient);
|
||||||
private readonly BASE_URL = 'http://localhost:3000/api/app/conditions';
|
private readonly BASE_URL = 'http://localhost:3000/api/app/conditions';
|
||||||
|
|
||||||
getConditions() {
|
getAll(): Observable<Condition[]> {
|
||||||
return this.http.get<Condition[]>(this.BASE_URL, {withCredentials: true});
|
return this.http.get<Condition[]>(this.BASE_URL, {withCredentials: true});
|
||||||
}
|
}
|
||||||
|
|
||||||
addCondition(condition: Condition) {
|
add(item: Condition): Observable<Condition> {
|
||||||
console.log("Adding condition:", condition);
|
console.log('Adding condition:', item);
|
||||||
return this.http.post(this.BASE_URL, condition, {withCredentials: true});
|
return this.http.post<Condition>(this.BASE_URL, item, {withCredentials: true});
|
||||||
}
|
}
|
||||||
|
|
||||||
updateCondition(id: string, condition: Condition) {
|
update(id: string | number, item: Condition): Observable<Condition> {
|
||||||
console.log("Updating condition:", id, condition);
|
console.log('Updating condition:', id, item);
|
||||||
return this.http.put(`${this.BASE_URL}/${id}`, condition, {withCredentials: true});
|
return this.http.put<Condition>(`${this.BASE_URL}/${id}`, item, {withCredentials: true});
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteCondition(id: string) {
|
delete(id: string | number): Observable<void> {
|
||||||
console.log("Deleting condition:", id);
|
console.log('Deleting condition:', id);
|
||||||
return this.http.delete(`${this.BASE_URL}/${id}`, {withCredentials: true});
|
return this.http.delete<void>(`${this.BASE_URL}/${id}`, {withCredentials: true});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,31 +1,32 @@
|
|||||||
import {inject, Injectable} from '@angular/core';
|
import {inject, Injectable} from '@angular/core';
|
||||||
import {HttpClient} from '@angular/common/http';
|
import {HttpClient} from '@angular/common/http';
|
||||||
|
import {Observable} from 'rxjs';
|
||||||
import {Product} from '../../interfaces/product';
|
import {Product} from '../../interfaces/product';
|
||||||
|
import {CrudService} from '../crud.service';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
export class ProductService {
|
export class ProductService implements CrudService<Product> {
|
||||||
|
|
||||||
private readonly http = inject(HttpClient);
|
private readonly http = inject(HttpClient);
|
||||||
private readonly BASE_URL = 'http://localhost:3000/api/app/products';
|
private readonly BASE_URL = 'http://localhost:3000/api/app/products';
|
||||||
|
|
||||||
getProducts() {
|
getAll(): Observable<Product[]> {
|
||||||
return this.http.get<Product[]>(this.BASE_URL, {withCredentials: true});
|
return this.http.get<Product[]>(this.BASE_URL, {withCredentials: true});
|
||||||
}
|
}
|
||||||
|
|
||||||
addProduct(product: Product) {
|
add(item: Product): Observable<Product> {
|
||||||
console.log("Adding product:", product);
|
console.log('Adding product:', item);
|
||||||
return this.http.post(this.BASE_URL, product, {withCredentials: true});
|
return this.http.post<Product>(this.BASE_URL, item, {withCredentials: true});
|
||||||
}
|
}
|
||||||
|
|
||||||
updateProduct(id: string, product: Product) {
|
update(id: string | number, item: Product): Observable<Product> {
|
||||||
console.log("Updating product:", id, product);
|
console.log('Updating product:', id, item);
|
||||||
return this.http.put(`${this.BASE_URL}/${id}`, product, {withCredentials: true});
|
return this.http.put<Product>(`${this.BASE_URL}/${id}`, item, {withCredentials: true});
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteProduct(id: string) {
|
delete(id: string | number): Observable<void> {
|
||||||
console.log("Deleting product:", id);
|
console.log('Deleting product:', id);
|
||||||
return this.http.delete(`${this.BASE_URL}/${id}`, {withCredentials: true});
|
return this.http.delete<void>(`${this.BASE_URL}/${id}`, {withCredentials: true});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
import { TestBed } from '@angular/core/testing';
|
|
||||||
|
|
||||||
import { UserService } from './user.service';
|
|
||||||
|
|
||||||
describe('UserService', () => {
|
|
||||||
let service: UserService;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
TestBed.configureTestingModule({});
|
|
||||||
service = TestBed.inject(UserService);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be created', () => {
|
|
||||||
expect(service).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import { Injectable } from '@angular/core';
|
|
||||||
|
|
||||||
@Injectable({
|
|
||||||
providedIn: 'root'
|
|
||||||
})
|
|
||||||
export class UserService {
|
|
||||||
|
|
||||||
constructor() { }
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user