Skip to content

Frontend Libraries & Architecture — Laundry Management System

Document Information

Field Value
Project Laundry Management System
Version 1.0
Language English
Framework Angular 19+
Document Type Frontend Libraries & Developer Guide

Table of Contents

  1. Project Structure
  2. Complete npm Package List
  3. Architecture Patterns
  4. NgRx — State Management
  5. PrimeNG Configuration
  6. i18n with ngx-translate
  7. Tauri Integration
  8. Print Services
  9. Export Services (Excel + PDF)
  10. Route Structure
  11. HTTP Interceptors
  12. Code Conventions
  13. ESLint + Prettier Configuration
  14. app.config.ts Template

1. Project Structure

frontend/laundry-app/
├── src/
│   ├── app/
│   │   ├── core/                         # Singleton services, guards, interceptors
│   │   │   ├── services/                 # api-base, auth, print, export, license, notification
│   │   │   ├── interceptors/             # jwt, client-token, error, loading
│   │   │   └── guards/                   # auth, license, role
│   │   ├── shared/                       # Reusable components, pipes, directives
│   │   │   ├── components/               # license-banner, confirm-dialog, status-badge, barcode-tag, empty-state
│   │   │   ├── pipes/                    # currency, date-hijri, status-label
│   │   │   └── directives/               # has-permission, auto-focus
│   │   ├── modules/                      # Lazy-loaded feature modules
│   │   │   ├── dashboard/                # Overview cards, revenue chart
│   │   │   ├── invoices/                 # Invoice CRUD, barcode tags
│   │   │   ├── customers/                # Customer CRUD, classifications
│   │   │   ├── payments/                 # Payment recording
│   │   │   ├── carpet/                   # Carpet receipts + invoices
│   │   │   ├── tailoring/                # Tailoring orders + tailors
│   │   │   ├── inventory/                # Stock + sales
│   │   │   ├── reports/                  # All 16 reports
│   │   │   ├── admin/                    # Settings, users, branches, sync
│   │   │   └── wizard/                   # Setup wizard (Tauri)
│   │   ├── layout/                       # App shell, sidebar, header
│   │   ├── app.component.ts
│   │   ├── app.config.ts                 # provideRouter, provideStore, provideHttpClient, provideTranslate
│   │   └── app.routes.ts
│   ├── assets/
│   │   ├── i18n/ {ar,en}.json
│   │   ├── styles/ {_variables, _rtl-arabic, _print-thermal, _print-a4}.scss
│   │   └── images/ {logo.svg}
│   └── environments/
│       ├── environment.ts
│       └── environment.prod.ts
├── src-tauri/                            # Tauri (Rust)
│   ├── tauri.conf.json
│   ├── Cargo.toml
│   └── src/main.rs
├── package.json
├── angular.json
├── tsconfig.json
├── jest.config.ts
├── .eslintrc.json
└── .prettierrc

2. Complete npm Package List

Package Version Purpose
@angular/core 19.* Core framework
@angular/router 19.* Routing and navigation
@angular/forms 19.* Reactive forms
@angular/common 19.* Common directives and pipes
@angular/platform-browser 19.* Browser support
@angular/service-worker 19.* PWA / offline caching
primeng 19.* UI component library
primeflex 3.* CSS utility classes
primeicons 7.* Icon library
chart.js 4.* Charting (via primeng/chart)
@ngx-translate/core 15.* i18n runtime translations
@ngx-translate/http-loader 8.* Load translation files
rxjs 7.* Reactive programming
xlsx (SheetJS) 0.20.* Excel export
html2canvas 1.* HTML to canvas
jspdf 2.* PDF generation
jsbarcode 3.* Barcode generation
@ngrx/store 18.* Redux store
@ngrx/effects 18.* Side effects
@ngrx/entity 18.* Entity CRUD helpers
@ngrx/router-store 18.* Router sync with store
@ngrx/store-devtools 18.* Redux DevTools
@ngrx/schematics 18.* CLI generators
@tauri-apps/api 2.* Tauri JS bridge
@tauri-apps/cli 2.* Tauri CLI
@tauri-apps/plugin-shell 2.* Docker commands
@tauri-apps/plugin-updater 2.* Auto-update
@tauri-apps/plugin-dialog 2.* File open/save dialogs
@tauri-apps/plugin-fs 2.* File system
@tauri-apps/plugin-process 2.* Process management
@tauri-apps/plugin-window-state 2.* Window state persistence
@tauri-apps/plugin-store 2.* Key-value config
@tauri-apps/plugin-os 2.* OS detection
typescript 5.* Language
@angular/cli 19.* Build tooling
jest 30.* Unit testing
jest-preset-angular 14.* Angular Jest preset
@types/jest 30.* Jest type definitions
@testing-library/angular 17.* Component testing helpers
eslint 9.* Linting
@angular-eslint/builder 19.* Angular ESLint
prettier 3.* Code formatting
prettier-plugin-organize-imports 4.* Organize imports
husky 9.* Git hooks
lint-staged 15.* Lint staged files

