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 localize pipe: {{ row.namePrimary | localize: row.nameSecondary }}.

Index

  1. Auth
  2. Dashboard
  3. Users & Roles & Permissions
  4. Master Data
  5. Assets
  6. Transfers
  7. Check-outs
  8. Audits
  9. Maintenance
  10. Reports
  11. Notifications & Profile
  12. Settings
  13. Audit log & Login audit
  14. Mobile shell

Auth

LoginComponentfeatures/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, switchMap chains into loadCurrentUser() so navigation only happens after authReady flips 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

DashboardComponentfeatures/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.read sees only the audit tile.
  • The "My audit tasks" widget builds its task list with dueAt: null, overdue: falseDueDate is no longer part of AuditAssignment.
  • Polls counts on focus + a refresh button.
  • Quick links to /my-audits, /pending-reviews, /work-orders.

Users & Roles & Permissions

UserListComponentfeatures/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.

UserDetailComponentfeatures/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) with validFrom/validUntil time bounds.
  • Direct permissions — grant/deny overrides (GrantPermission / RevokePermission).
  • Effective permissions — read-only flat list pulled from GetUserEffectivePermissions.
  • Sessions — table of active RefreshTokens with revoke action (RevokeUserSession).

RoleListComponentfeatures/roles/role-list/

Route /roles, permission role.read. Lists roles with member count + permission count. Built-in/system flags as tags. Create dialog.

RoleDetailComponentfeatures/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 catalog
  • selectedPerms, initialPerms — current vs loaded selection
  • dirty() — set-comparison computed
  • searchQuery, expandedGroups, filteredGroups()
  • Save calls AssignPermissionsToRole, then snapshots initialPerms so the Save button greys out again.

PermissionsBrowserComponentfeatures/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

  • OrganizationsComponentfeatures/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">.
  • OrganizationTreeComponentfeatures/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

AssetListComponentfeatures/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 prefers enum.assetStatus.{Key} translation and falls back to the localized DB name — this lets admins extend AssetStatus rows 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 by OrganizationChain/LocationChain/ClassificationChain arrays returned by GetAssets (built server-side). The icon hides when the chain is one node — see chainTooltip(chain) helper.
  • Print-label dropdown: single label or A4 sheet via <app-asset-label> / <app-asset-label-sheet>.
  • Edit button shows when asset.update is held; otherwise the row link is "view-only" (eye icon).

AssetDetailComponentfeatures/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 (via GetAssetHistory).
  • 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

TransferListComponentfeatures/transfers/transfer-list/

Route /transfers, asset-transfer.read.

  • Filter by status, requester, date range.
  • Status tag colors keyed off the TransferStatus enum.
  • "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.

TransferDetailComponentfeatures/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 as nvarchar(max) so embedded images are allowed. Read-only after Submit.
  • Lines — table of AssetTransferLines; each shows the asset code + name + per-line ReceivedAt/ReceivedStatus. Adding/removing only works while Status=Draft.
  • Action bar — context-dependent: Submit while Draft, Approve / Reject while Submitted, ReceiveLine / ReceiveAll / Complete while InTransit, Cancel until Approved.
  • Toast on success, problem-details detail on error.
  • Reject opens a dialog asking for RejectionReason (required by validator).

Check-outs

CheckOutsComponentfeatures/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.

AuditPlansComponentfeatures/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.

AuditPlanCreateComponentfeatures/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 ScopeType and the matching fields. Combined mode shows three pickers (Org/Loc/Class) and an IncludeChildren toggle. The "Preview scope" button calls PreviewScope to 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.

AuditPlanDetailComponentfeatures/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.

MyAuditsComponentfeatures/audits/my-audits/

Route /my-audits, audit-assignment.read. Filtered to AssignedUserId == currentUser. Sorted CreatedAt DESC.

AssignmentDetailComponentfeatures/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 Extra tag (no picker).
  • Identification method for extra rows is a static Manual tag.
  • Outcome for expected rows still picks from 4 outcomes (Found/NotFound/LocationMismatch/OrganizationMismatch).

