Skip to main content

πŸ—οΈ Architettura e Routing del Backoffice

Questa pagina spiega come l’app si avvia, come sono composti i moduli principali e come funziona il routing (incluso lazy loading e guardie).


πŸš€ Entry point: main.ts​

import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './modules/app.module';

platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.error(err));

πŸ“¦ AppModule​

ResponsabilitΓ :

  • Import dei moduli core (BrowserModule, RouterModule, StoreModule, EffectsModule, ecc.).
  • Configurazione NgRx (reducer login, LoginEffects, devtools).
  • Configurazione ngx-translate con loader custom AlbiTranslateLoader.
  • Registrazione servizi globali (Auth, Backend, Storage, Messaggistica).
  • Bootstrap del root component HomeComponent.
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { RouterModule } from '@angular/router';

import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
import { FormsModule } from '@angular/forms';
import { EffectsModule } from '@ngrx/effects';
import { StoreModule } from '@ngrx/store';
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
import { HomeComponent } from 'src/home/home.component';
import { AuthGuard } from 'src/login/guards/auth.guard';
import { LoginEffects } from 'src/login/store/login.effects';
import { loginReducer } from 'src/login/store/login.reducers';
import { AuthService } from 'src/services/auth.service';
import { BackendService } from 'src/services/backend.service';
import { DECLARATIONS, ROUTES } from './app.routing';
import { SharedModule } from './shared.module';
// import ngx-translate and the http loader
import { HttpClient } from '@angular/common/http';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { TranslateHttpLoader } from '@ngx-translate/http-loader';
import { HeaderMessageService } from 'src/services/header-message.service';
import { LocalStorageService } from 'src/services/local-storage.service';
import { MessageService } from 'src/services/message.service';
import { AlbiTranslateLoader } from './albiTranslateLoader';

@NgModule({
declarations: [
...DECLARATIONS
],
bootstrap: [HomeComponent], imports: [BrowserModule,
BrowserAnimationsModule,
// ngx-translate and the loader module
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useClass: AlbiTranslateLoader
}
}),
FormsModule,
RouterModule.forRoot(ROUTES),
StoreModule.forRoot({ login: loginReducer }),
EffectsModule.forRoot([LoginEffects]),
StoreDevtoolsModule.instrument({
maxAge: 25, // Retains last 25 states
logOnly: true, // Restrict extension to log-only mode
connectInZone: true
}),
SharedModule
],
providers: [
AuthService,
AuthGuard,
BackendService,
LocalStorageService,
HeaderMessageService,
MessageService,
provideHttpClient(withInterceptorsFromDi()),
]
})
export class AppModule { }

// required for AOT compilation
export function HttpLoaderFactory(http: HttpClient): TranslateHttpLoader {
return new TranslateHttpLoader(http);
}

🏠 HomeComponent (root)​

Cosa fa:

  • Imposta lingua di default (en) via TranslateService.
  • Salva l’URL richiesto alla prima navigazione (utile per redirect post-login).
  • Inizializza lo stato di login da storage (initLoginStateFromStorage).
  • Applica la classe tema light-mode al container root.
import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef } from '@angular/core';
import { NavigationStart, Router } from '@angular/router';
import { Store } from '@ngrx/store';
import { TranslateService } from "@ngx-translate/core";
import { filter, map, take, tap } from 'rxjs';
import { LOGIN_PAGE_ACTIONS } from 'src/login/store/login.actions';
import { LoginState } from 'src/login/store/login.state';
import { AuthService } from 'src/services/auth.service';