3. Architecture Patterns

Component Pattern — Smart/Dumb + Facade

Container (Smart)                         Presentational (Dumb)
┌──────────────────────┐                 ┌──────────────────────┐
│ InvoiceListContainer │                 │ InvoiceTable         │
│                      │                 │                      │
│ Injects: Facade      │── @Input ──────▶│ @Input() invoices[]  │
│ Uses: async pipe     │                 │ @Output() select     │
│ No HTTP calls        │◀─ @Output ────│                      │
│ changeDetection:Push │                 │ changeDetection:Push │
└──────────────────────┘                 └──────────────────────┘
    Injectable Facade
    ┌─────────────────┐
    │ InvoiceFacade    │
    │ dispatch(action) │
    │ select(selector) │
    │ NO NgRx import   │   ← Components never import Store/EFFECTS
    └─────────────────┘

NgRx Data Flow

Component (user clicks "Save")
Facade.dispatch(InvoiceActions.create(dto))
Action: [Invoice] Create
    ├──▶ Effect: calls API → dispatches [Invoice] Create Success / Failure
    └──▶ Reducer: updates state (loading = true)
         Selector: selectAllInvoices
         Component: async pipe renders updated list

4. NgRx — State Management

Store Registration (app.config.ts)

import { provideStore } from '@ngrx/store';
import { provideEffects } from '@ngrx/effects';
import { provideRouterStore } from '@ngrx/router-store';
import { provideStoreDevtools } from '@ngrx/store-devtools';
import { invoiceReducer } from './modules/invoices/store/invoice.reducer';
import { InvoiceEffects } from './modules/invoices/store/invoice.effects';

export const appConfig: ApplicationConfig = {
  providers: [
    provideStore({
      invoices: invoiceReducer,
      customers: customerReducer,
      payments: paymentReducer,
      carpet: carpetReducer,
      tailoring: tailoringReducer,
      inventory: inventoryReducer,
      reports: reportReducer,
      admin: adminReducer,
      wizard: wizardReducer,
    }),
    provideEffects([InvoiceEffects, CustomerEffects, PaymentEffects, ...]),
    provideRouterStore(),
    provideStoreDevtools({ maxAge: 50 }),
    // ... other providers
  ]
};

Actions

export const InvoiceActions = createActionGroup({
  source: 'Invoice',
  events: {
    'Load': emptyProps(),
    'Load Success': props<{ invoices: InvoiceDto[] }>(),
    'Load Failure': props<{ error: string }>(),
    'Create': props<{ dto: CreateInvoiceDto }>(),
    'Create Success': props<{ invoice: InvoiceDto }>(),
    'Create Failure': props<{ error: string }>(),
    'Change Status': props<{ id: string; status: OperationalStatus }>(),
    'Change Status Success': props<{ invoice: InvoiceDto }>(),
  }
});

Reducer

export const invoiceReducer = createReducer(
  initialState,
  on(InvoiceActions.load, (state) => ({ ...state, loading: true })),
  on(InvoiceActions.loadSuccess, (state, { invoices }) =>
    invoiceAdapter.setAll(invoices, { ...state, loading: false })),
  on(InvoiceActions.createSuccess, (state, { invoice }) =>
    invoiceAdapter.addOne(invoice, state)),
);

Effects

@Injectable()
export class InvoiceEffects {
  load$ = createEffect(() =>
    this.actions$.pipe(
      ofType(InvoiceActions.load),
      switchMap(() => this.invoiceService.getAll().pipe(
        map(invoices => InvoiceActions.loadSuccess({ invoices })),
        catchError(error => of(InvoiceActions.loadFailure({ error: error.message })))
      ))
    ));

  create$ = createEffect(() =>
    this.actions$.pipe(
      ofType(InvoiceActions.create),
      switchMap(({ dto }) => this.invoiceService.create(dto).pipe(
        map(invoice => InvoiceActions.createSuccess({ invoice })),
        catchError(error => of(InvoiceActions.createFailure({ error: error.message })))
      ))
    ));