Submit calls SubmitResult with a fresh clientSubmissionId (idempotency).

PendingReviewsComponentfeatures/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.

ResultReviewComponentfeatures/audits/result-review/

Route /audit-results/:id, audit-result.read.

  • Header: result code, submitter, plan, assignment, submission timestamp, review status.
  • Lines table — each AuditResultLine with outcome tag, observed location/org, condition rating, photos (thumbnails via auditPhoto async 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; emits audit.result.approved to submitter.

Maintenance

MaintenancePlansComponentfeatures/maintenance/maintenance-plans/

Route /maintenance-plans, maintenance-plan.read.

  • List with filters: search, code, IsActive, frequency, NextDue range.
  • "Assets" column shows the leaf code + +N for additional assets (multi-asset plans, recent migration).
  • Create + edit dialog with <p-multiSelect> for asset list, frequency picker, custom-days input (when Frequency=CustomDays), next-due picker, default vendor.
  • "Generate work orders" action emits one WO per plan-asset, blocked when NextDueDate > now.

MaintenancePlanDetailComponentfeatures/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.

MaintenanceRequestsComponentfeatures/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.

WorkOrdersComponentfeatures/maintenance/work-orders/

Route /work-orders, work-order.read. List with filters (status, type, priority, assignee, date range). Create-WO dialog. Detail link.

WorkOrderDetailComponentfeatures/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.assign
    • Assigned → InProgress: Startwork-order.update
    • InProgress → Completed: Complete (dialog: actual end, downtime hours, labor/parts cost, currency, resolution notes) — work-order.close
    • Open|Assigned|OnHold → Cancelled: Cancelwork-order.update
  • Costs auto-summed: TotalCost = LaborCost + PartsCost.

Reports

ReportsHubComponentfeatures/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

NotificationsInboxComponentfeatures/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.link is set, router.navigateByUrl(n.link). Cross-shell links are a known sharp edge — see 04 §Cross-shell redirect.

NotificationPreferencesComponentfeatures/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.

ProfileComponentfeatures/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

HierarchyConfigComponentfeatures/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.

NotificationTemplatesComponentfeatures/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 + SubjectSecondary text inputs, BodyPrimary + BodySecondary Quill instances.
  • "Insert merge field" toolbar reads available fields from NotificationEventCatalog.
  • "Preview" button calls PreviewTemplate to render a sample with mocked merge values.
  • Quirk: Quill auto-wraps text in <p> and converts spaces to &nbsp;. The backend NotificationService.Render method normalizes both before substituting merge fields.

AppLanguagesComponentfeatures/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.

TranslationsSettingsComponentfeatures/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.

EmailProviderSettingsComponentfeatures/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

AuditLogComponentfeatures/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.

LoginAuditComponentfeatures/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.

MobileHomeComponentfeatures/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.

MobileAssignmentComponentfeatures/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.
  • Read-only assets table at the bottom (the assignment's expected asset list).

ScanAuditComponentfeatures/mobile/scan-audit/

Embedded by MobileAssignmentComponent. Camera-based scanner.

  • Html5Qrcode from html5-qrcode. startScanning() enumerates cameras via Html5Qrcode.getCameras(), picks the first with a back/rear/environment label or the last device, falls back to facingMode: 'environment'. Specific error handlers for NotAllowedError/NotFoundError/NotReadableError/OverconstrainedError produce useful toasts.
  • Hierarchical location filter — auditor picks a location level (Building → Floor → Room) from a dropdown. The filter options are derived from expectedAssets[].expectedLocationChain so 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 from expected-pending to found (or location-mismatch if not in the active filter) or push a new extra row. Per-outcome audio beep + vibration feedback.
  • Manual entry<p-dialog> to type the printed asset code → LookupAssetByCodeForAuditapplyManualHit.
  • 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 drafteffect(() => ...) watches rows() and selectedByLevel(); debounces 1 s, then PUTs SaveDraft with the JSON payload. applyDraftPayload(payload) rehydrates on load. Mute preference persisted in localStorage["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:#000 on .reader-wrap and #scan-reader-region only — ::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