From 9039aa63d62b346faa1a0865c8b296ab969177eb Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Wed, 18 Mar 2026 17:21:30 -0400 Subject: [PATCH] feat: add benefit history feature with lazy loading and pagination - Implemented `getBenefitsHistory` method in `HomeRepository` to retrieve paginated benefit history. - Enhanced `BenefitsOverviewCubit` to manage loading and displaying benefit history. - Created `BenefitHistoryPage` for full-screen display of benefit history with infinite scroll support. - Added `BenefitHistoryPreview` widget for expandable history preview in benefit cards. - Introduced `BenefitHistoryRow` to display individual history records. - Updated `BenefitsOverviewState` to include history management fields. - Added new entities and use cases for handling benefit history. - Created design system documentation for UI patterns and known gaps. --- .claude/agent-memory/ui-ux-design/MEMORY.md | 7 + .../ui-ux-design/component-patterns.md | 87 +++++++++ .../agent-memory/ui-ux-design/design-gaps.md | 22 +++ .../ui-ux-design/design-system-tokens.md | 102 ++++++++++ .../core/lib/src/routing/staff/navigator.dart | 14 ++ .../lib/src/routing/staff/route_paths.dart | 3 + .../lib/src/l10n/en.i18n.json | 9 +- .../lib/src/l10n/es.i18n.json | 9 +- .../packages/domain/lib/krow_domain.dart | 1 + .../entities/benefits/benefit_history.dart | 100 ++++++++++ .../src/presentation/widgets/shift_card.dart | 1 - .../repositories/home_repository_impl.dart | 20 ++ .../domain/repositories/home_repository.dart | 6 + .../get_benefits_history_usecase.dart | 19 ++ .../benefits_overview_cubit.dart | 107 ++++++++++- .../benefits_overview_state.dart | 57 +++++- .../pages/benefit_history_page.dart | 162 ++++++++++++++++ .../pages/benefits_overview_page.dart | 5 +- .../benefits_overview/benefit_card.dart | 8 +- .../benefit_history_preview.dart | 178 ++++++++++++++++++ .../benefit_history_row.dart | 126 +++++++++++++ .../staff/home/lib/src/staff_home_module.dart | 23 ++- 22 files changed, 1047 insertions(+), 19 deletions(-) create mode 100644 .claude/agent-memory/ui-ux-design/MEMORY.md create mode 100644 .claude/agent-memory/ui-ux-design/component-patterns.md create mode 100644 .claude/agent-memory/ui-ux-design/design-gaps.md create mode 100644 .claude/agent-memory/ui-ux-design/design-system-tokens.md create mode 100644 apps/mobile/packages/domain/lib/src/entities/benefits/benefit_history.dart create mode 100644 apps/mobile/packages/features/staff/home/lib/src/domain/usecases/get_benefits_history_usecase.dart create mode 100644 apps/mobile/packages/features/staff/home/lib/src/presentation/pages/benefit_history_page.dart create mode 100644 apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/benefits_overview/benefit_history_preview.dart create mode 100644 apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/benefits_overview/benefit_history_row.dart diff --git a/.claude/agent-memory/ui-ux-design/MEMORY.md b/.claude/agent-memory/ui-ux-design/MEMORY.md new file mode 100644 index 00000000..38ee8187 --- /dev/null +++ b/.claude/agent-memory/ui-ux-design/MEMORY.md @@ -0,0 +1,7 @@ +# UI/UX Design Agent Memory + +## Index + +- [design-system-tokens.md](design-system-tokens.md) — Verified token values from actual source files +- [component-patterns.md](component-patterns.md) — Established component patterns in KROW staff app +- [design-gaps.md](design-gaps.md) — Known design system gaps and escalation items diff --git a/.claude/agent-memory/ui-ux-design/component-patterns.md b/.claude/agent-memory/ui-ux-design/component-patterns.md new file mode 100644 index 00000000..9ce4d7c2 --- /dev/null +++ b/.claude/agent-memory/ui-ux-design/component-patterns.md @@ -0,0 +1,87 @@ +--- +name: KROW Staff App Component Patterns +description: Established UI patterns, widget conventions, and design decisions confirmed in the KROW staff app codebase +type: project +--- + +## Card Pattern (standard surface) + +Cards use: +- `UiColors.cardViewBackground` (white) background +- `Border.all(color: UiColors.border)` outline +- `BorderRadius.circular(UiConstants.radiusBase)` = 12dp +- `EdgeInsets.all(UiConstants.space4)` = 16dp padding + +Do NOT use `UiColors.bgSecondary` as card background — that is for toggles/headers inside cards. + +## Section Toggle / Expand-Collapse Header + +Used for collapsible sections inside cards: +- Background: `UiColors.bgSecondary` +- Radius: `UiConstants.radiusMd` (6dp) +- Height: minimum 48dp (touch target) +- Label: `UiTypography.titleUppercase3m.textSecondary` for ALL-CAPS labels +- Trailing: `UiIcons.chevronDown` animated 180° via `AnimatedRotation`, 200ms +- Ripple: `InkWell` with `borderRadius: UiConstants.radiusMd` and splash `UiColors.primary.withValues(alpha: 0.06)` + +## Shimmer Loading Pattern + +Use `UiShimmer` wrapper + `UiShimmerLine` / `UiShimmerBox` / `UiShimmerCircle` primitives. +- Base color: `UiColors.muted` +- Highlight: `UiColors.background` +- For list content: 3 shimmer rows by default +- Do NOT use fixed height containers for shimmer — let content flow + +## Status Badge (read-only, non-interactive) + +Custom `Container` with pill shape: +- `borderRadius: UiConstants.radiusFull` +- `padding: EdgeInsets.symmetric(horizontal: space2, vertical: 2)` +- Label style: `UiTypography.footnote2b` +- Do NOT use the interactive `UiChip` widget for read-only display + +Status color mapping: +- ACTIVE: bg=`tagActive`, fg=`textSuccess` +- PENDING: bg=`tagPending`, fg=`textWarning` +- INACTIVE/ENDED: bg=`tagFreeze`, fg=`textSecondary` +- ERROR: bg=`tagError`, fg=`textError` + +## Inline Error Banner (inside card) + +NOT a full-page error — a compact container inside the widget: +- bg: `UiColors.tagError` +- radius: `UiConstants.radiusMd` +- Icon: `UiIcons.error` at `iconMd` (20dp), color: `UiColors.destructive` +- Title: `body2m.textError` +- Retry link: `body3r.primary` with `TextDecoration.underline` + +## Inline Empty State (inside card) + +NOT `UiEmptyState` widget (that is full-page). Use compact inline version: +- `Icon(UiIcons.clock, size: iconXl=32, color: UiColors.iconDisabled)` +- `body2r.textSecondary` label +- `EdgeInsets.symmetric(vertical: space6)` padding + +## AnimatedSize for Expand/Collapse + +```dart +AnimatedSize( + duration: const Duration(milliseconds: 250), + curve: Curves.easeInOut, + child: isExpanded ? content : const SizedBox.shrink(), +) +``` + +## Benefits Feature Structure + +Legacy benefits: `apps/mobile/legacy/legacy-staff-app/lib/features/profile/benefits/` +V2 domain entity: `apps/mobile/packages/domain/lib/src/entities/benefits/benefit.dart` +V2 history entity: needs creation at `packages/domain/lib/src/entities/benefits/benefit_history.dart` + +Benefit history is lazy-loaded per card (not with the initial overview fetch). +History state is cached in BLoC as `Map>>` keyed by benefitId. + +## Screen Page Pattern (overview pages) + +Uses `CustomScrollView` with `SliverList` for header + `SliverPadding` wrapping `SliverList.separated` for content. +Bottom padding on content sliver: `EdgeInsets.fromLTRB(16, 16, 16, 120)` to clear bottom nav bar. diff --git a/.claude/agent-memory/ui-ux-design/design-gaps.md b/.claude/agent-memory/ui-ux-design/design-gaps.md new file mode 100644 index 00000000..ba7f44f5 --- /dev/null +++ b/.claude/agent-memory/ui-ux-design/design-gaps.md @@ -0,0 +1,22 @@ +--- +name: KROW Design System Gaps and Escalations +description: Known missing tokens, open design questions, and items requiring escalation to PM or design system owner +type: project +--- + +## Open Escalations (as of 2026-03-18) + +### 1. No Dark Theme Token Definitions +**Severity:** High +**Detail:** `ui_colors.dart` defines a single light `ColorScheme`. Tag colors (`tagActive`, `tagPending`, `tagFreeze`, `tagError`) have no dark mode equivalents. No dark theme has been configured in `UiTheme`. +**Action:** Escalate to design system owner before any dark mode work. Until resolved, do not attempt dark mode overrides in feature widgets. + +### 2. V2 History API — trackedHours Sign Convention +**Severity:** Medium +**Detail:** `GET /staff/profile/benefits/history` returns `trackedHours` as a positive integer. There is no `transactionType` field to distinguish accruals from deductions (used hours). Design assumes accrual-only for now with `+` prefix in `UiColors.textSuccess`. +**Action:** Escalate to PM/backend. Recommend adding `transactionType: "ACCRUAL" | "USAGE"` or signed integer to distinguish visually. + +### 3. Missing Localization Keys for Benefits History +**Severity:** Low (implementation blocker, not design blocker) +**Detail:** New keys under `benefits.history.*` need to be added to both `en.i18n.json` and `es.i18n.json` in `packages/core_localization/lib/src/l10n/`. Must be coordinated with Mobile Feature Agent who runs `dart run slang`. +**Action:** Hand off key list to Mobile Feature Agent. diff --git a/.claude/agent-memory/ui-ux-design/design-system-tokens.md b/.claude/agent-memory/ui-ux-design/design-system-tokens.md new file mode 100644 index 00000000..49bed2b7 --- /dev/null +++ b/.claude/agent-memory/ui-ux-design/design-system-tokens.md @@ -0,0 +1,102 @@ +--- +name: KROW Design System Token Reference +description: Verified token values from actual source files in apps/mobile/packages/design_system/lib/src/ +type: reference +--- + +## Source Files (verified 2026-03-18) + +- `ui_colors.dart` — all color tokens +- `ui_typography.dart` — all text styles (primary font: Instrument Sans, secondary: Space Grotesk) +- `ui_constants.dart` — spacing, radius, icon sizes +- `ui_icons.dart` — icon aliases over LucideIcons (primary) + FontAwesomeIcons (secondary) + +## Key Color Tokens (hex values confirmed) + +| Token | Hex | Use | +|-------|-----|-----| +| `UiColors.background` | `#FAFBFC` | Page background | +| `UiColors.cardViewBackground` | `#FFFFFF` | Card surface | +| `UiColors.bgSecondary` | `#F1F3F5` | Toggle/section headers | +| `UiColors.bgThird` | `#EDF0F2` | — | +| `UiColors.primary` | `#0A39DF` | Brand blue | +| `UiColors.textPrimary` | `#121826` | Main text | +| `UiColors.textSecondary` | `#6A7382` | Secondary/muted text | +| `UiColors.textInactive` | `#9CA3AF` | Disabled/placeholder | +| `UiColors.textSuccess` | `#0A8159` | Green text (darker than success icon) | +| `UiColors.textError` | `#F04444` | Red text | +| `UiColors.textWarning` | `#D97706` | Amber text | +| `UiColors.success` | `#10B981` | Green brand color | +| `UiColors.destructive` | `#F04444` | Red brand color | +| `UiColors.border` | `#D1D5DB` | Default border | +| `UiColors.separatorSecondary` | `#F1F5F9` | Light dividers | +| `UiColors.tagActive` | `#DCFCE7` | Active status badge bg | +| `UiColors.tagPending` | `#FEF3C7` | Pending badge bg | +| `UiColors.tagError` | `#FEE2E2` | Error banner bg | +| `UiColors.tagFreeze` | `#F3F4F6` | Ended/frozen badge bg | +| `UiColors.tagInProgress` | `#DBEAFE` | In-progress badge bg | +| `UiColors.iconDisabled` | `#D1D5DB` | Disabled icon color | +| `UiColors.muted` | `#F1F3F5` | Shimmer base color | + +## Key Spacing Constants + +| Token | Value | +|-------|-------| +| `space1` | 4dp | +| `space2` | 8dp | +| `space3` | 12dp | +| `space4` | 16dp | +| `space5` | 20dp | +| `space6` | 24dp | +| `space8` | 32dp | +| `space10` | 40dp | +| `space12` | 48dp | + +## Key Radius Constants + +| Token | Value | +|-------|-------| +| `radiusSm` | 4dp | +| `radiusMd` (radiusMdValue) | 6dp | +| `radiusBase` | 12dp | +| `radiusLg` | 12dp (BorderRadius.circular(12)) | +| `radiusXl` | 16dp | +| `radius2xl` | 24dp | +| `radiusFull` | 999dp | + +NOTE: `radiusBase` is a `double` (12.0), `radiusLg` is a `BorderRadius`. Use `BorderRadius.circular(UiConstants.radiusBase)` when a double is needed. + +## Icon Sizes + +| Token | Value | +|-------|-------| +| `iconXs` | 12dp | +| `iconSm` | 16dp | +| `iconMd` | 20dp | +| `iconLg` | 24dp | +| `iconXl` | 32dp | + +## Key Typography Styles (Instrument Sans) + +| Token | Size | Weight | Notes | +|-------|------|--------|-------| +| `display1b` | 26px | 600 | letterSpacing: -1 | +| `title1b` | 18px | 600 | height: 1.5 | +| `title1m` | 18px | 500 | height: 1.5 | +| `title2b` | 16px | 600 | height: 1.1 | +| `body1m` | 16px | 600 | letterSpacing: -0.025 | +| `body1r` | 16px | 400 | letterSpacing: -0.05 | +| `body2b` | 14px | 700 | height: 1.5 | +| `body2m` | 14px | 500 | height: 1.5 | +| `body2r` | 14px | 400 | letterSpacing: 0.1 | +| `body3r` | 12px | 400 | height: 1.5 | +| `body3m` | 12px | 500 | letterSpacing: -0.1 | +| `footnote1r` | 12px | 400 | letterSpacing: 0.05 | +| `footnote1m` | 12px | 500 | — | +| `footnote2b` | 10px | 700 | — | +| `footnote2r` | 10px | 400 | — | +| `titleUppercase3m` | 12px | 500 | letterSpacing: 0.7 — use for ALL-CAPS section labels | + +## Typography Color Extension + +`UiTypography` styles have a `.textSecondary`, `.textSuccess`, `.textError`, `.textWarning`, `.primary`, `.white` extension defined in `TypographyColors`. Use these instead of `.copyWith(color: ...)` where possible for brevity. diff --git a/apps/mobile/packages/core/lib/src/routing/staff/navigator.dart b/apps/mobile/packages/core/lib/src/routing/staff/navigator.dart index 686ea53c..9a536a65 100644 --- a/apps/mobile/packages/core/lib/src/routing/staff/navigator.dart +++ b/apps/mobile/packages/core/lib/src/routing/staff/navigator.dart @@ -60,6 +60,20 @@ extension StaffNavigator on IModularNavigator { safePush(StaffPaths.benefits); } + /// Navigates to the full history page for a specific benefit. + void toBenefitHistory({ + required String benefitId, + required String benefitTitle, + }) { + safePush( + StaffPaths.benefitHistory, + arguments: { + 'benefitId': benefitId, + 'benefitTitle': benefitTitle, + }, + ); + } + void toStaffMain() { safePushNamedAndRemoveUntil('${StaffPaths.main}/home/', (_) => false); } diff --git a/apps/mobile/packages/core/lib/src/routing/staff/route_paths.dart b/apps/mobile/packages/core/lib/src/routing/staff/route_paths.dart index 42b159d3..c3ebff23 100644 --- a/apps/mobile/packages/core/lib/src/routing/staff/route_paths.dart +++ b/apps/mobile/packages/core/lib/src/routing/staff/route_paths.dart @@ -75,6 +75,9 @@ class StaffPaths { /// Benefits overview page. static const String benefits = '/worker-main/home/benefits'; + /// Benefit history page for a specific benefit. + static const String benefitHistory = '/worker-main/home/benefits/history'; + /// Shifts tab - view and manage shifts. /// /// Browse available shifts, accepted shifts, and shift history. diff --git a/apps/mobile/packages/core_localization/lib/src/l10n/en.i18n.json b/apps/mobile/packages/core_localization/lib/src/l10n/en.i18n.json index 5645f0c3..b4525aca 100644 --- a/apps/mobile/packages/core_localization/lib/src/l10n/en.i18n.json +++ b/apps/mobile/packages/core_localization/lib/src/l10n/en.i18n.json @@ -672,7 +672,14 @@ "status": { "pending": "Pending", "submitted": "Submitted" - } + }, + "history_header": "HISTORY", + "no_history": "No history yet", + "show_all": "Show all", + "hours_accrued": "+${hours}h accrued", + "hours_used": "-${hours}h used", + "history_page_title": "$benefit History", + "loading_more": "Loading..." } }, "auto_match": { diff --git a/apps/mobile/packages/core_localization/lib/src/l10n/es.i18n.json b/apps/mobile/packages/core_localization/lib/src/l10n/es.i18n.json index 389f4e87..027aa800 100644 --- a/apps/mobile/packages/core_localization/lib/src/l10n/es.i18n.json +++ b/apps/mobile/packages/core_localization/lib/src/l10n/es.i18n.json @@ -667,7 +667,14 @@ "status": { "pending": "Pendiente", "submitted": "Enviado" - } + }, + "history_header": "HISTORIAL", + "no_history": "Sin historial aún", + "show_all": "Ver todo", + "hours_accrued": "+${hours}h acumuladas", + "hours_used": "-${hours}h utilizadas", + "history_page_title": "Historial de $benefit", + "loading_more": "Cargando..." } }, "auto_match": { diff --git a/apps/mobile/packages/domain/lib/krow_domain.dart b/apps/mobile/packages/domain/lib/krow_domain.dart index 37569eec..c0122529 100644 --- a/apps/mobile/packages/domain/lib/krow_domain.dart +++ b/apps/mobile/packages/domain/lib/krow_domain.dart @@ -72,6 +72,7 @@ export 'src/entities/orders/recent_order.dart'; // Financial & Payroll export 'src/entities/benefits/benefit.dart'; +export 'src/entities/benefits/benefit_history.dart'; export 'src/entities/financial/invoice.dart'; export 'src/entities/financial/billing_account.dart'; export 'src/entities/financial/current_bill.dart'; diff --git a/apps/mobile/packages/domain/lib/src/entities/benefits/benefit_history.dart b/apps/mobile/packages/domain/lib/src/entities/benefits/benefit_history.dart new file mode 100644 index 00000000..f9933a37 --- /dev/null +++ b/apps/mobile/packages/domain/lib/src/entities/benefits/benefit_history.dart @@ -0,0 +1,100 @@ +import 'package:equatable/equatable.dart'; + +import 'package:krow_domain/src/entities/enums/benefit_status.dart'; + +/// A historical record of a staff benefit accrual period. +/// +/// Returned by `GET /staff/profile/benefits/history`. +class BenefitHistory extends Equatable { + /// Creates a [BenefitHistory] instance. + const BenefitHistory({ + required this.historyId, + required this.benefitId, + required this.benefitType, + required this.title, + required this.status, + required this.effectiveAt, + required this.trackedHours, + required this.targetHours, + this.endedAt, + this.notes, + }); + + /// Deserialises a [BenefitHistory] from a V2 API JSON map. + factory BenefitHistory.fromJson(Map json) { + return BenefitHistory( + historyId: json['historyId'] as String, + benefitId: json['benefitId'] as String, + benefitType: json['benefitType'] as String, + title: json['title'] as String, + status: BenefitStatus.fromJson(json['status'] as String?), + effectiveAt: DateTime.parse(json['effectiveAt'] as String), + endedAt: json['endedAt'] != null + ? DateTime.parse(json['endedAt'] as String) + : null, + trackedHours: (json['trackedHours'] as num).toInt(), + targetHours: (json['targetHours'] as num).toInt(), + notes: json['notes'] as String?, + ); + } + + /// Unique identifier for this history record. + final String historyId; + + /// The benefit this record belongs to. + final String benefitId; + + /// Type code (e.g. SICK_LEAVE, VACATION). + final String benefitType; + + /// Human-readable title. + final String title; + + /// Status of the benefit during this period. + final BenefitStatus status; + + /// When this benefit period became effective. + final DateTime effectiveAt; + + /// When this benefit period ended, or `null` if still active. + final DateTime? endedAt; + + /// Hours tracked during this period. + final int trackedHours; + + /// Target hours for this period. + final int targetHours; + + /// Optional notes about the accrual. + final String? notes; + + /// Serialises this [BenefitHistory] to a JSON map. + Map toJson() { + return { + 'historyId': historyId, + 'benefitId': benefitId, + 'benefitType': benefitType, + 'title': title, + 'status': status.toJson(), + 'effectiveAt': effectiveAt.toIso8601String(), + 'endedAt': endedAt?.toIso8601String(), + 'trackedHours': trackedHours, + 'targetHours': targetHours, + 'notes': notes, + }; + } + + @override + List get props => [ + historyId, + benefitId, + benefitType, + title, + status, + effectiveAt, + endedAt, + trackedHours, + targetHours, + notes, + ]; +} diff --git a/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/widgets/shift_card.dart b/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/widgets/shift_card.dart index 5681604c..fc3aa683 100644 --- a/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/widgets/shift_card.dart +++ b/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/widgets/shift_card.dart @@ -80,7 +80,6 @@ class _ShiftDetails extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(shift.title, style: UiTypography.body2b), - // TODO: Ask BE to add clientName to the listTodayShifts response. // Currently showing locationName as subtitle fallback. Text( shift.locationName ?? '', diff --git a/apps/mobile/packages/features/staff/home/lib/src/data/repositories/home_repository_impl.dart b/apps/mobile/packages/features/staff/home/lib/src/data/repositories/home_repository_impl.dart index bc69b23c..1356f2de 100644 --- a/apps/mobile/packages/features/staff/home/lib/src/data/repositories/home_repository_impl.dart +++ b/apps/mobile/packages/features/staff/home/lib/src/data/repositories/home_repository_impl.dart @@ -30,4 +30,24 @@ class HomeRepositoryImpl implements HomeRepository { final ProfileCompletion completion = ProfileCompletion.fromJson(data); return completion.completed; } + + @override + Future> getBenefitsHistory({ + int limit = 20, + int offset = 0, + }) async { + final ApiResponse response = await _apiService.get( + StaffEndpoints.benefitsHistory, + params: { + 'limit': limit, + 'offset': offset, + }, + ); + final List items = + response.data['items'] as List? ?? []; + return items + .map((dynamic json) => + BenefitHistory.fromJson(json as Map)) + .toList(); + } } diff --git a/apps/mobile/packages/features/staff/home/lib/src/domain/repositories/home_repository.dart b/apps/mobile/packages/features/staff/home/lib/src/domain/repositories/home_repository.dart index 91144b86..c4f6005b 100644 --- a/apps/mobile/packages/features/staff/home/lib/src/domain/repositories/home_repository.dart +++ b/apps/mobile/packages/features/staff/home/lib/src/domain/repositories/home_repository.dart @@ -12,4 +12,10 @@ abstract class HomeRepository { /// Retrieves whether the staff member's profile is complete. Future getProfileCompletion(); + + /// Retrieves paginated benefit history for the staff member. + Future> getBenefitsHistory({ + int limit = 20, + int offset = 0, + }); } diff --git a/apps/mobile/packages/features/staff/home/lib/src/domain/usecases/get_benefits_history_usecase.dart b/apps/mobile/packages/features/staff/home/lib/src/domain/usecases/get_benefits_history_usecase.dart new file mode 100644 index 00000000..654d63cc --- /dev/null +++ b/apps/mobile/packages/features/staff/home/lib/src/domain/usecases/get_benefits_history_usecase.dart @@ -0,0 +1,19 @@ +import 'package:krow_domain/krow_domain.dart'; +import 'package:staff_home/src/domain/repositories/home_repository.dart'; + +/// Use case for fetching paginated benefit history for a staff member. +/// +/// Delegates to [HomeRepository.getBenefitsHistory] and returns +/// a list of [BenefitHistory] records. +class GetBenefitsHistoryUseCase { + /// Creates a [GetBenefitsHistoryUseCase]. + GetBenefitsHistoryUseCase(this._repository); + + /// The repository used for data access. + final HomeRepository _repository; + + /// Executes the use case to fetch benefit history. + Future> call({int limit = 20, int offset = 0}) { + return _repository.getBenefitsHistory(limit: limit, offset: offset); + } +} diff --git a/apps/mobile/packages/features/staff/home/lib/src/presentation/blocs/benefits_overview/benefits_overview_cubit.dart b/apps/mobile/packages/features/staff/home/lib/src/presentation/blocs/benefits_overview/benefits_overview_cubit.dart index e53c19a1..7f11195d 100644 --- a/apps/mobile/packages/features/staff/home/lib/src/presentation/blocs/benefits_overview/benefits_overview_cubit.dart +++ b/apps/mobile/packages/features/staff/home/lib/src/presentation/blocs/benefits_overview/benefits_overview_cubit.dart @@ -3,22 +3,29 @@ import 'package:equatable/equatable.dart'; import 'package:krow_core/core.dart'; import 'package:krow_domain/krow_domain.dart'; import 'package:staff_home/src/domain/repositories/home_repository.dart'; +import 'package:staff_home/src/domain/usecases/get_benefits_history_usecase.dart'; part 'benefits_overview_state.dart'; /// Cubit managing the benefits overview page state. /// -/// Fetches the dashboard and extracts benefits for the detail page. +/// Fetches the dashboard benefits and lazily loads per-benefit history. class BenefitsOverviewCubit extends Cubit with BlocErrorHandler { /// Creates a [BenefitsOverviewCubit]. - BenefitsOverviewCubit({required HomeRepository repository}) - : _repository = repository, + BenefitsOverviewCubit({ + required HomeRepository repository, + required GetBenefitsHistoryUseCase getBenefitsHistory, + }) : _repository = repository, + _getBenefitsHistory = getBenefitsHistory, super(const BenefitsOverviewState.initial()); - /// The repository used for data access. + /// The repository used for dashboard data access. final HomeRepository _repository; + /// Use case for fetching benefit history. + final GetBenefitsHistoryUseCase _getBenefitsHistory; + /// Loads benefits from the dashboard endpoint. Future loadBenefits() async { if (isClosed) return; @@ -44,4 +51,96 @@ class BenefitsOverviewCubit extends Cubit }, ); } + + /// Loads benefit history for a specific benefit (lazy, on first expand). + /// + /// Skips if already loading or already loaded for the given [benefitId]. + Future loadBenefitHistory(String benefitId) async { + if (isClosed) return; + if (state.loadingHistoryIds.contains(benefitId)) return; + if (state.loadedHistoryIds.contains(benefitId)) return; + + emit(state.copyWith( + loadingHistoryIds: {...state.loadingHistoryIds, benefitId}, + )); + + await handleError( + emit: emit, + action: () async { + final List history = + await _getBenefitsHistory(limit: 20, offset: 0); + if (isClosed) return; + final List filtered = history + .where((BenefitHistory h) => h.benefitId == benefitId) + .toList(); + emit(state.copyWith( + historyByBenefitId: >{ + ...state.historyByBenefitId, + benefitId: filtered, + }, + loadingHistoryIds: {...state.loadingHistoryIds} + ..remove(benefitId), + loadedHistoryIds: {...state.loadedHistoryIds, benefitId}, + hasMoreHistory: { + ...state.hasMoreHistory, + benefitId: history.length >= 20, + }, + )); + }, + onError: (String errorKey) { + if (isClosed) return state; + return state.copyWith( + loadingHistoryIds: {...state.loadingHistoryIds} + ..remove(benefitId), + ); + }, + ); + } + + /// Loads more history for infinite scroll on the full history page. + /// + /// Appends results to existing history for the given [benefitId]. + Future loadMoreBenefitHistory(String benefitId) async { + if (isClosed) return; + if (state.loadingHistoryIds.contains(benefitId)) return; + if (!(state.hasMoreHistory[benefitId] ?? true)) return; + + final List existing = + state.historyByBenefitId[benefitId] ?? []; + + emit(state.copyWith( + loadingHistoryIds: {...state.loadingHistoryIds, benefitId}, + )); + + await handleError( + emit: emit, + action: () async { + final List history = + await _getBenefitsHistory(limit: 20, offset: existing.length); + if (isClosed) return; + final List filtered = history + .where((BenefitHistory h) => h.benefitId == benefitId) + .toList(); + emit(state.copyWith( + historyByBenefitId: >{ + ...state.historyByBenefitId, + benefitId: [...existing, ...filtered], + }, + loadingHistoryIds: {...state.loadingHistoryIds} + ..remove(benefitId), + hasMoreHistory: { + ...state.hasMoreHistory, + benefitId: history.length >= 20, + }, + )); + }, + onError: (String errorKey) { + if (isClosed) return state; + return state.copyWith( + loadingHistoryIds: {...state.loadingHistoryIds} + ..remove(benefitId), + ); + }, + ); + } } diff --git a/apps/mobile/packages/features/staff/home/lib/src/presentation/blocs/benefits_overview/benefits_overview_state.dart b/apps/mobile/packages/features/staff/home/lib/src/presentation/blocs/benefits_overview/benefits_overview_state.dart index 768a2146..a5525a69 100644 --- a/apps/mobile/packages/features/staff/home/lib/src/presentation/blocs/benefits_overview/benefits_overview_state.dart +++ b/apps/mobile/packages/features/staff/home/lib/src/presentation/blocs/benefits_overview/benefits_overview_state.dart @@ -1,33 +1,78 @@ part of 'benefits_overview_cubit.dart'; +/// Status of the benefits overview data fetch. enum BenefitsOverviewStatus { initial, loading, loaded, error } +/// State for [BenefitsOverviewCubit]. +/// +/// Holds both the top-level benefits list and per-benefit history data +/// used by [BenefitHistoryPreview] and [BenefitHistoryPage]. class BenefitsOverviewState extends Equatable { - final BenefitsOverviewStatus status; - final List benefits; - final String? errorMessage; - + /// Creates a [BenefitsOverviewState]. const BenefitsOverviewState({ required this.status, - this.benefits = const [], + this.benefits = const [], this.errorMessage, + this.historyByBenefitId = const >{}, + this.loadingHistoryIds = const {}, + this.loadedHistoryIds = const {}, + this.hasMoreHistory = const {}, }); + /// Initial state with no data. const BenefitsOverviewState.initial() : this(status: BenefitsOverviewStatus.initial); + /// Current status of the top-level benefits fetch. + final BenefitsOverviewStatus status; + + /// The list of staff benefits. + final List benefits; + + /// Error message when [status] is [BenefitsOverviewStatus.error]. + final String? errorMessage; + + /// Cached history records keyed by benefit ID. + final Map> historyByBenefitId; + + /// Benefit IDs currently loading history. + final Set loadingHistoryIds; + + /// Benefit IDs whose history has been loaded at least once. + final Set loadedHistoryIds; + + /// Whether more pages of history are available per benefit. + final Map hasMoreHistory; + + /// Creates a copy with the given fields replaced. BenefitsOverviewState copyWith({ BenefitsOverviewStatus? status, List? benefits, String? errorMessage, + Map>? historyByBenefitId, + Set? loadingHistoryIds, + Set? loadedHistoryIds, + Map? hasMoreHistory, }) { return BenefitsOverviewState( status: status ?? this.status, benefits: benefits ?? this.benefits, errorMessage: errorMessage ?? this.errorMessage, + historyByBenefitId: historyByBenefitId ?? this.historyByBenefitId, + loadingHistoryIds: loadingHistoryIds ?? this.loadingHistoryIds, + loadedHistoryIds: loadedHistoryIds ?? this.loadedHistoryIds, + hasMoreHistory: hasMoreHistory ?? this.hasMoreHistory, ); } @override - List get props => [status, benefits, errorMessage]; + List get props => [ + status, + benefits, + errorMessage, + historyByBenefitId, + loadingHistoryIds, + loadedHistoryIds, + hasMoreHistory, + ]; } diff --git a/apps/mobile/packages/features/staff/home/lib/src/presentation/pages/benefit_history_page.dart b/apps/mobile/packages/features/staff/home/lib/src/presentation/pages/benefit_history_page.dart new file mode 100644 index 00000000..de166a31 --- /dev/null +++ b/apps/mobile/packages/features/staff/home/lib/src/presentation/pages/benefit_history_page.dart @@ -0,0 +1,162 @@ +import 'package:core_localization/core_localization.dart'; +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_modular/flutter_modular.dart'; +import 'package:krow_domain/krow_domain.dart'; +import 'package:staff_home/src/presentation/blocs/benefits_overview/benefits_overview_cubit.dart'; +import 'package:staff_home/src/presentation/widgets/benefits_overview/benefit_history_row.dart'; + +/// Full-screen page displaying paginated benefit history. +/// +/// Supports infinite scroll via [ScrollController] and +/// [BenefitsOverviewCubit.loadMoreBenefitHistory]. +class BenefitHistoryPage extends StatefulWidget { + /// Creates a [BenefitHistoryPage]. + const BenefitHistoryPage({ + required this.benefitId, + required this.benefitTitle, + super.key, + }); + + /// The ID of the benefit whose history to display. + final String benefitId; + + /// The human-readable benefit title shown in the app bar. + final String benefitTitle; + + @override + State createState() => _BenefitHistoryPageState(); +} + +class _BenefitHistoryPageState extends State { + /// Scroll controller for infinite scroll detection. + final ScrollController _scrollController = ScrollController(); + + @override + void initState() { + super.initState(); + _scrollController.addListener(_onScroll); + + final BenefitsOverviewCubit cubit = + Modular.get(); + if (!cubit.state.loadedHistoryIds.contains(widget.benefitId)) { + cubit.loadBenefitHistory(widget.benefitId); + } + } + + @override + void dispose() { + _scrollController + ..removeListener(_onScroll) + ..dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final dynamic i18n = t.staff.home.benefits.overview; + final String pageTitle = + i18n.history_page_title(benefit: widget.benefitTitle) as String; + + return Scaffold( + appBar: UiAppBar( + title: pageTitle, + showBackButton: true, + ), + body: BlocProvider.value( + value: Modular.get(), + child: BlocBuilder( + buildWhen: (BenefitsOverviewState previous, + BenefitsOverviewState current) => + previous.historyByBenefitId[widget.benefitId] != + current.historyByBenefitId[widget.benefitId] || + previous.loadingHistoryIds != current.loadingHistoryIds || + previous.loadedHistoryIds != current.loadedHistoryIds, + builder: (BuildContext context, BenefitsOverviewState state) { + final bool isLoading = + state.loadingHistoryIds.contains(widget.benefitId); + final bool isLoaded = + state.loadedHistoryIds.contains(widget.benefitId); + final List history = + state.historyByBenefitId[widget.benefitId] ?? []; + final bool hasMore = state.hasMoreHistory[widget.benefitId] ?? true; + + // Initial loading state + if (isLoading && !isLoaded) { + return _buildLoadingSkeleton(); + } + + // Empty state + if (isLoaded && history.isEmpty) { + return UiEmptyState( + icon: UiIcons.clock, + title: i18n.no_history as String, + description: '', + ); + } + + // Loaded list with infinite scroll + return ListView.builder( + controller: _scrollController, + padding: const EdgeInsets.symmetric( + horizontal: UiConstants.space4, + vertical: UiConstants.space4, + ), + itemCount: history.length + (hasMore ? 1 : 0), + itemBuilder: (BuildContext context, int index) { + if (index >= history.length) { + // Bottom loading indicator + return isLoading + ? const Padding( + padding: EdgeInsets.all(UiConstants.space4), + child: Center(child: CircularProgressIndicator()), + ) + : const SizedBox.shrink(); + } + return BenefitHistoryRow(history: history[index]); + }, + ); + }, + ), + ), + ); + } + + /// Triggers loading more history when scrolled near the bottom. + void _onScroll() { + if (!_scrollController.hasClients) return; + final double maxScroll = _scrollController.position.maxScrollExtent; + final double currentScroll = _scrollController.offset; + if (maxScroll - currentScroll <= 200) { + final BenefitsOverviewCubit cubit = + ReadContext(context).read(); + cubit.loadMoreBenefitHistory(widget.benefitId); + } + } + + /// Builds a shimmer skeleton for the initial loading state. + Widget _buildLoadingSkeleton() { + return UiShimmer( + child: Padding( + padding: const EdgeInsets.all(UiConstants.space4), + child: Column( + children: [ + for (int i = 0; i < 8; i++) + Padding( + padding: + const EdgeInsets.symmetric(vertical: UiConstants.space2), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + UiShimmerLine(width: 100, height: 14), + UiShimmerLine(width: 80, height: 14), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/home/lib/src/presentation/pages/benefits_overview_page.dart b/apps/mobile/packages/features/staff/home/lib/src/presentation/pages/benefits_overview_page.dart index fad93b89..56015812 100644 --- a/apps/mobile/packages/features/staff/home/lib/src/presentation/pages/benefits_overview_page.dart +++ b/apps/mobile/packages/features/staff/home/lib/src/presentation/pages/benefits_overview_page.dart @@ -19,9 +19,8 @@ class BenefitsOverviewPage extends StatelessWidget { subtitle: t.staff.home.benefits.overview.subtitle, showBackButton: true, ), - body: BlocProvider( - create: (context) => - Modular.get()..loadBenefits(), + body: BlocProvider.value( + value: Modular.get()..loadBenefits(), child: BlocBuilder( builder: (context, state) { if (state.status == BenefitsOverviewStatus.loading || diff --git a/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/benefits_overview/benefit_card.dart b/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/benefits_overview/benefit_card.dart index 330bd8ee..24b1c3fe 100644 --- a/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/benefits_overview/benefit_card.dart +++ b/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/benefits_overview/benefit_card.dart @@ -2,8 +2,9 @@ import 'package:design_system/design_system.dart'; import 'package:flutter/material.dart'; import 'package:krow_domain/krow_domain.dart'; import 'package:staff_home/src/presentation/widgets/benefits_overview/benefit_card_header.dart'; +import 'package:staff_home/src/presentation/widgets/benefits_overview/benefit_history_preview.dart'; -/// Card widget displaying detailed benefit information. +/// Card widget displaying detailed benefit information with history preview. class BenefitCard extends StatelessWidget { /// Creates a [BenefitCard]. const BenefitCard({required this.benefit, super.key}); @@ -24,6 +25,11 @@ class BenefitCard extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ BenefitCardHeader(benefit: benefit), + const SizedBox(height: UiConstants.space4), + BenefitHistoryPreview( + benefitId: benefit.benefitId, + benefitTitle: benefit.title, + ), ], ), ); diff --git a/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/benefits_overview/benefit_history_preview.dart b/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/benefits_overview/benefit_history_preview.dart new file mode 100644 index 00000000..00392ed0 --- /dev/null +++ b/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/benefits_overview/benefit_history_preview.dart @@ -0,0 +1,178 @@ +import 'package:core_localization/core_localization.dart'; +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_modular/flutter_modular.dart'; +import 'package:krow_core/core.dart'; +import 'package:krow_domain/krow_domain.dart'; +import 'package:staff_home/src/presentation/blocs/benefits_overview/benefits_overview_cubit.dart'; +import 'package:staff_home/src/presentation/widgets/benefits_overview/benefit_history_row.dart'; + +/// Expandable preview section showing recent benefit history on a card. +/// +/// Collapses by default. On first expand, triggers a lazy load of history +/// for the given [benefitId] via [BenefitsOverviewCubit.loadBenefitHistory]. +/// Shows the first 5 records and a "Show all" button when more exist. +class BenefitHistoryPreview extends StatefulWidget { + /// Creates a [BenefitHistoryPreview]. + const BenefitHistoryPreview({ + required this.benefitId, + required this.benefitTitle, + super.key, + }); + + /// The ID of the benefit whose history to display. + final String benefitId; + + /// The human-readable benefit title, passed to the full history page. + final String benefitTitle; + + @override + State createState() => _BenefitHistoryPreviewState(); +} + +class _BenefitHistoryPreviewState extends State { + bool _isExpanded = false; + + @override + Widget build(BuildContext context) { + final dynamic i18n = t.staff.home.benefits.overview; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Divider(height: 1, color: UiColors.border), + InkWell( + onTap: _toggleExpanded, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: UiConstants.space4), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + i18n.history_header as String, + style: UiTypography.footnote2b.textSecondary, + ), + Icon( + _isExpanded ? UiIcons.chevronUp : UiIcons.chevronDown, + size: UiConstants.iconSm, + color: UiColors.iconSecondary, + ), + ], + ), + ), + ), + AnimatedSize( + duration: const Duration(milliseconds: 200), + curve: Curves.easeInOut, + alignment: Alignment.topCenter, + child: _isExpanded ? _buildContent(i18n) : const SizedBox.shrink(), + ), + ], + ); + } + + /// Toggles expansion and triggers history load on first expand. + void _toggleExpanded() { + setState(() { + _isExpanded = !_isExpanded; + }); + if (_isExpanded) { + final BenefitsOverviewCubit cubit = + ReadContext(context).read(); + cubit.loadBenefitHistory(widget.benefitId); + } + } + + /// Builds the expanded content section. + Widget _buildContent(dynamic i18n) { + return BlocBuilder( + buildWhen: (BenefitsOverviewState previous, + BenefitsOverviewState current) => + previous.historyByBenefitId[widget.benefitId] != + current.historyByBenefitId[widget.benefitId] || + previous.loadingHistoryIds != current.loadingHistoryIds || + previous.loadedHistoryIds != current.loadedHistoryIds, + builder: (BuildContext context, BenefitsOverviewState state) { + final bool isLoading = + state.loadingHistoryIds.contains(widget.benefitId); + final bool isLoaded = + state.loadedHistoryIds.contains(widget.benefitId); + final List history = + state.historyByBenefitId[widget.benefitId] ?? []; + + if (isLoading && !isLoaded) { + return _buildShimmer(); + } + + if (isLoaded && history.isEmpty) { + return Padding( + padding: const EdgeInsets.only(bottom: UiConstants.space3), + child: Text( + i18n.no_history as String, + style: UiTypography.body3r.textSecondary, + ), + ); + } + + final int previewCount = history.length > 5 ? 5 : history.length; + final bool showAll = history.length > 5; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + for (int i = 0; i < previewCount; i++) + BenefitHistoryRow(history: history[i]), + if (!showAll) + Align( + alignment: Alignment.centerRight, + child: TextButton( + onPressed: _navigateToFullHistory, + child: Text( + i18n.show_all as String, + style: UiTypography.footnote1m.copyWith( + color: UiColors.primary, + ), + ), + ), + ), + const SizedBox(height: UiConstants.space1), + ], + ); + }, + ); + } + + /// Builds shimmer placeholder rows while loading. + Widget _buildShimmer() { + return UiShimmer( + child: Padding( + padding: const EdgeInsets.only(bottom: UiConstants.space3), + child: Column( + children: [ + for (int i = 0; i < 3; i++) + Padding( + padding: + const EdgeInsets.symmetric(vertical: UiConstants.space2), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + UiShimmerLine(width: 100, height: 12), + UiShimmerLine(width: 60, height: 12), + ], + ), + ), + ], + ), + ), + ); + } + + /// Navigates to the full benefit history page. + void _navigateToFullHistory() { + Modular.to.toBenefitHistory( + benefitId: widget.benefitId, + benefitTitle: widget.benefitTitle, + ); + } +} diff --git a/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/benefits_overview/benefit_history_row.dart b/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/benefits_overview/benefit_history_row.dart new file mode 100644 index 00000000..6bf9601f --- /dev/null +++ b/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/benefits_overview/benefit_history_row.dart @@ -0,0 +1,126 @@ +import 'package:core_localization/core_localization.dart'; +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:krow_domain/krow_domain.dart'; + +/// A single row displaying one [BenefitHistory] record. +/// +/// Shows the effective date, optional notes, accrued hours badge, and a +/// status chip. Used in both [BenefitHistoryPreview] and [BenefitHistoryPage]. +class BenefitHistoryRow extends StatelessWidget { + /// Creates a [BenefitHistoryRow]. + const BenefitHistoryRow({required this.history, super.key}); + + /// The history record to display. + final BenefitHistory history; + + @override + Widget build(BuildContext context) { + final dynamic i18n = t.staff.home.benefits.overview; + + return Padding( + padding: const EdgeInsets.symmetric(vertical: UiConstants.space2), + child: Row( + children: [ + // Date column + Text( + DateFormat('d MMM, yyyy').format(history.effectiveAt), + style: UiTypography.footnote1r.textSecondary, + ), + const SizedBox(width: UiConstants.space3), + // Notes (takes remaining space) + Expanded( + child: history.notes != null && history.notes!.isNotEmpty + ? Text( + history.notes!, + style: UiTypography.body3r.textSecondary, + overflow: TextOverflow.ellipsis, + maxLines: 1, + ) + : const SizedBox.shrink(), + ), + const SizedBox(width: UiConstants.space2), + // Hours badge + _buildHoursBadge(i18n), + const SizedBox(width: UiConstants.space2), + // Status chip + _buildStatusChip(i18n), + ], + ), + ); + } + + /// Builds the hours badge showing tracked hours. + Widget _buildHoursBadge(dynamic i18n) { + final String label = '+${history.trackedHours}h'; + return Text( + label, + style: UiTypography.footnote2b.copyWith(color: UiColors.textSuccess), + ); + } + + /// Builds a chip indicating the benefit history status. + Widget _buildStatusChip(dynamic i18n) { + final _StatusStyle statusStyle = _resolveStatusStyle(history.status); + return Container( + padding: const EdgeInsets.symmetric( + horizontal: UiConstants.space2, + vertical: UiConstants.space1, + ), + decoration: BoxDecoration( + color: statusStyle.backgroundColor, + borderRadius: UiConstants.radiusFull, + border: Border.all(color: statusStyle.borderColor, width: 0.5), + ), + child: Text( + statusStyle.label, + style: UiTypography.footnote2m.copyWith(color: statusStyle.textColor), + ), + ); + } + + /// Maps a [BenefitStatus] to display style values. + _StatusStyle _resolveStatusStyle(BenefitStatus status) { + final dynamic i18n = t.staff.home.benefits.overview.status; + switch (status) { + case BenefitStatus.active: + return _StatusStyle( + label: i18n.submitted, + backgroundColor: UiColors.tagSuccess, + textColor: UiColors.textSuccess, + borderColor: UiColors.tagSuccess, + ); + case BenefitStatus.pending: + return _StatusStyle( + label: i18n.pending, + backgroundColor: UiColors.tagPending, + textColor: UiColors.mutedForeground, + borderColor: UiColors.border, + ); + case BenefitStatus.inactive: + case BenefitStatus.unknown: + return _StatusStyle( + label: i18n.pending, + backgroundColor: UiColors.muted, + textColor: UiColors.mutedForeground, + borderColor: UiColors.border, + ); + } + } +} + +/// Internal value type for status chip styling. +class _StatusStyle { + const _StatusStyle({ + required this.label, + required this.backgroundColor, + required this.textColor, + required this.borderColor, + }); + + final String label; + final Color backgroundColor; + final Color textColor; + final Color borderColor; +} diff --git a/apps/mobile/packages/features/staff/home/lib/src/staff_home_module.dart b/apps/mobile/packages/features/staff/home/lib/src/staff_home_module.dart index bcb4af20..57410288 100644 --- a/apps/mobile/packages/features/staff/home/lib/src/staff_home_module.dart +++ b/apps/mobile/packages/features/staff/home/lib/src/staff_home_module.dart @@ -4,9 +4,11 @@ import 'package:krow_core/core.dart'; import 'package:krow_domain/krow_domain.dart'; import 'package:staff_home/src/data/repositories/home_repository_impl.dart'; import 'package:staff_home/src/domain/repositories/home_repository.dart'; +import 'package:staff_home/src/domain/usecases/get_benefits_history_usecase.dart'; import 'package:staff_home/src/domain/usecases/get_home_shifts.dart'; import 'package:staff_home/src/presentation/blocs/benefits_overview/benefits_overview_cubit.dart'; import 'package:staff_home/src/presentation/blocs/home/home_cubit.dart'; +import 'package:staff_home/src/presentation/pages/benefit_history_page.dart'; import 'package:staff_home/src/presentation/pages/benefits_overview_page.dart'; import 'package:staff_home/src/presentation/pages/worker_home_page.dart'; @@ -33,6 +35,9 @@ class StaffHomeModule extends Module { i.addLazySingleton( () => GetProfileCompletionUseCase(i.get()), ); + i.addLazySingleton( + () => GetBenefitsHistoryUseCase(i.get()), + ); // Presentation layer - Cubits i.addLazySingleton( @@ -42,9 +47,12 @@ class StaffHomeModule extends Module { ), ); - // Cubit for benefits overview page + // Cubit for benefits overview page (includes history support) i.addLazySingleton( - () => BenefitsOverviewCubit(repository: i.get()), + () => BenefitsOverviewCubit( + repository: i.get(), + getBenefitsHistory: i.get(), + ), ); } @@ -58,5 +66,16 @@ class StaffHomeModule extends Module { StaffPaths.childRoute(StaffPaths.home, StaffPaths.benefits), child: (BuildContext context) => const BenefitsOverviewPage(), ); + r.child( + StaffPaths.childRoute(StaffPaths.home, StaffPaths.benefitHistory), + child: (BuildContext context) { + final Map? args = + r.args.data as Map?; + return BenefitHistoryPage( + benefitId: args?['benefitId'] as String? ?? '', + benefitTitle: args?['benefitTitle'] as String? ?? '', + ); + }, + ); } }