  constructor(private actions$: Actions, private invoiceService: InvoiceService) {}
}

Facade

@Injectable({ providedIn: 'root' })
export class InvoiceFacade {
  private store = inject(Store);

  invoices$ = this.store.select(selectAllInvoices);
  loading$ = this.store.select(selectInvoiceLoading);
  selectedInvoice$ = this.store.select(selectSelectedInvoice);

  load(): void { this.store.dispatch(InvoiceActions.load()); }
  create(dto: CreateInvoiceDto): void { this.store.dispatch(InvoiceActions.create({ dto })); }
  changeStatus(id: string, status: OperationalStatus): void {
    this.store.dispatch(InvoiceActions.changeStatus({ id, status }));
  }
}

5. PrimeNG Configuration

// app.config.ts
import { providePrimeNG } from 'primeng/config';
import Aura from '@primeng/themes/aura';

export const appConfig: ApplicationConfig = {
  providers: [
    providePrimeNG({
      theme: { preset: Aura },
      ripple: true,
      translation: {
        // Customize PrimeNG text for AR/EN
        dayNames: ['Sun','Mon',...],
        monthNames: ['Jan',...],
      }
    }),
  ]
};

6. i18n with ngx-translate

// app.config.ts
import { provideTranslateService } from '@ngx-translate/core';

provideTranslateService({
  defaultLanguage: 'ar',
  useDefaultLang: true,
});

// ar.json example
{
  "INVOICE": { "TITLE": "الفاتورة", "NUMBER": "رقم الفاتورة", ... },
  "COMMON": { "SAVE": "حفظ", "CANCEL": "إلغاء", ... }
}

7. Tauri Integration

// core/services/tauri-bridge.service.ts
@Injectable({ providedIn: 'root' })
export class TauriBridgeService {
  private isTauri = !!(window as any).__TAURI_INTERNALS__;

  async startDockerServices(): Promise<string> {
    if (!this.isTauri) return 'Not running in Tauri';
    const { invoke } = await import('@tauri-apps/api/core');
    return invoke('start_docker_services');
  }

  async readLicenseFile(path: string): Promise<ArrayBuffer> {
    const { invoke } = await import('@tauri-apps/api/core');
    return invoke('read_license_file', { path });
  }

  async computeClientToken(): Promise<string> {
    const { invoke } = await import('@tauri-apps/api/core');
    return invoke('compute_client_token');
  }

  // ... other Tauri command wrappers
}

8. Print Services

@Injectable({ providedIn: 'root' })
export class PrintService {
  printThermalReceipt(element: HTMLElement): void {
    const printWindow = window.open('', '_blank', 'width=300,height=600');
    printWindow!.document.write(`
      <html><head>
        <link rel="stylesheet" href="/assets/styles/_print-thermal.scss">
      </head><body>${element.innerHTML}</body></html>
    `);
    printWindow!.document.close();
    printWindow!.focus();
    printWindow!.print();
    printWindow!.close();
  }

  printA4Invoice(element: HTMLElement): void {
    // Similar, with _print-a4.scss
  }
}

9. Export Services (Excel + PDF)

@Injectable({ providedIn: 'root' })
export class ExportService {
  toExcel(data: any[], filename: string, sheetName: string): void {
    const ws = XLSX.utils.json_to_sheet(data);
    const wb = XLSX.utils.book_new();
    XLSX.utils.book_append_sheet(wb, ws, sheetName);
    XLSX.writeFile(wb, `${filename}.xlsx`);
  }

  async toPDF(element: HTMLElement, filename: string): Promise<void> {
    const canvas = await html2canvas(element, { scale: 2 });
    const imgData = canvas.toDataURL('image/png');
    const pdf = new jsPDF('p', 'mm', 'a4');
    const imgWidth = 210;
    const imgHeight = (canvas.height * imgWidth) / canvas.width;
    pdf.addImage(imgData, 'PNG', 0, 0, imgWidth, imgHeight);
    pdf.save(`${filename}.pdf`);
  }
}

10. Route Structure