@Component({
selector: 'app-root',
templateUrl: `./home.component.html`,
styleUrls: ['./home.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: false
})
export class HomeComponent implements AfterViewInit {

constructor(
private _store: Store<LoginState>,
private _authService: AuthService,
private _router: Router,
private _translate: TranslateService,
private _elementRef: ElementRef
) {
this._translate.setDefaultLang('en');
this._translate.use('en');
this._router.events.pipe(
filter(ev => ev instanceof NavigationStart),
map(ev => ev as NavigationStart),
tap((ev: NavigationStart) => this._authService.setRequestedUrl(ev.url)),
take(1),
).subscribe();
this._store.dispatch(LOGIN_PAGE_ACTIONS.initLoginStateFromStorage());
}
ngAfterViewInit(): void {
this._elementRef.nativeElement.classList.add('light-mode');
}
}

πŸ›£οΈ Routing principale (app.routing.ts)​

Concetti chiave:

  • Redirect iniziale '' β†’ /dashboard.
  • DashboardComponent Γ¨ protetto da AuthGuard (solo utenti autenticati e nel gruppo admin su Cognito hanno accesso).
  • Le sezioni sono caricate in lazy loading.
import { Routes } from '@angular/router';
import { AddressFormComponentComponent } from '@dashboard-components/address-form-component/address-form-component.component';
import { AppPageLayoutComponent } from '@dashboard-components/app-page-layout/app-page-layout.component';
import { CompanyCreationFormComponent } from '@dashboard-components/company-creation-form/company-creation-form.component';
import { GlassCounterComponent } from '@dashboard-components/glass-counter/glass-counter.component';
import { ImageCropComponent } from '@dashboard-components/image-crop-component/image-crop.component';
import { JsonViewerComponent } from '@dashboard-components/json-viewer/json-viewer.component';
import { PageTabsComponent } from '@dashboard-components/page-tabs-component/page-tabs.component';
import { VintageCreationFormComponent } from '@dashboard-components/vintage-creation-form/vintage-creation-form.component';
import { WineCreationFormComponent } from '@dashboard-components/wine-creation-form/wine-creation-form.component';
import { DashboardComponent } from 'src/dashboards/dashboard/dashboard.component';
import { CalculateQuantityPipe } from 'src/dashboards/pipes/calculateQuantityPipe';
import { DateFormatPipe } from 'src/dashboards/pipes/dateFormat.pipe';
import { DescriptionPipe } from 'src/dashboards/pipes/description.pipe';
import { GetFileFormatPipe } from 'src/dashboards/pipes/getFileFormat.pipe';
import { IncludedInArrayPipe } from 'src/dashboards/pipes/includedInArrayPipe';
import { MapObjectArrayPipe } from 'src/dashboards/pipes/mapObjectArray.pipe';
import { NumberToFixed } from 'src/dashboards/pipes/numberToFixed.pipe';
import { ObjectKeysPipe } from 'src/dashboards/pipes/objectKeys.pipe';
import { PrettyJsonPipe } from 'src/dashboards/pipes/prettyJson.pipe';
import { ResolveChipsArrayPipe } from 'src/dashboards/pipes/resolveChipsArray.pipe';
import { ResolveCountryCodePipe } from 'src/dashboards/pipes/resolveCountryCode.pipe';
import { ResolveNestedObjectFieldPipe } from 'src/dashboards/pipes/resolveNestedObjectFieldPipe';
import { ResolveUserCoordinatesPipe } from 'src/dashboards/pipes/resolveUserCoordinates.pipe';
import { SliderValueFormatPipe } from 'src/dashboards/pipes/sliderFormat.pipe';
import { UserNameOrEmailPipe } from 'src/dashboards/pipes/userEmailOrName.pipe';
import { CalculateAgePipe } from 'src/dashboards/user/pipes/calculateAge.Pipe';
import { ResolveCantinaAvailabilityPipe } from 'src/dashboards/user/pipes/resolveCantinaAvailability.pipe';
import { CalculateRatingsPercentagePipe } from 'src/dashboards/wine/pipes/calculateRatingsPercentage.pipe';
import { GetStatusLabelPipe } from 'src/dashboards/wine/pipes/getStatusLabel.pipe';
import { ImageStringBooleanPipe } from 'src/dashboards/wine/pipes/imageStringBoolean.pipe';
import { LocaleDescriptionPipe } from 'src/dashboards/wine/pipes/localeDescription.pipe';
import { ResolveFullnamePipe } from 'src/dashboards/wine/pipes/resolveFullname.pipe';
import { NotFoundComponent } from 'src/errros/not-found/not-found.component';
import { HomeComponent } from 'src/home/home.component';
import { AuthGuard } from 'src/login/guards/auth.guard';
import { LoginComponent } from 'src/login/login.component';
import { DestroyableSubscription } from 'src/utils/destroyable-subscription';

export const DECLARATIONS = [
LoginComponent,
HomeComponent,
DashboardComponent,
NotFoundComponent,
];

export const COMPONENT_DECLARATIONS = [
AppPageLayoutComponent,
]

export const EXPORT_DECLARATIONS = [
DestroyableSubscription,
DescriptionPipe,
DateFormatPipe,
UserNameOrEmailPipe,
GetFileFormatPipe,
GetStatusLabelPipe,
CalculateAgePipe,
ResolveCountryCodePipe,
SliderValueFormatPipe,
ObjectKeysPipe,
ResolveFullnamePipe,
LocaleDescriptionPipe,
ImageStringBooleanPipe,
ResolveNestedObjectFieldPipe,
CalculateQuantityPipe,
IncludedInArrayPipe,
CalculateRatingsPercentagePipe,
ResolveUserCoordinatesPipe,
GlassCounterComponent,
PageTabsComponent,
WineCreationFormComponent,
VintageCreationFormComponent,
ResolveChipsArrayPipe,

CompanyCreationFormComponent,
AddressFormComponentComponent,
ImageCropComponent,
NumberToFixed,
PrettyJsonPipe,
JsonViewerComponent,
ResolveCantinaAvailabilityPipe,
MapObjectArrayPipe,
];

export const ROUTES: Routes = [
{
path: '',
redirectTo: 'dashboard',
pathMatch: 'full'
},
{
path: 'login',
component: LoginComponent,
},
{
path: 'dashboard',
component: DashboardComponent,
canActivate: [AuthGuard],
children: [
{
path: 'wines',
loadChildren: () => import('./../dashboards/wine/modules/wine.module').then(m => m.WineModule)
},
{
path: 'companies',
loadChildren: () => import('../dashboards/company/company.module').then(m => m.CompanyModule)
},
{
path: 'document-pending',
loadChildren: () => import('../dashboards/document-verification/document-verification.module').then(m => m.DocumentVerificationModule)
},
{
path: 'companies/create',
loadChildren: () => import('../dashboards/company/company-create/company-create.module').then(m => m.CompanyCreateModule)
},
{
path: 'companies/:id',
loadChildren: () => import('../dashboards/company/company-detail/modules/company-detail.module').then(m => m.CompanyDetailModule)
},
{
path: 'users',
loadChildren: () => import('../dashboards/user/user.module').then(m => m.UserModule),
},
{
path: 'users/:id',
loadChildren: () => import('../dashboards/user/user-detail/modules/user-detail.module').then(m => m.UserDetailModule),
},
{
path: 'dispensers',
loadChildren: () => import('../dashboards/dispenser/dispenser.module').then(m => m.DispenserModule)
},
{
path: 'firmwares',
loadChildren: () => import('../dashboards/firmwares/firmwares.module').then(m => m.FirmwaresModule)
},
{
path: 'firmware-updates',
loadChildren: () => import('../dashboards/firmware-updates-job/firmware-updates-job.module').then(m => m.FirmwareUpdatesJobModule)
},
{
path: 'dispensers/:id',
loadChildren: () => import('../dashboards/dispenser/dispenser-detail/modules/dispenser-detail.module').then(m => m.DispenserDetailModule)
},
{
path: 'tags',
loadChildren: () => import('../dashboards/tags/tags.module').then(m => m.TagsModule)
},
{
path: 'smartbags',
loadChildren: () => import('../dashboards/smartbags/smartbags.module').then(m => m.SmartbagsModule)
},
{
path: 'smartbags/:id',
loadChildren: () => import('../dashboards/smartbags/smartbag-detail/smartbag-detail.module').then(m => m.SmartbagsDetailModule)
},
{
path: 'companies/:id/wine-lists/:winelistId',
loadChildren: () => import('../dashboards/wine-list-detail/wine-list-detail.module').then(m => m.WineListDetailModule)
},
{
path: 'qrcode',
loadChildren: () => import('../dashboards/qrcode/qrcode.module').then(m => m.QrcodeModule)
},
]
},
{
path: 'not-found',
component: NotFoundComponent
},
{
path: '**',
redirectTo: '/not-found'
}
];

πŸ“¦ SharedModule​

import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core';
import { AlbiUiLibraryModule } from "albi-ui/dist/albi-ui-library";
import { ImageCropperComponent } from 'ngx-image-cropper';
import { NgxQrcodeStylingComponent } from 'ngx-qrcode-styling';
import { COMPONENT_DECLARATIONS, EXPORT_DECLARATIONS } from './app.routing';

@NgModule({
declarations: [
...EXPORT_DECLARATIONS,
...COMPONENT_DECLARATIONS,
],
imports: [
CommonModule,
FormsModule,
ReactiveFormsModule,
RouterModule,
TranslateModule,
AlbiUiLibraryModule,
ImageCropperComponent,
NgxQrcodeStylingComponent,
],
exports: [
...EXPORT_DECLARATIONS,
...COMPONENT_DECLARATIONS,
FormsModule,
ReactiveFormsModule,
TranslateModule,
AlbiUiLibraryModule,
ImageCropperComponent,
RouterModule,
NgxQrcodeStylingComponent,
]
})
export class SharedModule { }

🌍 Traduzioni (AlbiTranslateLoader)​

import { TranslateLoader } from '@ngx-translate/core';
import { Observable, of } from 'rxjs';
import { EN_TRANSLATIONS } from 'src/translations/en/index.translations';
import { IT_TRANSLATIONS } from 'src/translations/it/index.translations';

export class AlbiTranslateLoader implements TranslateLoader {
getTranslation(lang: string): Observable<any> {
switch (lang) {
case 'it':
return of(IT_TRANSLATIONS);
case 'en':
default:
return of(EN_TRANSLATIONS);
}
}
}

πŸ”Ž Riepilogo operativo​

  • Bootstrap: main.ts β†’ AppModule.
  • Root: HomeComponent inizializza lingua, richieste e stato login.
  • Accesso: AuthGuard protegge /dashboard (richiede utente autenticato nel gruppo admin su Cognito).
  • Navigazione: lazy modules sotto /dashboard/* (wines, companies, users, dispensers, …).
  • Shared: componenti/pipe comuni centralizzati in SharedModule.
  • i18n: traduzioni fornite da AlbiTranslateLoader (en, it).

Per aggiungere una nuova sezione, creare un modulo in src/dashboards/<feature> ed esporre il relativo FeatureModule tramite loadChildren nelle ROUTES.