05 — Frontend Features
Every feature folder under frontend/src/app/features/, what its components do, what services they consume, and the key flows. Architecture/shells are in 04; endpoints in 07.
Pattern note. Every feature page that lists rows uses the same shape:
- PrimeNG
<p-table [lazy]="true" (onLazyLoad)="onLazy(...)" [paginator]="true" [totalRecords]="total()">for server-side paging.- A "filters" panel with a search input (debounced 300 ms in
onSearchChange) + an advanced filter accordion (<p-panel [toggleable]="true">).<app-import-export-toolbar>if the resource supports CRUD imports.auth.hasPermission('...')guards on Create / Edit / Delete buttons; the route guard already restricts read access.- Bilingual columns through the
localizepipe:{{ row.namePrimary | localize: row.nameSecondary }}.
Index
- Auth
- Dashboard
- Users & Roles & Permissions
- Master Data
- Assets
- Transfers
- Check-outs
- Audits
- Maintenance
- Reports
- Notifications & Profile
- Settings
- Audit log & Login audit
- Mobile shell
Auth
LoginComponent — features/auth/login/login.component.ts
Route: /login (no guard). Single component for login + password change prompts.
- Reactive form:
email,password,deviceId(auto-generated). - Calls
auth.login(...). On success,switchMapchains intoloadCurrentUser()so navigation only happens afterauthReadyflips true. - Theme + language pickers visible on the login page itself so non-English users land in their preferred language pre-auth.
- Branded background + dark-mode override block.
Dashboard
DashboardComponent — features/dashboard/dashboard.component.ts
Route: / (auth only — every signed-in user lands here).
- Tile grid summarizing counts/charts based on permissions: open audit assignments, pending reviews, in-progress work orders, recent transfers, etc.
- Each tile is conditional on the matching permission. A user with only
audit-assignment.readsees only the audit tile. - The "My audit tasks" widget builds its task list with
dueAt: null, overdue: false—DueDateis no longer part ofAuditAssignment. - Polls counts on focus + a refresh button.
- Quick links to
/my-audits,/pending-reviews,/work-orders.
Users & Roles & Permissions
UserListComponent — features/users/user-list/
Route /users, permission user.read. Standard list page: search, filter by IsActive/UserType/role, paged. Create-user dialog with full bilingual name + initial password + initial roles. Edit links to /users/:id. Soft-delete confirmation.
UserDetailComponent — features/users/user-detail/
Route /users/:id, any of user.read/create/update. Tabs:
- Profile — bilingual name, email, username, phone, manager, organization, default location, theme/language preferences.
- Roles — assign/remove (
AssignRoleToUser/RemoveRoleFromUser) withvalidFrom/validUntiltime bounds. - Direct permissions — grant/deny overrides (
GrantPermission/RevokePermission). - Effective permissions — read-only flat list pulled from
GetUserEffectivePermissions. - Sessions — table of active
RefreshTokenswith revoke action (RevokeUserSession).
RoleListComponent — features/roles/role-list/
Route /roles, permission role.read. Lists roles with member count + permission count. Built-in/system flags as tags. Create dialog.
RoleDetailComponent — features/roles/role-detail/
Route /roles/:id. Vertical-stacked module cards with collapsible accordions, a search filter across permission keys, a dirty-tracked save bar, and dark-mode overrides. Key signals:
permissionGroups()— module-grouped permission catalogselectedPerms,initialPerms— current vs loaded selectiondirty()— set-comparison computedsearchQuery,expandedGroups,filteredGroups()- Save calls
AssignPermissionsToRole, then snapshotsinitialPermsso the Save button greys out again.
PermissionsBrowserComponent — features/permissions/permissions-browser.component.ts
Route /permissions, permission permission.read. Read-only catalog browser: groups by Module, color-coded per "verb" segment (read/view/list/export blue, create/add green, update amber, delete red). Bilingual search; dangerous-permission badge.
Master Data
Five entities, three hierarchical and two flat. Each hierarchical entity has a list page + a tree page.
Organizations
OrganizationsComponent—features/master-data/organizations/organizations.component.ts. List view, hierarchy-aware filters (HierarchyPickerComponent). Create dialog with parent picker + manager dropdown. Includes<app-import-export-toolbar resource="organizations">.OrganizationTreeComponent—features/master-data/organizations/organization-tree/. Read-only tree view via<app-hierarchy-tree>. Permission:organization.view-tree(granular split — viewing the tree is a separate capability from viewing the list).
Locations
Same shape, with extra fields: LocationType enum picker, lat/lng inputs, geofence radius, address. LocationsComponent + LocationTreeComponent.
Classifications
Same shape, with RequiresSerialNumber, RequiresMaintenancePlan, DefaultUsefulLifeMonths. ClassificationsComponent + ClassificationTreeComponent.
Vendors / Manufacturers
Flat list pages — no tree, no hierarchy picker. Create/edit dialogs with all entity fields. <app-import-export-toolbar> wired to bulk import.
Assets
AssetListComponent — features/assets/asset-list/asset-list.component.ts
Route /assets, asset.read. The most-used page in the system.
- Filters: search, organization (via
HierarchyPickerComponent), location, classification, status, IsCritical, includeDescendants toggle. - Status column uses
statusLabel(key, namePrimary, nameSecondary)which prefersenum.assetStatus.{Key}translation and falls back to the localized DB name — this lets admins extendAssetStatusrows while keeping seeded statuses translatable. - Hierarchy chain tooltip (recent feature): info icon next to Organization/Location/Classification cells; tooltip shows root → leaf chain joined by
›. Backed byOrganizationChain/LocationChain/ClassificationChainarrays returned byGetAssets(built server-side). The icon hides when the chain is one node — seechainTooltip(chain)helper. - Print-label dropdown: single label or A4 sheet via
<app-asset-label>/<app-asset-label-sheet>. - Edit button shows when
asset.updateis held; otherwise the row link is "view-only" (eye icon).
AssetDetailComponent — features/assets/asset-detail/
Route /assets/:id. Tabs:
- Profile — bilingual name/description, image, org/location/classification/status pickers (each via
HierarchyPickerComponent), critical toggle, condition rating, acquisition method, current custodian dropdown. - Details — serial number, manufacturer, vendor, model, dimensions, prices, warranty, useful life, custom JSON.
- History — unified timeline merging
AssetStatusHistory+AssetLocationHistory+AssetOrganizationHistory+AssetCustodyHistory(viaGetAssetHistory). - Documents —
<app-documents-panel ownerType="Asset" [ownerId]="id">.
The form's status changes route through ChangeAssetStatus (writes a status-history row); other field updates go through UpdateAsset which the handler decomposes into per-history-table writes.
Transfers
TransferListComponent — features/transfers/transfer-list/
Route /transfers, asset-transfer.read.
- Filter by status, requester, date range.
- Status tag colors keyed off the
TransferStatusenum. - "Submit transfer" / "Approve" / "Reject" / "Receive lines" / "Complete" / "Cancel" actions all permission-gated. Most actions live on the detail page; the list mostly shows the row + status.
TransferDetailComponent — features/transfers/transfer-detail/
Route /transfers/:id.
- Header — code, status tag, from/to organization+location, requester + approver, requested/approved/completed timestamps, expected completion date.
- Reason — Quill rich-text editor (
<p-editor>); the backend stores it asnvarchar(max)so embedded images are allowed. Read-only after Submit. - Lines — table of
AssetTransferLines; each shows the asset code + name + per-lineReceivedAt/ReceivedStatus. Adding/removing only works whileStatus=Draft. - Action bar — context-dependent:
SubmitwhileDraft,Approve/RejectwhileSubmitted,ReceiveLine/ReceiveAll/CompletewhileInTransit,CanceluntilApproved. - Toast on success, problem-details detail on error.
- Reject opens a dialog asking for
RejectionReason(required by validator).
Check-outs
CheckOutsComponent — features/checkouts/checkouts.component.ts
Route /checkouts, check-out.read.
- Single-page list + create dialog (no separate detail page).
- Filters: status, custodian, date range.
- Create-dialog form: asset picker (filtered to assets without an active check-out — server enforces
UX (AssetId) WHERE [Status] = 'Active'), custodian picker, expected return date, purpose textarea. - Check-in dialog: condition rating (1–5), return notes.
- Cancel action soft-cancels an active check-out and restores the asset's previous status.
Audits
The largest feature module. Eight components covering plan management, assignment, mobile execution (shared with /mobile/*), and review.
AuditPlansComponent — features/audits/audit-plans/
Route /audit-plans, audit-plan.read. List of plans with status/priority filters. Status timeline tag (Draft/Scheduled/InProgress/Completed/Cancelled) with color severity. Create button → /audit-plans/new.
AuditPlanCreateComponent — features/audits/audit-plan-create/
Route /audit-plans/new, audit-plan.create. Multi-step form:
- Step 1: name, description, priority.
- Step 2: scope picker — repeating block where each row picks a
ScopeTypeand the matching fields.Combinedmode shows three pickers (Org/Loc/Class) and anIncludeChildrentoggle. The "Preview scope" button callsPreviewScopeto show how many assets resolve before creating. - Step 3: review + submit.
StartDate/EndDate are not part of the plan entity — the form has no date inputs.
AuditPlanDetailComponent — features/audits/audit-plan-detail/
Route /audit-plans/:id. Plan header + tabs:
- Scopes — read-only view of resolution rules.
- Assignments — table of
AuditAssignments. Create-assignment dialog (one active per plan invariant — server-enforced). Cancel action. - Audit results — once submitted, links to
/audit-results/:id.
MyAuditsComponent — features/audits/my-audits/
Route /my-audits, audit-assignment.read. Filtered to AssignedUserId == currentUser. Sorted CreatedAt DESC.
AssignmentDetailComponent — features/audits/assignment-detail/
Route /audit-assignments/:id. The desktop alternative to the mobile scanner. Tables for expected assets (one row per AuditAssignmentAsset) and extra findings (rows the auditor wants to add).
Notable UX choices:
- Extra findings asset dropdown shows only assets out of scope (subtracts expected ids from
assetOptions). - Outcome for extra rows is a static
Extratag (no picker). - Identification method for extra rows is a static
Manualtag. - Outcome for expected rows still picks from 4 outcomes (
Found/NotFound/LocationMismatch/OrganizationMismatch).
Submit calls SubmitResult with a fresh clientSubmissionId (idempotency).
PendingReviewsComponent — features/audits/pending-reviews/
Route /pending-reviews, audit-result.review. Reviewer's queue: list of audit results in ReviewStatus=PendingReview or InReview. Click → /audit-results/:id.
ResultReviewComponent — features/audits/result-review/
Route /audit-results/:id, audit-result.read.
- Header: result code, submitter, plan, assignment, submission timestamp, review status.
- Lines table — each
AuditResultLinewith outcome tag, observed location/org, condition rating, photos (thumbnails viaauditPhotoasync pipe). Inline review-action buttons: Approve, Reject, Modify (opens dialog with reviewer-corrected location/org pickers). - Bulk approve at the top — multiselect rows, single click to approve all.
- When all lines reviewed → assignment flips to
Completed; emitsaudit.result.approvedto submitter.
Maintenance
MaintenancePlansComponent — features/maintenance/maintenance-plans/
Route /maintenance-plans, maintenance-plan.read.
- List with filters: search, code, IsActive, frequency, NextDue range.
- "Assets" column shows the leaf code +
+Nfor additional assets (multi-asset plans, recent migration). - Create + edit dialog with
<p-multiSelect>for asset list, frequency picker, custom-days input (whenFrequency=CustomDays), next-due picker, default vendor. - "Generate work orders" action emits one WO per plan-asset, blocked when
NextDueDate > now.
MaintenancePlanDetailComponent — features/maintenance/maintenance-plan-detail/
Route /maintenance-plans/:id. Header + chip strip of plan-assets + table of historical work orders generated from this plan.
canGenerate()computed:plan.isActive && plan.assets.length > 0 && !notYetDue().notYetDue()computed:plan.nextDueDate && new Date(plan.nextDueDate) > now.- Generate button disabled with a tooltip showing the next-allowed date when
notYetDue.
MaintenanceRequestsComponent — features/maintenance/maintenance-requests/
Route /maintenance-requests, maintenance-request.read. List + filters (severity, status, asset, mineOnly). Create dialog with asset picker, severity, summary, description. Promote-to-WO action opens a dialog with WO details (type, priority, scheduled dates, assignee). Reject action requires reason.
WorkOrdersComponent — features/maintenance/work-orders/
Route /work-orders, work-order.read. List with filters (status, type, priority, assignee, date range). Create-WO dialog. Detail link.
WorkOrderDetailComponent — features/maintenance/work-order-detail/
Route /work-orders/:id.
- Header: status tag, priority tag, type, schedule, asset link, origin (Plan/Request/Manual link).
- Action toolbar (status-aware):
Open → Assigned: Assign (dialog: user OR vendor) —work-order.assignAssigned → InProgress: Start —work-order.updateInProgress → Completed: Complete (dialog: actual end, downtime hours, labor/parts cost, currency, resolution notes) —work-order.closeOpen|Assigned|OnHold → Cancelled: Cancel —work-order.update
- Costs auto-summed:
TotalCost = LaborCost + PartsCost.
Reports
ReportsHubComponent — features/reports/reports-hub/
Route /reports, any of the six per-report keys. Tile grid linking to each report. Tile shows only when the user has the matching report.{key}.read permission.
Per-report pages
All under features/reports/, all share the same shape: filterable table + Excel/PDF export buttons.
| Component | Route | Permission | Filters |
|---|---|---|---|
AssetInventoryReportComponent |
/reports/asset-inventory |
report.asset-inventory.read |
search, org, loc, class, status, isCritical |
AuditResultsReportComponent |
/reports/audit-results |
report.audit-results.read |
result-level filters |
AuditHistoryReportComponent |
/reports/audit-history |
report.audit-history.read |
search, asset, plan, outcome, reviewStatus, advanced filters |
MaintenanceHistoryReportComponent |
/reports/maintenance-history |
report.maintenance-history.read |
asset, work-order status, type, date range |
TransferHistoryReportComponent |
/reports/transfer-history |
report.transfer-history.read |
status, requester, date range |
CheckoutActivityReportComponent |
/reports/checkout-activity |
report.checkout-activity.read |
custodian, status, date range |
Export buttons gated by report.{key}.export-excel and report.{key}.export-pdf (granular per-format keys, so an organization can allow PDF-only or Excel-only access per role).
Notifications & Profile
NotificationsInboxComponent — features/notifications/notifications-inbox.component.ts
Routes /notifications and /mobile/notifications. Renders rendered text body via {{ n.body }} — InApp body comes pre-stripped of HTML by NotificationService.ToPlainText().
- Tabs: All / Unread.
- Filter by template key, severity.
- Per-row mark-as-read, delete; toolbar mark-all-read, clear all.
- Click row → if
n.linkis set,router.navigateByUrl(n.link). Cross-shell links are a known sharp edge — see 04 §Cross-shell redirect.
NotificationPreferencesComponent — features/notifications/preferences.component.ts
Route /preferences, me.notification-preference.update. Per-template-key, per-channel toggle grid. Lists every event from NotificationEventCatalog (returned by GetAvailableTemplates), each with InApp + Email checkboxes. Bulk update via UpdateMyPreferences.
ProfileComponent — features/profile/profile.component.ts
Routes /profile and /mobile/profile, me.profile.read.
- Editable: bilingual full name, phone, preferred language (drives the i18n service), preferred theme.
- Avatar upload (limited; backend stores
AvatarUrl). - Change password section (current + new + confirm) →
ChangePassword.
Settings
HierarchyConfigComponent — features/settings/hierarchy-config/
Route /settings/hierarchy, hierarchy-config.read. Per EntityType (Location/Organization/Classification), edits the level chain: drag-reorder, label primary/secondary inputs (with appLangDir), per-level CodeDigits. Save calls SetHierarchyConfig.
NotificationTemplatesComponent — features/settings/notification-templates/
Route /settings/notification-templates, notification-template.read.
- Master-detail layout: list of templates on the left, editor on the right.
- Editor body uses Quill (
<p-editor>from PrimeNG). For each row:Key,Channel,SubjectPrimary+SubjectSecondarytext inputs,BodyPrimary+BodySecondaryQuill instances. - "Insert merge field" toolbar reads available fields from
NotificationEventCatalog. - "Preview" button calls
PreviewTemplateto render a sample with mocked merge values. - Quirk: Quill auto-wraps text in
<p>and converts spaces to . The backendNotificationService.Rendermethod normalizes both before substituting merge fields.
AppLanguagesComponent — features/settings/app-languages/
Route /settings/languages, app-language.manage. Two dropdowns: PrimaryLanguageCode and SecondaryLanguageCode (must differ; usually en + ar). Updating these reshapes how LocalizePipe resolves bilingual fields system-wide.
TranslationsSettingsComponent — features/settings/translations/
Route /settings/translations, translation.read. Search + per-language-tab editor for the Translations table (admin overrides over the bundled translations.ts). Edit calls UpdateTranslation; reset-to-default calls DeleteTranslation.
EmailProviderSettingsComponent — features/settings/email-provider/
Route /settings/email, email-settings.manage. Form for the singleton row: SMTP host/port/SSL/credentials, From + Reply-To, redirect-all toggle for staging, batch size + poll interval + max attempts. "Send test email" button calls TestEmailProviderSettings. Password entered as plain text → backend encrypts via IDataProtector.
Audit log & Login audit
AuditLogComponent — features/audit-log/audit-log.component.ts
Route /audit-log, audit-log.read. Paged log with filters: entity name (dropdown sourced from GetEntityNames), entity id, user, action, date range, correlation id. Each row: collapsible "Changes" panel showing the JSON diff.
LoginAuditComponent — features/login-audit/login-audit.component.ts
Route /login-audit, login-audit.read. Login attempt log filtered by email, result, IP, date range. Result tag colors keyed off LoginResult enum.
Mobile shell
Three pages live under the /mobile shell. Two of them (profile, notifications) reuse the shared components.
MobileHomeComponent — features/mobile/mobile-home/
Route /mobile. Card list of the user's active assignments (GetMyAssignments) sorted CreatedAt DESC. Each card → /mobile/assignment/:id. Status tag, plan name, asset count.
MobileAssignmentComponent — features/mobile/mobile-assignment/
Route /mobile/assignment/:id, audit-assignment.read.
- Header: assignment summary, plan name, status, asset count.
- Two methods to complete an audit:
- Scan — embeds
<app-scan-audit>(see below). - Excel — three-step flow: download template, fill offline, upload back via
SubmitResultFromExcel.
- Scan — embeds
- Read-only assets table at the bottom (the assignment's expected asset list).
ScanAuditComponent — features/mobile/scan-audit/
Embedded by MobileAssignmentComponent. Camera-based scanner.
Html5Qrcodefromhtml5-qrcode.startScanning()enumerates cameras viaHtml5Qrcode.getCameras(), picks the first with aback/rear/environmentlabel or the last device, falls back tofacingMode: 'environment'. Specific error handlers forNotAllowedError/NotFoundError/NotReadableError/OverconstrainedErrorproduce useful toasts.- Hierarchical location filter — auditor picks a location level (Building → Floor → Room) from a dropdown. The filter options are derived from
expectedAssets[].expectedLocationChainso auditors only navigate through locations that actually contain assignment assets.availableLevels(),visibleLevels(),optionsAt(level)computeds drive this. Anchor logic: deepest common ancestor of all expected chains is auto-locked. - Scan flow — decoded QR → extract asset id (regex on
/assets/(<guid>)) →onDecoded→ flip an existing expected row fromexpected-pendingtofound(orlocation-mismatchif not in the active filter) or push a newextrarow. Per-outcome audio beep + vibration feedback. - Manual entry —
<p-dialog>to type the printed asset code →LookupAssetByCodeForAudit→applyManualHit. - Photo upload — per-row file picker; sends multipart to
UploadAuditPhoto; document id stored on the row. - Per-row edit — dialog for
observedConditionRating(1–5 rating component) +userNotes. - Auto-save draft —
effect(() => ...)watchesrows()andselectedByLevel(); debounces 1 s, then PUTsSaveDraftwith the JSON payload.applyDraftPayload(payload)rehydrates on load. Mute preference persisted inlocalStorage["scan-audit:muted"]. - Submit — confirm dialog showing scanned/found/missing/mismatch counts (with a soft-warning when
>25%of expected items still missing) →SubmitResult. - SCSS: keep
background:#000on.reader-wrapand#scan-reader-regiononly —::ng-deep div { background:#000 !important }paints over the qr-box overlay and kills the camera.
Where to go next
| To learn… | See |
|---|---|
| Build / run / seed / deploy | 06-operations |
| Endpoint URLs that these components call | 07-api-reference |
| The handler behind each component's API call | 02-backend-modules |