// app.routes.ts
export const routes: Routes = [
  { path: 'login',         loadComponent: () => import('./login/login.component') },
  { path: 'wizard',        loadChildren: () => import('./modules/wizard/routes') },
  {
    path: '',
    canActivate: [AuthGuard],
    loadComponent: () => import('./layout/app-shell/app-shell.component'),
    children: [
      { path: '',                 redirectTo: 'dashboard', pathMatch: 'full' },
      { path: 'dashboard',        loadChildren: () => import('./modules/dashboard/routes') },
      { path: 'invoices',         loadChildren: () => import('./modules/invoices/routes') },
      { path: 'customers',        loadChildren: () => import('./modules/customers/routes') },
      { path: 'payments',         loadChildren: () => import('./modules/payments/routes') },
      { path: 'carpet',           loadChildren: () => import('./modules/carpet/routes') },
      { path: 'tailoring',        loadChildren: () => import('./modules/tailoring/routes') },
      { path: 'inventory',        loadChildren: () => import('./modules/inventory/routes') },
      { path: 'reports',          loadChildren: () => import('./modules/reports/routes') },
      { path: 'admin',            loadChildren: () => import('./modules/admin/routes'), canActivate: [RoleGuard], data: { role: 'Admin' } },
    ]
  },
  { path: '**', redirectTo: '' }
];

11. HTTP Interceptors

// jwt.interceptor.ts
export const jwtInterceptor: HttpInterceptorFn = (req, next) => {
  const token = localStorage.getItem('access_token');
  if (token) {
    req = req.clone({ setHeaders: { Authorization: `Bearer ${token}` } });
  }
  return next(req);
};

// client-token.interceptor.ts
export const clientTokenInterceptor: HttpInterceptorFn = (req, next) => {
  const config = (window as any).__LAUNDRY_CONFIG__;
  if (config?.clientToken && config?.mode !== 'online') {
    req = req.clone({ setHeaders: { 'X-Laundry-Client-Token': config.clientToken } });
  }
  return next(req);
};

// app.config.ts
provideHttpClient(withInterceptors([jwtInterceptor, clientTokenInterceptor, errorInterceptor, loadingInterceptor]))

12. Code Conventions

Convention Rule
Files kebab-case for files: invoice-list.container.ts. PascalCase for classes: InvoiceListContainer.
Components Smart containers suffix -container. Dumb components use descriptive names.
Services Suffix Service for API. Suffix Facade for store abstraction.
Models Interfaces (not classes) for DTOs. interface InvoiceDto { ... }.
Imports Barrel exports in index.ts. Import from ./ not deep paths.
Async Use async pipe in templates. Subscribe only in Effects.
Change Detection OnPush on all components.
Standalone All components standalone: true. No NgModules.
i18n Use translate pipe or translateService.instant() for dynamic text. NEVER hardcode Arabic/English strings.

13. ESLint + Prettier Configuration

// .prettierrc
{
  "singleQuote": true,
  "trailingComma": "all",
  "printWidth": 100,
  "tabWidth": 2,
  "plugins": ["prettier-plugin-organize-imports"]
}
// .eslintrc.json
{
  "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:@angular-eslint/recommended"],
  "rules": {
    "@angular-eslint/component-selector": ["error", { "type": "element", "prefix": "app", "style": "kebab-case" }],
    "@angular-eslint/prefer-standalone": "error",
    "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }],
    "@typescript-eslint/explicit-function-return-type": "warn"
  }
}

14. app.config.ts Template

import { ApplicationConfig, isDevMode } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { provideAnimations } from '@angular/platform-browser/animations';
import { providePrimeNG } from 'primeng/config';
import Aura from '@primeng/themes/aura';
import { provideStore } from '@ngrx/store';
import { provideEffects } from '@ngrx/effects';
import { provideRouterStore } from '@ngrx/router-store';
import { provideStoreDevtools } from '@ngrx/store-devtools';
import { provideTranslateService } from '@ngx-translate/core';
import { routes } from './app.routes';
import { reducers } from './store';
import { effects } from './store/effects';
import { jwtInterceptor, clientTokenInterceptor, errorInterceptor, loadingInterceptor } from './core/interceptors';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes),
    provideHttpClient(withInterceptors([jwtInterceptor, clientTokenInterceptor, errorInterceptor, loadingInterceptor])),
    provideAnimations(),
    providePrimeNG({ theme: { preset: Aura }, ripple: true }),
    provideStore(reducers),
    provideEffects(effects),
    provideRouterStore(),
    provideStoreDevtools({ maxAge: 50, logOnly: !isDevMode() }),
    provideTranslateService({ defaultLanguage: 'ar', useDefaultLang: true }),
  ]
};

Revision History

Date Version Author Changes
2026-05-10 1.0 Frontend Lead Initial frontend libraries & architecture guide