This project follows strict engineering standards to ensure high-quality, maintainable code.
readonly properties.clients, loans).kebab-case.component.ts (e.g., user-profile.component.ts).kebab-case.service.ts (e.g., client-data.service.ts).kebab-case.model.ts (e.g., client-summary.model.ts).The UI layer is Ionic (@ionic/angular v8). Ionic is configured in mode: 'md' (src/app/app.config.ts), so components render in Material Design styling.
Angular Material has been removed entirely, and
npm run lintfails on any import of it. The table below records the equivalents, which are also the conventions for new code.@angular/cdkis retained and is fine to use for unstyled primitives (cdk-table, virtual scroll, a11y) — the shared data table is built on it.
Always import individual components from the standalone entry point and list them in the component's own imports array. Never use IonicModule.
import { IonButton, IonIcon } from '@ionic/angular/standalone'; @Component({ standalone: true, imports: [TranslateModule, IonButton, IonIcon], // ... })
| Material (removed) | Ionic equivalent |
|---|---|
mat-card | ion-card / ion-card-header / ion-card-title / ion-card-content |
mat-form-field + matInput | ion-item fill="outline" + ion-label position="stacked" + ion-input |
mat-select / mat-option | ion-select / ion-select-option |
button mat-button | ion-button |
button mat-icon-button + mat-icon | ion-button fill="clear" + ion-icon slot="icon-only" |
mat-checkbox / mat-slide-toggle | ion-checkbox / ion-toggle |
mat-spinner | ion-spinner |
mat-progress-bar | ion-progress-bar |
mat-datepicker | ion-datetime-button + ion-modal + ion-datetime (see below) |
matTooltip | [attr.title], or [attr.aria-label] on icon-only controls |
MatSnackBar | NotificationService (see below) |
MatDialog | DialogService (see below) |
mat-table + mat-paginator | the shared app-data-table, or cdk-table + app-paginator directly |
Ionic has no tooltip component. Use [attr.title] for hover text and always give icon-only buttons an [attr.aria-label].
<ion-item fill="outline"> <ion-label position="stacked">{{ 'TELLERS.START_DATE' | translate }}</ion-label> <ion-datetime-button datetime="start-date-picker"></ion-datetime-button> <ion-modal [keepContentsMounted]="true"> <ng-template> <ion-datetime id="start-date-picker" data-testid="start-date-picker" presentation="date" (ionChange)="onStartDateChange($event)" ></ion-datetime> </ng-template> </ion-modal> </ion-item>
The datetime attribute on the button must match the id on ion-datetime, and [keepContentsMounted]="true" is required for the value to bind before first open.
Ionic emits CustomEvents. Read detail.value, and keep a target.value fallback — unit tests dispatch plain DOM events:
onInput(event: Event): void { const value = (event as CustomEvent<{ value?: string }>).detail?.value ?? (event.target as HTMLInputElement)?.value ?? ''; // ... }
Bind (ionInput) for text entry and (ionChange) for selects, toggles and date pickers.
Icons are ionicons, referenced by kebab-case name:
<ion-icon name="create-outline" slot="icon-only"></ion-icon>
Every icon must be registered in src/app/core/icons.ts, which src/bootstrap.ts passes to addIcons() once at startup. An unregistered name renders as a blank space, silently. To add one, import it from ionicons/icons and add an entry to APP_ICONS; icons.spec.ts guards the registry's integrity.
Inject the services rather than Ionic's controllers directly — they keep durations, placement and confirm semantics consistent, and they are far easier to mock in tests.
private readonly notifications = inject(NotificationService); private readonly dialogService = inject(DialogService); this.notifications.success(this.translate.instant('CLIENTS.SAVED')); this.notifications.error(message); // 10s, preserves newlines in stacked API errors const confirmed = await this.dialogService.confirm({ title: this.translate.instant('COMMON.DELETE'), message: this.translate.instant('LOANS.CONFIRM_DELETE_NOTE'), destructive: true, });
Design tokens live in src/styles/_common.scss (--primary-color, --card-bg, --border-color, …). src/styles/_ionic-theme.scss maps Ionic's --ion-color-* and surface variables onto them, so Ionic components inherit the Fineract palette automatically.
--background, --border-radius, --padding-start) and shadow parts (::part(message)) — not by targeting internal classes.[data-theme='dark'] attribute set by ThemeService. Do not introduce a second mechanism such as Ionic's .ion-palette-dark or a bare prefers-color-scheme query.styles: [] array. Keep them small — the production build errors above 8 kB per component.npm run format to apply formatting; npm run format:check in CI.npm run lint. Note the sonarjs/void-use rule — the void operator is banned, so do not use void somePromise() to mark a floating promise.Runner: Vitest (npm test). The jest.config.ts in the repo root is unused.
Unit Tests: Every service and complex component should have a corresponding test.ts.
Test Coverage: Aim for at least 80% statement coverage.
Ionic in TestBed: Components that use Ionic overlays — directly, or via NotificationService / DialogService — need Ionic's providers, or they fail with NG0201: No provider found for _ModalController:
import { provideIonicTesting } from '../../testing/ionic-testing'; TestBed.configureTestingModule({ imports: [MyComponent, TranslateModule.forRoot()], providers: [provideIonicTesting()], });
Selectors: Give interactive elements both an id and a data-testid. The Playwright suite prefers getByRole/getByTestId; never write assertions against framework-internal classes.
npm run i18n:check to verify translations are complete.