From 4834266986af8ee817e15d08c1e80b91cc7868f6 Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Mon, 16 Mar 2026 15:59:22 -0400 Subject: [PATCH] feat: add entities for staff personal info, reports, shifts, and user sessions - Implemented StaffPersonalInfo entity for staff profile data. - Created ReportSummary entity for summarizing report metrics. - Added SpendReport and SpendDataPoint entities for spend reporting. - Introduced AssignedShift, CancelledShift, CompletedShift, OpenShift, PendingAssignment, ShiftDetail, TodayShift entities for shift management. - Developed ClientSession and StaffSession entities for user session management. --- .claude/agents/mobile-builder.md | 82 ++++- apps/mobile/packages/core/lib/core.dart | 5 + .../core/lib/src/config/app_config.dart | 6 + .../src/services/api_service/api_service.dart | 24 +- .../core_api_services/v2_api_endpoints.dart | 340 ++++++++++++++++++ .../src/services/api_service/dio_client.dart | 8 +- .../inspectors/idempotency_interceptor.dart | 24 ++ .../api_service/mixins/api_error_handler.dart | 135 +++++++ .../mixins/session_handler_mixin.dart | 244 +++++++++++++ apps/mobile/packages/core/pubspec.yaml | 1 + .../packages/domain/lib/krow_domain.dart | 157 ++++---- .../availability/availability_adapter.dart | 33 -- .../adapters/clock_in/clock_in_adapter.dart | 27 -- .../bank_account/bank_account_adapter.dart | 21 -- .../adapters/financial/payment_adapter.dart | 19 - .../adapters/financial/time_card_adapter.dart | 49 --- .../profile/bank_account_adapter.dart | 53 --- .../profile/emergency_contact_adapter.dart | 19 - .../adapters/profile/experience_adapter.dart | 18 - .../adapters/profile/tax_form_adapter.dart | 104 ------ .../adapters/shifts/break/break_adapter.dart | 39 -- .../src/adapters/shifts/shift_adapter.dart | 59 --- .../api_services/base_api_service.dart | 7 + .../availability/availability_day.dart | 86 +++++ .../availability/availability_slot.dart | 33 -- .../availability/day_availability.dart | 31 -- .../src/entities/availability/time_slot.dart | 39 ++ .../lib/src/entities/benefits/benefit.dart | 71 +++- .../src/entities/business/biz_contract.dart | 36 -- .../lib/src/entities/business/business.dart | 124 +++++-- .../entities/business/business_setting.dart | 41 --- .../src/entities/business/cost_center.dart | 33 +- .../domain/lib/src/entities/business/hub.dart | 122 +++++-- .../src/entities/business/hub_department.dart | 24 -- .../src/entities/business/hub_manager.dart | 54 +++ .../src/entities/business/team_member.dart | 61 ++++ .../lib/src/entities/business/vendor.dart | 83 ++++- .../src/entities/business/vendor_role.dart | 49 +++ .../entities/clock_in/attendance_status.dart | 57 ++- .../coverage_domain/assigned_worker.dart | 65 ++++ .../coverage_domain/core_team_member.dart | 68 ++++ .../coverage_domain/coverage_shift.dart | 57 --- .../coverage_domain/coverage_stats.dart | 78 ++-- .../coverage_domain/coverage_worker.dart | 55 --- .../coverage_domain/shift_with_workers.dart | 80 +++++ .../entities/coverage_domain/time_range.dart | 37 ++ .../lib/src/entities/enums/account_type.dart | 30 ++ .../entities/enums/application_status.dart | 48 +++ .../src/entities/enums/assignment_status.dart | 48 +++ .../enums/attendance_status_type.dart | 36 ++ .../entities/enums/availability_status.dart | 34 ++ .../src/entities/enums/benefit_status.dart | 34 ++ .../src/entities/enums/business_status.dart | 34 ++ .../src/entities/enums/invoice_status.dart | 48 +++ .../src/entities/enums/onboarding_status.dart | 33 ++ .../lib/src/entities/enums/order_type.dart | 36 ++ .../src/entities/enums/payment_status.dart | 36 ++ .../lib/src/entities/enums/shift_status.dart | 45 +++ .../lib/src/entities/enums/staff_status.dart | 36 ++ .../lib/src/entities/events/assignment.dart | 58 --- .../domain/lib/src/entities/events/event.dart | 70 ---- .../lib/src/entities/events/event_shift.dart | 28 -- .../entities/events/event_shift_position.dart | 53 --- .../lib/src/entities/events/work_session.dart | 32 -- .../src/entities/financial/bank_account.dart | 70 ++++ .../financial/bank_account/bank_account.dart | 27 -- .../bank_account/business_bank_account.dart | 26 -- .../bank_account/staff_bank_account.dart | 48 --- .../entities/financial/billing_account.dart | 77 ++++ .../entities/financial/billing_period.dart | 8 - .../src/entities/financial/current_bill.dart | 29 ++ .../lib/src/entities/financial/invoice.dart | 190 ++++------ .../entities/financial/invoice_decline.dart | 26 -- .../src/entities/financial/invoice_item.dart | 36 -- .../financial/payment_chart_point.dart | 37 ++ .../entities/financial/payment_summary.dart | 39 +- .../lib/src/entities/financial/savings.dart | 29 ++ .../src/entities/financial/spend_item.dart | 43 +++ .../src/entities/financial/staff_payment.dart | 130 ++++--- .../lib/src/entities/financial/time_card.dart | 136 +++---- .../src/entities/home/client_dashboard.dart | 75 ++++ .../src/entities/home/coverage_metrics.dart | 42 +++ .../entities/home/home_dashboard_data.dart | 45 --- .../entities/home/live_activity_metrics.dart | 43 +++ .../lib/src/entities/home/reorder_item.dart | 51 --- .../src/entities/home/spending_summary.dart | 37 ++ .../src/entities/home/staff_dashboard.dart | 80 +++++ .../orders/assigned_worker_summary.dart | 58 +++ .../src/entities/orders/one_time_order.dart | 98 ----- .../orders/one_time_order_position.dart | 62 ---- .../lib/src/entities/orders/order_item.dart | 192 +++++----- .../src/entities/orders/order_preview.dart | 235 ++++++++++++ .../lib/src/entities/orders/order_type.dart | 30 -- .../src/entities/orders/permanent_order.dart | 41 --- .../orders/permanent_order_position.dart | 60 ---- .../lib/src/entities/orders/recent_order.dart | 65 ++++ .../src/entities/orders/recurring_order.dart | 106 ------ .../orders/recurring_order_position.dart | 60 ---- .../lib/src/entities/orders/reorder_data.dart | 76 ---- .../entities/profile/attire_checklist.dart | 138 +++++++ .../lib/src/entities/profile/attire_item.dart | 86 ----- .../profile/attire_verification_status.dart | 39 -- .../lib/src/entities/profile/certificate.dart | 147 ++++++++ .../src/entities/profile/compliance_type.dart | 25 -- .../profile/document_verification_status.dart | 39 -- .../entities/profile/emergency_contact.dart | 102 +++--- .../entities/profile/experience_skill.dart | 30 -- .../lib/src/entities/profile/industry.dart | 21 -- .../entities/profile/privacy_settings.dart | 32 ++ .../entities/profile/profile_completion.dart | 81 +++++ .../entities/profile/profile_document.dart | 183 ++++++++++ .../profile/profile_section_status.dart | 76 ++++ .../entities/profile/relationship_type.dart | 6 - .../lib/src/entities/profile/schedule.dart | 32 -- .../entities/profile/staff_certificate.dart | 127 ------- .../profile/staff_certificate_status.dart | 23 -- .../staff_certificate_validation_status.dart | 22 -- .../src/entities/profile/staff_document.dart | 66 ---- .../entities/profile/staff_personal_info.dart | 105 ++++++ .../lib/src/entities/profile/tax_form.dart | 167 +++++---- .../ratings/business_staff_preference.dart | 35 -- .../lib/src/entities/ratings/penalty_log.dart | 36 -- .../src/entities/ratings/staff_rating.dart | 97 ++++- .../src/entities/reports/coverage_report.dart | 112 ++++-- .../entities/reports/daily_ops_report.dart | 115 +++--- .../src/entities/reports/forecast_report.dart | 163 ++++++--- .../src/entities/reports/no_show_report.dart | 181 ++++++++-- .../entities/reports/performance_report.dart | 99 +++-- .../src/entities/reports/report_summary.dart | 71 ++++ .../src/entities/reports/reports_summary.dart | 31 -- .../entities/reports/spend_data_point.dart | 95 +++++ .../src/entities/reports/spend_report.dart | 85 ----- .../src/entities/shifts/assigned_shift.dart | 100 ++++++ .../lib/src/entities/shifts/break/break.dart | 47 --- .../src/entities/shifts/cancelled_shift.dart | 69 ++++ .../src/entities/shifts/completed_shift.dart | 78 ++++ .../lib/src/entities/shifts/open_shift.dart | 106 ++++++ .../entities/shifts/pending_assignment.dart | 83 +++++ .../domain/lib/src/entities/shifts/shift.dart | 252 +++++++------ .../lib/src/entities/shifts/shift_detail.dart | 149 ++++++++ .../lib/src/entities/shifts/today_shift.dart | 88 +++++ .../lib/src/entities/skills/certificate.dart | 24 -- .../domain/lib/src/entities/skills/skill.dart | 29 -- .../src/entities/skills/skill_category.dart | 18 - .../lib/src/entities/skills/skill_kit.dart | 32 -- .../lib/src/entities/skills/staff_skill.dart | 58 --- .../lib/src/entities/support/addon.dart | 26 -- .../lib/src/entities/support/media.dart | 24 -- .../domain/lib/src/entities/support/tag.dart | 18 - .../src/entities/support/working_area.dart | 30 -- .../lib/src/entities/users/biz_member.dart | 174 ++++++++- .../src/entities/users/client_session.dart | 128 +++++++ .../lib/src/entities/users/hub_member.dart | 28 -- .../lib/src/entities/users/membership.dart | 32 -- .../domain/lib/src/entities/users/staff.dart | 235 ++++++++---- .../lib/src/entities/users/staff_session.dart | 177 +++++++++ .../domain/lib/src/entities/users/user.dart | 108 +++++- .../presentation/blocs/view_orders_cubit.dart | 8 +- .../presentation/widgets/view_order_card.dart | 2 +- 159 files changed, 6857 insertions(+), 3937 deletions(-) create mode 100644 apps/mobile/packages/core/lib/src/services/api_service/core_api_services/v2_api_endpoints.dart create mode 100644 apps/mobile/packages/core/lib/src/services/api_service/inspectors/idempotency_interceptor.dart create mode 100644 apps/mobile/packages/core/lib/src/services/api_service/mixins/api_error_handler.dart create mode 100644 apps/mobile/packages/core/lib/src/services/api_service/mixins/session_handler_mixin.dart delete mode 100644 apps/mobile/packages/domain/lib/src/adapters/availability/availability_adapter.dart delete mode 100644 apps/mobile/packages/domain/lib/src/adapters/clock_in/clock_in_adapter.dart delete mode 100644 apps/mobile/packages/domain/lib/src/adapters/financial/bank_account/bank_account_adapter.dart delete mode 100644 apps/mobile/packages/domain/lib/src/adapters/financial/payment_adapter.dart delete mode 100644 apps/mobile/packages/domain/lib/src/adapters/financial/time_card_adapter.dart delete mode 100644 apps/mobile/packages/domain/lib/src/adapters/profile/bank_account_adapter.dart delete mode 100644 apps/mobile/packages/domain/lib/src/adapters/profile/emergency_contact_adapter.dart delete mode 100644 apps/mobile/packages/domain/lib/src/adapters/profile/experience_adapter.dart delete mode 100644 apps/mobile/packages/domain/lib/src/adapters/profile/tax_form_adapter.dart delete mode 100644 apps/mobile/packages/domain/lib/src/adapters/shifts/break/break_adapter.dart delete mode 100644 apps/mobile/packages/domain/lib/src/adapters/shifts/shift_adapter.dart create mode 100644 apps/mobile/packages/domain/lib/src/entities/availability/availability_day.dart delete mode 100644 apps/mobile/packages/domain/lib/src/entities/availability/availability_slot.dart delete mode 100644 apps/mobile/packages/domain/lib/src/entities/availability/day_availability.dart create mode 100644 apps/mobile/packages/domain/lib/src/entities/availability/time_slot.dart delete mode 100644 apps/mobile/packages/domain/lib/src/entities/business/biz_contract.dart delete mode 100644 apps/mobile/packages/domain/lib/src/entities/business/business_setting.dart delete mode 100644 apps/mobile/packages/domain/lib/src/entities/business/hub_department.dart create mode 100644 apps/mobile/packages/domain/lib/src/entities/business/hub_manager.dart create mode 100644 apps/mobile/packages/domain/lib/src/entities/business/team_member.dart create mode 100644 apps/mobile/packages/domain/lib/src/entities/business/vendor_role.dart create mode 100644 apps/mobile/packages/domain/lib/src/entities/coverage_domain/assigned_worker.dart create mode 100644 apps/mobile/packages/domain/lib/src/entities/coverage_domain/core_team_member.dart delete mode 100644 apps/mobile/packages/domain/lib/src/entities/coverage_domain/coverage_shift.dart delete mode 100644 apps/mobile/packages/domain/lib/src/entities/coverage_domain/coverage_worker.dart create mode 100644 apps/mobile/packages/domain/lib/src/entities/coverage_domain/shift_with_workers.dart create mode 100644 apps/mobile/packages/domain/lib/src/entities/coverage_domain/time_range.dart create mode 100644 apps/mobile/packages/domain/lib/src/entities/enums/account_type.dart create mode 100644 apps/mobile/packages/domain/lib/src/entities/enums/application_status.dart create mode 100644 apps/mobile/packages/domain/lib/src/entities/enums/assignment_status.dart create mode 100644 apps/mobile/packages/domain/lib/src/entities/enums/attendance_status_type.dart create mode 100644 apps/mobile/packages/domain/lib/src/entities/enums/availability_status.dart create mode 100644 apps/mobile/packages/domain/lib/src/entities/enums/benefit_status.dart create mode 100644 apps/mobile/packages/domain/lib/src/entities/enums/business_status.dart create mode 100644 apps/mobile/packages/domain/lib/src/entities/enums/invoice_status.dart create mode 100644 apps/mobile/packages/domain/lib/src/entities/enums/onboarding_status.dart create mode 100644 apps/mobile/packages/domain/lib/src/entities/enums/order_type.dart create mode 100644 apps/mobile/packages/domain/lib/src/entities/enums/payment_status.dart create mode 100644 apps/mobile/packages/domain/lib/src/entities/enums/shift_status.dart create mode 100644 apps/mobile/packages/domain/lib/src/entities/enums/staff_status.dart delete mode 100644 apps/mobile/packages/domain/lib/src/entities/events/assignment.dart delete mode 100644 apps/mobile/packages/domain/lib/src/entities/events/event.dart delete mode 100644 apps/mobile/packages/domain/lib/src/entities/events/event_shift.dart delete mode 100644 apps/mobile/packages/domain/lib/src/entities/events/event_shift_position.dart delete mode 100644 apps/mobile/packages/domain/lib/src/entities/events/work_session.dart create mode 100644 apps/mobile/packages/domain/lib/src/entities/financial/bank_account.dart delete mode 100644 apps/mobile/packages/domain/lib/src/entities/financial/bank_account/bank_account.dart delete mode 100644 apps/mobile/packages/domain/lib/src/entities/financial/bank_account/business_bank_account.dart delete mode 100644 apps/mobile/packages/domain/lib/src/entities/financial/bank_account/staff_bank_account.dart create mode 100644 apps/mobile/packages/domain/lib/src/entities/financial/billing_account.dart delete mode 100644 apps/mobile/packages/domain/lib/src/entities/financial/billing_period.dart create mode 100644 apps/mobile/packages/domain/lib/src/entities/financial/current_bill.dart delete mode 100644 apps/mobile/packages/domain/lib/src/entities/financial/invoice_decline.dart delete mode 100644 apps/mobile/packages/domain/lib/src/entities/financial/invoice_item.dart create mode 100644 apps/mobile/packages/domain/lib/src/entities/financial/payment_chart_point.dart create mode 100644 apps/mobile/packages/domain/lib/src/entities/financial/savings.dart create mode 100644 apps/mobile/packages/domain/lib/src/entities/financial/spend_item.dart create mode 100644 apps/mobile/packages/domain/lib/src/entities/home/client_dashboard.dart create mode 100644 apps/mobile/packages/domain/lib/src/entities/home/coverage_metrics.dart delete mode 100644 apps/mobile/packages/domain/lib/src/entities/home/home_dashboard_data.dart create mode 100644 apps/mobile/packages/domain/lib/src/entities/home/live_activity_metrics.dart delete mode 100644 apps/mobile/packages/domain/lib/src/entities/home/reorder_item.dart create mode 100644 apps/mobile/packages/domain/lib/src/entities/home/spending_summary.dart create mode 100644 apps/mobile/packages/domain/lib/src/entities/home/staff_dashboard.dart create mode 100644 apps/mobile/packages/domain/lib/src/entities/orders/assigned_worker_summary.dart delete mode 100644 apps/mobile/packages/domain/lib/src/entities/orders/one_time_order.dart delete mode 100644 apps/mobile/packages/domain/lib/src/entities/orders/one_time_order_position.dart create mode 100644 apps/mobile/packages/domain/lib/src/entities/orders/order_preview.dart delete mode 100644 apps/mobile/packages/domain/lib/src/entities/orders/order_type.dart delete mode 100644 apps/mobile/packages/domain/lib/src/entities/orders/permanent_order.dart delete mode 100644 apps/mobile/packages/domain/lib/src/entities/orders/permanent_order_position.dart create mode 100644 apps/mobile/packages/domain/lib/src/entities/orders/recent_order.dart delete mode 100644 apps/mobile/packages/domain/lib/src/entities/orders/recurring_order.dart delete mode 100644 apps/mobile/packages/domain/lib/src/entities/orders/recurring_order_position.dart delete mode 100644 apps/mobile/packages/domain/lib/src/entities/orders/reorder_data.dart create mode 100644 apps/mobile/packages/domain/lib/src/entities/profile/attire_checklist.dart delete mode 100644 apps/mobile/packages/domain/lib/src/entities/profile/attire_item.dart delete mode 100644 apps/mobile/packages/domain/lib/src/entities/profile/attire_verification_status.dart create mode 100644 apps/mobile/packages/domain/lib/src/entities/profile/certificate.dart delete mode 100644 apps/mobile/packages/domain/lib/src/entities/profile/compliance_type.dart delete mode 100644 apps/mobile/packages/domain/lib/src/entities/profile/document_verification_status.dart delete mode 100644 apps/mobile/packages/domain/lib/src/entities/profile/experience_skill.dart delete mode 100644 apps/mobile/packages/domain/lib/src/entities/profile/industry.dart create mode 100644 apps/mobile/packages/domain/lib/src/entities/profile/privacy_settings.dart create mode 100644 apps/mobile/packages/domain/lib/src/entities/profile/profile_completion.dart create mode 100644 apps/mobile/packages/domain/lib/src/entities/profile/profile_document.dart create mode 100644 apps/mobile/packages/domain/lib/src/entities/profile/profile_section_status.dart delete mode 100644 apps/mobile/packages/domain/lib/src/entities/profile/relationship_type.dart delete mode 100644 apps/mobile/packages/domain/lib/src/entities/profile/schedule.dart delete mode 100644 apps/mobile/packages/domain/lib/src/entities/profile/staff_certificate.dart delete mode 100644 apps/mobile/packages/domain/lib/src/entities/profile/staff_certificate_status.dart delete mode 100644 apps/mobile/packages/domain/lib/src/entities/profile/staff_certificate_validation_status.dart delete mode 100644 apps/mobile/packages/domain/lib/src/entities/profile/staff_document.dart create mode 100644 apps/mobile/packages/domain/lib/src/entities/profile/staff_personal_info.dart delete mode 100644 apps/mobile/packages/domain/lib/src/entities/ratings/business_staff_preference.dart delete mode 100644 apps/mobile/packages/domain/lib/src/entities/ratings/penalty_log.dart create mode 100644 apps/mobile/packages/domain/lib/src/entities/reports/report_summary.dart delete mode 100644 apps/mobile/packages/domain/lib/src/entities/reports/reports_summary.dart create mode 100644 apps/mobile/packages/domain/lib/src/entities/reports/spend_data_point.dart delete mode 100644 apps/mobile/packages/domain/lib/src/entities/reports/spend_report.dart create mode 100644 apps/mobile/packages/domain/lib/src/entities/shifts/assigned_shift.dart delete mode 100644 apps/mobile/packages/domain/lib/src/entities/shifts/break/break.dart create mode 100644 apps/mobile/packages/domain/lib/src/entities/shifts/cancelled_shift.dart create mode 100644 apps/mobile/packages/domain/lib/src/entities/shifts/completed_shift.dart create mode 100644 apps/mobile/packages/domain/lib/src/entities/shifts/open_shift.dart create mode 100644 apps/mobile/packages/domain/lib/src/entities/shifts/pending_assignment.dart create mode 100644 apps/mobile/packages/domain/lib/src/entities/shifts/shift_detail.dart create mode 100644 apps/mobile/packages/domain/lib/src/entities/shifts/today_shift.dart delete mode 100644 apps/mobile/packages/domain/lib/src/entities/skills/certificate.dart delete mode 100644 apps/mobile/packages/domain/lib/src/entities/skills/skill.dart delete mode 100644 apps/mobile/packages/domain/lib/src/entities/skills/skill_category.dart delete mode 100644 apps/mobile/packages/domain/lib/src/entities/skills/skill_kit.dart delete mode 100644 apps/mobile/packages/domain/lib/src/entities/skills/staff_skill.dart delete mode 100644 apps/mobile/packages/domain/lib/src/entities/support/addon.dart delete mode 100644 apps/mobile/packages/domain/lib/src/entities/support/media.dart delete mode 100644 apps/mobile/packages/domain/lib/src/entities/support/tag.dart delete mode 100644 apps/mobile/packages/domain/lib/src/entities/support/working_area.dart create mode 100644 apps/mobile/packages/domain/lib/src/entities/users/client_session.dart delete mode 100644 apps/mobile/packages/domain/lib/src/entities/users/hub_member.dart delete mode 100644 apps/mobile/packages/domain/lib/src/entities/users/membership.dart create mode 100644 apps/mobile/packages/domain/lib/src/entities/users/staff_session.dart diff --git a/.claude/agents/mobile-builder.md b/.claude/agents/mobile-builder.md index 3d9009e0..55504099 100644 --- a/.claude/agents/mobile-builder.md +++ b/.claude/agents/mobile-builder.md @@ -67,6 +67,84 @@ If any of these files are missing or unreadable, notify the user before proceedi } ``` +## V2 API Migration Rules (Active Migration) + +The mobile apps are migrating from Firebase Data Connect (direct DB) to V2 REST API. Follow these rules for ALL new and migrated features: + +### Backend Access +- **Use `ApiService.get/post/put/delete`** for ALL backend calls — NEVER use Data Connect connectors +- Import `ApiService` from `package:krow_core/core.dart` +- Use `V2ApiEndpoints` from `package:krow_core/core.dart` for endpoint URLs +- V2 API docs are at `docs/BACKEND/API_GUIDES/V2/` — check response shapes before writing code + +### Domain Entities +- Domain entities live in `packages/domain/lib/src/entities/` with `fromJson`/`toJson` directly on the class +- No separate DTO or adapter layer — entities are self-serializing +- Entities are shared across all features via `package:krow_domain/krow_domain.dart` +- When migrating: check if the entity already exists and update its `fromJson` to match V2 API response shape + +### Feature Structure +- **RepoImpl lives in the feature package** at `data/repositories/` +- **Feature-level domain layer is optional** — only add `domain/` when the feature has use cases, validators, or feature-specific interfaces +- **Simple features** (read-only, no business logic) = just `data/` + `presentation/` +- Do NOT import from `packages/data_connect/` — it is deprecated + +### Status & Type Enums +All status/type fields from the V2 API must use Dart enums, NOT raw strings. Parse at the `fromJson` boundary with a safe fallback: +```dart +enum ShiftStatus { + open, assigned, active, completed, cancelled; + + static ShiftStatus fromJson(String value) { + switch (value) { + case 'OPEN': return ShiftStatus.open; + case 'ASSIGNED': return ShiftStatus.assigned; + case 'ACTIVE': return ShiftStatus.active; + case 'COMPLETED': return ShiftStatus.completed; + case 'CANCELLED': return ShiftStatus.cancelled; + default: return ShiftStatus.open; + } + } + + String toJson() { + switch (this) { + case ShiftStatus.open: return 'OPEN'; + case ShiftStatus.assigned: return 'ASSIGNED'; + case ShiftStatus.active: return 'ACTIVE'; + case ShiftStatus.completed: return 'COMPLETED'; + case ShiftStatus.cancelled: return 'CANCELLED'; + } + } +} +``` +Place shared enums (used by multiple entities) in `packages/domain/lib/src/entities/enums/`. Feature-specific enums can live in the entity file. + +### RepoImpl Pattern +```dart +class FeatureRepositoryImpl implements FeatureRepositoryInterface { + FeatureRepositoryImpl({required ApiService apiService}) + : _apiService = apiService; + + final ApiService _apiService; + + Future> getShifts() async { + final ApiResponse response = await _apiService.get(V2ApiEndpoints.staffShiftsAssigned); + final List items = response.data['shifts'] as List; + return items.map((dynamic json) => Shift.fromJson(json as Map)).toList(); + } +} +``` + +### DI Registration +```dart +// Inject ApiService (available from CoreModule) +i.add(() => FeatureRepositoryImpl( + apiService: i.get(), +)); +``` + +--- + ## Standard Workflow Follow these steps in order for every feature implementation: @@ -91,8 +169,8 @@ Follow these steps in order for every feature implementation: - Create barrel file exporting the domain public API ### 4. Data Layer -- Create models with `fromJson`/`toJson` methods -- Implement repository classes using `DataConnectService` +- Implement repository classes using `ApiService` with `V2ApiEndpoints` — NOT DataConnectService +- Parse V2 API JSON responses into domain entities via `Entity.fromJson()` - Map errors to domain `Failure` types - Create barrel file for data layer diff --git a/apps/mobile/packages/core/lib/core.dart b/apps/mobile/packages/core/lib/core.dart index 600ff74f..33e4e5ac 100644 --- a/apps/mobile/packages/core/lib/core.dart +++ b/apps/mobile/packages/core/lib/core.dart @@ -16,8 +16,13 @@ export 'src/routing/routing.dart'; export 'src/services/api_service/api_service.dart'; export 'src/services/api_service/dio_client.dart'; +// API Mixins +export 'src/services/api_service/mixins/api_error_handler.dart'; +export 'src/services/api_service/mixins/session_handler_mixin.dart'; + // Core API Services export 'src/services/api_service/core_api_services/core_api_endpoints.dart'; +export 'src/services/api_service/core_api_services/v2_api_endpoints.dart'; export 'src/services/api_service/core_api_services/file_upload/file_upload_service.dart'; export 'src/services/api_service/core_api_services/file_upload/file_upload_response.dart'; export 'src/services/api_service/core_api_services/signed_url/signed_url_service.dart'; diff --git a/apps/mobile/packages/core/lib/src/config/app_config.dart b/apps/mobile/packages/core/lib/src/config/app_config.dart index 6752f3c6..1dab4a2b 100644 --- a/apps/mobile/packages/core/lib/src/config/app_config.dart +++ b/apps/mobile/packages/core/lib/src/config/app_config.dart @@ -13,4 +13,10 @@ class AppConfig { static const String coreApiBaseUrl = String.fromEnvironment( 'CORE_API_BASE_URL', ); + + /// The base URL for the V2 Unified API gateway. + static const String v2ApiBaseUrl = String.fromEnvironment( + 'V2_API_BASE_URL', + defaultValue: 'https://krow-api-v2-933560802882.us-central1.run.app', + ); } diff --git a/apps/mobile/packages/core/lib/src/services/api_service/api_service.dart b/apps/mobile/packages/core/lib/src/services/api_service/api_service.dart index db1119c9..00c58020 100644 --- a/apps/mobile/packages/core/lib/src/services/api_service/api_service.dart +++ b/apps/mobile/packages/core/lib/src/services/api_service/api_service.dart @@ -86,6 +86,25 @@ class ApiService implements BaseApiService { } } + /// Performs a DELETE request to the specified [endpoint]. + @override + Future delete( + String endpoint, { + dynamic data, + Map? params, + }) async { + try { + final Response response = await _dio.delete( + endpoint, + data: data, + queryParameters: params, + ); + return _handleResponse(response); + } on DioException catch (e) { + return _handleError(e); + } + } + /// Extracts [ApiResponse] from a successful [Response]. ApiResponse _handleResponse(Response response) { return ApiResponse( @@ -96,6 +115,9 @@ class ApiService implements BaseApiService { } /// Extracts [ApiResponse] from a [DioException]. + /// + /// Supports both legacy error format and V2 API error envelope + /// (`{ code, message, details, requestId }`). ApiResponse _handleError(DioException e) { if (e.response?.data is Map) { final Map body = @@ -106,7 +128,7 @@ class ApiService implements BaseApiService { e.response?.statusCode?.toString() ?? 'error', message: body['message']?.toString() ?? e.message ?? 'Error occurred', - data: body['data'], + data: body['data'] ?? body['details'], errors: _parseErrors(body['errors']), ); } diff --git a/apps/mobile/packages/core/lib/src/services/api_service/core_api_services/v2_api_endpoints.dart b/apps/mobile/packages/core/lib/src/services/api_service/core_api_services/v2_api_endpoints.dart new file mode 100644 index 00000000..a902410f --- /dev/null +++ b/apps/mobile/packages/core/lib/src/services/api_service/core_api_services/v2_api_endpoints.dart @@ -0,0 +1,340 @@ +import '../../../config/app_config.dart'; + +/// Constants for V2 Unified API endpoints. +/// +/// All mobile read/write operations go through the V2 gateway which proxies +/// to the internal Query API and Command API services. +class V2ApiEndpoints { + V2ApiEndpoints._(); + + /// The base URL for the V2 Unified API gateway. + static const String baseUrl = AppConfig.v2ApiBaseUrl; + + // ── Auth ────────────────────────────────────────────────────────────── + + /// Client email/password sign-in. + static const String clientSignIn = '$baseUrl/auth/client/sign-in'; + + /// Client business registration. + static const String clientSignUp = '$baseUrl/auth/client/sign-up'; + + /// Client sign-out. + static const String clientSignOut = '$baseUrl/auth/client/sign-out'; + + /// Start staff phone verification (SMS). + static const String staffPhoneStart = '$baseUrl/auth/staff/phone/start'; + + /// Complete staff phone verification. + static const String staffPhoneVerify = '$baseUrl/auth/staff/phone/verify'; + + /// Generic sign-out. + static const String signOut = '$baseUrl/auth/sign-out'; + + /// Get current session data. + static const String session = '$baseUrl/auth/session'; + + // ── Staff Read ──────────────────────────────────────────────────────── + + /// Staff session data. + static const String staffSession = '$baseUrl/staff/session'; + + /// Staff dashboard overview. + static const String staffDashboard = '$baseUrl/staff/dashboard'; + + /// Staff profile completion status. + static const String staffProfileCompletion = + '$baseUrl/staff/profile-completion'; + + /// Staff availability schedule. + static const String staffAvailability = '$baseUrl/staff/availability'; + + /// Today's shifts for clock-in. + static const String staffClockInShiftsToday = + '$baseUrl/staff/clock-in/shifts/today'; + + /// Current clock-in status. + static const String staffClockInStatus = '$baseUrl/staff/clock-in/status'; + + /// Payments summary. + static const String staffPaymentsSummary = '$baseUrl/staff/payments/summary'; + + /// Payments history. + static const String staffPaymentsHistory = '$baseUrl/staff/payments/history'; + + /// Payments chart data. + static const String staffPaymentsChart = '$baseUrl/staff/payments/chart'; + + /// Assigned shifts. + static const String staffShiftsAssigned = '$baseUrl/staff/shifts/assigned'; + + /// Open shifts available to apply. + static const String staffShiftsOpen = '$baseUrl/staff/shifts/open'; + + /// Pending shift assignments. + static const String staffShiftsPending = '$baseUrl/staff/shifts/pending'; + + /// Cancelled shifts. + static const String staffShiftsCancelled = '$baseUrl/staff/shifts/cancelled'; + + /// Completed shifts. + static const String staffShiftsCompleted = '$baseUrl/staff/shifts/completed'; + + /// Shift details by ID. + static String staffShiftDetails(String shiftId) => + '$baseUrl/staff/shifts/$shiftId'; + + /// Staff profile sections overview. + static const String staffProfileSections = '$baseUrl/staff/profile/sections'; + + /// Personal info. + static const String staffPersonalInfo = '$baseUrl/staff/profile/personal-info'; + + /// Industries/experience. + static const String staffIndustries = '$baseUrl/staff/profile/industries'; + + /// Skills. + static const String staffSkills = '$baseUrl/staff/profile/skills'; + + /// Documents. + static const String staffDocuments = '$baseUrl/staff/profile/documents'; + + /// Attire items. + static const String staffAttire = '$baseUrl/staff/profile/attire'; + + /// Tax forms. + static const String staffTaxForms = '$baseUrl/staff/profile/tax-forms'; + + /// Emergency contacts. + static const String staffEmergencyContacts = + '$baseUrl/staff/profile/emergency-contacts'; + + /// Certificates. + static const String staffCertificates = '$baseUrl/staff/profile/certificates'; + + /// Bank accounts. + static const String staffBankAccounts = '$baseUrl/staff/profile/bank-accounts'; + + /// Benefits. + static const String staffBenefits = '$baseUrl/staff/profile/benefits'; + + /// Time card. + static const String staffTimeCard = '$baseUrl/staff/profile/time-card'; + + /// Privacy settings. + static const String staffPrivacy = '$baseUrl/staff/profile/privacy'; + + /// FAQs. + static const String staffFaqs = '$baseUrl/staff/faqs'; + + /// FAQs search. + static const String staffFaqsSearch = '$baseUrl/staff/faqs/search'; + + // ── Staff Write ─────────────────────────────────────────────────────── + + /// Staff profile setup. + static const String staffProfileSetup = '$baseUrl/staff/profile/setup'; + + /// Clock in. + static const String staffClockIn = '$baseUrl/staff/clock-in'; + + /// Clock out. + static const String staffClockOut = '$baseUrl/staff/clock-out'; + + /// Quick-set availability. + static const String staffAvailabilityQuickSet = + '$baseUrl/staff/availability/quick-set'; + + /// Apply for a shift. + static String staffShiftApply(String shiftId) => + '$baseUrl/staff/shifts/$shiftId/apply'; + + /// Accept a shift. + static String staffShiftAccept(String shiftId) => + '$baseUrl/staff/shifts/$shiftId/accept'; + + /// Decline a shift. + static String staffShiftDecline(String shiftId) => + '$baseUrl/staff/shifts/$shiftId/decline'; + + /// Request a shift swap. + static String staffShiftRequestSwap(String shiftId) => + '$baseUrl/staff/shifts/$shiftId/request-swap'; + + /// Update emergency contact by ID. + static String staffEmergencyContactUpdate(String contactId) => + '$baseUrl/staff/profile/emergency-contacts/$contactId'; + + /// Update tax form by type. + static String staffTaxFormUpdate(String formType) => + '$baseUrl/staff/profile/tax-forms/$formType'; + + /// Submit tax form by type. + static String staffTaxFormSubmit(String formType) => + '$baseUrl/staff/profile/tax-forms/$formType/submit'; + + /// Upload staff profile photo. + static const String staffProfilePhoto = '$baseUrl/staff/profile/photo'; + + /// Upload document by ID. + static String staffDocumentUpload(String documentId) => + '$baseUrl/staff/profile/documents/$documentId/upload'; + + /// Upload attire by ID. + static String staffAttireUpload(String documentId) => + '$baseUrl/staff/profile/attire/$documentId/upload'; + + /// Delete certificate by ID. + static String staffCertificateDelete(String certificateId) => + '$baseUrl/staff/profile/certificates/$certificateId'; + + // ── Client Read ─────────────────────────────────────────────────────── + + /// Client session data. + static const String clientSession = '$baseUrl/client/session'; + + /// Client dashboard. + static const String clientDashboard = '$baseUrl/client/dashboard'; + + /// Client reorders. + static const String clientReorders = '$baseUrl/client/reorders'; + + /// Billing accounts. + static const String clientBillingAccounts = '$baseUrl/client/billing/accounts'; + + /// Pending invoices. + static const String clientBillingInvoicesPending = + '$baseUrl/client/billing/invoices/pending'; + + /// Invoice history. + static const String clientBillingInvoicesHistory = + '$baseUrl/client/billing/invoices/history'; + + /// Current bill. + static const String clientBillingCurrentBill = + '$baseUrl/client/billing/current-bill'; + + /// Savings data. + static const String clientBillingSavings = '$baseUrl/client/billing/savings'; + + /// Spend breakdown. + static const String clientBillingSpendBreakdown = + '$baseUrl/client/billing/spend-breakdown'; + + /// Coverage overview. + static const String clientCoverage = '$baseUrl/client/coverage'; + + /// Coverage stats. + static const String clientCoverageStats = '$baseUrl/client/coverage/stats'; + + /// Core team. + static const String clientCoverageCoreTeam = + '$baseUrl/client/coverage/core-team'; + + /// Hubs list. + static const String clientHubs = '$baseUrl/client/hubs'; + + /// Cost centers. + static const String clientCostCenters = '$baseUrl/client/cost-centers'; + + /// Vendors. + static const String clientVendors = '$baseUrl/client/vendors'; + + /// Vendor roles by ID. + static String clientVendorRoles(String vendorId) => + '$baseUrl/client/vendors/$vendorId/roles'; + + /// Hub managers by ID. + static String clientHubManagers(String hubId) => + '$baseUrl/client/hubs/$hubId/managers'; + + /// Team members. + static const String clientTeamMembers = '$baseUrl/client/team-members'; + + /// View orders. + static const String clientOrdersView = '$baseUrl/client/orders/view'; + + /// Order reorder preview. + static String clientOrderReorderPreview(String orderId) => + '$baseUrl/client/orders/$orderId/reorder-preview'; + + /// Reports summary. + static const String clientReportsSummary = '$baseUrl/client/reports/summary'; + + /// Daily ops report. + static const String clientReportsDailyOps = + '$baseUrl/client/reports/daily-ops'; + + /// Spend report. + static const String clientReportsSpend = '$baseUrl/client/reports/spend'; + + /// Coverage report. + static const String clientReportsCoverage = + '$baseUrl/client/reports/coverage'; + + /// Forecast report. + static const String clientReportsForecast = + '$baseUrl/client/reports/forecast'; + + /// Performance report. + static const String clientReportsPerformance = + '$baseUrl/client/reports/performance'; + + /// No-show report. + static const String clientReportsNoShow = '$baseUrl/client/reports/no-show'; + + // ── Client Write ────────────────────────────────────────────────────── + + /// Create one-time order. + static const String clientOrdersOneTime = '$baseUrl/client/orders/one-time'; + + /// Create recurring order. + static const String clientOrdersRecurring = + '$baseUrl/client/orders/recurring'; + + /// Create permanent order. + static const String clientOrdersPermanent = + '$baseUrl/client/orders/permanent'; + + /// Edit order by ID. + static String clientOrderEdit(String orderId) => + '$baseUrl/client/orders/$orderId/edit'; + + /// Cancel order by ID. + static String clientOrderCancel(String orderId) => + '$baseUrl/client/orders/$orderId/cancel'; + + /// Create hub. + static const String clientHubCreate = '$baseUrl/client/hubs'; + + /// Update hub by ID. + static String clientHubUpdate(String hubId) => + '$baseUrl/client/hubs/$hubId'; + + /// Delete hub by ID. + static String clientHubDelete(String hubId) => + '$baseUrl/client/hubs/$hubId'; + + /// Assign NFC to hub. + static String clientHubAssignNfc(String hubId) => + '$baseUrl/client/hubs/$hubId/assign-nfc'; + + /// Assign managers to hub. + static String clientHubAssignManagers(String hubId) => + '$baseUrl/client/hubs/$hubId/managers'; + + /// Approve invoice. + static String clientInvoiceApprove(String invoiceId) => + '$baseUrl/client/billing/invoices/$invoiceId/approve'; + + /// Dispute invoice. + static String clientInvoiceDispute(String invoiceId) => + '$baseUrl/client/billing/invoices/$invoiceId/dispute'; + + /// Submit coverage review. + static const String clientCoverageReviews = + '$baseUrl/client/coverage/reviews'; + + /// Cancel late worker assignment. + static String clientCoverageCancelLateWorker(String assignmentId) => + '$baseUrl/client/coverage/late-workers/$assignmentId/cancel'; +} diff --git a/apps/mobile/packages/core/lib/src/services/api_service/dio_client.dart b/apps/mobile/packages/core/lib/src/services/api_service/dio_client.dart index 687524a7..f869e260 100644 --- a/apps/mobile/packages/core/lib/src/services/api_service/dio_client.dart +++ b/apps/mobile/packages/core/lib/src/services/api_service/dio_client.dart @@ -1,8 +1,9 @@ import 'package:dio/dio.dart'; import 'package:krow_core/src/services/api_service/inspectors/auth_interceptor.dart'; +import 'package:krow_core/src/services/api_service/inspectors/idempotency_interceptor.dart'; -/// A custom Dio client for the KROW project that includes basic configuration -/// and an [AuthInterceptor]. +/// A custom Dio client for the KROW project that includes basic configuration, +/// [AuthInterceptor], and [IdempotencyInterceptor]. class DioClient extends DioMixin implements Dio { DioClient([BaseOptions? baseOptions]) { options = @@ -18,10 +19,11 @@ class DioClient extends DioMixin implements Dio { // Add interceptors interceptors.addAll([ AuthInterceptor(), + IdempotencyInterceptor(), LogInterceptor( requestBody: true, responseBody: true, - ), // Added for better debugging + ), ]); } } diff --git a/apps/mobile/packages/core/lib/src/services/api_service/inspectors/idempotency_interceptor.dart b/apps/mobile/packages/core/lib/src/services/api_service/inspectors/idempotency_interceptor.dart new file mode 100644 index 00000000..828dd11b --- /dev/null +++ b/apps/mobile/packages/core/lib/src/services/api_service/inspectors/idempotency_interceptor.dart @@ -0,0 +1,24 @@ +import 'package:dio/dio.dart'; +import 'package:uuid/uuid.dart'; + +/// A Dio interceptor that adds an `Idempotency-Key` header to write requests. +/// +/// The V2 API requires an idempotency key for all POST, PUT, and DELETE +/// requests to prevent duplicate operations. A unique UUID v4 is generated +/// per request automatically. +class IdempotencyInterceptor extends Interceptor { + /// The UUID generator instance. + static const Uuid _uuid = Uuid(); + + @override + void onRequest( + RequestOptions options, + RequestInterceptorHandler handler, + ) { + final String method = options.method.toUpperCase(); + if (method == 'POST' || method == 'PUT' || method == 'DELETE') { + options.headers['Idempotency-Key'] = _uuid.v4(); + } + handler.next(options); + } +} diff --git a/apps/mobile/packages/core/lib/src/services/api_service/mixins/api_error_handler.dart b/apps/mobile/packages/core/lib/src/services/api_service/mixins/api_error_handler.dart new file mode 100644 index 00000000..da98e982 --- /dev/null +++ b/apps/mobile/packages/core/lib/src/services/api_service/mixins/api_error_handler.dart @@ -0,0 +1,135 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:dio/dio.dart'; +import 'package:flutter/material.dart'; +import 'package:krow_domain/krow_domain.dart'; + +/// Mixin to handle API layer errors and map them to domain exceptions. +/// +/// Use this in repository implementations to wrap [ApiService] calls. +/// It catches [DioException], [SocketException], etc., and throws +/// the appropriate [AppException] subclass. +mixin ApiErrorHandler { + /// Executes a Future and maps low-level exceptions to [AppException]. + /// + /// [timeout] defaults to 30 seconds. + Future executeProtected( + Future Function() action, { + Duration timeout = const Duration(seconds: 30), + }) async { + try { + return await action().timeout(timeout); + } on TimeoutException { + debugPrint( + 'ApiErrorHandler: Request timed out after ${timeout.inSeconds}s', + ); + throw ServiceUnavailableException( + technicalMessage: 'Request timed out after ${timeout.inSeconds}s', + ); + } on DioException catch (e) { + throw _mapDioException(e); + } on SocketException catch (e) { + throw NetworkException( + technicalMessage: 'SocketException: ${e.message}', + ); + } catch (e) { + // If it's already an AppException, rethrow it. + if (e is AppException) rethrow; + + final String errorStr = e.toString().toLowerCase(); + if (_isNetworkRelated(errorStr)) { + debugPrint('ApiErrorHandler: Network-related error: $e'); + throw NetworkException(technicalMessage: e.toString()); + } + + debugPrint('ApiErrorHandler: Unhandled exception caught: $e'); + throw UnknownException(technicalMessage: e.toString()); + } + } + + /// Maps a [DioException] to the appropriate [AppException]. + AppException _mapDioException(DioException e) { + switch (e.type) { + case DioExceptionType.connectionTimeout: + case DioExceptionType.sendTimeout: + case DioExceptionType.receiveTimeout: + debugPrint('ApiErrorHandler: Dio timeout: ${e.type}'); + return ServiceUnavailableException( + technicalMessage: 'Dio ${e.type}: ${e.message}', + ); + + case DioExceptionType.connectionError: + debugPrint('ApiErrorHandler: Connection error: ${e.message}'); + return NetworkException( + technicalMessage: 'Connection error: ${e.message}', + ); + + case DioExceptionType.badResponse: + final int? statusCode = e.response?.statusCode; + final String body = e.response?.data?.toString() ?? ''; + debugPrint( + 'ApiErrorHandler: Bad response $statusCode: $body', + ); + + if (statusCode == 401 || statusCode == 403) { + return NotAuthenticatedException( + technicalMessage: 'HTTP $statusCode: $body', + ); + } + if (statusCode == 404) { + return ServerException( + technicalMessage: 'HTTP 404: Not found — $body', + ); + } + if (statusCode == 429) { + return ServiceUnavailableException( + technicalMessage: 'Rate limited (429): $body', + ); + } + if (statusCode != null && statusCode >= 500) { + return ServiceUnavailableException( + technicalMessage: 'HTTP $statusCode: $body', + ); + } + return ServerException( + technicalMessage: 'HTTP $statusCode: $body', + ); + + case DioExceptionType.cancel: + return UnknownException( + technicalMessage: 'Request cancelled', + ); + + case DioExceptionType.badCertificate: + return NetworkException( + technicalMessage: 'Bad certificate: ${e.message}', + ); + + case DioExceptionType.unknown: + if (e.error is SocketException) { + return NetworkException( + technicalMessage: 'Socket error: ${e.error}', + ); + } + return UnknownException( + technicalMessage: 'Unknown Dio error: ${e.message}', + ); + } + } + + /// Checks if an error string is network-related. + bool _isNetworkRelated(String errorStr) { + return errorStr.contains('socketexception') || + errorStr.contains('network') || + errorStr.contains('offline') || + errorStr.contains('connection failed') || + errorStr.contains('unavailable') || + errorStr.contains('handshake') || + errorStr.contains('clientexception') || + errorStr.contains('failed host lookup') || + errorStr.contains('connection error') || + errorStr.contains('terminated') || + errorStr.contains('connectexception'); + } +} diff --git a/apps/mobile/packages/core/lib/src/services/api_service/mixins/session_handler_mixin.dart b/apps/mobile/packages/core/lib/src/services/api_service/mixins/session_handler_mixin.dart new file mode 100644 index 00000000..7c91cde1 --- /dev/null +++ b/apps/mobile/packages/core/lib/src/services/api_service/mixins/session_handler_mixin.dart @@ -0,0 +1,244 @@ +import 'dart:async'; + +import 'package:firebase_auth/firebase_auth.dart' as firebase_auth; +import 'package:flutter/cupertino.dart'; + +/// Enum representing the current session state. +enum SessionStateType { loading, authenticated, unauthenticated, error } + +/// Data class for session state. +class SessionState { + /// Creates a [SessionState]. + SessionState({required this.type, this.userId, this.errorMessage}); + + /// Creates a loading state. + factory SessionState.loading() => + SessionState(type: SessionStateType.loading); + + /// Creates an authenticated state. + factory SessionState.authenticated({required String userId}) => + SessionState(type: SessionStateType.authenticated, userId: userId); + + /// Creates an unauthenticated state. + factory SessionState.unauthenticated() => + SessionState(type: SessionStateType.unauthenticated); + + /// Creates an error state. + factory SessionState.error(String message) => + SessionState(type: SessionStateType.error, errorMessage: message); + + /// The type of session state. + final SessionStateType type; + + /// The current user ID (if authenticated). + final String? userId; + + /// Error message (if error occurred). + final String? errorMessage; + + @override + String toString() => + 'SessionState(type: $type, userId: $userId, error: $errorMessage)'; +} + +/// Mixin for handling Firebase Auth session management, token refresh, +/// and state emissions. +/// +/// Implementors must provide [auth] and [fetchUserRole]. The role fetch +/// should call `GET /auth/session` via [ApiService] instead of querying +/// Data Connect directly. +mixin SessionHandlerMixin { + /// Stream controller for session state changes. + final StreamController _sessionStateController = + StreamController.broadcast(); + + /// Last emitted session state (for late subscribers). + SessionState? _lastSessionState; + + /// Public stream for listening to session state changes. + /// Late subscribers will immediately receive the last emitted state. + Stream get onSessionStateChanged { + return _createStreamWithLastState(); + } + + /// Creates a stream that emits the last state before subscribing to new events. + Stream _createStreamWithLastState() async* { + if (_lastSessionState != null) { + yield _lastSessionState!; + } + yield* _sessionStateController.stream; + } + + /// Last token refresh timestamp to avoid excessive checks. + DateTime? _lastTokenRefreshTime; + + /// Subscription to auth state changes. + StreamSubscription? _authStateSubscription; + + /// Minimum interval between token refresh checks. + static const Duration _minRefreshCheckInterval = Duration(seconds: 2); + + /// Time before token expiry to trigger a refresh. + static const Duration _refreshThreshold = Duration(minutes: 5); + + /// Firebase Auth instance (to be provided by implementing class). + firebase_auth.FirebaseAuth get auth; + + /// List of allowed roles for this app (set during initialization). + List _allowedRoles = []; + + /// Initialize the auth state listener (call once on app startup). + void initializeAuthListener({ + List allowedRoles = const [], + }) { + _allowedRoles = allowedRoles; + + _authStateSubscription?.cancel(); + + _authStateSubscription = auth.authStateChanges().listen( + (firebase_auth.User? user) async { + if (user == null) { + handleSignOut(); + } else { + await _handleSignIn(user); + } + }, + onError: (Object error) { + _emitSessionState(SessionState.error(error.toString())); + }, + ); + } + + /// Validates if user has one of the allowed roles. + Future validateUserRole( + String userId, + List allowedRoles, + ) async { + try { + final String? userRole = await fetchUserRole(userId); + return userRole != null && allowedRoles.contains(userRole); + } catch (e) { + debugPrint('Failed to validate user role: $e'); + return false; + } + } + + /// Fetches user role from the backend. + /// + /// Implementors should call `GET /auth/session` via [ApiService] and + /// extract the role from the response. + Future fetchUserRole(String userId); + + /// Ensures the Firebase auth token is valid and refreshes if needed. + /// Retries up to 3 times with exponential backoff before emitting error. + Future ensureSessionValid() async { + final firebase_auth.User? user = auth.currentUser; + if (user == null) return; + + final DateTime now = DateTime.now(); + if (_lastTokenRefreshTime != null) { + final Duration timeSinceLastCheck = now.difference( + _lastTokenRefreshTime!, + ); + if (timeSinceLastCheck < _minRefreshCheckInterval) { + return; + } + } + + const int maxRetries = 3; + int retryCount = 0; + + while (retryCount < maxRetries) { + try { + final firebase_auth.IdTokenResult idToken = + await user.getIdTokenResult(); + final DateTime? expiryTime = idToken.expirationTime; + + if (expiryTime == null) return; + + final Duration timeUntilExpiry = expiryTime.difference(now); + if (timeUntilExpiry <= _refreshThreshold) { + await user.getIdTokenResult(); + } + + _lastTokenRefreshTime = now; + return; + } catch (e) { + retryCount++; + debugPrint( + 'Token validation error (attempt $retryCount/$maxRetries): $e', + ); + + if (retryCount >= maxRetries) { + _emitSessionState( + SessionState.error( + 'Token validation failed after $maxRetries attempts: $e', + ), + ); + return; + } + + final Duration backoffDuration = Duration( + seconds: 1 << (retryCount - 1), + ); + debugPrint( + 'Retrying token validation in ${backoffDuration.inSeconds}s', + ); + await Future.delayed(backoffDuration); + } + } + } + + /// Handle user sign-in event. + Future _handleSignIn(firebase_auth.User user) async { + try { + _emitSessionState(SessionState.loading()); + + if (_allowedRoles.isNotEmpty) { + final String? userRole = await fetchUserRole(user.uid); + + if (userRole == null) { + _emitSessionState(SessionState.unauthenticated()); + return; + } + + if (!_allowedRoles.contains(userRole)) { + await auth.signOut(); + _emitSessionState(SessionState.unauthenticated()); + return; + } + } + + final firebase_auth.IdTokenResult idToken = + await user.getIdTokenResult(); + if (idToken.expirationTime != null && + DateTime.now().difference(idToken.expirationTime!) < + const Duration(minutes: 5)) { + await user.getIdTokenResult(); + } + + _emitSessionState(SessionState.authenticated(userId: user.uid)); + } catch (e) { + _emitSessionState(SessionState.error(e.toString())); + } + } + + /// Handle user sign-out event. + void handleSignOut() { + _emitSessionState(SessionState.unauthenticated()); + } + + /// Emit session state update. + void _emitSessionState(SessionState state) { + _lastSessionState = state; + if (!_sessionStateController.isClosed) { + _sessionStateController.add(state); + } + } + + /// Dispose session handler resources. + Future disposeSessionHandler() async { + await _authStateSubscription?.cancel(); + await _sessionStateController.close(); + } +} diff --git a/apps/mobile/packages/core/pubspec.yaml b/apps/mobile/packages/core/pubspec.yaml index f40200eb..7b01f054 100644 --- a/apps/mobile/packages/core/pubspec.yaml +++ b/apps/mobile/packages/core/pubspec.yaml @@ -32,3 +32,4 @@ dependencies: flutter_local_notifications: ^21.0.0 shared_preferences: ^2.5.4 workmanager: ^0.9.0+3 + uuid: ^4.5.1 diff --git a/apps/mobile/packages/domain/lib/krow_domain.dart b/apps/mobile/packages/domain/lib/krow_domain.dart index c98147f3..2470cacf 100644 --- a/apps/mobile/packages/domain/lib/krow_domain.dart +++ b/apps/mobile/packages/domain/lib/krow_domain.dart @@ -6,6 +6,21 @@ /// Note: Repository Interfaces are now located in their respective Feature packages. library; +// Enums (shared status/type enums aligned with V2 CHECK constraints) +export 'src/entities/enums/account_type.dart'; +export 'src/entities/enums/application_status.dart'; +export 'src/entities/enums/assignment_status.dart'; +export 'src/entities/enums/attendance_status_type.dart'; +export 'src/entities/enums/availability_status.dart'; +export 'src/entities/enums/benefit_status.dart'; +export 'src/entities/enums/business_status.dart'; +export 'src/entities/enums/invoice_status.dart'; +export 'src/entities/enums/onboarding_status.dart'; +export 'src/entities/enums/order_type.dart'; +export 'src/entities/enums/payment_status.dart'; +export 'src/entities/enums/shift_status.dart'; +export 'src/entities/enums/staff_status.dart'; + // Core export 'src/core/services/api_services/api_response.dart'; export 'src/core/services/api_services/base_api_service.dart'; @@ -22,124 +37,90 @@ export 'src/core/models/device_location.dart'; // Users & Membership export 'src/entities/users/user.dart'; export 'src/entities/users/staff.dart'; -export 'src/entities/users/membership.dart'; export 'src/entities/users/biz_member.dart'; -export 'src/entities/users/hub_member.dart'; +export 'src/entities/users/staff_session.dart'; +export 'src/entities/users/client_session.dart'; // Business & Organization export 'src/entities/business/business.dart'; -export 'src/entities/business/business_setting.dart'; export 'src/entities/business/hub.dart'; -export 'src/entities/business/hub_department.dart'; export 'src/entities/business/vendor.dart'; export 'src/entities/business/cost_center.dart'; - -// Events & Assignments -export 'src/entities/events/event.dart'; -export 'src/entities/events/event_shift.dart'; -export 'src/entities/events/event_shift_position.dart'; -export 'src/entities/events/assignment.dart'; -export 'src/entities/events/work_session.dart'; +export 'src/entities/business/vendor_role.dart'; +export 'src/entities/business/hub_manager.dart'; +export 'src/entities/business/team_member.dart'; // Shifts export 'src/entities/shifts/shift.dart'; -export 'src/adapters/shifts/shift_adapter.dart'; -export 'src/entities/shifts/break/break.dart'; -export 'src/adapters/shifts/break/break_adapter.dart'; +export 'src/entities/shifts/today_shift.dart'; +export 'src/entities/shifts/assigned_shift.dart'; +export 'src/entities/shifts/open_shift.dart'; +export 'src/entities/shifts/pending_assignment.dart'; +export 'src/entities/shifts/cancelled_shift.dart'; +export 'src/entities/shifts/completed_shift.dart'; +export 'src/entities/shifts/shift_detail.dart'; -// Orders & Requests -export 'src/entities/orders/one_time_order.dart'; -export 'src/entities/orders/one_time_order_position.dart'; -export 'src/entities/orders/recurring_order.dart'; -export 'src/entities/orders/recurring_order_position.dart'; -export 'src/entities/orders/permanent_order.dart'; -export 'src/entities/orders/permanent_order_position.dart'; -export 'src/entities/orders/order_type.dart'; +// Orders export 'src/entities/orders/order_item.dart'; -export 'src/entities/orders/reorder_data.dart'; - -// Skills & Certs -export 'src/entities/skills/skill.dart'; -export 'src/entities/skills/skill_category.dart'; -export 'src/entities/skills/staff_skill.dart'; -export 'src/entities/skills/certificate.dart'; -export 'src/entities/skills/skill_kit.dart'; +export 'src/entities/orders/assigned_worker_summary.dart'; +export 'src/entities/orders/order_preview.dart'; +export 'src/entities/orders/recent_order.dart'; // Financial & Payroll export 'src/entities/benefits/benefit.dart'; export 'src/entities/financial/invoice.dart'; -export 'src/entities/financial/time_card.dart'; -export 'src/entities/financial/invoice_item.dart'; -export 'src/entities/financial/invoice_decline.dart'; -export 'src/entities/financial/staff_payment.dart'; +export 'src/entities/financial/billing_account.dart'; +export 'src/entities/financial/current_bill.dart'; +export 'src/entities/financial/savings.dart'; +export 'src/entities/financial/spend_item.dart'; +export 'src/entities/financial/bank_account.dart'; export 'src/entities/financial/payment_summary.dart'; -export 'src/entities/financial/billing_period.dart'; -export 'src/entities/financial/bank_account/bank_account.dart'; -export 'src/entities/financial/bank_account/business_bank_account.dart'; -export 'src/entities/financial/bank_account/staff_bank_account.dart'; -export 'src/adapters/financial/bank_account/bank_account_adapter.dart'; +export 'src/entities/financial/staff_payment.dart'; +export 'src/entities/financial/payment_chart_point.dart'; +export 'src/entities/financial/time_card.dart'; // Profile -export 'src/entities/profile/staff_document.dart'; -export 'src/entities/profile/document_verification_status.dart'; -export 'src/entities/profile/staff_certificate.dart'; -export 'src/entities/profile/compliance_type.dart'; -export 'src/entities/profile/staff_certificate_status.dart'; -export 'src/entities/profile/staff_certificate_validation_status.dart'; -export 'src/entities/profile/attire_item.dart'; -export 'src/entities/profile/attire_verification_status.dart'; -export 'src/entities/profile/relationship_type.dart'; -export 'src/entities/profile/industry.dart'; -export 'src/entities/profile/tax_form.dart'; - -// Ratings & Penalties -export 'src/entities/ratings/staff_rating.dart'; -export 'src/entities/ratings/penalty_log.dart'; -export 'src/entities/ratings/business_staff_preference.dart'; - -// Staff Profile +export 'src/entities/profile/staff_personal_info.dart'; +export 'src/entities/profile/profile_section_status.dart'; +export 'src/entities/profile/profile_completion.dart'; +export 'src/entities/profile/profile_document.dart'; +export 'src/entities/profile/certificate.dart'; export 'src/entities/profile/emergency_contact.dart'; +export 'src/entities/profile/tax_form.dart'; +export 'src/entities/profile/privacy_settings.dart'; +export 'src/entities/profile/attire_checklist.dart'; export 'src/entities/profile/accessibility.dart'; -export 'src/entities/profile/schedule.dart'; -// Support & Config -export 'src/entities/support/addon.dart'; -export 'src/entities/support/tag.dart'; -export 'src/entities/support/media.dart'; -export 'src/entities/support/working_area.dart'; +// Ratings +export 'src/entities/ratings/staff_rating.dart'; // Home -export 'src/entities/home/home_dashboard_data.dart'; -export 'src/entities/home/reorder_item.dart'; +export 'src/entities/home/client_dashboard.dart'; +export 'src/entities/home/spending_summary.dart'; +export 'src/entities/home/coverage_metrics.dart'; +export 'src/entities/home/live_activity_metrics.dart'; +export 'src/entities/home/staff_dashboard.dart'; -// Availability -export 'src/adapters/availability/availability_adapter.dart'; +// Clock-In & Availability export 'src/entities/clock_in/attendance_status.dart'; -export 'src/adapters/clock_in/clock_in_adapter.dart'; -export 'src/entities/availability/availability_slot.dart'; -export 'src/entities/availability/day_availability.dart'; +export 'src/entities/availability/availability_day.dart'; +export 'src/entities/availability/time_slot.dart'; // Coverage -export 'src/entities/coverage_domain/coverage_shift.dart'; -export 'src/entities/coverage_domain/coverage_worker.dart'; +export 'src/entities/coverage_domain/shift_with_workers.dart'; +export 'src/entities/coverage_domain/assigned_worker.dart'; +export 'src/entities/coverage_domain/time_range.dart'; export 'src/entities/coverage_domain/coverage_stats.dart'; +export 'src/entities/coverage_domain/core_team_member.dart'; -// Adapters -export 'src/adapters/profile/emergency_contact_adapter.dart'; -export 'src/adapters/profile/experience_adapter.dart'; -export 'src/entities/profile/experience_skill.dart'; -export 'src/adapters/profile/bank_account_adapter.dart'; -export 'src/adapters/profile/tax_form_adapter.dart'; -export 'src/adapters/financial/payment_adapter.dart'; +// Reports +export 'src/entities/reports/report_summary.dart'; +export 'src/entities/reports/daily_ops_report.dart'; +export 'src/entities/reports/spend_data_point.dart'; +export 'src/entities/reports/coverage_report.dart'; +export 'src/entities/reports/forecast_report.dart'; +export 'src/entities/reports/performance_report.dart'; +export 'src/entities/reports/no_show_report.dart'; // Exceptions export 'src/exceptions/app_exception.dart'; - -// Reports -export 'src/entities/reports/daily_ops_report.dart'; -export 'src/entities/reports/spend_report.dart'; -export 'src/entities/reports/coverage_report.dart'; -export 'src/entities/reports/forecast_report.dart'; -export 'src/entities/reports/no_show_report.dart'; -export 'src/entities/reports/performance_report.dart'; -export 'src/entities/reports/reports_summary.dart'; diff --git a/apps/mobile/packages/domain/lib/src/adapters/availability/availability_adapter.dart b/apps/mobile/packages/domain/lib/src/adapters/availability/availability_adapter.dart deleted file mode 100644 index f06ddeeb..00000000 --- a/apps/mobile/packages/domain/lib/src/adapters/availability/availability_adapter.dart +++ /dev/null @@ -1,33 +0,0 @@ -import '../../entities/availability/availability_slot.dart'; - -/// Adapter for [AvailabilitySlot] domain entity. -class AvailabilityAdapter { - static const Map> _slotDefinitions = >{ - 'MORNING': { - 'id': 'morning', - 'label': 'Morning', - 'timeRange': '4:00 AM - 12:00 PM', - }, - 'AFTERNOON': { - 'id': 'afternoon', - 'label': 'Afternoon', - 'timeRange': '12:00 PM - 6:00 PM', - }, - 'EVENING': { - 'id': 'evening', - 'label': 'Evening', - 'timeRange': '6:00 PM - 12:00 AM', - }, - }; - - /// Converts a backend slot name (e.g. 'MORNING') to a Domain [AvailabilitySlot]. - static AvailabilitySlot fromPrimitive(String slotName, {bool isAvailable = false}) { - final Map def = _slotDefinitions[slotName.toUpperCase()] ?? _slotDefinitions['MORNING']!; - return AvailabilitySlot( - id: def['id']!, - label: def['label']!, - timeRange: def['timeRange']!, - isAvailable: isAvailable, - ); - } -} diff --git a/apps/mobile/packages/domain/lib/src/adapters/clock_in/clock_in_adapter.dart b/apps/mobile/packages/domain/lib/src/adapters/clock_in/clock_in_adapter.dart deleted file mode 100644 index 049dd3cd..00000000 --- a/apps/mobile/packages/domain/lib/src/adapters/clock_in/clock_in_adapter.dart +++ /dev/null @@ -1,27 +0,0 @@ -import '../../entities/clock_in/attendance_status.dart'; - -/// Adapter for Clock In related data. -class ClockInAdapter { - - /// Converts primitive attendance data to [AttendanceStatus]. - static AttendanceStatus toAttendanceStatus({ - required String status, - DateTime? checkInTime, - DateTime? checkOutTime, - String? activeShiftId, - String? activeApplicationId, - }) { - final bool isCheckedIn = status == 'CHECKED_IN' || status == 'LATE'; // Assuming LATE is also checked in? - - // Statuses that imply active attendance: CHECKED_IN, LATE. - // Statuses that imply completed: CHECKED_OUT. - - return AttendanceStatus( - isCheckedIn: isCheckedIn, - checkInTime: checkInTime, - checkOutTime: checkOutTime, - activeShiftId: activeShiftId, - activeApplicationId: activeApplicationId, - ); - } -} diff --git a/apps/mobile/packages/domain/lib/src/adapters/financial/bank_account/bank_account_adapter.dart b/apps/mobile/packages/domain/lib/src/adapters/financial/bank_account/bank_account_adapter.dart deleted file mode 100644 index 167d1126..00000000 --- a/apps/mobile/packages/domain/lib/src/adapters/financial/bank_account/bank_account_adapter.dart +++ /dev/null @@ -1,21 +0,0 @@ -import '../../../entities/financial/bank_account/business_bank_account.dart'; - -/// Adapter for [BusinessBankAccount] to map data layer values to domain entity. -class BusinessBankAccountAdapter { - /// Maps primitive values to [BusinessBankAccount]. - static BusinessBankAccount fromPrimitives({ - required String id, - required String bank, - required String last4, - required bool isPrimary, - DateTime? expiryTime, - }) { - return BusinessBankAccount( - id: id, - bankName: bank, - last4: last4, - isPrimary: isPrimary, - expiryTime: expiryTime, - ); - } -} diff --git a/apps/mobile/packages/domain/lib/src/adapters/financial/payment_adapter.dart b/apps/mobile/packages/domain/lib/src/adapters/financial/payment_adapter.dart deleted file mode 100644 index 66446058..00000000 --- a/apps/mobile/packages/domain/lib/src/adapters/financial/payment_adapter.dart +++ /dev/null @@ -1,19 +0,0 @@ -import '../../entities/financial/staff_payment.dart'; - -/// Adapter for Payment related data. -class PaymentAdapter { - - /// Converts string status to [PaymentStatus]. - static PaymentStatus toPaymentStatus(String status) { - switch (status) { - case 'PAID': - return PaymentStatus.paid; - case 'PENDING': - return PaymentStatus.pending; - case 'FAILED': - return PaymentStatus.failed; - default: - return PaymentStatus.unknown; - } - } -} diff --git a/apps/mobile/packages/domain/lib/src/adapters/financial/time_card_adapter.dart b/apps/mobile/packages/domain/lib/src/adapters/financial/time_card_adapter.dart deleted file mode 100644 index 6c5033a7..00000000 --- a/apps/mobile/packages/domain/lib/src/adapters/financial/time_card_adapter.dart +++ /dev/null @@ -1,49 +0,0 @@ -import '../../entities/financial/time_card.dart'; - -/// Adapter for [TimeCard] to map data layer values to domain entity. -class TimeCardAdapter { - /// Maps primitive values to [TimeCard]. - static TimeCard fromPrimitives({ - required String id, - required String shiftTitle, - required String clientName, - required DateTime date, - required String startTime, - required String endTime, - required double totalHours, - required double hourlyRate, - required double totalPay, - required String status, - String? location, - }) { - return TimeCard( - id: id, - shiftTitle: shiftTitle, - clientName: clientName, - date: date, - startTime: startTime, - endTime: endTime, - totalHours: totalHours, - hourlyRate: hourlyRate, - totalPay: totalPay, - status: _stringToStatus(status), - location: location, - ); - } - - static TimeCardStatus _stringToStatus(String status) { - switch (status.toUpperCase()) { - case 'CHECKED_OUT': - case 'COMPLETED': - return TimeCardStatus.approved; // Assuming completed = approved for now - case 'PAID': - return TimeCardStatus.paid; // If this status exists - case 'DISPUTED': - return TimeCardStatus.disputed; - case 'CHECKED_IN': - case 'CONFIRMED': - default: - return TimeCardStatus.pending; - } - } -} diff --git a/apps/mobile/packages/domain/lib/src/adapters/profile/bank_account_adapter.dart b/apps/mobile/packages/domain/lib/src/adapters/profile/bank_account_adapter.dart deleted file mode 100644 index 133da163..00000000 --- a/apps/mobile/packages/domain/lib/src/adapters/profile/bank_account_adapter.dart +++ /dev/null @@ -1,53 +0,0 @@ -import '../../entities/financial/bank_account/staff_bank_account.dart'; - -/// Adapter for [StaffBankAccount] to map data layer values to domain entity. -class BankAccountAdapter { - /// Maps primitive values to [StaffBankAccount]. - static StaffBankAccount fromPrimitives({ - required String id, - required String userId, - required String bankName, - required String? type, - String? accountNumber, - String? last4, - String? sortCode, - bool? isPrimary, - }) { - return StaffBankAccount( - id: id, - userId: userId, - bankName: bankName, - accountNumber: accountNumber ?? '', - accountName: '', // Not provided by backend - last4: last4, - sortCode: sortCode, - type: _stringToType(type), - isPrimary: isPrimary ?? false, - ); - } - - static StaffBankAccountType _stringToType(String? value) { - if (value == null) return StaffBankAccountType.checking; - try { - // Assuming backend enum names match or are uppercase - return StaffBankAccountType.values.firstWhere( - (StaffBankAccountType e) => e.name.toLowerCase() == value.toLowerCase(), - orElse: () => StaffBankAccountType.other, - ); - } catch (_) { - return StaffBankAccountType.other; - } - } - - /// Converts domain type to string for backend. - static String typeToString(StaffBankAccountType type) { - switch (type) { - case StaffBankAccountType.checking: - return 'CHECKING'; - case StaffBankAccountType.savings: - return 'SAVINGS'; - default: - return 'CHECKING'; - } - } -} diff --git a/apps/mobile/packages/domain/lib/src/adapters/profile/emergency_contact_adapter.dart b/apps/mobile/packages/domain/lib/src/adapters/profile/emergency_contact_adapter.dart deleted file mode 100644 index 964f5735..00000000 --- a/apps/mobile/packages/domain/lib/src/adapters/profile/emergency_contact_adapter.dart +++ /dev/null @@ -1,19 +0,0 @@ -import '../../entities/profile/emergency_contact.dart'; - -/// Adapter for [EmergencyContact] to map data layer values to domain entity. -class EmergencyContactAdapter { - /// Maps primitive values to [EmergencyContact]. - static EmergencyContact fromPrimitives({ - required String id, - required String name, - required String phone, - String? relationship, - }) { - return EmergencyContact( - id: id, - name: name, - phone: phone, - relationship: EmergencyContact.stringToRelationshipType(relationship), - ); - } -} diff --git a/apps/mobile/packages/domain/lib/src/adapters/profile/experience_adapter.dart b/apps/mobile/packages/domain/lib/src/adapters/profile/experience_adapter.dart deleted file mode 100644 index 837f3ab1..00000000 --- a/apps/mobile/packages/domain/lib/src/adapters/profile/experience_adapter.dart +++ /dev/null @@ -1,18 +0,0 @@ -/// Adapter for Experience data (skills/industries) to map data layer values to domain models. -class ExperienceAdapter { - /// Converts a dynamic list (from backend AnyValue) to List. - /// - /// Handles nulls and converts elements to Strings. - static List fromDynamicList(dynamic data) { - if (data == null) return []; - - if (data is List) { - return data - .where((dynamic e) => e != null) - .map((dynamic e) => e.toString()) - .toList(); - } - - return []; - } -} diff --git a/apps/mobile/packages/domain/lib/src/adapters/profile/tax_form_adapter.dart b/apps/mobile/packages/domain/lib/src/adapters/profile/tax_form_adapter.dart deleted file mode 100644 index 8d4c8f6e..00000000 --- a/apps/mobile/packages/domain/lib/src/adapters/profile/tax_form_adapter.dart +++ /dev/null @@ -1,104 +0,0 @@ -import '../../entities/profile/tax_form.dart'; - -/// Adapter for [TaxForm] to map data layer values to domain entity. -class TaxFormAdapter { - /// Maps primitive values to [TaxForm]. - static TaxForm fromPrimitives({ - required String id, - required String type, - required String title, - String? subtitle, - String? description, - required String status, - String? staffId, - dynamic formData, - DateTime? createdAt, - DateTime? updatedAt, - }) { - final TaxFormType formType = _stringToType(type); - final TaxFormStatus formStatus = _stringToStatus(status); - final Map formDetails = - formData is Map ? Map.from(formData) : {}; - - if (formType == TaxFormType.i9) { - return I9TaxForm( - id: id, - title: title, - subtitle: subtitle, - description: description, - status: formStatus, - staffId: staffId, - formData: formDetails, - createdAt: createdAt, - updatedAt: updatedAt, - ); - } else { - return W4TaxForm( - id: id, - title: title, - subtitle: subtitle, - description: description, - status: formStatus, - staffId: staffId, - formData: formDetails, - createdAt: createdAt, - updatedAt: updatedAt, - ); - } - } - - static TaxFormType _stringToType(String? value) { - if (value == null) return TaxFormType.i9; - try { - return TaxFormType.values.firstWhere( - (TaxFormType e) => e.name.toLowerCase() == value.toLowerCase(), - orElse: () => TaxFormType.i9, - ); - } catch (_) { - return TaxFormType.i9; - } - } - - static TaxFormStatus _stringToStatus(String? value) { - if (value == null) return TaxFormStatus.notStarted; - try { - final String normalizedValue = value.replaceAll('_', '').toLowerCase(); - // map DRAFT to inProgress - if (normalizedValue == 'draft') return TaxFormStatus.inProgress; - - return TaxFormStatus.values.firstWhere( - (TaxFormStatus e) { - // Handle differences like not_started vs notStarted if any, - // but standardizing to lowercase is a good start. - // The enum names are camelCase in Dart, but might be SNAKE_CASE from backend. - final String normalizedEnum = e.name.toLowerCase(); - return normalizedValue == normalizedEnum; - }, - orElse: () => TaxFormStatus.notStarted, - ); - } catch (_) { - return TaxFormStatus.notStarted; - } - } - - /// Converts domain [TaxFormType] to string for backend. - static String typeToString(TaxFormType type) { - return type.name.toUpperCase(); - } - - /// Converts domain [TaxFormStatus] to string for backend. - static String statusToString(TaxFormStatus status) { - switch (status) { - case TaxFormStatus.notStarted: - return 'NOT_STARTED'; - case TaxFormStatus.inProgress: - return 'DRAFT'; - case TaxFormStatus.submitted: - return 'SUBMITTED'; - case TaxFormStatus.approved: - return 'APPROVED'; - case TaxFormStatus.rejected: - return 'REJECTED'; - } - } -} diff --git a/apps/mobile/packages/domain/lib/src/adapters/shifts/break/break_adapter.dart b/apps/mobile/packages/domain/lib/src/adapters/shifts/break/break_adapter.dart deleted file mode 100644 index 59f46949..00000000 --- a/apps/mobile/packages/domain/lib/src/adapters/shifts/break/break_adapter.dart +++ /dev/null @@ -1,39 +0,0 @@ -import '../../../entities/shifts/break/break.dart'; - -/// Adapter for Break related data. -class BreakAdapter { - /// Maps break data to a Break entity. - /// - /// [isPaid] whether the break is paid. - /// [breakTime] the string representation of the break duration (e.g., 'MIN_10', 'MIN_30'). - static Break fromData({ - required bool isPaid, - required String? breakTime, - }) { - return Break( - isBreakPaid: isPaid, - duration: _parseDuration(breakTime), - ); - } - - static BreakDuration _parseDuration(String? breakTime) { - if (breakTime == null) return BreakDuration.none; - - switch (breakTime.toUpperCase()) { - case 'MIN_10': - return BreakDuration.ten; - case 'MIN_15': - return BreakDuration.fifteen; - case 'MIN_20': - return BreakDuration.twenty; - case 'MIN_30': - return BreakDuration.thirty; - case 'MIN_45': - return BreakDuration.fortyFive; - case 'MIN_60': - return BreakDuration.sixty; - default: - return BreakDuration.none; - } - } -} diff --git a/apps/mobile/packages/domain/lib/src/adapters/shifts/shift_adapter.dart b/apps/mobile/packages/domain/lib/src/adapters/shifts/shift_adapter.dart deleted file mode 100644 index 6022d327..00000000 --- a/apps/mobile/packages/domain/lib/src/adapters/shifts/shift_adapter.dart +++ /dev/null @@ -1,59 +0,0 @@ -import 'package:intl/intl.dart'; -import '../../entities/shifts/shift.dart'; - -/// Adapter for Shift related data. -class ShiftAdapter { - /// Maps application data to a Shift entity. - /// - /// This method handles the common mapping logic used across different - /// repositories when converting application data from Data Connect to - /// domain Shift entities. - static Shift fromApplicationData({ - required String shiftId, - required String roleId, - required String roleName, - required String businessName, - String? companyLogoUrl, - required double costPerHour, - String? shiftLocation, - required String teamHubName, - DateTime? shiftDate, - DateTime? startTime, - DateTime? endTime, - DateTime? createdAt, - required String status, - String? description, - int? durationDays, - required int count, - int? assigned, - String? eventName, - bool hasApplied = false, - }) { - final String orderName = (eventName ?? '').trim().isNotEmpty - ? eventName! - : businessName; - final String title = '$roleName - $orderName'; - - return Shift( - id: shiftId, - roleId: roleId, - title: title, - clientName: businessName, - logoUrl: companyLogoUrl, - hourlyRate: costPerHour, - location: shiftLocation ?? '', - locationAddress: teamHubName, - date: shiftDate?.toIso8601String() ?? '', - startTime: startTime != null ? DateFormat('HH:mm').format(startTime) : '', - endTime: endTime != null ? DateFormat('HH:mm').format(endTime) : '', - createdDate: createdAt?.toIso8601String() ?? '', - status: status, - description: description, - durationDays: durationDays, - requiredSlots: count, - filledSlots: assigned ?? 0, - hasApplied: hasApplied, - ); - } -} - diff --git a/apps/mobile/packages/domain/lib/src/core/services/api_services/base_api_service.dart b/apps/mobile/packages/domain/lib/src/core/services/api_services/base_api_service.dart index ef9ccef6..a3dabfb0 100644 --- a/apps/mobile/packages/domain/lib/src/core/services/api_services/base_api_service.dart +++ b/apps/mobile/packages/domain/lib/src/core/services/api_services/base_api_service.dart @@ -27,4 +27,11 @@ abstract class BaseApiService { dynamic data, Map? params, }); + + /// Performs a DELETE request to the specified [endpoint]. + Future delete( + String endpoint, { + dynamic data, + Map? params, + }); } diff --git a/apps/mobile/packages/domain/lib/src/entities/availability/availability_day.dart b/apps/mobile/packages/domain/lib/src/entities/availability/availability_day.dart new file mode 100644 index 00000000..b7622698 --- /dev/null +++ b/apps/mobile/packages/domain/lib/src/entities/availability/availability_day.dart @@ -0,0 +1,86 @@ +import 'package:equatable/equatable.dart'; + +import 'package:krow_domain/src/entities/availability/time_slot.dart'; +import 'package:krow_domain/src/entities/enums/availability_status.dart'; + +/// Availability for a single calendar date. +/// +/// Returned by `GET /staff/availability`. The backend generates one entry +/// per date in the requested range by projecting the staff member's +/// recurring weekly availability pattern. +class AvailabilityDay extends Equatable { + /// Creates an [AvailabilityDay]. + const AvailabilityDay({ + required this.date, + required this.dayOfWeek, + required this.availabilityStatus, + this.slots = const [], + }); + + /// Deserialises from the V2 API JSON response. + factory AvailabilityDay.fromJson(Map json) { + final dynamic rawSlots = json['slots']; + final List parsedSlots = rawSlots is List + ? rawSlots + .map((dynamic e) => + TimeSlot.fromJson(e as Map)) + .toList() + : []; + + return AvailabilityDay( + date: json['date'] as String, + dayOfWeek: json['dayOfWeek'] as int, + availabilityStatus: + AvailabilityStatus.fromJson(json['availabilityStatus'] as String?), + slots: parsedSlots, + ); + } + + /// ISO date string (`YYYY-MM-DD`). + final String date; + + /// Day of week (0 = Sunday, 6 = Saturday). + final int dayOfWeek; + + /// Availability status for this day. + final AvailabilityStatus availabilityStatus; + + /// Time slots when the worker is available (relevant for `PARTIAL`). + final List slots; + + /// Whether the worker has any availability on this day. + bool get isAvailable => availabilityStatus != AvailabilityStatus.unavailable; + + /// Creates a copy with the given fields replaced. + AvailabilityDay copyWith({ + String? date, + int? dayOfWeek, + AvailabilityStatus? availabilityStatus, + List? slots, + }) { + return AvailabilityDay( + date: date ?? this.date, + dayOfWeek: dayOfWeek ?? this.dayOfWeek, + availabilityStatus: availabilityStatus ?? this.availabilityStatus, + slots: slots ?? this.slots, + ); + } + + /// Serialises to JSON. + Map toJson() { + return { + 'date': date, + 'dayOfWeek': dayOfWeek, + 'availabilityStatus': availabilityStatus.toJson(), + 'slots': slots.map((TimeSlot s) => s.toJson()).toList(), + }; + } + + @override + List get props => [ + date, + dayOfWeek, + availabilityStatus, + slots, + ]; +} diff --git a/apps/mobile/packages/domain/lib/src/entities/availability/availability_slot.dart b/apps/mobile/packages/domain/lib/src/entities/availability/availability_slot.dart deleted file mode 100644 index b0085bed..00000000 --- a/apps/mobile/packages/domain/lib/src/entities/availability/availability_slot.dart +++ /dev/null @@ -1,33 +0,0 @@ -import 'package:equatable/equatable.dart'; - -/// Represents a specific time slot within a day (e.g., Morning, Afternoon, Evening). -class AvailabilitySlot extends Equatable { - - const AvailabilitySlot({ - required this.id, - required this.label, - required this.timeRange, - this.isAvailable = true, - }); - final String id; - final String label; - final String timeRange; - final bool isAvailable; - - AvailabilitySlot copyWith({ - String? id, - String? label, - String? timeRange, - bool? isAvailable, - }) { - return AvailabilitySlot( - id: id ?? this.id, - label: label ?? this.label, - timeRange: timeRange ?? this.timeRange, - isAvailable: isAvailable ?? this.isAvailable, - ); - } - - @override - List get props => [id, label, timeRange, isAvailable]; -} diff --git a/apps/mobile/packages/domain/lib/src/entities/availability/day_availability.dart b/apps/mobile/packages/domain/lib/src/entities/availability/day_availability.dart deleted file mode 100644 index ee285830..00000000 --- a/apps/mobile/packages/domain/lib/src/entities/availability/day_availability.dart +++ /dev/null @@ -1,31 +0,0 @@ -import 'package:equatable/equatable.dart'; - -import 'availability_slot.dart'; - -/// Represents availability configuration for a specific date. -class DayAvailability extends Equatable { - - const DayAvailability({ - required this.date, - this.isAvailable = false, - this.slots = const [], - }); - final DateTime date; - final bool isAvailable; - final List slots; - - DayAvailability copyWith({ - DateTime? date, - bool? isAvailable, - List? slots, - }) { - return DayAvailability( - date: date ?? this.date, - isAvailable: isAvailable ?? this.isAvailable, - slots: slots ?? this.slots, - ); - } - - @override - List get props => [date, isAvailable, slots]; -} diff --git a/apps/mobile/packages/domain/lib/src/entities/availability/time_slot.dart b/apps/mobile/packages/domain/lib/src/entities/availability/time_slot.dart new file mode 100644 index 00000000..8bfcf812 --- /dev/null +++ b/apps/mobile/packages/domain/lib/src/entities/availability/time_slot.dart @@ -0,0 +1,39 @@ +import 'package:equatable/equatable.dart'; + +/// A time range within a day of availability. +/// +/// Embedded inside [AvailabilityDay.slots]. Times are stored as `HH:MM` +/// strings because the backend stores them in a JSONB array and they +/// are timezone-agnostic display values. +class TimeSlot extends Equatable { + /// Creates a [TimeSlot]. + const TimeSlot({ + required this.startTime, + required this.endTime, + }); + + /// Deserialises from a JSON map inside the availability slots array. + factory TimeSlot.fromJson(Map json) { + return TimeSlot( + startTime: json['startTime'] as String? ?? '00:00', + endTime: json['endTime'] as String? ?? '00:00', + ); + } + + /// Start time in `HH:MM` format. + final String startTime; + + /// End time in `HH:MM` format. + final String endTime; + + /// Serialises to JSON. + Map toJson() { + return { + 'startTime': startTime, + 'endTime': endTime, + }; + } + + @override + List get props => [startTime, endTime]; +} diff --git a/apps/mobile/packages/domain/lib/src/entities/benefits/benefit.dart b/apps/mobile/packages/domain/lib/src/entities/benefits/benefit.dart index 15bb6f99..59ea40a7 100644 --- a/apps/mobile/packages/domain/lib/src/entities/benefits/benefit.dart +++ b/apps/mobile/packages/domain/lib/src/entities/benefits/benefit.dart @@ -1,26 +1,73 @@ import 'package:equatable/equatable.dart'; -/// Represents a staff member's benefit balance. +import 'package:krow_domain/src/entities/enums/benefit_status.dart'; + +/// A benefit accrued by a staff member (e.g. sick leave, vacation). +/// +/// Returned by `GET /staff/profile/benefits`. class Benefit extends Equatable { - /// Creates a [Benefit]. + /// Creates a [Benefit] instance. const Benefit({ + required this.benefitId, + required this.benefitType, required this.title, - required this.entitlementHours, - required this.usedHours, + required this.status, + required this.trackedHours, + required this.targetHours, }); - /// The title of the benefit (e.g., Sick Leave, Holiday, Vacation). + /// Deserialises a [Benefit] from a V2 API JSON map. + factory Benefit.fromJson(Map json) { + return Benefit( + benefitId: json['benefitId'] as String, + benefitType: json['benefitType'] as String, + title: json['title'] as String, + status: BenefitStatus.fromJson(json['status'] as String?), + trackedHours: (json['trackedHours'] as num).toInt(), + targetHours: (json['targetHours'] as num).toInt(), + ); + } + + /// Unique identifier. + final String benefitId; + + /// Type code (e.g. SICK_LEAVE, VACATION). + final String benefitType; + + /// Human-readable title. final String title; - /// The total entitlement in hours. - final double entitlementHours; + /// Current benefit status. + final BenefitStatus status; - /// The hours used so far. - final double usedHours; + /// Hours tracked so far. + final int trackedHours; - /// The hours remaining. - double get remainingHours => entitlementHours - usedHours; + /// Target hours to accrue. + final int targetHours; + + /// Remaining hours to reach the target. + int get remainingHours => targetHours - trackedHours; + + /// Serialises this [Benefit] to a JSON map. + Map toJson() { + return { + 'benefitId': benefitId, + 'benefitType': benefitType, + 'title': title, + 'status': status.toJson(), + 'trackedHours': trackedHours, + 'targetHours': targetHours, + }; + } @override - List get props => [title, entitlementHours, usedHours]; + List get props => [ + benefitId, + benefitType, + title, + status, + trackedHours, + targetHours, + ]; } diff --git a/apps/mobile/packages/domain/lib/src/entities/business/biz_contract.dart b/apps/mobile/packages/domain/lib/src/entities/business/biz_contract.dart deleted file mode 100644 index 196c9eb8..00000000 --- a/apps/mobile/packages/domain/lib/src/entities/business/biz_contract.dart +++ /dev/null @@ -1,36 +0,0 @@ -import 'package:equatable/equatable.dart'; - -/// Represents a legal or service contract. -/// -/// Can be between a business and the platform, or a business and staff. -class BizContract extends Equatable { - - const BizContract({ - required this.id, - required this.businessId, - required this.name, - required this.startDate, - this.endDate, - required this.contentUrl, - }); - /// Unique identifier. - final String id; - - /// The [Business] party to the contract. - final String businessId; - - /// Descriptive name of the contract. - final String name; - - /// Valid from date. - final DateTime startDate; - - /// Valid until date (null if indefinite). - final DateTime? endDate; - - /// URL to the document content (PDF/HTML). - final String contentUrl; - - @override - List get props => [id, businessId, name, startDate, endDate, contentUrl]; -} \ No newline at end of file diff --git a/apps/mobile/packages/domain/lib/src/entities/business/business.dart b/apps/mobile/packages/domain/lib/src/entities/business/business.dart index c03e75c9..36339f32 100644 --- a/apps/mobile/packages/domain/lib/src/entities/business/business.dart +++ b/apps/mobile/packages/domain/lib/src/entities/business/business.dart @@ -1,47 +1,111 @@ import 'package:equatable/equatable.dart'; -/// The operating status of a [Business]. -enum BusinessStatus { - /// Business created but not yet approved. - pending, +import 'package:krow_domain/src/entities/enums/business_status.dart'; - /// Fully active and operational. - active, - - /// Temporarily suspended (e.g. for non-payment). - suspended, - - /// Permanently inactive. - inactive, -} - -/// Represents a Client Company / Business. +/// A client company registered on the platform. /// -/// This is the top-level organizational entity in the system. +/// Maps to the V2 `businesses` table. class Business extends Equatable { - + /// Creates a [Business] instance. const Business({ required this.id, - required this.name, - required this.registrationNumber, + required this.tenantId, + required this.slug, + required this.businessName, required this.status, - this.avatar, + this.contactName, + this.contactEmail, + this.contactPhone, + this.metadata = const {}, + this.createdAt, + this.updatedAt, }); - /// Unique identifier for the business. + + /// Deserialises a [Business] from a V2 API JSON map. + factory Business.fromJson(Map json) { + return Business( + id: json['id'] as String, + tenantId: json['tenantId'] as String, + slug: json['slug'] as String, + businessName: json['businessName'] as String, + status: BusinessStatus.fromJson(json['status'] as String?), + contactName: json['contactName'] as String?, + contactEmail: json['contactEmail'] as String?, + contactPhone: json['contactPhone'] as String?, + metadata: json['metadata'] is Map + ? Map.from(json['metadata'] as Map) + : const {}, + createdAt: json['createdAt'] != null + ? DateTime.parse(json['createdAt'] as String) + : null, + updatedAt: json['updatedAt'] != null + ? DateTime.parse(json['updatedAt'] as String) + : null, + ); + } + + /// Unique identifier. final String id; + /// Tenant this business belongs to. + final String tenantId; + + /// URL-safe slug. + final String slug; + /// Display name of the business. - final String name; + final String businessName; - /// Legal registration or tax number. - final String registrationNumber; - - /// Current operating status. + /// Current account status. final BusinessStatus status; - /// URL to the business logo. - final String? avatar; + /// Primary contact name. + final String? contactName; + + /// Primary contact email. + final String? contactEmail; + + /// Primary contact phone. + final String? contactPhone; + + /// Flexible metadata bag. + final Map metadata; + + /// When the record was created. + final DateTime? createdAt; + + /// When the record was last updated. + final DateTime? updatedAt; + + /// Serialises this [Business] to a JSON map. + Map toJson() { + return { + 'id': id, + 'tenantId': tenantId, + 'slug': slug, + 'businessName': businessName, + 'status': status.toJson(), + 'contactName': contactName, + 'contactEmail': contactEmail, + 'contactPhone': contactPhone, + 'metadata': metadata, + 'createdAt': createdAt?.toIso8601String(), + 'updatedAt': updatedAt?.toIso8601String(), + }; + } @override - List get props => [id, name, registrationNumber, status, avatar]; -} \ No newline at end of file + List get props => [ + id, + tenantId, + slug, + businessName, + status, + contactName, + contactEmail, + contactPhone, + metadata, + createdAt, + updatedAt, + ]; +} diff --git a/apps/mobile/packages/domain/lib/src/entities/business/business_setting.dart b/apps/mobile/packages/domain/lib/src/entities/business/business_setting.dart deleted file mode 100644 index 328cb39c..00000000 --- a/apps/mobile/packages/domain/lib/src/entities/business/business_setting.dart +++ /dev/null @@ -1,41 +0,0 @@ -import 'package:equatable/equatable.dart'; - -/// Represents payroll and operational configuration for a [Business]. -class BusinessSetting extends Equatable { - - const BusinessSetting({ - required this.id, - required this.businessId, - required this.prefix, - required this.overtimeEnabled, - this.clockInRequirement, - this.clockOutRequirement, - }); - /// Unique identifier for the settings record. - final String id; - - /// The [Business] these settings apply to. - final String businessId; - - /// Prefix for generated invoices (e.g., "INV-"). - final String prefix; - - /// Whether overtime calculations are applied. - final bool overtimeEnabled; - - /// Requirement method for clocking in (e.g. "qr_code", "geo_fence"). - final String? clockInRequirement; - - /// Requirement method for clocking out. - final String? clockOutRequirement; - - @override - List get props => [ - id, - businessId, - prefix, - overtimeEnabled, - clockInRequirement, - clockOutRequirement, - ]; -} \ No newline at end of file diff --git a/apps/mobile/packages/domain/lib/src/entities/business/cost_center.dart b/apps/mobile/packages/domain/lib/src/entities/business/cost_center.dart index 8d3d5528..33ad6b2e 100644 --- a/apps/mobile/packages/domain/lib/src/entities/business/cost_center.dart +++ b/apps/mobile/packages/domain/lib/src/entities/business/cost_center.dart @@ -1,22 +1,37 @@ import 'package:equatable/equatable.dart'; -/// Represents a financial cost center used for billing and tracking. +/// A financial cost center used for billing and tracking. +/// +/// Returned by `GET /client/cost-centers`. class CostCenter extends Equatable { + /// Creates a [CostCenter] instance. const CostCenter({ - required this.id, + required this.costCenterId, required this.name, - this.code, }); - /// Unique identifier. - final String id; + /// Deserialises a [CostCenter] from a V2 API JSON map. + factory CostCenter.fromJson(Map json) { + return CostCenter( + costCenterId: json['costCenterId'] as String, + name: json['name'] as String, + ); + } - /// Display name of the cost center. + /// Unique identifier. + final String costCenterId; + + /// Display name. final String name; - /// Optional alphanumeric code associated with this cost center. - final String? code; + /// Serialises this [CostCenter] to a JSON map. + Map toJson() { + return { + 'costCenterId': costCenterId, + 'name': name, + }; + } @override - List get props => [id, name, code]; + List get props => [costCenterId, name]; } diff --git a/apps/mobile/packages/domain/lib/src/entities/business/hub.dart b/apps/mobile/packages/domain/lib/src/entities/business/hub.dart index 79c06572..5c2212e0 100644 --- a/apps/mobile/packages/domain/lib/src/entities/business/hub.dart +++ b/apps/mobile/packages/domain/lib/src/entities/business/hub.dart @@ -1,51 +1,107 @@ import 'package:equatable/equatable.dart'; -import 'cost_center.dart'; - -/// The status of a [Hub]. -enum HubStatus { - /// Fully operational. - active, - - /// Closed or inactive. - inactive, - - /// Not yet ready for operations. - underConstruction, -} - -/// Represents a branch location or operational unit within a [Business]. +/// A physical clock-point location (hub) belonging to a business. +/// +/// Maps to the V2 `clock_points` table; returned by `GET /client/hubs`. class Hub extends Equatable { + /// Creates a [Hub] instance. const Hub({ - required this.id, - required this.businessId, + required this.hubId, required this.name, - required this.address, + this.fullAddress, + this.latitude, + this.longitude, this.nfcTagId, - required this.status, - this.costCenter, + this.city, + this.state, + this.zipCode, + this.costCenterId, + this.costCenterName, }); - /// Unique identifier. - final String id; - /// The parent [Business]. - final String businessId; + /// Deserialises a [Hub] from a V2 API JSON map. + factory Hub.fromJson(Map json) { + return Hub( + hubId: json['hubId'] as String, + name: json['name'] as String, + fullAddress: json['fullAddress'] as String?, + latitude: json['latitude'] != null + ? double.parse(json['latitude'].toString()) + : null, + longitude: json['longitude'] != null + ? double.parse(json['longitude'].toString()) + : null, + nfcTagId: json['nfcTagId'] as String?, + city: json['city'] as String?, + state: json['state'] as String?, + zipCode: json['zipCode'] as String?, + costCenterId: json['costCenterId'] as String?, + costCenterName: json['costCenterName'] as String?, + ); + } - /// Display name of the hub (e.g. "Downtown Branch"). + /// Unique identifier (clock_point id). + final String hubId; + + /// Display label for the hub. final String name; - /// Physical address of this hub. - final String address; + /// Full street address. + final String? fullAddress; - /// Unique identifier of the NFC tag assigned to this hub. + /// GPS latitude. + final double? latitude; + + /// GPS longitude. + final double? longitude; + + /// NFC tag UID assigned to this hub. final String? nfcTagId; - /// Operational status. - final HubStatus status; + /// City from metadata. + final String? city; - /// Assigned cost center for this hub. - final CostCenter? costCenter; + /// State from metadata. + final String? state; + + /// Zip code from metadata. + final String? zipCode; + + /// Associated cost center ID. + final String? costCenterId; + + /// Associated cost center name. + final String? costCenterName; + + /// Serialises this [Hub] to a JSON map. + Map toJson() { + return { + 'hubId': hubId, + 'name': name, + 'fullAddress': fullAddress, + 'latitude': latitude, + 'longitude': longitude, + 'nfcTagId': nfcTagId, + 'city': city, + 'state': state, + 'zipCode': zipCode, + 'costCenterId': costCenterId, + 'costCenterName': costCenterName, + }; + } @override - List get props => [id, businessId, name, address, nfcTagId, status, costCenter]; + List get props => [ + hubId, + name, + fullAddress, + latitude, + longitude, + nfcTagId, + city, + state, + zipCode, + costCenterId, + costCenterName, + ]; } diff --git a/apps/mobile/packages/domain/lib/src/entities/business/hub_department.dart b/apps/mobile/packages/domain/lib/src/entities/business/hub_department.dart deleted file mode 100644 index 3c6891bc..00000000 --- a/apps/mobile/packages/domain/lib/src/entities/business/hub_department.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:equatable/equatable.dart'; - -/// Represents a department within a [Hub]. -/// -/// Used for more granular organization of staff and events (e.g. "Kitchen", "Service"). -class HubDepartment extends Equatable { - - const HubDepartment({ - required this.id, - required this.hubId, - required this.name, - }); - /// Unique identifier. - final String id; - - /// The [Hub] this department belongs to. - final String hubId; - - /// Name of the department. - final String name; - - @override - List get props => [id, hubId, name]; -} \ No newline at end of file diff --git a/apps/mobile/packages/domain/lib/src/entities/business/hub_manager.dart b/apps/mobile/packages/domain/lib/src/entities/business/hub_manager.dart new file mode 100644 index 00000000..d86f6cff --- /dev/null +++ b/apps/mobile/packages/domain/lib/src/entities/business/hub_manager.dart @@ -0,0 +1,54 @@ +import 'package:equatable/equatable.dart'; + +/// A manager assigned to a hub (clock point). +/// +/// Returned by `GET /client/hubs/:id/managers`. +class HubManager extends Equatable { + /// Creates a [HubManager] instance. + const HubManager({ + required this.managerAssignmentId, + required this.businessMembershipId, + required this.managerId, + required this.name, + }); + + /// Deserialises a [HubManager] from a V2 API JSON map. + factory HubManager.fromJson(Map json) { + return HubManager( + managerAssignmentId: json['managerAssignmentId'] as String, + businessMembershipId: json['businessMembershipId'] as String, + managerId: json['managerId'] as String, + name: json['name'] as String, + ); + } + + /// Primary key of the hub_managers row. + final String managerAssignmentId; + + /// Business membership ID of the manager. + final String businessMembershipId; + + /// User ID of the manager. + final String managerId; + + /// Display name of the manager. + final String name; + + /// Serialises this [HubManager] to a JSON map. + Map toJson() { + return { + 'managerAssignmentId': managerAssignmentId, + 'businessMembershipId': businessMembershipId, + 'managerId': managerId, + 'name': name, + }; + } + + @override + List get props => [ + managerAssignmentId, + businessMembershipId, + managerId, + name, + ]; +} diff --git a/apps/mobile/packages/domain/lib/src/entities/business/team_member.dart b/apps/mobile/packages/domain/lib/src/entities/business/team_member.dart new file mode 100644 index 00000000..db0c0d0f --- /dev/null +++ b/apps/mobile/packages/domain/lib/src/entities/business/team_member.dart @@ -0,0 +1,61 @@ +import 'package:equatable/equatable.dart'; + +/// A member of a business team (business membership + user). +/// +/// Returned by `GET /client/team-members`. +class TeamMember extends Equatable { + /// Creates a [TeamMember] instance. + const TeamMember({ + required this.businessMembershipId, + required this.userId, + required this.name, + this.email, + this.role, + }); + + /// Deserialises a [TeamMember] from a V2 API JSON map. + factory TeamMember.fromJson(Map json) { + return TeamMember( + businessMembershipId: json['businessMembershipId'] as String, + userId: json['userId'] as String, + name: json['name'] as String, + email: json['email'] as String?, + role: json['role'] as String?, + ); + } + + /// Business membership primary key. + final String businessMembershipId; + + /// User ID. + final String userId; + + /// Display name. + final String name; + + /// Email address. + final String? email; + + /// Business role (owner, manager, member, viewer). + final String? role; + + /// Serialises this [TeamMember] to a JSON map. + Map toJson() { + return { + 'businessMembershipId': businessMembershipId, + 'userId': userId, + 'name': name, + 'email': email, + 'role': role, + }; + } + + @override + List get props => [ + businessMembershipId, + userId, + name, + email, + role, + ]; +} diff --git a/apps/mobile/packages/domain/lib/src/entities/business/vendor.dart b/apps/mobile/packages/domain/lib/src/entities/business/vendor.dart index 19d8bf98..8397933e 100644 --- a/apps/mobile/packages/domain/lib/src/entities/business/vendor.dart +++ b/apps/mobile/packages/domain/lib/src/entities/business/vendor.dart @@ -1,15 +1,86 @@ import 'package:equatable/equatable.dart'; -/// Represents a staffing vendor. +import 'package:krow_domain/src/entities/enums/business_status.dart'; + +/// A staffing vendor that supplies workers to businesses. +/// +/// Maps to the V2 `vendors` table. class Vendor extends Equatable { - const Vendor({required this.id, required this.name, required this.rates}); + /// Creates a [Vendor] instance. + const Vendor({ + required this.id, + required this.tenantId, + required this.slug, + required this.companyName, + required this.status, + this.contactName, + this.contactEmail, + this.contactPhone, + }); + /// Deserialises a [Vendor] from a V2 API JSON map. + factory Vendor.fromJson(Map json) { + return Vendor( + id: json['id'] as String? ?? json['vendorId'] as String, + tenantId: json['tenantId'] as String? ?? '', + slug: json['slug'] as String? ?? '', + companyName: json['companyName'] as String? ?? + json['vendorName'] as String? ?? + '', + status: BusinessStatus.fromJson(json['status'] as String?), + contactName: json['contactName'] as String?, + contactEmail: json['contactEmail'] as String?, + contactPhone: json['contactPhone'] as String?, + ); + } + + /// Unique identifier. final String id; - final String name; - /// A map of role names to hourly rates. - final Map rates; + /// Tenant this vendor belongs to. + final String tenantId; + + /// URL-safe slug. + final String slug; + + /// Display name of the vendor company. + final String companyName; + + /// Current account status. + final BusinessStatus status; + + /// Primary contact name. + final String? contactName; + + /// Primary contact email. + final String? contactEmail; + + /// Primary contact phone. + final String? contactPhone; + + /// Serialises this [Vendor] to a JSON map. + Map toJson() { + return { + 'id': id, + 'tenantId': tenantId, + 'slug': slug, + 'companyName': companyName, + 'status': status.toJson(), + 'contactName': contactName, + 'contactEmail': contactEmail, + 'contactPhone': contactPhone, + }; + } @override - List get props => [id, name, rates]; + List get props => [ + id, + tenantId, + slug, + companyName, + status, + contactName, + contactEmail, + contactPhone, + ]; } diff --git a/apps/mobile/packages/domain/lib/src/entities/business/vendor_role.dart b/apps/mobile/packages/domain/lib/src/entities/business/vendor_role.dart new file mode 100644 index 00000000..f2b1c07c --- /dev/null +++ b/apps/mobile/packages/domain/lib/src/entities/business/vendor_role.dart @@ -0,0 +1,49 @@ +import 'package:equatable/equatable.dart'; + +/// A role available through a vendor with its billing rate. +/// +/// Returned by `GET /client/vendors/:id/roles`. +class VendorRole extends Equatable { + /// Creates a [VendorRole] instance. + const VendorRole({ + required this.roleId, + required this.roleCode, + required this.roleName, + required this.hourlyRateCents, + }); + + /// Deserialises a [VendorRole] from a V2 API JSON map. + factory VendorRole.fromJson(Map json) { + return VendorRole( + roleId: json['roleId'] as String, + roleCode: json['roleCode'] as String, + roleName: json['roleName'] as String, + hourlyRateCents: (json['hourlyRateCents'] as num).toInt(), + ); + } + + /// Unique identifier from the roles catalog. + final String roleId; + + /// Short code for the role (e.g. BARISTA). + final String roleCode; + + /// Human-readable role name. + final String roleName; + + /// Billing rate in cents per hour. + final int hourlyRateCents; + + /// Serialises this [VendorRole] to a JSON map. + Map toJson() { + return { + 'roleId': roleId, + 'roleCode': roleCode, + 'roleName': roleName, + 'hourlyRateCents': hourlyRateCents, + }; + } + + @override + List get props => [roleId, roleCode, roleName, hourlyRateCents]; +} diff --git a/apps/mobile/packages/domain/lib/src/entities/clock_in/attendance_status.dart b/apps/mobile/packages/domain/lib/src/entities/clock_in/attendance_status.dart index 3d6bc3e1..043179d7 100644 --- a/apps/mobile/packages/domain/lib/src/entities/clock_in/attendance_status.dart +++ b/apps/mobile/packages/domain/lib/src/entities/clock_in/attendance_status.dart @@ -1,27 +1,56 @@ import 'package:equatable/equatable.dart'; -/// Simple entity to hold attendance state -class AttendanceStatus extends Equatable { +import 'package:krow_domain/src/entities/enums/attendance_status_type.dart'; +/// Current clock-in / attendance status of the staff member. +/// +/// Returned by `GET /staff/clock-in/status`. When no open session exists +/// the API returns `attendanceStatus: 'NOT_CLOCKED_IN'` with null IDs. +class AttendanceStatus extends Equatable { + /// Creates an [AttendanceStatus]. const AttendanceStatus({ - this.isCheckedIn = false, - this.checkInTime, - this.checkOutTime, this.activeShiftId, - this.activeApplicationId, + required this.attendanceStatus, + this.clockInAt, }); - final bool isCheckedIn; - final DateTime? checkInTime; - final DateTime? checkOutTime; + + /// Deserialises from the V2 API JSON response. + factory AttendanceStatus.fromJson(Map json) { + return AttendanceStatus( + activeShiftId: json['activeShiftId'] as String?, + attendanceStatus: + AttendanceStatusType.fromJson(json['attendanceStatus'] as String?), + clockInAt: json['clockInAt'] != null + ? DateTime.parse(json['clockInAt'] as String) + : null, + ); + } + + /// The shift id of the currently active attendance session, if any. final String? activeShiftId; - final String? activeApplicationId; + + /// Attendance session status. + final AttendanceStatusType attendanceStatus; + + /// Timestamp of clock-in, if currently clocked in. + final DateTime? clockInAt; + + /// Whether the worker is currently clocked in. + bool get isClockedIn => attendanceStatus == AttendanceStatusType.open; + + /// Serialises to JSON. + Map toJson() { + return { + 'activeShiftId': activeShiftId, + 'attendanceStatus': attendanceStatus.toJson(), + 'clockInAt': clockInAt?.toIso8601String(), + }; + } @override List get props => [ - isCheckedIn, - checkInTime, - checkOutTime, activeShiftId, - activeApplicationId, + attendanceStatus, + clockInAt, ]; } diff --git a/apps/mobile/packages/domain/lib/src/entities/coverage_domain/assigned_worker.dart b/apps/mobile/packages/domain/lib/src/entities/coverage_domain/assigned_worker.dart new file mode 100644 index 00000000..88ee6ebc --- /dev/null +++ b/apps/mobile/packages/domain/lib/src/entities/coverage_domain/assigned_worker.dart @@ -0,0 +1,65 @@ +import 'package:equatable/equatable.dart'; + +import 'package:krow_domain/src/entities/enums/assignment_status.dart'; + +/// A worker assigned to a coverage shift. +/// +/// Nested within [ShiftWithWorkers]. +class AssignedWorker extends Equatable { + /// Creates an [AssignedWorker] instance. + const AssignedWorker({ + required this.assignmentId, + required this.staffId, + required this.fullName, + required this.status, + this.checkInAt, + }); + + /// Deserialises an [AssignedWorker] from a V2 API JSON map. + factory AssignedWorker.fromJson(Map json) { + return AssignedWorker( + assignmentId: json['assignmentId'] as String, + staffId: json['staffId'] as String, + fullName: json['fullName'] as String, + status: AssignmentStatus.fromJson(json['status'] as String?), + checkInAt: json['checkInAt'] != null + ? DateTime.parse(json['checkInAt'] as String) + : null, + ); + } + + /// Assignment ID. + final String assignmentId; + + /// Staff member ID. + final String staffId; + + /// Worker display name. + final String fullName; + + /// Assignment status. + final AssignmentStatus status; + + /// When the worker clocked in (null if not yet). + final DateTime? checkInAt; + + /// Serialises this [AssignedWorker] to a JSON map. + Map toJson() { + return { + 'assignmentId': assignmentId, + 'staffId': staffId, + 'fullName': fullName, + 'status': status.toJson(), + 'checkInAt': checkInAt?.toIso8601String(), + }; + } + + @override + List get props => [ + assignmentId, + staffId, + fullName, + status, + checkInAt, + ]; +} diff --git a/apps/mobile/packages/domain/lib/src/entities/coverage_domain/core_team_member.dart b/apps/mobile/packages/domain/lib/src/entities/coverage_domain/core_team_member.dart new file mode 100644 index 00000000..760746e7 --- /dev/null +++ b/apps/mobile/packages/domain/lib/src/entities/coverage_domain/core_team_member.dart @@ -0,0 +1,68 @@ +import 'package:equatable/equatable.dart'; + +/// A staff member on the business's core team (favorites). +/// +/// Returned by `GET /client/coverage/core-team`. +class CoreTeamMember extends Equatable { + /// Creates a [CoreTeamMember] instance. + const CoreTeamMember({ + required this.staffId, + required this.fullName, + this.primaryRole, + required this.averageRating, + required this.ratingCount, + required this.favorite, + }); + + /// Deserialises a [CoreTeamMember] from a V2 API JSON map. + factory CoreTeamMember.fromJson(Map json) { + return CoreTeamMember( + staffId: json['staffId'] as String, + fullName: json['fullName'] as String, + primaryRole: json['primaryRole'] as String?, + averageRating: (json['averageRating'] as num).toDouble(), + ratingCount: (json['ratingCount'] as num).toInt(), + favorite: json['favorite'] as bool? ?? true, + ); + } + + /// Staff member ID. + final String staffId; + + /// Display name. + final String fullName; + + /// Primary role code. + final String? primaryRole; + + /// Average review rating (0-5). + final double averageRating; + + /// Total number of reviews. + final int ratingCount; + + /// Whether this staff is favorited by the business. + final bool favorite; + + /// Serialises this [CoreTeamMember] to a JSON map. + Map toJson() { + return { + 'staffId': staffId, + 'fullName': fullName, + 'primaryRole': primaryRole, + 'averageRating': averageRating, + 'ratingCount': ratingCount, + 'favorite': favorite, + }; + } + + @override + List get props => [ + staffId, + fullName, + primaryRole, + averageRating, + ratingCount, + favorite, + ]; +} diff --git a/apps/mobile/packages/domain/lib/src/entities/coverage_domain/coverage_shift.dart b/apps/mobile/packages/domain/lib/src/entities/coverage_domain/coverage_shift.dart deleted file mode 100644 index afc10d60..00000000 --- a/apps/mobile/packages/domain/lib/src/entities/coverage_domain/coverage_shift.dart +++ /dev/null @@ -1,57 +0,0 @@ -import 'package:equatable/equatable.dart'; -import 'coverage_worker.dart'; - -/// Domain entity representing a shift in the coverage view. -/// -/// This is a feature-specific domain entity that encapsulates shift information -/// including scheduling details and assigned workers. -class CoverageShift extends Equatable { - /// Creates a [CoverageShift]. - const CoverageShift({ - required this.id, - required this.title, - required this.location, - required this.startTime, - required this.workersNeeded, - required this.date, - required this.workers, - }); - - /// The unique identifier for the shift. - final String id; - - /// The title or role of the shift. - final String title; - - /// The location where the shift takes place. - final String location; - - /// The start time of the shift (e.g., "16:00"). - final String startTime; - - /// The number of workers needed for this shift. - final int workersNeeded; - - /// The date of the shift. - final DateTime date; - - /// The list of workers assigned to this shift. - final List workers; - - /// Calculates the coverage percentage for this shift. - int get coveragePercent { - if (workersNeeded == 0) return 0; - return ((workers.length / workersNeeded) * 100).round(); - } - - @override - List get props => [ - id, - title, - location, - startTime, - workersNeeded, - date, - workers, - ]; -} diff --git a/apps/mobile/packages/domain/lib/src/entities/coverage_domain/coverage_stats.dart b/apps/mobile/packages/domain/lib/src/entities/coverage_domain/coverage_stats.dart index 580116a9..02e8cf60 100644 --- a/apps/mobile/packages/domain/lib/src/entities/coverage_domain/coverage_stats.dart +++ b/apps/mobile/packages/domain/lib/src/entities/coverage_domain/coverage_stats.dart @@ -1,45 +1,69 @@ import 'package:equatable/equatable.dart'; -/// Domain entity representing coverage statistics. +/// Aggregated coverage statistics for a specific date. /// -/// Aggregates coverage metrics for a specific date. +/// Returned by `GET /client/coverage/stats`. class CoverageStats extends Equatable { - /// Creates a [CoverageStats]. + /// Creates a [CoverageStats] instance. const CoverageStats({ - required this.totalNeeded, - required this.totalConfirmed, - required this.checkedIn, - required this.enRoute, - required this.late, + required this.totalPositionsNeeded, + required this.totalPositionsConfirmed, + required this.totalWorkersCheckedIn, + required this.totalWorkersEnRoute, + required this.totalWorkersLate, + required this.totalCoveragePercentage, }); - /// The total number of workers needed. - final int totalNeeded; + /// Deserialises a [CoverageStats] from a V2 API JSON map. + factory CoverageStats.fromJson(Map json) { + return CoverageStats( + totalPositionsNeeded: (json['totalPositionsNeeded'] as num).toInt(), + totalPositionsConfirmed: (json['totalPositionsConfirmed'] as num).toInt(), + totalWorkersCheckedIn: (json['totalWorkersCheckedIn'] as num).toInt(), + totalWorkersEnRoute: (json['totalWorkersEnRoute'] as num).toInt(), + totalWorkersLate: (json['totalWorkersLate'] as num).toInt(), + totalCoveragePercentage: + (json['totalCoveragePercentage'] as num).toInt(), + ); + } - /// The total number of confirmed workers. - final int totalConfirmed; + /// Total positions that need to be filled. + final int totalPositionsNeeded; - /// The number of workers who have checked in. - final int checkedIn; + /// Total positions that have been confirmed. + final int totalPositionsConfirmed; - /// The number of workers en route. - final int enRoute; + /// Workers who have checked in. + final int totalWorkersCheckedIn; - /// The number of late workers. - final int late; + /// Workers en route (accepted but not checked in). + final int totalWorkersEnRoute; - /// Calculates the overall coverage percentage. - int get coveragePercent { - if (totalNeeded == 0) return 0; - return ((totalConfirmed / totalNeeded) * 100).round(); + /// Workers marked as late / no-show. + final int totalWorkersLate; + + /// Overall coverage percentage (0-100). + final int totalCoveragePercentage; + + /// Serialises this [CoverageStats] to a JSON map. + Map toJson() { + return { + 'totalPositionsNeeded': totalPositionsNeeded, + 'totalPositionsConfirmed': totalPositionsConfirmed, + 'totalWorkersCheckedIn': totalWorkersCheckedIn, + 'totalWorkersEnRoute': totalWorkersEnRoute, + 'totalWorkersLate': totalWorkersLate, + 'totalCoveragePercentage': totalCoveragePercentage, + }; } @override List get props => [ - totalNeeded, - totalConfirmed, - checkedIn, - enRoute, - late, + totalPositionsNeeded, + totalPositionsConfirmed, + totalWorkersCheckedIn, + totalWorkersEnRoute, + totalWorkersLate, + totalCoveragePercentage, ]; } diff --git a/apps/mobile/packages/domain/lib/src/entities/coverage_domain/coverage_worker.dart b/apps/mobile/packages/domain/lib/src/entities/coverage_domain/coverage_worker.dart deleted file mode 100644 index 3ade4d9d..00000000 --- a/apps/mobile/packages/domain/lib/src/entities/coverage_domain/coverage_worker.dart +++ /dev/null @@ -1,55 +0,0 @@ -import 'package:equatable/equatable.dart'; - -/// Worker status enum matching ApplicationStatus from Data Connect. -enum CoverageWorkerStatus { - /// Application is pending approval. - pending, - - /// Application has been accepted. - accepted, - - /// Application has been rejected. - rejected, - - /// Worker has confirmed attendance. - confirmed, - - /// Worker has checked in. - checkedIn, - - /// Worker has checked out. - checkedOut, - - /// Worker is late. - late, - - /// Worker did not show up. - noShow, - - /// Shift is completed. - completed, -} - -/// Domain entity representing a worker in the coverage view. -/// -/// This entity tracks worker status including check-in information. -class CoverageWorker extends Equatable { - /// Creates a [CoverageWorker]. - const CoverageWorker({ - required this.name, - required this.status, - this.checkInTime, - }); - - /// The name of the worker. - final String name; - - /// The status of the worker. - final CoverageWorkerStatus status; - - /// The time the worker checked in, if applicable. - final String? checkInTime; - - @override - List get props => [name, status, checkInTime]; -} diff --git a/apps/mobile/packages/domain/lib/src/entities/coverage_domain/shift_with_workers.dart b/apps/mobile/packages/domain/lib/src/entities/coverage_domain/shift_with_workers.dart new file mode 100644 index 00000000..476334f8 --- /dev/null +++ b/apps/mobile/packages/domain/lib/src/entities/coverage_domain/shift_with_workers.dart @@ -0,0 +1,80 @@ +import 'package:equatable/equatable.dart'; + +import 'assigned_worker.dart'; +import 'time_range.dart'; + +/// A shift in the coverage view with its assigned workers. +/// +/// Returned by `GET /client/coverage`. +class ShiftWithWorkers extends Equatable { + /// Creates a [ShiftWithWorkers] instance. + const ShiftWithWorkers({ + required this.shiftId, + required this.roleName, + required this.timeRange, + required this.requiredWorkerCount, + required this.assignedWorkerCount, + this.assignedWorkers = const [], + }); + + /// Deserialises a [ShiftWithWorkers] from a V2 API JSON map. + factory ShiftWithWorkers.fromJson(Map json) { + final dynamic workersRaw = json['assignedWorkers']; + final List workersList = workersRaw is List + ? workersRaw + .map((dynamic e) => + AssignedWorker.fromJson(e as Map)) + .toList() + : const []; + + return ShiftWithWorkers( + shiftId: json['shiftId'] as String, + roleName: json['roleName'] as String? ?? '', + timeRange: TimeRange.fromJson(json['timeRange'] as Map), + requiredWorkerCount: (json['requiredWorkerCount'] as num).toInt(), + assignedWorkerCount: (json['assignedWorkerCount'] as num).toInt(), + assignedWorkers: workersList, + ); + } + + /// Shift ID. + final String shiftId; + + /// Role name for this shift. + final String roleName; + + /// Start and end time range. + final TimeRange timeRange; + + /// Total workers required. + final int requiredWorkerCount; + + /// Workers currently assigned. + final int assignedWorkerCount; + + /// List of assigned workers with their statuses. + final List assignedWorkers; + + /// Serialises this [ShiftWithWorkers] to a JSON map. + Map toJson() { + return { + 'shiftId': shiftId, + 'roleName': roleName, + 'timeRange': timeRange.toJson(), + 'requiredWorkerCount': requiredWorkerCount, + 'assignedWorkerCount': assignedWorkerCount, + 'assignedWorkers': + assignedWorkers.map((AssignedWorker w) => w.toJson()).toList(), + }; + } + + @override + List get props => [ + shiftId, + roleName, + timeRange, + requiredWorkerCount, + assignedWorkerCount, + assignedWorkers, + ]; +} diff --git a/apps/mobile/packages/domain/lib/src/entities/coverage_domain/time_range.dart b/apps/mobile/packages/domain/lib/src/entities/coverage_domain/time_range.dart new file mode 100644 index 00000000..543deccd --- /dev/null +++ b/apps/mobile/packages/domain/lib/src/entities/coverage_domain/time_range.dart @@ -0,0 +1,37 @@ +import 'package:equatable/equatable.dart'; + +/// A time range with start and end timestamps. +/// +/// Used within [ShiftWithWorkers] for shift time windows. +class TimeRange extends Equatable { + /// Creates a [TimeRange] instance. + const TimeRange({ + required this.startsAt, + required this.endsAt, + }); + + /// Deserialises a [TimeRange] from a V2 API JSON map. + factory TimeRange.fromJson(Map json) { + return TimeRange( + startsAt: DateTime.parse(json['startsAt'] as String), + endsAt: DateTime.parse(json['endsAt'] as String), + ); + } + + /// Start timestamp. + final DateTime startsAt; + + /// End timestamp. + final DateTime endsAt; + + /// Serialises this [TimeRange] to a JSON map. + Map toJson() { + return { + 'startsAt': startsAt.toIso8601String(), + 'endsAt': endsAt.toIso8601String(), + }; + } + + @override + List get props => [startsAt, endsAt]; +} diff --git a/apps/mobile/packages/domain/lib/src/entities/enums/account_type.dart b/apps/mobile/packages/domain/lib/src/entities/enums/account_type.dart new file mode 100644 index 00000000..088b9213 --- /dev/null +++ b/apps/mobile/packages/domain/lib/src/entities/enums/account_type.dart @@ -0,0 +1,30 @@ +/// Type of bank account. +/// +/// Used by both staff bank accounts and client billing accounts. +enum AccountType { + /// Checking account. + checking('CHECKING'), + + /// Savings account. + savings('SAVINGS'), + + /// Fallback for unrecognised API values. + unknown('UNKNOWN'); + + const AccountType(this.value); + + /// The V2 API string representation. + final String value; + + /// Deserialises from a V2 API string with safe fallback. + static AccountType fromJson(String? value) { + if (value == null) return AccountType.checking; + for (final AccountType type in AccountType.values) { + if (type.value == value) return type; + } + return AccountType.unknown; + } + + /// Serialises to the V2 API string. + String toJson() => value; +} diff --git a/apps/mobile/packages/domain/lib/src/entities/enums/application_status.dart b/apps/mobile/packages/domain/lib/src/entities/enums/application_status.dart new file mode 100644 index 00000000..7374e41f --- /dev/null +++ b/apps/mobile/packages/domain/lib/src/entities/enums/application_status.dart @@ -0,0 +1,48 @@ +/// Status of a worker's application to a shift. +/// +/// Maps to the `status` CHECK constraint in the V2 `applications` table. +enum ApplicationStatus { + /// Application submitted, awaiting review. + pending('PENDING'), + + /// Application confirmed / approved. + confirmed('CONFIRMED'), + + /// Worker has checked in for the shift. + checkedIn('CHECKED_IN'), + + /// Worker is late for check-in. + late_('LATE'), + + /// Worker did not show up. + noShow('NO_SHOW'), + + /// Application / attendance completed. + completed('COMPLETED'), + + /// Application rejected. + rejected('REJECTED'), + + /// Application cancelled. + cancelled('CANCELLED'), + + /// Fallback for unrecognised API values. + unknown('UNKNOWN'); + + const ApplicationStatus(this.value); + + /// The V2 API string representation. + final String value; + + /// Deserialises from a V2 API string with safe fallback. + static ApplicationStatus fromJson(String? value) { + if (value == null) return ApplicationStatus.unknown; + for (final ApplicationStatus status in ApplicationStatus.values) { + if (status.value == value) return status; + } + return ApplicationStatus.unknown; + } + + /// Serialises to the V2 API string. + String toJson() => value; +} diff --git a/apps/mobile/packages/domain/lib/src/entities/enums/assignment_status.dart b/apps/mobile/packages/domain/lib/src/entities/enums/assignment_status.dart new file mode 100644 index 00000000..a446fe00 --- /dev/null +++ b/apps/mobile/packages/domain/lib/src/entities/enums/assignment_status.dart @@ -0,0 +1,48 @@ +/// Status of a worker's assignment to a shift. +/// +/// Maps to the `status` CHECK constraint in the V2 `assignments` table. +enum AssignmentStatus { + /// Worker has been assigned but not yet accepted. + assigned('ASSIGNED'), + + /// Worker accepted the assignment. + accepted('ACCEPTED'), + + /// Worker requested to swap this assignment. + swapRequested('SWAP_REQUESTED'), + + /// Worker has checked in. + checkedIn('CHECKED_IN'), + + /// Worker has checked out. + checkedOut('CHECKED_OUT'), + + /// Assignment completed. + completed('COMPLETED'), + + /// Assignment cancelled. + cancelled('CANCELLED'), + + /// Worker did not show up. + noShow('NO_SHOW'), + + /// Fallback for unrecognised API values. + unknown('UNKNOWN'); + + const AssignmentStatus(this.value); + + /// The V2 API string representation. + final String value; + + /// Deserialises from a V2 API string with safe fallback. + static AssignmentStatus fromJson(String? value) { + if (value == null) return AssignmentStatus.unknown; + for (final AssignmentStatus status in AssignmentStatus.values) { + if (status.value == value) return status; + } + return AssignmentStatus.unknown; + } + + /// Serialises to the V2 API string. + String toJson() => value; +} diff --git a/apps/mobile/packages/domain/lib/src/entities/enums/attendance_status_type.dart b/apps/mobile/packages/domain/lib/src/entities/enums/attendance_status_type.dart new file mode 100644 index 00000000..e339f797 --- /dev/null +++ b/apps/mobile/packages/domain/lib/src/entities/enums/attendance_status_type.dart @@ -0,0 +1,36 @@ +/// Attendance session status for clock-in tracking. +/// +/// Maps to the `status` CHECK constraint in the V2 `attendance_events` table. +enum AttendanceStatusType { + /// Worker has not clocked in yet. + notClockedIn('NOT_CLOCKED_IN'), + + /// Attendance session is open (worker is clocked in). + open('OPEN'), + + /// Attendance session is closed (worker clocked out). + closed('CLOSED'), + + /// Attendance record is disputed. + disputed('DISPUTED'), + + /// Fallback for unrecognised API values. + unknown('UNKNOWN'); + + const AttendanceStatusType(this.value); + + /// The V2 API string representation. + final String value; + + /// Deserialises from a V2 API string with safe fallback. + static AttendanceStatusType fromJson(String? value) { + if (value == null) return AttendanceStatusType.notClockedIn; + for (final AttendanceStatusType status in AttendanceStatusType.values) { + if (status.value == value) return status; + } + return AttendanceStatusType.unknown; + } + + /// Serialises to the V2 API string. + String toJson() => value; +} diff --git a/apps/mobile/packages/domain/lib/src/entities/enums/availability_status.dart b/apps/mobile/packages/domain/lib/src/entities/enums/availability_status.dart new file mode 100644 index 00000000..150f08c6 --- /dev/null +++ b/apps/mobile/packages/domain/lib/src/entities/enums/availability_status.dart @@ -0,0 +1,34 @@ +/// Availability status for a calendar day. +/// +/// Used by the staff availability feature to indicate whether a worker +/// is available on a given date. +enum AvailabilityStatus { + /// Worker is available for the full day. + available('AVAILABLE'), + + /// Worker is not available. + unavailable('UNAVAILABLE'), + + /// Worker is available for partial time slots. + partial('PARTIAL'), + + /// Fallback for unrecognised API values. + unknown('UNKNOWN'); + + const AvailabilityStatus(this.value); + + /// The V2 API string representation. + final String value; + + /// Deserialises from a V2 API string with safe fallback. + static AvailabilityStatus fromJson(String? value) { + if (value == null) return AvailabilityStatus.unavailable; + for (final AvailabilityStatus status in AvailabilityStatus.values) { + if (status.value == value) return status; + } + return AvailabilityStatus.unknown; + } + + /// Serialises to the V2 API string. + String toJson() => value; +} diff --git a/apps/mobile/packages/domain/lib/src/entities/enums/benefit_status.dart b/apps/mobile/packages/domain/lib/src/entities/enums/benefit_status.dart new file mode 100644 index 00000000..4483d475 --- /dev/null +++ b/apps/mobile/packages/domain/lib/src/entities/enums/benefit_status.dart @@ -0,0 +1,34 @@ +/// Status of a staff benefit accrual. +/// +/// Used by the benefits feature to track whether a benefit is currently +/// active, paused, or pending activation. +enum BenefitStatus { + /// Benefit is active and accruing. + active('ACTIVE'), + + /// Benefit is inactive / paused. + inactive('INACTIVE'), + + /// Benefit is pending activation. + pending('PENDING'), + + /// Fallback for unrecognised API values. + unknown('UNKNOWN'); + + const BenefitStatus(this.value); + + /// The V2 API string representation. + final String value; + + /// Deserialises from a V2 API string with safe fallback. + static BenefitStatus fromJson(String? value) { + if (value == null) return BenefitStatus.unknown; + for (final BenefitStatus status in BenefitStatus.values) { + if (status.value == value) return status; + } + return BenefitStatus.unknown; + } + + /// Serialises to the V2 API string. + String toJson() => value; +} diff --git a/apps/mobile/packages/domain/lib/src/entities/enums/business_status.dart b/apps/mobile/packages/domain/lib/src/entities/enums/business_status.dart new file mode 100644 index 00000000..721b999f --- /dev/null +++ b/apps/mobile/packages/domain/lib/src/entities/enums/business_status.dart @@ -0,0 +1,34 @@ +/// Account status of a business or vendor. +/// +/// Maps to the `status` CHECK constraint in the V2 `businesses` and +/// `vendors` tables. +enum BusinessStatus { + /// Account is active. + active('ACTIVE'), + + /// Account is inactive / suspended. + inactive('INACTIVE'), + + /// Account has been archived. + archived('ARCHIVED'), + + /// Fallback for unrecognised API values. + unknown('UNKNOWN'); + + const BusinessStatus(this.value); + + /// The V2 API string representation. + final String value; + + /// Deserialises from a V2 API string with safe fallback. + static BusinessStatus fromJson(String? value) { + if (value == null) return BusinessStatus.unknown; + for (final BusinessStatus status in BusinessStatus.values) { + if (status.value == value) return status; + } + return BusinessStatus.unknown; + } + + /// Serialises to the V2 API string. + String toJson() => value; +} diff --git a/apps/mobile/packages/domain/lib/src/entities/enums/invoice_status.dart b/apps/mobile/packages/domain/lib/src/entities/enums/invoice_status.dart new file mode 100644 index 00000000..f592dfb3 --- /dev/null +++ b/apps/mobile/packages/domain/lib/src/entities/enums/invoice_status.dart @@ -0,0 +1,48 @@ +/// Lifecycle status of an invoice. +/// +/// Maps to the `status` CHECK constraint in the V2 `invoices` table. +enum InvoiceStatus { + /// Invoice created but not yet sent. + draft('DRAFT'), + + /// Invoice sent, awaiting payment. + pending('PENDING'), + + /// Invoice under review. + pendingReview('PENDING_REVIEW'), + + /// Invoice approved for payment. + approved('APPROVED'), + + /// Invoice paid. + paid('PAID'), + + /// Invoice overdue. + overdue('OVERDUE'), + + /// Invoice disputed by the client. + disputed('DISPUTED'), + + /// Invoice voided / cancelled. + void_('VOID'), + + /// Fallback for unrecognised API values. + unknown('UNKNOWN'); + + const InvoiceStatus(this.value); + + /// The V2 API string representation. + final String value; + + /// Deserialises from a V2 API string with safe fallback. + static InvoiceStatus fromJson(String? value) { + if (value == null) return InvoiceStatus.unknown; + for (final InvoiceStatus status in InvoiceStatus.values) { + if (status.value == value) return status; + } + return InvoiceStatus.unknown; + } + + /// Serialises to the V2 API string. + String toJson() => value; +} diff --git a/apps/mobile/packages/domain/lib/src/entities/enums/onboarding_status.dart b/apps/mobile/packages/domain/lib/src/entities/enums/onboarding_status.dart new file mode 100644 index 00000000..151add4a --- /dev/null +++ b/apps/mobile/packages/domain/lib/src/entities/enums/onboarding_status.dart @@ -0,0 +1,33 @@ +/// Onboarding progress status for a staff member. +/// +/// Maps to the `onboarding_status` CHECK constraint in the V2 `staffs` table. +enum OnboardingStatus { + /// Onboarding not yet started. + pending('PENDING'), + + /// Onboarding in progress. + inProgress('IN_PROGRESS'), + + /// Onboarding completed. + completed('COMPLETED'), + + /// Fallback for unrecognised API values. + unknown('UNKNOWN'); + + const OnboardingStatus(this.value); + + /// The V2 API string representation. + final String value; + + /// Deserialises from a V2 API string with safe fallback. + static OnboardingStatus fromJson(String? value) { + if (value == null) return OnboardingStatus.unknown; + for (final OnboardingStatus status in OnboardingStatus.values) { + if (status.value == value) return status; + } + return OnboardingStatus.unknown; + } + + /// Serialises to the V2 API string. + String toJson() => value; +} diff --git a/apps/mobile/packages/domain/lib/src/entities/enums/order_type.dart b/apps/mobile/packages/domain/lib/src/entities/enums/order_type.dart new file mode 100644 index 00000000..ca3cc1bc --- /dev/null +++ b/apps/mobile/packages/domain/lib/src/entities/enums/order_type.dart @@ -0,0 +1,36 @@ +/// Type of order placed by a business client. +/// +/// Maps to the `order_type` CHECK constraint in the V2 `orders` table. +enum OrderType { + /// A single occurrence order. + oneTime('ONE_TIME'), + + /// A recurring/repeating order. + recurring('RECURRING'), + + /// A permanent/ongoing order. + permanent('PERMANENT'), + + /// A rapid-fill order. + rapid('RAPID'), + + /// Fallback for unrecognised API values. + unknown('UNKNOWN'); + + const OrderType(this.value); + + /// The V2 API string representation. + final String value; + + /// Deserialises from a V2 API string with safe fallback. + static OrderType fromJson(String? value) { + if (value == null) return OrderType.unknown; + for (final OrderType type in OrderType.values) { + if (type.value == value) return type; + } + return OrderType.unknown; + } + + /// Serialises to the V2 API string. + String toJson() => value; +} diff --git a/apps/mobile/packages/domain/lib/src/entities/enums/payment_status.dart b/apps/mobile/packages/domain/lib/src/entities/enums/payment_status.dart new file mode 100644 index 00000000..4c3446b7 --- /dev/null +++ b/apps/mobile/packages/domain/lib/src/entities/enums/payment_status.dart @@ -0,0 +1,36 @@ +/// Payment processing status. +/// +/// Maps to the `payment_status` CHECK constraint in the V2 schema. +enum PaymentStatus { + /// Payment not yet processed. + pending('PENDING'), + + /// Payment is being processed. + processing('PROCESSING'), + + /// Payment completed successfully. + paid('PAID'), + + /// Payment processing failed. + failed('FAILED'), + + /// Fallback for unrecognised API values. + unknown('UNKNOWN'); + + const PaymentStatus(this.value); + + /// The V2 API string representation. + final String value; + + /// Deserialises from a V2 API string with safe fallback. + static PaymentStatus fromJson(String? value) { + if (value == null) return PaymentStatus.unknown; + for (final PaymentStatus status in PaymentStatus.values) { + if (status.value == value) return status; + } + return PaymentStatus.unknown; + } + + /// Serialises to the V2 API string. + String toJson() => value; +} diff --git a/apps/mobile/packages/domain/lib/src/entities/enums/shift_status.dart b/apps/mobile/packages/domain/lib/src/entities/enums/shift_status.dart new file mode 100644 index 00000000..0288ad7a --- /dev/null +++ b/apps/mobile/packages/domain/lib/src/entities/enums/shift_status.dart @@ -0,0 +1,45 @@ +/// Lifecycle status of a shift. +/// +/// Maps to the `status` CHECK constraint in the V2 `shifts` table. +enum ShiftStatus { + /// Shift created but not yet published. + draft('DRAFT'), + + /// Open for applications. + open('OPEN'), + + /// Awaiting worker confirmation. + pendingConfirmation('PENDING_CONFIRMATION'), + + /// All roles filled and confirmed. + assigned('ASSIGNED'), + + /// Currently in progress. + active('ACTIVE'), + + /// Shift finished. + completed('COMPLETED'), + + /// Shift cancelled. + cancelled('CANCELLED'), + + /// Fallback for unrecognised API values. + unknown('UNKNOWN'); + + const ShiftStatus(this.value); + + /// The V2 API string representation. + final String value; + + /// Deserialises from a V2 API string with safe fallback. + static ShiftStatus fromJson(String? value) { + if (value == null) return ShiftStatus.unknown; + for (final ShiftStatus status in ShiftStatus.values) { + if (status.value == value) return status; + } + return ShiftStatus.unknown; + } + + /// Serialises to the V2 API string. + String toJson() => value; +} diff --git a/apps/mobile/packages/domain/lib/src/entities/enums/staff_status.dart b/apps/mobile/packages/domain/lib/src/entities/enums/staff_status.dart new file mode 100644 index 00000000..78667065 --- /dev/null +++ b/apps/mobile/packages/domain/lib/src/entities/enums/staff_status.dart @@ -0,0 +1,36 @@ +/// Account status of a staff member. +/// +/// Maps to the `status` CHECK constraint in the V2 `staffs` table. +enum StaffStatus { + /// Staff account is active. + active('ACTIVE'), + + /// Staff has been invited but not yet onboarded. + invited('INVITED'), + + /// Staff account is inactive / suspended. + inactive('INACTIVE'), + + /// Staff account has been blocked. + blocked('BLOCKED'), + + /// Fallback for unrecognised API values. + unknown('UNKNOWN'); + + const StaffStatus(this.value); + + /// The V2 API string representation. + final String value; + + /// Deserialises from a V2 API string with safe fallback. + static StaffStatus fromJson(String? value) { + if (value == null) return StaffStatus.unknown; + for (final StaffStatus status in StaffStatus.values) { + if (status.value == value) return status; + } + return StaffStatus.unknown; + } + + /// Serialises to the V2 API string. + String toJson() => value; +} diff --git a/apps/mobile/packages/domain/lib/src/entities/events/assignment.dart b/apps/mobile/packages/domain/lib/src/entities/events/assignment.dart deleted file mode 100644 index 197281a5..00000000 --- a/apps/mobile/packages/domain/lib/src/entities/events/assignment.dart +++ /dev/null @@ -1,58 +0,0 @@ -import 'package:equatable/equatable.dart'; - -/// The status of a staff [Assignment]. -enum AssignmentStatus { - /// Staff member has been assigned but hasn't confirmed. - assigned, - - /// Staff member has accepted the assignment. - confirmed, - - /// Work is currently in progress (Clocked In). - ongoing, - - /// Work completed successfully (Clocked Out). - completed, - - /// Staff rejected the assignment offer. - declinedByStaff, - - /// Staff canceled after accepting. - canceledByStaff, - - /// Staff did not show up. - noShowed, -} - -/// Represents the link between a [Staff] member and an [EventShiftPosition]. -class Assignment extends Equatable { - - const Assignment({ - required this.id, - required this.positionId, - required this.staffId, - required this.status, - this.clockIn, - this.clockOut, - }); - /// Unique identifier. - final String id; - - /// The job position being filled. - final String positionId; - - /// The staff member filling the position. - final String staffId; - - /// Current status of the assignment. - final AssignmentStatus status; - - /// Actual timestamp when staff clocked in. - final DateTime? clockIn; - - /// Actual timestamp when staff clocked out. - final DateTime? clockOut; - - @override - List get props => [id, positionId, staffId, status, clockIn, clockOut]; -} \ No newline at end of file diff --git a/apps/mobile/packages/domain/lib/src/entities/events/event.dart b/apps/mobile/packages/domain/lib/src/entities/events/event.dart deleted file mode 100644 index d7def36f..00000000 --- a/apps/mobile/packages/domain/lib/src/entities/events/event.dart +++ /dev/null @@ -1,70 +0,0 @@ -import 'package:equatable/equatable.dart'; - -/// The workflow status of an [Event]. -enum EventStatus { - /// Created but incomplete. - draft, - - /// Waiting for approval or publication. - pending, - - /// Published and staff have been assigned. - assigned, - - /// Fully confirmed and ready to start. - confirmed, - - /// Currently in progress. - active, - - /// Work has finished. - finished, - - /// All post-event processes (invoicing) complete. - completed, - - /// Archived. - closed, - - /// Flagged for administrative review. - underReview, -} - -/// Represents a Job Posting or Event. -/// -/// This is the central entity for scheduling work. An Event contains [EventShift]s. -class Event extends Equatable { - - const Event({ - required this.id, - required this.businessId, - required this.hubId, - required this.name, - required this.date, - required this.status, - required this.contractType, - }); - /// Unique identifier. - final String id; - - /// The [Business] hosting the event. - final String businessId; - - /// The [Hub] location. - final String hubId; - - /// Title of the event. - final String name; - - /// Date of the event. - final DateTime date; - - /// Current workflow status. - final EventStatus status; - - /// Type of employment contract (e.g., 'freelance', 'permanent'). - final String contractType; - - @override - List get props => [id, businessId, hubId, name, date, status, contractType]; -} \ No newline at end of file diff --git a/apps/mobile/packages/domain/lib/src/entities/events/event_shift.dart b/apps/mobile/packages/domain/lib/src/entities/events/event_shift.dart deleted file mode 100644 index 32a025e3..00000000 --- a/apps/mobile/packages/domain/lib/src/entities/events/event_shift.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'package:equatable/equatable.dart'; - -/// Represents a specific time block or "shift" within an [Event]. -/// -/// An Event can have multiple shifts (e.g. "Morning Shift", "Evening Shift"). -class EventShift extends Equatable { - - const EventShift({ - required this.id, - required this.eventId, - required this.name, - required this.address, - }); - /// Unique identifier. - final String id; - - /// The [Event] this shift belongs to. - final String eventId; - - /// Descriptive name (e.g. "Setup Crew"). - final String name; - - /// Specific address for this shift (if different from Hub). - final String address; - - @override - List get props => [id, eventId, name, address]; -} \ No newline at end of file diff --git a/apps/mobile/packages/domain/lib/src/entities/events/event_shift_position.dart b/apps/mobile/packages/domain/lib/src/entities/events/event_shift_position.dart deleted file mode 100644 index 8eb226f0..00000000 --- a/apps/mobile/packages/domain/lib/src/entities/events/event_shift_position.dart +++ /dev/null @@ -1,53 +0,0 @@ -import 'package:equatable/equatable.dart'; - -/// Represents a specific job opening within a [EventShift]. -/// -/// Defines the requirement for a specific [Skill], the quantity needed, and the pay. -class EventShiftPosition extends Equatable { - - const EventShiftPosition({ - required this.id, - required this.shiftId, - required this.skillId, - required this.count, - required this.rate, - required this.startTime, - required this.endTime, - required this.breakDurationMinutes, - }); - /// Unique identifier. - final String id; - - /// The [EventShift] this position is part of. - final String shiftId; - - /// The [Skill] required for this position. - final String skillId; - - /// Number of staff needed. - final int count; - - /// Hourly pay rate. - final double rate; - - /// Start time of this specific position. - final DateTime startTime; - - /// End time of this specific position. - final DateTime endTime; - - /// Deducted break duration in minutes. - final int breakDurationMinutes; - - @override - List get props => [ - id, - shiftId, - skillId, - count, - rate, - startTime, - endTime, - breakDurationMinutes, - ]; -} \ No newline at end of file diff --git a/apps/mobile/packages/domain/lib/src/entities/events/work_session.dart b/apps/mobile/packages/domain/lib/src/entities/events/work_session.dart deleted file mode 100644 index ef06a323..00000000 --- a/apps/mobile/packages/domain/lib/src/entities/events/work_session.dart +++ /dev/null @@ -1,32 +0,0 @@ -import 'package:equatable/equatable.dart'; - -/// Represents a verified record of time worked. -/// -/// Derived from [Assignment] clock-in/out times, used for payroll. -class WorkSession extends Equatable { - - const WorkSession({ - required this.id, - required this.assignmentId, - required this.startTime, - this.endTime, - required this.breakDurationMinutes, - }); - /// Unique identifier. - final String id; - - /// The [Assignment] this session belongs to. - final String assignmentId; - - /// Verified start time. - final DateTime startTime; - - /// Verified end time. - final DateTime? endTime; - - /// Verified break duration. - final int breakDurationMinutes; - - @override - List get props => [id, assignmentId, startTime, endTime, breakDurationMinutes]; -} \ No newline at end of file diff --git a/apps/mobile/packages/domain/lib/src/entities/financial/bank_account.dart b/apps/mobile/packages/domain/lib/src/entities/financial/bank_account.dart new file mode 100644 index 00000000..bde621a4 --- /dev/null +++ b/apps/mobile/packages/domain/lib/src/entities/financial/bank_account.dart @@ -0,0 +1,70 @@ +import 'package:equatable/equatable.dart'; + +import 'package:krow_domain/src/entities/enums/account_type.dart'; + +/// A bank account belonging to a staff member. +/// +/// Returned by `GET /staff/profile/bank-accounts`. +class BankAccount extends Equatable { + /// Creates a [BankAccount] instance. + const BankAccount({ + required this.accountId, + required this.bankName, + required this.providerReference, + this.last4, + required this.isPrimary, + required this.accountType, + }); + + /// Deserialises a [BankAccount] from a V2 API JSON map. + factory BankAccount.fromJson(Map json) { + return BankAccount( + accountId: json['accountId'] as String, + bankName: json['bankName'] as String, + providerReference: json['providerReference'] as String, + last4: json['last4'] as String?, + isPrimary: json['isPrimary'] as bool, + accountType: AccountType.fromJson(json['accountType'] as String?), + ); + } + + /// Unique identifier. + final String accountId; + + /// Name of the bank / payment provider. + final String bankName; + + /// External provider reference. + final String providerReference; + + /// Last 4 digits of the account number. + final String? last4; + + /// Whether this is the primary payout account. + final bool isPrimary; + + /// Account type. + final AccountType accountType; + + /// Serialises this [BankAccount] to a JSON map. + Map toJson() { + return { + 'accountId': accountId, + 'bankName': bankName, + 'providerReference': providerReference, + 'last4': last4, + 'isPrimary': isPrimary, + 'accountType': accountType.toJson(), + }; + } + + @override + List get props => [ + accountId, + bankName, + providerReference, + last4, + isPrimary, + accountType, + ]; +} diff --git a/apps/mobile/packages/domain/lib/src/entities/financial/bank_account/bank_account.dart b/apps/mobile/packages/domain/lib/src/entities/financial/bank_account/bank_account.dart deleted file mode 100644 index 04af8402..00000000 --- a/apps/mobile/packages/domain/lib/src/entities/financial/bank_account/bank_account.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:equatable/equatable.dart'; - -/// Abstract base class for all types of bank accounts. -abstract class BankAccount extends Equatable { - /// Creates a [BankAccount]. - const BankAccount({ - required this.id, - required this.bankName, - required this.isPrimary, - this.last4, - }); - - /// Unique identifier. - final String id; - - /// Name of the bank or provider. - final String bankName; - - /// Whether this is the primary payment method. - final bool isPrimary; - - /// Last 4 digits of the account/card. - final String? last4; - - @override - List get props => [id, bankName, isPrimary, last4]; -} diff --git a/apps/mobile/packages/domain/lib/src/entities/financial/bank_account/business_bank_account.dart b/apps/mobile/packages/domain/lib/src/entities/financial/bank_account/business_bank_account.dart deleted file mode 100644 index 8ad3d48e..00000000 --- a/apps/mobile/packages/domain/lib/src/entities/financial/bank_account/business_bank_account.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'bank_account.dart'; - -/// Domain model representing a business bank account or payment method. -class BusinessBankAccount extends BankAccount { - /// Creates a [BusinessBankAccount]. - const BusinessBankAccount({ - required super.id, - required super.bankName, - required String last4, - required super.isPrimary, - this.expiryTime, - }) : super(last4: last4); - - /// Expiration date if applicable. - final DateTime? expiryTime; - - @override - List get props => [ - ...super.props, - expiryTime, - ]; - - /// Getter for non-nullable last4 in Business context. - @override - String get last4 => super.last4!; -} diff --git a/apps/mobile/packages/domain/lib/src/entities/financial/bank_account/staff_bank_account.dart b/apps/mobile/packages/domain/lib/src/entities/financial/bank_account/staff_bank_account.dart deleted file mode 100644 index 3f2f034e..00000000 --- a/apps/mobile/packages/domain/lib/src/entities/financial/bank_account/staff_bank_account.dart +++ /dev/null @@ -1,48 +0,0 @@ -import 'bank_account.dart'; - -/// Type of staff bank account. -enum StaffBankAccountType { - /// Checking account. - checking, - - /// Savings account. - savings, - - /// Other type. - other, -} - -/// Domain entity representing a staff's bank account. -class StaffBankAccount extends BankAccount { - /// Creates a [StaffBankAccount]. - const StaffBankAccount({ - required super.id, - required this.userId, - required super.bankName, - required this.accountNumber, - required this.accountName, - required super.isPrimary, - super.last4, - this.sortCode, - this.type = StaffBankAccountType.checking, - }); - - /// User identifier. - final String userId; - - /// Full account number. - final String accountNumber; - - /// Name of the account holder. - final String accountName; - - /// Sort code (optional). - final String? sortCode; - - /// Account type. - final StaffBankAccountType type; - - @override - List get props => - [...super.props, userId, accountNumber, accountName, sortCode, type]; -} diff --git a/apps/mobile/packages/domain/lib/src/entities/financial/billing_account.dart b/apps/mobile/packages/domain/lib/src/entities/financial/billing_account.dart new file mode 100644 index 00000000..738c0c4c --- /dev/null +++ b/apps/mobile/packages/domain/lib/src/entities/financial/billing_account.dart @@ -0,0 +1,77 @@ +import 'package:equatable/equatable.dart'; + +import 'package:krow_domain/src/entities/enums/account_type.dart'; + +/// A billing/bank account belonging to a business. +/// +/// Returned by `GET /client/billing/accounts`. +class BillingAccount extends Equatable { + /// Creates a [BillingAccount] instance. + const BillingAccount({ + required this.accountId, + required this.bankName, + required this.providerReference, + this.last4, + required this.isPrimary, + required this.accountType, + this.routingNumberMasked, + }); + + /// Deserialises a [BillingAccount] from a V2 API JSON map. + factory BillingAccount.fromJson(Map json) { + return BillingAccount( + accountId: json['accountId'] as String, + bankName: json['bankName'] as String, + providerReference: json['providerReference'] as String, + last4: json['last4'] as String?, + isPrimary: json['isPrimary'] as bool, + accountType: AccountType.fromJson(json['accountType'] as String?), + routingNumberMasked: json['routingNumberMasked'] as String?, + ); + } + + /// Unique identifier. + final String accountId; + + /// Name of the bank / payment provider. + final String bankName; + + /// External provider reference (e.g. Stripe account ID). + final String providerReference; + + /// Last 4 digits of the account number. + final String? last4; + + /// Whether this is the primary billing account. + final bool isPrimary; + + /// Account type. + final AccountType accountType; + + /// Masked routing number. + final String? routingNumberMasked; + + /// Serialises this [BillingAccount] to a JSON map. + Map toJson() { + return { + 'accountId': accountId, + 'bankName': bankName, + 'providerReference': providerReference, + 'last4': last4, + 'isPrimary': isPrimary, + 'accountType': accountType.toJson(), + 'routingNumberMasked': routingNumberMasked, + }; + } + + @override + List get props => [ + accountId, + bankName, + providerReference, + last4, + isPrimary, + accountType, + routingNumberMasked, + ]; +} diff --git a/apps/mobile/packages/domain/lib/src/entities/financial/billing_period.dart b/apps/mobile/packages/domain/lib/src/entities/financial/billing_period.dart deleted file mode 100644 index c26a4108..00000000 --- a/apps/mobile/packages/domain/lib/src/entities/financial/billing_period.dart +++ /dev/null @@ -1,8 +0,0 @@ -/// Defines the period for billing calculations. -enum BillingPeriod { - /// Weekly billing period. - week, - - /// Monthly billing period. - month, -} diff --git a/apps/mobile/packages/domain/lib/src/entities/financial/current_bill.dart b/apps/mobile/packages/domain/lib/src/entities/financial/current_bill.dart new file mode 100644 index 00000000..b7722e81 --- /dev/null +++ b/apps/mobile/packages/domain/lib/src/entities/financial/current_bill.dart @@ -0,0 +1,29 @@ +import 'package:equatable/equatable.dart'; + +/// The current outstanding bill for a business. +/// +/// Returned by `GET /client/billing/current-bill`. +class CurrentBill extends Equatable { + /// Creates a [CurrentBill] instance. + const CurrentBill({required this.currentBillCents}); + + /// Deserialises a [CurrentBill] from a V2 API JSON map. + factory CurrentBill.fromJson(Map json) { + return CurrentBill( + currentBillCents: (json['currentBillCents'] as num).toInt(), + ); + } + + /// Outstanding bill amount in cents. + final int currentBillCents; + + /// Serialises this [CurrentBill] to a JSON map. + Map toJson() { + return { + 'currentBillCents': currentBillCents, + }; + } + + @override + List get props => [currentBillCents]; +} diff --git a/apps/mobile/packages/domain/lib/src/entities/financial/invoice.dart b/apps/mobile/packages/domain/lib/src/entities/financial/invoice.dart index 64341884..7d370fd3 100644 --- a/apps/mobile/packages/domain/lib/src/entities/financial/invoice.dart +++ b/apps/mobile/packages/domain/lib/src/entities/financial/invoice.dart @@ -1,148 +1,88 @@ import 'package:equatable/equatable.dart'; -/// The workflow status of an [Invoice]. -enum InvoiceStatus { - /// Generated but not yet sent/finalized. - open, +import 'package:krow_domain/src/entities/enums/invoice_status.dart'; - /// Client has disputed a line item. - disputed, - - /// Dispute has been handled. - resolved, - - /// Invoice accepted by client. - verified, - - /// Payment received. - paid, - - /// Payment reconciled in accounting. - reconciled, - - /// Payment not received by due date. - overdue, -} - -/// Represents a bill sent to a [Business] for services rendered. +/// An invoice issued to a business for services rendered. +/// +/// Returned by `GET /client/billing/invoices/*`. class Invoice extends Equatable { - + /// Creates an [Invoice] instance. const Invoice({ - required this.id, - required this.eventId, - required this.businessId, + required this.invoiceId, + required this.invoiceNumber, + required this.amountCents, required this.status, - required this.totalAmount, - required this.workAmount, - required this.addonsAmount, - this.invoiceNumber, - this.issueDate, - this.title, - this.clientName, - this.locationAddress, - this.staffCount, - this.totalHours, - this.workers = const [], + this.dueDate, + this.paymentDate, + this.vendorId, + this.vendorName, }); + + /// Deserialises an [Invoice] from a V2 API JSON map. + factory Invoice.fromJson(Map json) { + return Invoice( + invoiceId: json['invoiceId'] as String, + invoiceNumber: json['invoiceNumber'] as String, + amountCents: (json['amountCents'] as num).toInt(), + status: InvoiceStatus.fromJson(json['status'] as String?), + dueDate: json['dueDate'] != null + ? DateTime.parse(json['dueDate'] as String) + : null, + paymentDate: json['paymentDate'] != null + ? DateTime.parse(json['paymentDate'] as String) + : null, + vendorId: json['vendorId'] as String?, + vendorName: json['vendorName'] as String?, + ); + } + /// Unique identifier. - final String id; - - /// The [Event] this invoice covers. - final String eventId; - - /// The [Business] being billed. - final String businessId; - - /// Current payment/approval status. - final InvoiceStatus status; - - /// Grand total amount. - final double totalAmount; - - /// Total amount for labor costs. - final double workAmount; - - /// Total amount for addons/extras. - final double addonsAmount; + final String invoiceId; /// Human-readable invoice number. - final String? invoiceNumber; + final String invoiceNumber; - /// Date when the invoice was issued. - final DateTime? issueDate; + /// Total amount in cents. + final int amountCents; - /// Human-readable title (e.g. event name). - final String? title; + /// Current invoice lifecycle status. + final InvoiceStatus status; - /// Name of the client business. - final String? clientName; + /// When payment is due. + final DateTime? dueDate; - /// Address of the event/location. - final String? locationAddress; + /// When the invoice was paid (history endpoint). + final DateTime? paymentDate; - /// Number of staff worked. - final int? staffCount; + /// Vendor ID associated with this invoice. + final String? vendorId; - /// Total hours worked. - final double? totalHours; + /// Vendor company name. + final String? vendorName; - /// List of workers associated with this invoice. - final List workers; + /// Serialises this [Invoice] to a JSON map. + Map toJson() { + return { + 'invoiceId': invoiceId, + 'invoiceNumber': invoiceNumber, + 'amountCents': amountCents, + 'status': status.toJson(), + 'dueDate': dueDate?.toIso8601String(), + 'paymentDate': paymentDate?.toIso8601String(), + 'vendorId': vendorId, + 'vendorName': vendorName, + }; + } @override List get props => [ - id, - eventId, - businessId, - status, - totalAmount, - workAmount, - addonsAmount, + invoiceId, invoiceNumber, - issueDate, - title, - clientName, - locationAddress, - staffCount, - totalHours, - workers, - ]; -} - -/// Represents a worker entry in an [Invoice]. -class InvoiceWorker extends Equatable { - const InvoiceWorker({ - required this.name, - required this.role, - required this.amount, - required this.hours, - required this.rate, - this.checkIn, - this.checkOut, - this.breakMinutes = 0, - this.avatarUrl, - }); - - final String name; - final String role; - final double amount; - final double hours; - final double rate; - final DateTime? checkIn; - final DateTime? checkOut; - final int breakMinutes; - final String? avatarUrl; - - @override - List get props => [ - name, - role, - amount, - hours, - rate, - checkIn, - checkOut, - breakMinutes, - avatarUrl, + amountCents, + status, + dueDate, + paymentDate, + vendorId, + vendorName, ]; } diff --git a/apps/mobile/packages/domain/lib/src/entities/financial/invoice_decline.dart b/apps/mobile/packages/domain/lib/src/entities/financial/invoice_decline.dart deleted file mode 100644 index 1d0a8035..00000000 --- a/apps/mobile/packages/domain/lib/src/entities/financial/invoice_decline.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'package:equatable/equatable.dart'; - -/// Represents a reason or log for a declined [Invoice]. -class InvoiceDecline extends Equatable { - - const InvoiceDecline({ - required this.id, - required this.invoiceId, - required this.reason, - required this.declinedAt, - }); - /// Unique identifier. - final String id; - - /// The [Invoice] that was declined. - final String invoiceId; - - /// Reason provided by the client. - final String reason; - - /// When the decline happened. - final DateTime declinedAt; - - @override - List get props => [id, invoiceId, reason, declinedAt]; -} \ No newline at end of file diff --git a/apps/mobile/packages/domain/lib/src/entities/financial/invoice_item.dart b/apps/mobile/packages/domain/lib/src/entities/financial/invoice_item.dart deleted file mode 100644 index e661334a..00000000 --- a/apps/mobile/packages/domain/lib/src/entities/financial/invoice_item.dart +++ /dev/null @@ -1,36 +0,0 @@ -import 'package:equatable/equatable.dart'; - -/// Represents a line item in an [Invoice]. -/// -/// Corresponds to the work done by one [Staff] member. -class InvoiceItem extends Equatable { - - const InvoiceItem({ - required this.id, - required this.invoiceId, - required this.staffId, - required this.workHours, - required this.rate, - required this.amount, - }); - /// Unique identifier. - final String id; - - /// The [Invoice] this item belongs to. - final String invoiceId; - - /// The [Staff] member whose work is being billed. - final String staffId; - - /// Total billed hours. - final double workHours; - - /// Hourly rate applied. - final double rate; - - /// Total line item amount (workHours * rate). - final double amount; - - @override - List get props => [id, invoiceId, staffId, workHours, rate, amount]; -} \ No newline at end of file diff --git a/apps/mobile/packages/domain/lib/src/entities/financial/payment_chart_point.dart b/apps/mobile/packages/domain/lib/src/entities/financial/payment_chart_point.dart new file mode 100644 index 00000000..2e9f92f0 --- /dev/null +++ b/apps/mobile/packages/domain/lib/src/entities/financial/payment_chart_point.dart @@ -0,0 +1,37 @@ +import 'package:equatable/equatable.dart'; + +/// A single data point in the staff payment chart. +/// +/// Returned by `GET /staff/payments/chart`. +class PaymentChartPoint extends Equatable { + /// Creates a [PaymentChartPoint] instance. + const PaymentChartPoint({ + required this.bucket, + required this.amountCents, + }); + + /// Deserialises a [PaymentChartPoint] from a V2 API JSON map. + factory PaymentChartPoint.fromJson(Map json) { + return PaymentChartPoint( + bucket: DateTime.parse(json['bucket'] as String), + amountCents: (json['amountCents'] as num).toInt(), + ); + } + + /// Time bucket start (day, week, or month). + final DateTime bucket; + + /// Aggregated payment amount in cents for this bucket. + final int amountCents; + + /// Serialises this [PaymentChartPoint] to a JSON map. + Map toJson() { + return { + 'bucket': bucket.toIso8601String(), + 'amountCents': amountCents, + }; + } + + @override + List get props => [bucket, amountCents]; +} diff --git a/apps/mobile/packages/domain/lib/src/entities/financial/payment_summary.dart b/apps/mobile/packages/domain/lib/src/entities/financial/payment_summary.dart index 5a905853..1d090b5a 100644 --- a/apps/mobile/packages/domain/lib/src/entities/financial/payment_summary.dart +++ b/apps/mobile/packages/domain/lib/src/entities/financial/payment_summary.dart @@ -1,24 +1,29 @@ import 'package:equatable/equatable.dart'; -/// Summary of staff earnings. +/// Aggregated payment summary for a staff member over a date range. +/// +/// Returned by `GET /staff/payments/summary`. class PaymentSummary extends Equatable { + /// Creates a [PaymentSummary] instance. + const PaymentSummary({required this.totalEarningsCents}); - const PaymentSummary({ - required this.weeklyEarnings, - required this.monthlyEarnings, - required this.pendingEarnings, - required this.totalEarnings, - }); - final double weeklyEarnings; - final double monthlyEarnings; - final double pendingEarnings; - final double totalEarnings; + /// Deserialises a [PaymentSummary] from a V2 API JSON map. + factory PaymentSummary.fromJson(Map json) { + return PaymentSummary( + totalEarningsCents: (json['totalEarningsCents'] as num).toInt(), + ); + } + + /// Total earnings in cents for the queried period. + final int totalEarningsCents; + + /// Serialises this [PaymentSummary] to a JSON map. + Map toJson() { + return { + 'totalEarningsCents': totalEarningsCents, + }; + } @override - List get props => [ - weeklyEarnings, - monthlyEarnings, - pendingEarnings, - totalEarnings, - ]; + List get props => [totalEarningsCents]; } diff --git a/apps/mobile/packages/domain/lib/src/entities/financial/savings.dart b/apps/mobile/packages/domain/lib/src/entities/financial/savings.dart new file mode 100644 index 00000000..66126d71 --- /dev/null +++ b/apps/mobile/packages/domain/lib/src/entities/financial/savings.dart @@ -0,0 +1,29 @@ +import 'package:equatable/equatable.dart'; + +/// Accumulated savings for a business. +/// +/// Returned by `GET /client/billing/savings`. +class Savings extends Equatable { + /// Creates a [Savings] instance. + const Savings({required this.savingsCents}); + + /// Deserialises a [Savings] from a V2 API JSON map. + factory Savings.fromJson(Map json) { + return Savings( + savingsCents: (json['savingsCents'] as num).toInt(), + ); + } + + /// Total savings amount in cents. + final int savingsCents; + + /// Serialises this [Savings] to a JSON map. + Map toJson() { + return { + 'savingsCents': savingsCents, + }; + } + + @override + List get props => [savingsCents]; +} diff --git a/apps/mobile/packages/domain/lib/src/entities/financial/spend_item.dart b/apps/mobile/packages/domain/lib/src/entities/financial/spend_item.dart new file mode 100644 index 00000000..be8f9cf1 --- /dev/null +++ b/apps/mobile/packages/domain/lib/src/entities/financial/spend_item.dart @@ -0,0 +1,43 @@ +import 'package:equatable/equatable.dart'; + +/// A single category in the spend breakdown. +/// +/// Returned by `GET /client/billing/spend-breakdown`. +class SpendItem extends Equatable { + /// Creates a [SpendItem] instance. + const SpendItem({ + required this.category, + required this.amountCents, + required this.percentage, + }); + + /// Deserialises a [SpendItem] from a V2 API JSON map. + factory SpendItem.fromJson(Map json) { + return SpendItem( + category: json['category'] as String, + amountCents: (json['amountCents'] as num).toInt(), + percentage: (json['percentage'] as num).toDouble(), + ); + } + + /// Role/category name. + final String category; + + /// Total spend in cents for this category. + final int amountCents; + + /// Percentage of total spend. + final double percentage; + + /// Serialises this [SpendItem] to a JSON map. + Map toJson() { + return { + 'category': category, + 'amountCents': amountCents, + 'percentage': percentage, + }; + } + + @override + List get props => [category, amountCents, percentage]; +} diff --git a/apps/mobile/packages/domain/lib/src/entities/financial/staff_payment.dart b/apps/mobile/packages/domain/lib/src/entities/financial/staff_payment.dart index 75cd8d8e..3df1b383 100644 --- a/apps/mobile/packages/domain/lib/src/entities/financial/staff_payment.dart +++ b/apps/mobile/packages/domain/lib/src/entities/financial/staff_payment.dart @@ -1,76 +1,88 @@ import 'package:equatable/equatable.dart'; -/// Status of a staff payout. -enum PaymentStatus { - /// Payout calculated but not processed. - pending, +import 'package:krow_domain/src/entities/enums/payment_status.dart'; - /// Submitted to banking provider. - processing, - - /// Successfully transferred to staff. - paid, - - /// Transfer failed. - failed, - - /// Status unknown. - unknown, -} - -/// Represents a payout to a [Staff] member for a completed [Assignment]. -class StaffPayment extends Equatable { - - const StaffPayment({ - required this.id, - required this.staffId, - required this.assignmentId, - required this.amount, +/// A single payment record for a staff member. +/// +/// Returned by `GET /staff/payments/history`. +class PaymentRecord extends Equatable { + /// Creates a [PaymentRecord] instance. + const PaymentRecord({ + required this.paymentId, + required this.amountCents, + required this.date, required this.status, - this.paidAt, - this.shiftTitle, - this.shiftLocation, - this.locationAddress, - this.hoursWorked, - this.hourlyRate, - this.workedTime, + this.shiftName, + this.location, + this.hourlyRateCents, + this.minutesWorked, }); + + /// Deserialises a [PaymentRecord] from a V2 API JSON map. + factory PaymentRecord.fromJson(Map json) { + return PaymentRecord( + paymentId: json['paymentId'] as String, + amountCents: (json['amountCents'] as num).toInt(), + date: DateTime.parse(json['date'] as String), + status: PaymentStatus.fromJson(json['status'] as String?), + shiftName: json['shiftName'] as String?, + location: json['location'] as String?, + hourlyRateCents: json['hourlyRateCents'] != null + ? (json['hourlyRateCents'] as num).toInt() + : null, + minutesWorked: json['minutesWorked'] != null + ? (json['minutesWorked'] as num).toInt() + : null, + ); + } + /// Unique identifier. - final String id; + final String paymentId; - /// The recipient [Staff]. - final String staffId; + /// Payment amount in cents. + final int amountCents; - /// The [Assignment] being paid for. - final String assignmentId; + /// Date the payment was processed or created. + final DateTime date; - /// Amount to be paid. - final double amount; - - /// Processing status. + /// Payment processing status. final PaymentStatus status; - /// When the payment was successfully processed. - final DateTime? paidAt; + /// Title of the associated shift. + final String? shiftName; - /// Title of the shift worked. - final String? shiftTitle; + /// Location/hub name. + final String? location; - /// Location/hub name of the shift. - final String? shiftLocation; + /// Hourly pay rate in cents. + final int? hourlyRateCents; - /// Address of the shift location. - final String? locationAddress; + /// Total minutes worked for this payment. + final int? minutesWorked; - /// Number of hours worked. - final double? hoursWorked; - - /// Hourly rate for the shift. - final double? hourlyRate; - - /// Work session duration or status. - final String? workedTime; + /// Serialises this [PaymentRecord] to a JSON map. + Map toJson() { + return { + 'paymentId': paymentId, + 'amountCents': amountCents, + 'date': date.toIso8601String(), + 'status': status.toJson(), + 'shiftName': shiftName, + 'location': location, + 'hourlyRateCents': hourlyRateCents, + 'minutesWorked': minutesWorked, + }; + } @override - List get props => [id, staffId, assignmentId, amount, status, paidAt, shiftTitle, shiftLocation, locationAddress, hoursWorked, hourlyRate, workedTime]; -} \ No newline at end of file + List get props => [ + paymentId, + amountCents, + date, + status, + shiftName, + location, + hourlyRateCents, + minutesWorked, + ]; +} diff --git a/apps/mobile/packages/domain/lib/src/entities/financial/time_card.dart b/apps/mobile/packages/domain/lib/src/entities/financial/time_card.dart index bb70cdd7..a5d459ec 100644 --- a/apps/mobile/packages/domain/lib/src/entities/financial/time_card.dart +++ b/apps/mobile/packages/domain/lib/src/entities/financial/time_card.dart @@ -1,78 +1,88 @@ import 'package:equatable/equatable.dart'; -/// Status of a time card. -enum TimeCardStatus { - /// Waiting for approval or payment. - pending, - /// Approved by manager. - approved, - /// Payment has been issued. - paid, - /// Disputed by staff or client. - disputed; - - /// Whether the card is approved. - bool get isApproved => this == TimeCardStatus.approved; - /// Whether the card is paid. - bool get isPaid => this == TimeCardStatus.paid; - /// Whether the card is disputed. - bool get isDisputed => this == TimeCardStatus.disputed; - /// Whether the card is pending. - bool get isPending => this == TimeCardStatus.pending; -} - -/// Represents a time card for a staff member. -class TimeCard extends Equatable { - - /// Creates a [TimeCard]. - const TimeCard({ - required this.id, - required this.shiftTitle, - required this.clientName, +/// A single time-card entry for a completed shift. +/// +/// Returned by `GET /staff/profile/time-card`. +class TimeCardEntry extends Equatable { + /// Creates a [TimeCardEntry] instance. + const TimeCardEntry({ required this.date, - required this.startTime, - required this.endTime, - required this.totalHours, - required this.hourlyRate, - required this.totalPay, - required this.status, + required this.shiftName, this.location, + this.clockInAt, + this.clockOutAt, + required this.minutesWorked, + this.hourlyRateCents, + required this.totalPayCents, }); - /// Unique identifier of the time card (often matches Application ID). - final String id; - /// Title of the shift. - final String shiftTitle; - /// Name of the client business. - final String clientName; + + /// Deserialises a [TimeCardEntry] from a V2 API JSON map. + factory TimeCardEntry.fromJson(Map json) { + return TimeCardEntry( + date: DateTime.parse(json['date'] as String), + shiftName: json['shiftName'] as String, + location: json['location'] as String?, + clockInAt: json['clockInAt'] != null + ? DateTime.parse(json['clockInAt'] as String) + : null, + clockOutAt: json['clockOutAt'] != null + ? DateTime.parse(json['clockOutAt'] as String) + : null, + minutesWorked: (json['minutesWorked'] as num).toInt(), + hourlyRateCents: json['hourlyRateCents'] != null + ? (json['hourlyRateCents'] as num).toInt() + : null, + totalPayCents: (json['totalPayCents'] as num).toInt(), + ); + } + /// Date of the shift. final DateTime date; - /// Actual or scheduled start time. - final String startTime; - /// Actual or scheduled end time. - final String endTime; - /// Total hours worked. - final double totalHours; - /// Hourly pay rate. - final double hourlyRate; - /// Total pay amount. - final double totalPay; - /// Current status of the time card. - final TimeCardStatus status; - /// Location name. + + /// Title of the shift. + final String shiftName; + + /// Location/hub name. final String? location; + /// Clock-in timestamp. + final DateTime? clockInAt; + + /// Clock-out timestamp. + final DateTime? clockOutAt; + + /// Total minutes worked (regular + overtime). + final int minutesWorked; + + /// Hourly pay rate in cents. + final int? hourlyRateCents; + + /// Gross pay in cents. + final int totalPayCents; + + /// Serialises this [TimeCardEntry] to a JSON map. + Map toJson() { + return { + 'date': date.toIso8601String(), + 'shiftName': shiftName, + 'location': location, + 'clockInAt': clockInAt?.toIso8601String(), + 'clockOutAt': clockOutAt?.toIso8601String(), + 'minutesWorked': minutesWorked, + 'hourlyRateCents': hourlyRateCents, + 'totalPayCents': totalPayCents, + }; + } + @override List get props => [ - id, - shiftTitle, - clientName, date, - startTime, - endTime, - totalHours, - hourlyRate, - totalPay, - status, + shiftName, location, + clockInAt, + clockOutAt, + minutesWorked, + hourlyRateCents, + totalPayCents, ]; } diff --git a/apps/mobile/packages/domain/lib/src/entities/home/client_dashboard.dart b/apps/mobile/packages/domain/lib/src/entities/home/client_dashboard.dart new file mode 100644 index 00000000..94376019 --- /dev/null +++ b/apps/mobile/packages/domain/lib/src/entities/home/client_dashboard.dart @@ -0,0 +1,75 @@ +import 'package:equatable/equatable.dart'; + +import 'coverage_metrics.dart'; +import 'live_activity_metrics.dart'; +import 'spending_summary.dart'; + +/// Client dashboard data aggregating key business metrics. +/// +/// Returned by `GET /client/dashboard`. +class ClientDashboard extends Equatable { + /// Creates a [ClientDashboard] instance. + const ClientDashboard({ + required this.userName, + required this.businessName, + required this.businessId, + required this.spending, + required this.coverage, + required this.liveActivity, + }); + + /// Deserialises a [ClientDashboard] from a V2 API JSON map. + factory ClientDashboard.fromJson(Map json) { + return ClientDashboard( + userName: json['userName'] as String, + businessName: json['businessName'] as String, + businessId: json['businessId'] as String, + spending: + SpendingSummary.fromJson(json['spending'] as Map), + coverage: + CoverageMetrics.fromJson(json['coverage'] as Map), + liveActivity: LiveActivityMetrics.fromJson( + json['liveActivity'] as Map), + ); + } + + /// Display name of the logged-in user. + final String userName; + + /// Name of the business. + final String businessName; + + /// Business ID. + final String businessId; + + /// Spending summary. + final SpendingSummary spending; + + /// Today's coverage metrics. + final CoverageMetrics coverage; + + /// Live activity metrics. + final LiveActivityMetrics liveActivity; + + /// Serialises this [ClientDashboard] to a JSON map. + Map toJson() { + return { + 'userName': userName, + 'businessName': businessName, + 'businessId': businessId, + 'spending': spending.toJson(), + 'coverage': coverage.toJson(), + 'liveActivity': liveActivity.toJson(), + }; + } + + @override + List get props => [ + userName, + businessName, + businessId, + spending, + coverage, + liveActivity, + ]; +} diff --git a/apps/mobile/packages/domain/lib/src/entities/home/coverage_metrics.dart b/apps/mobile/packages/domain/lib/src/entities/home/coverage_metrics.dart new file mode 100644 index 00000000..43357b2d --- /dev/null +++ b/apps/mobile/packages/domain/lib/src/entities/home/coverage_metrics.dart @@ -0,0 +1,42 @@ +import 'package:equatable/equatable.dart'; + +/// Today's coverage metrics nested in [ClientDashboard]. +class CoverageMetrics extends Equatable { + /// Creates a [CoverageMetrics] instance. + const CoverageMetrics({ + required this.neededWorkersToday, + required this.filledWorkersToday, + required this.openPositionsToday, + }); + + /// Deserialises a [CoverageMetrics] from a V2 API JSON map. + factory CoverageMetrics.fromJson(Map json) { + return CoverageMetrics( + neededWorkersToday: (json['neededWorkersToday'] as num).toInt(), + filledWorkersToday: (json['filledWorkersToday'] as num).toInt(), + openPositionsToday: (json['openPositionsToday'] as num).toInt(), + ); + } + + /// Workers needed today. + final int neededWorkersToday; + + /// Workers filled today. + final int filledWorkersToday; + + /// Open (unfilled) positions today. + final int openPositionsToday; + + /// Serialises this [CoverageMetrics] to a JSON map. + Map toJson() { + return { + 'neededWorkersToday': neededWorkersToday, + 'filledWorkersToday': filledWorkersToday, + 'openPositionsToday': openPositionsToday, + }; + } + + @override + List get props => + [neededWorkersToday, filledWorkersToday, openPositionsToday]; +} diff --git a/apps/mobile/packages/domain/lib/src/entities/home/home_dashboard_data.dart b/apps/mobile/packages/domain/lib/src/entities/home/home_dashboard_data.dart deleted file mode 100644 index 681e7a22..00000000 --- a/apps/mobile/packages/domain/lib/src/entities/home/home_dashboard_data.dart +++ /dev/null @@ -1,45 +0,0 @@ -import 'package:equatable/equatable.dart'; - -/// Entity representing dashboard data for the home screen. -/// -/// This entity provides aggregated metrics such as spending and shift counts -/// for both the current week and the upcoming 7 days. -class HomeDashboardData extends Equatable { - - /// Creates a [HomeDashboardData] instance. - const HomeDashboardData({ - required this.weeklySpending, - required this.next7DaysSpending, - required this.weeklyShifts, - required this.next7DaysScheduled, - required this.totalNeeded, - required this.totalFilled, - }); - /// Total spending for the current week. - final double weeklySpending; - - /// Projected spending for the next 7 days. - final double next7DaysSpending; - - /// Total shifts scheduled for the current week. - final int weeklyShifts; - - /// Shifts scheduled for the next 7 days. - final int next7DaysScheduled; - - /// Total workers needed for today's shifts. - final int totalNeeded; - - /// Total workers filled for today's shifts. - final int totalFilled; - - @override - List get props => [ - weeklySpending, - next7DaysSpending, - weeklyShifts, - next7DaysScheduled, - totalNeeded, - totalFilled, - ]; -} diff --git a/apps/mobile/packages/domain/lib/src/entities/home/live_activity_metrics.dart b/apps/mobile/packages/domain/lib/src/entities/home/live_activity_metrics.dart new file mode 100644 index 00000000..2f04b637 --- /dev/null +++ b/apps/mobile/packages/domain/lib/src/entities/home/live_activity_metrics.dart @@ -0,0 +1,43 @@ +import 'package:equatable/equatable.dart'; + +/// Live activity metrics nested in [ClientDashboard]. +class LiveActivityMetrics extends Equatable { + /// Creates a [LiveActivityMetrics] instance. + const LiveActivityMetrics({ + required this.lateWorkersToday, + required this.checkedInWorkersToday, + required this.averageShiftCostCents, + }); + + /// Deserialises a [LiveActivityMetrics] from a V2 API JSON map. + factory LiveActivityMetrics.fromJson(Map json) { + return LiveActivityMetrics( + lateWorkersToday: (json['lateWorkersToday'] as num).toInt(), + checkedInWorkersToday: (json['checkedInWorkersToday'] as num).toInt(), + averageShiftCostCents: + (json['averageShiftCostCents'] as num).toInt(), + ); + } + + /// Workers marked late/no-show today. + final int lateWorkersToday; + + /// Workers who have checked in today. + final int checkedInWorkersToday; + + /// Average shift cost in cents. + final int averageShiftCostCents; + + /// Serialises this [LiveActivityMetrics] to a JSON map. + Map toJson() { + return { + 'lateWorkersToday': lateWorkersToday, + 'checkedInWorkersToday': checkedInWorkersToday, + 'averageShiftCostCents': averageShiftCostCents, + }; + } + + @override + List get props => + [lateWorkersToday, checkedInWorkersToday, averageShiftCostCents]; +} diff --git a/apps/mobile/packages/domain/lib/src/entities/home/reorder_item.dart b/apps/mobile/packages/domain/lib/src/entities/home/reorder_item.dart deleted file mode 100644 index d13a80bd..00000000 --- a/apps/mobile/packages/domain/lib/src/entities/home/reorder_item.dart +++ /dev/null @@ -1,51 +0,0 @@ -import 'package:equatable/equatable.dart'; - -/// Summary of a completed order used for reorder suggestions. -class ReorderItem extends Equatable { - const ReorderItem({ - required this.orderId, - required this.title, - required this.location, - required this.totalCost, - required this.workers, - required this.type, - this.hourlyRate = 0, - this.hours = 0, - }); - - /// Unique identifier of the order. - final String orderId; - - /// Display title of the order (e.g., event name or first shift title). - final String title; - - /// Location of the order (e.g., first shift location). - final String location; - - /// Total calculated cost for the order. - final double totalCost; - - /// Total number of workers required for the order. - final int workers; - - /// The type of order (e.g., ONE_TIME, RECURRING). - final String type; - - /// Average or primary hourly rate (optional, for display). - final double hourlyRate; - - /// Total hours for the order (optional, for display). - final double hours; - - @override - List get props => [ - orderId, - title, - location, - totalCost, - workers, - type, - hourlyRate, - hours, - ]; -} diff --git a/apps/mobile/packages/domain/lib/src/entities/home/spending_summary.dart b/apps/mobile/packages/domain/lib/src/entities/home/spending_summary.dart new file mode 100644 index 00000000..407e8578 --- /dev/null +++ b/apps/mobile/packages/domain/lib/src/entities/home/spending_summary.dart @@ -0,0 +1,37 @@ +import 'package:equatable/equatable.dart'; + +/// Spending summary nested in [ClientDashboard]. +class SpendingSummary extends Equatable { + /// Creates a [SpendingSummary] instance. + const SpendingSummary({ + required this.weeklySpendCents, + required this.projectedNext7DaysCents, + }); + + /// Deserialises a [SpendingSummary] from a V2 API JSON map. + factory SpendingSummary.fromJson(Map json) { + return SpendingSummary( + weeklySpendCents: (json['weeklySpendCents'] as num).toInt(), + projectedNext7DaysCents: + (json['projectedNext7DaysCents'] as num).toInt(), + ); + } + + /// Total spend this week in cents. + final int weeklySpendCents; + + /// Projected spend for the next 7 days in cents. + final int projectedNext7DaysCents; + + /// Serialises this [SpendingSummary] to a JSON map. + Map toJson() { + return { + 'weeklySpendCents': weeklySpendCents, + 'projectedNext7DaysCents': projectedNext7DaysCents, + }; + } + + @override + List get props => + [weeklySpendCents, projectedNext7DaysCents]; +} diff --git a/apps/mobile/packages/domain/lib/src/entities/home/staff_dashboard.dart b/apps/mobile/packages/domain/lib/src/entities/home/staff_dashboard.dart new file mode 100644 index 00000000..c299a3ac --- /dev/null +++ b/apps/mobile/packages/domain/lib/src/entities/home/staff_dashboard.dart @@ -0,0 +1,80 @@ +import 'package:equatable/equatable.dart'; + +import '../benefits/benefit.dart'; + +/// Staff dashboard data with shifts and benefits overview. +/// +/// Returned by `GET /staff/dashboard`. +class StaffDashboard extends Equatable { + /// Creates a [StaffDashboard] instance. + const StaffDashboard({ + required this.staffName, + this.todaysShifts = const >[], + this.tomorrowsShifts = const >[], + this.recommendedShifts = const >[], + this.benefits = const [], + }); + + /// Deserialises a [StaffDashboard] from a V2 API JSON map. + factory StaffDashboard.fromJson(Map json) { + final dynamic benefitsRaw = json['benefits']; + final List benefitsList = benefitsRaw is List + ? benefitsRaw + .map((dynamic e) => Benefit.fromJson(e as Map)) + .toList() + : const []; + + return StaffDashboard( + staffName: json['staffName'] as String, + todaysShifts: _castShiftList(json['todaysShifts']), + tomorrowsShifts: _castShiftList(json['tomorrowsShifts']), + recommendedShifts: _castShiftList(json['recommendedShifts']), + benefits: benefitsList, + ); + } + + /// Display name of the staff member. + final String staffName; + + /// Shifts assigned for today. + final List> todaysShifts; + + /// Shifts assigned for tomorrow. + final List> tomorrowsShifts; + + /// Recommended open shifts. + final List> recommendedShifts; + + /// Active benefits. + final List benefits; + + /// Serialises this [StaffDashboard] to a JSON map. + Map toJson() { + return { + 'staffName': staffName, + 'todaysShifts': todaysShifts, + 'tomorrowsShifts': tomorrowsShifts, + 'recommendedShifts': recommendedShifts, + 'benefits': benefits.map((Benefit b) => b.toJson()).toList(), + }; + } + + static List> _castShiftList(dynamic raw) { + if (raw is List) { + return raw + .map((dynamic e) => + Map.from(e as Map)) + .toList(); + } + return const >[]; + } + + @override + List get props => [ + staffName, + todaysShifts, + tomorrowsShifts, + recommendedShifts, + benefits, + ]; +} diff --git a/apps/mobile/packages/domain/lib/src/entities/orders/assigned_worker_summary.dart b/apps/mobile/packages/domain/lib/src/entities/orders/assigned_worker_summary.dart new file mode 100644 index 00000000..38fbf730 --- /dev/null +++ b/apps/mobile/packages/domain/lib/src/entities/orders/assigned_worker_summary.dart @@ -0,0 +1,58 @@ +import 'package:equatable/equatable.dart'; + +import 'package:krow_domain/src/entities/enums/application_status.dart'; + +/// Summary of a worker assigned to an order line item. +/// +/// Nested within [OrderItem]. +class AssignedWorkerSummary extends Equatable { + /// Creates an [AssignedWorkerSummary] instance. + const AssignedWorkerSummary({ + this.applicationId, + this.workerName, + this.role, + this.confirmationStatus, + }); + + /// Deserialises an [AssignedWorkerSummary] from a V2 API JSON map. + factory AssignedWorkerSummary.fromJson(Map json) { + return AssignedWorkerSummary( + applicationId: json['applicationId'] as String?, + workerName: json['workerName'] as String?, + role: json['role'] as String?, + confirmationStatus: json['confirmationStatus'] != null + ? ApplicationStatus.fromJson(json['confirmationStatus'] as String?) + : null, + ); + } + + /// Application ID for this worker assignment. + final String? applicationId; + + /// Display name of the worker. + final String? workerName; + + /// Role the worker is assigned to. + final String? role; + + /// Confirmation status of the assignment. + final ApplicationStatus? confirmationStatus; + + /// Serialises this [AssignedWorkerSummary] to a JSON map. + Map toJson() { + return { + 'applicationId': applicationId, + 'workerName': workerName, + 'role': role, + 'confirmationStatus': confirmationStatus?.toJson(), + }; + } + + @override + List get props => [ + applicationId, + workerName, + role, + confirmationStatus, + ]; +} diff --git a/apps/mobile/packages/domain/lib/src/entities/orders/one_time_order.dart b/apps/mobile/packages/domain/lib/src/entities/orders/one_time_order.dart deleted file mode 100644 index fe50bd20..00000000 --- a/apps/mobile/packages/domain/lib/src/entities/orders/one_time_order.dart +++ /dev/null @@ -1,98 +0,0 @@ -import 'package:equatable/equatable.dart'; -import 'one_time_order_position.dart'; - -/// Represents a customer's request for a single event or shift. -/// -/// Encapsulates the date, primary location, and a list of specific [OneTimeOrderPosition] requirements. -class OneTimeOrder extends Equatable { - - const OneTimeOrder({ - required this.date, - required this.location, - required this.positions, - this.hub, - this.eventName, - this.vendorId, - this.hubManagerId, - this.roleRates = const {}, - }); - /// The specific date for the shift or event. - final DateTime date; - - /// The primary location where the work will take place. - final String location; - - /// The list of positions and headcounts required for this order. - final List positions; - - /// Selected hub details for this order. - final OneTimeOrderHubDetails? hub; - - /// Optional order name. - final String? eventName; - - /// Selected vendor id for this order. - final String? vendorId; - - /// Optional hub manager id. - final String? hubManagerId; - - /// Role hourly rates keyed by role id. - final Map roleRates; - - @override - List get props => [ - date, - location, - positions, - hub, - eventName, - vendorId, - hubManagerId, - roleRates, - ]; -} - -/// Minimal hub details used during order creation. -class OneTimeOrderHubDetails extends Equatable { - const OneTimeOrderHubDetails({ - required this.id, - required this.name, - required this.address, - this.placeId, - this.latitude, - this.longitude, - this.city, - this.state, - this.street, - this.country, - this.zipCode, - }); - - final String id; - final String name; - final String address; - final String? placeId; - final double? latitude; - final double? longitude; - final String? city; - final String? state; - final String? street; - final String? country; - final String? zipCode; - - @override - List get props => [ - id, - name, - address, - placeId, - latitude, - longitude, - city, - state, - street, - country, - zipCode, - ]; -} diff --git a/apps/mobile/packages/domain/lib/src/entities/orders/one_time_order_position.dart b/apps/mobile/packages/domain/lib/src/entities/orders/one_time_order_position.dart deleted file mode 100644 index 8e9776be..00000000 --- a/apps/mobile/packages/domain/lib/src/entities/orders/one_time_order_position.dart +++ /dev/null @@ -1,62 +0,0 @@ -import 'package:equatable/equatable.dart'; - -/// Represents a specific position requirement within a [OneTimeOrder]. -/// -/// Defines the role, headcount, and scheduling details for a single staffing requirement. -class OneTimeOrderPosition extends Equatable { - - const OneTimeOrderPosition({ - required this.role, - required this.count, - required this.startTime, - required this.endTime, - this.lunchBreak = 'NO_BREAK', - this.location, - }); - /// The job role or title required. - final String role; - - /// The number of workers required for this position. - final int count; - - /// The scheduled start time (e.g., "09:00 AM"). - final String startTime; - - /// The scheduled end time (e.g., "05:00 PM"). - final String endTime; - - /// The break duration enum value (e.g., NO_BREAK, MIN_15, MIN_30). - final String lunchBreak; - - /// Optional specific location for this position, if different from the order's main location. - final String? location; - - @override - List get props => [ - role, - count, - startTime, - endTime, - lunchBreak, - location, - ]; - - /// Creates a copy of this position with the given fields replaced. - OneTimeOrderPosition copyWith({ - String? role, - int? count, - String? startTime, - String? endTime, - String? lunchBreak, - String? location, - }) { - return OneTimeOrderPosition( - role: role ?? this.role, - count: count ?? this.count, - startTime: startTime ?? this.startTime, - endTime: endTime ?? this.endTime, - lunchBreak: lunchBreak ?? this.lunchBreak, - location: location ?? this.location, - ); - } -} diff --git a/apps/mobile/packages/domain/lib/src/entities/orders/order_item.dart b/apps/mobile/packages/domain/lib/src/entities/orders/order_item.dart index 88ae8091..473495ca 100644 --- a/apps/mobile/packages/domain/lib/src/entities/orders/order_item.dart +++ b/apps/mobile/packages/domain/lib/src/entities/orders/order_item.dart @@ -1,117 +1,137 @@ import 'package:equatable/equatable.dart'; -import 'order_type.dart'; +import 'package:krow_domain/src/entities/enums/order_type.dart'; +import 'package:krow_domain/src/entities/enums/shift_status.dart'; -/// Represents a customer's view of an order or shift. +import 'assigned_worker_summary.dart'; + +/// A line item within an order, representing a role needed for a shift. /// -/// This entity captures the details necessary for the dashboard/view orders screen, -/// including status and worker assignments. +/// Returned by `GET /client/orders/view`. class OrderItem extends Equatable { - /// Creates an [OrderItem]. + /// Creates an [OrderItem] instance. const OrderItem({ - required this.id, + required this.itemId, required this.orderId, required this.orderType, - required this.title, - required this.clientName, - required this.status, + required this.roleName, required this.date, - required this.startTime, - required this.endTime, - required this.location, - required this.locationAddress, - required this.filled, - required this.workersNeeded, - required this.hourlyRate, - required this.eventName, - this.hours = 0, - this.totalValue = 0, - this.confirmedApps = const >[], - this.hubManagerId, - this.hubManagerName, + required this.startsAt, + required this.endsAt, + required this.requiredWorkerCount, + required this.filledCount, + required this.hourlyRateCents, + required this.totalCostCents, + this.locationName, + required this.status, + this.workers = const [], }); - /// Unique identifier of the order. - final String id; + /// Deserialises an [OrderItem] from a V2 API JSON map. + factory OrderItem.fromJson(Map json) { + final dynamic workersRaw = json['workers']; + final List workersList = workersRaw is List + ? workersRaw + .map((dynamic e) => AssignedWorkerSummary.fromJson( + e as Map)) + .toList() + : const []; - /// Parent order identifier. + return OrderItem( + itemId: json['itemId'] as String, + orderId: json['orderId'] as String, + orderType: OrderType.fromJson(json['orderType'] as String?), + roleName: json['roleName'] as String, + date: DateTime.parse(json['date'] as String), + startsAt: DateTime.parse(json['startsAt'] as String), + endsAt: DateTime.parse(json['endsAt'] as String), + requiredWorkerCount: (json['requiredWorkerCount'] as num).toInt(), + filledCount: (json['filledCount'] as num).toInt(), + hourlyRateCents: (json['hourlyRateCents'] as num).toInt(), + totalCostCents: (json['totalCostCents'] as num).toInt(), + locationName: json['locationName'] as String?, + status: ShiftStatus.fromJson(json['status'] as String?), + workers: workersList, + ); + } + + /// Shift-role ID (primary key). + final String itemId; + + /// Parent order ID. final String orderId; - /// The type of order (e.g., ONE_TIME, PERMANENT). + /// Order type (ONE_TIME, RECURRING, PERMANENT, RAPID). final OrderType orderType; - /// Title or name of the role. - final String title; + /// Name of the role. + final String roleName; - /// Name of the client company. - final String clientName; + /// Shift date. + final DateTime date; - /// status of the order (e.g., 'open', 'filled', 'completed'). - final String status; + /// Shift start time. + final DateTime startsAt; - /// Date of the shift (ISO format). - final String date; + /// Shift end time. + final DateTime endsAt; - /// Start time of the shift. - final String startTime; + /// Total workers required. + final int requiredWorkerCount; - /// End time of the shift. - final String endTime; + /// Workers currently assigned/filled. + final int filledCount; - /// Location name. - final String location; + /// Billing rate in cents per hour. + final int hourlyRateCents; - /// Full address of the location. - final String locationAddress; + /// Total cost in cents. + final int totalCostCents; - /// Number of workers currently filled. - final int filled; + /// Location/hub name. + final String? locationName; - /// Total number of workers required. - final int workersNeeded; + /// Shift status. + final ShiftStatus status; - /// Hourly pay rate. - final double hourlyRate; + /// Assigned workers for this line item. + final List workers; - /// Total hours for the shift role. - final double hours; - - /// Total value for the shift role. - final double totalValue; - - /// Name of the event. - final String eventName; - - /// List of confirmed worker applications. - final List> confirmedApps; - - /// Optional ID of the assigned hub manager. - final String? hubManagerId; - - /// Optional Name of the assigned hub manager. - final String? hubManagerName; + /// Serialises this [OrderItem] to a JSON map. + Map toJson() { + return { + 'itemId': itemId, + 'orderId': orderId, + 'orderType': orderType.toJson(), + 'roleName': roleName, + 'date': date.toIso8601String(), + 'startsAt': startsAt.toIso8601String(), + 'endsAt': endsAt.toIso8601String(), + 'requiredWorkerCount': requiredWorkerCount, + 'filledCount': filledCount, + 'hourlyRateCents': hourlyRateCents, + 'totalCostCents': totalCostCents, + 'locationName': locationName, + 'status': status.toJson(), + 'workers': workers.map((AssignedWorkerSummary w) => w.toJson()).toList(), + }; + } @override List get props => [ - id, - orderId, - orderType, - title, - clientName, - status, - date, - startTime, - endTime, - location, - locationAddress, - filled, - workersNeeded, - hourlyRate, - hours, - totalValue, - eventName, - confirmedApps, - hubManagerId, - hubManagerName, - ]; + itemId, + orderId, + orderType, + roleName, + date, + startsAt, + endsAt, + requiredWorkerCount, + filledCount, + hourlyRateCents, + totalCostCents, + locationName, + status, + workers, + ]; } diff --git a/apps/mobile/packages/domain/lib/src/entities/orders/order_preview.dart b/apps/mobile/packages/domain/lib/src/entities/orders/order_preview.dart new file mode 100644 index 00000000..3bc41648 --- /dev/null +++ b/apps/mobile/packages/domain/lib/src/entities/orders/order_preview.dart @@ -0,0 +1,235 @@ +import 'package:equatable/equatable.dart'; + +/// A preview of an order for reordering purposes. +/// +/// Returned by `GET /client/orders/:id/reorder-preview`. +class OrderPreview extends Equatable { + /// Creates an [OrderPreview] instance. + const OrderPreview({ + required this.orderId, + required this.title, + this.description, + this.startsAt, + this.endsAt, + this.locationName, + this.locationAddress, + this.metadata = const {}, + this.shifts = const [], + }); + + /// Deserialises an [OrderPreview] from a V2 API JSON map. + factory OrderPreview.fromJson(Map json) { + final dynamic shiftsRaw = json['shifts']; + final List shiftsList = shiftsRaw is List + ? shiftsRaw + .map((dynamic e) => + OrderPreviewShift.fromJson(e as Map)) + .toList() + : const []; + + return OrderPreview( + orderId: json['orderId'] as String, + title: json['title'] as String, + description: json['description'] as String?, + startsAt: json['startsAt'] != null + ? DateTime.parse(json['startsAt'] as String) + : null, + endsAt: json['endsAt'] != null + ? DateTime.parse(json['endsAt'] as String) + : null, + locationName: json['locationName'] as String?, + locationAddress: json['locationAddress'] as String?, + metadata: json['metadata'] is Map + ? Map.from(json['metadata'] as Map) + : const {}, + shifts: shiftsList, + ); + } + + /// Order ID. + final String orderId; + + /// Order title. + final String title; + + /// Order description. + final String? description; + + /// Order start time. + final DateTime? startsAt; + + /// Order end time. + final DateTime? endsAt; + + /// Location name. + final String? locationName; + + /// Location address. + final String? locationAddress; + + /// Flexible metadata bag. + final Map metadata; + + /// Shifts with their roles from the original order. + final List shifts; + + /// Serialises this [OrderPreview] to a JSON map. + Map toJson() { + return { + 'orderId': orderId, + 'title': title, + 'description': description, + 'startsAt': startsAt?.toIso8601String(), + 'endsAt': endsAt?.toIso8601String(), + 'locationName': locationName, + 'locationAddress': locationAddress, + 'metadata': metadata, + 'shifts': shifts.map((OrderPreviewShift s) => s.toJson()).toList(), + }; + } + + @override + List get props => [ + orderId, + title, + description, + startsAt, + endsAt, + locationName, + locationAddress, + metadata, + shifts, + ]; +} + +/// A shift within a reorder preview. +class OrderPreviewShift extends Equatable { + /// Creates an [OrderPreviewShift] instance. + const OrderPreviewShift({ + required this.shiftId, + required this.shiftCode, + required this.title, + required this.startsAt, + required this.endsAt, + this.roles = const [], + }); + + /// Deserialises an [OrderPreviewShift] from a V2 API JSON map. + factory OrderPreviewShift.fromJson(Map json) { + final dynamic rolesRaw = json['roles']; + final List rolesList = rolesRaw is List + ? rolesRaw + .map((dynamic e) => + OrderPreviewRole.fromJson(e as Map)) + .toList() + : const []; + + return OrderPreviewShift( + shiftId: json['shiftId'] as String, + shiftCode: json['shiftCode'] as String, + title: json['title'] as String, + startsAt: DateTime.parse(json['startsAt'] as String), + endsAt: DateTime.parse(json['endsAt'] as String), + roles: rolesList, + ); + } + + /// Shift ID. + final String shiftId; + + /// Shift code. + final String shiftCode; + + /// Shift title. + final String title; + + /// Shift start time. + final DateTime startsAt; + + /// Shift end time. + final DateTime endsAt; + + /// Roles in this shift. + final List roles; + + /// Serialises this [OrderPreviewShift] to a JSON map. + Map toJson() { + return { + 'shiftId': shiftId, + 'shiftCode': shiftCode, + 'title': title, + 'startsAt': startsAt.toIso8601String(), + 'endsAt': endsAt.toIso8601String(), + 'roles': roles.map((OrderPreviewRole r) => r.toJson()).toList(), + }; + } + + @override + List get props => + [shiftId, shiftCode, title, startsAt, endsAt, roles]; +} + +/// A role within a reorder preview shift. +class OrderPreviewRole extends Equatable { + /// Creates an [OrderPreviewRole] instance. + const OrderPreviewRole({ + required this.roleId, + required this.roleCode, + required this.roleName, + required this.workersNeeded, + required this.payRateCents, + required this.billRateCents, + }); + + /// Deserialises an [OrderPreviewRole] from a V2 API JSON map. + factory OrderPreviewRole.fromJson(Map json) { + return OrderPreviewRole( + roleId: json['roleId'] as String, + roleCode: json['roleCode'] as String, + roleName: json['roleName'] as String, + workersNeeded: (json['workersNeeded'] as num).toInt(), + payRateCents: (json['payRateCents'] as num).toInt(), + billRateCents: (json['billRateCents'] as num).toInt(), + ); + } + + /// Role ID. + final String roleId; + + /// Role code. + final String roleCode; + + /// Role name. + final String roleName; + + /// Workers needed for this role. + final int workersNeeded; + + /// Pay rate in cents per hour. + final int payRateCents; + + /// Bill rate in cents per hour. + final int billRateCents; + + /// Serialises this [OrderPreviewRole] to a JSON map. + Map toJson() { + return { + 'roleId': roleId, + 'roleCode': roleCode, + 'roleName': roleName, + 'workersNeeded': workersNeeded, + 'payRateCents': payRateCents, + 'billRateCents': billRateCents, + }; + } + + @override + List get props => [ + roleId, + roleCode, + roleName, + workersNeeded, + payRateCents, + billRateCents, + ]; +} diff --git a/apps/mobile/packages/domain/lib/src/entities/orders/order_type.dart b/apps/mobile/packages/domain/lib/src/entities/orders/order_type.dart deleted file mode 100644 index f4385b5b..00000000 --- a/apps/mobile/packages/domain/lib/src/entities/orders/order_type.dart +++ /dev/null @@ -1,30 +0,0 @@ -/// Defines the type of an order. -enum OrderType { - /// A single occurrence shift. - oneTime, - - /// A long-term or permanent staffing position. - permanent, - - /// Shifts that repeat on a defined schedule. - recurring, - - /// A quickly created shift. - rapid; - - /// Creates an [OrderType] from a string value (typically from the backend). - static OrderType fromString(String value) { - switch (value.toUpperCase()) { - case 'ONE_TIME': - return OrderType.oneTime; - case 'PERMANENT': - return OrderType.permanent; - case 'RECURRING': - return OrderType.recurring; - case 'RAPID': - return OrderType.rapid; - default: - return OrderType.oneTime; - } - } -} diff --git a/apps/mobile/packages/domain/lib/src/entities/orders/permanent_order.dart b/apps/mobile/packages/domain/lib/src/entities/orders/permanent_order.dart deleted file mode 100644 index ef950f87..00000000 --- a/apps/mobile/packages/domain/lib/src/entities/orders/permanent_order.dart +++ /dev/null @@ -1,41 +0,0 @@ -import 'package:equatable/equatable.dart'; -import 'one_time_order.dart'; -import 'one_time_order_position.dart'; - -/// Represents a customer's request for permanent/ongoing staffing. -class PermanentOrder extends Equatable { - const PermanentOrder({ - required this.startDate, - required this.permanentDays, - required this.positions, - this.hub, - this.eventName, - this.vendorId, - this.hubManagerId, - this.roleRates = const {}, - }); - - final DateTime startDate; - - /// List of days (e.g., ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday']) - final List permanentDays; - - final List positions; - final OneTimeOrderHubDetails? hub; - final String? eventName; - final String? vendorId; - final String? hubManagerId; - final Map roleRates; - - @override - List get props => [ - startDate, - permanentDays, - positions, - hub, - eventName, - vendorId, - hubManagerId, - roleRates, - ]; -} diff --git a/apps/mobile/packages/domain/lib/src/entities/orders/permanent_order_position.dart b/apps/mobile/packages/domain/lib/src/entities/orders/permanent_order_position.dart deleted file mode 100644 index fb4d1e1b..00000000 --- a/apps/mobile/packages/domain/lib/src/entities/orders/permanent_order_position.dart +++ /dev/null @@ -1,60 +0,0 @@ -import 'package:equatable/equatable.dart'; - -/// Represents a specific position requirement within a [PermanentOrder]. -class PermanentOrderPosition extends Equatable { - const PermanentOrderPosition({ - required this.role, - required this.count, - required this.startTime, - required this.endTime, - this.lunchBreak = 'NO_BREAK', - this.location, - }); - - /// The job role or title required. - final String role; - - /// The number of workers required for this position. - final int count; - - /// The scheduled start time (e.g., "09:00 AM"). - final String startTime; - - /// The scheduled end time (e.g., "05:00 PM"). - final String endTime; - - /// The break duration enum value (e.g., NO_BREAK, MIN_15, MIN_30). - final String lunchBreak; - - /// Optional specific location for this position, if different from the order's main location. - final String? location; - - @override - List get props => [ - role, - count, - startTime, - endTime, - lunchBreak, - location, - ]; - - /// Creates a copy of this position with the given fields replaced. - PermanentOrderPosition copyWith({ - String? role, - int? count, - String? startTime, - String? endTime, - String? lunchBreak, - String? location, - }) { - return PermanentOrderPosition( - role: role ?? this.role, - count: count ?? this.count, - startTime: startTime ?? this.startTime, - endTime: endTime ?? this.endTime, - lunchBreak: lunchBreak ?? this.lunchBreak, - location: location ?? this.location, - ); - } -} diff --git a/apps/mobile/packages/domain/lib/src/entities/orders/recent_order.dart b/apps/mobile/packages/domain/lib/src/entities/orders/recent_order.dart new file mode 100644 index 00000000..453a048b --- /dev/null +++ b/apps/mobile/packages/domain/lib/src/entities/orders/recent_order.dart @@ -0,0 +1,65 @@ +import 'package:equatable/equatable.dart'; + +import 'package:krow_domain/src/entities/enums/order_type.dart'; + +/// A recently completed order available for reordering. +/// +/// Returned by `GET /client/reorders`. +class RecentOrder extends Equatable { + /// Creates a [RecentOrder] instance. + const RecentOrder({ + required this.id, + required this.title, + this.date, + this.hubName, + required this.positionCount, + required this.orderType, + }); + + /// Deserialises a [RecentOrder] from a V2 API JSON map. + factory RecentOrder.fromJson(Map json) { + return RecentOrder( + id: json['id'] as String, + title: json['title'] as String, + date: json['date'] != null + ? DateTime.parse(json['date'] as String) + : null, + hubName: json['hubName'] as String?, + positionCount: (json['positionCount'] as num).toInt(), + orderType: OrderType.fromJson(json['orderType'] as String?), + ); + } + + /// Order ID. + final String id; + + /// Order title. + final String title; + + /// Order date. + final DateTime? date; + + /// Hub/location name. + final String? hubName; + + /// Number of positions in the order. + final int positionCount; + + /// Type of order (ONE_TIME, RECURRING, PERMANENT, RAPID). + final OrderType orderType; + + /// Serialises this [RecentOrder] to a JSON map. + Map toJson() { + return { + 'id': id, + 'title': title, + 'date': date?.toIso8601String(), + 'hubName': hubName, + 'positionCount': positionCount, + 'orderType': orderType.toJson(), + }; + } + + @override + List get props => [id, title, date, hubName, positionCount, orderType]; +} diff --git a/apps/mobile/packages/domain/lib/src/entities/orders/recurring_order.dart b/apps/mobile/packages/domain/lib/src/entities/orders/recurring_order.dart deleted file mode 100644 index 76f00720..00000000 --- a/apps/mobile/packages/domain/lib/src/entities/orders/recurring_order.dart +++ /dev/null @@ -1,106 +0,0 @@ -import 'package:equatable/equatable.dart'; -import 'recurring_order_position.dart'; - -/// Represents a recurring staffing request spanning a date range. -class RecurringOrder extends Equatable { - const RecurringOrder({ - required this.startDate, - required this.endDate, - required this.recurringDays, - required this.location, - required this.positions, - this.hub, - this.eventName, - this.vendorId, - this.hubManagerId, - this.roleRates = const {}, - }); - - /// Start date for the recurring schedule. - final DateTime startDate; - - /// End date for the recurring schedule. - final DateTime endDate; - - /// Days of the week to repeat on (e.g., ["S", "M", ...]). - final List recurringDays; - - /// The primary location where the work will take place. - final String location; - - /// The list of positions and headcounts required for this order. - final List positions; - - /// Selected hub details for this order. - final RecurringOrderHubDetails? hub; - - /// Optional order name. - final String? eventName; - - /// Selected vendor id for this order. - final String? vendorId; - - /// Optional hub manager id. - final String? hubManagerId; - - /// Role hourly rates keyed by role id. - final Map roleRates; - - @override - List get props => [ - startDate, - endDate, - recurringDays, - location, - positions, - hub, - eventName, - vendorId, - hubManagerId, - roleRates, - ]; -} - -/// Minimal hub details used during recurring order creation. -class RecurringOrderHubDetails extends Equatable { - const RecurringOrderHubDetails({ - required this.id, - required this.name, - required this.address, - this.placeId, - this.latitude, - this.longitude, - this.city, - this.state, - this.street, - this.country, - this.zipCode, - }); - - final String id; - final String name; - final String address; - final String? placeId; - final double? latitude; - final double? longitude; - final String? city; - final String? state; - final String? street; - final String? country; - final String? zipCode; - - @override - List get props => [ - id, - name, - address, - placeId, - latitude, - longitude, - city, - state, - street, - country, - zipCode, - ]; -} diff --git a/apps/mobile/packages/domain/lib/src/entities/orders/recurring_order_position.dart b/apps/mobile/packages/domain/lib/src/entities/orders/recurring_order_position.dart deleted file mode 100644 index 9fdc2161..00000000 --- a/apps/mobile/packages/domain/lib/src/entities/orders/recurring_order_position.dart +++ /dev/null @@ -1,60 +0,0 @@ -import 'package:equatable/equatable.dart'; - -/// Represents a specific position requirement within a [RecurringOrder]. -class RecurringOrderPosition extends Equatable { - const RecurringOrderPosition({ - required this.role, - required this.count, - required this.startTime, - required this.endTime, - this.lunchBreak = 'NO_BREAK', - this.location, - }); - - /// The job role or title required. - final String role; - - /// The number of workers required for this position. - final int count; - - /// The scheduled start time (e.g., "09:00 AM"). - final String startTime; - - /// The scheduled end time (e.g., "05:00 PM"). - final String endTime; - - /// The break duration enum value (e.g., NO_BREAK, MIN_15, MIN_30). - final String lunchBreak; - - /// Optional specific location for this position, if different from the order's main location. - final String? location; - - @override - List get props => [ - role, - count, - startTime, - endTime, - lunchBreak, - location, - ]; - - /// Creates a copy of this position with the given fields replaced. - RecurringOrderPosition copyWith({ - String? role, - int? count, - String? startTime, - String? endTime, - String? lunchBreak, - String? location, - }) { - return RecurringOrderPosition( - role: role ?? this.role, - count: count ?? this.count, - startTime: startTime ?? this.startTime, - endTime: endTime ?? this.endTime, - lunchBreak: lunchBreak ?? this.lunchBreak, - location: location ?? this.location, - ); - } -} diff --git a/apps/mobile/packages/domain/lib/src/entities/orders/reorder_data.dart b/apps/mobile/packages/domain/lib/src/entities/orders/reorder_data.dart deleted file mode 100644 index 2f325d3a..00000000 --- a/apps/mobile/packages/domain/lib/src/entities/orders/reorder_data.dart +++ /dev/null @@ -1,76 +0,0 @@ -import 'package:equatable/equatable.dart'; -import 'one_time_order.dart'; -import 'order_type.dart'; - -/// Represents the full details of an order retrieved for reordering. -class ReorderData extends Equatable { - const ReorderData({ - required this.orderId, - required this.orderType, - required this.eventName, - required this.vendorId, - required this.hub, - required this.positions, - this.date, - this.startDate, - this.endDate, - this.recurringDays = const [], - this.permanentDays = const [], - }); - - final String orderId; - final OrderType orderType; - final String eventName; - final String? vendorId; - final OneTimeOrderHubDetails hub; - final List positions; - - // One-time specific - final DateTime? date; - - // Recurring/Permanent specific - final DateTime? startDate; - final DateTime? endDate; - final List recurringDays; - final List permanentDays; - - @override - List get props => [ - orderId, - orderType, - eventName, - vendorId, - hub, - positions, - date, - startDate, - endDate, - recurringDays, - permanentDays, - ]; -} - -class ReorderPosition extends Equatable { - const ReorderPosition({ - required this.roleId, - required this.count, - required this.startTime, - required this.endTime, - this.lunchBreak = 'NO_BREAK', - }); - - final String roleId; - final int count; - final String startTime; - final String endTime; - final String lunchBreak; - - @override - List get props => [ - roleId, - count, - startTime, - endTime, - lunchBreak, - ]; -} diff --git a/apps/mobile/packages/domain/lib/src/entities/profile/attire_checklist.dart b/apps/mobile/packages/domain/lib/src/entities/profile/attire_checklist.dart new file mode 100644 index 00000000..f779576e --- /dev/null +++ b/apps/mobile/packages/domain/lib/src/entities/profile/attire_checklist.dart @@ -0,0 +1,138 @@ +import 'package:equatable/equatable.dart'; + +/// Status of an attire checklist item. +enum AttireItemStatus { + /// Photo has not been uploaded yet. + notUploaded, + + /// Upload is pending review. + pending, + + /// Photo has been verified/approved. + verified, + + /// Photo was rejected. + rejected, + + /// Document has expired. + expired, +} + +/// An attire checklist item for a staff member. +/// +/// Returned by `GET /staff/profile/attire`. Joins the `documents` catalog +/// (filtered to ATTIRE type) with the staff-specific `staff_documents` record. +class AttireChecklist extends Equatable { + /// Creates an [AttireChecklist] instance. + const AttireChecklist({ + required this.documentId, + required this.name, + this.description = '', + this.mandatory = true, + this.staffDocumentId, + this.photoUri, + required this.status, + this.verificationStatus, + }); + + /// Deserialises an [AttireChecklist] from the V2 API JSON response. + factory AttireChecklist.fromJson(Map json) { + return AttireChecklist( + documentId: json['documentId'] as String, + name: json['name'] as String, + description: json['description'] as String? ?? '', + mandatory: json['mandatory'] as bool? ?? true, + staffDocumentId: json['staffDocumentId'] as String?, + photoUri: json['photoUri'] as String?, + status: _parseStatus(json['status'] as String?), + verificationStatus: json['verificationStatus'] as String?, + ); + } + + /// Catalog document definition ID (UUID). + final String documentId; + + /// Human-readable attire item name. + final String name; + + /// Description of the attire requirement. + final String description; + + /// Whether this attire item is mandatory. + final bool mandatory; + + /// Staff-specific document record ID, or null if not uploaded. + final String? staffDocumentId; + + /// URI to the uploaded attire photo. + final String? photoUri; + + /// Current status of the attire item. + final AttireItemStatus status; + + /// Detailed verification status string (from metadata). + final String? verificationStatus; + + /// Whether a photo has been uploaded. + bool get isUploaded => staffDocumentId != null; + + /// Serialises this [AttireChecklist] to a JSON map. + Map toJson() { + return { + 'documentId': documentId, + 'name': name, + 'description': description, + 'mandatory': mandatory, + 'staffDocumentId': staffDocumentId, + 'photoUri': photoUri, + 'status': _statusToString(status), + 'verificationStatus': verificationStatus, + }; + } + + @override + List get props => [ + documentId, + name, + description, + mandatory, + staffDocumentId, + photoUri, + status, + verificationStatus, + ]; + + /// Parses a status string from the API. + static AttireItemStatus _parseStatus(String? value) { + switch (value?.toUpperCase()) { + case 'NOT_UPLOADED': + return AttireItemStatus.notUploaded; + case 'PENDING': + return AttireItemStatus.pending; + case 'VERIFIED': + return AttireItemStatus.verified; + case 'REJECTED': + return AttireItemStatus.rejected; + case 'EXPIRED': + return AttireItemStatus.expired; + default: + return AttireItemStatus.notUploaded; + } + } + + /// Converts an [AttireItemStatus] to its API string. + static String _statusToString(AttireItemStatus status) { + switch (status) { + case AttireItemStatus.notUploaded: + return 'NOT_UPLOADED'; + case AttireItemStatus.pending: + return 'PENDING'; + case AttireItemStatus.verified: + return 'VERIFIED'; + case AttireItemStatus.rejected: + return 'REJECTED'; + case AttireItemStatus.expired: + return 'EXPIRED'; + } + } +} diff --git a/apps/mobile/packages/domain/lib/src/entities/profile/attire_item.dart b/apps/mobile/packages/domain/lib/src/entities/profile/attire_item.dart deleted file mode 100644 index d794ca9e..00000000 --- a/apps/mobile/packages/domain/lib/src/entities/profile/attire_item.dart +++ /dev/null @@ -1,86 +0,0 @@ -import 'package:equatable/equatable.dart'; - -import 'attire_verification_status.dart'; - -/// Represents an attire item that a staff member might need or possess. -/// -/// Attire items are specific clothing or equipment required for jobs. -class AttireItem extends Equatable { - /// Creates an [AttireItem]. - const AttireItem({ - required this.id, - required this.code, - required this.label, - this.description, - this.imageUrl, - this.isMandatory = false, - this.verificationStatus, - this.photoUrl, - this.verificationId, - }); - - /// Unique identifier of the attire item (UUID). - final String id; - - /// String code for the attire item (e.g. BLACK_TSHIRT). - final String code; - - /// Display name of the item. - final String label; - - /// Optional description for the attire item. - final String? description; - - /// URL of the reference image. - final String? imageUrl; - - /// Whether this item is mandatory for onboarding. - final bool isMandatory; - - /// The current verification status of the uploaded photo. - final AttireVerificationStatus? verificationStatus; - - /// The URL of the photo uploaded by the staff member. - final String? photoUrl; - - /// The ID of the verification record. - final String? verificationId; - - @override - List get props => [ - id, - code, - label, - description, - imageUrl, - isMandatory, - verificationStatus, - photoUrl, - verificationId, - ]; - - /// Creates a copy of this [AttireItem] with the given fields replaced. - AttireItem copyWith({ - String? id, - String? code, - String? label, - String? description, - String? imageUrl, - bool? isMandatory, - AttireVerificationStatus? verificationStatus, - String? photoUrl, - String? verificationId, - }) { - return AttireItem( - id: id ?? this.id, - code: code ?? this.code, - label: label ?? this.label, - description: description ?? this.description, - imageUrl: imageUrl ?? this.imageUrl, - isMandatory: isMandatory ?? this.isMandatory, - verificationStatus: verificationStatus ?? this.verificationStatus, - photoUrl: photoUrl ?? this.photoUrl, - verificationId: verificationId ?? this.verificationId, - ); - } -} diff --git a/apps/mobile/packages/domain/lib/src/entities/profile/attire_verification_status.dart b/apps/mobile/packages/domain/lib/src/entities/profile/attire_verification_status.dart deleted file mode 100644 index f766e8dc..00000000 --- a/apps/mobile/packages/domain/lib/src/entities/profile/attire_verification_status.dart +++ /dev/null @@ -1,39 +0,0 @@ -/// Represents the verification status of an attire item photo. -enum AttireVerificationStatus { - /// Job is created and waiting to be processed. - pending('PENDING'), - - /// Job is currently being processed by machine or human. - processing('PROCESSING'), - - /// Machine verification passed automatically. - autoPass('AUTO_PASS'), - - /// Machine verification failed automatically. - autoFail('AUTO_FAIL'), - - /// Machine results are inconclusive and require human review. - needsReview('NEEDS_REVIEW'), - - /// Human reviewer approved the verification. - approved('APPROVED'), - - /// Human reviewer rejected the verification. - rejected('REJECTED'), - - /// An error occurred during processing. - error('ERROR'); - - const AttireVerificationStatus(this.value); - - /// The string value expected by the Core API. - final String value; - - /// Creates a [AttireVerificationStatus] from a string. - static AttireVerificationStatus fromString(String value) { - return AttireVerificationStatus.values.firstWhere( - (AttireVerificationStatus e) => e.value == value, - orElse: () => AttireVerificationStatus.error, - ); - } -} diff --git a/apps/mobile/packages/domain/lib/src/entities/profile/certificate.dart b/apps/mobile/packages/domain/lib/src/entities/profile/certificate.dart new file mode 100644 index 00000000..388e154a --- /dev/null +++ b/apps/mobile/packages/domain/lib/src/entities/profile/certificate.dart @@ -0,0 +1,147 @@ +import 'package:equatable/equatable.dart'; + +/// Status of a staff certificate. +enum CertificateStatus { + /// Certificate uploaded, pending verification. + pending, + + /// Certificate has been verified. + verified, + + /// Certificate was rejected. + rejected, + + /// Certificate has expired. + expired, +} + +/// A staff certificate record (e.g. food hygiene, SIA badge). +/// +/// Returned by `GET /staff/profile/certificates`. Maps to the V2 +/// `certificates` table with additional metadata fields. +/// Named [StaffCertificate] to distinguish from the catalog [Certificate] +/// definition in `skills/certificate.dart`. +class StaffCertificate extends Equatable { + /// Creates a [StaffCertificate] instance. + const StaffCertificate({ + required this.certificateId, + required this.certificateType, + required this.name, + this.fileUri, + this.issuer, + this.certificateNumber, + this.issuedAt, + this.expiresAt, + required this.status, + this.verificationStatus, + }); + + /// Deserialises a [StaffCertificate] from the V2 API JSON response. + factory StaffCertificate.fromJson(Map json) { + return StaffCertificate( + certificateId: json['certificateId'] as String, + certificateType: json['certificateType'] as String, + name: json['name'] as String, + fileUri: json['fileUri'] as String?, + issuer: json['issuer'] as String?, + certificateNumber: json['certificateNumber'] as String?, + issuedAt: json['issuedAt'] != null ? DateTime.parse(json['issuedAt'] as String) : null, + expiresAt: json['expiresAt'] != null ? DateTime.parse(json['expiresAt'] as String) : null, + status: _parseStatus(json['status'] as String?), + verificationStatus: json['verificationStatus'] as String?, + ); + } + + /// Certificate record UUID. + final String certificateId; + + /// Type code (e.g. "FOOD_HYGIENE", "SIA_BADGE"). + final String certificateType; + + /// Human-readable name (from metadata or falls back to certificateType). + final String name; + + /// URI to the uploaded certificate file. + final String? fileUri; + + /// Issuing authority name (from metadata). + final String? issuer; + + /// Certificate number/ID. + final String? certificateNumber; + + /// When the certificate was issued. + final DateTime? issuedAt; + + /// When the certificate expires. + final DateTime? expiresAt; + + /// Current verification status. + final CertificateStatus status; + + /// Detailed verification status string (from metadata). + final String? verificationStatus; + + /// Whether the certificate has expired based on [expiresAt]. + bool get isExpired => expiresAt != null && expiresAt!.isBefore(DateTime.now()); + + /// Serialises this [StaffCertificate] to a JSON map. + Map toJson() { + return { + 'certificateId': certificateId, + 'certificateType': certificateType, + 'name': name, + 'fileUri': fileUri, + 'issuer': issuer, + 'certificateNumber': certificateNumber, + 'issuedAt': issuedAt?.toIso8601String(), + 'expiresAt': expiresAt?.toIso8601String(), + 'status': _statusToString(status), + 'verificationStatus': verificationStatus, + }; + } + + @override + List get props => [ + certificateId, + certificateType, + name, + fileUri, + issuer, + certificateNumber, + issuedAt, + expiresAt, + status, + verificationStatus, + ]; + + /// Parses a status string from the API. + static CertificateStatus _parseStatus(String? value) { + switch (value?.toUpperCase()) { + case 'PENDING': + return CertificateStatus.pending; + case 'VERIFIED': + return CertificateStatus.verified; + case 'REJECTED': + return CertificateStatus.rejected; + case 'EXPIRED': + return CertificateStatus.expired; + default: + return CertificateStatus.pending; + } + } + + /// Converts a [CertificateStatus] to its API string. + static String _statusToString(CertificateStatus status) { + switch (status) { + case CertificateStatus.pending: + return 'PENDING'; + case CertificateStatus.verified: + return 'VERIFIED'; + case CertificateStatus.rejected: + return 'REJECTED'; + case CertificateStatus.expired: + return 'EXPIRED'; + } + } +} diff --git a/apps/mobile/packages/domain/lib/src/entities/profile/compliance_type.dart b/apps/mobile/packages/domain/lib/src/entities/profile/compliance_type.dart deleted file mode 100644 index ce5533ce..00000000 --- a/apps/mobile/packages/domain/lib/src/entities/profile/compliance_type.dart +++ /dev/null @@ -1,25 +0,0 @@ -/// Represents the broad category of a compliance certificate. -enum ComplianceType { - backgroundCheck('BACKGROUND_CHECK'), - foodHandler('FOOD_HANDLER'), - rbs('RBS'), - legal('LEGAL'), - operational('OPERATIONAL'), - safety('SAFETY'), - training('TRAINING'), - license('LICENSE'), - other('OTHER'); - - const ComplianceType(this.value); - - /// The string value expected by the backend. - final String value; - - /// Creates a [ComplianceType] from a string. - static ComplianceType fromString(String value) { - return ComplianceType.values.firstWhere( - (ComplianceType e) => e.value == value, - orElse: () => ComplianceType.other, - ); - } -} diff --git a/apps/mobile/packages/domain/lib/src/entities/profile/document_verification_status.dart b/apps/mobile/packages/domain/lib/src/entities/profile/document_verification_status.dart deleted file mode 100644 index 99b47bb8..00000000 --- a/apps/mobile/packages/domain/lib/src/entities/profile/document_verification_status.dart +++ /dev/null @@ -1,39 +0,0 @@ -/// Represents the verification status of a compliance document. -enum DocumentVerificationStatus { - /// Job is created and waiting to be processed. - pending('PENDING'), - - /// Job is currently being processed by machine or human. - processing('PROCESSING'), - - /// Machine verification passed automatically. - autoPass('AUTO_PASS'), - - /// Machine verification failed automatically. - autoFail('AUTO_FAIL'), - - /// Machine results are inconclusive and require human review. - needsReview('NEEDS_REVIEW'), - - /// Human reviewer approved the verification. - approved('APPROVED'), - - /// Human reviewer rejected the verification. - rejected('REJECTED'), - - /// An error occurred during processing. - error('ERROR'); - - const DocumentVerificationStatus(this.value); - - /// The string value expected by the Core API. - final String value; - - /// Creates a [DocumentVerificationStatus] from a string. - static DocumentVerificationStatus fromString(String value) { - return DocumentVerificationStatus.values.firstWhere( - (DocumentVerificationStatus e) => e.value == value, - orElse: () => DocumentVerificationStatus.error, - ); - } -} diff --git a/apps/mobile/packages/domain/lib/src/entities/profile/emergency_contact.dart b/apps/mobile/packages/domain/lib/src/entities/profile/emergency_contact.dart index ed3d438d..6c16bc71 100644 --- a/apps/mobile/packages/domain/lib/src/entities/profile/emergency_contact.dart +++ b/apps/mobile/packages/domain/lib/src/entities/profile/emergency_contact.dart @@ -1,75 +1,89 @@ import 'package:equatable/equatable.dart'; -import 'relationship_type.dart'; -/// Represents an emergency contact for a user. +/// An emergency contact for a staff member. /// -/// Critical for staff safety during shifts. +/// Maps to the V2 `emergency_contacts` table. Returned by +/// `GET /staff/profile/emergency-contacts`. class EmergencyContact extends Equatable { - + /// Creates an [EmergencyContact] instance. const EmergencyContact({ - required this.id, - required this.name, - required this.relationship, + required this.contactId, + required this.fullName, required this.phone, + required this.relationshipType, + this.isPrimary = false, }); - /// Unique identifier. - final String id; + /// Deserialises an [EmergencyContact] from a V2 API JSON response. + factory EmergencyContact.fromJson(Map json) { + return EmergencyContact( + contactId: json['contactId'] as String, + fullName: json['fullName'] as String, + phone: json['phone'] as String, + relationshipType: json['relationshipType'] as String, + isPrimary: json['isPrimary'] as bool? ?? false, + ); + } - /// Full name of the contact. - final String name; + /// Unique contact record ID (UUID). + final String contactId; - /// Relationship to the user (e.g. "Spouse", "Parent"). - final RelationshipType relationship; + /// Full name of the contact person. + final String fullName; /// Phone number. final String phone; - @override - List get props => [id, name, relationship, phone]; + /// Relationship to the staff member (e.g. "SPOUSE", "PARENT", "FRIEND"). + final String relationshipType; + + /// Whether this is the primary emergency contact. + final bool isPrimary; + + /// Serialises this [EmergencyContact] to a JSON map. + Map toJson() { + return { + 'contactId': contactId, + 'fullName': fullName, + 'phone': phone, + 'relationshipType': relationshipType, + 'isPrimary': isPrimary, + }; + } /// Returns a copy of this [EmergencyContact] with the given fields replaced. EmergencyContact copyWith({ - String? id, - String? name, + String? contactId, + String? fullName, String? phone, - RelationshipType? relationship, + String? relationshipType, + bool? isPrimary, }) { return EmergencyContact( - id: id ?? this.id, - name: name ?? this.name, + contactId: contactId ?? this.contactId, + fullName: fullName ?? this.fullName, phone: phone ?? this.phone, - relationship: relationship ?? this.relationship, + relationshipType: relationshipType ?? this.relationshipType, + isPrimary: isPrimary ?? this.isPrimary, ); } - /// Returns an empty [EmergencyContact]. + /// Returns an empty [EmergencyContact] for form initialisation. static EmergencyContact empty() { return const EmergencyContact( - id: '', - name: '', + contactId: '', + fullName: '', phone: '', - relationship: RelationshipType.family, + relationshipType: 'FAMILY', ); } - /// Converts a string value to a [RelationshipType]. - static RelationshipType stringToRelationshipType(String? value) { - if (value != null) { - final String strVal = value.toUpperCase(); - switch (strVal) { - case 'FAMILY': - return RelationshipType.family; - case 'SPOUSE': - return RelationshipType.spouse; - case 'FRIEND': - return RelationshipType.friend; - case 'OTHER': - return RelationshipType.other; - default: - return RelationshipType.other; - } - } - return RelationshipType.other; - } + @override + List get props => [ + contactId, + fullName, + phone, + relationshipType, + isPrimary, + ]; } diff --git a/apps/mobile/packages/domain/lib/src/entities/profile/experience_skill.dart b/apps/mobile/packages/domain/lib/src/entities/profile/experience_skill.dart deleted file mode 100644 index 05676b23..00000000 --- a/apps/mobile/packages/domain/lib/src/entities/profile/experience_skill.dart +++ /dev/null @@ -1,30 +0,0 @@ - -enum ExperienceSkill { - foodService('food_service'), - bartending('bartending'), - eventSetup('event_setup'), - hospitality('hospitality'), - warehouse('warehouse'), - customerService('customer_service'), - cleaning('cleaning'), - security('security'), - retail('retail'), - cooking('cooking'), - cashier('cashier'), - server('server'), - barista('barista'), - hostHostess('host_hostess'), - busser('busser'), - driving('driving'); - - final String value; - const ExperienceSkill(this.value); - - static ExperienceSkill? fromString(String value) { - try { - return ExperienceSkill.values.firstWhere((ExperienceSkill e) => e.value == value); - } catch (_) { - return null; - } - } -} diff --git a/apps/mobile/packages/domain/lib/src/entities/profile/industry.dart b/apps/mobile/packages/domain/lib/src/entities/profile/industry.dart deleted file mode 100644 index f0de201e..00000000 --- a/apps/mobile/packages/domain/lib/src/entities/profile/industry.dart +++ /dev/null @@ -1,21 +0,0 @@ - -enum Industry { - hospitality('hospitality'), - foodService('food_service'), - warehouse('warehouse'), - events('events'), - retail('retail'), - healthcare('healthcare'), - other('other'); - - final String value; - const Industry(this.value); - - static Industry? fromString(String value) { - try { - return Industry.values.firstWhere((Industry e) => e.value == value); - } catch (_) { - return null; - } - } -} diff --git a/apps/mobile/packages/domain/lib/src/entities/profile/privacy_settings.dart b/apps/mobile/packages/domain/lib/src/entities/profile/privacy_settings.dart new file mode 100644 index 00000000..3001a05e --- /dev/null +++ b/apps/mobile/packages/domain/lib/src/entities/profile/privacy_settings.dart @@ -0,0 +1,32 @@ +import 'package:equatable/equatable.dart'; + +/// Staff privacy settings returned by `GET /staff/profile/privacy`. +/// +/// Currently contains a single visibility flag stored in the staff +/// metadata blob. May expand as more privacy controls are added. +class PrivacySettings extends Equatable { + /// Creates a [PrivacySettings] instance. + const PrivacySettings({ + this.profileVisible = true, + }); + + /// Deserialises [PrivacySettings] from the V2 API JSON response. + factory PrivacySettings.fromJson(Map json) { + return PrivacySettings( + profileVisible: json['profileVisible'] as bool? ?? true, + ); + } + + /// Whether the staff profile is visible to businesses. + final bool profileVisible; + + /// Serialises this [PrivacySettings] to a JSON map. + Map toJson() { + return { + 'profileVisible': profileVisible, + }; + } + + @override + List get props => [profileVisible]; +} diff --git a/apps/mobile/packages/domain/lib/src/entities/profile/profile_completion.dart b/apps/mobile/packages/domain/lib/src/entities/profile/profile_completion.dart new file mode 100644 index 00000000..4b22fd4d --- /dev/null +++ b/apps/mobile/packages/domain/lib/src/entities/profile/profile_completion.dart @@ -0,0 +1,81 @@ +import 'package:equatable/equatable.dart'; + +/// Profile completion data returned by `GET /staff/profile-completion`. +/// +/// Contains per-field completion booleans derived from the staff metadata, +/// plus an overall completion flag and a list of missing fields. +class ProfileCompletion extends Equatable { + /// Creates a [ProfileCompletion] instance. + const ProfileCompletion({ + required this.staffId, + this.completed = false, + this.missingFields = const [], + this.fields = const {}, + }); + + /// Deserialises a [ProfileCompletion] from the V2 API JSON response. + factory ProfileCompletion.fromJson(Map json) { + final Object? rawFields = json['fields']; + final Map fields = {}; + if (rawFields is Map) { + for (final MapEntry entry in rawFields.entries) { + fields[entry.key] = entry.value == true; + } + } + + return ProfileCompletion( + staffId: json['staffId'] as String, + completed: json['completed'] as bool? ?? false, + missingFields: _parseStringList(json['missingFields']), + fields: fields, + ); + } + + /// Staff profile UUID. + final String staffId; + + /// Whether all required fields are complete. + final bool completed; + + /// List of field names that are still missing. + final List missingFields; + + /// Per-field completion map (field name to boolean). + /// + /// Known keys: firstName, lastName, email, phone, preferredLocations, + /// skills, industries, emergencyContact. + final Map fields; + + /// Percentage of fields completed (0 - 100). + int get percentComplete { + if (fields.isEmpty) return 0; + final int completedCount = fields.values.where((bool v) => v).length; + return ((completedCount / fields.length) * 100).round(); + } + + /// Serialises this [ProfileCompletion] to a JSON map. + Map toJson() { + return { + 'staffId': staffId, + 'completed': completed, + 'missingFields': missingFields, + 'fields': fields, + }; + } + + @override + List get props => [ + staffId, + completed, + missingFields, + fields, + ]; + + /// Parses a dynamic value into a list of strings. + static List _parseStringList(Object? value) { + if (value is List) { + return value.map((Object? e) => e.toString()).toList(); + } + return const []; + } +} diff --git a/apps/mobile/packages/domain/lib/src/entities/profile/profile_document.dart b/apps/mobile/packages/domain/lib/src/entities/profile/profile_document.dart new file mode 100644 index 00000000..d044f4e5 --- /dev/null +++ b/apps/mobile/packages/domain/lib/src/entities/profile/profile_document.dart @@ -0,0 +1,183 @@ +import 'package:equatable/equatable.dart'; + +/// Status of a profile document. +enum ProfileDocumentStatus { + /// Document has not been uploaded yet. + notUploaded, + + /// Upload is pending review. + pending, + + /// Document has been verified/approved. + verified, + + /// Document was rejected. + rejected, + + /// Document has expired. + expired, +} + +/// Type category of a profile document. +enum ProfileDocumentType { + /// General compliance document. + document, + + /// Government-issued ID. + governmentId, + + /// Attire/uniform photo. + attire, + + /// Tax form (I-9, W-4). + taxForm, +} + +/// A profile document (or document requirement) for a staff member. +/// +/// Returned by `GET /staff/profile/documents`. Joins the `documents` catalog +/// with the staff-specific `staff_documents` record (if uploaded). +class ProfileDocument extends Equatable { + /// Creates a [ProfileDocument] instance. + const ProfileDocument({ + required this.documentId, + required this.documentType, + required this.name, + this.staffDocumentId, + this.fileUri, + required this.status, + this.expiresAt, + this.metadata = const {}, + }); + + /// Deserialises a [ProfileDocument] from the V2 API JSON response. + factory ProfileDocument.fromJson(Map json) { + return ProfileDocument( + documentId: json['documentId'] as String, + documentType: _parseDocumentType(json['documentType'] as String?), + name: json['name'] as String, + staffDocumentId: json['staffDocumentId'] as String?, + fileUri: json['fileUri'] as String?, + status: _parseStatus(json['status'] as String?), + expiresAt: json['expiresAt'] != null ? DateTime.parse(json['expiresAt'] as String) : null, + metadata: (json['metadata'] as Map?) ?? const {}, + ); + } + + /// Catalog document definition ID (UUID). + final String documentId; + + /// Type category of this document. + final ProfileDocumentType documentType; + + /// Human-readable document name. + final String name; + + /// Staff-specific document record ID, or null if not yet uploaded. + final String? staffDocumentId; + + /// URI to the uploaded file. + final String? fileUri; + + /// Current status of the document. + final ProfileDocumentStatus status; + + /// When the document expires, if applicable. + final DateTime? expiresAt; + + /// Flexible metadata JSON blob. + final Map metadata; + + /// Whether the document has been uploaded. + bool get isUploaded => staffDocumentId != null; + + /// Serialises this [ProfileDocument] to a JSON map. + Map toJson() { + return { + 'documentId': documentId, + 'documentType': _documentTypeToString(documentType), + 'name': name, + 'staffDocumentId': staffDocumentId, + 'fileUri': fileUri, + 'status': _statusToString(status), + 'expiresAt': expiresAt?.toIso8601String(), + 'metadata': metadata, + }; + } + + @override + List get props => [ + documentId, + documentType, + name, + staffDocumentId, + fileUri, + status, + expiresAt, + metadata, + ]; + + /// Parses a document type string from the API. + static ProfileDocumentType _parseDocumentType(String? value) { + switch (value?.toUpperCase()) { + case 'DOCUMENT': + return ProfileDocumentType.document; + case 'GOVERNMENT_ID': + return ProfileDocumentType.governmentId; + case 'ATTIRE': + return ProfileDocumentType.attire; + case 'TAX_FORM': + return ProfileDocumentType.taxForm; + default: + return ProfileDocumentType.document; + } + } + + /// Parses a status string from the API. + static ProfileDocumentStatus _parseStatus(String? value) { + switch (value?.toUpperCase()) { + case 'NOT_UPLOADED': + return ProfileDocumentStatus.notUploaded; + case 'PENDING': + return ProfileDocumentStatus.pending; + case 'VERIFIED': + return ProfileDocumentStatus.verified; + case 'REJECTED': + return ProfileDocumentStatus.rejected; + case 'EXPIRED': + return ProfileDocumentStatus.expired; + default: + return ProfileDocumentStatus.notUploaded; + } + } + + /// Converts a [ProfileDocumentType] to its API string. + static String _documentTypeToString(ProfileDocumentType type) { + switch (type) { + case ProfileDocumentType.document: + return 'DOCUMENT'; + case ProfileDocumentType.governmentId: + return 'GOVERNMENT_ID'; + case ProfileDocumentType.attire: + return 'ATTIRE'; + case ProfileDocumentType.taxForm: + return 'TAX_FORM'; + } + } + + /// Converts a [ProfileDocumentStatus] to its API string. + static String _statusToString(ProfileDocumentStatus status) { + switch (status) { + case ProfileDocumentStatus.notUploaded: + return 'NOT_UPLOADED'; + case ProfileDocumentStatus.pending: + return 'PENDING'; + case ProfileDocumentStatus.verified: + return 'VERIFIED'; + case ProfileDocumentStatus.rejected: + return 'REJECTED'; + case ProfileDocumentStatus.expired: + return 'EXPIRED'; + } + } +} diff --git a/apps/mobile/packages/domain/lib/src/entities/profile/profile_section_status.dart b/apps/mobile/packages/domain/lib/src/entities/profile/profile_section_status.dart new file mode 100644 index 00000000..9386e5e9 --- /dev/null +++ b/apps/mobile/packages/domain/lib/src/entities/profile/profile_section_status.dart @@ -0,0 +1,76 @@ +import 'package:equatable/equatable.dart'; + +/// Profile section completion status returned by `GET /staff/profile/sections`. +/// +/// Boolean flags indicating whether each profile section has been completed. +/// Used to show progress indicators and gate onboarding flows. +class ProfileSectionStatus extends Equatable { + /// Creates a [ProfileSectionStatus] instance. + const ProfileSectionStatus({ + this.personalInfoCompleted = false, + this.emergencyContactCompleted = false, + this.experienceCompleted = false, + this.attireCompleted = false, + this.taxFormsCompleted = false, + this.benefitsConfigured = false, + this.certificateCount = 0, + }); + + /// Deserialises a [ProfileSectionStatus] from the V2 API JSON response. + factory ProfileSectionStatus.fromJson(Map json) { + return ProfileSectionStatus( + personalInfoCompleted: json['personalInfoCompleted'] as bool? ?? false, + emergencyContactCompleted: json['emergencyContactCompleted'] as bool? ?? false, + experienceCompleted: json['experienceCompleted'] as bool? ?? false, + attireCompleted: json['attireCompleted'] as bool? ?? false, + taxFormsCompleted: json['taxFormsCompleted'] as bool? ?? false, + benefitsConfigured: json['benefitsConfigured'] as bool? ?? false, + certificateCount: (json['certificateCount'] as num?)?.toInt() ?? 0, + ); + } + + /// Whether personal info (name, email, phone, locations) is complete. + final bool personalInfoCompleted; + + /// Whether at least one emergency contact exists. + final bool emergencyContactCompleted; + + /// Whether skills and industries are filled in. + final bool experienceCompleted; + + /// Whether all required attire items are verified. + final bool attireCompleted; + + /// Whether all tax forms (I-9, W-4) are verified. + final bool taxFormsCompleted; + + /// Whether at least one benefit is configured. + final bool benefitsConfigured; + + /// Number of uploaded certificates. + final int certificateCount; + + /// Serialises this [ProfileSectionStatus] to a JSON map. + Map toJson() { + return { + 'personalInfoCompleted': personalInfoCompleted, + 'emergencyContactCompleted': emergencyContactCompleted, + 'experienceCompleted': experienceCompleted, + 'attireCompleted': attireCompleted, + 'taxFormsCompleted': taxFormsCompleted, + 'benefitsConfigured': benefitsConfigured, + 'certificateCount': certificateCount, + }; + } + + @override + List get props => [ + personalInfoCompleted, + emergencyContactCompleted, + experienceCompleted, + attireCompleted, + taxFormsCompleted, + benefitsConfigured, + certificateCount, + ]; +} diff --git a/apps/mobile/packages/domain/lib/src/entities/profile/relationship_type.dart b/apps/mobile/packages/domain/lib/src/entities/profile/relationship_type.dart deleted file mode 100644 index 2d4dde1a..00000000 --- a/apps/mobile/packages/domain/lib/src/entities/profile/relationship_type.dart +++ /dev/null @@ -1,6 +0,0 @@ -enum RelationshipType { - family, - spouse, - friend, - other, -} diff --git a/apps/mobile/packages/domain/lib/src/entities/profile/schedule.dart b/apps/mobile/packages/domain/lib/src/entities/profile/schedule.dart deleted file mode 100644 index 5aeb8131..00000000 --- a/apps/mobile/packages/domain/lib/src/entities/profile/schedule.dart +++ /dev/null @@ -1,32 +0,0 @@ -import 'package:equatable/equatable.dart'; - -/// Represents general availability schedule for a [Staff] member. -/// -/// Defines recurring availability (e.g., "Mondays 9-5"). -class Schedule extends Equatable { - - const Schedule({ - required this.id, - required this.staffId, - required this.dayOfWeek, - required this.startTime, - required this.endTime, - }); - /// Unique identifier. - final String id; - - /// The [Staff] member. - final String staffId; - - /// Day of the week (1 = Monday, 7 = Sunday). - final int dayOfWeek; - - /// Start time of availability. - final DateTime startTime; - - /// End time of availability. - final DateTime endTime; - - @override - List get props => [id, staffId, dayOfWeek, startTime, endTime]; -} \ No newline at end of file diff --git a/apps/mobile/packages/domain/lib/src/entities/profile/staff_certificate.dart b/apps/mobile/packages/domain/lib/src/entities/profile/staff_certificate.dart deleted file mode 100644 index a4da1b29..00000000 --- a/apps/mobile/packages/domain/lib/src/entities/profile/staff_certificate.dart +++ /dev/null @@ -1,127 +0,0 @@ -import 'package:equatable/equatable.dart'; - -import 'compliance_type.dart'; -import 'staff_certificate_status.dart'; -import 'staff_certificate_validation_status.dart'; - -/// Represents a staff's compliance certificate record. -class StaffCertificate extends Equatable { - const StaffCertificate({ - required this.id, - required this.staffId, - required this.name, - this.description, - this.expiryDate, - required this.status, - this.certificateUrl, - this.icon, - required this.certificationType, - this.issuer, - this.certificateNumber, - this.validationStatus, - this.verificationId, - this.createdAt, - this.updatedAt, - }); - - /// The unique identifier of the certificate record. - final String id; - - /// The ID of the staff member. - final String staffId; - - /// The display name of the certificate. - final String name; - - /// A description or details about the certificate. - final String? description; - - /// The expiration date of the certificate. - final DateTime? expiryDate; - - /// The current state of the certificate. - final StaffCertificateStatus status; - - /// The URL of the stored certificate file/image. - final String? certificateUrl; - - /// An icon to display for this certificate type. - final String? icon; - - /// The category of compliance this certificate fits into. - final ComplianceType certificationType; - - /// The issuing body or authority. - final String? issuer; - - /// Document number or reference. - final String? certificateNumber; - - /// The ID of the verification record. - final String? verificationId; - - /// Recent validation/verification results. - final StaffCertificateValidationStatus? validationStatus; - - /// Creation timestamp. - final DateTime? createdAt; - - /// Last update timestamp. - final DateTime? updatedAt; - - @override - List get props => [ - id, - staffId, - name, - description, - expiryDate, - status, - certificateUrl, - icon, - certificationType, - issuer, - certificateNumber, - validationStatus, - verificationId, - createdAt, - updatedAt, - ]; - - /// Creates a copy of this [StaffCertificate] with updated fields. - StaffCertificate copyWith({ - String? id, - String? staffId, - String? name, - String? description, - DateTime? expiryDate, - StaffCertificateStatus? status, - String? certificateUrl, - String? icon, - ComplianceType? certificationType, - String? issuer, - String? certificateNumber, - StaffCertificateValidationStatus? validationStatus, - String? verificationId, - DateTime? createdAt, - DateTime? updatedAt, - }) { - return StaffCertificate( - id: id ?? this.id, - staffId: staffId ?? this.staffId, - name: name ?? this.name, - description: description ?? this.description, - expiryDate: expiryDate ?? this.expiryDate, - status: status ?? this.status, - certificateUrl: certificateUrl ?? this.certificateUrl, - icon: icon ?? this.icon, - certificationType: certificationType ?? this.certificationType, - issuer: issuer ?? this.issuer, - certificateNumber: certificateNumber ?? this.certificateNumber, - validationStatus: validationStatus ?? this.validationStatus, - verificationId: verificationId ?? this.verificationId, - createdAt: createdAt ?? this.createdAt, - updatedAt: updatedAt ?? this.updatedAt, - ); - } -} diff --git a/apps/mobile/packages/domain/lib/src/entities/profile/staff_certificate_status.dart b/apps/mobile/packages/domain/lib/src/entities/profile/staff_certificate_status.dart deleted file mode 100644 index b39e096c..00000000 --- a/apps/mobile/packages/domain/lib/src/entities/profile/staff_certificate_status.dart +++ /dev/null @@ -1,23 +0,0 @@ -/// Represents the current validity status of a staff certificate. -enum StaffCertificateStatus { - current('CURRENT'), - expiringSoon('EXPIRING_SOON'), - completed('COMPLETED'), - pending('PENDING'), - expired('EXPIRED'), - expiring('EXPIRING'), - notStarted('NOT_STARTED'); - - const StaffCertificateStatus(this.value); - - /// The string value expected by the backend. - final String value; - - /// Creates a [StaffCertificateStatus] from a string. - static StaffCertificateStatus fromString(String value) { - return StaffCertificateStatus.values.firstWhere( - (StaffCertificateStatus e) => e.value == value, - orElse: () => StaffCertificateStatus.notStarted, - ); - } -} diff --git a/apps/mobile/packages/domain/lib/src/entities/profile/staff_certificate_validation_status.dart b/apps/mobile/packages/domain/lib/src/entities/profile/staff_certificate_validation_status.dart deleted file mode 100644 index 19d30358..00000000 --- a/apps/mobile/packages/domain/lib/src/entities/profile/staff_certificate_validation_status.dart +++ /dev/null @@ -1,22 +0,0 @@ -/// Represents the verification or review state for a staff certificate. -enum StaffCertificateValidationStatus { - approved('APPROVED'), - pendingExpertReview('PENDING_EXPERT_REVIEW'), - rejected('REJECTED'), - aiVerified('AI_VERIFIED'), - aiFlagged('AI_FLAGGED'), - manualReviewNeeded('MANUAL_REVIEW_NEEDED'); - - const StaffCertificateValidationStatus(this.value); - - /// The string value expected by the backend. - final String value; - - /// Creates a [StaffCertificateValidationStatus] from a string. - static StaffCertificateValidationStatus fromString(String value) { - return StaffCertificateValidationStatus.values.firstWhere( - (StaffCertificateValidationStatus e) => e.value == value, - orElse: () => StaffCertificateValidationStatus.manualReviewNeeded, - ); - } -} diff --git a/apps/mobile/packages/domain/lib/src/entities/profile/staff_document.dart b/apps/mobile/packages/domain/lib/src/entities/profile/staff_document.dart deleted file mode 100644 index d8dd5958..00000000 --- a/apps/mobile/packages/domain/lib/src/entities/profile/staff_document.dart +++ /dev/null @@ -1,66 +0,0 @@ -import 'package:equatable/equatable.dart'; - -import 'document_verification_status.dart'; - -/// Status of a compliance document. -enum DocumentStatus { verified, pending, missing, rejected, expired } - -/// Represents a staff compliance document. -class StaffDocument extends Equatable { - const StaffDocument({ - required this.id, - required this.staffId, - required this.documentId, - required this.name, - this.description, - required this.status, - this.documentUrl, - this.expiryDate, - this.verificationId, - this.verificationStatus, - }); - - /// The unique identifier of the staff document record. - final String id; - - /// The ID of the staff member. - final String staffId; - - /// The ID of the document definition. - final String documentId; - - /// The name of the document. - final String name; - - /// A description of the document. - final String? description; - - /// The status of the document. - final DocumentStatus status; - - /// The URL of the uploaded document image/file. - final String? documentUrl; - - /// The expiry date of the document. - final DateTime? expiryDate; - - /// The ID of the verification record. - final String? verificationId; - - /// The detailed verification status. - final DocumentVerificationStatus? verificationStatus; - - @override - List get props => [ - id, - staffId, - documentId, - name, - description, - status, - documentUrl, - expiryDate, - verificationId, - verificationStatus, - ]; -} diff --git a/apps/mobile/packages/domain/lib/src/entities/profile/staff_personal_info.dart b/apps/mobile/packages/domain/lib/src/entities/profile/staff_personal_info.dart new file mode 100644 index 00000000..e394e27d --- /dev/null +++ b/apps/mobile/packages/domain/lib/src/entities/profile/staff_personal_info.dart @@ -0,0 +1,105 @@ +import 'package:equatable/equatable.dart'; + +/// Staff personal information returned by `GET /staff/profile/personal-info`. +/// +/// Contains name, contact details, bio, location preferences, and +/// experience metadata extracted from the staffs table and its metadata blob. +class StaffPersonalInfo extends Equatable { + /// Creates a [StaffPersonalInfo] instance. + const StaffPersonalInfo({ + required this.staffId, + this.firstName, + this.lastName, + this.bio, + this.preferredLocations = const [], + this.maxDistanceMiles, + this.industries = const [], + this.skills = const [], + this.email, + this.phone, + }); + + /// Deserialises a [StaffPersonalInfo] from the V2 API JSON response. + factory StaffPersonalInfo.fromJson(Map json) { + return StaffPersonalInfo( + staffId: json['staffId'] as String, + firstName: json['firstName'] as String?, + lastName: json['lastName'] as String?, + bio: json['bio'] as String?, + preferredLocations: _parseStringList(json['preferredLocations']), + maxDistanceMiles: (json['maxDistanceMiles'] as num?)?.toInt(), + industries: _parseStringList(json['industries']), + skills: _parseStringList(json['skills']), + email: json['email'] as String?, + phone: json['phone'] as String?, + ); + } + + /// Staff profile UUID. + final String staffId; + + /// First name (from metadata or parsed from fullName). + final String? firstName; + + /// Last name (from metadata or parsed from fullName). + final String? lastName; + + /// Short biography or description. + final String? bio; + + /// Preferred work locations (city names or addresses). + final List preferredLocations; + + /// Maximum commute distance in miles. + final int? maxDistanceMiles; + + /// Industry codes the staff is experienced in. + final List industries; + + /// Skill codes the staff has. + final List skills; + + /// Contact email. + final String? email; + + /// Contact phone number. + final String? phone; + + /// Serialises this [StaffPersonalInfo] to a JSON map. + Map toJson() { + return { + 'staffId': staffId, + 'firstName': firstName, + 'lastName': lastName, + 'bio': bio, + 'preferredLocations': preferredLocations, + 'maxDistanceMiles': maxDistanceMiles, + 'industries': industries, + 'skills': skills, + 'email': email, + 'phone': phone, + }; + } + + @override + List get props => [ + staffId, + firstName, + lastName, + bio, + preferredLocations, + maxDistanceMiles, + industries, + skills, + email, + phone, + ]; + + /// Parses a dynamic value into a list of strings. + static List _parseStringList(Object? value) { + if (value is List) { + return value.map((Object? e) => e.toString()).toList(); + } + return const []; + } +} diff --git a/apps/mobile/packages/domain/lib/src/entities/profile/tax_form.dart b/apps/mobile/packages/domain/lib/src/entities/profile/tax_form.dart index bc3967b1..80153533 100644 --- a/apps/mobile/packages/domain/lib/src/entities/profile/tax_form.dart +++ b/apps/mobile/packages/domain/lib/src/entities/profile/tax_form.dart @@ -1,78 +1,117 @@ import 'package:equatable/equatable.dart'; -enum TaxFormType { i9, w4 } +/// Status of a tax form. +enum TaxFormStatus { + /// Form has not been started. + notStarted, -enum TaxFormStatus { notStarted, inProgress, submitted, approved, rejected } + /// Form is partially filled. + inProgress, -abstract class TaxForm extends Equatable { + /// Form has been submitted for review. + submitted, + /// Form has been approved. + approved, + + /// Form was rejected. + rejected, +} + +/// A tax form (e.g. I-9, W-4) for a staff member. +/// +/// Returned by `GET /staff/profile/tax-forms`. Joins the `documents` catalog +/// (filtered to TAX_FORM type) with the staff-specific `staff_documents` record. +class TaxForm extends Equatable { + /// Creates a [TaxForm] instance. const TaxForm({ - required this.id, - required this.title, - this.subtitle, - this.description, - this.status = TaxFormStatus.notStarted, - this.staffId, - this.formData = const {}, - this.createdAt, - this.updatedAt, + required this.documentId, + required this.formType, + this.staffDocumentId, + required this.status, + this.fields = const {}, }); - final String id; - TaxFormType get type; - final String title; - final String? subtitle; - final String? description; + + /// Deserialises a [TaxForm] from the V2 API JSON response. + factory TaxForm.fromJson(Map json) { + return TaxForm( + documentId: json['documentId'] as String, + formType: json['formType'] as String, + staffDocumentId: json['staffDocumentId'] as String?, + status: _parseStatus(json['status'] as String?), + fields: (json['fields'] as Map?) ?? const {}, + ); + } + + /// Catalog document definition ID (UUID). + final String documentId; + + /// Form type name (e.g. "I-9", "W-4"). + final String formType; + + /// Staff-specific document record ID, or null if not started. + final String? staffDocumentId; + + /// Current status of the tax form. final TaxFormStatus status; - final String? staffId; - final Map formData; - final DateTime? createdAt; - final DateTime? updatedAt; + + /// Saved form field data (partial or complete). + final Map fields; + + /// Whether the form has been started. + bool get isStarted => staffDocumentId != null; + + /// Serialises this [TaxForm] to a JSON map. + Map toJson() { + return { + 'documentId': documentId, + 'formType': formType, + 'staffDocumentId': staffDocumentId, + 'status': _statusToString(status), + 'fields': fields, + }; + } @override List get props => [ - id, - type, - title, - subtitle, - description, + documentId, + formType, + staffDocumentId, status, - staffId, - formData, - createdAt, - updatedAt, + fields, ]; -} - -class I9TaxForm extends TaxForm { - const I9TaxForm({ - required super.id, - required super.title, - super.subtitle, - super.description, - super.status, - super.staffId, - super.formData, - super.createdAt, - super.updatedAt, - }); - - @override - TaxFormType get type => TaxFormType.i9; -} - -class W4TaxForm extends TaxForm { - const W4TaxForm({ - required super.id, - required super.title, - super.subtitle, - super.description, - super.status, - super.staffId, - super.formData, - super.createdAt, - super.updatedAt, - }); - - @override - TaxFormType get type => TaxFormType.w4; + + /// Parses a status string from the API. + static TaxFormStatus _parseStatus(String? value) { + switch (value?.toUpperCase()) { + case 'NOT_STARTED': + return TaxFormStatus.notStarted; + case 'IN_PROGRESS': + return TaxFormStatus.inProgress; + case 'SUBMITTED': + return TaxFormStatus.submitted; + case 'APPROVED': + return TaxFormStatus.approved; + case 'REJECTED': + return TaxFormStatus.rejected; + default: + return TaxFormStatus.notStarted; + } + } + + /// Converts a [TaxFormStatus] to its API string. + static String _statusToString(TaxFormStatus status) { + switch (status) { + case TaxFormStatus.notStarted: + return 'NOT_STARTED'; + case TaxFormStatus.inProgress: + return 'IN_PROGRESS'; + case TaxFormStatus.submitted: + return 'SUBMITTED'; + case TaxFormStatus.approved: + return 'APPROVED'; + case TaxFormStatus.rejected: + return 'REJECTED'; + } + } } diff --git a/apps/mobile/packages/domain/lib/src/entities/ratings/business_staff_preference.dart b/apps/mobile/packages/domain/lib/src/entities/ratings/business_staff_preference.dart deleted file mode 100644 index 1f56eecb..00000000 --- a/apps/mobile/packages/domain/lib/src/entities/ratings/business_staff_preference.dart +++ /dev/null @@ -1,35 +0,0 @@ -import 'package:equatable/equatable.dart'; - -/// The type of preference a business has for a staff member. -enum PreferenceType { - /// Business wants to prioritize this staff member. - favorite, - - /// Business does not want to work with this staff member. - blocked, -} - -/// Represents a business's specific preference for a staff member. -class BusinessStaffPreference extends Equatable { - - const BusinessStaffPreference({ - required this.id, - required this.businessId, - required this.staffId, - required this.type, - }); - /// Unique identifier. - final String id; - - /// The [Business] holding the preference. - final String businessId; - - /// The [Staff] member. - final String staffId; - - /// Whether they are a favorite or blocked. - final PreferenceType type; - - @override - List get props => [id, businessId, staffId, type]; -} \ No newline at end of file diff --git a/apps/mobile/packages/domain/lib/src/entities/ratings/penalty_log.dart b/apps/mobile/packages/domain/lib/src/entities/ratings/penalty_log.dart deleted file mode 100644 index d42e46f0..00000000 --- a/apps/mobile/packages/domain/lib/src/entities/ratings/penalty_log.dart +++ /dev/null @@ -1,36 +0,0 @@ -import 'package:equatable/equatable.dart'; - -/// Represents a penalty issued to a staff member. -/// -/// Penalties are issued for no-shows, cancellations, or poor conduct. -class PenaltyLog extends Equatable { - - const PenaltyLog({ - required this.id, - required this.staffId, - required this.assignmentId, - required this.reason, - required this.points, - required this.issuedAt, - }); - /// Unique identifier. - final String id; - - /// The [Staff] member penalized. - final String staffId; - - /// The [Assignment] context (if applicable). - final String assignmentId; - - /// Reason for the penalty. - final String reason; - - /// Score points deducted from staff profile. - final int points; - - /// When the penalty was issued. - final DateTime issuedAt; - - @override - List get props => [id, staffId, assignmentId, reason, points, issuedAt]; -} \ No newline at end of file diff --git a/apps/mobile/packages/domain/lib/src/entities/ratings/staff_rating.dart b/apps/mobile/packages/domain/lib/src/entities/ratings/staff_rating.dart index b51a44ae..a1a70391 100644 --- a/apps/mobile/packages/domain/lib/src/entities/ratings/staff_rating.dart +++ b/apps/mobile/packages/domain/lib/src/entities/ratings/staff_rating.dart @@ -1,34 +1,103 @@ import 'package:equatable/equatable.dart'; -/// Represents a rating given to a staff member by a client. +/// A review left for a staff member after an assignment. +/// +/// Maps to the V2 `staff_reviews` table. class StaffRating extends Equatable { - + /// Creates a [StaffRating] instance. const StaffRating({ required this.id, - required this.staffId, - required this.eventId, + required this.tenantId, required this.businessId, + required this.staffId, + required this.assignmentId, + this.reviewerUserId, required this.rating, - this.comment, + this.reviewText, + this.tags = const [], + this.createdAt, }); + + /// Deserialises a [StaffRating] from a V2 API JSON map. + factory StaffRating.fromJson(Map json) { + final dynamic tagsRaw = json['tags']; + final List tagsList = tagsRaw is List + ? tagsRaw.map((dynamic e) => e.toString()).toList() + : const []; + + return StaffRating( + id: json['id'] as String, + tenantId: json['tenantId'] as String, + businessId: json['businessId'] as String, + staffId: json['staffId'] as String, + assignmentId: json['assignmentId'] as String, + reviewerUserId: json['reviewerUserId'] as String?, + rating: (json['rating'] as num).toInt(), + reviewText: json['reviewText'] as String?, + tags: tagsList, + createdAt: json['createdAt'] != null + ? DateTime.parse(json['createdAt'] as String) + : null, + ); + } + /// Unique identifier. final String id; - /// The [Staff] being rated. + /// Tenant ID. + final String tenantId; + + /// Business that left the review. + final String businessId; + + /// Staff member being reviewed. final String staffId; - /// The [Event] context. - final String eventId; + /// Assignment this review is for. + final String assignmentId; - /// The [Business] leaving the rating. - final String businessId; + /// User who wrote the review. + final String? reviewerUserId; /// Star rating (1-5). final int rating; - /// Optional feedback text. - final String? comment; + /// Free-text review. + final String? reviewText; + + /// Tags associated with the review (e.g. punctual, professional). + final List tags; + + /// When the review was created. + final DateTime? createdAt; + + /// Serialises this [StaffRating] to a JSON map. + Map toJson() { + return { + 'id': id, + 'tenantId': tenantId, + 'businessId': businessId, + 'staffId': staffId, + 'assignmentId': assignmentId, + 'reviewerUserId': reviewerUserId, + 'rating': rating, + 'reviewText': reviewText, + 'tags': tags, + 'createdAt': createdAt?.toIso8601String(), + }; + } @override - List get props => [id, staffId, eventId, businessId, rating, comment]; -} \ No newline at end of file + List get props => [ + id, + tenantId, + businessId, + staffId, + assignmentId, + reviewerUserId, + rating, + reviewText, + tags, + createdAt, + ]; +} diff --git a/apps/mobile/packages/domain/lib/src/entities/reports/coverage_report.dart b/apps/mobile/packages/domain/lib/src/entities/reports/coverage_report.dart index 0a4db09b..9ed90277 100644 --- a/apps/mobile/packages/domain/lib/src/entities/reports/coverage_report.dart +++ b/apps/mobile/packages/domain/lib/src/entities/reports/coverage_report.dart @@ -1,37 +1,109 @@ -// ignore_for_file: always_specify_types, depend_on_referenced_packages, dead_code, dead_null_aware_expression, unused_local_variable, unused_import, sort_constructors_first, prefer_final_fields, prefer_const_constructors, deprecated_member_use, implicit_call_tearoffs import 'package:equatable/equatable.dart'; +/// Coverage report with daily breakdown. +/// +/// Returned by `GET /client/reports/coverage`. class CoverageReport extends Equatable { - + /// Creates a [CoverageReport] instance. const CoverageReport({ - required this.overallCoverage, - required this.totalNeeded, - required this.totalFilled, - required this.dailyCoverage, + required this.averageCoveragePercentage, + required this.filledWorkers, + required this.neededWorkers, + this.chart = const [], }); - final double overallCoverage; - final int totalNeeded; - final int totalFilled; - final List dailyCoverage; + + /// Deserialises a [CoverageReport] from a V2 API JSON map. + factory CoverageReport.fromJson(Map json) { + final dynamic chartRaw = json['chart']; + final List chartList = chartRaw is List + ? chartRaw + .map((dynamic e) => + CoverageDayPoint.fromJson(e as Map)) + .toList() + : const []; + + return CoverageReport( + averageCoveragePercentage: + (json['averageCoveragePercentage'] as num).toInt(), + filledWorkers: (json['filledWorkers'] as num).toInt(), + neededWorkers: (json['neededWorkers'] as num).toInt(), + chart: chartList, + ); + } + + /// Average coverage across the period (0-100). + final int averageCoveragePercentage; + + /// Total filled workers across the period. + final int filledWorkers; + + /// Total needed workers across the period. + final int neededWorkers; + + /// Daily coverage data points. + final List chart; + + /// Serialises this [CoverageReport] to a JSON map. + Map toJson() { + return { + 'averageCoveragePercentage': averageCoveragePercentage, + 'filledWorkers': filledWorkers, + 'neededWorkers': neededWorkers, + 'chart': chart.map((CoverageDayPoint p) => p.toJson()).toList(), + }; + } @override - List get props => [overallCoverage, totalNeeded, totalFilled, dailyCoverage]; + List get props => [ + averageCoveragePercentage, + filledWorkers, + neededWorkers, + chart, + ]; } -class CoverageDay extends Equatable { - - const CoverageDay({ - required this.date, +/// A single day in the coverage chart. +class CoverageDayPoint extends Equatable { + /// Creates a [CoverageDayPoint] instance. + const CoverageDayPoint({ + required this.day, required this.needed, required this.filled, - required this.percentage, + required this.coveragePercentage, }); - final DateTime date; + + /// Deserialises a [CoverageDayPoint] from a V2 API JSON map. + factory CoverageDayPoint.fromJson(Map json) { + return CoverageDayPoint( + day: DateTime.parse(json['day'] as String), + needed: (json['needed'] as num).toInt(), + filled: (json['filled'] as num).toInt(), + coveragePercentage: (json['coveragePercentage'] as num).toDouble(), + ); + } + + /// Date. + final DateTime day; + + /// Workers needed. final int needed; + + /// Workers filled. final int filled; - final double percentage; + + /// Coverage percentage for this day. + final double coveragePercentage; + + /// Serialises this [CoverageDayPoint] to a JSON map. + Map toJson() { + return { + 'day': day.toIso8601String(), + 'needed': needed, + 'filled': filled, + 'coveragePercentage': coveragePercentage, + }; + } @override - List get props => [date, needed, filled, percentage]; + List get props => [day, needed, filled, coveragePercentage]; } - diff --git a/apps/mobile/packages/domain/lib/src/entities/reports/daily_ops_report.dart b/apps/mobile/packages/domain/lib/src/entities/reports/daily_ops_report.dart index 47d01056..09ced16d 100644 --- a/apps/mobile/packages/domain/lib/src/entities/reports/daily_ops_report.dart +++ b/apps/mobile/packages/domain/lib/src/entities/reports/daily_ops_report.dart @@ -1,65 +1,72 @@ -// ignore_for_file: always_specify_types, depend_on_referenced_packages, dead_code, dead_null_aware_expression, unused_local_variable, unused_import, sort_constructors_first, prefer_final_fields, prefer_const_constructors, deprecated_member_use, implicit_call_tearoffs import 'package:equatable/equatable.dart'; -class DailyOpsReport extends Equatable { +import '../coverage_domain/shift_with_workers.dart'; +/// Daily operations report with shift-level detail. +/// +/// Returned by `GET /client/reports/daily-ops`. +class DailyOpsReport extends Equatable { + /// Creates a [DailyOpsReport] instance. const DailyOpsReport({ - required this.scheduledShifts, - required this.workersConfirmed, - required this.inProgressShifts, - required this.completedShifts, - required this.shifts, + required this.totalShifts, + required this.totalWorkersDeployed, + required this.totalHoursWorked, + required this.onTimeArrivalPercentage, + this.shifts = const [], }); - final int scheduledShifts; - final int workersConfirmed; - final int inProgressShifts; - final int completedShifts; - final List shifts; + + /// Deserialises a [DailyOpsReport] from a V2 API JSON map. + factory DailyOpsReport.fromJson(Map json) { + final dynamic shiftsRaw = json['shifts']; + final List shiftsList = shiftsRaw is List + ? shiftsRaw + .map((dynamic e) => + ShiftWithWorkers.fromJson(e as Map)) + .toList() + : const []; + + return DailyOpsReport( + totalShifts: (json['totalShifts'] as num).toInt(), + totalWorkersDeployed: (json['totalWorkersDeployed'] as num).toInt(), + totalHoursWorked: (json['totalHoursWorked'] as num).toInt(), + onTimeArrivalPercentage: + (json['onTimeArrivalPercentage'] as num).toInt(), + shifts: shiftsList, + ); + } + + /// Total number of shifts for the day. + final int totalShifts; + + /// Total workers deployed. + final int totalWorkersDeployed; + + /// Total hours worked (rounded). + final int totalHoursWorked; + + /// Percentage of on-time arrivals (0-100). + final int onTimeArrivalPercentage; + + /// Individual shift details with worker assignments. + final List shifts; + + /// Serialises this [DailyOpsReport] to a JSON map. + Map toJson() { + return { + 'totalShifts': totalShifts, + 'totalWorkersDeployed': totalWorkersDeployed, + 'totalHoursWorked': totalHoursWorked, + 'onTimeArrivalPercentage': onTimeArrivalPercentage, + 'shifts': shifts.map((ShiftWithWorkers s) => s.toJson()).toList(), + }; + } @override List get props => [ - scheduledShifts, - workersConfirmed, - inProgressShifts, - completedShifts, + totalShifts, + totalWorkersDeployed, + totalHoursWorked, + onTimeArrivalPercentage, shifts, ]; } - -class DailyOpsShift extends Equatable { - - const DailyOpsShift({ - required this.id, - required this.title, - required this.location, - required this.startTime, - required this.endTime, - required this.workersNeeded, - required this.filled, - required this.status, - this.hourlyRate, - }); - final String id; - final String title; - final String location; - final DateTime startTime; - final DateTime endTime; - final int workersNeeded; - final int filled; - final String status; - final double? hourlyRate; - - @override - List get props => [ - id, - title, - location, - startTime, - endTime, - workersNeeded, - filled, - status, - hourlyRate, - ]; -} - diff --git a/apps/mobile/packages/domain/lib/src/entities/reports/forecast_report.dart b/apps/mobile/packages/domain/lib/src/entities/reports/forecast_report.dart index c4c14568..20c7a3a1 100644 --- a/apps/mobile/packages/domain/lib/src/entities/reports/forecast_report.dart +++ b/apps/mobile/packages/domain/lib/src/entities/reports/forecast_report.dart @@ -1,79 +1,128 @@ -// ignore_for_file: always_specify_types, depend_on_referenced_packages, dead_code, dead_null_aware_expression, unused_local_variable, unused_import, sort_constructors_first, prefer_final_fields, prefer_const_constructors, deprecated_member_use, implicit_call_tearoffs import 'package:equatable/equatable.dart'; +/// Staffing and spend forecast report. +/// +/// Returned by `GET /client/reports/forecast`. class ForecastReport extends Equatable { - + /// Creates a [ForecastReport] instance. const ForecastReport({ - required this.projectedSpend, - required this.projectedWorkers, - required this.averageLaborCost, - required this.chartData, - this.totalShifts = 0, - this.totalHours = 0.0, - this.avgWeeklySpend = 0.0, - this.weeklyBreakdown = const [], + required this.forecastSpendCents, + required this.averageWeeklySpendCents, + required this.totalShifts, + required this.totalWorkerHours, + this.weeks = const [], }); - final double projectedSpend; - final int projectedWorkers; - final double averageLaborCost; - final List chartData; - // New fields for the updated design + /// Deserialises a [ForecastReport] from a V2 API JSON map. + factory ForecastReport.fromJson(Map json) { + final dynamic weeksRaw = json['weeks']; + final List weeksList = weeksRaw is List + ? weeksRaw + .map((dynamic e) => + ForecastWeek.fromJson(e as Map)) + .toList() + : const []; + + return ForecastReport( + forecastSpendCents: (json['forecastSpendCents'] as num).toInt(), + averageWeeklySpendCents: + (json['averageWeeklySpendCents'] as num).toInt(), + totalShifts: (json['totalShifts'] as num).toInt(), + totalWorkerHours: (json['totalWorkerHours'] as num).toDouble(), + weeks: weeksList, + ); + } + + /// Total forecast spend in cents. + final int forecastSpendCents; + + /// Average weekly spend in cents. + final int averageWeeklySpendCents; + + /// Total forecasted shifts. final int totalShifts; - final double totalHours; - final double avgWeeklySpend; - final List weeklyBreakdown; + + /// Total forecasted worker-hours. + final double totalWorkerHours; + + /// Weekly breakdown. + final List weeks; + + /// Serialises this [ForecastReport] to a JSON map. + Map toJson() { + return { + 'forecastSpendCents': forecastSpendCents, + 'averageWeeklySpendCents': averageWeeklySpendCents, + 'totalShifts': totalShifts, + 'totalWorkerHours': totalWorkerHours, + 'weeks': weeks.map((ForecastWeek w) => w.toJson()).toList(), + }; + } @override List get props => [ - projectedSpend, - projectedWorkers, - averageLaborCost, - chartData, + forecastSpendCents, + averageWeeklySpendCents, totalShifts, - totalHours, - avgWeeklySpend, - weeklyBreakdown, + totalWorkerHours, + weeks, ]; } -class ForecastPoint extends Equatable { - - const ForecastPoint({ - required this.date, - required this.projectedCost, - required this.workersNeeded, - }); - final DateTime date; - final double projectedCost; - final int workersNeeded; - - @override - List get props => [date, projectedCost, workersNeeded]; -} - +/// A single week in the forecast breakdown. class ForecastWeek extends Equatable { - + /// Creates a [ForecastWeek] instance. const ForecastWeek({ - required this.weekNumber, - required this.totalCost, - required this.shiftsCount, - required this.hoursCount, - required this.avgCostPerShift, + required this.week, + required this.shiftCount, + required this.workerHours, + required this.forecastSpendCents, + required this.averageShiftCostCents, }); - final int weekNumber; - final double totalCost; - final int shiftsCount; - final double hoursCount; - final double avgCostPerShift; + + /// Deserialises a [ForecastWeek] from a V2 API JSON map. + factory ForecastWeek.fromJson(Map json) { + return ForecastWeek( + week: DateTime.parse(json['week'] as String), + shiftCount: (json['shiftCount'] as num).toInt(), + workerHours: (json['workerHours'] as num).toDouble(), + forecastSpendCents: (json['forecastSpendCents'] as num).toInt(), + averageShiftCostCents: (json['averageShiftCostCents'] as num).toInt(), + ); + } + + /// Start of the week. + final DateTime week; + + /// Number of shifts in this week. + final int shiftCount; + + /// Total worker-hours in this week. + final double workerHours; + + /// Forecast spend in cents for this week. + final int forecastSpendCents; + + /// Average cost per shift in cents. + final int averageShiftCostCents; + + /// Serialises this [ForecastWeek] to a JSON map. + Map toJson() { + return { + 'week': week.toIso8601String(), + 'shiftCount': shiftCount, + 'workerHours': workerHours, + 'forecastSpendCents': forecastSpendCents, + 'averageShiftCostCents': averageShiftCostCents, + }; + } @override List get props => [ - weekNumber, - totalCost, - shiftsCount, - hoursCount, - avgCostPerShift, + week, + shiftCount, + workerHours, + forecastSpendCents, + averageShiftCostCents, ]; } - diff --git a/apps/mobile/packages/domain/lib/src/entities/reports/no_show_report.dart b/apps/mobile/packages/domain/lib/src/entities/reports/no_show_report.dart index 5e6f9fe7..f4f9047c 100644 --- a/apps/mobile/packages/domain/lib/src/entities/reports/no_show_report.dart +++ b/apps/mobile/packages/domain/lib/src/entities/reports/no_show_report.dart @@ -1,35 +1,174 @@ -// ignore_for_file: always_specify_types, depend_on_referenced_packages, dead_code, dead_null_aware_expression, unused_local_variable, unused_import, sort_constructors_first, prefer_final_fields, prefer_const_constructors, deprecated_member_use, implicit_call_tearoffs import 'package:equatable/equatable.dart'; +/// No-show report with per-worker incident details. +/// +/// Returned by `GET /client/reports/no-show`. class NoShowReport extends Equatable { - + /// Creates a [NoShowReport] instance. const NoShowReport({ - required this.totalNoShows, - required this.noShowRate, - required this.flaggedWorkers, + required this.totalNoShowCount, + required this.noShowRatePercentage, + required this.workersWhoNoShowed, + this.items = const [], }); - final int totalNoShows; - final double noShowRate; - final List flaggedWorkers; + + /// Deserialises a [NoShowReport] from a V2 API JSON map. + factory NoShowReport.fromJson(Map json) { + final dynamic itemsRaw = json['items']; + final List itemsList = itemsRaw is List + ? itemsRaw + .map((dynamic e) => + NoShowWorkerItem.fromJson(e as Map)) + .toList() + : const []; + + return NoShowReport( + totalNoShowCount: (json['totalNoShowCount'] as num).toInt(), + noShowRatePercentage: (json['noShowRatePercentage'] as num).toInt(), + workersWhoNoShowed: (json['workersWhoNoShowed'] as num).toInt(), + items: itemsList, + ); + } + + /// Total no-show incidents in the period. + final int totalNoShowCount; + + /// No-show rate as a percentage (0-100). + final int noShowRatePercentage; + + /// Distinct workers who had at least one no-show. + final int workersWhoNoShowed; + + /// Per-worker breakdown with individual incidents. + final List items; + + /// Serialises this [NoShowReport] to a JSON map. + Map toJson() { + return { + 'totalNoShowCount': totalNoShowCount, + 'noShowRatePercentage': noShowRatePercentage, + 'workersWhoNoShowed': workersWhoNoShowed, + 'items': items.map((NoShowWorkerItem i) => i.toJson()).toList(), + }; + } @override - List get props => [totalNoShows, noShowRate, flaggedWorkers]; + List get props => [ + totalNoShowCount, + noShowRatePercentage, + workersWhoNoShowed, + items, + ]; } -class NoShowWorker extends Equatable { - - const NoShowWorker({ - required this.id, - required this.fullName, - required this.noShowCount, - required this.reliabilityScore, +/// A worker with no-show incidents. +class NoShowWorkerItem extends Equatable { + /// Creates a [NoShowWorkerItem] instance. + const NoShowWorkerItem({ + required this.staffId, + required this.staffName, + required this.incidentCount, + required this.riskStatus, + this.incidents = const [], }); - final String id; - final String fullName; - final int noShowCount; - final double reliabilityScore; + + /// Deserialises a [NoShowWorkerItem] from a V2 API JSON map. + factory NoShowWorkerItem.fromJson(Map json) { + final dynamic incidentsRaw = json['incidents']; + final List incidentsList = incidentsRaw is List + ? incidentsRaw + .map((dynamic e) => + NoShowIncident.fromJson(e as Map)) + .toList() + : const []; + + return NoShowWorkerItem( + staffId: json['staffId'] as String, + staffName: json['staffName'] as String, + incidentCount: (json['incidentCount'] as num).toInt(), + riskStatus: json['riskStatus'] as String, + incidents: incidentsList, + ); + } + + /// Staff member ID. + final String staffId; + + /// Staff display name. + final String staffName; + + /// Total no-show count. + final int incidentCount; + + /// Risk level (HIGH, MEDIUM). + final String riskStatus; + + /// Individual no-show incidents. + final List incidents; + + /// Serialises this [NoShowWorkerItem] to a JSON map. + Map toJson() { + return { + 'staffId': staffId, + 'staffName': staffName, + 'incidentCount': incidentCount, + 'riskStatus': riskStatus, + 'incidents': incidents.map((NoShowIncident i) => i.toJson()).toList(), + }; + } @override - List get props => [id, fullName, noShowCount, reliabilityScore]; + List get props => [ + staffId, + staffName, + incidentCount, + riskStatus, + incidents, + ]; } +/// A single no-show incident. +class NoShowIncident extends Equatable { + /// Creates a [NoShowIncident] instance. + const NoShowIncident({ + required this.shiftId, + required this.shiftTitle, + required this.roleName, + required this.date, + }); + + /// Deserialises a [NoShowIncident] from a V2 API JSON map. + factory NoShowIncident.fromJson(Map json) { + return NoShowIncident( + shiftId: json['shiftId'] as String, + shiftTitle: json['shiftTitle'] as String, + roleName: json['roleName'] as String, + date: DateTime.parse(json['date'] as String), + ); + } + + /// Shift ID. + final String shiftId; + + /// Shift title. + final String shiftTitle; + + /// Role name. + final String roleName; + + /// Date of the incident. + final DateTime date; + + /// Serialises this [NoShowIncident] to a JSON map. + Map toJson() { + return { + 'shiftId': shiftId, + 'shiftTitle': shiftTitle, + 'roleName': roleName, + 'date': date.toIso8601String(), + }; + } + + @override + List get props => [shiftId, shiftTitle, roleName, date]; +} diff --git a/apps/mobile/packages/domain/lib/src/entities/reports/performance_report.dart b/apps/mobile/packages/domain/lib/src/entities/reports/performance_report.dart index 51bb79b5..1e29bedc 100644 --- a/apps/mobile/packages/domain/lib/src/entities/reports/performance_report.dart +++ b/apps/mobile/packages/domain/lib/src/entities/reports/performance_report.dart @@ -1,37 +1,78 @@ -// ignore_for_file: always_specify_types, depend_on_referenced_packages, dead_code, dead_null_aware_expression, unused_local_variable, unused_import, sort_constructors_first, prefer_final_fields, prefer_const_constructors, deprecated_member_use, implicit_call_tearoffs import 'package:equatable/equatable.dart'; +/// Workforce performance report. +/// +/// Returned by `GET /client/reports/performance`. class PerformanceReport extends Equatable { - + /// Creates a [PerformanceReport] instance. const PerformanceReport({ - required this.fillRate, - required this.completionRate, - required this.onTimeRate, - required this.avgFillTimeHours, - required this.keyPerformanceIndicators, + required this.averagePerformanceScore, + required this.fillRatePercentage, + required this.completionRatePercentage, + required this.onTimeRatePercentage, + required this.averageFillTimeMinutes, + required this.totalShiftsCovered, + required this.noShowRatePercentage, }); - final double fillRate; - final double completionRate; - final double onTimeRate; - final double avgFillTimeHours; // in hours - final List keyPerformanceIndicators; + + /// Deserialises a [PerformanceReport] from a V2 API JSON map. + factory PerformanceReport.fromJson(Map json) { + return PerformanceReport( + averagePerformanceScore: + (json['averagePerformanceScore'] as num).toDouble(), + fillRatePercentage: (json['fillRatePercentage'] as num).toInt(), + completionRatePercentage: + (json['completionRatePercentage'] as num).toInt(), + onTimeRatePercentage: (json['onTimeRatePercentage'] as num).toInt(), + averageFillTimeMinutes: + (json['averageFillTimeMinutes'] as num).toDouble(), + totalShiftsCovered: (json['totalShiftsCovered'] as num).toInt(), + noShowRatePercentage: (json['noShowRatePercentage'] as num).toInt(), + ); + } + + /// Average staff review score (0-5). + final double averagePerformanceScore; + + /// Percentage of shifts fully filled (0-100). + final int fillRatePercentage; + + /// Percentage of shifts completed (0-100). + final int completionRatePercentage; + + /// Percentage of on-time arrivals (0-100). + final int onTimeRatePercentage; + + /// Average time to fill a shift in minutes. + final double averageFillTimeMinutes; + + /// Total shifts that were completed/covered. + final int totalShiftsCovered; + + /// No-show rate as a percentage (0-100). + final int noShowRatePercentage; + + /// Serialises this [PerformanceReport] to a JSON map. + Map toJson() { + return { + 'averagePerformanceScore': averagePerformanceScore, + 'fillRatePercentage': fillRatePercentage, + 'completionRatePercentage': completionRatePercentage, + 'onTimeRatePercentage': onTimeRatePercentage, + 'averageFillTimeMinutes': averageFillTimeMinutes, + 'totalShiftsCovered': totalShiftsCovered, + 'noShowRatePercentage': noShowRatePercentage, + }; + } @override - List get props => [fillRate, completionRate, onTimeRate, avgFillTimeHours, keyPerformanceIndicators]; + List get props => [ + averagePerformanceScore, + fillRatePercentage, + completionRatePercentage, + onTimeRatePercentage, + averageFillTimeMinutes, + totalShiftsCovered, + noShowRatePercentage, + ]; } - -class PerformanceMetric extends Equatable { // e.g. 0.05 for +5% - - const PerformanceMetric({ - required this.label, - required this.value, - required this.trend, - }); - final String label; - final String value; - final double trend; - - @override - List get props => [label, value, trend]; -} - diff --git a/apps/mobile/packages/domain/lib/src/entities/reports/report_summary.dart b/apps/mobile/packages/domain/lib/src/entities/reports/report_summary.dart new file mode 100644 index 00000000..0ef8cdec --- /dev/null +++ b/apps/mobile/packages/domain/lib/src/entities/reports/report_summary.dart @@ -0,0 +1,71 @@ +import 'package:equatable/equatable.dart'; + +/// High-level summary of all report metrics. +/// +/// Returned by `GET /client/reports/summary`. +class ReportSummary extends Equatable { + /// Creates a [ReportSummary] instance. + const ReportSummary({ + required this.totalShifts, + required this.totalSpendCents, + required this.averageCoveragePercentage, + required this.averagePerformanceScore, + required this.noShowCount, + required this.forecastAccuracyPercentage, + }); + + /// Deserialises a [ReportSummary] from a V2 API JSON map. + factory ReportSummary.fromJson(Map json) { + return ReportSummary( + totalShifts: (json['totalShifts'] as num).toInt(), + totalSpendCents: (json['totalSpendCents'] as num).toInt(), + averageCoveragePercentage: + (json['averageCoveragePercentage'] as num).toInt(), + averagePerformanceScore: + (json['averagePerformanceScore'] as num).toDouble(), + noShowCount: (json['noShowCount'] as num).toInt(), + forecastAccuracyPercentage: + (json['forecastAccuracyPercentage'] as num).toInt(), + ); + } + + /// Total number of shifts in the period. + final int totalShifts; + + /// Total spend in cents. + final int totalSpendCents; + + /// Average coverage percentage (0-100). + final int averageCoveragePercentage; + + /// Average performance score (0-5). + final double averagePerformanceScore; + + /// Total no-show incidents. + final int noShowCount; + + /// Forecast accuracy percentage. + final int forecastAccuracyPercentage; + + /// Serialises this [ReportSummary] to a JSON map. + Map toJson() { + return { + 'totalShifts': totalShifts, + 'totalSpendCents': totalSpendCents, + 'averageCoveragePercentage': averageCoveragePercentage, + 'averagePerformanceScore': averagePerformanceScore, + 'noShowCount': noShowCount, + 'forecastAccuracyPercentage': forecastAccuracyPercentage, + }; + } + + @override + List get props => [ + totalShifts, + totalSpendCents, + averageCoveragePercentage, + averagePerformanceScore, + noShowCount, + forecastAccuracyPercentage, + ]; +} diff --git a/apps/mobile/packages/domain/lib/src/entities/reports/reports_summary.dart b/apps/mobile/packages/domain/lib/src/entities/reports/reports_summary.dart deleted file mode 100644 index 0fb635e5..00000000 --- a/apps/mobile/packages/domain/lib/src/entities/reports/reports_summary.dart +++ /dev/null @@ -1,31 +0,0 @@ -// ignore_for_file: always_specify_types, depend_on_referenced_packages, dead_code, dead_null_aware_expression, unused_local_variable, unused_import, sort_constructors_first, prefer_final_fields, prefer_const_constructors, deprecated_member_use, implicit_call_tearoffs -import 'package:equatable/equatable.dart'; - -class ReportsSummary extends Equatable { - - const ReportsSummary({ - required this.totalHours, - required this.otHours, - required this.totalSpend, - required this.fillRate, - required this.avgFillTimeHours, - required this.noShowRate, - }); - final double totalHours; - final double otHours; - final double totalSpend; - final double fillRate; - final double avgFillTimeHours; - final double noShowRate; - - @override - List get props => [ - totalHours, - otHours, - totalSpend, - fillRate, - avgFillTimeHours, - noShowRate, - ]; -} - diff --git a/apps/mobile/packages/domain/lib/src/entities/reports/spend_data_point.dart b/apps/mobile/packages/domain/lib/src/entities/reports/spend_data_point.dart new file mode 100644 index 00000000..30480fae --- /dev/null +++ b/apps/mobile/packages/domain/lib/src/entities/reports/spend_data_point.dart @@ -0,0 +1,95 @@ +import 'package:equatable/equatable.dart'; + +import '../financial/spend_item.dart'; + +/// Spend report with total, chart data points, and category breakdown. +/// +/// Returned by `GET /client/reports/spend`. +class SpendReport extends Equatable { + /// Creates a [SpendReport] instance. + const SpendReport({ + required this.totalSpendCents, + this.chart = const [], + this.breakdown = const [], + }); + + /// Deserialises a [SpendReport] from a V2 API JSON map. + factory SpendReport.fromJson(Map json) { + final dynamic chartRaw = json['chart']; + final List chartList = chartRaw is List + ? chartRaw + .map((dynamic e) => + SpendDataPoint.fromJson(e as Map)) + .toList() + : const []; + + final dynamic breakdownRaw = json['breakdown']; + final List breakdownList = breakdownRaw is List + ? breakdownRaw + .map((dynamic e) => + SpendItem.fromJson(e as Map)) + .toList() + : const []; + + return SpendReport( + totalSpendCents: (json['totalSpendCents'] as num).toInt(), + chart: chartList, + breakdown: breakdownList, + ); + } + + /// Total spend in cents for the period. + final int totalSpendCents; + + /// Time-series chart data. + final List chart; + + /// Category breakdown. + final List breakdown; + + /// Serialises this [SpendReport] to a JSON map. + Map toJson() { + return { + 'totalSpendCents': totalSpendCents, + 'chart': chart.map((SpendDataPoint p) => p.toJson()).toList(), + 'breakdown': breakdown.map((SpendItem i) => i.toJson()).toList(), + }; + } + + @override + List get props => [totalSpendCents, chart, breakdown]; +} + +/// A single data point in the spend chart. +class SpendDataPoint extends Equatable { + /// Creates a [SpendDataPoint] instance. + const SpendDataPoint({ + required this.bucket, + required this.amountCents, + }); + + /// Deserialises a [SpendDataPoint] from a V2 API JSON map. + factory SpendDataPoint.fromJson(Map json) { + return SpendDataPoint( + bucket: DateTime.parse(json['bucket'] as String), + amountCents: (json['amountCents'] as num).toInt(), + ); + } + + /// Time bucket (day or week). + final DateTime bucket; + + /// Amount in cents for this bucket. + final int amountCents; + + /// Serialises this [SpendDataPoint] to a JSON map. + Map toJson() { + return { + 'bucket': bucket.toIso8601String(), + 'amountCents': amountCents, + }; + } + + @override + List get props => [bucket, amountCents]; +} diff --git a/apps/mobile/packages/domain/lib/src/entities/reports/spend_report.dart b/apps/mobile/packages/domain/lib/src/entities/reports/spend_report.dart deleted file mode 100644 index 8594fe96..00000000 --- a/apps/mobile/packages/domain/lib/src/entities/reports/spend_report.dart +++ /dev/null @@ -1,85 +0,0 @@ -// ignore_for_file: always_specify_types, depend_on_referenced_packages, dead_code, dead_null_aware_expression, unused_local_variable, unused_import, sort_constructors_first, prefer_final_fields, prefer_const_constructors, deprecated_member_use, implicit_call_tearoffs -import 'package:equatable/equatable.dart'; - -class SpendReport extends Equatable { - - const SpendReport({ - required this.totalSpend, - required this.averageCost, - required this.paidInvoices, - required this.pendingInvoices, - required this.overdueInvoices, - required this.invoices, - required this.chartData, - required this.industryBreakdown, - }); - final double totalSpend; - final double averageCost; - final int paidInvoices; - final int pendingInvoices; - final int overdueInvoices; - final List invoices; - final List chartData; - final List industryBreakdown; - - @override - List get props => [ - totalSpend, - averageCost, - paidInvoices, - pendingInvoices, - overdueInvoices, - invoices, - chartData, - industryBreakdown, - ]; -} - -class SpendIndustryCategory extends Equatable { - - const SpendIndustryCategory({ - required this.name, - required this.amount, - required this.percentage, - }); - final String name; - final double amount; - final double percentage; - - @override - List get props => [name, amount, percentage]; -} - -class SpendInvoice extends Equatable { - - const SpendInvoice({ - required this.id, - required this.invoiceNumber, - required this.issueDate, - required this.amount, - required this.status, - required this.vendorName, - this.industry, - }); - final String id; - final String invoiceNumber; - final DateTime issueDate; - final double amount; - final String status; - final String vendorName; - final String? industry; - - @override - List get props => [id, invoiceNumber, issueDate, amount, status, vendorName, industry]; -} - -class SpendChartPoint extends Equatable { - - const SpendChartPoint({required this.date, required this.amount}); - final DateTime date; - final double amount; - - @override - List get props => [date, amount]; -} - diff --git a/apps/mobile/packages/domain/lib/src/entities/shifts/assigned_shift.dart b/apps/mobile/packages/domain/lib/src/entities/shifts/assigned_shift.dart new file mode 100644 index 00000000..a41371be --- /dev/null +++ b/apps/mobile/packages/domain/lib/src/entities/shifts/assigned_shift.dart @@ -0,0 +1,100 @@ +import 'package:equatable/equatable.dart'; + +import 'package:krow_domain/src/entities/enums/assignment_status.dart'; +import 'package:krow_domain/src/entities/enums/order_type.dart'; + +/// A shift that has been assigned to the staff member. +/// +/// Returned by `GET /staff/shifts/assigned`. Represents an upcoming or +/// in-progress assignment with scheduling and pay information. +class AssignedShift extends Equatable { + /// Creates an [AssignedShift]. + const AssignedShift({ + required this.assignmentId, + required this.shiftId, + required this.roleName, + required this.location, + required this.date, + required this.startTime, + required this.endTime, + required this.hourlyRateCents, + required this.orderType, + required this.status, + }); + + /// Deserialises from the V2 API JSON response. + factory AssignedShift.fromJson(Map json) { + return AssignedShift( + assignmentId: json['assignmentId'] as String, + shiftId: json['shiftId'] as String, + roleName: json['roleName'] as String, + location: json['location'] as String? ?? '', + date: DateTime.parse(json['date'] as String), + startTime: DateTime.parse(json['startTime'] as String), + endTime: DateTime.parse(json['endTime'] as String), + hourlyRateCents: json['hourlyRateCents'] as int? ?? 0, + orderType: OrderType.fromJson(json['orderType'] as String?), + status: AssignmentStatus.fromJson(json['status'] as String?), + ); + } + + /// The assignment row id. + final String assignmentId; + + /// The shift row id. + final String shiftId; + + /// Display name of the role the worker is filling. + final String roleName; + + /// Human-readable location label. + final String location; + + /// The date of the shift (same as startTime but kept for grouping). + final DateTime date; + + /// Scheduled start time. + final DateTime startTime; + + /// Scheduled end time. + final DateTime endTime; + + /// Pay rate in cents per hour. + final int hourlyRateCents; + + /// Order type. + final OrderType orderType; + + /// Assignment status. + final AssignmentStatus status; + + /// Serialises to JSON. + Map toJson() { + return { + 'assignmentId': assignmentId, + 'shiftId': shiftId, + 'roleName': roleName, + 'location': location, + 'date': date.toIso8601String(), + 'startTime': startTime.toIso8601String(), + 'endTime': endTime.toIso8601String(), + 'hourlyRateCents': hourlyRateCents, + 'orderType': orderType.toJson(), + 'status': status.toJson(), + }; + } + + @override + List get props => [ + assignmentId, + shiftId, + roleName, + location, + date, + startTime, + endTime, + hourlyRateCents, + orderType, + status, + ]; +} diff --git a/apps/mobile/packages/domain/lib/src/entities/shifts/break/break.dart b/apps/mobile/packages/domain/lib/src/entities/shifts/break/break.dart deleted file mode 100644 index b90750bd..00000000 --- a/apps/mobile/packages/domain/lib/src/entities/shifts/break/break.dart +++ /dev/null @@ -1,47 +0,0 @@ -import 'package:equatable/equatable.dart'; - -/// Enum representing common break durations in minutes. -enum BreakDuration { - /// No break. - none(0), - - /// 10 minutes break. - ten(10), - - /// 15 minutes break. - fifteen(15), - - /// 20 minutes break. - twenty(20), - - /// 30 minutes break. - thirty(30), - - /// 45 minutes break. - fortyFive(45), - - /// 60 minutes break. - sixty(60); - - /// The duration in minutes. - final int minutes; - - const BreakDuration(this.minutes); -} - -/// Represents a break configuration for a shift. -class Break extends Equatable { - const Break({ - required this.duration, - required this.isBreakPaid, - }); - - /// The duration of the break. - final BreakDuration duration; - - /// Whether the break is paid or unpaid. - final bool isBreakPaid; - - @override - List get props => [duration, isBreakPaid]; -} diff --git a/apps/mobile/packages/domain/lib/src/entities/shifts/cancelled_shift.dart b/apps/mobile/packages/domain/lib/src/entities/shifts/cancelled_shift.dart new file mode 100644 index 00000000..6fb4741d --- /dev/null +++ b/apps/mobile/packages/domain/lib/src/entities/shifts/cancelled_shift.dart @@ -0,0 +1,69 @@ +import 'package:equatable/equatable.dart'; + +/// A shift whose assignment was cancelled. +/// +/// Returned by `GET /staff/shifts/cancelled`. Shows past assignments +/// that were cancelled along with the reason, if provided. +class CancelledShift extends Equatable { + /// Creates a [CancelledShift]. + const CancelledShift({ + required this.assignmentId, + required this.shiftId, + required this.title, + required this.location, + required this.date, + this.cancellationReason, + }); + + /// Deserialises from the V2 API JSON response. + factory CancelledShift.fromJson(Map json) { + return CancelledShift( + assignmentId: json['assignmentId'] as String, + shiftId: json['shiftId'] as String, + title: json['title'] as String? ?? '', + location: json['location'] as String? ?? '', + date: DateTime.parse(json['date'] as String), + cancellationReason: json['cancellationReason'] as String?, + ); + } + + /// The assignment row id. + final String assignmentId; + + /// The shift row id. + final String shiftId; + + /// Display title of the shift. + final String title; + + /// Human-readable location label. + final String location; + + /// The original date of the shift. + final DateTime date; + + /// Reason for cancellation, from assignment metadata. + final String? cancellationReason; + + /// Serialises to JSON. + Map toJson() { + return { + 'assignmentId': assignmentId, + 'shiftId': shiftId, + 'title': title, + 'location': location, + 'date': date.toIso8601String(), + 'cancellationReason': cancellationReason, + }; + } + + @override + List get props => [ + assignmentId, + shiftId, + title, + location, + date, + cancellationReason, + ]; +} diff --git a/apps/mobile/packages/domain/lib/src/entities/shifts/completed_shift.dart b/apps/mobile/packages/domain/lib/src/entities/shifts/completed_shift.dart new file mode 100644 index 00000000..01b6f005 --- /dev/null +++ b/apps/mobile/packages/domain/lib/src/entities/shifts/completed_shift.dart @@ -0,0 +1,78 @@ +import 'package:equatable/equatable.dart'; + +import 'package:krow_domain/src/entities/enums/payment_status.dart'; + +/// A shift the staff member has completed. +/// +/// Returned by `GET /staff/shifts/completed`. Includes worked time and +/// payment tracking status. +class CompletedShift extends Equatable { + /// Creates a [CompletedShift]. + const CompletedShift({ + required this.assignmentId, + required this.shiftId, + required this.title, + required this.location, + required this.date, + required this.minutesWorked, + required this.paymentStatus, + }); + + /// Deserialises from the V2 API JSON response. + factory CompletedShift.fromJson(Map json) { + return CompletedShift( + assignmentId: json['assignmentId'] as String, + shiftId: json['shiftId'] as String, + title: json['title'] as String? ?? '', + location: json['location'] as String? ?? '', + date: DateTime.parse(json['date'] as String), + minutesWorked: json['minutesWorked'] as int? ?? 0, + paymentStatus: PaymentStatus.fromJson(json['paymentStatus'] as String?), + ); + } + + /// The assignment row id. + final String assignmentId; + + /// The shift row id. + final String shiftId; + + /// Display title of the shift. + final String title; + + /// Human-readable location label. + final String location; + + /// The date the shift was worked. + final DateTime date; + + /// Total minutes worked (regular + overtime). + final int minutesWorked; + + /// Payment processing status. + final PaymentStatus paymentStatus; + + /// Serialises to JSON. + Map toJson() { + return { + 'assignmentId': assignmentId, + 'shiftId': shiftId, + 'title': title, + 'location': location, + 'date': date.toIso8601String(), + 'minutesWorked': minutesWorked, + 'paymentStatus': paymentStatus.toJson(), + }; + } + + @override + List get props => [ + assignmentId, + shiftId, + title, + location, + date, + minutesWorked, + paymentStatus, + ]; +} diff --git a/apps/mobile/packages/domain/lib/src/entities/shifts/open_shift.dart b/apps/mobile/packages/domain/lib/src/entities/shifts/open_shift.dart new file mode 100644 index 00000000..8481b343 --- /dev/null +++ b/apps/mobile/packages/domain/lib/src/entities/shifts/open_shift.dart @@ -0,0 +1,106 @@ +import 'package:equatable/equatable.dart'; + +import 'package:krow_domain/src/entities/enums/order_type.dart'; + +/// An open shift available for the staff member to apply to. +/// +/// Returned by `GET /staff/shifts/open`. Includes both genuinely open +/// shift-roles and swap-requested assignments from other workers. +class OpenShift extends Equatable { + /// Creates an [OpenShift]. + const OpenShift({ + required this.shiftId, + required this.roleId, + required this.roleName, + required this.location, + required this.date, + required this.startTime, + required this.endTime, + required this.hourlyRateCents, + required this.orderType, + required this.instantBook, + required this.requiredWorkerCount, + }); + + /// Deserialises from the V2 API JSON response. + factory OpenShift.fromJson(Map json) { + return OpenShift( + shiftId: json['shiftId'] as String, + roleId: json['roleId'] as String, + roleName: json['roleName'] as String, + location: json['location'] as String? ?? '', + date: DateTime.parse(json['date'] as String), + startTime: DateTime.parse(json['startTime'] as String), + endTime: DateTime.parse(json['endTime'] as String), + hourlyRateCents: json['hourlyRateCents'] as int? ?? 0, + orderType: OrderType.fromJson(json['orderType'] as String?), + instantBook: json['instantBook'] as bool? ?? false, + requiredWorkerCount: json['requiredWorkerCount'] as int? ?? 1, + ); + } + + /// The shift row id. + final String shiftId; + + /// The shift-role row id. + final String roleId; + + /// Display name of the role. + final String roleName; + + /// Human-readable location label. + final String location; + + /// Date of the shift. + final DateTime date; + + /// Scheduled start time. + final DateTime startTime; + + /// Scheduled end time. + final DateTime endTime; + + /// Pay rate in cents per hour. + final int hourlyRateCents; + + /// Order type. + final OrderType orderType; + + /// Whether the shift supports instant booking (no approval needed). + final bool instantBook; + + /// Number of workers still required for this role. + final int requiredWorkerCount; + + /// Serialises to JSON. + Map toJson() { + return { + 'shiftId': shiftId, + 'roleId': roleId, + 'roleName': roleName, + 'location': location, + 'date': date.toIso8601String(), + 'startTime': startTime.toIso8601String(), + 'endTime': endTime.toIso8601String(), + 'hourlyRateCents': hourlyRateCents, + 'orderType': orderType.toJson(), + 'instantBook': instantBook, + 'requiredWorkerCount': requiredWorkerCount, + }; + } + + @override + List get props => [ + shiftId, + roleId, + roleName, + location, + date, + startTime, + endTime, + hourlyRateCents, + orderType, + instantBook, + requiredWorkerCount, + ]; +} diff --git a/apps/mobile/packages/domain/lib/src/entities/shifts/pending_assignment.dart b/apps/mobile/packages/domain/lib/src/entities/shifts/pending_assignment.dart new file mode 100644 index 00000000..b22d6bc4 --- /dev/null +++ b/apps/mobile/packages/domain/lib/src/entities/shifts/pending_assignment.dart @@ -0,0 +1,83 @@ +import 'package:equatable/equatable.dart'; + +/// An assignment awaiting the staff member's acceptance. +/// +/// Returned by `GET /staff/shifts/pending`. These are assignments with +/// status `ASSIGNED` that require the worker to accept or decline. +class PendingAssignment extends Equatable { + /// Creates a [PendingAssignment]. + const PendingAssignment({ + required this.assignmentId, + required this.shiftId, + required this.title, + required this.roleName, + required this.startTime, + required this.endTime, + required this.location, + required this.responseDeadline, + }); + + /// Deserialises from the V2 API JSON response. + factory PendingAssignment.fromJson(Map json) { + return PendingAssignment( + assignmentId: json['assignmentId'] as String, + shiftId: json['shiftId'] as String, + title: json['title'] as String? ?? '', + roleName: json['roleName'] as String, + startTime: DateTime.parse(json['startTime'] as String), + endTime: DateTime.parse(json['endTime'] as String), + location: json['location'] as String? ?? '', + responseDeadline: DateTime.parse(json['responseDeadline'] as String), + ); + } + + /// The assignment row id. + final String assignmentId; + + /// The shift row id. + final String shiftId; + + /// Display title of the shift. + final String title; + + /// Display name of the role the worker is filling. + final String roleName; + + /// Scheduled start time. + final DateTime startTime; + + /// Scheduled end time. + final DateTime endTime; + + /// Human-readable location label. + final String location; + + /// Deadline by which the worker must respond. + final DateTime responseDeadline; + + /// Serialises to JSON. + Map toJson() { + return { + 'assignmentId': assignmentId, + 'shiftId': shiftId, + 'title': title, + 'roleName': roleName, + 'startTime': startTime.toIso8601String(), + 'endTime': endTime.toIso8601String(), + 'location': location, + 'responseDeadline': responseDeadline.toIso8601String(), + }; + } + + @override + List get props => [ + assignmentId, + shiftId, + title, + roleName, + startTime, + endTime, + location, + responseDeadline, + ]; +} diff --git a/apps/mobile/packages/domain/lib/src/entities/shifts/shift.dart b/apps/mobile/packages/domain/lib/src/entities/shifts/shift.dart index a6d6fdeb..4d6f11cd 100644 --- a/apps/mobile/packages/domain/lib/src/entities/shifts/shift.dart +++ b/apps/mobile/packages/domain/lib/src/entities/shifts/shift.dart @@ -1,145 +1,143 @@ import 'package:equatable/equatable.dart'; -import 'package:krow_domain/src/entities/shifts/break/break.dart'; +import 'package:krow_domain/src/entities/enums/shift_status.dart'; + +/// Core shift entity aligned with the V2 `shifts` table. +/// +/// This entity is used by the clock-in feature and other contexts where +/// the full shift record (including geolocation) is needed. For +/// list-view-specific shapes see [TodayShift], [AssignedShift], etc. class Shift extends Equatable { + /// Creates a [Shift]. const Shift({ required this.id, + required this.orderId, required this.title, - required this.clientName, - this.logoUrl, - required this.hourlyRate, - required this.location, - required this.locationAddress, - required this.date, - required this.startTime, - required this.endTime, - required this.createdDate, - this.tipsAvailable, - this.travelTime, - this.mealProvided, - this.parkingAvailable, - this.gasCompensation, - this.description, - this.instructions, - this.managers, + required this.status, + required this.startsAt, + required this.endsAt, + this.timezone = 'UTC', + this.locationName, + this.locationAddress, this.latitude, this.longitude, - this.status, - this.durationDays, - this.requiredSlots, - this.filledSlots, - this.roleId, - this.hasApplied, - this.totalValue, - this.breakInfo, - this.orderId, - this.orderType, - this.startDate, - this.endDate, - this.recurringDays, - this.permanentDays, - this.schedules, + this.geofenceRadiusMeters, + required this.requiredWorkers, + required this.assignedWorkers, + this.notes, }); + /// Deserialises from the V2 API JSON response. + factory Shift.fromJson(Map json) { + return Shift( + id: json['id'] as String, + orderId: json['orderId'] as String, + title: json['title'] as String? ?? '', + status: ShiftStatus.fromJson(json['status'] as String?), + startsAt: DateTime.parse(json['startsAt'] as String), + endsAt: DateTime.parse(json['endsAt'] as String), + timezone: json['timezone'] as String? ?? 'UTC', + locationName: json['locationName'] as String?, + locationAddress: json['locationAddress'] as String?, + latitude: _parseDouble(json['latitude']), + longitude: _parseDouble(json['longitude']), + geofenceRadiusMeters: json['geofenceRadiusMeters'] as int?, + requiredWorkers: json['requiredWorkers'] as int? ?? 1, + assignedWorkers: json['assignedWorkers'] as int? ?? 0, + notes: json['notes'] as String?, + ); + } + + /// The shift row id. final String id; + + /// The parent order id. + final String orderId; + + /// Display title. final String title; - final String clientName; - final String? logoUrl; - final double hourlyRate; - final String location; - final String locationAddress; - final String date; - final String startTime; - final String endTime; - final String createdDate; - final bool? tipsAvailable; - final bool? travelTime; - final bool? mealProvided; - final bool? parkingAvailable; - final bool? gasCompensation; - final String? description; - final String? instructions; - final List? managers; + + /// Shift lifecycle status. + final ShiftStatus status; + + /// Scheduled start timestamp. + final DateTime startsAt; + + /// Scheduled end timestamp. + final DateTime endsAt; + + /// IANA timezone identifier (e.g. `America/New_York`). + final String timezone; + + /// Location display name (from clock-point label or shift). + final String? locationName; + + /// Street address. + final String? locationAddress; + + /// Latitude for geofence validation. final double? latitude; + + /// Longitude for geofence validation. final double? longitude; - final String? status; - final int? durationDays; // For multi-day shifts - final int? requiredSlots; - final int? filledSlots; - final String? roleId; - final bool? hasApplied; - final double? totalValue; - final Break? breakInfo; - final String? orderId; - final String? orderType; - final String? startDate; - final String? endDate; - final List? recurringDays; - final List? permanentDays; - final List? schedules; + + /// Geofence radius in meters; null means use clock-point default. + final int? geofenceRadiusMeters; + + /// Total workers required across all roles. + final int requiredWorkers; + + /// Workers currently assigned. + final int assignedWorkers; + + /// Free-form notes for the shift. + final String? notes; + + /// Serialises to JSON. + Map toJson() { + return { + 'id': id, + 'orderId': orderId, + 'title': title, + 'status': status.toJson(), + 'startsAt': startsAt.toIso8601String(), + 'endsAt': endsAt.toIso8601String(), + 'timezone': timezone, + 'locationName': locationName, + 'locationAddress': locationAddress, + 'latitude': latitude, + 'longitude': longitude, + 'geofenceRadiusMeters': geofenceRadiusMeters, + 'requiredWorkers': requiredWorkers, + 'assignedWorkers': assignedWorkers, + 'notes': notes, + }; + } + + static double? _parseDouble(dynamic value) { + if (value == null) return null; + if (value is double) return value; + if (value is int) return value.toDouble(); + if (value is String) return double.tryParse(value); + return null; + } @override List get props => [ - id, - title, - clientName, - logoUrl, - hourlyRate, - location, - locationAddress, - date, - startTime, - endTime, - createdDate, - tipsAvailable, - travelTime, - mealProvided, - parkingAvailable, - gasCompensation, - description, - instructions, - managers, - latitude, - longitude, - status, - durationDays, - requiredSlots, - filledSlots, - roleId, - hasApplied, - totalValue, - breakInfo, - orderId, - orderType, - startDate, - endDate, - recurringDays, - permanentDays, - schedules, - ]; -} - -class ShiftSchedule extends Equatable { - const ShiftSchedule({ - required this.date, - required this.startTime, - required this.endTime, - }); - - final String date; - final String startTime; - final String endTime; - - @override - List get props => [date, startTime, endTime]; -} - -class ShiftManager extends Equatable { - const ShiftManager({required this.name, required this.phone, this.avatar}); - - final String name; - final String phone; - final String? avatar; - @override - List get props => [name, phone, avatar]; + id, + orderId, + title, + status, + startsAt, + endsAt, + timezone, + locationName, + locationAddress, + latitude, + longitude, + geofenceRadiusMeters, + requiredWorkers, + assignedWorkers, + notes, + ]; } diff --git a/apps/mobile/packages/domain/lib/src/entities/shifts/shift_detail.dart b/apps/mobile/packages/domain/lib/src/entities/shifts/shift_detail.dart new file mode 100644 index 00000000..793dc07e --- /dev/null +++ b/apps/mobile/packages/domain/lib/src/entities/shifts/shift_detail.dart @@ -0,0 +1,149 @@ +import 'package:equatable/equatable.dart'; + +import 'package:krow_domain/src/entities/enums/application_status.dart'; +import 'package:krow_domain/src/entities/enums/assignment_status.dart'; +import 'package:krow_domain/src/entities/enums/order_type.dart'; + +/// Full detail view of a shift for the staff member. +/// +/// Returned by `GET /staff/shifts/:shiftId`. Contains everything needed +/// to render the shift details page including location coordinates for +/// the map, pay information, and the worker's own assignment/application +/// status if applicable. +class ShiftDetail extends Equatable { + /// Creates a [ShiftDetail]. + const ShiftDetail({ + required this.shiftId, + required this.title, + this.description, + required this.location, + this.address, + required this.date, + required this.startTime, + required this.endTime, + required this.roleId, + required this.roleName, + required this.hourlyRateCents, + required this.orderType, + required this.requiredCount, + required this.confirmedCount, + this.assignmentStatus, + this.applicationStatus, + }); + + /// Deserialises from the V2 API JSON response. + factory ShiftDetail.fromJson(Map json) { + return ShiftDetail( + shiftId: json['shiftId'] as String, + title: json['title'] as String? ?? '', + description: json['description'] as String?, + location: json['location'] as String? ?? '', + address: json['address'] as String?, + date: DateTime.parse(json['date'] as String), + startTime: DateTime.parse(json['startTime'] as String), + endTime: DateTime.parse(json['endTime'] as String), + roleId: json['roleId'] as String, + roleName: json['roleName'] as String, + hourlyRateCents: json['hourlyRateCents'] as int? ?? 0, + orderType: OrderType.fromJson(json['orderType'] as String?), + requiredCount: json['requiredCount'] as int? ?? 1, + confirmedCount: json['confirmedCount'] as int? ?? 0, + assignmentStatus: json['assignmentStatus'] != null + ? AssignmentStatus.fromJson(json['assignmentStatus'] as String?) + : null, + applicationStatus: json['applicationStatus'] != null + ? ApplicationStatus.fromJson(json['applicationStatus'] as String?) + : null, + ); + } + + /// The shift row id. + final String shiftId; + + /// Display title of the shift. + final String title; + + /// Optional description from the order. + final String? description; + + /// Human-readable location label. + final String location; + + /// Street address of the shift location. + final String? address; + + /// Date of the shift (same as startTime, kept for display grouping). + final DateTime date; + + /// Scheduled start time. + final DateTime startTime; + + /// Scheduled end time. + final DateTime endTime; + + /// The shift-role row id. + final String roleId; + + /// Display name of the role. + final String roleName; + + /// Pay rate in cents per hour. + final int hourlyRateCents; + + /// Order type. + final OrderType orderType; + + /// Total workers required for this shift-role. + final int requiredCount; + + /// Workers already confirmed for this shift-role. + final int confirmedCount; + + /// Current worker's assignment status, if assigned. + final AssignmentStatus? assignmentStatus; + + /// Current worker's application status, if applied. + final ApplicationStatus? applicationStatus; + + /// Serialises to JSON. + Map toJson() { + return { + 'shiftId': shiftId, + 'title': title, + 'description': description, + 'location': location, + 'address': address, + 'date': date.toIso8601String(), + 'startTime': startTime.toIso8601String(), + 'endTime': endTime.toIso8601String(), + 'roleId': roleId, + 'roleName': roleName, + 'hourlyRateCents': hourlyRateCents, + 'orderType': orderType.toJson(), + 'requiredCount': requiredCount, + 'confirmedCount': confirmedCount, + 'assignmentStatus': assignmentStatus?.toJson(), + 'applicationStatus': applicationStatus?.toJson(), + }; + } + + @override + List get props => [ + shiftId, + title, + description, + location, + address, + date, + startTime, + endTime, + roleId, + roleName, + hourlyRateCents, + orderType, + requiredCount, + confirmedCount, + assignmentStatus, + applicationStatus, + ]; +} diff --git a/apps/mobile/packages/domain/lib/src/entities/shifts/today_shift.dart b/apps/mobile/packages/domain/lib/src/entities/shifts/today_shift.dart new file mode 100644 index 00000000..e1520964 --- /dev/null +++ b/apps/mobile/packages/domain/lib/src/entities/shifts/today_shift.dart @@ -0,0 +1,88 @@ +import 'package:equatable/equatable.dart'; + +import 'package:krow_domain/src/entities/enums/attendance_status_type.dart'; + +/// A shift assigned to the staff member for today. +/// +/// Returned by `GET /staff/clock-in/shifts/today`. Includes attendance +/// status so the clock-in screen can show whether the worker has already +/// checked in. +class TodayShift extends Equatable { + /// Creates a [TodayShift]. + const TodayShift({ + required this.assignmentId, + required this.shiftId, + required this.roleName, + required this.location, + required this.startTime, + required this.endTime, + required this.attendanceStatus, + this.clockInAt, + }); + + /// Deserialises from the V2 API JSON response. + factory TodayShift.fromJson(Map json) { + return TodayShift( + assignmentId: json['assignmentId'] as String, + shiftId: json['shiftId'] as String, + roleName: json['roleName'] as String, + location: json['location'] as String? ?? '', + startTime: DateTime.parse(json['startTime'] as String), + endTime: DateTime.parse(json['endTime'] as String), + attendanceStatus: AttendanceStatusType.fromJson(json['attendanceStatus'] as String?), + clockInAt: json['clockInAt'] != null + ? DateTime.parse(json['clockInAt'] as String) + : null, + ); + } + + /// The assignment row id. + final String assignmentId; + + /// The shift row id. + final String shiftId; + + /// Display name of the role the worker is filling. + final String roleName; + + /// Human-readable location label (clock-point or shift location). + final String location; + + /// Scheduled start time. + final DateTime startTime; + + /// Scheduled end time. + final DateTime endTime; + + /// Current attendance status. + final AttendanceStatusType attendanceStatus; + + /// Timestamp of clock-in, if any. + final DateTime? clockInAt; + + /// Serialises to JSON. + Map toJson() { + return { + 'assignmentId': assignmentId, + 'shiftId': shiftId, + 'roleName': roleName, + 'location': location, + 'startTime': startTime.toIso8601String(), + 'endTime': endTime.toIso8601String(), + 'attendanceStatus': attendanceStatus.toJson(), + 'clockInAt': clockInAt?.toIso8601String(), + }; + } + + @override + List get props => [ + assignmentId, + shiftId, + roleName, + location, + startTime, + endTime, + attendanceStatus, + clockInAt, + ]; +} diff --git a/apps/mobile/packages/domain/lib/src/entities/skills/certificate.dart b/apps/mobile/packages/domain/lib/src/entities/skills/certificate.dart deleted file mode 100644 index fd6065f8..00000000 --- a/apps/mobile/packages/domain/lib/src/entities/skills/certificate.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:equatable/equatable.dart'; - -/// Represents a required certificate definition. -/// -/// Examples: "Food Hygiene Level 2", "SIA Badge". -class Certificate extends Equatable { - - const Certificate({ - required this.id, - required this.name, - required this.isRequired, - }); - /// Unique identifier. - final String id; - - /// Display name of the certificate. - final String name; - - /// Whether this certificate is mandatory for platform access or specific roles. - final bool isRequired; - - @override - List get props => [id, name, isRequired]; -} \ No newline at end of file diff --git a/apps/mobile/packages/domain/lib/src/entities/skills/skill.dart b/apps/mobile/packages/domain/lib/src/entities/skills/skill.dart deleted file mode 100644 index f61b68e7..00000000 --- a/apps/mobile/packages/domain/lib/src/entities/skills/skill.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'package:equatable/equatable.dart'; - -/// Represents a job category / skill type. -/// -/// Examples: "Waiter", "Security Guard", "Bartender". -/// Linked to a [SkillCategory]. -class Skill extends Equatable { - - const Skill({ - required this.id, - required this.categoryId, - required this.name, - required this.basePrice, - }); - /// Unique identifier. - final String id; - - /// The broader category (e.g. "Hospitality"). - final String categoryId; - - /// Display name of the skill. - final String name; - - /// Default hourly rate suggested for this skill. - final double basePrice; - - @override - List get props => [id, categoryId, name, basePrice]; -} \ No newline at end of file diff --git a/apps/mobile/packages/domain/lib/src/entities/skills/skill_category.dart b/apps/mobile/packages/domain/lib/src/entities/skills/skill_category.dart deleted file mode 100644 index 091fce05..00000000 --- a/apps/mobile/packages/domain/lib/src/entities/skills/skill_category.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:equatable/equatable.dart'; - -/// Represents a broad category of skills (e.g. "Hospitality", "Logistics"). -class SkillCategory extends Equatable { - - const SkillCategory({ - required this.id, - required this.name, - }); - /// Unique identifier. - final String id; - - /// Display name. - final String name; - - @override - List get props => [id, name]; -} \ No newline at end of file diff --git a/apps/mobile/packages/domain/lib/src/entities/skills/skill_kit.dart b/apps/mobile/packages/domain/lib/src/entities/skills/skill_kit.dart deleted file mode 100644 index eca88467..00000000 --- a/apps/mobile/packages/domain/lib/src/entities/skills/skill_kit.dart +++ /dev/null @@ -1,32 +0,0 @@ -import 'package:equatable/equatable.dart'; - -/// Represents required equipment or uniform for a specific [Skill]. -/// -/// Examples: "Black Shirt" (Uniform), "Safety Boots" (Equipment). -class SkillKit extends Equatable { - - const SkillKit({ - required this.id, - required this.skillId, - required this.name, - required this.isRequired, - required this.type, - }); - /// Unique identifier. - final String id; - - /// The [Skill] this kit applies to. - final String skillId; - - /// Description of the item. - final String name; - - /// Whether the staff member MUST possess this item. - final bool isRequired; - - /// Type of kit ('uniform' or 'equipment'). - final String type; - - @override - List get props => [id, skillId, name, isRequired, type]; -} \ No newline at end of file diff --git a/apps/mobile/packages/domain/lib/src/entities/skills/staff_skill.dart b/apps/mobile/packages/domain/lib/src/entities/skills/staff_skill.dart deleted file mode 100644 index b868c9d7..00000000 --- a/apps/mobile/packages/domain/lib/src/entities/skills/staff_skill.dart +++ /dev/null @@ -1,58 +0,0 @@ -import 'package:equatable/equatable.dart'; - -/// The expertise level of a staff member in a specific skill. -enum SkillLevel { - /// Entry level. - beginner, - - /// Experienced. - skilled, - - /// Expert / Managerial level. - professional, -} - -/// The verification status of a claimed skill. -enum StaffSkillStatus { - /// Claimed by staff, waiting for admin approval. - pending, - - /// Verified by admin (documents checked). - verified, - - /// Rejected by admin. - rejected, -} - -/// Represents a staff member's qualification in a specific [Skill]. -class StaffSkill extends Equatable { - - const StaffSkill({ - required this.id, - required this.staffId, - required this.skillId, - required this.level, - required this.experienceYears, - required this.status, - }); - /// Unique identifier. - final String id; - - /// The [Staff] member. - final String staffId; - - /// The [Skill] they possess. - final String skillId; - - /// Their expertise level. - final SkillLevel level; - - /// Years of experience. - final int experienceYears; - - /// Verification status. - final StaffSkillStatus status; - - @override - List get props => [id, staffId, skillId, level, experienceYears, status]; -} \ No newline at end of file diff --git a/apps/mobile/packages/domain/lib/src/entities/support/addon.dart b/apps/mobile/packages/domain/lib/src/entities/support/addon.dart deleted file mode 100644 index fd85edba..00000000 --- a/apps/mobile/packages/domain/lib/src/entities/support/addon.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'package:equatable/equatable.dart'; - -/// Represents a financial addon/bonus/deduction applied to an Invoice or Payment. -class Addon extends Equatable { - - const Addon({ - required this.id, - required this.name, - required this.amount, - required this.type, - }); - /// Unique identifier. - final String id; - - /// Description (e.g. "Travel Expense"). - final String name; - - /// Monetary value. - final double amount; - - /// Type ('credit' or 'debit'). - final String type; - - @override - List get props => [id, name, amount, type]; -} \ No newline at end of file diff --git a/apps/mobile/packages/domain/lib/src/entities/support/media.dart b/apps/mobile/packages/domain/lib/src/entities/support/media.dart deleted file mode 100644 index 329cdca6..00000000 --- a/apps/mobile/packages/domain/lib/src/entities/support/media.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:equatable/equatable.dart'; - -/// Represents a media file reference. -/// -/// Used for avatars, certificates, or event photos. -class Media extends Equatable { - - const Media({ - required this.id, - required this.url, - required this.type, - }); - /// Unique identifier. - final String id; - - /// External URL to the file. - final String url; - - /// MIME type or general type (image, pdf). - final String type; - - @override - List get props => [id, url, type]; -} \ No newline at end of file diff --git a/apps/mobile/packages/domain/lib/src/entities/support/tag.dart b/apps/mobile/packages/domain/lib/src/entities/support/tag.dart deleted file mode 100644 index 44d4db9d..00000000 --- a/apps/mobile/packages/domain/lib/src/entities/support/tag.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:equatable/equatable.dart'; - -/// Represents a descriptive tag used for categorizing events or staff. -class Tag extends Equatable { - - const Tag({ - required this.id, - required this.label, - }); - /// Unique identifier. - final String id; - - /// Text label. - final String label; - - @override - List get props => [id, label]; -} \ No newline at end of file diff --git a/apps/mobile/packages/domain/lib/src/entities/support/working_area.dart b/apps/mobile/packages/domain/lib/src/entities/support/working_area.dart deleted file mode 100644 index aa5d8d56..00000000 --- a/apps/mobile/packages/domain/lib/src/entities/support/working_area.dart +++ /dev/null @@ -1,30 +0,0 @@ -import 'package:equatable/equatable.dart'; - -/// Represents a geographical area where a [Staff] member is willing to work. -class WorkingArea extends Equatable { - - const WorkingArea({ - required this.id, - required this.name, - required this.centerLat, - required this.centerLng, - required this.radiusKm, - }); - /// Unique identifier. - final String id; - - /// Name of the area (e.g. "London Zone 1"). - final String name; - - /// Latitude of the center point. - final double centerLat; - - /// Longitude of the center point. - final double centerLng; - - /// Radius in Kilometers. - final double radiusKm; - - @override - List get props => [id, name, centerLat, centerLng, radiusKm]; -} \ No newline at end of file diff --git a/apps/mobile/packages/domain/lib/src/entities/users/biz_member.dart b/apps/mobile/packages/domain/lib/src/entities/users/biz_member.dart index 2f3bcf34..ee071a75 100644 --- a/apps/mobile/packages/domain/lib/src/entities/users/biz_member.dart +++ b/apps/mobile/packages/domain/lib/src/entities/users/biz_member.dart @@ -1,28 +1,172 @@ import 'package:equatable/equatable.dart'; -/// Represents a member of a Business. -/// -/// Grants a user access to business-level operations. -class BizMember extends Equatable { +/// Membership status within a business. +enum BusinessMembershipStatus { + /// The user has been invited but has not accepted. + invited, - const BizMember({ + /// The membership is active. + active, + + /// The membership has been suspended. + suspended, + + /// The membership has been removed. + removed, +} + +/// Role within a business. +enum BusinessRole { + /// Full administrative control. + owner, + + /// Can manage operations but not billing. + manager, + + /// Standard team member. + member, + + /// Read-only access. + viewer, +} + +/// Represents a user's membership in a business. +/// +/// Maps to the V2 `business_memberships` table. +class BusinessMembership extends Equatable { + /// Creates a [BusinessMembership] instance. + const BusinessMembership({ required this.id, + required this.tenantId, required this.businessId, - required this.userId, - required this.role, + this.userId, + this.invitedEmail, + required this.membershipStatus, + required this.businessRole, + this.businessName, + this.businessSlug, + this.metadata = const {}, + this.createdAt, + this.updatedAt, }); - /// Unique identifier for this membership. + + /// Deserialises a [BusinessMembership] from a V2 API JSON response. + factory BusinessMembership.fromJson(Map json) { + return BusinessMembership( + id: json['membershipId'] as String, + tenantId: json['tenantId'] as String, + businessId: json['businessId'] as String, + userId: json['userId'] as String?, + invitedEmail: json['invitedEmail'] as String?, + membershipStatus: _parseMembershipStatus(json['membershipStatus'] as String? ?? json['status'] as String?), + businessRole: _parseBusinessRole(json['role'] as String? ?? json['businessRole'] as String?), + businessName: json['businessName'] as String?, + businessSlug: json['businessSlug'] as String?, + metadata: (json['metadata'] as Map?) ?? const {}, + createdAt: json['createdAt'] != null ? DateTime.parse(json['createdAt'] as String) : null, + updatedAt: json['updatedAt'] != null ? DateTime.parse(json['updatedAt'] as String) : null, + ); + } + + /// Unique membership ID (UUID). final String id; - /// The [Business] the user belongs to. + /// Tenant this membership belongs to. + final String tenantId; + + /// The business this membership grants access to. final String businessId; - /// The [User] who is a member. - final String userId; + /// The user who holds this membership. Null for invite-only records. + final String? userId; - /// The role within the business. - final String role; + /// Email used for the invitation, before the user claims it. + final String? invitedEmail; + + /// Current status of this membership. + final BusinessMembershipStatus membershipStatus; + + /// The role granted within the business. + final BusinessRole businessRole; + + /// Business display name (joined from businesses table in session context). + final String? businessName; + + /// Business URL slug (joined from businesses table in session context). + final String? businessSlug; + + /// Flexible metadata JSON blob. + final Map metadata; + + /// When this membership was created. + final DateTime? createdAt; + + /// When this membership was last updated. + final DateTime? updatedAt; + + /// Serialises this [BusinessMembership] to a JSON map. + Map toJson() { + return { + 'membershipId': id, + 'tenantId': tenantId, + 'businessId': businessId, + 'userId': userId, + 'invitedEmail': invitedEmail, + 'membershipStatus': membershipStatus.name.toUpperCase(), + 'businessRole': businessRole.name.toUpperCase(), + 'businessName': businessName, + 'businessSlug': businessSlug, + 'metadata': metadata, + 'createdAt': createdAt?.toIso8601String(), + 'updatedAt': updatedAt?.toIso8601String(), + }; + } @override - List get props => [id, businessId, userId, role]; -} \ No newline at end of file + List get props => [ + id, + tenantId, + businessId, + userId, + invitedEmail, + membershipStatus, + businessRole, + businessName, + businessSlug, + metadata, + createdAt, + updatedAt, + ]; + + /// Parses a membership status string. + static BusinessMembershipStatus _parseMembershipStatus(String? value) { + switch (value?.toUpperCase()) { + case 'INVITED': + return BusinessMembershipStatus.invited; + case 'ACTIVE': + return BusinessMembershipStatus.active; + case 'SUSPENDED': + return BusinessMembershipStatus.suspended; + case 'REMOVED': + return BusinessMembershipStatus.removed; + default: + return BusinessMembershipStatus.active; + } + } + + /// Parses a business role string. + static BusinessRole _parseBusinessRole(String? value) { + switch (value?.toUpperCase()) { + case 'OWNER': + return BusinessRole.owner; + case 'MANAGER': + return BusinessRole.manager; + case 'MEMBER': + return BusinessRole.member; + case 'VIEWER': + return BusinessRole.viewer; + default: + return BusinessRole.member; + } + } +} diff --git a/apps/mobile/packages/domain/lib/src/entities/users/client_session.dart b/apps/mobile/packages/domain/lib/src/entities/users/client_session.dart new file mode 100644 index 00000000..e0704a1d --- /dev/null +++ b/apps/mobile/packages/domain/lib/src/entities/users/client_session.dart @@ -0,0 +1,128 @@ +import 'package:equatable/equatable.dart'; + +/// Client session context returned by `GET /client/session`. +/// +/// Contains the authenticated client user's profile, their business context, +/// and tenant information. Populated from `requireClientContext()` in the +/// V2 query API. +class ClientSession extends Equatable { + /// Creates a [ClientSession] instance. + const ClientSession({ + required this.businessId, + required this.businessName, + this.businessSlug, + this.businessRole, + this.membershipId, + this.userId, + this.displayName, + this.email, + this.phone, + this.tenantId, + this.tenantName, + this.tenantSlug, + }); + + /// Deserialises a [ClientSession] from the V2 session API response. + /// + /// The response shape comes from `requireClientContext()` which returns + /// `{ user, tenant, business, vendor, staff }`. + factory ClientSession.fromJson(Map json) { + final Map business = + (json['business'] as Map?) ?? const {}; + final Map user = + (json['user'] as Map?) ?? const {}; + final Map tenant = + (json['tenant'] as Map?) ?? const {}; + + return ClientSession( + businessId: business['businessId'] as String, + businessName: business['businessName'] as String, + businessSlug: business['businessSlug'] as String?, + businessRole: business['role'] as String?, + membershipId: business['membershipId'] as String?, + userId: user['userId'] as String?, + displayName: user['displayName'] as String?, + email: user['email'] as String?, + phone: user['phone'] as String?, + tenantId: tenant['tenantId'] as String?, + tenantName: tenant['tenantName'] as String?, + tenantSlug: tenant['tenantSlug'] as String?, + ); + } + + /// Business UUID. + final String businessId; + + /// Business display name. + final String businessName; + + /// Business URL slug. + final String? businessSlug; + + /// The user's role within the business (owner, manager, member, viewer). + final String? businessRole; + + /// Business membership record ID. + final String? membershipId; + + /// Firebase Auth UID. + final String? userId; + + /// User display name. + final String? displayName; + + /// User email address. + final String? email; + + /// User phone number. + final String? phone; + + /// Tenant UUID. + final String? tenantId; + + /// Tenant display name. + final String? tenantName; + + /// Tenant URL slug. + final String? tenantSlug; + + /// Serialises this [ClientSession] to a JSON map. + Map toJson() { + return { + 'business': { + 'businessId': businessId, + 'businessName': businessName, + 'businessSlug': businessSlug, + 'role': businessRole, + 'membershipId': membershipId, + }, + 'user': { + 'userId': userId, + 'displayName': displayName, + 'email': email, + 'phone': phone, + }, + 'tenant': { + 'tenantId': tenantId, + 'tenantName': tenantName, + 'tenantSlug': tenantSlug, + }, + }; + } + + @override + List get props => [ + businessId, + businessName, + businessSlug, + businessRole, + membershipId, + userId, + displayName, + email, + phone, + tenantId, + tenantName, + tenantSlug, + ]; +} diff --git a/apps/mobile/packages/domain/lib/src/entities/users/hub_member.dart b/apps/mobile/packages/domain/lib/src/entities/users/hub_member.dart deleted file mode 100644 index a6bd7a7f..00000000 --- a/apps/mobile/packages/domain/lib/src/entities/users/hub_member.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'package:equatable/equatable.dart'; - -/// Represents a member of a Hub. -/// -/// Grants a user access to specific [Hub] operations, distinct from [BizMember]. -class HubMember extends Equatable { - - const HubMember({ - required this.id, - required this.hubId, - required this.userId, - required this.role, - }); - /// Unique identifier for this membership. - final String id; - - /// The [Hub] the user belongs to. - final String hubId; - - /// The [User] who is a member. - final String userId; - - /// The role within the hub. - final String role; - - @override - List get props => [id, hubId, userId, role]; -} \ No newline at end of file diff --git a/apps/mobile/packages/domain/lib/src/entities/users/membership.dart b/apps/mobile/packages/domain/lib/src/entities/users/membership.dart deleted file mode 100644 index c09ea2ae..00000000 --- a/apps/mobile/packages/domain/lib/src/entities/users/membership.dart +++ /dev/null @@ -1,32 +0,0 @@ -import 'package:equatable/equatable.dart'; - -/// Represents a polymorphic membership to an organization unit. -/// -/// Allows a [User] to be a member of either a [Business] or a [Hub]. -class Membership extends Equatable { - - const Membership({ - required this.id, - required this.userId, - required this.memberableId, - required this.memberableType, - required this.role, - }); - /// Unique identifier for the membership record. - final String id; - - /// The [User] holding this membership. - final String userId; - - /// The ID of the organization unit (Business or Hub). - final String memberableId; - - /// The type of the organization unit ('business' or 'hub'). - final String memberableType; - - /// The role within that organization (e.g., 'manager', 'viewer'). - final String role; - - @override - List get props => [id, userId, memberableId, memberableType, role]; -} \ No newline at end of file diff --git a/apps/mobile/packages/domain/lib/src/entities/users/staff.dart b/apps/mobile/packages/domain/lib/src/entities/users/staff.dart index 4a6eca3b..f0245dea 100644 --- a/apps/mobile/packages/domain/lib/src/entities/users/staff.dart +++ b/apps/mobile/packages/domain/lib/src/entities/users/staff.dart @@ -1,114 +1,211 @@ import 'package:equatable/equatable.dart'; -/// The lifecycle status of a [Staff] account. +/// Lifecycle status of a staff account in V2. enum StaffStatus { - /// Account created but profile not started. - registered, - - /// Profile submitted and awaiting verification. - pending, - - /// Profile information filled but not submitted for verification. - completedProfile, - - /// Profile verified by admin. - verified, - - /// Staff is currently active and eligible for work. + /// Staff is active and eligible for work. active, - /// Account is temporarily suspended. - blocked, + /// Staff has been invited but has not completed onboarding. + invited, - /// Account is permanently inactive. + /// Staff account has been deactivated. inactive, + + /// Staff account has been blocked by an admin. + blocked, } -/// Represents a worker profile. +/// Onboarding progress of a staff member. +enum OnboardingStatus { + /// Onboarding has not started. + pending, + + /// Onboarding is in progress. + inProgress, + + /// Onboarding is complete. + completed, +} + +/// Represents a worker profile in the KROW platform. /// -/// Contains all personal and professional details of a staff member. -/// Linked to a [User] via [authProviderId]. +/// Maps to the V2 `staffs` table. Linked to a [User] via [userId]. class Staff extends Equatable { + /// Creates a [Staff] instance. const Staff({ required this.id, - required this.authProviderId, - required this.name, - required this.email, + required this.tenantId, + this.userId, + required this.fullName, + this.email, this.phone, - this.avatar, required this.status, - this.address, - this.totalShifts, - this.averageRating, - this.onTimeRate, - this.noShowCount, - this.cancellationCount, - this.reliabilityScore, - this.preferredLocations, + this.primaryRole, + required this.onboardingStatus, + this.averageRating = 0, + this.ratingCount = 0, + this.metadata = const {}, + this.workforceId, + this.vendorId, + this.workforceNumber, + this.createdAt, + this.updatedAt, }); - /// Unique identifier for the staff profile. + + /// Deserialises a [Staff] from a V2 API JSON response. + /// + /// Handles both session context shape and standalone staff rows. + factory Staff.fromJson(Map json) { + return Staff( + id: json['staffId'] as String, + tenantId: json['tenantId'] as String, + userId: json['userId'] as String?, + fullName: json['fullName'] as String, + email: json['email'] as String?, + phone: json['phone'] as String?, + status: _parseStaffStatus(json['status'] as String?), + primaryRole: json['primaryRole'] as String?, + onboardingStatus: _parseOnboardingStatus(json['onboardingStatus'] as String?), + averageRating: _parseDouble(json['averageRating']), + ratingCount: (json['ratingCount'] as num?)?.toInt() ?? 0, + metadata: (json['metadata'] as Map?) ?? const {}, + workforceId: json['workforceId'] as String?, + vendorId: json['vendorId'] as String?, + workforceNumber: json['workforceNumber'] as String?, + createdAt: json['createdAt'] != null ? DateTime.parse(json['createdAt'] as String) : null, + updatedAt: json['updatedAt'] != null ? DateTime.parse(json['updatedAt'] as String) : null, + ); + } + + /// Unique staff profile ID (UUID). final String id; - /// Link to the [User] authentication record. - final String authProviderId; + /// Tenant this staff belongs to. + final String tenantId; + + /// Firebase Auth UID linking to the [User] record. Null for invited-only staff. + final String? userId; /// Full display name. - final String name; + final String fullName; /// Contact email. - final String email; + final String? email; /// Contact phone number. final String? phone; - /// Profile picture URL. - final String? avatar; - - /// Current workflow status of the staff member. + /// Current account status. final StaffStatus status; - /// The user's physical address. - /// - /// Can be used for location-based job matching. - final String? address; + /// Primary job role (e.g. "bartender", "server"). + final String? primaryRole; - /// The total number of shifts completed. - final int? totalShifts; + /// Onboarding progress. + final OnboardingStatus onboardingStatus; - /// The average rating from businesses. - final double? averageRating; + /// Average rating from businesses (0.00 - 5.00). + final double averageRating; - /// The percentage of shifts arrived on time. - final int? onTimeRate; + /// Total number of ratings received. + final int ratingCount; - /// The number of no-shows. - final int? noShowCount; + /// Flexible metadata JSON blob. + final Map metadata; - /// The number of cancellations within 24h. - final int? cancellationCount; + /// Workforce record ID if the staff is in a vendor workforce. + final String? workforceId; - /// The reliability score (0-100). - final int? reliabilityScore; + /// Vendor ID associated via workforce. + final String? vendorId; - /// Preferred work locations. - final List? preferredLocations; + /// Workforce number (employee ID within the vendor). + final String? workforceNumber; + + /// When the staff record was created. + final DateTime? createdAt; + + /// When the staff record was last updated. + final DateTime? updatedAt; + + /// Serialises this [Staff] to a JSON map. + Map toJson() { + return { + 'staffId': id, + 'tenantId': tenantId, + 'userId': userId, + 'fullName': fullName, + 'email': email, + 'phone': phone, + 'status': status.name.toUpperCase(), + 'primaryRole': primaryRole, + 'onboardingStatus': onboardingStatus.name.toUpperCase(), + 'averageRating': averageRating, + 'ratingCount': ratingCount, + 'metadata': metadata, + 'workforceId': workforceId, + 'vendorId': vendorId, + 'workforceNumber': workforceNumber, + 'createdAt': createdAt?.toIso8601String(), + 'updatedAt': updatedAt?.toIso8601String(), + }; + } @override List get props => [ id, - authProviderId, - name, + tenantId, + userId, + fullName, email, phone, - avatar, status, - address, - totalShifts, + primaryRole, + onboardingStatus, averageRating, - onTimeRate, - noShowCount, - cancellationCount, - reliabilityScore, - preferredLocations, + ratingCount, + metadata, + workforceId, + vendorId, + workforceNumber, + createdAt, + updatedAt, ]; + + /// Parses a status string into a [StaffStatus]. + static StaffStatus _parseStaffStatus(String? value) { + switch (value?.toUpperCase()) { + case 'ACTIVE': + return StaffStatus.active; + case 'INVITED': + return StaffStatus.invited; + case 'INACTIVE': + return StaffStatus.inactive; + case 'BLOCKED': + return StaffStatus.blocked; + default: + return StaffStatus.active; + } + } + + /// Parses an onboarding status string into an [OnboardingStatus]. + static OnboardingStatus _parseOnboardingStatus(String? value) { + switch (value?.toUpperCase()) { + case 'PENDING': + return OnboardingStatus.pending; + case 'IN_PROGRESS': + return OnboardingStatus.inProgress; + case 'COMPLETED': + return OnboardingStatus.completed; + default: + return OnboardingStatus.pending; + } + } + + /// Safely parses a numeric value to double. + static double _parseDouble(Object? value) { + if (value is num) return value.toDouble(); + if (value is String) return double.tryParse(value) ?? 0; + return 0; + } } diff --git a/apps/mobile/packages/domain/lib/src/entities/users/staff_session.dart b/apps/mobile/packages/domain/lib/src/entities/users/staff_session.dart new file mode 100644 index 00000000..a1b9e4de --- /dev/null +++ b/apps/mobile/packages/domain/lib/src/entities/users/staff_session.dart @@ -0,0 +1,177 @@ +import 'package:equatable/equatable.dart'; + +import 'package:krow_domain/src/entities/enums/onboarding_status.dart'; +import 'package:krow_domain/src/entities/enums/staff_status.dart'; + +/// Staff session context returned by `GET /staff/session`. +/// +/// Contains the authenticated staff member's profile summary, tenant context, +/// and optional workforce/vendor linkage. Populated from the actor-context +/// query in the V2 query API. +class StaffSession extends Equatable { + /// Creates a [StaffSession] instance. + const StaffSession({ + required this.staffId, + required this.tenantId, + required this.fullName, + this.email, + this.phone, + this.primaryRole, + this.onboardingStatus, + this.status, + this.averageRating = 0, + this.ratingCount = 0, + this.workforceId, + this.vendorId, + this.workforceNumber, + this.metadata = const {}, + this.userId, + this.tenantName, + this.tenantSlug, + }); + + /// Deserialises a [StaffSession] from the V2 session API response. + /// + /// The response shape comes from `requireStaffContext()` which returns + /// `{ user, tenant, business, vendor, staff }`. + factory StaffSession.fromJson(Map json) { + final Map staff = + (json['staff'] as Map?) ?? json; + final Map user = + (json['user'] as Map?) ?? const {}; + final Map tenant = + (json['tenant'] as Map?) ?? const {}; + + return StaffSession( + staffId: staff['staffId'] as String, + tenantId: staff['tenantId'] as String, + fullName: staff['fullName'] as String, + email: staff['email'] as String?, + phone: staff['phone'] as String?, + primaryRole: staff['primaryRole'] as String?, + onboardingStatus: staff['onboardingStatus'] != null + ? OnboardingStatus.fromJson(staff['onboardingStatus'] as String?) + : null, + status: staff['status'] != null + ? StaffStatus.fromJson(staff['status'] as String?) + : null, + averageRating: _parseDouble(staff['averageRating']), + ratingCount: (staff['ratingCount'] as num?)?.toInt() ?? 0, + workforceId: staff['workforceId'] as String?, + vendorId: staff['vendorId'] as String?, + workforceNumber: staff['workforceNumber'] as String?, + metadata: (staff['metadata'] as Map?) ?? const {}, + userId: user['userId'] as String?, + tenantName: tenant['tenantName'] as String?, + tenantSlug: tenant['tenantSlug'] as String?, + ); + } + + /// Staff profile UUID. + final String staffId; + + /// Tenant this staff belongs to. + final String tenantId; + + /// Full display name. + final String fullName; + + /// Contact email. + final String? email; + + /// Contact phone number. + final String? phone; + + /// Primary job role code (e.g. "bartender"). + final String? primaryRole; + + /// Onboarding progress. + final OnboardingStatus? onboardingStatus; + + /// Account status. + final StaffStatus? status; + + /// Average rating from businesses (0.00 - 5.00). + final double averageRating; + + /// Total number of ratings received. + final int ratingCount; + + /// Workforce record ID if linked to a vendor. + final String? workforceId; + + /// Vendor ID associated via workforce. + final String? vendorId; + + /// Employee number within the vendor. + final String? workforceNumber; + + /// Flexible metadata JSON blob from the staffs table. + final Map metadata; + + /// Firebase Auth UID from the user context. + final String? userId; + + /// Tenant display name from the tenant context. + final String? tenantName; + + /// Tenant URL slug from the tenant context. + final String? tenantSlug; + + /// Serialises this [StaffSession] to a JSON map. + Map toJson() { + return { + 'staff': { + 'staffId': staffId, + 'tenantId': tenantId, + 'fullName': fullName, + 'email': email, + 'phone': phone, + 'primaryRole': primaryRole, + 'onboardingStatus': onboardingStatus?.toJson(), + 'status': status?.toJson(), + 'averageRating': averageRating, + 'ratingCount': ratingCount, + 'workforceId': workforceId, + 'vendorId': vendorId, + 'workforceNumber': workforceNumber, + 'metadata': metadata, + }, + 'user': { + 'userId': userId, + }, + 'tenant': { + 'tenantName': tenantName, + 'tenantSlug': tenantSlug, + }, + }; + } + + @override + List get props => [ + staffId, + tenantId, + fullName, + email, + phone, + primaryRole, + onboardingStatus, + status, + averageRating, + ratingCount, + workforceId, + vendorId, + workforceNumber, + metadata, + userId, + tenantName, + tenantSlug, + ]; + + /// Safely parses a numeric value to double. + static double _parseDouble(Object? value) { + if (value is num) return value.toDouble(); + if (value is String) return double.tryParse(value) ?? 0; + return 0; + } +} diff --git a/apps/mobile/packages/domain/lib/src/entities/users/user.dart b/apps/mobile/packages/domain/lib/src/entities/users/user.dart index fc300f59..fe2c5785 100644 --- a/apps/mobile/packages/domain/lib/src/entities/users/user.dart +++ b/apps/mobile/packages/domain/lib/src/entities/users/user.dart @@ -1,30 +1,108 @@ import 'package:equatable/equatable.dart'; -/// Represents a base authenticated user in the KROW platform. -/// -/// This entity corresponds to the Firebase Auth user record and acts as the -/// linkage between the authentication system and the specific [Staff] or Client profiles. -class User extends Equatable { +/// Account status for a platform user. +enum UserStatus { + /// User is active and can sign in. + active, + /// User has been invited but has not signed in yet. + invited, + + /// User account has been disabled by an admin. + disabled, +} + +/// Represents an authenticated user in the KROW platform. +/// +/// Maps to the V2 `users` table. The [id] is the Firebase Auth UID. +class User extends Equatable { + /// Creates a [User] instance. const User({ required this.id, - required this.email, + this.email, + this.displayName, this.phone, - required this.role, + required this.status, + this.metadata = const {}, + this.createdAt, + this.updatedAt, }); - /// The unique identifier from the authentication provider (e.g., Firebase UID). + + /// Deserialises a [User] from a V2 API JSON response. + factory User.fromJson(Map json) { + return User( + id: json['userId'] as String, + email: json['email'] as String?, + displayName: json['displayName'] as String?, + phone: json['phone'] as String?, + status: _parseUserStatus(json['status'] as String?), + metadata: (json['metadata'] as Map?) ?? const {}, + createdAt: json['createdAt'] != null ? DateTime.parse(json['createdAt'] as String) : null, + updatedAt: json['updatedAt'] != null ? DateTime.parse(json['updatedAt'] as String) : null, + ); + } + + /// Firebase Auth UID. Primary key in the V2 `users` table. final String id; /// The user's email address. - final String email; + final String? email; - /// The user's phone number, if available. + /// The user's display name. + final String? displayName; + + /// The user's phone number. final String? phone; - /// The primary role of the user (e.g., 'staff', 'client_admin'). - /// This determines the initial routing and permissions. - final String role; + /// Current account status. + final UserStatus status; + + /// Flexible metadata JSON blob. + final Map metadata; + + /// When the user record was created. + final DateTime? createdAt; + + /// When the user record was last updated. + final DateTime? updatedAt; + + /// Serialises this [User] to a JSON map. + Map toJson() { + return { + 'userId': id, + 'email': email, + 'displayName': displayName, + 'phone': phone, + 'status': status.name.toUpperCase(), + 'metadata': metadata, + 'createdAt': createdAt?.toIso8601String(), + 'updatedAt': updatedAt?.toIso8601String(), + }; + } @override - List get props => [id, email, phone, role]; -} \ No newline at end of file + List get props => [ + id, + email, + displayName, + phone, + status, + metadata, + createdAt, + updatedAt, + ]; + + /// Parses a status string from the API into a [UserStatus]. + static UserStatus _parseUserStatus(String? value) { + switch (value?.toUpperCase()) { + case 'ACTIVE': + return UserStatus.active; + case 'INVITED': + return UserStatus.invited; + case 'DISABLED': + return UserStatus.disabled; + default: + return UserStatus.active; + } + } +} diff --git a/apps/mobile/packages/features/client/orders/view_orders/lib/src/presentation/blocs/view_orders_cubit.dart b/apps/mobile/packages/features/client/orders/view_orders/lib/src/presentation/blocs/view_orders_cubit.dart index 3a8a1bfb..a4bb9c25 100644 --- a/apps/mobile/packages/features/client/orders/view_orders/lib/src/presentation/blocs/view_orders_cubit.dart +++ b/apps/mobile/packages/features/client/orders/view_orders/lib/src/presentation/blocs/view_orders_cubit.dart @@ -255,7 +255,7 @@ class ViewOrdersCubit extends Cubit return filtered; } else if (state.filterTab == 'active') { final List filtered = ordersOnDate - .where((OrderItem s) => s.status == 'IN_PROGRESS') + .where((OrderItem s) => s.status == ShiftStatus.active) .toList(); print( 'ViewOrders tab=active statuses=${ordersOnDate.map((OrderItem s) => s.status).toList()} filtered=${filtered.length}', @@ -263,7 +263,7 @@ class ViewOrdersCubit extends Cubit return filtered; } else if (state.filterTab == 'completed') { final List filtered = ordersOnDate - .where((OrderItem s) => s.status == 'COMPLETED') + .where((OrderItem s) => s.status == ShiftStatus.completed) .toList(); print( 'ViewOrders tab=completed statuses=${ordersOnDate.map((OrderItem s) => s.status).toList()} filtered=${filtered.length}', @@ -284,14 +284,14 @@ class ViewOrdersCubit extends Cubit return state.orders .where( (OrderItem s) => - s.date == selectedDateStr && s.status == 'IN_PROGRESS', + s.date == selectedDateStr && s.status == ShiftStatus.active, ) .length; } else if (category == 'completed') { return state.orders .where( (OrderItem s) => - s.date == selectedDateStr && s.status == 'COMPLETED', + s.date == selectedDateStr && s.status == ShiftStatus.completed, ) .length; } diff --git a/apps/mobile/packages/features/client/orders/view_orders/lib/src/presentation/widgets/view_order_card.dart b/apps/mobile/packages/features/client/orders/view_orders/lib/src/presentation/widgets/view_order_card.dart index ca085684..ba60a932 100644 --- a/apps/mobile/packages/features/client/orders/view_orders/lib/src/presentation/widgets/view_order_card.dart +++ b/apps/mobile/packages/features/client/orders/view_orders/lib/src/presentation/widgets/view_order_card.dart @@ -111,7 +111,7 @@ class _ViewOrderCardState extends State { /// Returns true if the edit icon should be shown. /// Hidden for completed orders and for past orders (shift has ended). bool _canEditOrder(OrderItem order) { - if (order.status == 'COMPLETED') return false; + if (order.status == ShiftStatus.completed) return false; if (order.date.isEmpty) return true; try { final DateTime orderDate = DateTime.parse(order.date);