From 7a5c130289b16ce7073a1baded3b09aeb0a8d407 Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Mon, 9 Mar 2026 15:01:18 -0400 Subject: [PATCH 1/7] refactor: change singleton registrations to lazySingleton for improved performance --- .../packages/core/lib/src/core_module.dart | 26 +++++++++---------- .../billing/lib/src/billing_module.dart | 20 +++++++------- .../lib/src/coverage_module.dart | 8 +++--- .../lib/src/client_main_module.dart | 2 +- .../lib/src/view_orders_module.dart | 2 +- .../staff/home/lib/src/staff_home_module.dart | 2 +- .../profile/lib/src/staff_profile_module.dart | 2 +- .../faqs/lib/src/staff_faqs_module.dart | 6 ++--- .../src/staff_privacy_security_module.dart | 10 +++---- .../staff_main/lib/src/staff_main_module.dart | 4 +-- 10 files changed, 41 insertions(+), 41 deletions(-) diff --git a/apps/mobile/packages/core/lib/src/core_module.dart b/apps/mobile/packages/core/lib/src/core_module.dart index 3f1c9f0c..5c71f6aa 100644 --- a/apps/mobile/packages/core/lib/src/core_module.dart +++ b/apps/mobile/packages/core/lib/src/core_module.dart @@ -13,35 +13,35 @@ class CoreModule extends Module { @override void exportedBinds(Injector i) { // 1. Register the base HTTP client - i.addSingleton(() => DioClient()); + i.addLazySingleton(() => DioClient()); // 2. Register the base API service - i.addSingleton(() => ApiService(i.get())); + i.addLazySingleton(() => ApiService(i.get())); // 3. Register Core API Services (Orchestrators) - i.addSingleton( + i.addLazySingleton( () => FileUploadService(i.get()), ); - i.addSingleton( + i.addLazySingleton( () => SignedUrlService(i.get()), ); - i.addSingleton( + i.addLazySingleton( () => VerificationService(i.get()), ); - i.addSingleton(() => LlmService(i.get())); - i.addSingleton( + i.addLazySingleton(() => LlmService(i.get())); + i.addLazySingleton( () => RapidOrderService(i.get()), ); // 4. Register Device dependency - i.addSingleton(() => ImagePicker()); + i.addLazySingleton(() => ImagePicker()); // 5. Register Device Services - i.addSingleton(() => CameraService(i.get())); - i.addSingleton(() => GalleryService(i.get())); - i.addSingleton(FilePickerService.new); - i.addSingleton(AudioRecorderService.new); - i.addSingleton( + i.addLazySingleton(() => CameraService(i.get())); + i.addLazySingleton(() => GalleryService(i.get())); + i.addLazySingleton(FilePickerService.new); + i.addLazySingleton(AudioRecorderService.new); + i.addLazySingleton( () => DeviceFileUploadService( cameraService: i.get(), galleryService: i.get(), diff --git a/apps/mobile/packages/features/client/billing/lib/src/billing_module.dart b/apps/mobile/packages/features/client/billing/lib/src/billing_module.dart index 9ad44e3e..b2bf37d8 100644 --- a/apps/mobile/packages/features/client/billing/lib/src/billing_module.dart +++ b/apps/mobile/packages/features/client/billing/lib/src/billing_module.dart @@ -24,20 +24,20 @@ class BillingModule extends Module { @override void binds(Injector i) { // Repositories - i.addSingleton(BillingRepositoryImpl.new); + i.addLazySingleton(BillingRepositoryImpl.new); // Use Cases - i.addSingleton(GetBankAccountsUseCase.new); - i.addSingleton(GetCurrentBillAmountUseCase.new); - i.addSingleton(GetSavingsAmountUseCase.new); - i.addSingleton(GetPendingInvoicesUseCase.new); - i.addSingleton(GetInvoiceHistoryUseCase.new); - i.addSingleton(GetSpendingBreakdownUseCase.new); - i.addSingleton(ApproveInvoiceUseCase.new); - i.addSingleton(DisputeInvoiceUseCase.new); + i.addLazySingleton(GetBankAccountsUseCase.new); + i.addLazySingleton(GetCurrentBillAmountUseCase.new); + i.addLazySingleton(GetSavingsAmountUseCase.new); + i.addLazySingleton(GetPendingInvoicesUseCase.new); + i.addLazySingleton(GetInvoiceHistoryUseCase.new); + i.addLazySingleton(GetSpendingBreakdownUseCase.new); + i.addLazySingleton(ApproveInvoiceUseCase.new); + i.addLazySingleton(DisputeInvoiceUseCase.new); // BLoCs - i.addSingleton( + i.addLazySingleton( () => BillingBloc( getBankAccounts: i.get(), getCurrentBillAmount: i.get(), diff --git a/apps/mobile/packages/features/client/client_coverage/lib/src/coverage_module.dart b/apps/mobile/packages/features/client/client_coverage/lib/src/coverage_module.dart index aa36826c..cd741711 100644 --- a/apps/mobile/packages/features/client/client_coverage/lib/src/coverage_module.dart +++ b/apps/mobile/packages/features/client/client_coverage/lib/src/coverage_module.dart @@ -16,14 +16,14 @@ class CoverageModule extends Module { @override void binds(Injector i) { // Repositories - i.addSingleton(CoverageRepositoryImpl.new); + i.addLazySingleton(CoverageRepositoryImpl.new); // Use Cases - i.addSingleton(GetShiftsForDateUseCase.new); - i.addSingleton(GetCoverageStatsUseCase.new); + i.addLazySingleton(GetShiftsForDateUseCase.new); + i.addLazySingleton(GetCoverageStatsUseCase.new); // BLoCs - i.addSingleton(CoverageBloc.new); + i.addLazySingleton(CoverageBloc.new); } @override diff --git a/apps/mobile/packages/features/client/client_main/lib/src/client_main_module.dart b/apps/mobile/packages/features/client/client_main/lib/src/client_main_module.dart index 24762388..1204f1e9 100644 --- a/apps/mobile/packages/features/client/client_main/lib/src/client_main_module.dart +++ b/apps/mobile/packages/features/client/client_main/lib/src/client_main_module.dart @@ -13,7 +13,7 @@ import 'presentation/pages/client_main_page.dart'; class ClientMainModule extends Module { @override void binds(Injector i) { - i.addSingleton(ClientMainCubit.new); + i.addLazySingleton(ClientMainCubit.new); } @override diff --git a/apps/mobile/packages/features/client/orders/view_orders/lib/src/view_orders_module.dart b/apps/mobile/packages/features/client/orders/view_orders/lib/src/view_orders_module.dart index 6ba187d2..7229767c 100644 --- a/apps/mobile/packages/features/client/orders/view_orders/lib/src/view_orders_module.dart +++ b/apps/mobile/packages/features/client/orders/view_orders/lib/src/view_orders_module.dart @@ -27,7 +27,7 @@ class ViewOrdersModule extends Module { i.add(GetAcceptedApplicationsForDayUseCase.new); // BLoCs - i.addSingleton(ViewOrdersCubit.new); + i.addLazySingleton(ViewOrdersCubit.new); } @override diff --git a/apps/mobile/packages/features/staff/home/lib/src/staff_home_module.dart b/apps/mobile/packages/features/staff/home/lib/src/staff_home_module.dart index 0b319174..921a304a 100644 --- a/apps/mobile/packages/features/staff/home/lib/src/staff_home_module.dart +++ b/apps/mobile/packages/features/staff/home/lib/src/staff_home_module.dart @@ -33,7 +33,7 @@ class StaffHomeModule extends Module { ); // Presentation layer - Cubits - i.addSingleton( + i.addLazySingleton( () => HomeCubit( repository: i.get(), getProfileCompletion: i.get(), diff --git a/apps/mobile/packages/features/staff/profile/lib/src/staff_profile_module.dart b/apps/mobile/packages/features/staff/profile/lib/src/staff_profile_module.dart index f9b720cb..c49c8ecf 100644 --- a/apps/mobile/packages/features/staff/profile/lib/src/staff_profile_module.dart +++ b/apps/mobile/packages/features/staff/profile/lib/src/staff_profile_module.dart @@ -68,7 +68,7 @@ class StaffProfileModule extends Module { // Presentation layer - Cubit as singleton to avoid recreation // BlocProvider will use this same instance, preventing state emission after close - i.addSingleton( + i.addLazySingleton( () => ProfileCubit( i.get(), i.get(), diff --git a/apps/mobile/packages/features/staff/profile_sections/support/faqs/lib/src/staff_faqs_module.dart b/apps/mobile/packages/features/staff/profile_sections/support/faqs/lib/src/staff_faqs_module.dart index 6faf7c3a..a7e9da46 100644 --- a/apps/mobile/packages/features/staff/profile_sections/support/faqs/lib/src/staff_faqs_module.dart +++ b/apps/mobile/packages/features/staff/profile_sections/support/faqs/lib/src/staff_faqs_module.dart @@ -17,17 +17,17 @@ class FaqsModule extends Module { @override void binds(Injector i) { // Repository - i.addSingleton( + i.addLazySingleton( () => FaqsRepositoryImpl(), ); // Use Cases - i.addSingleton( + i.addLazySingleton( () => GetFaqsUseCase( i(), ), ); - i.addSingleton( + i.addLazySingleton( () => SearchFaqsUseCase( i(), ), diff --git a/apps/mobile/packages/features/staff/profile_sections/support/privacy_security/lib/src/staff_privacy_security_module.dart b/apps/mobile/packages/features/staff/profile_sections/support/privacy_security/lib/src/staff_privacy_security_module.dart index 22b0d405..81ce8a74 100644 --- a/apps/mobile/packages/features/staff/profile_sections/support/privacy_security/lib/src/staff_privacy_security_module.dart +++ b/apps/mobile/packages/features/staff/profile_sections/support/privacy_security/lib/src/staff_privacy_security_module.dart @@ -25,29 +25,29 @@ class PrivacySecurityModule extends Module { @override void binds(Injector i) { // Repository - i.addSingleton( + i.addLazySingleton( () => PrivacySettingsRepositoryImpl( Modular.get(), ), ); // Use Cases - i.addSingleton( + i.addLazySingleton( () => GetProfileVisibilityUseCase( i(), ), ); - i.addSingleton( + i.addLazySingleton( () => UpdateProfileVisibilityUseCase( i(), ), ); - i.addSingleton( + i.addLazySingleton( () => GetTermsUseCase( i(), ), ); - i.addSingleton( + i.addLazySingleton( () => GetPrivacyPolicyUseCase( i(), ), diff --git a/apps/mobile/packages/features/staff/staff_main/lib/src/staff_main_module.dart b/apps/mobile/packages/features/staff/staff_main/lib/src/staff_main_module.dart index 21493654..a479da35 100644 --- a/apps/mobile/packages/features/staff/staff_main/lib/src/staff_main_module.dart +++ b/apps/mobile/packages/features/staff/staff_main/lib/src/staff_main_module.dart @@ -26,12 +26,12 @@ class StaffMainModule extends Module { @override void binds(Injector i) { // Register the StaffConnectorRepository from data_connect - i.addSingleton( + i.addLazySingleton( StaffConnectorRepositoryImpl.new, ); // Register the use case from data_connect - i.addSingleton( + i.addLazySingleton( () => GetProfileCompletionUseCase( repository: i.get(), ), From 2484c6cff2604a63b2b605f98921e2ae5b28c94b Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Mon, 9 Mar 2026 16:26:53 -0400 Subject: [PATCH 2/7] Refactor code structure for improved readability and maintainability --- .../apps/client/android/app/build.gradle.kts | 26 +- .../app/{ => src/dev}/google-services.json | 144 +- .../android/app/src/main/AndroidManifest.xml | 2 +- .../android/app/src/prod/google-services.json | 24 + .../app/src/stage/google-services.json | 48 + .../ios/Runner.xcodeproj/project.pbxproj | 886 +++++- .../xcshareddata/xcschemes/dev.xcscheme | 78 + .../xcshareddata/xcschemes/prod.xcscheme | 78 + .../xcshareddata/xcschemes/stage.xcscheme | 78 + apps/mobile/apps/client/ios/Runner/Info.plist | 4 +- .../ios/config/dev/GoogleService-Info.plist | 36 + .../ios/config/prod/GoogleService-Info.plist | 30 + .../ios/config/stage/GoogleService-Info.plist | 30 + .../client/ios/scripts/firebase-config.sh | 19 + .../apps/client/lib/firebase_options.dart | 167 +- .../apps/staff/android/app/build.gradle.kts | 22 +- .../app/{ => src/dev}/google-services.json | 146 +- .../android/app/src/main/AndroidManifest.xml | 2 +- .../android/app/src/prod/google-services.json | 24 + .../app/src/stage/google-services.json | 48 + .../ios/Runner.xcodeproj/project.pbxproj | 889 +++++- .../xcshareddata/xcschemes/dev.xcscheme | 78 + .../xcshareddata/xcschemes/prod.xcscheme | 78 + .../xcshareddata/xcschemes/stage.xcscheme | 78 + apps/mobile/apps/staff/ios/Runner/Info.plist | 4 +- .../dev}/GoogleService-Info.plist | 8 +- .../ios/config/prod/GoogleService-Info.plist | 30 + .../ios/config/stage/GoogleService-Info.plist | 30 + .../apps/staff/ios/scripts/firebase-config.sh | 19 + .../apps/staff/lib/firebase_options.dart | 167 +- apps/mobile/config.dev.json | 3 +- apps/mobile/config.prod.json | 5 + apps/mobile/config.stage.json | 5 + apps/mobile/packages/core/lib/core.dart | 1 + .../core/lib/src/config/app_environment.dart | 46 + codemagic.yaml | 73 +- docs/DESIGN/product-specification.md | 2778 +++++++++++++++++ makefiles/mobile.mk | 19 +- 38 files changed, 5894 insertions(+), 309 deletions(-) rename apps/mobile/apps/client/android/app/{ => src/dev}/google-services.json (94%) create mode 100644 apps/mobile/apps/client/android/app/src/prod/google-services.json create mode 100644 apps/mobile/apps/client/android/app/src/stage/google-services.json create mode 100644 apps/mobile/apps/client/ios/Runner.xcodeproj/xcshareddata/xcschemes/dev.xcscheme create mode 100644 apps/mobile/apps/client/ios/Runner.xcodeproj/xcshareddata/xcschemes/prod.xcscheme create mode 100644 apps/mobile/apps/client/ios/Runner.xcodeproj/xcshareddata/xcschemes/stage.xcscheme create mode 100644 apps/mobile/apps/client/ios/config/dev/GoogleService-Info.plist create mode 100644 apps/mobile/apps/client/ios/config/prod/GoogleService-Info.plist create mode 100644 apps/mobile/apps/client/ios/config/stage/GoogleService-Info.plist create mode 100755 apps/mobile/apps/client/ios/scripts/firebase-config.sh rename apps/mobile/apps/staff/android/app/{ => src/dev}/google-services.json (94%) create mode 100644 apps/mobile/apps/staff/android/app/src/prod/google-services.json create mode 100644 apps/mobile/apps/staff/android/app/src/stage/google-services.json create mode 100644 apps/mobile/apps/staff/ios/Runner.xcodeproj/xcshareddata/xcschemes/dev.xcscheme create mode 100644 apps/mobile/apps/staff/ios/Runner.xcodeproj/xcshareddata/xcschemes/prod.xcscheme create mode 100644 apps/mobile/apps/staff/ios/Runner.xcodeproj/xcshareddata/xcschemes/stage.xcscheme rename apps/mobile/apps/staff/ios/{Runner => config/dev}/GoogleService-Info.plist (79%) create mode 100644 apps/mobile/apps/staff/ios/config/prod/GoogleService-Info.plist create mode 100644 apps/mobile/apps/staff/ios/config/stage/GoogleService-Info.plist create mode 100755 apps/mobile/apps/staff/ios/scripts/firebase-config.sh create mode 100644 apps/mobile/config.prod.json create mode 100644 apps/mobile/config.stage.json create mode 100644 apps/mobile/packages/core/lib/src/config/app_environment.dart create mode 100644 docs/DESIGN/product-specification.md diff --git a/apps/mobile/apps/client/android/app/build.gradle.kts b/apps/mobile/apps/client/android/app/build.gradle.kts index 323e6fd0..15f3f341 100644 --- a/apps/mobile/apps/client/android/app/build.gradle.kts +++ b/apps/mobile/apps/client/android/app/build.gradle.kts @@ -29,7 +29,7 @@ val keystoreProperties = Properties().apply { } android { - namespace = "com.krowwithus.client" + namespace = "dev.krowwithus.client" compileSdk = flutter.compileSdkVersion ndkVersion = flutter.ndkVersion @@ -43,8 +43,7 @@ android { } defaultConfig { - applicationId = "com.krowwithus.client" - // You can update the following values to match your application needs. + // applicationId is set per flavor below // For more information, see: https://flutter.dev/to/review-gradle-config. minSdk = flutter.minSdkVersion targetSdk = flutter.targetSdkVersion @@ -53,6 +52,25 @@ android { manifestPlaceholders["GOOGLE_MAPS_API_KEY"] = dartEnvironmentVariables["GOOGLE_MAPS_API_KEY"] ?: "" } + flavorDimensions += "environment" + productFlavors { + create("dev") { + dimension = "environment" + applicationId = "dev.krowwithus.client" + resValue("string", "app_name", "KROW With Us Business [DEV]") + } + create("stage") { + dimension = "environment" + applicationId = "stage.krowwithus.client" + resValue("string", "app_name", "KROW With Us Business [STG]") + } + create("prod") { + dimension = "environment" + applicationId = "prod.krowwithus.client" + resValue("string", "app_name", "KROW Client") + } + } + signingConfigs { create("release") { if (System.getenv()["CI"] == "true") { @@ -73,8 +91,6 @@ android { buildTypes { release { - // TODO: Add your own signing config for the release build. - // Signing with the debug keys for now, so `flutter run --release` works. signingConfig = signingConfigs.getByName("release") } } diff --git a/apps/mobile/apps/client/android/app/google-services.json b/apps/mobile/apps/client/android/app/src/dev/google-services.json similarity index 94% rename from apps/mobile/apps/client/android/app/google-services.json rename to apps/mobile/apps/client/android/app/src/dev/google-services.json index e7c91c27..ca0a39ea 100644 --- a/apps/mobile/apps/client/android/app/google-services.json +++ b/apps/mobile/apps/client/android/app/src/dev/google-services.json @@ -5,78 +5,6 @@ "storage_bucket": "krow-workforce-dev.firebasestorage.app" }, "client": [ - { - "client_info": { - "mobilesdk_app_id": "1:933560802882:android:edcddb83ea4bbb517757db", - "android_client_info": { - "package_name": "com.krow.app.business.dev" - } - }, - "oauth_client": [ - { - "client_id": "933560802882-grp98a1v7amflnnup68vh01tj06eaem1.apps.googleusercontent.com", - "client_type": 3 - } - ], - "api_key": [ - { - "current_key": "AIzaSyDBYhflhK6DThKnS7RM-9raKdvyKzLUjY4" - } - ], - "services": { - "appinvite_service": { - "other_platform_oauth_client": [ - { - "client_id": "933560802882-grp98a1v7amflnnup68vh01tj06eaem1.apps.googleusercontent.com", - "client_type": 3 - }, - { - "client_id": "933560802882-29olj9ku64jbe9h7flinha6hbi8qrluh.apps.googleusercontent.com", - "client_type": 2, - "ios_info": { - "bundle_id": "com.krowwithus.staff" - } - } - ] - } - } - }, - { - "client_info": { - "mobilesdk_app_id": "1:933560802882:android:d49b8c0f4d19e95e7757db", - "android_client_info": { - "package_name": "com.krow.app.staff.dev" - } - }, - "oauth_client": [ - { - "client_id": "933560802882-grp98a1v7amflnnup68vh01tj06eaem1.apps.googleusercontent.com", - "client_type": 3 - } - ], - "api_key": [ - { - "current_key": "AIzaSyDBYhflhK6DThKnS7RM-9raKdvyKzLUjY4" - } - ], - "services": { - "appinvite_service": { - "other_platform_oauth_client": [ - { - "client_id": "933560802882-grp98a1v7amflnnup68vh01tj06eaem1.apps.googleusercontent.com", - "client_type": 3 - }, - { - "client_id": "933560802882-29olj9ku64jbe9h7flinha6hbi8qrluh.apps.googleusercontent.com", - "client_type": 2, - "ios_info": { - "bundle_id": "com.krowwithus.staff" - } - } - ] - } - } - }, { "client_info": { "mobilesdk_app_id": "1:933560802882:android:da13569105659ead7757db", @@ -164,6 +92,78 @@ ] } } + }, + { + "client_info": { + "mobilesdk_app_id": "1:933560802882:android:1eb46251032273cb7757db", + "android_client_info": { + "package_name": "dev.krowwithus.client" + } + }, + "oauth_client": [ + { + "client_id": "933560802882-grp98a1v7amflnnup68vh01tj06eaem1.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyDBYhflhK6DThKnS7RM-9raKdvyKzLUjY4" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "933560802882-grp98a1v7amflnnup68vh01tj06eaem1.apps.googleusercontent.com", + "client_type": 3 + }, + { + "client_id": "933560802882-29olj9ku64jbe9h7flinha6hbi8qrluh.apps.googleusercontent.com", + "client_type": 2, + "ios_info": { + "bundle_id": "com.krowwithus.staff" + } + } + ] + } + } + }, + { + "client_info": { + "mobilesdk_app_id": "1:933560802882:android:ee100eab75b6b04c7757db", + "android_client_info": { + "package_name": "dev.krowwithus.staff" + } + }, + "oauth_client": [ + { + "client_id": "933560802882-grp98a1v7amflnnup68vh01tj06eaem1.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyDBYhflhK6DThKnS7RM-9raKdvyKzLUjY4" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "933560802882-grp98a1v7amflnnup68vh01tj06eaem1.apps.googleusercontent.com", + "client_type": 3 + }, + { + "client_id": "933560802882-29olj9ku64jbe9h7flinha6hbi8qrluh.apps.googleusercontent.com", + "client_type": 2, + "ios_info": { + "bundle_id": "com.krowwithus.staff" + } + } + ] + } + } } ], "configuration_version": "1" diff --git a/apps/mobile/apps/client/android/app/src/main/AndroidManifest.xml b/apps/mobile/apps/client/android/app/src/main/AndroidManifest.xml index 555727c2..9416b135 100644 --- a/apps/mobile/apps/client/android/app/src/main/AndroidManifest.xml +++ b/apps/mobile/apps/client/android/app/src/main/AndroidManifest.xml @@ -1,6 +1,6 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/mobile/apps/client/ios/Runner.xcodeproj/xcshareddata/xcschemes/prod.xcscheme b/apps/mobile/apps/client/ios/Runner.xcodeproj/xcshareddata/xcschemes/prod.xcscheme new file mode 100644 index 00000000..0f874d80 --- /dev/null +++ b/apps/mobile/apps/client/ios/Runner.xcodeproj/xcshareddata/xcschemes/prod.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/mobile/apps/client/ios/Runner.xcodeproj/xcshareddata/xcschemes/stage.xcscheme b/apps/mobile/apps/client/ios/Runner.xcodeproj/xcshareddata/xcschemes/stage.xcscheme new file mode 100644 index 00000000..87c22c02 --- /dev/null +++ b/apps/mobile/apps/client/ios/Runner.xcodeproj/xcshareddata/xcschemes/stage.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/mobile/apps/client/ios/Runner/Info.plist b/apps/mobile/apps/client/ios/Runner/Info.plist index e67d5b5d..bdc600e2 100644 --- a/apps/mobile/apps/client/ios/Runner/Info.plist +++ b/apps/mobile/apps/client/ios/Runner/Info.plist @@ -5,7 +5,7 @@ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName - KROW With Us Client + $(APP_NAME) CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier @@ -13,7 +13,7 @@ CFBundleInfoDictionaryVersion 6.0 CFBundleName - KROW With Us Client + $(APP_NAME) CFBundlePackageType APPL CFBundleShortVersionString diff --git a/apps/mobile/apps/client/ios/config/dev/GoogleService-Info.plist b/apps/mobile/apps/client/ios/config/dev/GoogleService-Info.plist new file mode 100644 index 00000000..75f58041 --- /dev/null +++ b/apps/mobile/apps/client/ios/config/dev/GoogleService-Info.plist @@ -0,0 +1,36 @@ + + + + + CLIENT_ID + 933560802882-jpv087j5jenp1h63mc9ge51767s3l2ac.apps.googleusercontent.com + REVERSED_CLIENT_ID + com.googleusercontent.apps.933560802882-jpv087j5jenp1h63mc9ge51767s3l2ac + ANDROID_CLIENT_ID + 933560802882-fbqg2icq24bmci3f84evjrbth5huh87f.apps.googleusercontent.com + API_KEY + AIzaSyDyEXkzZAWpXXe4dAesYaZflt5BEtMn9tA + GCM_SENDER_ID + 933560802882 + PLIST_VERSION + 1 + BUNDLE_ID + dev.krowwithus.client + PROJECT_ID + krow-workforce-dev + STORAGE_BUCKET + krow-workforce-dev.firebasestorage.app + IS_ADS_ENABLED + + IS_ANALYTICS_ENABLED + + IS_APPINVITE_ENABLED + + IS_GCM_ENABLED + + IS_SIGNIN_ENABLED + + GOOGLE_APP_ID + 1:933560802882:ios:7e179dfdd1a8994c7757db + + \ No newline at end of file diff --git a/apps/mobile/apps/client/ios/config/prod/GoogleService-Info.plist b/apps/mobile/apps/client/ios/config/prod/GoogleService-Info.plist new file mode 100644 index 00000000..daf42001 --- /dev/null +++ b/apps/mobile/apps/client/ios/config/prod/GoogleService-Info.plist @@ -0,0 +1,30 @@ + + + + + API_KEY + + GCM_SENDER_ID + + PLIST_VERSION + 1 + BUNDLE_ID + prod.krowwithus.client + PROJECT_ID + krow-workforce-prod + STORAGE_BUCKET + krow-workforce-prod.firebasestorage.app + IS_ADS_ENABLED + + IS_ANALYTICS_ENABLED + + IS_APPINVITE_ENABLED + + IS_GCM_ENABLED + + IS_SIGNIN_ENABLED + + GOOGLE_APP_ID + + + diff --git a/apps/mobile/apps/client/ios/config/stage/GoogleService-Info.plist b/apps/mobile/apps/client/ios/config/stage/GoogleService-Info.plist new file mode 100644 index 00000000..631c0d6c --- /dev/null +++ b/apps/mobile/apps/client/ios/config/stage/GoogleService-Info.plist @@ -0,0 +1,30 @@ + + + + + API_KEY + AIzaSyCgTXI3QhbEK3r4J5y7ek_6AxqhmR99QjY + GCM_SENDER_ID + 1032971403708 + PLIST_VERSION + 1 + BUNDLE_ID + stage.krowwithus.client + PROJECT_ID + krow-workforce-staging + STORAGE_BUCKET + krow-workforce-staging.firebasestorage.app + IS_ADS_ENABLED + + IS_ANALYTICS_ENABLED + + IS_APPINVITE_ENABLED + + IS_GCM_ENABLED + + IS_SIGNIN_ENABLED + + GOOGLE_APP_ID + 1:1032971403708:ios:0ff547e80f5324ed356bb9 + + \ No newline at end of file diff --git a/apps/mobile/apps/client/ios/scripts/firebase-config.sh b/apps/mobile/apps/client/ios/scripts/firebase-config.sh new file mode 100755 index 00000000..b700a0ad --- /dev/null +++ b/apps/mobile/apps/client/ios/scripts/firebase-config.sh @@ -0,0 +1,19 @@ +#!/bin/bash +# Copy the correct GoogleService-Info.plist based on the build configuration. +# This script should be added as a "Run Script" build phase in Xcode, +# BEFORE the "Compile Sources" phase. +# +# The FLUTTER_FLAVOR environment variable is set by Flutter when building +# with --flavor. It maps to: dev, stage, prod. + +FLAVOR="${FLUTTER_FLAVOR:-dev}" +PLIST_SOURCE="${PROJECT_DIR}/config/${FLAVOR}/GoogleService-Info.plist" +PLIST_DEST="${PROJECT_DIR}/Runner/GoogleService-Info.plist" + +if [ ! -f "$PLIST_SOURCE" ]; then + echo "error: GoogleService-Info.plist not found for flavor '${FLAVOR}' at ${PLIST_SOURCE}" + exit 1 +fi + +echo "Copying GoogleService-Info.plist for flavor: ${FLAVOR}" +cp "${PLIST_SOURCE}" "${PLIST_DEST}" diff --git a/apps/mobile/apps/client/lib/firebase_options.dart b/apps/mobile/apps/client/lib/firebase_options.dart index f703aa10..20904852 100644 --- a/apps/mobile/apps/client/lib/firebase_options.dart +++ b/apps/mobile/apps/client/lib/firebase_options.dart @@ -1,44 +1,22 @@ -// File generated by FlutterFire CLI. - import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; import 'package:flutter/foundation.dart' show defaultTargetPlatform, kIsWeb, TargetPlatform; +import 'package:krow_core/core.dart'; -/// Default [FirebaseOptions] for use with your Firebase apps. +/// Environment-aware [FirebaseOptions] for the Client app. /// -/// Example: -/// ```dart -/// import 'firebase_options.dart'; -/// // ... -/// await Firebase.initializeApp( -/// options: DefaultFirebaseOptions.currentPlatform, -/// ); -/// ``` +/// Selects the correct Firebase configuration based on the compile-time +/// `ENV` dart define (dev, stage, prod). Defaults to dev. class DefaultFirebaseOptions { static FirebaseOptions get currentPlatform { if (kIsWeb) { - return web; + return _webOptions; } switch (defaultTargetPlatform) { case TargetPlatform.android: - return android; + return _androidOptions; case TargetPlatform.iOS: - return ios; - case TargetPlatform.macOS: - throw UnsupportedError( - 'DefaultFirebaseOptions have not been configured for macos - ' - 'you can reconfigure this by running the FlutterFire CLI again.', - ); - case TargetPlatform.windows: - throw UnsupportedError( - 'DefaultFirebaseOptions have not been configured for windows - ' - 'you can reconfigure this by running the FlutterFire CLI again.', - ); - case TargetPlatform.linux: - throw UnsupportedError( - 'DefaultFirebaseOptions have not been configured for linux - ' - 'you can reconfigure this by running the FlutterFire CLI again.', - ); + return _iosOptions; default: throw UnsupportedError( 'DefaultFirebaseOptions are not supported for this platform.', @@ -46,7 +24,65 @@ class DefaultFirebaseOptions { } } - static const FirebaseOptions web = FirebaseOptions( + static FirebaseOptions get _androidOptions { + switch (AppEnvironment.current) { + case AppEnvironment.dev: + return _devAndroid; + case AppEnvironment.stage: + return _stageAndroid; + case AppEnvironment.prod: + return _prodAndroid; + } + } + + static FirebaseOptions get _iosOptions { + switch (AppEnvironment.current) { + case AppEnvironment.dev: + return _devIos; + case AppEnvironment.stage: + return _stageIos; + case AppEnvironment.prod: + return _prodIos; + } + } + + static FirebaseOptions get _webOptions { + switch (AppEnvironment.current) { + case AppEnvironment.dev: + return _devWeb; + case AppEnvironment.stage: + return _stageWeb; + case AppEnvironment.prod: + return _prodWeb; + } + } + + // =========================================================================== + // DEV (krow-workforce-dev) + // =========================================================================== + + static const FirebaseOptions _devAndroid = FirebaseOptions( + apiKey: 'AIzaSyDBYhflhK6DThKnS7RM-9raKdvyKzLUjY4', + appId: '1:933560802882:android:1eb46251032273cb7757db', + messagingSenderId: '933560802882', + projectId: 'krow-workforce-dev', + storageBucket: 'krow-workforce-dev.firebasestorage.app', + ); + + static const FirebaseOptions _devIos = FirebaseOptions( + apiKey: 'AIzaSyDyEXkzZAWpXXe4dAesYaZflt5BEtMn9tA', + appId: '1:933560802882:ios:7e179dfdd1a8994c7757db', + messagingSenderId: '933560802882', + projectId: 'krow-workforce-dev', + storageBucket: 'krow-workforce-dev.firebasestorage.app', + androidClientId: + '933560802882-fbqg2icq24bmci3f84evjrbth5huh87f.apps.googleusercontent.com', + iosClientId: + '933560802882-jpv087j5jenp1h63mc9ge51767s3l2ac.apps.googleusercontent.com', + iosBundleId: 'dev.krowwithus.client', + ); + + static const FirebaseOptions _devWeb = FirebaseOptions( apiKey: 'AIzaSyBqRtZPMGU-Sz5x5UnRrunKu5NSWYyPRn8', appId: '1:933560802882:web:173a841992885bb27757db', messagingSenderId: '933560802882', @@ -56,23 +92,62 @@ class DefaultFirebaseOptions { measurementId: 'G-9S7WEQTDKX', ); - static const FirebaseOptions android = FirebaseOptions( - apiKey: 'AIzaSyDBYhflhK6DThKnS7RM-9raKdvyKzLUjY4', - appId: '1:933560802882:android:da13569105659ead7757db', - messagingSenderId: '933560802882', - projectId: 'krow-workforce-dev', - storageBucket: 'krow-workforce-dev.firebasestorage.app', + // =========================================================================== + // STAGE (krow-workforce-staging) + // =========================================================================== + + static const FirebaseOptions _stageAndroid = FirebaseOptions( + apiKey: 'AIzaSyCgTXI3QhbEK3r4J5y7ek_6AxqhmR99QjY', + appId: '1:1032971403708:android:1ab9badf171c3aca356bb9', + messagingSenderId: '1032971403708', + projectId: 'krow-workforce-staging', + storageBucket: 'krow-workforce-staging.firebasestorage.app', ); - static const FirebaseOptions ios = FirebaseOptions( - apiKey: 'AIzaSyDyEXkzZAWpXXe4dAesYaZflt5BEtMn9tA', - appId: '1:933560802882:ios:d2b6d743608e2a527757db', - messagingSenderId: '933560802882', - projectId: 'krow-workforce-dev', - storageBucket: 'krow-workforce-dev.firebasestorage.app', - androidClientId: '933560802882-fbqg2icq24bmci3f84evjrbth5huh87f.apps.googleusercontent.com', - iosClientId: '933560802882-jqpv1l3gjmi3m87b2gu1iq4lg46lkdfg.apps.googleusercontent.com', - iosBundleId: 'com.krowwithus.client', + static const FirebaseOptions _stageIos = FirebaseOptions( + apiKey: 'AIzaSyCgTXI3QhbEK3r4J5y7ek_6AxqhmR99QjY', + appId: '1:1032971403708:ios:0ff547e80f5324ed356bb9', + messagingSenderId: '1032971403708', + projectId: 'krow-workforce-staging', + storageBucket: 'krow-workforce-staging.firebasestorage.app', + iosBundleId: 'stage.krowwithus.client', ); -} \ No newline at end of file + static const FirebaseOptions _stageWeb = FirebaseOptions( + apiKey: 'AIzaSyCgTXI3QhbEK3r4J5y7ek_6AxqhmR99QjY', + appId: '', // TODO: Register web app in krow-workforce-staging + messagingSenderId: '1032971403708', + projectId: 'krow-workforce-staging', + storageBucket: 'krow-workforce-staging.firebasestorage.app', + ); + + // =========================================================================== + // PROD (krow-workforce-prod) + // TODO: Fill in after creating krow-workforce-prod Firebase project + // =========================================================================== + + static const FirebaseOptions _prodAndroid = FirebaseOptions( + apiKey: '', // TODO: Add prod API key + appId: '', // TODO: Add prod app ID + messagingSenderId: '', // TODO: Add prod sender ID + projectId: 'krow-workforce-prod', + storageBucket: 'krow-workforce-prod.firebasestorage.app', + ); + + static const FirebaseOptions _prodIos = FirebaseOptions( + apiKey: '', // TODO: Add prod API key + appId: '', // TODO: Add prod app ID + messagingSenderId: '', // TODO: Add prod sender ID + projectId: 'krow-workforce-prod', + storageBucket: 'krow-workforce-prod.firebasestorage.app', + iosBundleId: 'prod.krowwithus.client', + ); + + static const FirebaseOptions _prodWeb = FirebaseOptions( + apiKey: '', // TODO: Add prod API key + appId: '', // TODO: Add prod app ID + messagingSenderId: '', // TODO: Add prod sender ID + projectId: 'krow-workforce-prod', + storageBucket: 'krow-workforce-prod.firebasestorage.app', + ); +} diff --git a/apps/mobile/apps/staff/android/app/build.gradle.kts b/apps/mobile/apps/staff/android/app/build.gradle.kts index 0f7dd24a..4111f66b 100644 --- a/apps/mobile/apps/staff/android/app/build.gradle.kts +++ b/apps/mobile/apps/staff/android/app/build.gradle.kts @@ -43,8 +43,7 @@ android { } defaultConfig { - applicationId = "com.krowwithus.staff" - // You can update the following values to match your application needs. + // applicationId is set per flavor below // For more information, see: https://flutter.dev/to/review-gradle-config. minSdk = flutter.minSdkVersion targetSdk = flutter.targetSdkVersion @@ -54,6 +53,25 @@ android { manifestPlaceholders["GOOGLE_MAPS_API_KEY"] = dartEnvironmentVariables["GOOGLE_MAPS_API_KEY"] ?: "" } + flavorDimensions += "environment" + productFlavors { + create("dev") { + dimension = "environment" + applicationId = "dev.krowwithus.staff" + resValue("string", "app_name", "KROW With Us [DEV]") + } + create("stage") { + dimension = "environment" + applicationId = "stage.krowwithus.staff" + resValue("string", "app_name", "KROW With Us [STG]") + } + create("prod") { + dimension = "environment" + applicationId = "prod.krowwithus.staff" + resValue("string", "app_name", "KROW Staff") + } + } + signingConfigs { create("release") { if (System.getenv()["CI"] == "true") { diff --git a/apps/mobile/apps/staff/android/app/google-services.json b/apps/mobile/apps/staff/android/app/src/dev/google-services.json similarity index 94% rename from apps/mobile/apps/staff/android/app/google-services.json rename to apps/mobile/apps/staff/android/app/src/dev/google-services.json index 8d5acf3a..ca0a39ea 100644 --- a/apps/mobile/apps/staff/android/app/google-services.json +++ b/apps/mobile/apps/staff/android/app/src/dev/google-services.json @@ -5,78 +5,6 @@ "storage_bucket": "krow-workforce-dev.firebasestorage.app" }, "client": [ - { - "client_info": { - "mobilesdk_app_id": "1:933560802882:android:edcddb83ea4bbb517757db", - "android_client_info": { - "package_name": "com.krow.app.business.dev" - } - }, - "oauth_client": [ - { - "client_id": "933560802882-grp98a1v7amflnnup68vh01tj06eaem1.apps.googleusercontent.com", - "client_type": 3 - } - ], - "api_key": [ - { - "current_key": "AIzaSyDBYhflhK6DThKnS7RM-9raKdvyKzLUjY4" - } - ], - "services": { - "appinvite_service": { - "other_platform_oauth_client": [ - { - "client_id": "933560802882-grp98a1v7amflnnup68vh01tj06eaem1.apps.googleusercontent.com", - "client_type": 3 - }, - { - "client_id": "933560802882-29olj9ku64jbe9h7flinha6hbi8qrluh.apps.googleusercontent.com", - "client_type": 2, - "ios_info": { - "bundle_id": "com.krowwithus.staff" - } - } - ] - } - } - }, - { - "client_info": { - "mobilesdk_app_id": "1:933560802882:android:d49b8c0f4d19e95e7757db", - "android_client_info": { - "package_name": "com.krow.app.staff.dev" - } - }, - "oauth_client": [ - { - "client_id": "933560802882-grp98a1v7amflnnup68vh01tj06eaem1.apps.googleusercontent.com", - "client_type": 3 - } - ], - "api_key": [ - { - "current_key": "AIzaSyDBYhflhK6DThKnS7RM-9raKdvyKzLUjY4" - } - ], - "services": { - "appinvite_service": { - "other_platform_oauth_client": [ - { - "client_id": "933560802882-grp98a1v7amflnnup68vh01tj06eaem1.apps.googleusercontent.com", - "client_type": 3 - }, - { - "client_id": "933560802882-29olj9ku64jbe9h7flinha6hbi8qrluh.apps.googleusercontent.com", - "client_type": 2, - "ios_info": { - "bundle_id": "com.krowwithus.staff" - } - } - ] - } - } - }, { "client_info": { "mobilesdk_app_id": "1:933560802882:android:da13569105659ead7757db", @@ -164,7 +92,79 @@ ] } } + }, + { + "client_info": { + "mobilesdk_app_id": "1:933560802882:android:1eb46251032273cb7757db", + "android_client_info": { + "package_name": "dev.krowwithus.client" + } + }, + "oauth_client": [ + { + "client_id": "933560802882-grp98a1v7amflnnup68vh01tj06eaem1.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyDBYhflhK6DThKnS7RM-9raKdvyKzLUjY4" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "933560802882-grp98a1v7amflnnup68vh01tj06eaem1.apps.googleusercontent.com", + "client_type": 3 + }, + { + "client_id": "933560802882-29olj9ku64jbe9h7flinha6hbi8qrluh.apps.googleusercontent.com", + "client_type": 2, + "ios_info": { + "bundle_id": "com.krowwithus.staff" + } + } + ] + } + } + }, + { + "client_info": { + "mobilesdk_app_id": "1:933560802882:android:ee100eab75b6b04c7757db", + "android_client_info": { + "package_name": "dev.krowwithus.staff" + } + }, + "oauth_client": [ + { + "client_id": "933560802882-grp98a1v7amflnnup68vh01tj06eaem1.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyDBYhflhK6DThKnS7RM-9raKdvyKzLUjY4" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "933560802882-grp98a1v7amflnnup68vh01tj06eaem1.apps.googleusercontent.com", + "client_type": 3 + }, + { + "client_id": "933560802882-29olj9ku64jbe9h7flinha6hbi8qrluh.apps.googleusercontent.com", + "client_type": 2, + "ios_info": { + "bundle_id": "com.krowwithus.staff" + } + } + ] + } + } } ], "configuration_version": "1" -} +} \ No newline at end of file diff --git a/apps/mobile/apps/staff/android/app/src/main/AndroidManifest.xml b/apps/mobile/apps/staff/android/app/src/main/AndroidManifest.xml index 0e093d51..9416b135 100644 --- a/apps/mobile/apps/staff/android/app/src/main/AndroidManifest.xml +++ b/apps/mobile/apps/staff/android/app/src/main/AndroidManifest.xml @@ -1,6 +1,6 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/mobile/apps/staff/ios/Runner.xcodeproj/xcshareddata/xcschemes/prod.xcscheme b/apps/mobile/apps/staff/ios/Runner.xcodeproj/xcshareddata/xcschemes/prod.xcscheme new file mode 100644 index 00000000..35bf1848 --- /dev/null +++ b/apps/mobile/apps/staff/ios/Runner.xcodeproj/xcshareddata/xcschemes/prod.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/mobile/apps/staff/ios/Runner.xcodeproj/xcshareddata/xcschemes/stage.xcscheme b/apps/mobile/apps/staff/ios/Runner.xcodeproj/xcshareddata/xcschemes/stage.xcscheme new file mode 100644 index 00000000..35bf1848 --- /dev/null +++ b/apps/mobile/apps/staff/ios/Runner.xcodeproj/xcshareddata/xcschemes/stage.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/mobile/apps/staff/ios/Runner/Info.plist b/apps/mobile/apps/staff/ios/Runner/Info.plist index 257da050..bdc600e2 100644 --- a/apps/mobile/apps/staff/ios/Runner/Info.plist +++ b/apps/mobile/apps/staff/ios/Runner/Info.plist @@ -5,7 +5,7 @@ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName - KROW With Us Staff + $(APP_NAME) CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier @@ -13,7 +13,7 @@ CFBundleInfoDictionaryVersion 6.0 CFBundleName - KROW With Us Staff + $(APP_NAME) CFBundlePackageType APPL CFBundleShortVersionString diff --git a/apps/mobile/apps/staff/ios/Runner/GoogleService-Info.plist b/apps/mobile/apps/staff/ios/config/dev/GoogleService-Info.plist similarity index 79% rename from apps/mobile/apps/staff/ios/Runner/GoogleService-Info.plist rename to apps/mobile/apps/staff/ios/config/dev/GoogleService-Info.plist index 7fc4d7e6..acd9bbb6 100644 --- a/apps/mobile/apps/staff/ios/Runner/GoogleService-Info.plist +++ b/apps/mobile/apps/staff/ios/config/dev/GoogleService-Info.plist @@ -3,9 +3,9 @@ CLIENT_ID - 933560802882-29olj9ku64jbe9h7flinha6hbi8qrluh.apps.googleusercontent.com + 933560802882-fphpkdjubve8k7e8ogqj3fk1qducv3sg.apps.googleusercontent.com REVERSED_CLIENT_ID - com.googleusercontent.apps.933560802882-29olj9ku64jbe9h7flinha6hbi8qrluh + com.googleusercontent.apps.933560802882-fphpkdjubve8k7e8ogqj3fk1qducv3sg ANDROID_CLIENT_ID 933560802882-fbqg2icq24bmci3f84evjrbth5huh87f.apps.googleusercontent.com API_KEY @@ -15,7 +15,7 @@ PLIST_VERSION 1 BUNDLE_ID - com.krowwithus.staff + dev.krowwithus.staff PROJECT_ID krow-workforce-dev STORAGE_BUCKET @@ -31,6 +31,6 @@ IS_SIGNIN_ENABLED GOOGLE_APP_ID - 1:933560802882:ios:fa584205b356de937757db + 1:933560802882:ios:edf97dab6eb87b977757db \ No newline at end of file diff --git a/apps/mobile/apps/staff/ios/config/prod/GoogleService-Info.plist b/apps/mobile/apps/staff/ios/config/prod/GoogleService-Info.plist new file mode 100644 index 00000000..78f75702 --- /dev/null +++ b/apps/mobile/apps/staff/ios/config/prod/GoogleService-Info.plist @@ -0,0 +1,30 @@ + + + + + API_KEY + + GCM_SENDER_ID + + PLIST_VERSION + 1 + BUNDLE_ID + prod.krowwithus.staff + PROJECT_ID + krow-workforce-prod + STORAGE_BUCKET + krow-workforce-prod.firebasestorage.app + IS_ADS_ENABLED + + IS_ANALYTICS_ENABLED + + IS_APPINVITE_ENABLED + + IS_GCM_ENABLED + + IS_SIGNIN_ENABLED + + GOOGLE_APP_ID + + + diff --git a/apps/mobile/apps/staff/ios/config/stage/GoogleService-Info.plist b/apps/mobile/apps/staff/ios/config/stage/GoogleService-Info.plist new file mode 100644 index 00000000..7035bac5 --- /dev/null +++ b/apps/mobile/apps/staff/ios/config/stage/GoogleService-Info.plist @@ -0,0 +1,30 @@ + + + + + API_KEY + AIzaSyCgTXI3QhbEK3r4J5y7ek_6AxqhmR99QjY + GCM_SENDER_ID + 1032971403708 + PLIST_VERSION + 1 + BUNDLE_ID + stage.krowwithus.staff + PROJECT_ID + krow-workforce-staging + STORAGE_BUCKET + krow-workforce-staging.firebasestorage.app + IS_ADS_ENABLED + + IS_ANALYTICS_ENABLED + + IS_APPINVITE_ENABLED + + IS_GCM_ENABLED + + IS_SIGNIN_ENABLED + + GOOGLE_APP_ID + 1:1032971403708:ios:8c2bbd76bc4f55d9356bb9 + + \ No newline at end of file diff --git a/apps/mobile/apps/staff/ios/scripts/firebase-config.sh b/apps/mobile/apps/staff/ios/scripts/firebase-config.sh new file mode 100755 index 00000000..b700a0ad --- /dev/null +++ b/apps/mobile/apps/staff/ios/scripts/firebase-config.sh @@ -0,0 +1,19 @@ +#!/bin/bash +# Copy the correct GoogleService-Info.plist based on the build configuration. +# This script should be added as a "Run Script" build phase in Xcode, +# BEFORE the "Compile Sources" phase. +# +# The FLUTTER_FLAVOR environment variable is set by Flutter when building +# with --flavor. It maps to: dev, stage, prod. + +FLAVOR="${FLUTTER_FLAVOR:-dev}" +PLIST_SOURCE="${PROJECT_DIR}/config/${FLAVOR}/GoogleService-Info.plist" +PLIST_DEST="${PROJECT_DIR}/Runner/GoogleService-Info.plist" + +if [ ! -f "$PLIST_SOURCE" ]; then + echo "error: GoogleService-Info.plist not found for flavor '${FLAVOR}' at ${PLIST_SOURCE}" + exit 1 +fi + +echo "Copying GoogleService-Info.plist for flavor: ${FLAVOR}" +cp "${PLIST_SOURCE}" "${PLIST_DEST}" diff --git a/apps/mobile/apps/staff/lib/firebase_options.dart b/apps/mobile/apps/staff/lib/firebase_options.dart index 3945a3a2..c47d4164 100644 --- a/apps/mobile/apps/staff/lib/firebase_options.dart +++ b/apps/mobile/apps/staff/lib/firebase_options.dart @@ -1,44 +1,22 @@ -// File generated by FlutterFire CLI. - import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; import 'package:flutter/foundation.dart' show defaultTargetPlatform, kIsWeb, TargetPlatform; +import 'package:krow_core/core.dart'; -/// Default [FirebaseOptions] for use with your Firebase apps. +/// Environment-aware [FirebaseOptions] for the Staff app. /// -/// Example: -/// ```dart -/// import 'firebase_options.dart'; -/// // ... -/// await Firebase.initializeApp( -/// options: DefaultFirebaseOptions.currentPlatform, -/// ); -/// ``` +/// Selects the correct Firebase configuration based on the compile-time +/// `ENV` dart define (dev, stage, prod). Defaults to dev. class DefaultFirebaseOptions { static FirebaseOptions get currentPlatform { if (kIsWeb) { - return web; + return _webOptions; } switch (defaultTargetPlatform) { case TargetPlatform.android: - return android; + return _androidOptions; case TargetPlatform.iOS: - return ios; - case TargetPlatform.macOS: - throw UnsupportedError( - 'DefaultFirebaseOptions have not been configured for macos - ' - 'you can reconfigure this by running the FlutterFire CLI again.', - ); - case TargetPlatform.windows: - throw UnsupportedError( - 'DefaultFirebaseOptions have not been configured for windows - ' - 'you can reconfigure this by running the FlutterFire CLI again.', - ); - case TargetPlatform.linux: - throw UnsupportedError( - 'DefaultFirebaseOptions have not been configured for linux - ' - 'you can reconfigure this by running the FlutterFire CLI again.', - ); + return _iosOptions; default: throw UnsupportedError( 'DefaultFirebaseOptions are not supported for this platform.', @@ -46,7 +24,65 @@ class DefaultFirebaseOptions { } } - static const FirebaseOptions web = FirebaseOptions( + static FirebaseOptions get _androidOptions { + switch (AppEnvironment.current) { + case AppEnvironment.dev: + return _devAndroid; + case AppEnvironment.stage: + return _stageAndroid; + case AppEnvironment.prod: + return _prodAndroid; + } + } + + static FirebaseOptions get _iosOptions { + switch (AppEnvironment.current) { + case AppEnvironment.dev: + return _devIos; + case AppEnvironment.stage: + return _stageIos; + case AppEnvironment.prod: + return _prodIos; + } + } + + static FirebaseOptions get _webOptions { + switch (AppEnvironment.current) { + case AppEnvironment.dev: + return _devWeb; + case AppEnvironment.stage: + return _stageWeb; + case AppEnvironment.prod: + return _prodWeb; + } + } + + // =========================================================================== + // DEV (krow-workforce-dev) + // =========================================================================== + + static const FirebaseOptions _devAndroid = FirebaseOptions( + apiKey: 'AIzaSyDBYhflhK6DThKnS7RM-9raKdvyKzLUjY4', + appId: '1:933560802882:android:ee100eab75b6b04c7757db', + messagingSenderId: '933560802882', + projectId: 'krow-workforce-dev', + storageBucket: 'krow-workforce-dev.firebasestorage.app', + ); + + static const FirebaseOptions _devIos = FirebaseOptions( + apiKey: 'AIzaSyDyEXkzZAWpXXe4dAesYaZflt5BEtMn9tA', + appId: '1:933560802882:ios:edf97dab6eb87b977757db', + messagingSenderId: '933560802882', + projectId: 'krow-workforce-dev', + storageBucket: 'krow-workforce-dev.firebasestorage.app', + androidClientId: + '933560802882-fbqg2icq24bmci3f84evjrbth5huh87f.apps.googleusercontent.com', + iosClientId: + '933560802882-fphpkdjubve8k7e8ogqj3fk1qducv3sg.apps.googleusercontent.com', + iosBundleId: 'dev.krowwithus.staff', + ); + + static const FirebaseOptions _devWeb = FirebaseOptions( apiKey: 'AIzaSyBqRtZPMGU-Sz5x5UnRrunKu5NSWYyPRn8', appId: '1:933560802882:web:173a841992885bb27757db', messagingSenderId: '933560802882', @@ -56,23 +92,62 @@ class DefaultFirebaseOptions { measurementId: 'G-9S7WEQTDKX', ); - static const FirebaseOptions android = FirebaseOptions( - apiKey: 'AIzaSyDBYhflhK6DThKnS7RM-9raKdvyKzLUjY4', - appId: '1:933560802882:android:1ae05d85c865f77c7757db', - messagingSenderId: '933560802882', - projectId: 'krow-workforce-dev', - storageBucket: 'krow-workforce-dev.firebasestorage.app', + // =========================================================================== + // STAGE (krow-workforce-staging) + // =========================================================================== + + static const FirebaseOptions _stageAndroid = FirebaseOptions( + apiKey: 'AIzaSyCgTXI3QhbEK3r4J5y7ek_6AxqhmR99QjY', + appId: '1:1032971403708:android:14e471d055e59597356bb9', + messagingSenderId: '1032971403708', + projectId: 'krow-workforce-staging', + storageBucket: 'krow-workforce-staging.firebasestorage.app', ); - static const FirebaseOptions ios = FirebaseOptions( - apiKey: 'AIzaSyDyEXkzZAWpXXe4dAesYaZflt5BEtMn9tA', - appId: '1:933560802882:ios:fa584205b356de937757db', - messagingSenderId: '933560802882', - projectId: 'krow-workforce-dev', - storageBucket: 'krow-workforce-dev.firebasestorage.app', - androidClientId: '933560802882-fbqg2icq24bmci3f84evjrbth5huh87f.apps.googleusercontent.com', - iosClientId: '933560802882-29olj9ku64jbe9h7flinha6hbi8qrluh.apps.googleusercontent.com', - iosBundleId: 'com.krowwithus.staff', + static const FirebaseOptions _stageIos = FirebaseOptions( + apiKey: 'AIzaSyCgTXI3QhbEK3r4J5y7ek_6AxqhmR99QjY', + appId: '1:1032971403708:ios:8c2bbd76bc4f55d9356bb9', + messagingSenderId: '1032971403708', + projectId: 'krow-workforce-staging', + storageBucket: 'krow-workforce-staging.firebasestorage.app', + iosBundleId: 'stage.krowwithus.staff', ); -} \ No newline at end of file + static const FirebaseOptions _stageWeb = FirebaseOptions( + apiKey: 'AIzaSyCgTXI3QhbEK3r4J5y7ek_6AxqhmR99QjY', + appId: '', // TODO: Register web app in krow-workforce-staging + messagingSenderId: '1032971403708', + projectId: 'krow-workforce-staging', + storageBucket: 'krow-workforce-staging.firebasestorage.app', + ); + + // =========================================================================== + // PROD (krow-workforce-prod) + // TODO: Fill in after creating krow-workforce-prod Firebase project + // =========================================================================== + + static const FirebaseOptions _prodAndroid = FirebaseOptions( + apiKey: '', // TODO: Add prod API key + appId: '', // TODO: Add prod app ID + messagingSenderId: '', // TODO: Add prod sender ID + projectId: 'krow-workforce-prod', + storageBucket: 'krow-workforce-prod.firebasestorage.app', + ); + + static const FirebaseOptions _prodIos = FirebaseOptions( + apiKey: '', // TODO: Add prod API key + appId: '', // TODO: Add prod app ID + messagingSenderId: '', // TODO: Add prod sender ID + projectId: 'krow-workforce-prod', + storageBucket: 'krow-workforce-prod.firebasestorage.app', + iosBundleId: 'prod.krowwithus.staff', + ); + + static const FirebaseOptions _prodWeb = FirebaseOptions( + apiKey: '', // TODO: Add prod API key + appId: '', // TODO: Add prod app ID + messagingSenderId: '', // TODO: Add prod sender ID + projectId: 'krow-workforce-prod', + storageBucket: 'krow-workforce-prod.firebasestorage.app', + ); +} diff --git a/apps/mobile/config.dev.json b/apps/mobile/config.dev.json index a6d85eec..9afaadb4 100644 --- a/apps/mobile/config.dev.json +++ b/apps/mobile/config.dev.json @@ -1,4 +1,5 @@ { + "ENV": "dev", "GOOGLE_MAPS_API_KEY": "AIzaSyAyRS9I4xxoVPAX91RJvWJHszB3ZY3-IC0", "CORE_API_BASE_URL": "https://krow-core-api-e3g6witsvq-uc.a.run.app" -} \ No newline at end of file +} diff --git a/apps/mobile/config.prod.json b/apps/mobile/config.prod.json new file mode 100644 index 00000000..4356dd24 --- /dev/null +++ b/apps/mobile/config.prod.json @@ -0,0 +1,5 @@ +{ + "ENV": "prod", + "GOOGLE_MAPS_API_KEY": "", + "CORE_API_BASE_URL": "" +} diff --git a/apps/mobile/config.stage.json b/apps/mobile/config.stage.json new file mode 100644 index 00000000..df7655bd --- /dev/null +++ b/apps/mobile/config.stage.json @@ -0,0 +1,5 @@ +{ + "ENV": "stage", + "GOOGLE_MAPS_API_KEY": "AIzaSyAyRS9I4xxoVPAX91RJvWJHszB3ZY3-IC0", + "CORE_API_BASE_URL": "https://krow-core-api-staging-e3g6witsvq-uc.a.run.app" +} diff --git a/apps/mobile/packages/core/lib/core.dart b/apps/mobile/packages/core/lib/core.dart index d76a363f..e8743adc 100644 --- a/apps/mobile/packages/core/lib/core.dart +++ b/apps/mobile/packages/core/lib/core.dart @@ -9,6 +9,7 @@ export 'src/presentation/widgets/web_mobile_frame.dart'; export 'src/presentation/mixins/bloc_error_handler.dart'; export 'src/presentation/observers/core_bloc_observer.dart'; export 'src/config/app_config.dart'; +export 'src/config/app_environment.dart'; export 'src/routing/routing.dart'; export 'src/services/api_service/api_service.dart'; export 'src/services/api_service/dio_client.dart'; diff --git a/apps/mobile/packages/core/lib/src/config/app_environment.dart b/apps/mobile/packages/core/lib/src/config/app_environment.dart new file mode 100644 index 00000000..d0bd8405 --- /dev/null +++ b/apps/mobile/packages/core/lib/src/config/app_environment.dart @@ -0,0 +1,46 @@ +/// Represents the application environment. +enum AppEnvironment { + dev, + stage, + prod; + + /// Resolves the current environment from the compile-time `ENV` dart define. + /// Defaults to [AppEnvironment.dev] if not set or unrecognized. + static AppEnvironment get current { + const String envString = String.fromEnvironment('ENV', defaultValue: 'dev'); + return AppEnvironment.values.firstWhere( + (AppEnvironment e) => e.name == envString, + orElse: () => AppEnvironment.dev, + ); + } + + /// Whether the app is running in production. + bool get isProduction => this == AppEnvironment.prod; + + /// Whether the app is running in a non-production environment. + bool get isNonProduction => !isProduction; + + /// The Firebase project ID for this environment. + String get firebaseProjectId { + switch (this) { + case AppEnvironment.dev: + return 'krow-workforce-dev'; + case AppEnvironment.stage: + return 'krow-workforce-staging'; + case AppEnvironment.prod: + return 'krow-workforce-prod'; + } + } + + /// A display label for the environment (empty for prod). + String get label { + switch (this) { + case AppEnvironment.dev: + return '[DEV]'; + case AppEnvironment.stage: + return '[STG]'; + case AppEnvironment.prod: + return ''; + } + } +} diff --git a/codemagic.yaml b/codemagic.yaml index d90d8463..2101a658 100644 --- a/codemagic.yaml +++ b/codemagic.yaml @@ -4,50 +4,50 @@ # Reusable script for building the Flutter app client-app-android-apk-build-script: &client-app-android-apk-build-script - name: 👷 🤖 Build Client App APK (Android) + name: Build Client App APK (Android) script: | dart pub global activate melos export PATH="$PATH":"$HOME/.pub-cache/bin" make mobile-install - make mobile-client-build PLATFORM=apk MODE=release + make mobile-client-build PLATFORM=apk MODE=release ENV=$ENV client-app-ios-build-script: &client-app-ios-build-script - name: 👷 🍎 Build Client App (iOS) + name: Build Client App (iOS) script: | dart pub global activate melos export PATH="$PATH":"$HOME/.pub-cache/bin" make mobile-install - make mobile-client-build PLATFORM=ios MODE=release + make mobile-client-build PLATFORM=ios MODE=release ENV=$ENV staff-app-android-apk-build-script: &staff-app-android-apk-build-script - name: 👷 🤖 Build Staff App APK (Android) + name: Build Staff App APK (Android) script: | dart pub global activate melos export PATH="$PATH":"$HOME/.pub-cache/bin" make mobile-install - make mobile-staff-build PLATFORM=apk MODE=release + make mobile-staff-build PLATFORM=apk MODE=release ENV=$ENV staff-app-ios-build-script: &staff-app-ios-build-script - name: 👷 🍎 Build Staff App (iOS) + name: Build Staff App (iOS) script: | dart pub global activate melos export PATH="$PATH":"$HOME/.pub-cache/bin" make mobile-install - make mobile-staff-build PLATFORM=ios MODE=release + make mobile-staff-build PLATFORM=ios MODE=release ENV=$ENV # Reusable script for distributing Android to Firebase distribute-android-script: &distribute-android-script - name: 🚛 🤖 Distribute Android to Firebase App Distribution + name: Distribute Android to Firebase App Distribution script: | # Distribute Android APK - # Note: Using wildcards to catch app-release.apk - APP_PATH=$(find apps/mobile/apps -name "app-release.apk" | head -n 1) + # Note: With flavors the APK is in a flavor-specific subdirectory + APP_PATH=$(find apps/mobile/apps -name "app-${ENV}-release.apk" -o -name "app-release.apk" | head -n 1) if [ -z "$APP_PATH" ]; then echo "No APK found!" exit 1 fi echo "Found APK at: $APP_PATH" - + firebase appdistribution:distribute "$APP_PATH" \ --app $FIREBASE_APP_ID_ANDROID \ --release-notes "Build $FCI_BUILD_NUMBER - Environment: $ENV" \ @@ -56,7 +56,7 @@ distribute-android-script: &distribute-android-script # Reusable script for distributing iOS to Firebase distribute-ios-script: &distribute-ios-script - name: 🚛🍎 Distribute iOS to Firebase App Distribution + name: Distribute iOS to Firebase App Distribution script: | # Distribute iOS IPA_PATH=$(find apps/mobile/apps -name "*.ipa" | head -n 1) @@ -74,7 +74,7 @@ distribute-ios-script: &distribute-ios-script # Reusable script for web quality checks web-quality-script: &web-quality-script - name: ✅ Web Quality Checks + name: Web Quality Checks script: | npm install -g pnpm cd apps/web @@ -85,7 +85,7 @@ web-quality-script: &web-quality-script # Reusable script for mobile quality checks mobile-quality-script: &mobile-quality-script - name: ✅ Mobile Quality Checks + name: Mobile Quality Checks script: | dart pub global activate melos export PATH="$PATH":"$HOME/.pub-cache/bin" @@ -98,7 +98,7 @@ workflows: # Quality workflow (Web + Mobile) # ================================================================================= quality-gates-dev: - name: ✅ Quality Gates (Dev) + name: Quality Gates (Dev) working_directory: . instance_type: mac_mini_m2 max_build_duration: 60 @@ -129,7 +129,7 @@ workflows: artifacts: - apps/mobile/apps/client/build/app/outputs/flutter-apk/*.apk - apps/mobile/apps/client/build/ios/ipa/*.ipa - - apps/mobile/apps/client/build/app/outputs/bundle/release/app-release.aab + - apps/mobile/apps/client/build/app/outputs/bundle/**/*.aab - /tmp/xcodebuild_logs/*.log - flutter_drive.log cache: @@ -153,7 +153,7 @@ workflows: artifacts: - apps/mobile/apps/staff/build/app/outputs/flutter-apk/*.apk - apps/mobile/apps/staff/build/ios/ipa/*.ipa - - apps/mobile/apps/staff/build/app/outputs/bundle/release/app-release.aab + - apps/mobile/apps/staff/build/app/outputs/bundle/**/*.aab - /tmp/xcodebuild_logs/*.log - flutter_drive.log cache: @@ -167,7 +167,7 @@ workflows: # ================================================================================= client-app-dev-android: <<: *client-app-base - name: 🚛 🤖 Client App Dev (Android App Distribution) + name: Client App Dev (Android App Distribution) environment: flutter: stable xcode: latest @@ -184,7 +184,7 @@ workflows: client-app-staging-android: <<: *client-app-base - name: 🚛🤖 Client App Staging (Android App Distribution) + name: Client App Staging (Android App Distribution) environment: flutter: stable xcode: latest @@ -194,23 +194,19 @@ workflows: android_signing: - keystore: KROW_CLIENT_STAGING vars: - ENV: staging + ENV: stage scripts: - *client-app-android-apk-build-script - *distribute-android-script client-app-prod-android: <<: *client-app-base - name: 🚛 🤖 Client App Prod (Android App Distribution) + name: Client App Prod (Android App Distribution) environment: groups: - client_app_prod_credentials android_signing: - keystore: KROW_CLIENT_PROD - keystore_environment_variable: CM_KEYSTORE_PATH_CLIENT - keystore_password_environment_variable: CM_KEYSTORE_PASSWORD_CLIENT - key_alias_environment_variable: CM_KEY_ALIAS_CLIENT - key_password_environment_variable: CM_KEY_PASSWORD_CLIENT vars: ENV: prod scripts: @@ -222,7 +218,7 @@ workflows: # ================================================================================= client-app-dev-ios: <<: *client-app-base - name: 🚛 🍎 Client App Dev (iOS App Distribution) + name: Client App Dev (iOS App Distribution) environment: groups: - client_app_dev_credentials @@ -234,19 +230,19 @@ workflows: client-app-staging-ios: <<: *client-app-base - name: 🚛 🍎 Client App Staging (iOS App Distribution) + name: Client App Staging (iOS App Distribution) environment: groups: - client_app_staging_credentials vars: - ENV: staging + ENV: stage scripts: - *client-app-ios-build-script - *distribute-ios-script client-app-prod-ios: <<: *client-app-base - name: 🚛 🍎 Client App Prod (iOS App Distribution) + name: Client App Prod (iOS App Distribution) environment: groups: - client_app_prod_credentials @@ -261,7 +257,7 @@ workflows: # ================================================================================= staff-app-dev-android: <<: *staff-app-base - name: 🚛 🤖 👨‍🍳 Staff App Dev (Android App Distribution) + name: Staff App Dev (Android App Distribution) environment: flutter: stable xcode: latest @@ -278,7 +274,7 @@ workflows: staff-app-staging-android: <<: *staff-app-base - name: 🚛 🤖 👨‍🍳 Staff App Staging (Android App Distribution) + name: Staff App Staging (Android App Distribution) environment: flutter: stable xcode: latest @@ -288,14 +284,14 @@ workflows: android_signing: - keystore: KROW_STAFF_STAGING vars: - ENV: staging + ENV: stage scripts: - *staff-app-android-apk-build-script - *distribute-android-script staff-app-prod-android: <<: *staff-app-base - name: 🚛 🤖 👨‍🍳 Staff App Prod (Android App Distribution) + name: Staff App Prod (Android App Distribution) environment: flutter: stable xcode: latest @@ -315,7 +311,7 @@ workflows: # ================================================================================= staff-app-dev-ios: <<: *staff-app-base - name: 🚛 🍎 👨‍🍳 Staff App Dev (iOS App Distribution) + name: Staff App Dev (iOS App Distribution) environment: groups: - staff_app_dev_credentials @@ -327,19 +323,19 @@ workflows: staff-app-staging-ios: <<: *staff-app-base - name: 🚛 🍎 👨‍🍳 Staff App Staging (iOS App Distribution) + name: Staff App Staging (iOS App Distribution) environment: groups: - staff_app_staging_credentials vars: - ENV: staging + ENV: stage scripts: - *staff-app-ios-build-script - *distribute-ios-script staff-app-prod-ios: <<: *staff-app-base - name: 🚛 🍎 👨‍🍳 Staff App Prod (iOS App Distribution) + name: Staff App Prod (iOS App Distribution) environment: groups: - staff_app_prod_credentials @@ -348,4 +344,3 @@ workflows: scripts: - *staff-app-ios-build-script - *distribute-ios-script - diff --git a/docs/DESIGN/product-specification.md b/docs/DESIGN/product-specification.md new file mode 100644 index 00000000..a15ae177 --- /dev/null +++ b/docs/DESIGN/product-specification.md @@ -0,0 +1,2778 @@ +# KROW Workforce Management Platform +## Product Specification for Designers + +--- + +## Document Information + +**Version**: 1.0 +**Last Updated**: March 9, 2026 +**Purpose**: This document describes the functional behavior and user experience of KROW's mobile workforce management platform from a design perspective. + +--- + +## Table of Contents + +1. [Introduction](#introduction) +2. [Platform Overview](#platform-overview) +3. [Client Application](#client-application) + - [Authentication](#client-authentication) + - [Home Dashboard](#client-home-dashboard) + - [Billing](#client-billing) + - [Coverage](#client-coverage) + - [Hubs](#client-hubs) + - [Orders](#client-orders) + - [Reports](#client-reports) + - [Settings](#client-settings) +4. [Staff Application](#staff-application) + - [Authentication](#staff-authentication) + - [Home Dashboard](#staff-home-dashboard) + - [Clock In/Out](#staff-clock-in-out) + - [Shifts](#staff-shifts) + - [Availability](#staff-availability) + - [Payments](#staff-payments) + - [Profile](#staff-profile) + - [Profile Sections](#staff-profile-sections) +5. [Glossary](#glossary) + +--- + +## Introduction + +### Purpose + +This document provides a comprehensive overview of the KROW Workforce Management Platform's mobile applications. It is designed specifically for **designers** who need to understand, redesign, or create new user experiences without needing to read code. + +### Scope + +This document covers: +- **Two mobile applications**: Client (Business) app and Staff (Worker) app +- **All features**: Complete functionality across both apps +- **User flows**: How users navigate and interact with the system +- **User stories**: What users can do and why they would do it +- **Inputs and outputs**: What data users provide and what they receive + +This document does **NOT** cover: +- Technical implementation details +- Backend systems or APIs +- Code architecture +- Performance specifications + +### How to Use This Document + +- Each feature is broken down into **user stories** following the format: + - **As a** [type of user] + - **I want to** [perform an action] + - **So that** [I achieve a goal] + +- Complex flows include **Mermaid diagrams** for visual clarity +- **Inputs** describe what users need to provide +- **Outputs** describe what users see or receive +- **Edge cases** highlight special scenarios or error conditions + +### Document Conventions + +- **Client** = Business users who hire staff +- **Staff** = Workers who accept shifts and perform jobs +- **Hub** = A business location/venue where work is performed +- **Shift** = A scheduled work period with specific start/end times +- **Order** = A staffing request placed by a client +- **Coverage** = The fulfillment status of shifts for a given day + +--- + +## Platform Overview + +### What is KROW? + +KROW is a **workforce management platform** that connects businesses (clients) with workers (staff) for flexible staffing needs. The platform consists of two mobile applications: + +1. **Client Application** - Used by businesses to: + - Request staffing for their locations + - Manage multiple business locations (hubs) + - Track worker attendance and performance + - Review and approve invoices + - Monitor business metrics and reports + +2. **Staff Application** - Used by workers to: + - Find and accept available shifts + - Set their weekly availability + - Check in and out of shifts with location verification + - Track earnings and request early payments + - Complete onboarding and compliance requirements + +### Key Concepts + +- **Hub**: A physical business location where staff work (e.g., a restaurant, warehouse, or event venue) +- **Shift**: A scheduled work period with a specific role, start time, end time, and location +- **Order**: A staffing request created by a client specifying positions needed, dates, times, and location +- **Position**: A role within a shift (e.g., Server, Cook, Warehouse Associate) +- **Coverage**: The percentage or count of filled vs. unfilled positions for a given time period +- **Invoice**: A bill generated based on completed shifts, showing worker hours and total cost +- **Reliability Score**: A metric showing how dependable a staff member is (based on attendance, punctuality, cancellations) + +--- + +# Client Application + +The Client Application is designed for **business owners and managers** who need to staff their locations, track worker performance, and manage operational costs. + +--- + +## Client: Authentication + +### Purpose +Allow business users to create accounts, sign in, and manage their authentication sessions. + +### User Stories + +#### Story 1: Create Business Account +**As a** business owner +**I want to** create a new KROW account with my company information +**So that** I can start requesting staff for my business locations + +**Task Flow:** +1. User initiates account creation process +2. User provides required business information: + - Company name + - Email address (for login and communications) + - Password (meeting security requirements) + - Password confirmation (to prevent typos) +3. System validates all provided information +4. System creates business account +5. User gains authenticated access to the platform + +**Information Required:** +- Company name (text, required) +- Business email address (email format, must be unique) +- Secure password (minimum length, complexity requirements) +- Password confirmation (must match) + +**Information Provided to User:** +- Account creation success confirmation +- Validation errors if any (e.g., "Email already in use", "Password too weak", "Passwords don't match") +- Access to authenticated features + +**Edge Cases:** +- Duplicate email: System prevents creation with clear error message +- Network interruption: System provides retry mechanism +- Invalid data format: Real-time validation feedback during input +- Incomplete information: Clear indication of what's missing + +--- + +#### Story 2: Sign In with Email +**As a** returning business user +**I want to** authenticate using my registered email and password +**So that** I can access my business data and features + +**Task Flow:** +1. User initiates authentication process +2. User provides credentials: + - Registered email address + - Account password +3. System validates credentials against stored records +4. System grants authenticated access upon successful validation + +**Information Required:** +- Email address (must match registered account) +- Password (must match stored credential) + +**Information Provided to User:** +- Authentication success confirmation +- Access to authenticated features +- Clear error messaging if credentials invalid ("Invalid credentials") + +**Edge Cases:** +- Forgotten password: System provides credential recovery mechanism (not yet implemented) +- Multiple failed attempts: Temporary access restriction may be triggered for account protection +- Network interruption: Retry capability provided + +--- + +#### Story 3: Sign In with Social Authentication +**As a** business user +**I want to** authenticate using my existing Google or Apple account +**So that** I can access the platform quickly without managing separate passwords + +**Task Flow:** +1. User selects social authentication provider (Google or Apple) +2. System initiates OAuth flow with selected provider +3. User authorizes KROW to access their account through provider interface +4. System receives authorization token from provider +5. System establishes authenticated session +6. User gains access to platform features + +**Information Required:** +- Social provider choice (Google or Apple) +- Authorization approval through provider's authentication system + +**Information Provided to User:** +- Authentication success confirmation +- Access to authenticated features +- Error message if authorization fails with retry option + +**Edge Cases:** +- User cancels authorization: Process terminated, user can retry or use alternative method +- Account doesn't exist: System may create new account automatically or indicate linking requirement +- Authorization server unavailable: Clear error message with alternative authentication options + +--- + +## Client: Home Dashboard + +### Purpose +Provide clients with a customizable dashboard showing key business metrics, quick action shortcuts, and important notifications. Users can personalize widget visibility and order. + +### User Stories + +#### Story 1: View Business Dashboard +**As a** client +**I want to** access a comprehensive overview of my key business metrics and pending tasks +**So that** I can quickly understand my business status and identify actions needed + +**Task Flow:** +1. User accesses primary business overview (default view after authentication) +2. System presents summary information modules displaying: + - Current day's coverage status + - Spending Insights : Weekly cost overview and projections + - Upcoming scheduled shifts + - Past orders with reorder capability +3. User can access detailed information for any metric area + +**Information Required:** +- None (view-only access to business data) + +**Information Provided to User:** +- Spending Insights : Weekly cost overview and projections +- Today's staff coverage status +- Past orders with reorder capability +- Upcoming shift schedule summary + +**Edge Cases:** +- No data available: Empty states with guidance prompts ("No pending invoices", "Create your first order") +- Data loading: Progressive display of information as it becomes available +- Partial data failure: Available information shown with indication of what couldn't be loaded + +--- + +#### Story 2: Customize Dashboard Layout +**As a** client +**I want to** personalize which business metrics are visible and their priority order +**So that** I can focus on information most relevant to my operational needs + +**Task Flow:** +1. User initiates dashboard customization mode +2. System enables customization capabilities: + - Metric modules become repositionable + - Visibility controls become available for each module +3. User adjusts module positions to reflect preferred priority +4. User toggles visibility for individual metrics +5. User saves customization preferences +6. System persists user preferences +7. Dashboard reflects updated layout and visible metrics + +**Information Required:** +- Module position preferences (sequential ordering) +- Module visibility preferences (shown/hidden for each) + +**Information Provided to User:** +- Visual feedback during customization process +- Immediate visibility changes when toggling metrics +- Confirmation of saved preferences ("Layout saved") +- Personalized dashboard reflecting choices + +**Edge Cases:** +- All metrics hidden: System displays guidance to enable at least one metric +- Reset capability: Option to restore default configuration + +--- + +#### Story 3: Reset Dashboard to Defaults +**As a** client +**I want to** restore the dashboard to its original configuration +**So that** I can undo my customizations if they're not meeting my needs + +**Task Flow:** +1. User initiates dashboard customization mode +2. User requests restoration to default configuration +3. System requests confirmation of this action +4. User confirms restoration +5. System restores original metric order and visibility settings +6. User exits customization mode + +**Information Required:** +- User confirmation (proceed or cancel restoration) + +**Information Provided to User:** +- Confirmation prompt explaining what will be reset +- Success message ("Dashboard reset to default") +- Dashboard displaying default configuration + +--- + +## Client: Billing + +### Purpose +Manage invoices, review spending, and approve payments for completed shifts. Clients can track billing periods and drill down into invoice details. + +### User Stories + +#### Story 1: View Billing Summary +**As a** client +**I want to** review my total staffing expenditure and invoice status for a selected time period +**So that** I can understand and monitor my labor costs effectively + +**Task Flow:** +1. User accesses billing information +2. User selects time period for analysis: Today | This Week | This Month | This Quarter (default: This Week) +3. System presents comprehensive spending data: + - Total expenditure for selected period + - Spending breakdown by category or location + - Pending invoices requiring attention (count and total value) + - Historical invoice records +4. User can access additional details for any displayed information + +**Information Required:** +- Time period selection (predefined options) + +**Information Provided to User:** +- Total spending amount for period (prominently displayed) +- Visual or categorical breakdown of spending +- Pending invoices summary (quantity and total amount) +- Historical invoice list with key details + +**Edge Cases:** +- No spending activity in period: Display $0 with explanatory message +- No pending invoices: Confirmation message that all invoices are processed +- Data loading: Progressive disclosure as information becomes available + +--- + +#### Story 2: Review and Approve Pending Invoice +**As a** client +**I want to** examine invoice details and approve or contest charges +**So that** I can ensure payment accuracy for completed work + +**Task Flow:** +1. User accesses pending invoices collection +2. User reviews list of all invoices awaiting approval +3. User selects specific invoice for detailed review +4. System presents comprehensive invoice information: + - Worker identification + - Work period (date and time range) + - Hours worked (with break time calculations) + - Compensation rate + - Total calculated cost + - Work location (hub) +5. User examines all details +6. User approves invoice or requests modifications +7. System processes decision and updates invoice status accordingly + +**Information Required:** +- Invoice selection (from pending list) +- Approval decision (approve or request changes) +- If requesting changes: specific discrepancy details and notes + +**Information Provided to User:** +- Complete invoice breakdown with all work details +- Worker performance notes if applicable +- Approval confirmation ("Invoice approved", moved to history) +- Change request form for documenting specific issues +- Error notification if processing fails (with retry capability) + +**Edge Cases:** +- Disputed hours: Ability to flag time discrepancies with supporting notes +- Worker attendance issues: Relevant notes displayed on invoice (lateness, absence) +- Break time adjustments: Accurate reflection in calculated hours +- Processing failure: Retry mechanism with error explanation + +--- + +#### Story 3: Review Invoice History +**As a** client +**I want to** access my past approved invoices and payment records +**So that** I can track historical spending and reference previous payments + +**Task Flow:** +1. User accesses billing information +2. User navigates to historical invoice section +3. System presents past invoice records with: + - Date of invoice + - Total payment amount + - Payment status (Paid) + - Associated location(s) +4. User can select any invoice for comprehensive details +5. Historical invoice details include all original information plus payment confirmation date + +**Information Required:** +- Invoice selection (from historical list) + +**Information Provided to User:** +- chronological list of all processed invoices +- Full details of any selected historical invoice +- Payment confirmation information + +**Edge Cases:** +- No payment history: Display message indicating no invoices have been processed yet +- Extensive history: Progressive loading mechanism for large volumes + +--- + +## Client: Coverage + +### Purpose +Provide real-time visibility into daily staffing levels, unfilled positions, and worker status. Clients can quickly identify coverage gaps and take action to fill them. + +### User Stories + +#### Story 1: View Daily Coverage Status +**As a** client +**I want to** assess which shifts have assigned workers and which remain unfilled for a specific date +**So that** I can identify staffing gaps and ensure adequate coverage + +**Task Flow:** +1. User accesses staffing coverage information +2. System displays current day's coverage by default +3. User reviews comprehensive coverage data: + - Selected date + - Coverage statistics: total shifts, filled positions, unfilled positions, coverage percentage + - Critical alerts if essential shifts lack staff + - Complete shift inventory showing: + - Worker assignment (name if filled, or unfilled status) + - Work period (start and end times) + - Required role or position + - Work location + - Current status (filled, unfilled, issue indicators like lateness) +4. User can examine all scheduled shifts + +**Information Required:** +- None (defaults to current date) + +**Information Provided to User:** +- Coverage metrics and percentages +- Visual status indicators for each shift +- Critical alerts for staffing gaps +- Complete shift details with assignment status + +**Edge Cases:** +- No shifts scheduled: Message indicating no shifts exist for selected date +- Full coverage achieved: Celebration message for 100% staffing +- Worker delays: Status indicators showing "Running late" with appropriate urgency marking +- Data loading: Progressive display as information becomes available + +--- + +#### Story 2: Select Different Date +**As a** client +**I want to** view coverage information for any specific date +**So that** I can plan ahead or review past staffing performance + +**Task Flow:** +1. User initiates date selection process +2. System presents calendar date picker +3. User selects desired date (past, present, or future) +4. System retrieves and displays coverage data for selected date +5. Date indicator updates to reflect current selection + +**Information Required:** +- Date selection (from calendar interface, any valid date) + +**Information Provided to User:** +- Coverage data specific to selected date +- Updated statistics and shift inventory +- Date confirmation showing current selection + +**Edge Cases:** +- Future date without scheduled shifts: Message indicating no shifts planned yet +- Past date: Historical data with final outcomes (completed, no-show, issues resolved) +- Current date: Real-time status information + +--- + +#### Story 3: Re-post Unfilled Shift +**As a** client +**I want to** broadcast available shifts to recruit additional workers +**So that** I can fill last-minute staffing gaps quickly + +**Task Flow:** +1. User reviews coverage showing unfilled positions +2. User identifies specific unfilled shift +3. User initiates re-posting action for that shift +4. System creates new recruitment notification to available workers +5. System confirms successful re-posting +6. Shift status updates to reflect active recruitment + +**Information Required:** +- Shift selection (from unfilled positions) +- Re-post confirmation + +**Information Provided to User:** +- Success confirmation ("Shift re-posted successfully") +- Updated shift status showing recruiting state +- Recruiting progress indicators + +**Edge Cases:** +- Past shift time: Prevention of re-posting with explanation that shift time has elapsed +- Already recruiting: Indication that shift is already being actively recruited +- No eligible workers: Notification if no workers meet shift requirements + +--- + +#### Story 4: Refresh Coverage Data +**As a** client +**I want to** obtain the most current worker assignments and status information +**So that** I can make decisions based on up-to-date staffing data + +**Task Flow:** +1. User initiates data refresh +2. System displays loading state +3. System retrieves latest coverage information from server +4. Coverage information updates with current data +5. System displays timestamp of last update + +**Information Required:** +- User-initiated refresh request + +**Information Provided to User:** +- Loading state indicator +- Updated coverage information reflecting latest changes +- Timestamp showing when data was last refreshed + +**Edge Cases:** +- No network connection: Error message with retry option +- Refresh already in progress: Prevention of duplicate requests +- No changes since last refresh: Confirmation that data is already current + +--- + +## Client: Hubs + +### Purpose +Manage business locations (hubs) where shifts take place. Clients can add, edit, view, and delete hub information. + +### User Stories + +#### Story 1: View All Hubs +**As a** client +**I want to** access comprehensive information about all my business locations +**So that** I can quickly review and manage my operational sites + +**Task Flow:** +1. User accesses business locations management +2. System presents: + - Summary information (guidance or total location count) + - Capability to add new locations + - Inventory of existing locations displaying: + - Location name + - Physical address + - Key location details +3. User can browse all locations +4. User can access detailed information for any location + +**Information Required:** +- None (view-only access to location data) + +**Information Provided to User:** +- Complete list of business locations +- Location count summary (e.g., "You have 5 active hubs") +- Quick-access to key location details + +**Edge Cases:** +- No locations registered: Empty state with guidance to add first location and prominent capability to do so + +--- + +#### Story 2: Add New Hub +**As a** client +**I want to** register a new business location in my account +**So that** I can schedule staff shifts at that site + +**Task Flow:** +1. User initiates new location creation +2. System presents location information form (creation mode) +3. User provides required location details: + - Location name (text identifier) + - Physical address (full address information, possibly multi-line) + - Cost center assignment (selection from predefined options) + - NFC tag identifier (optional for location verification) +4. User submits location information +5. System validates provided data and creates location record +6. System confirms successful creation +7. New location appears in complete locations inventory + +**Information Required:** +- Location name (required text) +- Physical address (required text, may span multiple lines) +- Cost center assignment (required selection from predefined list) +- NFC tag identifier (optional text) + +**Information Provided to User:** +- Creation success confirmation ("Hub created successfully") +- New location now available in inventory +- Return to locations overview + +**Edge Cases:** +- Incomplete required information: Submission prevented with clear indication of missing fields +- Duplicate location name: Warning provided (but may be allowed) +- Network connectivity issues: Retry mechanism offered +- Invalid data format: Real-time validation feedback + +--- + +#### Story 3: View Hub Details +**As a** client +**I want to** access comprehensive information about a specific business location +**So that** I can reference its address, cost center, and other operational details + +**Task Flow:** +1. User selects specific location from inventory +2. System presents complete location information: + - Location name + - Full physical address + - Cost center assignment + - NFC tag identifier (if assigned) + - Additional metadata (creation date, shift statistics, etc. if available) +3. User reviews information +4. User can initiate location modification + OR request location removal + OR return to locations inventory + +**Information Required:** +- Location selection (from inventory) + +**Information Provided to User:** +- Complete location details +- Capability to modify or remove location +- All associated metadata + +--- + +#### Story 4: Edit Existing Hub +**As a** client +**I want to** update information for an existing business location +**So that** I can maintain accuracy when location details change + +**Task Flow:** +1. User accesses location details +2. User initiates modification mode +3. System presents location information form (edit mode) with current values pre-populated +4. User modifies fields as needed: + - Location name + - Physical address + - Cost center assignment + - NFC tag identifier +5. User submits updated information +6. System validates and applies changes +7. System confirms successful update +8. Updated location information displayed + +**Information Required:** +- Modified field values (same structure as location creation) +- Update confirmation + +**Information Provided to User:** +- Update success confirmation ("Hub updated successfully") +- Refreshed location details reflecting changes +- Return to location details view + +**Edge Cases:** +- Modification cancellation: Changes discarded, return to unmodified details +- No changes made: Notification that no modifications were detected +- Invalid data: Validation feedback before submission allowed +- Network issues: Retry mechanism with preserved changes + +--- + +#### Story 5: Delete Hub +**As a** client +**I want to** remove a business location from my account +**So that** I can maintain a clean inventory of only active operational sites + +**Task Flow:** +1. User accesses location details +2. User initiates deletion process +3. System requests deletion confirmation with warning about permanence +4. User confirms deletion intent +5. System removes location from account +6. System confirms successful deletion +7. Location no longer appears in inventory + +**Information Required:** +- Deletion confirmation (proceed or cancel) +- Understanding of permanent action + +**Information Provided to User:** +- Deletion confirmation (\"Hub deleted successfully\") +- Updated inventory without removed location +- Return to locations overview + +**Edge Cases:** +- Location has active scheduled shifts: Additional warning about impact on shifts with confirmation +- Cancellation of deletion: Returns to details without removing location +- Location has historical data: Confirmation that historical records will be preserved even after location removal + +--- + +## Client: Orders + +### Purpose +Create staffing requests (orders) for business locations. Clients can specify positions needed, dates/times, and choose from multiple order types based on their staffing needs. + +### Order Types Overview + +- **One-Time**: Request staff for a single date (e.g., special event, busy day) +- **Recurring**: Request staff for specific days each week over a period (e.g., every Monday and Friday for 4 weeks) +- **Permanent**: Request staff for certain days indefinitely (e.g., every weekday ongoing) +- **Rapid**: Quick emergency staffing request (simplified flow) + +### User Stories + +#### Story 1: Choose Order Type +**As a** client +**I want to** select the type of staffing order that matches my business need +**So that** I can create an appropriate staffing request + +**Task Flow:** +1. User initiates order creation process +2. System presents order type options with descriptions: + - **One-Time Order** - "Need staff for a single day" + - **Recurring Order** - "Need staff on specific days each week" + - **Permanent Order** - "Need staff indefinitely for certain days" + - **Rapid Order** - "Emergency staffing needed now" +3. User selects desired order type +4. System directs to appropriate order configuration process + +**Information Required:** +- Order type selection (one of four available types) + +**Information Provided to User:** +- Clear descriptions of each order type's purpose +- Access to selected order type's configuration process + +--- + +#### Story 2: Create One-Time Order +**As a** client +**I want to** request staff for a single day +**So that** I can handle a special event or unusually busy day + +```mermaid +graph TD + A[Start: Select One-Time Order] --> B[Provide Event Name] + B --> C[Select Vendor] + C --> D[Select Date] + D --> E[Select Hub Location] + E --> F[Optional: Select Hub Manager] + F --> G[Add First Position] + G --> H{Add Another Position?} + H -->|Yes| I[Add Position] + I --> J[Specify Role
Set Count
Set Times
Set Break] + J --> H + H -->|No| K[Review Order Summary] + K --> L{Form Valid?} + L -->|No| M[Review Validation Errors] + M --> B + L -->|Yes| N[Submit Order] + N --> O[Success Confirmation] + O --> P[View Order in Orders List] +``` + +**Task Flow:** +1. User selects One-Time Order type +2. User provides base order information: + - **Event name**: Text description of the event (e.g., "Grand Opening") + - **Vendor**: Selection from available vendors + - **Date**: Calendar date selection + - **Hub**: Selection from user's registered locations + - System automatically retrieves available hub managers for selected location + - **Hub manager**: Optional manager assignment +3. User defines required positions (can add multiple): + - Initiate position addition + - Specify for each position: + - **Role**: Selection from available roles (e.g., Server, Cook, Bartender) + - **Count**: Quantity of workers needed (numeric value) + - **Start time**: Work period begin time + - **End time**: Work period end time + - **Lunch break**: Whether break is included (yes/no) + - Confirm position addition + - Position added to order +4. User can add additional positions by repeating position definition +5. User can remove positions from order as needed +6. User reviews complete order summary: + - Event details (name, date, location) + - All positions with timing details + - Total workers required +7. User submits order for processing +8. System validates all information and creates staffing request +9. System confirms successful order placement +10. User can access order in orders list, filtered to show the order date + +**Information Required:** +- Event name (text description) +- Vendor (selection from available options) +- Date (calendar date) +- Hub location (selection from user's registered locations) +- Hub manager (optional selection) +- For each position: + - Role (selection from predefined roles) + - Worker count (numeric, minimum 1) + - Start time (time value) + - End time (time value) + - Break inclusion (boolean yes/no) + +**Information Provided to User:** +- Order summary preview showing all details +- Validation feedback (which fields need attention) +- Success confirmation ("Order placed successfully") +- Access to view completed order + +**Edge Cases:** +- No positions defined: Submission prevented until at least one position added +- End time precedes start time: Validation error for that position +- Past date selected: Warning or prevention based on business rules +- No hub managers available: Manager field remains optional or shows empty +- Network failure: Retry mechanism with order data preserved +- Validation errors: Clear indication of which fields require correction + +--- + +#### Story 3: Create Recurring Order +**As a** client +**I want to** request staff for specific days each week over a defined period +**So that** I can handle predictable busy periods without creating multiple individual orders + +**Task Flow:** +1. User selects Recurring Order type +2. User provides order information (similar structure to One-Time): + - Event name + - Vendor selection + - **Start date**: First day to begin recurring schedule + - **End date**: Final day of recurring schedule (maximum 29 days from start) + - **Recurring days**: Which days of the week should repeat (Monday through Sunday) + - Hub location + - Hub manager (optional) +3. User defines required positions (same process as One-Time Order) +4. User reviews order summary displaying: + - Selected recurring weekdays + - Date range coverage + - All position requirements +5. User submits order +6. System creates individual shift postings for each selected weekday within the date range + +**Information Required:** +- Same as One-Time Order, plus: + - Start date (calendar date) + - End date (calendar date, maximum 29 days after start) + - Day selections (Monday through Sunday) + +**Information Provided to User:** +- Order summary showing complete recurrence pattern +- Success notification indicating quantity of shifts created +- Access to view all created orders + +**Edge Cases:** +- No days selected: Submission prevented until at least one weekday chosen +- End date precedes start date: Validation error +- Date range exceeds 29 days: Error message with maximum limit explanation +- Single day selected: System processes as valid recurring pattern for that day + +--- + +#### Story 4: Create Permanent Order +**As a** client +**I want to** request staff for certain days indefinitely +**So that** I can fill long-term positions without specifying an end date + +**Task Flow:** +1. User selects Permanent Order type +2. User provides order information (similar to Recurring): + - Event name + - Vendor selection + - **Start date**: When ongoing staffing begins (no end date required) + - **Recurring days**: Which days of the week (Monday through Sunday) + - Hub location + - Hub manager (optional) +3. User defines required positions +4. User reviews order summary displaying: + - "Permanent" or "Ongoing" status indicator + - Start date + - Selected recurring weekdays + - All position requirements +5. User submits order +6. System creates ongoing shift postings without defined end date + +**Information Required:** +- Same as Recurring Order, but only start date (no end date) +- Day selections (Monday through Sunday) + +**Information Provided to User:** +- Order summary with "Permanent" status indication +- Success confirmation +- Access to view permanent order + +**Edge Cases:** +- No days selected: Submission prevented until at least one weekday chosen +- Modifying or canceling permanent order: Requires separate management action (not covered in creation flow) + +--- + +#### Story 5: Create Rapid Order +**As a** client +**I want to** quickly request emergency staffing +**So that** I can fill urgent last-minute needs efficiently + +**Task Flow:** +1. User selects Rapid Order type +2. System presents simplified order creation process: + - Possibly voice input capability or quick templates + - Only essential fields required (location, role, count, timing) +3. User provides minimal required information +4. User submits immediate staffing request +5. System fast-tracks order with high priority to available workers + +**Information Required:** +- Minimal essential fields (specific requirements TBD based on rapid_order implementation) +- Possibly voice description capability + +**Information Provided to User:** +- Rapid confirmation of request received +- Immediate visibility of order to eligible workers + +**Edge Cases:** +- Time-critical situations requiring fastest possible response +- Higher visibility or priority level to worker community +- Simplified validation for speed + +--- + +#### Story 6: View All Orders +**As a** client +**I want to** access a comprehensive list of all staffing orders I've created +**So that** I can monitor their status and details + +**Task Flow:** +1. User accesses orders management area +2. System presents orders in organized format (calendar or list structure) +3. User can filter by date range if desired +4. Order entries display key information: + - Order date(s) or date range + - Order type (One-Time, Recurring, Permanent) + - Associated location(s) + - Position count + - Fill status (e.g., "5 of 10 positions filled") +5. User can access detailed information for any order + +**Information Required:** +- Optional date range filter +- Refresh capability for updated information + +**Information Provided to User:** +- Complete inventory of all orders +- Status indicators for each order +- Fill progress tracking + +**Edge Cases:** +- No orders created yet: Guidance prompt to create first order +- Cancelled orders: Display with "Cancelled" status +- Very large order history: Progressive loading mechanism + +--- + +## Client: Reports + +### Purpose +Provide comprehensive business intelligence through various report types. Clients can track KPIs, analyze spending, monitor performance, and make data-driven decisions. + +### User Stories + +#### Story 1: View Reports Summary +**As a** client +**I want to** access a high-level overview of key business metrics for a selected time period +**So that** I can quickly understand my business performance + +**Task Flow:** +1. User accesses business reports area +2. User views period options: Today | Week | Month | Quarter +3. User selects desired time period (default: Week) +4. System presents reports overview displaying: + - Summary metric information (total orders, fill rate, total expenditure) + - Access points for detailed report categories: + - Daily Operations analysis + - Performance metrics + - Spend Analysis + - Coverage analysis + - Forecast projections + - No-Show Tracking +5. User can access any detailed report category + +**Information Required:** +- Period selection (predefined options) + +**Information Provided to User:** +- Summary metrics for chosen period +- Access to all detailed report types + +**Edge Cases:** +- No data for selected period: Message indicating no activity during timeframe + +--- + +#### Story 2: View Performance Report (KPIs) +**As a** client +**I want to** see my business performance KPIs with visual indicators +**So that** I can identify areas needing improvement + +**User Flow:** +1. User taps "Performance Report" from hub +2. User sees date/period selector +3. Report displays: + - **Overall Performance Score**: 0-100 with rating (Excellent ≥90, Good 75-89, Needs Work <75) + - **4 Key Performance Indicators**: + - **Fill Rate**: Percentage of positions filled (Target: 95%) + - **Completion Rate**: Percentage of shifts completed without issues + - **On-Time Rate**: Percentage of workers arriving on time + - **Average Fill Time**: How quickly positions are filled (Target: 3 hours) +4. Each KPI shows: + - Progress bar with percentage + - Color coding: Green (≥90%), Yellow (75-89%), Red (<75%) + - Comparison to target +5. User can change period to see trends + +**Inputs:** +- Date/period selection + +**Outputs:** +- Overall score with rating +- 4 KPI cards with progress bars and colors +- Visual indicators for meeting/missing targets + +**Edge Cases:** +- Insufficient data: Shows "Need more data to calculate" message +- All KPIs excellent: Green theme with celebration message + +--- + +#### Story 3: View Spend Report +**As a** client +**I want to** analyze my staffing costs over time +**So that** I can manage my budget and identify cost-optimization opportunities + +**Task Flow:** +1. User accesses Spend Report from reports overview +2. User selects time period (week with Monday-Sunday view, or custom date range) +3. System presents financial analysis displaying: + - **Total Spend**: Prominently displayed total expenditure for selected period + - **Spending Breakdown** across multiple dimensions: + - By business location (visual distribution) + - By role or position type + - By day of week + - Cost per hour metrics +4. User can drill into breakdown sections for additional detail + +**Information Required:** +- Time period selection (week or custom date range) + +**Information Provided to User:** +- Total expenditure amount +- Visual data representations (pie, bar, line formats) +- Multi-dimensional spending breakdown +- Trend analysis over time + +**Edge Cases:** +- No expenditure in period: Display $0 with explanatory message +- Significant spending anomalies: Highlighted with warning indicators + +--- + +#### Story 4: View Daily Operations Report +**As a** client +**I want to** access a comprehensive snapshot of operations for a specific date +**So that** I can review that day's performance and activities + +**Task Flow:** +1. User accesses Daily Operations report from reports overview +2. User selects specific date +3. System presents operational analysis displaying: + - Orders created that day (count) + - Positions filled (count and percentage) + - Total staffing expenditure for the day + - Worker attendance summary + - Issues or incidents if any occurred +4. User reviews all operational metrics + +**Information Required:** +- Date selection + +**Information Provided to User:** +- Complete operational metrics for chosen date +- Summary information with counts and financial amounts +- Attendance and performance indicators + +**Edge Cases:** +- Future date selected: Indication that data not yet available +- No activity on date: Message indicating no operations occurred +- Incomplete data: Clear indication of what information is still pending + +--- + +#### Story 5: View Coverage Report +**As a** client +**I want to** analyze my shift fill rates over time +**So that** I can identify patterns and improve my staffing strategy + +**Task Flow:** +1. User accesses Coverage Report from reports overview +2. User selects date range for analysis +3. System presents coverage analysis displaying: + - Overall coverage percentage (e.g., 87% of shifts filled) + - Unfilled positions count with alerts + - Multi-dimensional breakdown: + - By business location + - By position type + - By day of week + - By time of day + - Trend visualization showing coverage changes over time +4. User identifies patterns in low-coverage periods or locations + +**Information Required:** +- Date range selection + +**Information Provided to User:** +- Overall coverage percentage +- Detailed unfilled positions inventory +- Multi-dimensional breakdown analysis +- Trend analysis revealing patterns + +**Edge Cases:** +- 100% coverage achieved: Success celebration message +- Chronic low coverage areas: Highlighted with improvement recommendations +- Insufficient data: Indication that broader date range would provide better analysis + +--- + +#### Story 6: View Forecast Report +**As a** client +**I want to** access predicted staffing demand and supply trends +**So that** I can plan proactively and avoid staffing shortages + +**Task Flow:** +1. User accesses Forecast Report from reports overview +2. System presents predictive analysis displaying: + - Projected staffing demand for upcoming weeks + - Available worker supply trend projections + - Gap analysis comparing demand against supply + - Strategic recommendations (e.g., "Consider posting shifts earlier to improve fill rates") +3. User reviews projections and trend visualizations + +**Information Required:** +- Optional date range for forecast period + +**Information Provided to User:** +- Demand trend projections +- Supply trend projections +- Gap analysis identifying potential shortfalls +- Actionable recommendations for optimization + +**Edge Cases:** +- New account with limited historical data: Message indicating forecast model is being developed +- Highly variable patterns: Wider confidence intervals shown +- Stable demand: High confidence projections with reinforcement + +--- + +#### Story 7: View No-Show Report +**As a** client +**I want to** track worker reliability and shift attendance issues +**So that** I can address recurring problems and improve operational consistency + +**Task Flow:** +1. User accesses No-Show Report from reports overview +2. User selects date range for analysis +3. System presents reliability analysis displaying: + - Total no-show occurrences and rate (percentage of scheduled shifts) + - Workers flagged for multiple no-show incidents + - Shifts most frequently affected by no-shows + - Breakdown by business location +4. User reviews data to identify reliability patterns + +**Information Required:** +- Date range selection + +**Information Provided to User:** +- No-show count and percentage rate +- Worker list with incident counts +- Location-based breakdown +- Recommendations for reliability improvement + +**Edge Cases:** +- Perfect attendance record: Celebration message for zero no-shows +- High no-show rate: Alert highlighting the issue with suggested actions +- Repeated offenders: Clear identification for potential intervention + +--- + +## Client: Settings + +### Purpose +Manage user profile information, account preferences, and app settings. Clients can update their personal details and sign out. + +### User Stories + +#### Story 1: View Profile and Settings +**As a** client +**I want to** access my profile information and account configuration options +**So that** I can verify my details and access account management capabilities + +**Task Flow:** +1. User accesses account settings area +2. System displays: + - Profile information: + - Profile picture or avatar + - User name + - Company name + - Configuration options: + - Profile editing capability + - Preferences (if applicable) + - Help and Support access (if applicable) + - Sign out capability +3. User reviews information and available options + +**Information Required:** +- None (view-only access to profile data) + +**Information Provided to User:** +- Complete profile information display +- Available settings and configuration menu options + +--- + +#### Story 2: Edit Profile Information +**As a** client +**I want to** update my personal information +**So that** my account details remain current and accurate + +**Task Flow:** +1. User initiates profile editing process +2. System presents profile information form with current values: + - Profile picture (with capability to change) + - First name + - Last name + - Email address + - Phone number +3. User modifies desired fields +4. User submits updated information +5. System validates and applies changes +6. System confirms successful update +7. User returns to settings view with updated information + +**Information Required:** +- Profile picture (image file) +- First name (text) +- Last name (text) +- Email address (valid email format) +- Phone number (valid phone format) + +**Information Provided to User:** +- Update success confirmation ("Profile updated successfully") +- Refreshed profile information in settings +- Return to settings overview + +**Edge Cases:** +- Invalid email format: Validation error with format guidance +- Required fields empty: Submission prevented until complete +- Modification cancellation: Changes discarded, return to unmodified state +- Email already registered to another account: Error message indicating conflict +- Network issues: Retry mechanism with preserved changes + +--- + +#### Story 3: Sign Out +**As a** client +**I want to** terminate my authenticated session +**So that** I can secure my account when not in use + +**Task Flow:** +1. User initiates sign out process +2. System requests confirmation: "Are you sure you want to sign out?" +3. User confirms intent to sign out +4. System terminates authenticated session +5. User returned to authentication entry point + +**Information Required:** +- Sign out confirmation (proceed or cancel) + +**Information Provided to User:** +- Sign out confirmation +- Access to authentication entry point +- Session cleared confirmation + +**Edge Cases:** +- Cancellation of sign out: Return to settings without terminating session +- Automatic timeout: Session termination after period of inactivity (if implemented) +- Unsaved changes elsewhere: Warning about potential data loss (if applicable) + +--- + +# Staff Application + +The Staff Application is designed for **workers** who want to find shifts, track their work, manage availability, and get paid. + +--- + +## Staff: Authentication + +### Purpose +Allow workers to sign up and sign in using phone number verification. This includes a multi-step profile setup wizard for new users. + +### User Stories + +#### Story 1: Sign Up with Phone Number +**As a** new worker +**I want to** create an account using my phone number +**So that** I can start finding work through KROW + +```mermaid +graph TD + A[Start: Open App] --> B[Begin Registration] + B --> C[Choose Sign Up] + C --> D[Provide Phone Number
10-digit US format] + D --> E{Phone Valid?} + E -->|No| F[Validation Error] + F --> D + E -->|Yes| G[Request Verification Code] + G --> H[31-second Cooldown Period] + H --> I[Receive SMS with OTP] + I --> J[Provide 6-digit OTP] + J --> K{OTP Correct?} + K -->|No| L[Verification Error
Retry Available] + L --> J + K -->|Yes| M[Account Created] + M --> N[Begin Profile Setup] + N --> O[Step 1: Basic Info] + O --> P[Step 2: Location Preferences] + P --> Q[Step 3: Experience/Skills] + Q --> R[Submit Profile] + R --> S[Access Home Dashboard] +``` + +**Task Flow - Phone Verification:** +1. User opens application and initiates account creation process +2. User selects registration option +3. System presents phone verification process +4. User provides phone number: + - 10 digits for US numbers + - System automatically formats as input progresses (e.g., (555) 123-4567) +5. User requests verification code delivery +6. System sends SMS with 6-digit OTP +7. System initiates cooldown timer (31 seconds before resend capability) +8. User receives SMS and provides 6-digit OTP +9. System verifies OTP authenticity +10. Upon successful verification, system creates user account +11. User proceeds to Profile Setup wizard + +**Information Required:** +- Phone number (10 digits, automatically formatted) +- OTP code (6 digits from SMS) + +**Information Provided to User:** +- SMS delivery confirmation: "Code sent to (555) 123-4567" +- Cooldown timer: "Resend available in 31s" +- Successful verification: Proceed to Profile Setup +- Verification failure: "Invalid code. Please try again." with retry capability + +**Edge Cases:** +- Invalid phone format: Immediate error message display +- OTP expiration: Capability to request new code +- Excessive failed attempts: Temporary account lock with "Try again in X minutes" message +- SMS not received: "Resend Code" capability available after cooldown period +- Network issues: Retry mechanism for code delivery + +--- + +#### Story 2: Complete Profile Setup Wizard +**As a** new worker +**I want to** complete my profile setup through guided steps +**So that** clients can discover me and I can be matched to appropriate work opportunities + +**Task Flow - 3-Step Wizard:** + +**Step 1: Basic Information** +1. User sees progress indicator ("Step 1 of 3") +2. User provides full name (minimum 2 characters) +3. User proceeds to next step + +**Step 2: Location Preferences** +1. User sees progress indicator ("Step 2 of 3") +2. System presents multi-select list of available work locations/areas +3. User selects preferred work locations (minimum one required) +4. User can return to previous step or proceed forward + +**Step 3: Experience & Skills** +1. User sees progress indicator ("Step 3 of 3") +2. System presents multi-select list of roles/skills (e.g., Server, Cook, Warehouse) +3. User selects all applicable skills (minimum one required) +4. User can return to previous step or complete setup +5. User submits completed profile information +6. System validates all provided information +7. Upon validation success, user gains access to main application + +**Information Required:** +- **Step 1**: Full name (text, minimum 2 characters) +- **Step 2**: Location preferences (multi-select, minimum 1 selection) +- **Step 3**: Skills and experience (multi-select, minimum 1 selection) + +**Information Provided to User:** +- Progress indicators throughout wizard (1 of 3, 2 of 3, 3 of 3) +- Navigation controls (back, next, submit capabilities) +- Validation feedback for incomplete or invalid data +- Welcome confirmation: "Welcome to KROW!" +- Access to main application features + +**Edge Cases:** +- Required fields incomplete: Forward progression prevented until requirements met +- Returning to authentication from Step 1: May result in progress loss (warning appropriate) +- Application closure during setup: Progress preserved for later completion +- Network interruption: Setup data preserved locally for retry + +--- + +#### Story 3: Sign In with Phone Number +**As a** returning worker +**I want to** authenticate using my phone number +**So that** I can quickly access my account + +**Task Flow:** +1. User initiates authentication process +2. System presents phone verification (same process as registration) +3. User provides phone number +4. User requests verification code +5. System sends OTP via SMS +6. User provides received OTP +7. System verifies OTP authenticity +8. Upon successful verification, user gains authenticated access +9. User accesses main application + +**Information Required:** +- Phone number (10 digits) +- OTP code (6 digits from SMS) + +**Information Provided to User:** +- SMS delivery confirmation +- Successful authentication: Access to main application +- Authentication failure: "Invalid code" with retry capability + +**Edge Cases:** +- Unregistered phone number: Error message "No account found. Please sign up." +- Incomplete profile: Automatic redirect to Profile Setup wizard at last incomplete step +- All standard OTP edge cases (expiration, too many attempts, SMS delivery issues) + +--- + +## Staff: Home Dashboard + +### Purpose +Provide workers with a personalized dashboard showing shift summaries, recommendations, benefits overview, and quick actions. + +### User Stories + +#### Story 1: View Shift Summary +**As a** worker +**I want to** access my upcoming shifts and discover recommended opportunities +**So that** I can plan my schedule and find new work + +**Task Flow:** +1. User accesses main application overview +2. System presents personalized information: + - **Personal greeting**: "Hello, [Worker Name]" + - **Today's Schedule**: + - All shifts scheduled for current day + - Each displaying: Time, location, role, current status + - **Tomorrow's Schedule**: + - Preview of next day's commitments + - **Recommended Opportunities**: + - Algorithm-suggested shifts based on preferences and work history + - Capability to browse complete opportunity marketplace + - **Benefits Summary**: + - Quick overview of benefits information + - Access to detailed benefits information +3. User can browse all information sections +4. User can access detailed information for any individual shift +5. User can access complete shift marketplace + +**Information Required:** +- None (view-only access to dashboard data) + +**Information Provided to User:** +- Personalized greeting +- Today's complete shift schedule +- Tomorrow's shift preview +- Algorithmically recommended shifts +- Benefits information summary +- Quick access to key features + +**Edge Cases:** +- No shifts scheduled today: Message "No shifts scheduled today" with access to shift discovery +- Incomplete profile: Banner prompting profile completion to unlock shift recommendations and features +- No recommended shifts: Alternative messaging suggesting profile enhancement or shift marketplace browsing + +--- + +#### Story 2: Enable Auto-Match for Shifts +**As a** worker +**I want to** automatically receive shift matches based on my preferences +**So that** I don't miss opportunities without constantly monitoring the application + +**Task Flow:** +1. User sees auto-match capability toggle in application +2. User activates auto-match feature +3. System confirms activation: "Auto-match enabled. You'll be notified when shifts matching your preferences are available." +4. User receives push notifications when suitable shifts are identified +5. User can deactivate auto-match by toggling off + +**Information Required:** +- Auto-match preference (enabled/disabled) + +**Information Provided to User:** +- Activation confirmation message +- Push notifications when matching shifts are found +- Settings indicator showing auto-match status + +**Edge Cases:** +- Incomplete profile: Auto-match unavailable with guidance "Complete your profile to enable auto-match" +- Push notifications disabled: Prompt to enable device notification permissions +- No matching shifts found: Suggestions to broaden preferences or check back later + +--- + +#### Story 3: View Benefits Information +**As a** worker +**I want to** learn about benefits available to me +**So that** I understand the complete value proposition of working through KROW + +**Task Flow:** +1. User accesses benefits information from overview +2. System presents comprehensive benefits overview displaying: + - Complete list of available benefits (e.g., health insurance, early pay, performance bonuses) + - Detailed description of each benefit + - Eligibility requirements for each benefit + - Instructions for accessing or enrolling in each benefit +3. User reviews all benefits information +4. User returns to main overview + +**Information Required:** +- None (view-only access to benefits data) + +**Information Provided to User:** +- Complete benefits inventory with descriptions +- Eligibility criteria for each benefit +- Enrollment or access instructions +- Current eligibility status if applicable + +**Edge Cases:** +- Not yet qualified for benefits: Message indicating "Complete more shifts to unlock benefits" with progress tracking +- Partially eligible: Clear indication of which benefits are currently accessible +- Enrollment required: Call-to-action for benefits requiring active enrollment + +--- + +## Staff: Clock In Out + +### Purpose +Track worker attendance with location verification. Workers can check in and out of shifts, log break times, and enable commute tracking. + +### User Stories + +#### Story 1: Check In to Shift with Location Verification +**As a** worker +**I want to** register my arrival to a shift with automatic location verification \n**So that** I confirm my presence and initiate time tracking + +```mermaid +graph TD + A[Start: Access Clock In] --> B[Load Today's Shifts] + B --> C{Multiple Shifts?} + C -->|Yes| D[Select Specific Shift] + C -->|No| E[Shift Auto-Selected] + D --> F[Request Location Permission] + E --> F + F --> G{Permission Granted?} + G -->|No| H[Error: Location Required] + G -->|Yes| I[Acquire Current Location] + I --> J[Calculate Distance from Venue] + J --> K{Within 500m?} + K -->|No| L[Warning: Too Far from Venue] + K -->|Yes| M[Enable Check-In] + M --> N{Confirmation Method?} + N -->|Swipe| O[Swipe Gesture Confirmation] + N -->|Action| P[Direct Confirmation] + O --> Q[Optional: Provide Check-In Notes] + P --> Q + Q --> R[Submit Check-In] + R --> S[Success: Arrival Registered] + S --> T[Display Check-Out Capability
Show Break Logging
Show Commute Tracking] +``` + +**Task Flow:** +1. User accesses attendance tracking area +2. System loads today's scheduled shifts +3. Shift selection:\n - If multiple shifts scheduled: User selects desired shift\n - If single shift: System auto-selects\n4. System requests location access permission (if not previously granted)\n5. User grants location access\n6. System acquires user's current geographical position\n7. System calculates distance from designated shift venue\n8. If within 500 meter radius:\n - Check-in capability becomes available\n - Distance information displayed (e.g., \"120m away\")\n9. User can register arrival via two methods:\n - **Gesture confirmation**: Swipe action across designated area\n - **Direct confirmation**: Direct action submission\n10. Optional notes interface appears (user can provide additional information or skip)\n11. User confirms arrival registration\n12. System confirms successful check-in: \"Checked in to [Shift Name]\"\n13. Interface updates to show:\n - Check-in timestamp\n - Break logging capability\n - Check-out capability\n - Optional: Commute tracking features\n\n**Information Required:**\n- Location permission (system request)\n- Shift selection (if multiple available)\n- Check-in confirmation (gesture or direct action)\n- Optional arrival notes (text)\n\n**Information Provided to User:**\n- Current distance from venue location\n- Location verification status\n- Check-in confirmation with precise timestamp\n- Updated interface showing departure registration capability\n\n**Edge Cases:**\n- **Location permission denied**: Error message \"Location access required to check in\" with guidance to device settings\n- **Distance exceeds threshold** (>500m): Warning \"You're too far from the venue. Move closer to check in.\" with actual distance displayed\n- **GPS signal unavailable**: Error \"Unable to determine location. Check your connection.\"\n- **Already registered arrival**: Display \"Already checked in at [time]\" with departure registration capability\n- **Incorrect shift selected**: User can modify selection before arrival confirmation\n- **Network connectivity issues**: Queue check-in for submission when connection restored + +--- + +#### Story 2: Log Break Time +**As a** worker +**I want to** record when I take breaks +**So that** my break time is accurately tracked and properly deducted from billable hours + +**Task Flow:** +1. User has registered arrival to shift +2. System displays break logging capability +3. User initiates break period recording +4. System displays running timer tracking break duration +5. User completes break and ends break period recording +6. System records total break duration +7. Optional: User can categorize break type (lunch, rest, etc.) + +**Information Required:** +- Break start (user-initiated) +- Break end (user-initiated) +- Optional: Break type classification + +**Information Provided to User:** +- Active break timer display +- Total break time recorded +- Confirmation of break logging + +**Edge Cases:** +- Forgot to end break: Capability to manually adjust break duration +- Multiple breaks: System tracks each break period independently with cumulative tracking +- System interruption: Break timer continues in background, recovers on re-access + +--- + +#### Story 3: Check Out of Shift +**As a** worker +**I want to** register my departure from a shift +**So that** my work time is fully recorded for compensation + +**Task Flow:** +1. User has registered arrival and completed work +2. User initiates departure registration +3. Optional notes interface appears +4. User provides additional information (if desired) or skips +5. User confirms departure +6. System verifies location again (same 500m proximity requirement) +7. System records departure timestamp +8. System calculates total work time (arrival - departure minus breaks) +9. System presents work summary displaying: + - Arrival time + - Departure time + - Total hours worked + - Break time deducted + - Estimated compensation (if available) + +**Information Required:**\n- Departure confirmation\n- Optional departure notes (text)\n- Location verification\n\n**Information Provided to User:**\n- Departure confirmation with precise timestamp\n- Comprehensive work summary (hours worked, breaks taken, estimated pay)\n- Complete time tracking information\n\n**Edge Cases:**\n- Departure distance exceeds venue threshold: Warning message but may allow with approval workflow\n- Forgot to register departure: Supervisor manual adjustment capability or automatic departure at scheduled shift end\n- Early departure: Warning \"Shift not yet complete. Confirm early check-out?\" with acknowledgment required\n- Network issues: Queue departure registration for submission when connected + +--- + +#### Story 4: Enable Commute Tracking +**As a** worker +**I want to** enable commute tracking +**So that** clients can monitor my estimated arrival time + +**Task Flow:** +1. After registering shift arrival, user sees commute tracking capability +2. User enables commute tracking +3. System begins continuous location monitoring +4. System calculates estimated time of arrival to venue +5. ETA information displayed to user and visible to client +6. System provides real-time updates of distance and ETA +7. When user proximity reaches venue (distance < 50m), system automatically disables commute mode + +**Information Required:** +- Commute tracking preference (enabled/disabled) +- Continuous location updates + +**Information Provided to User:** +- Estimated arrival time (e.g., "Arriving in 12 minutes") +- Distance to venue (e.g., "2.3 km away") +- Real-time progress updates + +**Edge Cases:** +- Location tracking interruption: System displays last known position +- Arrival but ETA persisting: Auto-clears when within 50m proximity +- Privacy preference: User can disable tracking at any time +- Route changes: ETA automatically recalculates based on current position + +--- + +## Staff: Shifts + +### Purpose +Comprehensive shift management including browsing available shifts (marketplace), managing assigned shifts, and viewing shift history. + +### User Stories + +#### Story 1: View My Assigned Shifts +**As a** worker +**I want to** access all shifts I'm assigned to +**So that** I can plan my schedule and track my commitments + +**Task Flow:** +1. User accesses shift management area +2. System displays "My Shifts" view by default +3. User reviews complete list of assigned/accepted shifts (chronologically sorted) +4. Each shift entry displays: + - Date and day of week + - Start and end times + - Role or position + - Location name and address + - Compensation rate + - Status (Upcoming, Confirmed, Pending) +5. User can browse all shifts +6. User can access detailed information for any shift +7. User can confirm attendance or cancel shift (if policy permits) + +**Information Required:** +- None (view-only access to assigned shifts) + +**Information Provided to User:** +- Complete inventory of assigned shifts +- Shift status indicators +- Quick action capabilities (Confirm, Cancel if allowed) + +**Edge Cases:** +- No assigned shifts: Message "No upcoming shifts. Browse available shifts in Find tab." +- Cancelled shifts: Display with "Cancelled" status +- Past shifts: May display with "View Feedback" or "View Details" capability +- Conflicting shifts: Visual indicators or warnings + +--- + +#### Story 2: Browse and Book Available Shifts +**As a** worker +**I want to** browse available shifts in the marketplace and commit to ones that interest me +**So that** I can fill my schedule and maximize earnings + +```mermaid +graph TD + A[Start: Access Find Shifts] --> B[Load Available Shifts] + B --> C[Display Shift Inventory] + C --> D{User Action?} + D -->|Filter| E[Select Job Type Filter] + E --> F[Apply Filter] + F --> C + D -->|Search| G[Provide Search Query
Location or keyword] + G --> H[Apply Search] + H --> C + D -->|View Shift| I[Select Shift] + I --> J[Open Shift Details] + J --> K[Verify Profile Status] + K --> L{Profile Complete?} + L -->|No| M[Profile Completion Required
Complete profile to book shifts] + M --> N[Access Profile Completion] + L -->|Yes| O[Display Complete Shift Information
Date, Time, Location
Pay, Requirements] + O --> P{User Decision?} + P -->|Book| Q[Initiate Booking] + Q --> R[Confirm Booking] + R --> S[Submit Booking Request] + S --> T[Success: Shift Assignment Confirmed] + T --> U[View in My Shifts] + P -->|Decline| V[Return to Marketplace] + V --> C +``` + +**Task Flow:** +1. User accesses shift marketplace +2. System loads all available shifts matching user's preferences +3. User reviews shift inventory displaying: + - Date and time period + - Duration + - Role or position + - Location + - Compensation rate (hourly or flat rate) + - Distance from user's current location (if location enabled) +4. User can apply filters: + - By job type (selection from available categories) + - By search criteria (text input for location or keywords) +5. User selects specific shift for detailed review +6. System presents comprehensive shift information: + - Complete date and time details + - Venue name and complete address + - Compensation breakdown + - Required qualifications (skills needed) + - Break schedule + - Job description + - **Profile completion requirement** (if incomplete) +7. If profile complete, booking capability enabled +8. User initiates booking process +9. System requests confirmation: "Confirm booking for [Shift Name] on [Date]?" +10. User confirms booking intent +11. System processes shift assignment +12. Success confirmation: "Shift booked successfully!" +13. Shift now appears in "My Shifts" area +14. User can access My Shifts or continue browsing + +**Information Required:** +- Job type filter (selection from available options) +- Search query (text input) +- Shift selection +- Booking confirmation + +**Information Provided to User:** +- Filtered or searched shift results +- Complete shift details +- Booking confirmation dialog +- Success confirmation +- Shift assignment in My Shifts area + +**Edge Cases:** +- **Profile incomplete**: Booking disabled or hidden; message displayed: "Complete your profile to book shifts" with profile access link +- **Shift capacity reached**: Error message "This shift has been filled. Try another." +- **Schedule conflict**: Warning "You have another shift at this time. Booking will create a conflict." +- **No matching shifts**: Empty state with "No shifts match your criteria" and filter reset capability +- **Distance consideration**: Warning for distant shifts but booking still permitted +- **Network issues**: Queue booking request for submission when connected + +--- + +#### Story 3: Decline Available Shift +**As a** worker +**I want to** remove shifts I'm not interested in from my view \n**So that** I can focus on opportunities that better match my preferences + +**Task Flow:** +1. User reviews shift details +2. User indicates disinterest in specific shift\n3. System removes shift from user's feed or marks as declined +4. User returns to marketplace +5. Optional: System requests feedback \"Why did you decline?\" + +**Information Required:** +- Decline action confirmation +- Optional: Decline reason feedback + +**Information Provided to User:** +- Shift removed from current view +- Optional: Feedback collection interface + +**Edge Cases:** +- Declined shift may reappear if search filters change +- Frequent declines: System may adjust recommendation algorithm\n- Undo capability: Brief window to reverse decline action if available +- Too many declines: System may adjust recommendations + +--- + +#### Story 4: View Shift History +**As a** worker +**I want to** access all my past completed shifts +**So that** I can reference previous work and track my earnings history + +**Task Flow:** +1. User accesses shift history area +2. System presents chronologically ordered list of completed shifts (most recent first) +3. Each shift entry displays: + - Date + - Role or position + - Location name + - Hours worked + - Total compensation + - Status (Completed, No-Show, Cancelled) +4. User can access detailed information for any shift +5. Historical shift details may include: + - Arrival and departure timestamps + - Break duration + - Client feedback or rating (if available) +6. User can filter history by date range + +**Information Required:** +- None (view-only access to historical data) +- Optional: Date range filter + +**Information Provided to User:** +- Complete inventory of past shifts +- Total earnings over selected period +- Detailed information for individual shifts + +**Edge Cases:** +- No history available: Message "No completed shifts yet" with encouragement +- Disputed shift: Status indicator showing "Under Review" +- Multiple pages: Progressive loading or pagination for extensive history + +--- + +## Staff: Availability + +### Purpose +Allow workers to set their weekly availability, indicating which days and times they are free to work. This helps the system match workers to appropriate shifts. + +### User Stories + +#### Story 1: Set Weekly Availability +**As a** worker +**I want to** indicate which days of the week I'm available to work +**So that** I only receive shift offers matching my schedule + +**Task Flow:** +1. User accesses availability management +2. System displays current week (Monday-Sunday) +3. For each day, information presented: + - Day name (e.g., "Monday") + - Date + - Availability status control (available/unavailable) + - Optional: Specific time slot controls (if granular availability enabled) +4. User adjusts availability status for each day +5. System automatically saves changes (optimistic updates) +6. Brief confirmation: "Availability saved" + +**Information Required:** +- Day availability status (7 day selections) +- Optional: Time slot availability within each day + +**Information Provided to User:** +- Visual confirmation of status changes +- Automatic save confirmation +- Updated availability reflected in shift matching algorithm + +**Edge Cases:** +- All days marked unavailable: Warning "No availability set. You won't receive shift offers." +- Changes during system loading: Queue changes for application after load completion +- Network issues: Local changes preserved and synchronized when connected + +--- + +#### Story 2: Use Quick Availability Presets +**As a** worker +**I want to** quickly apply common availability patterns +**So that** I don't need to configure each day individually + +**Task Flow:** +1. User sees quick preset options: + - "All Week" - All 7 days available + - "Weekdays Only" - Monday-Friday available + - "Weekends Only" - Saturday-Sunday available + - "Clear All" - All days unavailable +2. User selects desired preset +3. System applies pattern to all day availability settings +4. Changes automatically saved +5. User can manually adjust individual days after applying preset + +**Information Required:** +- Preset selection + +**Information Provided to User:** +- Availability settings updated to match preset +- Automatic save confirmation + +**Edge Cases:** +- Current settings already match preset: Confirmation displayed even without changes +- Manual adjustments after preset: Overrides preset for specific days + +--- + +#### Story 3: Set Availability for Future Weeks +**As a** worker +**I want to** configure my availability for upcoming weeks +**So that** I can plan ahead and indicate future unavailability (vacation, etc.) + +**Task Flow:** +1. User sees week navigation controls +2. User navigates forward to view next week +3. Week view updates to display selected week's dates +4. User configures availability for that week using standard controls or presets +5. User can continue navigating to additional future weeks +6. User can return to current week at any time + +**Information Required:** +- Week navigation (forward/backward) +- Day availability settings for each week + +**Information Provided to User:** +- Week view updates to selected timeframe +- Availability settings saved per week +- Week identifier display (e.g., "Week of March 10") + +**Edge Cases:** +- Future range limit: May restrict to 4-8 weeks ahead +- Past weeks: Cannot edit historical weeks (read-only or hidden) +- Current week changes: Immediate effect on shift matching + +--- + +## Staff: Payments + +### Purpose +Track earnings, view payment history, and access early pay options. Workers can see their financial data and request faster payment when needed. + +### User Stories + +#### Story 1: View Earnings Summary +**As a** worker +**I want to** access my current balance and earnings trends over time +**So that** I understand my earned income and payment schedule + +**Task Flow:** +1. User accesses payments and earnings area +2. System presents financial overview displaying: + - **Balance Information**: + - Total account balance (prominently displayed) + - Amount available for early payment access + - Next scheduled payout date + - **Earnings Trend Visualization**: + - Visual representation of earnings over time + - Selectable time periods (Day, Week, Month) + - **Payment History Preview**: + - Recent transaction summary +3. User reviews financial summary information + +**Information Required:** +- None (view-only access to financial data) + +**Information Provided to User:** +- Current balance amount +- Earnings trend visualization +- Payment history preview + +**Edge Cases:** +- No earnings yet: Display $0.00 with message "Complete shifts to start earning" +- Negative balance: Alert indication if deductions exceed earnings +- Pending payments: Clear indication of amounts in processing + +--- + +#### Story 2: View Payment History +**As a** worker +**I want to** access detailed records of all my payments and withdrawals +**So that** I can track my complete financial transaction history + +**Task Flow:** +1. User navigates to Payment History section or accesses complete history +2. System presents comprehensive transaction list displaying: + - Date and time + - Transaction description (Shift payment, Early pay, ATM withdrawal) + - Amount (positive for deposits, negative for withdrawals) + - Status (Completed, Pending, Failed) + - Payment method (Direct deposit, Early pay, etc.) +3. User can apply filters: + - Time period (Day, Week, Month) + - Transaction type (All, Deposits, Withdrawals) +4. User can access detailed information for any transaction + +**Information Required:** +- Optional: Period filter selection +- Optional: Transaction type filter +- Transaction selection for details + +**Information Provided to User:** +- Filtered transaction inventory +- Detailed information for individual transactions + +**Edge Cases:** +- No transaction history: Message "No payment history yet" +- Failed transaction: Display with error indicator and explanation +- Large history: Progressive loading or pagination mechanism + +--- + +#### Story 3: Request Early Payment +**As a** worker +**I want to** request early access to my earned but not yet paid balance +**So that** I can access funds immediately when needed + +**Task Flow:** +1. User accesses early payment capability +2. System presents early payment option displaying: + - Available balance for early access (e.g., $340.00) + - Fee information (if applicable) + - Processing timeframe (e.g., "Instant" or "Within 1 hour") +3. User specifies amount to request: + - Amount input (currency) + - Cannot exceed available balance +4. User selects payment destination: + - Bank account (if registered) + - Debit card (if supported) +5. User reviews transaction summary: + - Requested amount + - Processing fee (if any) + - Net amount to receive + - Destination account (masked last 4 digits) +6. User confirms early payment request +7. System processes request +8. Success confirmation: "Early pay request submitted. Funds arriving soon!" +9. Transaction appears in payment history with "Pending" status + +**Information Required:** +- Amount to request (currency value) +- Payment destination selection +- Transaction confirmation + +**Information Provided to User:** +- Available balance for early access +- Fee calculation and disclosure +- Success confirmation +- Updated balance reflecting request +- New transaction in history + +**Edge Cases:** +- **Insufficient balance**: Error "Not enough earned balance for early pay" +- **No registered account**: Prompt "Add a bank account to use early pay" with profile navigation +- **Minimum amount requirement**: Error "Minimum early pay amount is $20" +- **Daily limit reached**: Error "You've reached your daily early pay limit. Try again tomorrow." +- **Fee disclosure**: Clear presentation of all fees before confirmation +- **Network issues**: Queue request for submission when connected + +--- + +## Staff: Profile + +### Purpose +Central hub for worker's personal information, profile completion tracking, reliability score, and navigation to profile sections (onboarding, compliance, finances, support). + +### User Stories + +#### Story 1: View Profile Overview +**As a** worker +**I want to** access my profile information and completion status +**So that** I understand requirements and how clients evaluate my reliability + +**Task Flow:** +1. User accesses profile area +2. System presents profile overview displaying: + - **Profile Header Information**: + - Profile picture + - Full name + - Reliability score (0-5 stars or percentage) + - **Reliability Statistics**: + - Total shifts completed (count) + - On-time arrival percentage + - Cancellation count + - **Profile Completion Status** (4 categories): + - Onboarding (Personal info, experience, preferences) + - Compliance (Tax forms, documents, certificates) + - Finances (Bank account, payment info) + - Support (FAQs, privacy settings) + - Each section displaying: + - Section name + - Completion percentage (e.g., "75% complete") + - Outstanding items (e.g., "2 items remaining") + - Access to continue completion +3. User reviews overview information +4. User can access any section for completion + +**Information Required:** +- None (view-only access to profile data) + +**Information Provided to User:** +- Complete profile information display +- Reliability score and detailed statistics +- Completion status for all sections +- Navigation to all profile sections + +**Edge Cases:** +- Profile 100% complete: Success indicator "Your profile is complete!" +- Low reliability score: Tips for improvement displayed +- Critical items missing: Alert "Complete [Section] to unlock full access" +- First-time view: Guidance on completing essential sections first + +--- + +#### Story 2: Navigate to Profile Sections +**As a** worker +**I want to** easily access different parts of my profile to complete or update information +**So that** I can maintain an accurate and complete profile + +**Task Flow:** +1. Worker reviews profile overview +2. Worker selects a profile section (Onboarding, Compliance, Finances, or Support) +3. System loads that section's data and features +4. Worker completes tasks within that section (see Profile Sections stories) +5. Worker returns to profile overview +6. Completion percentage recalculates to reflect changes + +**Information Required:** +- Section selection (Onboarding, Compliance, Finances, or Support) + +**Information Provided to User:** +- Selected section data and available actions +- Updated completion status after returning + +--- + +#### Story 3: View Reliability Score Details +**As a** worker +**I want to** understand how my reliability score is calculated +**So that** I know how to improve it and understand how clients evaluate me + +**Task Flow:** +1. Worker views reliability score on profile +2. Worker requests detailed score information +3. System provides score breakdown showing: + - Score breakdown (factors: on-time arrivals, completions, cancellations, client ratings) + - How each factor impacts score + - Tips for improvement +4. Worker reviews information +5. Worker dismisses details and returns to profile + +**Information Required:** +- Request for reliability score details + +**Information Provided to User:** +- Score breakdown by factor (on-time arrivals, completions, cancellations, client ratings) +- Factor impact explanations +- Improvement suggestions + +--- + +#### Story 4: Sign Out from Profile +**As a** worker +**I want to** sign out of my account +**So that** my information is secure when I'm not using the app + +**Task Flow:** +1. Worker navigates to sign out option in profile +2. Worker initiates sign out action +3. System requests sign out confirmation: "Are you sure you want to sign out?" +4. Worker confirms sign out +5. System terminates user session +6. System returns worker to authentication state + +**Information Required:** +- Sign out initiation +- Confirmation of sign out intent + +**Information Provided to User:** +- Sign out confirmation request +- Session termination confirmation +- Return to authentication state + +**Edge Cases:** +- Cancel confirmation: Session remains active, worker returns to profile + +--- + +## Staff: Profile Sections + +### Purpose +Detailed sub-features for completing different aspects of a worker's profile. Organized into 4 categories: Onboarding, Compliance, Finances, and Support. + +### Categories +1. **Onboarding** - Personal info, experience, emergency contacts, attire +2. **Compliance** - Tax forms, identity documents, certificates +3. **Finances** - Bank account setup, timecard management +4. **Support** - FAQs, privacy & security settings + +--- + +### Onboarding Section + +#### Story 1: Complete Personal Information +**As a** worker +**I want to** provide my personal details +**So that** clients can identify me and I can receive important communications + +**Task Flow:** +1. Worker accesses Onboarding → Profile Info section +2. System presents required personal information fields: + - Full name (may be pre-filled from signup) + - Date of birth + - Email address + - Secondary contact information + - Profile photo + - Language preference + - Preferred work locations +3. Worker provides or updates information +4. Worker supplies profile photo: + - Worker provides photo via camera capture or existing photo + - Worker adjusts/crops photo if needed + - Worker confirms photo selection +5. Worker submits information +6. System validates data and persists changes +7. System confirms: "Profile information updated" + +**Information Required:** +- Full name +- Date of birth +- Email address +- Secondary contact information +- Profile photo (image file) +- Language preference +- Preferred work locations + +**Information Provided to User:** +- Validation feedback for each field +- Success confirmation +- Updated profile data + +**Edge Cases:** +- Invalid email: Validation error message +- Age under 18: May require additional verification +- Photo too large: Compression applied or size error message + +--- + +#### Story 2: Document Work Experience +**As a** worker +**I want to** list my work history and skills +**So that** clients see my qualifications and I'm matched to appropriate jobs + +**Task Flow:** +1. Worker accesses Onboarding → Experience section +2. System displays existing experience entries (if any) +3. Worker initiates adding new experience entry +4. System requests experience details: + - Job title/role + - Years of experience + - Skills (Server, Cook, Driver, etc.) + - References (optional) +5. Worker provides experience information +6. Worker submits entry +7. System adds experience to worker's profile +8. Worker can add multiple entries +9. Worker can modify or remove entries + +**Information Required:** +- Job title/role +- Years of experience (numeric) +- Skills (multi-select: Server, Cook, Driver, etc.) +- References (optional) + +**Information Provided to User:** +- List of all experience entries +- Success confirmation for each operation + +**Edge Cases:** +- No experience: Worker can skip or indicate "Entry Level" +- Maximum entries: System may limit to 5-10 entries + +--- + +#### Story 3: Add Emergency Contact +**As a** worker +**I want to** provide emergency contact information +**So that** someone can be reached if something happens while I'm working + +**Task Flow:** +1. Worker accesses Onboarding → Emergency Contact section +2. System displays existing contacts (if any) +3. Worker initiates adding new contact +4. System requests contact details: + - Full name + - Relationship (Spouse, Parent, Sibling, Friend) + - Phone number +5. Worker provides contact information +6. Worker submits contact +7. System adds contact to worker's profile +8. Worker can add multiple contacts (primary, secondary) + +**Information Required:** +- Contact full name +- Relationship type (Spouse, Parent, Sibling, Friend) +- Phone number + +**Information Provided to User:** +- List of all emergency contacts +- Success confirmation + +**Edge Cases:** +- Required for profile completion +- Workers can designate primary contact + +--- + +#### Story 4: Upload Attire Photo +**As a** worker +**I want to** upload photos showing my work attire +**So that** clients can verify I meet dress code requirements + +**Task Flow:** +1. Worker accesses Onboarding → Attire section +2. System provides instructions: "Take photos of yourself in appropriate work attire" +3. Worker initiates photo submission +4. Worker provides photo via camera capture or existing photo +5. System displays photo preview +6. Worker confirms photo or provides alternative +7. System stores photo in worker's profile +8. Worker can submit multiple photos (front view, full body, etc.) + +**Information Required:** +- Attire photo (image file from camera or existing photo) +- Photo confirmation + +**Information Provided to User:** +- Uploaded photos display +- Success confirmation +- Photo requirements (e.g., "Full body, professional attire") + +**Edge Cases:** +- Photo requirements stated (e.g., "Full body, professional attire") +- Photos may require admin approval + +--- + +### Compliance Section + +#### Story 5: Upload Tax Forms +**As a** worker +**I want to** upload required tax documentation +**So that** I'm legally compliant and can receive payment + +```mermaid +graph TD + A[Start: Navigate to Compliance - Tax Forms] --> B[View Required Forms List] + B --> C{Forms Uploaded?} + C -->|No| D[See Required Forms
W-4, W-9, State Tax] + C -->|Yes| E[See Uploaded Status
Green Checkmarks] + D --> F[Tap Upload Form Button] + F --> G{Choose Upload Method} + G -->|Camera| H[Open Camera
Capture Document] + G -->|Gallery| I[Open Gallery
Select Existing Photo] + G -->|File| J[Open File Picker
Select PDF] + H --> K[Preview Captured Image] + I --> K + J --> K + K --> L{Image Clear?} + L -->|No| M[Retake or Choose Different] + M --> G + L -->|Yes| N[Confirm Upload] + N --> O[System Processes
OCR/Validation] + O --> P{Valid Document?} + P -->|No| Q[Show Error
Please upload correct form] + Q --> F + P -->|Yes| R[Success: Form Uploaded] + R --> S[Status Changes to Pending Review] + S --> T[Admin Reviews if Required] + T --> U{Approved?} + U -->|Yes| V[Status: Verified Green Check] + U -->|No| W[Status: Rejected - Reason Shown] + W --> F +``` + +**Task Flow:** +1. Worker accesses Compliance → Tax Forms section +2. System displays list of required forms: + - W-4 (Federal withholding) + - W-9 (Tax identification) + - State tax forms (if applicable) +3. System shows status for each form: + - Not uploaded + - Pending review + - Approved +4. Worker selects form to upload and provides document: + - Via camera capture + - Via existing photo + - Via PDF file selection +5. Worker captures or selects document +6. System displays document preview +7. Worker confirms submission +8. System performs basic validation (document type, clarity) +9. System confirms: "Tax form uploaded. Pending review." +10. Status changes to "Pending Review" +11. Admin reviews and approves/rejects +12. Worker receives approval status update + +**Information Required:** +- Tax form document (image or PDF) +- Document confirmation + +**Information Provided to User:** +- Upload progress +- Success confirmation +- Form status (Not uploaded, Pending, Approved, Rejected) +- Approval/rejection notifications + +**Edge Cases:** +- **Blurry photo**: System may reject or warn "Document not clear. Please retake." +- **Wrong form**: Validation error "This doesn't appear to be a W-4 form" +- **Rejected by admin**: User receives notification with reason and option to re-upload +- **Signature required**: Form may require digital signature before upload +- **Expiration**: Some forms expire and require re-upload annually + +--- + +#### Story 6: Upload Identity Documents +**As a** worker +**I want to** verify my identity with required documents +**So that** I can meet compliance requirements and be eligible to work + +**Task Flow:** +1. Worker accesses Compliance → Documents section +2. System displays required documents: + - Driver's license or state ID (both sides) + - Social Security Number verification + - Address verification (utility bill, lease, etc.) +3. Worker uploads each document (same process as tax forms) +4. System performs verification and routes to admin for review +5. Status updates to Approved when complete + +**Information Required:** +- ID document photos (front and back) +- Social Security Number (secure entry or document) +- Address proof document + +**Information Provided to User:** +- Upload confirmations for each document +- Verification status +- Approval notifications + +**Edge Cases:** +- SSN must be securely transmitted and encrypted +- ID expiration date: System tracks and notifies before expiry +- Address verification may require recent document (within 60 days) + +--- + +#### Story 7: Upload Professional Certificates +**As a** worker +**I want to** upload professional licenses and certifications +**So that** I can qualify for specialized shifts requiring credentials + +**Task Flow:** +1. Worker accesses Compliance → Certificates section +2. System displays optional/required certificates based on roles: + - Food Handler's Permit + - Bartending License + - Forklift Certification + - CPR/First Aid + - Background check status +3. Worker uploads applicable certificates +4. Worker provides expiration date for each certificate +5. System tracks expiration and sends renewal reminders +6. Admin reviews and approves certificates +7. System updates worker's eligible roles based on approved certificates + +**Information Required:** +- Certificate documents (photos or PDFs) +- Certificate number (optional) +- Expiration date + +**Information Provided to User:** +- List of certificates with expiration dates +- Renewal reminders (notifications) +- Newly unlocked roles + +**Edge Cases:** +- Expired certificate: Warning displayed, worker cannot accept related shifts +- Background check status: May be handled separately +- Temporary certificates: Short-term expiration dates supported + +--- + +### Finances Section + +#### Story 8: Set Up Bank Account +**As a** worker +**I want to** add my bank account information +**So that** I can receive direct deposit payments + +**Task Flow:** +1. Worker accesses Finances → Bank Account section +2. If no account on file, system presents option to add bank account +3. Worker initiates bank account setup +4. System requests account details: + - Bank name + - Account holder name + - Account number + - Routing number + - Account type (Checking or Savings) +5. Worker provides banking information +6. Worker submits details +7. System may verify account (micro-deposits or instant verification) +8. System confirms: "Bank account added" +9. System displays masked account information (last 4 digits only) + +**Information Required:** +- Bank name +- Account holder name +- Account number (secure) +- Routing number (9 digits, secure) +- Account type (Checking or Savings) + +**Information Provided to User:** +- Masked account display (e.g., "••••1234") +- Account verification status + +**Edge Cases:** +- Invalid routing number: Validation error +- Verification failed: Worker must confirm account via micro-deposits +- Multiple accounts: Workers can add backup account +- Edit or remove account: Modification and removal available + +--- + +#### Story 9: View and Dispute Timecard +**As a** worker +**I want to** view my recorded hours and dispute any errors +**So that** I'm paid correctly for time worked + +**Task Flow:** +1. Worker accesses Finances → Time Card section +2. System displays list of recent shifts with recorded hours: + - Shift date + - Check-in time + - Check-out time + - Break duration + - Total hours + - Pay amount +3. Worker selects a shift to view details +4. If hours are incorrect, worker initiates dispute +5. System requests dispute information: + - What's wrong? (multiple options or free text) + - Correct hours (manual entry) + - Notes/explanation +6. Worker submits dispute +7. System notifies client/manager +8. System tracks dispute status (Submitted, Under Review, Resolved) + +**Information Required:** +- Shift selection +- Dispute reason +- Corrected hours (numeric) +- Explanation + +**Information Provided to User:** +- Timecard details for all shifts +- Dispute submission confirmation +- Dispute status updates + +**Edge Cases:** +- Adjustment approved: Payment corrected +- Adjustment denied: Reason provided, worker can escalate +- Multiple disputes: May flag for review + +--- + +### Support Section + +#### Story 10: Access FAQs +**As a** worker +**I want to** find answers to common questions +**So that** I can resolve issues without contacting support + +**Task Flow:** +1. Worker accesses Support → FAQs section +2. System displays FAQ categories: + - Getting Started + - Shifts & Scheduling + - Payments + - Technical Issues +3. Worker selects a category +4. System displays list of frequently asked questions for that category +5. Worker selects a question +6. System displays detailed answer +7. Worker can search FAQs using text query +8. If issue not resolved, worker can contact support + +**Information Required:** +- Category selection +- Question selection +- Search query (optional) + +**Information Provided to User:** +- FAQ categories list +- Questions and answers for selected category +- Search results matching query +- Contact support option + +**Edge Cases:** +- No results for search: System shows "No matching FAQs" with Contact Support option +- Links in answers: May reference relevant sections or features + +--- + +#### Story 11: Manage Privacy & Security +**As a** worker +**I want to** control my privacy settings and account security +**So that** my personal information is protected + +**Task Flow:** +1. Worker accesses Support → Privacy & Security section +2. System presents security and privacy options: + - **Change Password**: + - Current password verification + - New password entry (with strength indicator) + - New password confirmation + - **Two-Factor Authentication**: + - Enable/disable 2FA + - Setup instructions if enabling + - **Privacy Settings**: + - Profile visibility controls + - Communication preferences + - **Data Access**: + - Download personal data (export to file) + - Delete account (requires confirmation) +3. Worker makes desired changes +4. Worker saves changes +5. System provides confirmation for each change + +**Information Required:** +- Current password (for password change) +- New password (secure) +- Password confirmation +- 2FA preference (enable/disable) +- Privacy preferences +- Data export request +- Account deletion confirmation + +**Information Provided to User:** +- Success confirmations for each change +- 2FA setup instructions +- Data export file (when ready) +- Account deletion confirmation + +**Edge Cases:** +- **Password requirements**: Minimum length, complexity rules enforced and displayed +- **2FA setup**: Requires phone or authenticator app +- **Delete account**: Multi-step confirmation with warnings about data loss +- **Data export**: May take time to prepare, delivered via email + +--- + +# Glossary + +### Client Application Terms + +- **Client**: A business owner or manager who uses the app to request staffing and manage operations. +- **Coverage**: The percentage or count of filled positions versus total positions needed for a given time period. 100% coverage means all shifts are filled. +- **Cost Center**: An accounting designation for tracking expenses by location or department within a business. +- **Hub**: A physical business location or venue where staff work (e.g., restaurant, warehouse, event venue). +- **Hub Manager**: A supervising employee at a hub location who oversees on-site operations. +- **Invoice**: A bill for services rendered, detailing worker hours, pay rates, and total costs for completed shifts. +- **NFC Tag**: Near Field Communication tag used for quick check-ins via phone tap at a physical location. +- **Order**: A staffing request created by a client specifying positions needed, dates, times, and location. + - **One-Time Order**: Single-day staffing request + - **Recurring Order**: Weekly pattern repeated over a limited period (max 29 days) + - **Permanent Order**: Ongoing staffing for certain days with no end date + - **Rapid Order**: Emergency/expedited staffing request +- **Position**: A role or job function within a shift (e.g., Server, Cook, Bartender, Warehouse Associate). +- **Vendor**: A staffing agency or organization providing workers (may be internal to KROW). + +### Staff Application Terms + +- **Auto-Match**: A feature that automatically notifies workers of shifts matching their preferences and availability. +- **Break**: A rest period during a shift, which is tracked separately and deducted from billable hours. +- **Check-In**: The action of confirming arrival at a shift location, typically with location verification. +- **Check-Out**: The action of ending a shift and recording total time worked. +- **Commute Mode**: A tracking feature showing the worker's real-time location and ETA to the venue. +- **Early Pay**: A service allowing workers to access earned wages before the regular pay date, often for a fee. +- **Geo-Fencing**: Location verification that ensures a worker is within a certain distance (500m) of the venue. +- **Marketplace**: The "Find Shifts" tab where workers browse and book available shifts. +- **OTP (One-Time Password)**: A temporary 6-digit code sent via SMS for authentication. +- **Profile Completion Gate**: A requirement that workers complete certain profile sections before they can book shifts. +- **Reliability Score**: A rating (0-5 or percentage) based on attendance, punctuality, completion rate, and client feedback. +- **Shift**: A scheduled work period with specific start/end times, location, and role. +- **Timecard**: A record of hours worked, including check-in, check-out, and break times. + +### Shared Terms + +- **Business Location**: See Hub above. +- **Role**: A job function or position type (e.g., Server, Cook, Driver). +- **Staff/Worker**: A person who accepts and performs shifts through the KROW platform. +- **Status**: The current state of an order, shift, invoice, or document (e.g., Pending, Approved, Completed, Cancelled). + +--- + +## Document End + +**Total Features Documented**: 18 (9 Client + 9 Staff) +**Total User Stories**: 60+ +**Total Mermaid Diagrams**: 4 + +This document provides a complete functional overview of the KROW Workforce Management Platform from a design perspective, enabling designers to understand user needs, flows, and interactions without needing to understand the underlying code implementation. diff --git a/makefiles/mobile.mk b/makefiles/mobile.mk index 4338cb7b..4b1e3dee 100644 --- a/makefiles/mobile.mk +++ b/makefiles/mobile.mk @@ -8,6 +8,9 @@ MOBILE_DIR := apps/mobile # Find your device ID with: flutter devices DEVICE ?= android +# Environment (dev, stage, prod) — defaults to dev +ENV ?= dev + # --- General --- mobile-install: install-melos dataconnect-generate-sdk @echo "--> Bootstrapping mobile workspace (Melos)..." @@ -40,35 +43,35 @@ mobile-hot-restart: # --- Client App --- mobile-client-dev-android: dataconnect-generate-sdk - @echo "--> Running client app on Android (device: $(DEVICE))..." - @cd $(MOBILE_DIR) && melos run start:client -- -d $(DEVICE) --dart-define-from-file=../../config.dev.json + @echo "--> Running client app on Android (device: $(DEVICE), env: $(ENV))..." + @cd $(MOBILE_DIR) && melos run start:client -- -d $(DEVICE) --flavor $(ENV) --dart-define-from-file=../../config.$(ENV).json mobile-client-build: dataconnect-generate-sdk @if [ -z "$(PLATFORM)" ]; then \ echo "ERROR: PLATFORM is required (e.g. make mobile-client-build PLATFORM=apk)"; exit 1; \ fi $(eval MODE ?= release) - @echo "--> Building client app for $(PLATFORM) in $(MODE) mode..." + @echo "--> Building client app for $(PLATFORM) in $(MODE) mode (env: $(ENV))..." @cd $(MOBILE_DIR) && \ melos exec --scope="core_localization" -- "dart run slang" && \ melos exec --scope="core_localization" -- "dart run build_runner build --delete-conflicting-outputs" && \ - melos exec --scope="krowwithus_client" -- "flutter build $(PLATFORM) --$(MODE) --dart-define-from-file=../../config.dev.json" + melos exec --scope="krowwithus_client" -- "flutter build $(PLATFORM) --$(MODE) --flavor $(ENV) --dart-define-from-file=../../config.$(ENV).json" # --- Staff App --- mobile-staff-dev-android: dataconnect-generate-sdk - @echo "--> Running staff app on Android (device: $(DEVICE))..." - @cd $(MOBILE_DIR) && melos run start:staff -- -d $(DEVICE) --dart-define-from-file=../../config.dev.json + @echo "--> Running staff app on Android (device: $(DEVICE), env: $(ENV))..." + @cd $(MOBILE_DIR) && melos run start:staff -- -d $(DEVICE) --flavor $(ENV) --dart-define-from-file=../../config.$(ENV).json mobile-staff-build: dataconnect-generate-sdk @if [ -z "$(PLATFORM)" ]; then \ echo "ERROR: PLATFORM is required (e.g. make mobile-staff-build PLATFORM=apk)"; exit 1; \ fi $(eval MODE ?= release) - @echo "--> Building staff app for $(PLATFORM) in $(MODE) mode..." + @echo "--> Building staff app for $(PLATFORM) in $(MODE) mode (env: $(ENV))..." @cd $(MOBILE_DIR) && \ melos exec --scope="core_localization" -- "dart run slang" && \ melos exec --scope="core_localization" -- "dart run build_runner build --delete-conflicting-outputs" && \ - melos exec --scope="krowwithus_staff" -- "flutter build $(PLATFORM) --$(MODE) --dart-define-from-file=../../config.dev.json" + melos exec --scope="krowwithus_staff" -- "flutter build $(PLATFORM) --$(MODE) --flavor $(ENV) --dart-define-from-file=../../config.$(ENV).json" # --- E2E (Maestro) --- # Set env before running: TEST_CLIENT_EMAIL, TEST_CLIENT_PASSWORD, TEST_CLIENT_COMPANY, TEST_STAFF_PHONE, TEST_STAFF_OTP, TEST_STAFF_SIGNUP_PHONE From c4dbdb5dcbc29666d38ffa3c91c1e8dd08793fee Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Mon, 9 Mar 2026 16:34:42 -0400 Subject: [PATCH 3/7] feat: implement flavor-specific key properties for staging and production environments --- .../apps/client/android/app/build.gradle.kts | 16 ++++++++++++++-- .../{key.properties => key.dev.properties} | 0 .../apps/client/android/key.prod.properties | 9 +++++++++ .../apps/client/android/key.stage.properties | 9 +++++++++ .../apps/staff/android/app/build.gradle.kts | 16 ++++++++++++++-- .../{key.properties => key.dev.properties} | 0 .../apps/staff/android/key.prod.properties | 9 +++++++++ .../apps/staff/android/key.stage.properties | 9 +++++++++ 8 files changed, 64 insertions(+), 4 deletions(-) rename apps/mobile/apps/client/android/{key.properties => key.dev.properties} (100%) create mode 100644 apps/mobile/apps/client/android/key.prod.properties create mode 100644 apps/mobile/apps/client/android/key.stage.properties rename apps/mobile/apps/staff/android/{key.properties => key.dev.properties} (100%) create mode 100644 apps/mobile/apps/staff/android/key.prod.properties create mode 100644 apps/mobile/apps/staff/android/key.stage.properties diff --git a/apps/mobile/apps/client/android/app/build.gradle.kts b/apps/mobile/apps/client/android/app/build.gradle.kts index 15f3f341..26417d23 100644 --- a/apps/mobile/apps/client/android/app/build.gradle.kts +++ b/apps/mobile/apps/client/android/app/build.gradle.kts @@ -21,8 +21,20 @@ dartDefinesString.split(",").forEach { } } +// Load flavor-specific key properties: key.dev.properties, key.stage.properties, key.prod.properties +// The active flavor is resolved from the Gradle task name (e.g. assembleDevRelease -> dev) +fun resolveFlavorFromTask(): String { + val taskNames = gradle.startParameter.taskNames.joinToString(" ").lowercase() + return when { + taskNames.contains("prod") -> "prod" + taskNames.contains("stage") -> "stage" + else -> "dev" + } +} + +val activeFlavorForSigning = resolveFlavorFromTask() val keystoreProperties = Properties().apply { - val propertiesFile = rootProject.file("key.properties") + val propertiesFile = rootProject.file("key.${activeFlavorForSigning}.properties") if (propertiesFile.exists()) { load(propertiesFile.inputStream()) } @@ -80,7 +92,7 @@ android { keyAlias = System.getenv()["CM_KEY_ALIAS"] keyPassword = System.getenv()["CM_KEY_PASSWORD"] } else { - // Local development environment + // Local development environment — loads from key..properties keyAlias = keystoreProperties["keyAlias"] as String? keyPassword = keystoreProperties["keyPassword"] as String? storeFile = keystoreProperties["storeFile"]?.let { file(it) } diff --git a/apps/mobile/apps/client/android/key.properties b/apps/mobile/apps/client/android/key.dev.properties similarity index 100% rename from apps/mobile/apps/client/android/key.properties rename to apps/mobile/apps/client/android/key.dev.properties diff --git a/apps/mobile/apps/client/android/key.prod.properties b/apps/mobile/apps/client/android/key.prod.properties new file mode 100644 index 00000000..5612e20a --- /dev/null +++ b/apps/mobile/apps/client/android/key.prod.properties @@ -0,0 +1,9 @@ +storePassword=krowwithus +keyPassword=krowwithus +keyAlias=krow_client_prod +storeFile=krow_with_us_client_prod.jks + +### +### Client Prod +### SHA1: B2:80:46:90:7F:E5:9E:86:62:7B:06:90:AC:C0:20:02:73:5B:20:5C +### SHA256: D8:3C:B0:07:B5:95:3C:82:2F:2C:A9:F6:8D:6F:77:B9:31:9D:BE:E9:74:4A:59:D9:7F:DC:EB:E2:C6:26:AB:27 diff --git a/apps/mobile/apps/client/android/key.stage.properties b/apps/mobile/apps/client/android/key.stage.properties new file mode 100644 index 00000000..0ac47cb7 --- /dev/null +++ b/apps/mobile/apps/client/android/key.stage.properties @@ -0,0 +1,9 @@ +storePassword=krowwithus +keyPassword=krowwithus +keyAlias=krow_client_stage +storeFile=krow_with_us_client_stage.jks + +### +### Client Stage +### SHA1: 89:9F:12:9E:A5:18:AC:1D:75:73:29:0B:F2:C2:E6:EB:38:B0:F0:A0 +### SHA256: 80:13:10:CB:88:A8:8D:E9:F6:9E:D6:55:53:9C:BE:2D:D4:9C:7A:26:56:A3:E9:70:7C:F5:9A:A7:20:1A:6D:FE diff --git a/apps/mobile/apps/staff/android/app/build.gradle.kts b/apps/mobile/apps/staff/android/app/build.gradle.kts index 4111f66b..d3f19e5f 100644 --- a/apps/mobile/apps/staff/android/app/build.gradle.kts +++ b/apps/mobile/apps/staff/android/app/build.gradle.kts @@ -21,8 +21,20 @@ dartDefinesString.split(",").forEach { } } +// Load flavor-specific key properties: key.dev.properties, key.stage.properties, key.prod.properties +// The active flavor is resolved from the Gradle task name (e.g. assembleDevRelease -> dev) +fun resolveFlavorFromTask(): String { + val taskNames = gradle.startParameter.taskNames.joinToString(" ").lowercase() + return when { + taskNames.contains("prod") -> "prod" + taskNames.contains("stage") -> "stage" + else -> "dev" + } +} + +val activeFlavorForSigning = resolveFlavorFromTask() val keystoreProperties = Properties().apply { - val propertiesFile = rootProject.file("key.properties") + val propertiesFile = rootProject.file("key.${activeFlavorForSigning}.properties") if (propertiesFile.exists()) { load(propertiesFile.inputStream()) } @@ -81,7 +93,7 @@ android { keyAlias = System.getenv()["CM_KEY_ALIAS"] keyPassword = System.getenv()["CM_KEY_PASSWORD"] } else { - // Local development environment + // Local development environment — loads from key..properties keyAlias = keystoreProperties["keyAlias"] as String? keyPassword = keystoreProperties["keyPassword"] as String? storeFile = keystoreProperties["storeFile"]?.let { file(it) } diff --git a/apps/mobile/apps/staff/android/key.properties b/apps/mobile/apps/staff/android/key.dev.properties similarity index 100% rename from apps/mobile/apps/staff/android/key.properties rename to apps/mobile/apps/staff/android/key.dev.properties diff --git a/apps/mobile/apps/staff/android/key.prod.properties b/apps/mobile/apps/staff/android/key.prod.properties new file mode 100644 index 00000000..272755ca --- /dev/null +++ b/apps/mobile/apps/staff/android/key.prod.properties @@ -0,0 +1,9 @@ +storePassword=krowwithus +keyPassword=krowwithus +keyAlias=krow_staff_prod +storeFile=krow_with_us_staff_prod.jks + +### +### Staff Prod +### SHA1: B3:9A:AE:EC:8D:A2:C8:88:5F:FA:AC:9B:31:0A:AC:F3:D6:7D:82:83 +### SHA256: 0C:F3:5F:B5:C5:DA:E3:94:E1:FB:9E:D9:84:4F:2D:4A:E5:1B:48:FB:33:A1:DD:F3:43:41:22:32:A4:9A:25:E8 diff --git a/apps/mobile/apps/staff/android/key.stage.properties b/apps/mobile/apps/staff/android/key.stage.properties new file mode 100644 index 00000000..0fef76d1 --- /dev/null +++ b/apps/mobile/apps/staff/android/key.stage.properties @@ -0,0 +1,9 @@ +storePassword=krowwithus +keyPassword=krowwithus +keyAlias=krow_staff_stage +storeFile=krow_with_us_staff_stage.jks + +### +### Staff Stage +### SHA1: E8:C4:B8:F5:5E:19:04:31:D6:E5:16:76:47:62:D0:5B:2F:F3:CE:05 +### SHA256: 25:55:68:E6:77:03:33:E1:D0:4E:F4:75:6E:6B:3D:3D:A2:DB:9B:2B:5E:AD:FF:CD:22:64:CE:3F:E8:AF:60:50 From 093cc4e0a416d2792f2cfd49df04c7198b54de21 Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Mon, 9 Mar 2026 17:00:17 -0400 Subject: [PATCH 4/7] feat: enhance workflow names with emojis for better clarity and visual appeal --- codemagic.yaml | 50 +++++++++++++++++++++++++------------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/codemagic.yaml b/codemagic.yaml index 2101a658..f391b001 100644 --- a/codemagic.yaml +++ b/codemagic.yaml @@ -4,7 +4,7 @@ # Reusable script for building the Flutter app client-app-android-apk-build-script: &client-app-android-apk-build-script - name: Build Client App APK (Android) + name: 👷 🤖 Build Client App APK (Android) script: | dart pub global activate melos export PATH="$PATH":"$HOME/.pub-cache/bin" @@ -12,7 +12,7 @@ client-app-android-apk-build-script: &client-app-android-apk-build-script make mobile-client-build PLATFORM=apk MODE=release ENV=$ENV client-app-ios-build-script: &client-app-ios-build-script - name: Build Client App (iOS) + name: 👷 🍎 Build Client App (iOS) script: | dart pub global activate melos export PATH="$PATH":"$HOME/.pub-cache/bin" @@ -20,7 +20,7 @@ client-app-ios-build-script: &client-app-ios-build-script make mobile-client-build PLATFORM=ios MODE=release ENV=$ENV staff-app-android-apk-build-script: &staff-app-android-apk-build-script - name: Build Staff App APK (Android) + name: 👷 🤖 Build Staff App APK (Android) script: | dart pub global activate melos export PATH="$PATH":"$HOME/.pub-cache/bin" @@ -28,7 +28,7 @@ staff-app-android-apk-build-script: &staff-app-android-apk-build-script make mobile-staff-build PLATFORM=apk MODE=release ENV=$ENV staff-app-ios-build-script: &staff-app-ios-build-script - name: Build Staff App (iOS) + name: 👷 🍎 Build Staff App (iOS) script: | dart pub global activate melos export PATH="$PATH":"$HOME/.pub-cache/bin" @@ -37,7 +37,7 @@ staff-app-ios-build-script: &staff-app-ios-build-script # Reusable script for distributing Android to Firebase distribute-android-script: &distribute-android-script - name: Distribute Android to Firebase App Distribution + name: 🚀 🤖 Distribute Android to Firebase App Distribution script: | # Distribute Android APK # Note: With flavors the APK is in a flavor-specific subdirectory @@ -56,7 +56,7 @@ distribute-android-script: &distribute-android-script # Reusable script for distributing iOS to Firebase distribute-ios-script: &distribute-ios-script - name: Distribute iOS to Firebase App Distribution + name: 🚀 🍎 Distribute iOS to Firebase App Distribution script: | # Distribute iOS IPA_PATH=$(find apps/mobile/apps -name "*.ipa" | head -n 1) @@ -74,7 +74,7 @@ distribute-ios-script: &distribute-ios-script # Reusable script for web quality checks web-quality-script: &web-quality-script - name: Web Quality Checks + name: 🌐 ✅ Web Quality Checks script: | npm install -g pnpm cd apps/web @@ -85,7 +85,7 @@ web-quality-script: &web-quality-script # Reusable script for mobile quality checks mobile-quality-script: &mobile-quality-script - name: Mobile Quality Checks + name: 📱 ✅ Mobile Quality Checks script: | dart pub global activate melos export PATH="$PATH":"$HOME/.pub-cache/bin" @@ -98,7 +98,7 @@ workflows: # Quality workflow (Web + Mobile) # ================================================================================= quality-gates-dev: - name: Quality Gates (Dev) + name: 🛡️ Quality Gates (Dev) working_directory: . instance_type: mac_mini_m2 max_build_duration: 60 @@ -163,11 +163,11 @@ workflows: - $FCI_BUILD_DIR/apps/mobile/apps/staff/.dart_tool # ================================================================================= - # Client App Workflows - Android + # 💼 Client App Workflows - Android # ================================================================================= client-app-dev-android: <<: *client-app-base - name: Client App Dev (Android App Distribution) + name: 🚚 🤖 Client App Dev (Android → Firebase App Distribution) environment: flutter: stable xcode: latest @@ -184,7 +184,7 @@ workflows: client-app-staging-android: <<: *client-app-base - name: Client App Staging (Android App Distribution) + name: 🚚 🤖 Client App Staging (Android → Firebase App Distribution) environment: flutter: stable xcode: latest @@ -201,7 +201,7 @@ workflows: client-app-prod-android: <<: *client-app-base - name: Client App Prod (Android App Distribution) + name: 🚚 🤖 Client App Prod (Android → Firebase App Distribution) environment: groups: - client_app_prod_credentials @@ -214,11 +214,11 @@ workflows: - *distribute-android-script # ================================================================================= - # Client App Workflows - iOS + # 💼 Client App Workflows - iOS # ================================================================================= client-app-dev-ios: <<: *client-app-base - name: Client App Dev (iOS App Distribution) + name: 🚚 🍎 Client App Dev (iOS → Firebase App Distribution) environment: groups: - client_app_dev_credentials @@ -230,7 +230,7 @@ workflows: client-app-staging-ios: <<: *client-app-base - name: Client App Staging (iOS App Distribution) + name: 🚚 🍎 Client App Staging (iOS → Firebase App Distribution) environment: groups: - client_app_staging_credentials @@ -242,7 +242,7 @@ workflows: client-app-prod-ios: <<: *client-app-base - name: Client App Prod (iOS App Distribution) + name: 🚚 🍎 Client App Prod (iOS → Firebase App Distribution) environment: groups: - client_app_prod_credentials @@ -253,11 +253,11 @@ workflows: - *distribute-ios-script # ================================================================================= - # Staff App Workflows - Android + # 👨‍🍳 Staff App Workflows - Android # ================================================================================= staff-app-dev-android: <<: *staff-app-base - name: Staff App Dev (Android App Distribution) + name: 🚚 🤖 👨‍🍳 Staff App Dev (Android → Firebase App Distribution) environment: flutter: stable xcode: latest @@ -274,7 +274,7 @@ workflows: staff-app-staging-android: <<: *staff-app-base - name: Staff App Staging (Android App Distribution) + name: 🚚 🤖 👨‍🍳 Staff App Staging (Android → Firebase App Distribution) environment: flutter: stable xcode: latest @@ -291,7 +291,7 @@ workflows: staff-app-prod-android: <<: *staff-app-base - name: Staff App Prod (Android App Distribution) + name: 🚚 🤖 👨‍🍳 Staff App Prod (Android → Firebase App Distribution) environment: flutter: stable xcode: latest @@ -307,11 +307,11 @@ workflows: - *distribute-android-script # ================================================================================= - # Staff App Workflows - iOS + # 👨‍🍳 Staff App Workflows - iOS # ================================================================================= staff-app-dev-ios: <<: *staff-app-base - name: Staff App Dev (iOS App Distribution) + name: 🚚 🍎 👨‍🍳 Staff App Dev (iOS → Firebase App Distribution) environment: groups: - staff_app_dev_credentials @@ -323,7 +323,7 @@ workflows: staff-app-staging-ios: <<: *staff-app-base - name: Staff App Staging (iOS App Distribution) + name: 🚚 🍎 👨‍🍳 Staff App Staging (iOS → Firebase App Distribution) environment: groups: - staff_app_staging_credentials @@ -335,7 +335,7 @@ workflows: staff-app-prod-ios: <<: *staff-app-base - name: Staff App Prod (iOS App Distribution) + name: 🚚 🍎 👨‍🍳 Staff App Prod (iOS → Firebase App Distribution) environment: groups: - staff_app_prod_credentials From fe984624313ee9be6e43441002d64c5fd1a03d5f Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Mon, 9 Mar 2026 17:27:56 -0400 Subject: [PATCH 5/7] feat: update launch configurations and build scripts for staging and production environments --- .vscode/launch.json | 96 +++++++++++++++++++++++++++++++++++++++--- apps/mobile/melos.yaml | 39 ++++++++--------- codemagic.yaml | 8 ++-- makefiles/mobile.mk | 12 +++--- 4 files changed, 120 insertions(+), 35 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 437dd654..9205497b 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,41 +1,127 @@ { "version": "0.2.0", "configurations": [ + // ===================== Client App ===================== { - "name": "Client (Dev) - Android", + "name": "Client [DEV] - Android", "request": "launch", "type": "dart", "program": "apps/mobile/apps/client/lib/main.dart", "args": [ + "--flavor", "dev", "--dart-define-from-file=${workspaceFolder}/apps/mobile/config.dev.json" ] }, { - "name": "Client (Dev) - iOS", + "name": "Client [DEV] - iOS", "request": "launch", "type": "dart", "program": "apps/mobile/apps/client/lib/main.dart", "args": [ + "--flavor", "dev", "--dart-define-from-file=${workspaceFolder}/apps/mobile/config.dev.json" ] }, { - "name": "Staff (Dev) - Android", + "name": "Client [STG] - Android", + "request": "launch", + "type": "dart", + "program": "apps/mobile/apps/client/lib/main.dart", + "args": [ + "--flavor", "stage", + "--dart-define-from-file=${workspaceFolder}/apps/mobile/config.stage.json" + ] + }, + { + "name": "Client [STG] - iOS", + "request": "launch", + "type": "dart", + "program": "apps/mobile/apps/client/lib/main.dart", + "args": [ + "--flavor", "stage", + "--dart-define-from-file=${workspaceFolder}/apps/mobile/config.stage.json" + ] + }, + { + "name": "Client [PROD] - Android", + "request": "launch", + "type": "dart", + "program": "apps/mobile/apps/client/lib/main.dart", + "args": [ + "--flavor", "prod", + "--dart-define-from-file=${workspaceFolder}/apps/mobile/config.prod.json" + ] + }, + { + "name": "Client [PROD] - iOS", + "request": "launch", + "type": "dart", + "program": "apps/mobile/apps/client/lib/main.dart", + "args": [ + "--flavor", "prod", + "--dart-define-from-file=${workspaceFolder}/apps/mobile/config.prod.json" + ] + }, + // ===================== Staff App ===================== + { + "name": "Staff [DEV] - Android", "request": "launch", "type": "dart", "program": "apps/mobile/apps/staff/lib/main.dart", "args": [ + "--flavor", "dev", "--dart-define-from-file=${workspaceFolder}/apps/mobile/config.dev.json" ] }, { - "name": "Staff (Dev) - iOS", + "name": "Staff [DEV] - iOS", "request": "launch", "type": "dart", "program": "apps/mobile/apps/staff/lib/main.dart", "args": [ + "--flavor", "dev", "--dart-define-from-file=${workspaceFolder}/apps/mobile/config.dev.json" ] + }, + { + "name": "Staff [STG] - Android", + "request": "launch", + "type": "dart", + "program": "apps/mobile/apps/staff/lib/main.dart", + "args": [ + "--flavor", "stage", + "--dart-define-from-file=${workspaceFolder}/apps/mobile/config.stage.json" + ] + }, + { + "name": "Staff [STG] - iOS", + "request": "launch", + "type": "dart", + "program": "apps/mobile/apps/staff/lib/main.dart", + "args": [ + "--flavor", "stage", + "--dart-define-from-file=${workspaceFolder}/apps/mobile/config.stage.json" + ] + }, + { + "name": "Staff [PROD] - Android", + "request": "launch", + "type": "dart", + "program": "apps/mobile/apps/staff/lib/main.dart", + "args": [ + "--flavor", "prod", + "--dart-define-from-file=${workspaceFolder}/apps/mobile/config.prod.json" + ] + }, + { + "name": "Staff [PROD] - iOS", + "request": "launch", + "type": "dart", + "program": "apps/mobile/apps/staff/lib/main.dart", + "args": [ + "--flavor", "prod", + "--dart-define-from-file=${workspaceFolder}/apps/mobile/config.prod.json" + ] } ] -} \ No newline at end of file +} diff --git a/apps/mobile/melos.yaml b/apps/mobile/melos.yaml index ae2cce43..4320c631 100644 --- a/apps/mobile/melos.yaml +++ b/apps/mobile/melos.yaml @@ -14,15 +14,14 @@ scripts: echo " 🚀 KROW WORKFORCE CUSTOM COMMANDS 🚀" echo "============================================================" echo " BUILD COMMANDS:" - echo " - melos run build:client : Build Client App (APK)" - echo " - melos run build:staff : Build Staff App (APK)" + echo " - melos run build:client -- -- --flavor --dart-define-from-file=../../config..json" + echo " - melos run build:staff -- -- --flavor --dart-define-from-file=../../config..json" echo " - melos run build:design-system : Build Design System Viewer" echo "" echo " DEBUG/START COMMANDS:" - echo " - melos run start:client -- -d : Run Client App" - echo " - melos run start:staff -- -d : Run Staff App" + echo " - melos run start:client -- -d --flavor --dart-define-from-file=../../config..json" + echo " - melos run start:staff -- -d --flavor --dart-define-from-file=../../config..json" echo " - melos run start:design-system : Run DS Viewer" - echo " (e.g., melos run start:client -- -d chrome)" echo "" echo " CODE GENERATION:" echo " - melos run gen:l10n : Generate Slang l10n" @@ -49,32 +48,30 @@ scripts: packageFilters: dependsOn: build_runner + # Single-line scripts so that melos run arg forwarding works via -- + # Usage: melos run build:client -- apk --release --flavor dev --dart-define-from-file=../../config.dev.json build:client: - run: | - melos run gen:l10n --filter="core_localization" - melos run gen:build --filter="core_localization" - melos exec --scope="krowwithus_client" -- "flutter build apk" - description: "Build the Client app (Android APK by default)." + run: melos exec --scope="krowwithus_client" -- flutter build + description: "Build the Client app. Pass args via --: -- --flavor --dart-define-from-file=../../config..json" build:staff: - run: | - melos run gen:l10n --filter="core_localization" - melos run gen:build --filter="core_localization" - melos exec --scope="krowwithus_staff" -- "flutter build apk" - description: "Build the Staff app (Android APK by default)." + run: melos exec --scope="krowwithus_staff" -- flutter build + description: "Build the Staff app. Pass args via --: -- --flavor --dart-define-from-file=../../config..json" build:design-system-viewer: - run: melos exec --scope="design_system_viewer" -- "flutter build apk" + run: melos exec --scope="design_system_viewer" -- flutter build apk description: "Build the Design System Viewer app (Android APK by default)." + # Single-line scripts so that melos run arg forwarding works via -- + # Usage: melos run start:client -- -d android --flavor dev --dart-define-from-file=../../config.dev.json start:client: - run: melos exec --scope="krowwithus_client" -- "flutter run" - description: "Start the Client app. Pass platform using -- -d , e.g. -d chrome" + run: melos exec --scope="krowwithus_client" -- flutter run + description: "Start the Client app. Pass args via --: -d --flavor --dart-define-from-file=../../config..json" start:staff: - run: melos exec --scope="krowwithus_staff" -- "flutter run" - description: "Start the Staff app. Pass platform using -- -d , e.g. -d chrome" + run: melos exec --scope="krowwithus_staff" -- flutter run + description: "Start the Staff app. Pass args via --: -d --flavor --dart-define-from-file=../../config..json" start:design-system-viewer: - run: melos exec --scope="design_system_viewer" -- "flutter run" + run: melos exec --scope="design_system_viewer" -- flutter run description: "Start the Design System Viewer app. Pass platform using -- -d , e.g. -d chrome" diff --git a/codemagic.yaml b/codemagic.yaml index f391b001..1dd6fac4 100644 --- a/codemagic.yaml +++ b/codemagic.yaml @@ -40,10 +40,12 @@ distribute-android-script: &distribute-android-script name: 🚀 🤖 Distribute Android to Firebase App Distribution script: | # Distribute Android APK - # Note: With flavors the APK is in a flavor-specific subdirectory - APP_PATH=$(find apps/mobile/apps -name "app-${ENV}-release.apk" -o -name "app-release.apk" | head -n 1) + # With flavors the APK is at: build/app/outputs/apk//release/app--release.apk + APP_PATH=$(find apps/mobile/apps -name "app-${ENV}-release.apk" | head -n 1) if [ -z "$APP_PATH" ]; then - echo "No APK found!" + echo "❌ No APK found matching app-${ENV}-release.apk — was --flavor ${ENV} applied during build?" + echo "Listing all APKs found:" + find apps/mobile/apps -name "*.apk" -type f exit 1 fi echo "Found APK at: $APP_PATH" diff --git a/makefiles/mobile.mk b/makefiles/mobile.mk index 4b1e3dee..de6dbc0d 100644 --- a/makefiles/mobile.mk +++ b/makefiles/mobile.mk @@ -53,9 +53,9 @@ mobile-client-build: dataconnect-generate-sdk $(eval MODE ?= release) @echo "--> Building client app for $(PLATFORM) in $(MODE) mode (env: $(ENV))..." @cd $(MOBILE_DIR) && \ - melos exec --scope="core_localization" -- "dart run slang" && \ - melos exec --scope="core_localization" -- "dart run build_runner build --delete-conflicting-outputs" && \ - melos exec --scope="krowwithus_client" -- "flutter build $(PLATFORM) --$(MODE) --flavor $(ENV) --dart-define-from-file=../../config.$(ENV).json" + melos run gen:l10n && \ + melos run gen:build && \ + melos run build:client -- $(PLATFORM) --$(MODE) --flavor $(ENV) --dart-define-from-file=../../config.$(ENV).json # --- Staff App --- mobile-staff-dev-android: dataconnect-generate-sdk @@ -69,9 +69,9 @@ mobile-staff-build: dataconnect-generate-sdk $(eval MODE ?= release) @echo "--> Building staff app for $(PLATFORM) in $(MODE) mode (env: $(ENV))..." @cd $(MOBILE_DIR) && \ - melos exec --scope="core_localization" -- "dart run slang" && \ - melos exec --scope="core_localization" -- "dart run build_runner build --delete-conflicting-outputs" && \ - melos exec --scope="krowwithus_staff" -- "flutter build $(PLATFORM) --$(MODE) --flavor $(ENV) --dart-define-from-file=../../config.$(ENV).json" + melos run gen:l10n && \ + melos run gen:build && \ + melos run build:staff -- $(PLATFORM) --$(MODE) --flavor $(ENV) --dart-define-from-file=../../config.$(ENV).json # --- E2E (Maestro) --- # Set env before running: TEST_CLIENT_EMAIL, TEST_CLIENT_PASSWORD, TEST_CLIENT_COMPANY, TEST_STAFF_PHONE, TEST_STAFF_OTP, TEST_STAFF_SIGNUP_PHONE From 972951fd9619ec44794b36ae3c5fec80da186249 Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Mon, 9 Mar 2026 18:09:25 -0400 Subject: [PATCH 6/7] feat: implement conditional google-services processing and update Firebase configurations for staging and production environments --- .../apps/client/android/app/build.gradle.kts | 14 ++++++++ .../android/app/src/prod/google-services.json | 24 -------------- apps/mobile/apps/client/firebase.json | 32 ++++++++++++++++++- .../ios/config/prod/GoogleService-Info.plist | 30 ----------------- .../apps/staff/android/app/build.gradle.kts | 14 ++++++++ .../android/app/src/prod/google-services.json | 24 -------------- .../ios/config/dev/GoogleService-Info.plist | 8 ++--- .../ios/config/prod/GoogleService-Info.plist | 30 ----------------- .../ios/config/stage/GoogleService-Info.plist | 4 +-- makefiles/mobile.mk | 4 +-- 10 files changed, 67 insertions(+), 117 deletions(-) delete mode 100644 apps/mobile/apps/client/android/app/src/prod/google-services.json delete mode 100644 apps/mobile/apps/client/ios/config/prod/GoogleService-Info.plist delete mode 100644 apps/mobile/apps/staff/android/app/src/prod/google-services.json delete mode 100644 apps/mobile/apps/staff/ios/config/prod/GoogleService-Info.plist diff --git a/apps/mobile/apps/client/android/app/build.gradle.kts b/apps/mobile/apps/client/android/app/build.gradle.kts index 26417d23..cf4c5b37 100644 --- a/apps/mobile/apps/client/android/app/build.gradle.kts +++ b/apps/mobile/apps/client/android/app/build.gradle.kts @@ -108,6 +108,20 @@ android { } } +// Skip google-services processing for flavors whose google-services.json +// contains placeholder values (e.g. prod before the Firebase project exists). +// Once a real config is dropped in, the task automatically re-enables. +afterEvaluate { + tasks.matching { + it.name.startsWith("process") && it.name.endsWith("GoogleServices") + }.configureEach { + val taskFlavor = name.removePrefix("process").removeSuffix("GoogleServices") + .removeSuffix("Debug").removeSuffix("Release").lowercase() + val configFile = file("src/$taskFlavor/google-services.json") + enabled = configFile.exists() && configFile.readText().contains("\"mobilesdk_app_id\": \"1:") + } +} + flutter { source = "../.." } diff --git a/apps/mobile/apps/client/android/app/src/prod/google-services.json b/apps/mobile/apps/client/android/app/src/prod/google-services.json deleted file mode 100644 index 002c33ee..00000000 --- a/apps/mobile/apps/client/android/app/src/prod/google-services.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "project_info": { - "project_number": "", - "project_id": "krow-workforce-prod", - "storage_bucket": "krow-workforce-prod.firebasestorage.app" - }, - "client": [ - { - "client_info": { - "mobilesdk_app_id": "", - "android_client_info": { - "package_name": "prod.krowwithus.client" - } - }, - "oauth_client": [], - "api_key": [ - { - "current_key": "" - } - ] - } - ], - "configuration_version": "1" -} diff --git a/apps/mobile/apps/client/firebase.json b/apps/mobile/apps/client/firebase.json index 09f707ae..86449ce7 100644 --- a/apps/mobile/apps/client/firebase.json +++ b/apps/mobile/apps/client/firebase.json @@ -1 +1,31 @@ -{"flutter":{"platforms":{"android":{"default":{"projectId":"krow-workforce-dev","appId":"1:933560802882:android:da13569105659ead7757db","fileOutput":"android/app/google-services.json"}},"ios":{"default":{"projectId":"krow-workforce-dev","appId":"1:933560802882:ios:d2b6d743608e2a527757db","uploadDebugSymbols":false,"fileOutput":"ios/Runner/GoogleService-Info.plist"}},"dart":{"lib/firebase_options.dart":{"projectId":"krow-workforce-dev","configurations":{"android":"1:933560802882:android:da13569105659ead7757db","ios":"1:933560802882:ios:d2b6d743608e2a527757db","web":"1:933560802882:web:173a841992885bb27757db"}}}}}} \ No newline at end of file +{ + "flutter": { + "platforms": { + "android": { + "default": { + "projectId": "krow-workforce-dev", + "appId": "1:933560802882:android:da13569105659ead7757db", + "fileOutput": "android/app/google-services.json" + } + }, + "ios": { + "default": { + "projectId": "krow-workforce-dev", + "appId": "1:933560802882:ios:d2b6d743608e2a527757db", + "uploadDebugSymbols": false, + "fileOutput": "ios/Runner/GoogleService-Info.plist" + } + }, + "dart": { + "lib/firebase_options.dart": { + "projectId": "krow-workforce-dev", + "configurations": { + "android": "1:933560802882:android:da13569105659ead7757db", + "ios": "1:933560802882:ios:d2b6d743608e2a527757db", + "web": "1:933560802882:web:173a841992885bb27757db" + } + } + } + } + } +} \ No newline at end of file diff --git a/apps/mobile/apps/client/ios/config/prod/GoogleService-Info.plist b/apps/mobile/apps/client/ios/config/prod/GoogleService-Info.plist deleted file mode 100644 index daf42001..00000000 --- a/apps/mobile/apps/client/ios/config/prod/GoogleService-Info.plist +++ /dev/null @@ -1,30 +0,0 @@ - - - - - API_KEY - - GCM_SENDER_ID - - PLIST_VERSION - 1 - BUNDLE_ID - prod.krowwithus.client - PROJECT_ID - krow-workforce-prod - STORAGE_BUCKET - krow-workforce-prod.firebasestorage.app - IS_ADS_ENABLED - - IS_ANALYTICS_ENABLED - - IS_APPINVITE_ENABLED - - IS_GCM_ENABLED - - IS_SIGNIN_ENABLED - - GOOGLE_APP_ID - - - diff --git a/apps/mobile/apps/staff/android/app/build.gradle.kts b/apps/mobile/apps/staff/android/app/build.gradle.kts index d3f19e5f..96155fc9 100644 --- a/apps/mobile/apps/staff/android/app/build.gradle.kts +++ b/apps/mobile/apps/staff/android/app/build.gradle.kts @@ -112,6 +112,20 @@ android { } } +// Skip google-services processing for flavors whose google-services.json +// contains placeholder values (e.g. prod before the Firebase project exists). +// Once a real config is dropped in, the task automatically re-enables. +afterEvaluate { + tasks.matching { + it.name.startsWith("process") && it.name.endsWith("GoogleServices") + }.configureEach { + val taskFlavor = name.removePrefix("process").removeSuffix("GoogleServices") + .removeSuffix("Debug").removeSuffix("Release").lowercase() + val configFile = file("src/$taskFlavor/google-services.json") + enabled = configFile.exists() && configFile.readText().contains("\"mobilesdk_app_id\": \"1:") + } +} + flutter { source = "../.." } diff --git a/apps/mobile/apps/staff/android/app/src/prod/google-services.json b/apps/mobile/apps/staff/android/app/src/prod/google-services.json deleted file mode 100644 index 07bbc30b..00000000 --- a/apps/mobile/apps/staff/android/app/src/prod/google-services.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "project_info": { - "project_number": "", - "project_id": "krow-workforce-prod", - "storage_bucket": "krow-workforce-prod.firebasestorage.app" - }, - "client": [ - { - "client_info": { - "mobilesdk_app_id": "", - "android_client_info": { - "package_name": "prod.krowwithus.staff" - } - }, - "oauth_client": [], - "api_key": [ - { - "current_key": "" - } - ] - } - ], - "configuration_version": "1" -} diff --git a/apps/mobile/apps/staff/ios/config/dev/GoogleService-Info.plist b/apps/mobile/apps/staff/ios/config/dev/GoogleService-Info.plist index acd9bbb6..75f58041 100644 --- a/apps/mobile/apps/staff/ios/config/dev/GoogleService-Info.plist +++ b/apps/mobile/apps/staff/ios/config/dev/GoogleService-Info.plist @@ -3,9 +3,9 @@ CLIENT_ID - 933560802882-fphpkdjubve8k7e8ogqj3fk1qducv3sg.apps.googleusercontent.com + 933560802882-jpv087j5jenp1h63mc9ge51767s3l2ac.apps.googleusercontent.com REVERSED_CLIENT_ID - com.googleusercontent.apps.933560802882-fphpkdjubve8k7e8ogqj3fk1qducv3sg + com.googleusercontent.apps.933560802882-jpv087j5jenp1h63mc9ge51767s3l2ac ANDROID_CLIENT_ID 933560802882-fbqg2icq24bmci3f84evjrbth5huh87f.apps.googleusercontent.com API_KEY @@ -15,7 +15,7 @@ PLIST_VERSION 1 BUNDLE_ID - dev.krowwithus.staff + dev.krowwithus.client PROJECT_ID krow-workforce-dev STORAGE_BUCKET @@ -31,6 +31,6 @@ IS_SIGNIN_ENABLED GOOGLE_APP_ID - 1:933560802882:ios:edf97dab6eb87b977757db + 1:933560802882:ios:7e179dfdd1a8994c7757db \ No newline at end of file diff --git a/apps/mobile/apps/staff/ios/config/prod/GoogleService-Info.plist b/apps/mobile/apps/staff/ios/config/prod/GoogleService-Info.plist deleted file mode 100644 index 78f75702..00000000 --- a/apps/mobile/apps/staff/ios/config/prod/GoogleService-Info.plist +++ /dev/null @@ -1,30 +0,0 @@ - - - - - API_KEY - - GCM_SENDER_ID - - PLIST_VERSION - 1 - BUNDLE_ID - prod.krowwithus.staff - PROJECT_ID - krow-workforce-prod - STORAGE_BUCKET - krow-workforce-prod.firebasestorage.app - IS_ADS_ENABLED - - IS_ANALYTICS_ENABLED - - IS_APPINVITE_ENABLED - - IS_GCM_ENABLED - - IS_SIGNIN_ENABLED - - GOOGLE_APP_ID - - - diff --git a/apps/mobile/apps/staff/ios/config/stage/GoogleService-Info.plist b/apps/mobile/apps/staff/ios/config/stage/GoogleService-Info.plist index 7035bac5..631c0d6c 100644 --- a/apps/mobile/apps/staff/ios/config/stage/GoogleService-Info.plist +++ b/apps/mobile/apps/staff/ios/config/stage/GoogleService-Info.plist @@ -9,7 +9,7 @@ PLIST_VERSION 1 BUNDLE_ID - stage.krowwithus.staff + stage.krowwithus.client PROJECT_ID krow-workforce-staging STORAGE_BUCKET @@ -25,6 +25,6 @@ IS_SIGNIN_ENABLED GOOGLE_APP_ID - 1:1032971403708:ios:8c2bbd76bc4f55d9356bb9 + 1:1032971403708:ios:0ff547e80f5324ed356bb9 \ No newline at end of file diff --git a/makefiles/mobile.mk b/makefiles/mobile.mk index de6dbc0d..9cdf38db 100644 --- a/makefiles/mobile.mk +++ b/makefiles/mobile.mk @@ -55,7 +55,7 @@ mobile-client-build: dataconnect-generate-sdk @cd $(MOBILE_DIR) && \ melos run gen:l10n && \ melos run gen:build && \ - melos run build:client -- $(PLATFORM) --$(MODE) --flavor $(ENV) --dart-define-from-file=../../config.$(ENV).json + melos exec --scope="krowwithus_client" -- flutter build $(PLATFORM) --$(MODE) --flavor $(ENV) --dart-define-from-file=../../config.$(ENV).json # --- Staff App --- mobile-staff-dev-android: dataconnect-generate-sdk @@ -71,7 +71,7 @@ mobile-staff-build: dataconnect-generate-sdk @cd $(MOBILE_DIR) && \ melos run gen:l10n && \ melos run gen:build && \ - melos run build:staff -- $(PLATFORM) --$(MODE) --flavor $(ENV) --dart-define-from-file=../../config.$(ENV).json + melos exec --scope="krowwithus_staff" -- flutter build $(PLATFORM) --$(MODE) --flavor $(ENV) --dart-define-from-file=../../config.$(ENV).json # --- E2E (Maestro) --- # Set env before running: TEST_CLIENT_EMAIL, TEST_CLIENT_PASSWORD, TEST_CLIENT_COMPANY, TEST_STAFF_PHONE, TEST_STAFF_OTP, TEST_STAFF_SIGNUP_PHONE From 316a148726251a2c673b51659afd79bf66e3bb76 Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Mon, 9 Mar 2026 19:49:23 -0400 Subject: [PATCH 7/7] feat: Implement review order flow for one-time, recurring, and permanent orders - Added ReviewOrderPage to handle order review before submission. - Created ReviewOrderArguments model to pass data between pages. - Implemented schedule sections for one-time, recurring, and permanent orders. - Enhanced navigation flow to confirm order submission after review. - Refactored order submission logic in OneTimeOrderPage, PermanentOrderPage, and RecurringOrderPage. - Introduced utility functions for time parsing and scheduling. - Created reusable widgets for displaying order information in the review section. - Updated navigation methods to use popSafe for safer back navigation. - Added MainActivity for Android platform integration. --- .../architecture-reviewer/MEMORY.md | 33 +++++ .claude/agents/architecture-reviewer.md | 4 +- .../apps/client/android/app/build.gradle.kts | 2 +- .../client}/MainActivity.kt | 0 .../krowwithus_staff/MainActivity.kt | 5 - .../staff}/MainActivity.kt | 0 .../lib/src/routing/client/navigator.dart | 7 ++ .../lib/src/routing/client/route_paths.dart | 5 + .../lib/src/l10n/en.i18n.json | 3 + .../lib/src/l10n/es.i18n.json | 3 + .../lib/src/create_order_module.dart | 8 ++ .../one_time_order/one_time_order_state.dart | 73 +++++++++++ .../permanent_order_state.dart | 45 +++++++ .../recurring_order_state.dart | 45 +++++++ .../models/review_order_arguments.dart | 48 ++++++++ .../pages/one_time_order_page.dart | 53 +++++++- .../pages/permanent_order_page.dart | 89 +++++++------- .../pages/recurring_order_page.dart | 90 +++++++------- .../presentation/pages/review_order_page.dart | 88 ++++++++++++++ .../presentation/utils/schedule_utils.dart | 47 ++++++++ .../utils/time_parsing_utils.dart | 28 +++++ .../one_time_schedule_section.dart | 31 +++++ .../permanent_schedule_section.dart | 31 +++++ .../recurring_schedule_section.dart | 34 ++++++ .../review_order/review_order_action_bar.dart | 74 ++++++++++++ .../review_order_basics_card.dart | 33 +++++ .../review_order/review_order_header.dart | 33 +++++ .../review_order/review_order_info_row.dart | 40 ++++++ .../review_order_positions_card.dart | 102 ++++++++++++++++ .../review_order_section_card.dart | 59 +++++++++ .../review_order_total_banner.dart | 41 +++++++ .../review_order/review_order_view.dart | 114 ++++++++++++++++++ 32 files changed, 1165 insertions(+), 103 deletions(-) create mode 100644 .claude/agent-memory/architecture-reviewer/MEMORY.md rename apps/mobile/apps/client/android/app/src/main/kotlin/com/{example/krow_client => krowwithus/client}/MainActivity.kt (100%) delete mode 100644 apps/mobile/apps/staff/android/app/src/main/kotlin/com/krowwithus/krowwithus_staff/MainActivity.kt rename apps/mobile/apps/staff/android/app/src/main/kotlin/com/{example/krow_staff => krowwithus/staff}/MainActivity.kt (100%) create mode 100644 apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/models/review_order_arguments.dart create mode 100644 apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/pages/review_order_page.dart create mode 100644 apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/utils/schedule_utils.dart create mode 100644 apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/utils/time_parsing_utils.dart create mode 100644 apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/review_order/one_time_schedule_section.dart create mode 100644 apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/review_order/permanent_schedule_section.dart create mode 100644 apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/review_order/recurring_schedule_section.dart create mode 100644 apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/review_order/review_order_action_bar.dart create mode 100644 apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/review_order/review_order_basics_card.dart create mode 100644 apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/review_order/review_order_header.dart create mode 100644 apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/review_order/review_order_info_row.dart create mode 100644 apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/review_order/review_order_positions_card.dart create mode 100644 apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/review_order/review_order_section_card.dart create mode 100644 apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/review_order/review_order_total_banner.dart create mode 100644 apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/review_order/review_order_view.dart diff --git a/.claude/agent-memory/architecture-reviewer/MEMORY.md b/.claude/agent-memory/architecture-reviewer/MEMORY.md new file mode 100644 index 00000000..d23f742e --- /dev/null +++ b/.claude/agent-memory/architecture-reviewer/MEMORY.md @@ -0,0 +1,33 @@ +# Architecture Reviewer Memory + +## Project Structure Confirmed +- Feature packages: `apps/mobile/packages/features///` +- Domain: `apps/mobile/packages/domain/` +- Design system: `apps/mobile/packages/design_system/` +- Core: `apps/mobile/packages/core/` +- Data Connect: `apps/mobile/packages/data_connect/` +- `client_orders_common` is at `apps/mobile/packages/features/client/orders/orders_common/` (shared across order features) + +## BLoC Registration Pattern +- BLoCs registered with `i.add<>()` (transient) per CLAUDE.md -- NOT singletons +- This means `BlocProvider(create:)` is CORRECT (not `BlocProvider.value()`) +- `SafeBloc` mixin exists in core alongside `BlocErrorHandler` + +## Known Pre-existing Issues (create_order feature) +- All 3 order BLoCs make direct `_service.connector` calls for loading vendors, hubs, roles, and managers instead of going through use cases/repositories (CRITICAL per rules, but pre-existing) +- `firebase_data_connect` and `firebase_auth` are listed as direct dependencies in `client_create_order/pubspec.yaml` (should only be in `data_connect` package) +- All 3 order pages use `Modular.to.pop()` instead of `Modular.to.popSafe()` for the back button + +## Design System Tokens +- Colors: `UiColors.*` +- Typography: `UiTypography.*` +- Spacing: `UiConstants.space*` (e.g., `space3`, `space4`, `space6`) +- App bar: `UiAppBar` + +## Review Patterns (grep-based checks) +- `Color(0x` for hardcoded colors +- `TextStyle(` for custom text styles +- `Navigator.` for direct navigator usage +- `import.*features/` for cross-feature imports (must be zero) +- `_service.connector` in BLoC files for direct data connect calls +- `Modular.to.pop()` for unsafe navigation (should be `popSafe()`) diff --git a/.claude/agents/architecture-reviewer.md b/.claude/agents/architecture-reviewer.md index 0205922d..8918f26d 100644 --- a/.claude/agents/architecture-reviewer.md +++ b/.claude/agents/architecture-reviewer.md @@ -1,12 +1,12 @@ --- -name: architecture-reviewer +name: mobile-architecture-reviewer description: "Use this agent when code changes need to be reviewed for Clean Architecture compliance, design system adherence, and established pattern conformance in the KROW Workforce mobile platform. This includes pull request reviews, branch comparisons, or any time new or modified code needs architectural validation.\\n\\nExamples:\\n\\n- Example 1:\\n user: \"Review the changes in the current branch for architecture compliance\"\\n assistant: \"I'll use the Architecture Review Agent to perform a comprehensive architectural review of the current changes.\"\\n \\n The user wants a code review, so use the Agent tool to launch the architecture-reviewer agent to analyze the changes.\\n \\n\\n- Example 2:\\n user: \"I just finished implementing the scheduling feature. Here's the PR.\"\\n assistant: \"Let me use the Architecture Review Agent to review your scheduling feature implementation for Clean Architecture compliance and design system adherence.\"\\n \\n A new feature has been implemented. Use the Agent tool to launch the architecture-reviewer agent to validate the code against architectural rules before it gets merged.\\n \\n\\n- Example 3:\\n user: \"Can you check if my BLoC implementation follows our patterns?\"\\n assistant: \"I'll launch the Architecture Review Agent to validate your BLoC implementation against our established patterns including SessionHandlerMixin, BlocErrorHandler, and singleton registration.\"\\n \\n The user is asking about pattern compliance for a specific component. Use the Agent tool to launch the architecture-reviewer agent to check BLoC patterns.\\n \\n\\n- Example 4 (proactive usage):\\n Context: Another agent or the user has just completed a significant code change to a mobile feature.\\n assistant: \"The feature implementation is complete. Let me now run the Architecture Review Agent to ensure everything complies with our Clean Architecture rules and design system before we proceed.\"\\n \\n Since significant mobile feature code was written, proactively use the Agent tool to launch the architecture-reviewer agent to catch violations early.\\n " model: opus color: green memory: project --- -You are the **Architecture Review Agent**, an elite software architect specializing in Clean Architecture enforcement for the KROW Workforce Flutter mobile platform. You have deep expertise in Flutter/Dart, BLoC state management, Clean Architecture layer separation, and design system governance. You operate with **zero tolerance** for critical and high-severity violations. +You are the **Mobile Architecture Review Agent**, an elite software architect specializing in Clean Architecture enforcement for the KROW Workforce Flutter mobile platform. You have deep expertise in Flutter/Dart, BLoC state management, Clean Architecture layer separation, and design system governance. You operate with **zero tolerance** for critical and high-severity violations. ## Initialization diff --git a/apps/mobile/apps/client/android/app/build.gradle.kts b/apps/mobile/apps/client/android/app/build.gradle.kts index cf4c5b37..837bc911 100644 --- a/apps/mobile/apps/client/android/app/build.gradle.kts +++ b/apps/mobile/apps/client/android/app/build.gradle.kts @@ -41,7 +41,7 @@ val keystoreProperties = Properties().apply { } android { - namespace = "dev.krowwithus.client" + namespace = "com.krowwithus.client" compileSdk = flutter.compileSdkVersion ndkVersion = flutter.ndkVersion diff --git a/apps/mobile/apps/client/android/app/src/main/kotlin/com/example/krow_client/MainActivity.kt b/apps/mobile/apps/client/android/app/src/main/kotlin/com/krowwithus/client/MainActivity.kt similarity index 100% rename from apps/mobile/apps/client/android/app/src/main/kotlin/com/example/krow_client/MainActivity.kt rename to apps/mobile/apps/client/android/app/src/main/kotlin/com/krowwithus/client/MainActivity.kt diff --git a/apps/mobile/apps/staff/android/app/src/main/kotlin/com/krowwithus/krowwithus_staff/MainActivity.kt b/apps/mobile/apps/staff/android/app/src/main/kotlin/com/krowwithus/krowwithus_staff/MainActivity.kt deleted file mode 100644 index 994d7695..00000000 --- a/apps/mobile/apps/staff/android/app/src/main/kotlin/com/krowwithus/krowwithus_staff/MainActivity.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.krowwithus.krowwithus_staff - -import io.flutter.embedding.android.FlutterActivity - -class MainActivity : FlutterActivity() diff --git a/apps/mobile/apps/staff/android/app/src/main/kotlin/com/example/krow_staff/MainActivity.kt b/apps/mobile/apps/staff/android/app/src/main/kotlin/com/krowwithus/staff/MainActivity.kt similarity index 100% rename from apps/mobile/apps/staff/android/app/src/main/kotlin/com/example/krow_staff/MainActivity.kt rename to apps/mobile/apps/staff/android/app/src/main/kotlin/com/krowwithus/staff/MainActivity.kt diff --git a/apps/mobile/packages/core/lib/src/routing/client/navigator.dart b/apps/mobile/packages/core/lib/src/routing/client/navigator.dart index 54746a8d..e767ade7 100644 --- a/apps/mobile/packages/core/lib/src/routing/client/navigator.dart +++ b/apps/mobile/packages/core/lib/src/routing/client/navigator.dart @@ -210,6 +210,13 @@ extension ClientNavigator on IModularNavigator { safePush(ClientPaths.createOrderPermanent, arguments: arguments); } + /// Pushes the review order page before submission. + /// + /// Returns `true` if the user confirmed submission, `null` if they went back. + Future toCreateOrderReview({Object? arguments}) async { + return safePush(ClientPaths.createOrderReview, arguments: arguments); + } + // ========================================================================== // VIEW ORDER // ========================================================================== diff --git a/apps/mobile/packages/core/lib/src/routing/client/route_paths.dart b/apps/mobile/packages/core/lib/src/routing/client/route_paths.dart index 7575229d..a7e7e174 100644 --- a/apps/mobile/packages/core/lib/src/routing/client/route_paths.dart +++ b/apps/mobile/packages/core/lib/src/routing/client/route_paths.dart @@ -154,4 +154,9 @@ class ClientPaths { /// /// Create a long-term or permanent staffing position. static const String createOrderPermanent = '/create-order/permanent'; + + /// Review order before submission. + /// + /// Summary page shown before posting any order type. + static const String createOrderReview = '/create-order/review'; } diff --git a/apps/mobile/packages/core_localization/lib/src/l10n/en.i18n.json b/apps/mobile/packages/core_localization/lib/src/l10n/en.i18n.json index 8b597294..a69e7984 100644 --- a/apps/mobile/packages/core_localization/lib/src/l10n/en.i18n.json +++ b/apps/mobile/packages/core_localization/lib/src/l10n/en.i18n.json @@ -397,6 +397,9 @@ "title": "Permanent Order", "subtitle": "Long-term staffing placement", "placeholder": "Permanent Order Flow (Work in Progress)" + }, + "review": { + "invalid_arguments": "Unable to load order review. Please go back and try again." } }, "client_main": { diff --git a/apps/mobile/packages/core_localization/lib/src/l10n/es.i18n.json b/apps/mobile/packages/core_localization/lib/src/l10n/es.i18n.json index cb5f4477..f4b30b63 100644 --- a/apps/mobile/packages/core_localization/lib/src/l10n/es.i18n.json +++ b/apps/mobile/packages/core_localization/lib/src/l10n/es.i18n.json @@ -397,6 +397,9 @@ "title": "Orden Permanente", "subtitle": "Colocaci\u00f3n de personal a largo plazo", "placeholder": "Flujo de Orden Permanente (Trabajo en Progreso)" + }, + "review": { + "invalid_arguments": "No se pudo cargar la revisi\u00f3n de la orden. Por favor, regresa e intenta de nuevo." } }, "client_main": { diff --git a/apps/mobile/packages/features/client/orders/create_order/lib/src/create_order_module.dart b/apps/mobile/packages/features/client/orders/create_order/lib/src/create_order_module.dart index b17c6513..84a33c9a 100644 --- a/apps/mobile/packages/features/client/orders/create_order/lib/src/create_order_module.dart +++ b/apps/mobile/packages/features/client/orders/create_order/lib/src/create_order_module.dart @@ -18,6 +18,7 @@ import 'presentation/pages/one_time_order_page.dart'; import 'presentation/pages/permanent_order_page.dart'; import 'presentation/pages/rapid_order_page.dart'; import 'presentation/pages/recurring_order_page.dart'; +import 'presentation/pages/review_order_page.dart'; /// Module for the Client Create Order feature. /// @@ -95,5 +96,12 @@ class ClientCreateOrderModule extends Module { ), child: (BuildContext context) => const PermanentOrderPage(), ); + r.child( + ClientPaths.childRoute( + ClientPaths.createOrder, + ClientPaths.createOrderReview, + ), + child: (BuildContext context) => const ReviewOrderPage(), + ); } } diff --git a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/blocs/one_time_order/one_time_order_state.dart b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/blocs/one_time_order/one_time_order_state.dart index c2964f35..96fb40f3 100644 --- a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/blocs/one_time_order/one_time_order_state.dart +++ b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/blocs/one_time_order/one_time_order_state.dart @@ -1,5 +1,6 @@ import 'package:equatable/equatable.dart'; import 'package:krow_domain/krow_domain.dart'; +import '../../utils/time_parsing_utils.dart'; enum OneTimeOrderStatus { initial, loading, success, failure } @@ -98,6 +99,78 @@ class OneTimeOrderState extends Equatable { ); } + /// Looks up a role name by its ID, returns `null` if not found. + String? roleNameById(String id) { + for (final OneTimeOrderRoleOption r in roles) { + if (r.id == id) return r.name; + } + return null; + } + + /// Looks up a role cost-per-hour by its ID, returns `0` if not found. + double roleCostById(String id) { + for (final OneTimeOrderRoleOption r in roles) { + if (r.id == id) return r.costPerHour; + } + return 0; + } + + /// Total number of workers across all positions. + int get totalWorkers => positions.fold( + 0, + (int sum, OneTimeOrderPosition p) => sum + p.count, + ); + + /// Sum of (count * costPerHour) across all positions. + double get totalCostPerHour => positions.fold( + 0, + (double sum, OneTimeOrderPosition p) => + sum + (p.count * roleCostById(p.role)), + ); + + /// Estimated total cost: sum of (count * costPerHour * hours) per position. + double get estimatedTotal { + double total = 0; + for (final OneTimeOrderPosition p in positions) { + final double hours = parseHoursFromTimes(p.startTime, p.endTime); + total += p.count * roleCostById(p.role) * hours; + } + return total; + } + + /// Time range string from the first position (e.g. "6:00 AM \u2013 2:00 PM"). + String get shiftTimeRange { + if (positions.isEmpty) return ''; + final OneTimeOrderPosition first = positions.first; + return '${first.startTime} \u2013 ${first.endTime}'; + } + + /// Formatted shift duration from the first position (e.g. "8 hrs (30 min break)"). + String get shiftDuration { + if (positions.isEmpty) return ''; + final OneTimeOrderPosition first = positions.first; + final double hours = parseHoursFromTimes(first.startTime, first.endTime); + if (hours <= 0) return ''; + + final int wholeHours = hours.floor(); + final int minutes = ((hours - wholeHours) * 60).round(); + final StringBuffer buffer = StringBuffer(); + + if (wholeHours > 0) buffer.write('$wholeHours hrs'); + if (minutes > 0) { + if (wholeHours > 0) buffer.write(' '); + buffer.write('$minutes min'); + } + + if (first.lunchBreak != null && + first.lunchBreak != 'NO_BREAK' && + first.lunchBreak!.isNotEmpty) { + buffer.write(' (${first.lunchBreak} break)'); + } + + return buffer.toString(); + } + @override List get props => [ date, diff --git a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/blocs/permanent_order/permanent_order_state.dart b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/blocs/permanent_order/permanent_order_state.dart index 4cd04e66..229ff05d 100644 --- a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/blocs/permanent_order/permanent_order_state.dart +++ b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/blocs/permanent_order/permanent_order_state.dart @@ -1,5 +1,6 @@ import 'package:equatable/equatable.dart'; import 'package:krow_domain/krow_domain.dart'; +import '../../utils/time_parsing_utils.dart'; enum PermanentOrderStatus { initial, loading, success, failure } @@ -118,6 +119,50 @@ class PermanentOrderState extends Equatable { ); } + /// Looks up a role name by its ID, returns `null` if not found. + String? roleNameById(String id) { + for (final PermanentOrderRoleOption r in roles) { + if (r.id == id) return r.name; + } + return null; + } + + /// Looks up a role cost-per-hour by its ID, returns `0` if not found. + double roleCostById(String id) { + for (final PermanentOrderRoleOption r in roles) { + if (r.id == id) return r.costPerHour; + } + return 0; + } + + /// Total number of workers across all positions. + int get totalWorkers => positions.fold( + 0, + (int sum, PermanentOrderPosition p) => sum + p.count, + ); + + /// Sum of (count * costPerHour) across all positions. + double get totalCostPerHour => positions.fold( + 0, + (double sum, PermanentOrderPosition p) => + sum + (p.count * roleCostById(p.role)), + ); + + /// Estimated total cost: sum of (count * costPerHour * hours) per position. + double get estimatedTotal { + double total = 0; + for (final PermanentOrderPosition p in positions) { + final double hours = parseHoursFromTimes(p.startTime, p.endTime); + total += p.count * roleCostById(p.role) * hours; + } + return total; + } + + /// Formatted repeat days (e.g. "Mon, Tue, Wed"). + String get formattedRepeatDays => permanentDays.map( + (String day) => day[0] + day.substring(1).toLowerCase(), + ).join(', '); + @override List get props => [ startDate, diff --git a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/blocs/recurring_order/recurring_order_state.dart b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/blocs/recurring_order/recurring_order_state.dart index 8a22eb64..eaa5d0b4 100644 --- a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/blocs/recurring_order/recurring_order_state.dart +++ b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/blocs/recurring_order/recurring_order_state.dart @@ -1,5 +1,6 @@ import 'package:equatable/equatable.dart'; import 'package:krow_domain/krow_domain.dart'; +import '../../utils/time_parsing_utils.dart'; enum RecurringOrderStatus { initial, loading, success, failure } @@ -125,6 +126,50 @@ class RecurringOrderState extends Equatable { ); } + /// Looks up a role name by its ID, returns `null` if not found. + String? roleNameById(String id) { + for (final RecurringOrderRoleOption r in roles) { + if (r.id == id) return r.name; + } + return null; + } + + /// Looks up a role cost-per-hour by its ID, returns `0` if not found. + double roleCostById(String id) { + for (final RecurringOrderRoleOption r in roles) { + if (r.id == id) return r.costPerHour; + } + return 0; + } + + /// Total number of workers across all positions. + int get totalWorkers => positions.fold( + 0, + (int sum, RecurringOrderPosition p) => sum + p.count, + ); + + /// Sum of (count * costPerHour) across all positions. + double get totalCostPerHour => positions.fold( + 0, + (double sum, RecurringOrderPosition p) => + sum + (p.count * roleCostById(p.role)), + ); + + /// Estimated total cost: sum of (count * costPerHour * hours) per position. + double get estimatedTotal { + double total = 0; + for (final RecurringOrderPosition p in positions) { + final double hours = parseHoursFromTimes(p.startTime, p.endTime); + total += p.count * roleCostById(p.role) * hours; + } + return total; + } + + /// Formatted repeat days (e.g. "Mon, Tue, Wed"). + String get formattedRepeatDays => recurringDays.map( + (String day) => day[0] + day.substring(1).toLowerCase(), + ).join(', '); + @override List get props => [ startDate, diff --git a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/models/review_order_arguments.dart b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/models/review_order_arguments.dart new file mode 100644 index 00000000..f833ca8b --- /dev/null +++ b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/models/review_order_arguments.dart @@ -0,0 +1,48 @@ +import '../widgets/review_order/review_order_positions_card.dart'; + +/// Identifies the order type for rendering the correct schedule layout +/// on the review page. +enum ReviewOrderType { oneTime, recurring, permanent } + +/// Data transfer object passed as route arguments to the [ReviewOrderPage]. +/// +/// Contains pre-formatted display strings for every section of the review +/// summary. The form page is responsible for converting BLoC state into +/// these human-readable values before navigating. +class ReviewOrderArguments { + const ReviewOrderArguments({ + required this.orderType, + required this.orderName, + required this.hubName, + required this.shiftContactName, + required this.positions, + required this.totalWorkers, + required this.totalCostPerHour, + required this.estimatedTotal, + this.scheduleDate, + this.scheduleTime, + this.scheduleDuration, + this.scheduleStartDate, + this.scheduleEndDate, + this.scheduleRepeatDays, + }); + + final ReviewOrderType orderType; + final String orderName; + final String hubName; + final String shiftContactName; + final List positions; + final int totalWorkers; + final double totalCostPerHour; + final double estimatedTotal; + + /// One-time order schedule fields. + final String? scheduleDate; + final String? scheduleTime; + final String? scheduleDuration; + + /// Recurring / permanent order schedule fields. + final String? scheduleStartDate; + final String? scheduleEndDate; + final String? scheduleRepeatDays; +} diff --git a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/pages/one_time_order_page.dart b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/pages/one_time_order_page.dart index 1c83311f..2dfe92ef 100644 --- a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/pages/one_time_order_page.dart +++ b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/pages/one_time_order_page.dart @@ -2,18 +2,24 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_modular/flutter_modular.dart'; import 'package:client_orders_common/client_orders_common.dart'; +import 'package:intl/intl.dart'; import 'package:krow_core/core.dart'; import 'package:krow_domain/krow_domain.dart'; import '../blocs/one_time_order/one_time_order_bloc.dart'; import '../blocs/one_time_order/one_time_order_event.dart'; import '../blocs/one_time_order/one_time_order_state.dart'; +import '../models/review_order_arguments.dart'; +import '../widgets/review_order/review_order_positions_card.dart'; /// Page for creating a one-time staffing order. -/// Users can specify the date, location, and multiple staff positions required. /// -/// This page initializes the [OneTimeOrderBloc] and displays the [OneTimeOrderView] -/// from the common orders package. It follows the KROW Clean Architecture by being -/// a [StatelessWidget] and mapping local BLoC state to generic UI models. +/// ## Submission Flow +/// +/// When the user taps "Create Order", this page does NOT submit directly. +/// Instead it navigates to [ReviewOrderPage] with a snapshot of the current +/// BLoC state formatted as [ReviewOrderArguments]. If the user confirms on +/// the review page (pops with `true`), this page then fires +/// [OneTimeOrderSubmitted] on the BLoC to perform the actual API call. class OneTimeOrderPage extends StatelessWidget { /// Creates a [OneTimeOrderPage]. const OneTimeOrderPage({super.key}); @@ -90,15 +96,50 @@ class OneTimeOrderPage extends StatelessWidget { }, onPositionRemoved: (int index) => bloc.add(OneTimeOrderPositionRemoved(index)), - onSubmit: () => bloc.add(const OneTimeOrderSubmitted()), + onSubmit: () => _navigateToReview(state, bloc), onDone: () => Modular.to.toOrdersSpecificDate(state.date), - onBack: () => Modular.to.pop(), + onBack: () => Modular.to.popSafe(), ); }, ), ); } + /// Builds [ReviewOrderArguments] from the current BLoC state and navigates + /// to the review page. Submits the order only if the user confirms. + Future _navigateToReview( + OneTimeOrderState state, + OneTimeOrderBloc bloc, + ) async { + final List reviewPositions = state.positions.map( + (OneTimeOrderPosition p) => ReviewPositionItem( + roleName: state.roleNameById(p.role) ?? p.role, + workerCount: p.count, + costPerHour: state.roleCostById(p.role), + ), + ).toList(); + + final bool? confirmed = await Modular.to.toCreateOrderReview( + arguments: ReviewOrderArguments( + orderType: ReviewOrderType.oneTime, + orderName: state.eventName, + hubName: state.selectedHub?.name ?? '', + shiftContactName: state.selectedManager?.name ?? '', + positions: reviewPositions, + totalWorkers: state.totalWorkers, + totalCostPerHour: state.totalCostPerHour, + estimatedTotal: state.estimatedTotal, + scheduleDate: DateFormat.yMMMEd().format(state.date), + scheduleTime: state.shiftTimeRange, + scheduleDuration: state.shiftDuration, + ), + ); + + if (confirmed == true) { + bloc.add(const OneTimeOrderSubmitted()); + } + } + OrderFormStatus _mapStatus(OneTimeOrderStatus status) { switch (status) { case OneTimeOrderStatus.initial: diff --git a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/pages/permanent_order_page.dart b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/pages/permanent_order_page.dart index 26109e7a..d1220dd2 100644 --- a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/pages/permanent_order_page.dart +++ b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/pages/permanent_order_page.dart @@ -2,13 +2,24 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_modular/flutter_modular.dart'; import 'package:client_orders_common/client_orders_common.dart'; +import 'package:intl/intl.dart'; import 'package:krow_core/core.dart'; import 'package:krow_domain/krow_domain.dart' hide PermanentOrderPosition; import '../blocs/permanent_order/permanent_order_bloc.dart'; import '../blocs/permanent_order/permanent_order_event.dart'; import '../blocs/permanent_order/permanent_order_state.dart'; +import '../models/review_order_arguments.dart'; +import '../utils/schedule_utils.dart'; +import '../widgets/review_order/review_order_positions_card.dart'; /// Page for creating a permanent staffing order. +/// +/// ## Submission Flow +/// +/// When the user taps "Create Order", this page navigates to +/// [ReviewOrderPage] with a snapshot of the current BLoC state formatted +/// as [ReviewOrderArguments]. If the user confirms (pops with `true`), +/// this page fires [PermanentOrderSubmitted] on the BLoC. class PermanentOrderPage extends StatelessWidget { /// Creates a [PermanentOrderPage]. const PermanentOrderPage({super.key}); @@ -89,64 +100,54 @@ class PermanentOrderPage extends StatelessWidget { }, onPositionRemoved: (int index) => bloc.add(PermanentOrderPositionRemoved(index)), - onSubmit: () => bloc.add(const PermanentOrderSubmitted()), + onSubmit: () => _navigateToReview(state, bloc), onDone: () { - final DateTime initialDate = _firstPermanentShiftDate( + final DateTime initialDate = firstScheduledShiftDate( state.startDate, + state.startDate.add(const Duration(days: 29)), state.permanentDays, ); - // Navigate to orders page with the initial date set to the first recurring shift date Modular.to.toOrdersSpecificDate(initialDate); }, - onBack: () => Modular.to.pop(), + onBack: () => Modular.to.popSafe(), ); }, ), ); } - DateTime _firstPermanentShiftDate( - DateTime startDate, - List permanentDays, - ) { - final DateTime start = DateTime( - startDate.year, - startDate.month, - startDate.day, - ); - final DateTime end = start.add(const Duration(days: 29)); - final Set selected = permanentDays.toSet(); - for ( - DateTime day = start; - !day.isAfter(end); - day = day.add(const Duration(days: 1)) - ) { - if (selected.contains(_weekdayLabel(day))) { - return day; - } - } - return start; - } + /// Builds [ReviewOrderArguments] from the current BLoC state and navigates + /// to the review page. Submits the order only if the user confirms. + Future _navigateToReview( + PermanentOrderState state, + PermanentOrderBloc bloc, + ) async { + final List reviewPositions = state.positions.map( + (PermanentOrderPosition p) => ReviewPositionItem( + roleName: state.roleNameById(p.role) ?? p.role, + workerCount: p.count, + costPerHour: state.roleCostById(p.role), + ), + ).toList(); - String _weekdayLabel(DateTime date) { - switch (date.weekday) { - case DateTime.monday: - return 'MON'; - case DateTime.tuesday: - return 'TUE'; - case DateTime.wednesday: - return 'WED'; - case DateTime.thursday: - return 'THU'; - case DateTime.friday: - return 'FRI'; - case DateTime.saturday: - return 'SAT'; - case DateTime.sunday: - return 'SUN'; - default: - return 'SUN'; + final bool? confirmed = await Modular.to.toCreateOrderReview( + arguments: ReviewOrderArguments( + orderType: ReviewOrderType.permanent, + orderName: state.eventName, + hubName: state.selectedHub?.name ?? '', + shiftContactName: state.selectedManager?.name ?? '', + positions: reviewPositions, + totalWorkers: state.totalWorkers, + totalCostPerHour: state.totalCostPerHour, + estimatedTotal: state.estimatedTotal, + scheduleStartDate: DateFormat.yMMMd().format(state.startDate), + scheduleRepeatDays: state.formattedRepeatDays, + ), + ); + + if (confirmed == true) { + bloc.add(const PermanentOrderSubmitted()); } } diff --git a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/pages/recurring_order_page.dart b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/pages/recurring_order_page.dart index c65c26a3..c7fe4979 100644 --- a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/pages/recurring_order_page.dart +++ b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/pages/recurring_order_page.dart @@ -2,13 +2,24 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_modular/flutter_modular.dart'; import 'package:client_orders_common/client_orders_common.dart'; +import 'package:intl/intl.dart'; import 'package:krow_core/core.dart'; import 'package:krow_domain/krow_domain.dart' hide RecurringOrderPosition; import '../blocs/recurring_order/recurring_order_bloc.dart'; import '../blocs/recurring_order/recurring_order_event.dart'; import '../blocs/recurring_order/recurring_order_state.dart'; +import '../models/review_order_arguments.dart'; +import '../utils/schedule_utils.dart'; +import '../widgets/review_order/review_order_positions_card.dart'; /// Page for creating a recurring staffing order. +/// +/// ## Submission Flow +/// +/// When the user taps "Create Order", this page navigates to +/// [ReviewOrderPage] with a snapshot of the current BLoC state formatted +/// as [ReviewOrderArguments]. If the user confirms (pops with `true`), +/// this page fires [RecurringOrderSubmitted] on the BLoC. class RecurringOrderPage extends StatelessWidget { /// Creates a [RecurringOrderPage]. const RecurringOrderPage({super.key}); @@ -92,7 +103,7 @@ class RecurringOrderPage extends StatelessWidget { }, onPositionRemoved: (int index) => bloc.add(RecurringOrderPositionRemoved(index)), - onSubmit: () => bloc.add(const RecurringOrderSubmitted()), + onSubmit: () => _navigateToReview(state, bloc), onDone: () { final DateTime maxEndDate = state.startDate.add( const Duration(days: 29), @@ -101,64 +112,53 @@ class RecurringOrderPage extends StatelessWidget { state.endDate.isAfter(maxEndDate) ? maxEndDate : state.endDate; - final DateTime initialDate = _firstRecurringShiftDate( + final DateTime initialDate = firstScheduledShiftDate( state.startDate, effectiveEndDate, state.recurringDays, ); - // Navigate to orders page with the initial date set to the first recurring shift date Modular.to.toOrdersSpecificDate(initialDate); }, - onBack: () => Modular.to.pop(), + onBack: () => Modular.to.popSafe(), ); }, ), ); } - DateTime _firstRecurringShiftDate( - DateTime startDate, - DateTime endDate, - List recurringDays, - ) { - final DateTime start = DateTime( - startDate.year, - startDate.month, - startDate.day, - ); - final DateTime end = DateTime(endDate.year, endDate.month, endDate.day); - final Set selected = recurringDays.toSet(); - for ( - DateTime day = start; - !day.isAfter(end); - day = day.add(const Duration(days: 1)) - ) { - if (selected.contains(_weekdayLabel(day))) { - return day; - } - } - return start; - } + /// Builds [ReviewOrderArguments] from the current BLoC state and navigates + /// to the review page. Submits the order only if the user confirms. + Future _navigateToReview( + RecurringOrderState state, + RecurringOrderBloc bloc, + ) async { + final List reviewPositions = state.positions.map( + (RecurringOrderPosition p) => ReviewPositionItem( + roleName: state.roleNameById(p.role) ?? p.role, + workerCount: p.count, + costPerHour: state.roleCostById(p.role), + ), + ).toList(); - String _weekdayLabel(DateTime date) { - switch (date.weekday) { - case DateTime.monday: - return 'MON'; - case DateTime.tuesday: - return 'TUE'; - case DateTime.wednesday: - return 'WED'; - case DateTime.thursday: - return 'THU'; - case DateTime.friday: - return 'FRI'; - case DateTime.saturday: - return 'SAT'; - case DateTime.sunday: - return 'SUN'; - default: - return 'SUN'; + final bool? confirmed = await Modular.to.toCreateOrderReview( + arguments: ReviewOrderArguments( + orderType: ReviewOrderType.recurring, + orderName: state.eventName, + hubName: state.selectedHub?.name ?? '', + shiftContactName: state.selectedManager?.name ?? '', + positions: reviewPositions, + totalWorkers: state.totalWorkers, + totalCostPerHour: state.totalCostPerHour, + estimatedTotal: state.estimatedTotal, + scheduleStartDate: DateFormat.yMMMd().format(state.startDate), + scheduleEndDate: DateFormat.yMMMd().format(state.endDate), + scheduleRepeatDays: state.formattedRepeatDays, + ), + ); + + if (confirmed == true) { + bloc.add(const RecurringOrderSubmitted()); } } diff --git a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/pages/review_order_page.dart b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/pages/review_order_page.dart new file mode 100644 index 00000000..44500629 --- /dev/null +++ b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/pages/review_order_page.dart @@ -0,0 +1,88 @@ +import 'package:core_localization/core_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_modular/flutter_modular.dart'; +import 'package:krow_core/core.dart'; +import '../models/review_order_arguments.dart'; +import '../widgets/review_order/one_time_schedule_section.dart'; +import '../widgets/review_order/permanent_schedule_section.dart'; +import '../widgets/review_order/recurring_schedule_section.dart'; +import '../widgets/review_order/review_order_view.dart'; + +/// Review step in the order creation flow. +/// +/// ## Navigation Flow +/// +/// ``` +/// Form Page (one-time / recurring / permanent) +/// -> user taps "Create Order" +/// -> navigates here with [ReviewOrderArguments] +/// -> user reviews summary +/// -> "Post Order" => pops with `true` => form page submits via BLoC +/// -> back / "Edit" => pops without result => form page resumes editing +/// ``` +/// +/// This page is purely presentational. It receives all display data via +/// [ReviewOrderArguments] and does not hold any BLoC. The calling form +/// page owns the BLoC and only fires the submit event after this page +/// confirms. +class ReviewOrderPage extends StatelessWidget { + /// Creates a [ReviewOrderPage]. + const ReviewOrderPage({super.key}); + + @override + Widget build(BuildContext context) { + final Object? rawArgs = Modular.args.data; + if (rawArgs is! ReviewOrderArguments) { + return Scaffold( + body: Center( + child: Text(t.client_create_order.review.invalid_arguments), + ), + ); + } + + final ReviewOrderArguments args = rawArgs; + final bool showEdit = args.orderType != ReviewOrderType.oneTime; + + return ReviewOrderView( + orderName: args.orderName, + hubName: args.hubName, + shiftContactName: args.shiftContactName, + scheduleSection: _buildScheduleSection(args, showEdit), + positions: args.positions, + totalWorkers: args.totalWorkers, + totalCostPerHour: args.totalCostPerHour, + estimatedTotal: args.estimatedTotal, + showEditButtons: showEdit, + onEditBasics: showEdit ? () => Modular.to.popSafe() : null, + onEditSchedule: showEdit ? () => Modular.to.popSafe() : null, + onEditPositions: showEdit ? () => Modular.to.popSafe() : null, + onBack: () => Modular.to.popSafe(), + onSubmit: () => Modular.to.popSafe(true), + ); + } + + /// Builds the schedule section widget matching the order type. + Widget _buildScheduleSection(ReviewOrderArguments args, bool showEdit) { + switch (args.orderType) { + case ReviewOrderType.oneTime: + return OneTimeScheduleSection( + date: args.scheduleDate ?? '', + time: args.scheduleTime ?? '', + duration: args.scheduleDuration ?? '', + ); + case ReviewOrderType.recurring: + return RecurringScheduleSection( + startDate: args.scheduleStartDate ?? '', + endDate: args.scheduleEndDate ?? '', + repeatDays: args.scheduleRepeatDays ?? '', + onEdit: showEdit ? () => Modular.to.popSafe() : null, + ); + case ReviewOrderType.permanent: + return PermanentScheduleSection( + startDate: args.scheduleStartDate ?? '', + repeatDays: args.scheduleRepeatDays ?? '', + onEdit: showEdit ? () => Modular.to.popSafe() : null, + ); + } + } +} diff --git a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/utils/schedule_utils.dart b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/utils/schedule_utils.dart new file mode 100644 index 00000000..4928816c --- /dev/null +++ b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/utils/schedule_utils.dart @@ -0,0 +1,47 @@ +/// Returns the uppercase three-letter weekday label for [date]. +/// +/// Maps `DateTime.weekday` (1=Monday..7=Sunday) to labels like "MON", "TUE". +String weekdayLabel(DateTime date) { + switch (date.weekday) { + case DateTime.monday: + return 'MON'; + case DateTime.tuesday: + return 'TUE'; + case DateTime.wednesday: + return 'WED'; + case DateTime.thursday: + return 'THU'; + case DateTime.friday: + return 'FRI'; + case DateTime.saturday: + return 'SAT'; + case DateTime.sunday: + return 'SUN'; + default: + return 'SUN'; + } +} + +/// Finds the first date within [startDate]..[endDate] whose weekday matches +/// one of the [selectedDays] labels (e.g. "MON", "TUE"). +/// +/// Returns [startDate] if no match is found. +DateTime firstScheduledShiftDate( + DateTime startDate, + DateTime endDate, + List selectedDays, +) { + final DateTime start = DateTime(startDate.year, startDate.month, startDate.day); + final DateTime end = DateTime(endDate.year, endDate.month, endDate.day); + final Set selected = selectedDays.toSet(); + for ( + DateTime day = start; + !day.isAfter(end); + day = day.add(const Duration(days: 1)) + ) { + if (selected.contains(weekdayLabel(day))) { + return day; + } + } + return start; +} diff --git a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/utils/time_parsing_utils.dart b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/utils/time_parsing_utils.dart new file mode 100644 index 00000000..0cf51154 --- /dev/null +++ b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/utils/time_parsing_utils.dart @@ -0,0 +1,28 @@ +import 'package:intl/intl.dart'; + +/// Parses a time string in common formats ("6:00 PM", "18:00", "6:00PM"). +/// +/// Returns `null` if no format matches. +DateTime? parseTime(String time) { + for (final String format in ['h:mm a', 'HH:mm', 'h:mma']) { + try { + return DateFormat(format).parse(time.trim()); + } catch (_) { + continue; + } + } + return null; +} + +/// Calculates the number of hours between [startTime] and [endTime]. +/// +/// Handles overnight shifts (negative difference wraps to 24h). +/// Returns `0` if either time string cannot be parsed. +double parseHoursFromTimes(String startTime, String endTime) { + final DateTime? start = parseTime(startTime); + final DateTime? end = parseTime(endTime); + if (start == null || end == null) return 0; + Duration diff = end.difference(start); + if (diff.isNegative) diff += const Duration(hours: 24); + return diff.inMinutes / 60; +} diff --git a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/review_order/one_time_schedule_section.dart b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/review_order/one_time_schedule_section.dart new file mode 100644 index 00000000..190b1215 --- /dev/null +++ b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/review_order/one_time_schedule_section.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; +import 'review_order_info_row.dart'; +import 'review_order_section_card.dart'; + +/// Schedule section for one-time orders. +/// +/// Displays: Date, Time (start-end), Duration (with break info). +class OneTimeScheduleSection extends StatelessWidget { + const OneTimeScheduleSection({ + required this.date, + required this.time, + required this.duration, + super.key, + }); + + final String date; + final String time; + final String duration; + + @override + Widget build(BuildContext context) { + return ReviewOrderSectionCard( + title: 'Schedule', + children: [ + ReviewOrderInfoRow(label: 'Date', value: date), + ReviewOrderInfoRow(label: 'Time', value: time), + ReviewOrderInfoRow(label: 'Duration', value: duration), + ], + ); + } +} diff --git a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/review_order/permanent_schedule_section.dart b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/review_order/permanent_schedule_section.dart new file mode 100644 index 00000000..e656bb17 --- /dev/null +++ b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/review_order/permanent_schedule_section.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; +import 'review_order_info_row.dart'; +import 'review_order_section_card.dart'; + +/// Schedule section for permanent orders. +/// +/// Displays: Start Date, Repeat days (no end date). +class PermanentScheduleSection extends StatelessWidget { + const PermanentScheduleSection({ + required this.startDate, + required this.repeatDays, + this.onEdit, + super.key, + }); + + final String startDate; + final String repeatDays; + final VoidCallback? onEdit; + + @override + Widget build(BuildContext context) { + return ReviewOrderSectionCard( + title: 'Schedule', + onEdit: onEdit, + children: [ + ReviewOrderInfoRow(label: 'Start Date', value: startDate), + ReviewOrderInfoRow(label: 'Repeat', value: repeatDays), + ], + ); + } +} diff --git a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/review_order/recurring_schedule_section.dart b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/review_order/recurring_schedule_section.dart new file mode 100644 index 00000000..d1bbcab3 --- /dev/null +++ b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/review_order/recurring_schedule_section.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; +import 'review_order_info_row.dart'; +import 'review_order_section_card.dart'; + +/// Schedule section for recurring orders. +/// +/// Displays: Start Date, End Date, Repeat days. +class RecurringScheduleSection extends StatelessWidget { + const RecurringScheduleSection({ + required this.startDate, + required this.endDate, + required this.repeatDays, + this.onEdit, + super.key, + }); + + final String startDate; + final String endDate; + final String repeatDays; + final VoidCallback? onEdit; + + @override + Widget build(BuildContext context) { + return ReviewOrderSectionCard( + title: 'Schedule', + onEdit: onEdit, + children: [ + ReviewOrderInfoRow(label: 'Start Date', value: startDate), + ReviewOrderInfoRow(label: 'End Date', value: endDate), + ReviewOrderInfoRow(label: 'Repeat', value: repeatDays), + ], + ); + } +} diff --git a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/review_order/review_order_action_bar.dart b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/review_order/review_order_action_bar.dart new file mode 100644 index 00000000..12f01da7 --- /dev/null +++ b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/review_order/review_order_action_bar.dart @@ -0,0 +1,74 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// Bottom action bar with a back button and primary submit button. +/// +/// The back button is a compact outlined button with a chevron icon. +/// The submit button fills the remaining space. +class ReviewOrderActionBar extends StatelessWidget { + const ReviewOrderActionBar({ + required this.onBack, + required this.onSubmit, + this.submitLabel = 'Post Order', + this.isLoading = false, + super.key, + }); + + final VoidCallback onBack; + final VoidCallback? onSubmit; + final String submitLabel; + final bool isLoading; + + @override + Widget build(BuildContext context) { + return SafeArea( + top: false, + child: Padding( + padding: const EdgeInsets.only( + left: UiConstants.space6, + right: UiConstants.space6, + top: UiConstants.space3, + bottom: UiConstants.space10, + ), + child: Row( + children: [ + SizedBox( + width: 80, + height: 52, + child: OutlinedButton( + onPressed: onBack, + style: OutlinedButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: UiConstants.radiusXl, + ), + side: const BorderSide( + color: UiColors.border, + width: 1.5, + ), + ), + child: const Icon( + UiIcons.chevronLeft, + size: UiConstants.iconMd, + color: UiColors.iconPrimary, + ), + ), + ), + const SizedBox(width: UiConstants.space3), + Expanded( + child: SizedBox( + height: 52, + child: UiButton.primary( + text: submitLabel, + onPressed: onSubmit, + isLoading: isLoading, + size: UiButtonSize.large, + fullWidth: true, + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/review_order/review_order_basics_card.dart b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/review_order/review_order_basics_card.dart new file mode 100644 index 00000000..26655b13 --- /dev/null +++ b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/review_order/review_order_basics_card.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; +import 'review_order_info_row.dart'; +import 'review_order_section_card.dart'; + +/// Displays the "Basics" section card showing order name, hub, and +/// shift contact information. +class ReviewOrderBasicsCard extends StatelessWidget { + const ReviewOrderBasicsCard({ + required this.orderName, + required this.hubName, + required this.shiftContactName, + this.onEdit, + super.key, + }); + + final String orderName; + final String hubName; + final String shiftContactName; + final VoidCallback? onEdit; + + @override + Widget build(BuildContext context) { + return ReviewOrderSectionCard( + title: 'Basics', + onEdit: onEdit, + children: [ + ReviewOrderInfoRow(label: 'Order Name', value: orderName), + ReviewOrderInfoRow(label: 'Hub', value: hubName), + ReviewOrderInfoRow(label: 'Shift Contact', value: shiftContactName), + ], + ); + } +} diff --git a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/review_order/review_order_header.dart b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/review_order/review_order_header.dart new file mode 100644 index 00000000..75c05c80 --- /dev/null +++ b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/review_order/review_order_header.dart @@ -0,0 +1,33 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// Displays the "Review & Submit" title and subtitle at the top of the +/// review order page. +class ReviewOrderHeader extends StatelessWidget { + const ReviewOrderHeader({super.key}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only( + left: UiConstants.space6, + right: UiConstants.space6, + top: UiConstants.space4, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Review & Submit', + style: UiTypography.headline2m, + ), + const SizedBox(height: UiConstants.space1), + Text( + 'Confirm details before posting', + style: UiTypography.body2r.textSecondary, + ), + ], + ), + ); + } +} diff --git a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/review_order/review_order_info_row.dart b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/review_order/review_order_info_row.dart new file mode 100644 index 00000000..9add76e5 --- /dev/null +++ b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/review_order/review_order_info_row.dart @@ -0,0 +1,40 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// A single key-value row used inside review section cards. +/// +/// Displays a label on the left and a value on the right in a +/// space-between layout. +class ReviewOrderInfoRow extends StatelessWidget { + const ReviewOrderInfoRow({ + required this.label, + required this.value, + super.key, + }); + + final String label; + final String value; + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Text( + label, + style: UiTypography.body3r.textSecondary, + ), + ), + const SizedBox(width: UiConstants.space3), + Flexible( + child: Text( + value, + style: UiTypography.body3m, + textAlign: TextAlign.end, + ), + ), + ], + ); + } +} diff --git a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/review_order/review_order_positions_card.dart b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/review_order/review_order_positions_card.dart new file mode 100644 index 00000000..18812630 --- /dev/null +++ b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/review_order/review_order_positions_card.dart @@ -0,0 +1,102 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; +import 'review_order_info_row.dart'; + +/// Displays a summary of all positions with a divider and total row. +/// +/// Each position shows the role name and "N workers . $X/hr". +/// A divider separates the individual positions from the total. +class ReviewOrderPositionsCard extends StatelessWidget { + const ReviewOrderPositionsCard({ + required this.positions, + required this.totalWorkers, + required this.totalCostPerHour, + this.onEdit, + super.key, + }); + + final List positions; + final int totalWorkers; + final double totalCostPerHour; + final VoidCallback? onEdit; + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: UiColors.white, + borderRadius: UiConstants.radiusXl, + border: Border.all(color: UiColors.border), + ), + padding: const EdgeInsets.all(UiConstants.space4), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'POSITIONS', + style: UiTypography.titleUppercase4b.textSecondary, + ), + if (onEdit != null) + GestureDetector( + onTap: onEdit, + child: Text( + 'Edit', + style: UiTypography.body3m.primary, + ), + ), + ], + ), + ...positions.map( + (ReviewPositionItem position) => Padding( + padding: const EdgeInsets.only(top: UiConstants.space3), + child: ReviewOrderInfoRow( + label: position.roleName, + value: + '${position.workerCount} workers \u00B7 \$${position.costPerHour.toStringAsFixed(0)}/hr', + ), + ), + ), + Padding( + padding: const EdgeInsets.only(top: UiConstants.space3), + child: Container( + height: 1, + color: UiColors.bgSecondary, + ), + ), + Padding( + padding: const EdgeInsets.only(top: UiConstants.space3), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Total', + style: UiTypography.body3m, + ), + Text( + '$totalWorkers workers \u00B7 \$${totalCostPerHour.toStringAsFixed(0)}/hr', + style: UiTypography.body3b.primary, + ), + ], + ), + ), + ], + ), + ); + } +} + +/// A single position item for the positions card. +class ReviewPositionItem { + const ReviewPositionItem({ + required this.roleName, + required this.workerCount, + required this.costPerHour, + }); + + final String roleName; + final int workerCount; + final double costPerHour; +} diff --git a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/review_order/review_order_section_card.dart b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/review_order/review_order_section_card.dart new file mode 100644 index 00000000..33f8b5e8 --- /dev/null +++ b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/review_order/review_order_section_card.dart @@ -0,0 +1,59 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// A card that groups related review information with a section header. +/// +/// Displays an uppercase section title with an optional "Edit" action +/// and a list of child rows. +class ReviewOrderSectionCard extends StatelessWidget { + const ReviewOrderSectionCard({ + required this.title, + required this.children, + this.onEdit, + super.key, + }); + + final String title; + final List children; + final VoidCallback? onEdit; + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: UiColors.white, + borderRadius: UiConstants.radiusXl, + border: Border.all(color: UiColors.border), + ), + padding: const EdgeInsets.all(UiConstants.space4), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + title.toUpperCase(), + style: UiTypography.titleUppercase4b.textSecondary, + ), + if (onEdit != null) + GestureDetector( + onTap: onEdit, + child: Text( + 'Edit', + style: UiTypography.body3m.primary, + ), + ), + ], + ), + ...children.map( + (Widget child) => Padding( + padding: const EdgeInsets.only(top: UiConstants.space3), + child: child, + ), + ), + ], + ), + ); + } +} diff --git a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/review_order/review_order_total_banner.dart b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/review_order/review_order_total_banner.dart new file mode 100644 index 00000000..0b34924b --- /dev/null +++ b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/review_order/review_order_total_banner.dart @@ -0,0 +1,41 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// A highlighted banner displaying the estimated total cost. +/// +/// Uses the primary inverse background color with a bold price display. +class ReviewOrderTotalBanner extends StatelessWidget { + const ReviewOrderTotalBanner({ + required this.totalAmount, + super.key, + }); + + final double totalAmount; + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric( + horizontal: UiConstants.space4, + vertical: UiConstants.space4, + ), + decoration: BoxDecoration( + color: UiColors.primaryInverse, + borderRadius: UiConstants.radiusLg, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Estimated Total', + style: UiTypography.body2m, + ), + Text( + '\$${totalAmount.toStringAsFixed(2)}', + style: UiTypography.headline3b.primary, + ), + ], + ), + ); + } +} diff --git a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/review_order/review_order_view.dart b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/review_order/review_order_view.dart new file mode 100644 index 00000000..46fb7453 --- /dev/null +++ b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/review_order/review_order_view.dart @@ -0,0 +1,114 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; +import 'review_order_action_bar.dart'; +import 'review_order_basics_card.dart'; +import 'review_order_header.dart'; +import 'review_order_positions_card.dart'; +import 'review_order_total_banner.dart'; + +/// The main review order view that displays a summary of the order +/// before submission. +/// +/// This is a "dumb" widget that receives all data via constructor parameters +/// and exposes callbacks for user interactions. It does NOT interact with +/// any BLoC directly. +/// +/// The [scheduleSection] widget is injected to allow different schedule +/// layouts per order type (one-time, recurring, permanent). +class ReviewOrderView extends StatelessWidget { + const ReviewOrderView({ + required this.orderName, + required this.hubName, + required this.shiftContactName, + required this.scheduleSection, + required this.positions, + required this.totalWorkers, + required this.totalCostPerHour, + required this.estimatedTotal, + required this.onBack, + required this.onSubmit, + this.showEditButtons = false, + this.onEditBasics, + this.onEditSchedule, + this.onEditPositions, + this.submitLabel = 'Post Order', + this.isLoading = false, + super.key, + }); + + final String orderName; + final String hubName; + final String shiftContactName; + final Widget scheduleSection; + final List positions; + final int totalWorkers; + final double totalCostPerHour; + final double estimatedTotal; + final VoidCallback onBack; + final VoidCallback? onSubmit; + final bool showEditButtons; + final VoidCallback? onEditBasics; + final VoidCallback? onEditSchedule; + final VoidCallback? onEditPositions; + final String submitLabel; + final bool isLoading; + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: UiColors.bgMenu, + appBar: UiAppBar( + showBackButton: true, + onLeadingPressed: onBack, + ), + body: Column( + children: [ + Expanded( + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const ReviewOrderHeader(), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: UiConstants.space6, + ), + child: Column( + children: [ + const SizedBox(height: UiConstants.space4), + ReviewOrderBasicsCard( + orderName: orderName, + hubName: hubName, + shiftContactName: shiftContactName, + onEdit: showEditButtons ? onEditBasics : null, + ), + const SizedBox(height: UiConstants.space3), + scheduleSection, + const SizedBox(height: UiConstants.space3), + ReviewOrderPositionsCard( + positions: positions, + totalWorkers: totalWorkers, + totalCostPerHour: totalCostPerHour, + onEdit: showEditButtons ? onEditPositions : null, + ), + const SizedBox(height: UiConstants.space3), + ReviewOrderTotalBanner(totalAmount: estimatedTotal), + const SizedBox(height: UiConstants.space4), + ], + ), + ), + ], + ), + ), + ), + ReviewOrderActionBar( + onBack: onBack, + onSubmit: onSubmit, + submitLabel: submitLabel, + isLoading: isLoading, + ), + ], + ), + ); + } +}