add admin navbar and brand/platform management components
This commit is contained in:
@@ -4,7 +4,7 @@ import {provideRouter} from '@angular/router';
|
||||
import {routes} from './app.routes';
|
||||
import {provideHttpClient, withInterceptors} from '@angular/common/http';
|
||||
import {provideAnimationsAsync} from '@angular/platform-browser/animations/async';
|
||||
import {authTokenInterceptor} from './interceptors/authToken/auth-token.interceptor';
|
||||
import {authTokenInterceptor} from './interceptors/auth-token.interceptor';
|
||||
import {AuthService} from './services/auth/auth.service';
|
||||
import {catchError, firstValueFrom, of} from 'rxjs';
|
||||
|
||||
|
||||
@@ -3,10 +3,10 @@ import {HomeComponent} from './pages/home/home.component';
|
||||
import {RegisterComponent} from './pages/register/register.component';
|
||||
import {LoginComponent} from './pages/login/login.component';
|
||||
import {ProfileComponent} from './pages/profile/profile.component';
|
||||
import {guestOnlyCanActivate, guestOnlyCanMatch} from './guards/guest-only/guest-only.guard';
|
||||
import {authOnlyCanActivate, authOnlyCanMatch} from './guards/auth-only/auth-only.guard';
|
||||
import {guestOnlyCanActivate, guestOnlyCanMatch} from './guards/guest-only.guard';
|
||||
import {authOnlyCanActivate, authOnlyCanMatch} from './guards/auth-only.guard';
|
||||
import {AdminComponent} from './pages/admin/admin.component';
|
||||
import {adminOnlyCanActivate, adminOnlyCanMatch} from './guards/admin-only/admin-only.guard';
|
||||
import {adminOnlyCanActivate, adminOnlyCanMatch} from './guards/admin-only.guard';
|
||||
import {AddProductComponent} from './pages/add-product/add-product.component';
|
||||
|
||||
export const routes: Routes = [
|
||||
|
||||
@@ -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>
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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> 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>
|
||||
138
client/src/app/components/brands-list/brands-list.component.ts
Normal file
138
client/src/app/components/brands-list/brands-list.component.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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> 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>
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { inject } from '@angular/core';
|
||||
import { CanActivateFn, CanMatchFn, Router, UrlTree, ActivatedRouteSnapshot, Route } from '@angular/router';
|
||||
import { AuthService } from '../../services/auth/auth.service';
|
||||
import { AuthService } from '../services/auth/auth.service';
|
||||
|
||||
function requireAdmin(url?: string): boolean | UrlTree {
|
||||
const authService: AuthService = inject(AuthService);
|
||||
@@ -1,17 +0,0 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { CanActivateFn } from '@angular/router';
|
||||
|
||||
import { adminOnlyGuard } from './admin-only.guard';
|
||||
|
||||
describe('adminOnlyGuard', () => {
|
||||
const executeGuard: CanActivateFn = (...guardParameters) =>
|
||||
TestBed.runInInjectionContext(() => adminOnlyGuard(...guardParameters));
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(executeGuard).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import { inject } from '@angular/core';
|
||||
import { CanActivateFn, CanMatchFn, Router, UrlTree, ActivatedRouteSnapshot, Route } from '@angular/router';
|
||||
import { AuthService } from '../../services/auth/auth.service';
|
||||
import { AuthService } from '../services/auth/auth.service';
|
||||
|
||||
function requireAuth(url?: string): boolean | UrlTree {
|
||||
const authService = inject(AuthService);
|
||||
@@ -1,17 +0,0 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { CanMatchFn } from '@angular/router';
|
||||
|
||||
import { authOnlyGuard } from './auth-only.guard';
|
||||
|
||||
describe('authOnlyGuard', () => {
|
||||
const executeGuard: CanMatchFn = (...guardParameters) =>
|
||||
TestBed.runInInjectionContext(() => authOnlyGuard(...guardParameters));
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(executeGuard).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import { inject } from '@angular/core';
|
||||
import { Router, UrlTree, CanActivateFn, CanMatchFn } from '@angular/router';
|
||||
import { AuthService } from '../../services/auth/auth.service';
|
||||
import { AuthService } from '../services/auth/auth.service';
|
||||
|
||||
function redirectIfLoggedIn(): boolean | UrlTree {
|
||||
const authService = inject(AuthService);
|
||||
@@ -1,17 +0,0 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { CanMatchFn } from '@angular/router';
|
||||
|
||||
import { guestOnlyGuard } from './guest-only.guard';
|
||||
|
||||
describe('guestOnlyGuard', () => {
|
||||
const executeGuard: CanMatchFn = (...guardParameters) =>
|
||||
TestBed.runInInjectionContext(() => guestOnlyGuard(...guardParameters));
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(executeGuard).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import {HttpErrorResponse, HttpInterceptorFn} from '@angular/common/http';
|
||||
import {inject} from '@angular/core';
|
||||
import {AuthService} from '../../services/auth/auth.service';
|
||||
import {AuthService} from '../services/auth/auth.service';
|
||||
import {catchError, switchMap, throwError} from 'rxjs';
|
||||
|
||||
let isRefreshing = false;
|
||||
@@ -19,7 +19,7 @@ export const authTokenInterceptor: HttpInterceptorFn = (req, next) => {
|
||||
catchError((error: any) => {
|
||||
const is401 = error instanceof HttpErrorResponse && error.status === 401;
|
||||
|
||||
// si 401 et pas déjà en refresh, tente un refresh puis rejoue la requête 1 fois
|
||||
// si 401 et pas déjà en refresh, tente un refresh puis rejoue la requête une fois
|
||||
if (is401 && !isRefreshing) {
|
||||
isRefreshing = true;
|
||||
return inject(AuthService).refresh().pipe(
|
||||
@@ -1,17 +0,0 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { HttpInterceptorFn } from '@angular/common/http';
|
||||
|
||||
import { authTokenInterceptor } from './auth-token.interceptor';
|
||||
|
||||
describe('authTokenInterceptor', () => {
|
||||
const interceptor: HttpInterceptorFn = (req, next) =>
|
||||
TestBed.runInInjectionContext(() => authTokenInterceptor(req, next));
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(interceptor).toBeTruthy();
|
||||
});
|
||||
});
|
||||
4
client/src/app/interfaces/brand.ts
Normal file
4
client/src/app/interfaces/brand.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export interface Brand {
|
||||
id: string | number;
|
||||
name: string;
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import { Credentials } from './credentials';
|
||||
|
||||
describe('Credentials', () => {
|
||||
it('should create an instance', () => {
|
||||
expect(new Credentials()).toBeTruthy();
|
||||
});
|
||||
});
|
||||
7
client/src/app/interfaces/platform.ts
Normal file
7
client/src/app/interfaces/platform.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import {Brand} from './brand';
|
||||
|
||||
export interface Platform {
|
||||
id: string | number;
|
||||
name: string;
|
||||
brand: Brand | undefined;
|
||||
}
|
||||
7
client/src/app/interfaces/user.ts
Normal file
7
client/src/app/interfaces/user.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export interface User {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
username: string;
|
||||
email: string ;
|
||||
role: string;
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import { Brand } from './brand';
|
||||
|
||||
describe('Brand', () => {
|
||||
it('should create an instance', () => {
|
||||
expect(new Brand()).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -1,3 +0,0 @@
|
||||
export class Brand {
|
||||
name: string = '';
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import { UserModel } from './user.model';
|
||||
|
||||
describe('UserModel', () => {
|
||||
it('should create an instance', () => {
|
||||
expect(new UserModel()).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -1,7 +0,0 @@
|
||||
export class User {
|
||||
firstName: string = '';
|
||||
lastName: string = '';
|
||||
username: string = '';
|
||||
email: string = '';
|
||||
role: string = '';
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
<section class="auth-wrap">
|
||||
<mat-card class="auth-card">
|
||||
<mat-card-header>
|
||||
<mat-card-title>Gestion des produits</mat-card-title>
|
||||
<mat-card-subtitle>Create a new account</mat-card-subtitle>
|
||||
<mat-card-title>Ajouter un produit</mat-card-title>
|
||||
</mat-card-header>
|
||||
|
||||
<mat-card-content>
|
||||
@@ -61,7 +60,7 @@
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Marque</mat-label>
|
||||
<mat-select disableRipple>
|
||||
@for (brand of brands; track brand.name) {
|
||||
@for (brand of brands; track brand.id) {
|
||||
<mat-option [value]="brand">{{ brand.name }}</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
@@ -71,9 +70,9 @@
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Plateforme</mat-label>
|
||||
<mat-select disableRipple>
|
||||
<mat-option value="1">Option 1</mat-option>
|
||||
<mat-option value="2">Option 2</mat-option>
|
||||
<mat-option value="3">Option 3</mat-option>
|
||||
@for (platform of platforms; track platform.id) {
|
||||
<mat-option [value]="platform">{{ platform.name }}</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AddProductComponent } from './add-product.component';
|
||||
|
||||
describe('AddProductComponent', () => {
|
||||
let component: AddProductComponent;
|
||||
let fixture: ComponentFixture<AddProductComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [AddProductComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(AddProductComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
MatCardActions,
|
||||
MatCardContent,
|
||||
MatCardHeader,
|
||||
MatCardSubtitle,
|
||||
MatCardTitle
|
||||
} from "@angular/material/card";
|
||||
import {MatCheckbox} from "@angular/material/checkbox";
|
||||
@@ -24,7 +23,9 @@ import {MatOption, MatSelect} from '@angular/material/select';
|
||||
import {Router, RouterLink} from '@angular/router';
|
||||
import {Subscription} from 'rxjs';
|
||||
import {BrandService} from '../../services/brand/brand.service';
|
||||
import {Brand} from '../../models/brand/brand';
|
||||
import {Brand} from '../../interfaces/brand';
|
||||
import {PlatformService} from '../../services/platform/platform.service';
|
||||
import {Platform} from '../../interfaces/platform';
|
||||
|
||||
@Component({
|
||||
selector: 'app-add-product',
|
||||
@@ -36,7 +37,6 @@ import {Brand} from '../../models/brand/brand';
|
||||
MatCardActions,
|
||||
MatCardContent,
|
||||
MatCardHeader,
|
||||
MatCardSubtitle,
|
||||
MatCardTitle,
|
||||
MatCheckbox,
|
||||
MatDivider,
|
||||
@@ -60,11 +60,13 @@ export class AddProductComponent implements OnInit, OnDestroy {
|
||||
isLoading = false;
|
||||
|
||||
brands: Brand[] = [];
|
||||
platforms: Platform[] = [];
|
||||
|
||||
private readonly router: Router = inject(Router);
|
||||
|
||||
private addProductSubscription: Subscription | null = null;
|
||||
private readonly addProductSubscription: Subscription | null = null;
|
||||
private readonly brandService: BrandService = inject(BrandService);
|
||||
private readonly platformService = inject(PlatformService);
|
||||
|
||||
constructor(private readonly formBuilder: FormBuilder) {
|
||||
this.addProductForm = this.formBuilder.group({
|
||||
@@ -126,6 +128,18 @@ export class AddProductComponent implements OnInit, OnDestroy {
|
||||
console.log('Finished fetching brands:', this.brands);
|
||||
}
|
||||
});
|
||||
|
||||
this.platformService.getPlatforms().subscribe({
|
||||
next: (platforms) => {
|
||||
this.platforms = platforms;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error fetching platforms:', error);
|
||||
},
|
||||
complete: () => {
|
||||
console.log('Finished fetching platforms:', this.platforms);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
|
||||
@@ -1 +1 @@
|
||||
<p>admin works!</p>
|
||||
<app-admin-navbar></app-admin-navbar>
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AdminComponent } from './admin.component';
|
||||
|
||||
describe('AdminComponent', () => {
|
||||
let component: AdminComponent;
|
||||
let fixture: ComponentFixture<AdminComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [AdminComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(AdminComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -1,12 +1,15 @@
|
||||
import { Component } from '@angular/core';
|
||||
import {AdminNavbarComponent} from '../../components/admin-navbar/admin-navbar.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-admin',
|
||||
standalone: true,
|
||||
imports: [],
|
||||
templateUrl: './admin.component.html',
|
||||
styleUrl: './admin.component.css'
|
||||
standalone: true,
|
||||
imports: [
|
||||
AdminNavbarComponent
|
||||
],
|
||||
styleUrls: ['./admin.component.scss']
|
||||
})
|
||||
export class AdminComponent {
|
||||
export class AdminComponent{
|
||||
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@ import {FormBuilder, ReactiveFormsModule, Validators} from '@angular/forms';
|
||||
import {AuthService} from '../../services/auth/auth.service';
|
||||
import {Router} from '@angular/router';
|
||||
import {Subscription} from 'rxjs';
|
||||
import {Credentials} from '../../interfaces/credentials/credentials';
|
||||
import {User} from '../../models/user/user.model';
|
||||
import {Credentials} from '../../interfaces/credentials';
|
||||
import {User} from '../../interfaces/user';
|
||||
import {MatInput} from '@angular/material/input';
|
||||
import {MatButton} from '@angular/material/button';
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
import {MatIcon} from '@angular/material/icon';
|
||||
import {MatButton} from '@angular/material/button';
|
||||
import {AuthService} from '../../services/auth/auth.service';
|
||||
import {User} from '../../models/user/user.model';
|
||||
import {User} from '../../interfaces/user';
|
||||
import {Router} from '@angular/router';
|
||||
|
||||
@Component({
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AuthService } from './auth.service';
|
||||
|
||||
describe('LoginService', () => {
|
||||
let service: AuthService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
service = TestBed.inject(AuthService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -1,8 +1,8 @@
|
||||
import {inject, Injectable, signal} from '@angular/core';
|
||||
import {catchError, map, of, switchMap, tap} from 'rxjs';
|
||||
import {Credentials} from '../../interfaces/credentials/credentials';
|
||||
import {Credentials} from '../../interfaces/credentials';
|
||||
import {HttpClient} from '@angular/common/http';
|
||||
import {User} from '../../models/user/user.model';
|
||||
import {User} from '../../interfaces/user';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { BrandService } from './brand.service';
|
||||
|
||||
describe('BrandService', () => {
|
||||
let service: BrandService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
service = TestBed.inject(BrandService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import {inject, Injectable} from '@angular/core';
|
||||
import {HttpClient} from '@angular/common/http';
|
||||
import {Brand} from '../../models/brand/brand';
|
||||
import {Brand} from '../../interfaces/brand';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
@@ -8,9 +8,24 @@ import {Brand} from '../../models/brand/brand';
|
||||
export class BrandService {
|
||||
|
||||
private readonly http = inject(HttpClient);
|
||||
private readonly BASE_URL = 'http://localhost:3000/brands';
|
||||
private readonly BASE_URL = 'http://localhost:3000/api/brands';
|
||||
|
||||
getBrands() {
|
||||
return this.http.get<Brand[]>(this.BASE_URL, {withCredentials: true});
|
||||
}
|
||||
|
||||
addBrand(brand: Brand) {
|
||||
console.log("Adding brand:", brand);
|
||||
return this.http.post(this.BASE_URL, brand, {withCredentials: true});
|
||||
}
|
||||
|
||||
updateBrand(id: string, brand: Brand) {
|
||||
console.log("Updating brand:", id, brand);
|
||||
return this.http.put(`${this.BASE_URL}/${id}`, brand, {withCredentials: true});
|
||||
}
|
||||
|
||||
deleteBrand(id: string) {
|
||||
console.log("Deleting brand:", id);
|
||||
return this.http.delete(`${this.BASE_URL}/${id}`, {withCredentials: true});
|
||||
}
|
||||
}
|
||||
|
||||
31
client/src/app/services/platform/platform.service.ts
Normal file
31
client/src/app/services/platform/platform.service.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import {inject, Injectable} from '@angular/core';
|
||||
import {HttpClient} from '@angular/common/http';
|
||||
import {Platform} from '../../interfaces/platform';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class PlatformService {
|
||||
|
||||
private readonly http = inject(HttpClient);
|
||||
private readonly BASE_URL = 'http://localhost:3000/api/platforms';
|
||||
|
||||
getPlatforms() {
|
||||
return this.http.get<Platform[]>(this.BASE_URL, {withCredentials: true});
|
||||
}
|
||||
|
||||
addPlatform(platform: Platform) {
|
||||
console.log("Adding platform:", platform);
|
||||
return this.http.post(this.BASE_URL, platform, {withCredentials: true});
|
||||
}
|
||||
|
||||
updatePlatform(id: string, platform: Platform) {
|
||||
console.log("Updating platform:", id, platform);
|
||||
return this.http.put(`${this.BASE_URL}/${id}`, platform, {withCredentials: true});
|
||||
}
|
||||
|
||||
deletePlatform(id: string) {
|
||||
console.log("Deleting platform:", id);
|
||||
return this.http.delete(`${this.BASE_URL}/${id}`, {withCredentials: true});
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ProductService } from './product.service';
|
||||
|
||||
describe('ProductService', () => {
|
||||
let service: ProductService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
service = TestBed.inject(ProductService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user