Frontend Libraries & Architecture — Laundry Management System
| Field |
Value |
| Project |
Laundry Management System |
| Version |
1.0 |
| Language |
English |
| Framework |
Angular 19+ |
| Document Type |
Frontend Libraries & Developer Guide |
Table of Contents
- Project Structure
- Complete npm Package List
- Architecture Patterns
- NgRx — State Management
- PrimeNG Configuration
- i18n with ngx-translate
- Tauri Integration
- Print Services
- Export Services (Excel + PDF)
- Route Structure
- HTTP Interceptors
- Code Conventions
- ESLint + Prettier Configuration
- 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 |