From b526a672bda8b577e2d6f862d9b257769b667707 Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Sun, 1 Feb 2026 15:34:27 -0500 Subject: [PATCH 01/15] feat: Integrate Firebase configuration and Google Maps Places Autocomplete for address validation --- .../client/android/app/google-services.json | 64 +++++------------ .../ios/Runner.xcodeproj/project.pbxproj | 4 ++ .../ios/Runner/GoogleService-Info.plist | 36 ++++++++++ .../apps/client/lib/firebase_options.dart | 38 ++++++++++ apps/mobile/apps/client/lib/main.dart | 6 +- apps/mobile/apps/client/web/index.html | 23 ++++++ .../staff/android/app/google-services.json | 72 ------------------- .../ios/Runner.xcodeproj/project.pbxproj | 4 ++ .../staff/ios/Runner/GoogleService-Info.plist | 36 ++++++++++ .../apps/staff/lib/firebase_options.dart | 36 ++++++++++ apps/mobile/apps/staff/lib/main.dart | 64 ++++++++--------- apps/mobile/apps/staff/pubspec.yaml | 13 ++-- apps/mobile/apps/staff/web/index.html | 23 ++++++ 13 files changed, 263 insertions(+), 156 deletions(-) create mode 100644 apps/mobile/apps/client/ios/Runner/GoogleService-Info.plist create mode 100644 apps/mobile/apps/client/lib/firebase_options.dart create mode 100644 apps/mobile/apps/staff/ios/Runner/GoogleService-Info.plist create mode 100644 apps/mobile/apps/staff/lib/firebase_options.dart diff --git a/apps/mobile/apps/client/android/app/google-services.json b/apps/mobile/apps/client/android/app/google-services.json index 7533ddc2..20ad2e48 100644 --- a/apps/mobile/apps/client/android/app/google-services.json +++ b/apps/mobile/apps/client/android/app/google-services.json @@ -5,42 +5,6 @@ "storage_bucket": "krow-workforce-dev.firebasestorage.app" }, "client": [ - { - "client_info": { - "mobilesdk_app_id": "1:933560802882:android:87d41566f8dda41d7757db", - "android_client_info": { - "package_name": "com.example.krow_workforce" - } - }, - "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-dppsapp5i3lsfrlm1mhob2s21peofg1t.apps.googleusercontent.com", - "client_type": 2, - "ios_info": { - "bundle_id": "com.krow.app.staff.dev" - } - } - ] - } - } - }, { "client_info": { "mobilesdk_app_id": "1:933560802882:android:edcddb83ea4bbb517757db", @@ -67,10 +31,10 @@ "client_type": 3 }, { - "client_id": "933560802882-dppsapp5i3lsfrlm1mhob2s21peofg1t.apps.googleusercontent.com", + "client_id": "933560802882-29olj9ku64jbe9h7flinha6hbi8qrluh.apps.googleusercontent.com", "client_type": 2, "ios_info": { - "bundle_id": "com.krow.app.staff.dev" + "bundle_id": "com.krowwithus.staff" } } ] @@ -103,10 +67,10 @@ "client_type": 3 }, { - "client_id": "933560802882-dppsapp5i3lsfrlm1mhob2s21peofg1t.apps.googleusercontent.com", + "client_id": "933560802882-29olj9ku64jbe9h7flinha6hbi8qrluh.apps.googleusercontent.com", "client_type": 2, "ios_info": { - "bundle_id": "com.krow.app.staff.dev" + "bundle_id": "com.krowwithus.staff" } } ] @@ -139,10 +103,10 @@ "client_type": 3 }, { - "client_id": "933560802882-dppsapp5i3lsfrlm1mhob2s21peofg1t.apps.googleusercontent.com", + "client_id": "933560802882-29olj9ku64jbe9h7flinha6hbi8qrluh.apps.googleusercontent.com", "client_type": 2, "ios_info": { - "bundle_id": "com.krow.app.staff.dev" + "bundle_id": "com.krowwithus.staff" } } ] @@ -151,12 +115,20 @@ }, { "client_info": { - "mobilesdk_app_id": "1:933560802882:android:d26bde4ee337b0b17757db", + "mobilesdk_app_id": "1:933560802882:android:1ae05d85c865f77c7757db", "android_client_info": { - "package_name": "com.krowwithus.krow_workforce.dev" + "package_name": "com.krowwithus.staff" } }, "oauth_client": [ + { + "client_id": "933560802882-ikdfv3o5f47g36qqgvfq55o4m19n7gk4.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "com.krowwithus.staff", + "certificate_hash": "ac917ae8470ab29f1107c773c6017ff5ea5d102d" + } + }, { "client_id": "933560802882-grp98a1v7amflnnup68vh01tj06eaem1.apps.googleusercontent.com", "client_type": 3 @@ -175,10 +147,10 @@ "client_type": 3 }, { - "client_id": "933560802882-dppsapp5i3lsfrlm1mhob2s21peofg1t.apps.googleusercontent.com", + "client_id": "933560802882-29olj9ku64jbe9h7flinha6hbi8qrluh.apps.googleusercontent.com", "client_type": 2, "ios_info": { - "bundle_id": "com.krow.app.staff.dev" + "bundle_id": "com.krowwithus.staff" } } ] diff --git a/apps/mobile/apps/client/ios/Runner.xcodeproj/project.pbxproj b/apps/mobile/apps/client/ios/Runner.xcodeproj/project.pbxproj index 4e834d1f..f5989365 100644 --- a/apps/mobile/apps/client/ios/Runner.xcodeproj/project.pbxproj +++ b/apps/mobile/apps/client/ios/Runner.xcodeproj/project.pbxproj @@ -14,6 +14,7 @@ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + E8C1A28BFABAEE32FB779C9A /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 221E00B70DE845BE3D50D0A0 /* GoogleService-Info.plist */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -42,6 +43,7 @@ /* Begin PBXFileReference section */ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 221E00B70DE845BE3D50D0A0 /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.plist"; sourceTree = ""; }; 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; @@ -94,6 +96,7 @@ 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, 331C8082294A63A400263BE5 /* RunnerTests */, + 221E00B70DE845BE3D50D0A0 /* GoogleService-Info.plist */, ); sourceTree = ""; }; @@ -216,6 +219,7 @@ 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + E8C1A28BFABAEE32FB779C9A /* GoogleService-Info.plist in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/apps/mobile/apps/client/ios/Runner/GoogleService-Info.plist b/apps/mobile/apps/client/ios/Runner/GoogleService-Info.plist new file mode 100644 index 00000000..fbbfcc69 --- /dev/null +++ b/apps/mobile/apps/client/ios/Runner/GoogleService-Info.plist @@ -0,0 +1,36 @@ + + + + + CLIENT_ID + 933560802882-jqpv1l3gjmi3m87b2gu1iq4lg46lkdfg.apps.googleusercontent.com + REVERSED_CLIENT_ID + com.googleusercontent.apps.933560802882-jqpv1l3gjmi3m87b2gu1iq4lg46lkdfg + ANDROID_CLIENT_ID + 933560802882-ikdfv3o5f47g36qqgvfq55o4m19n7gk4.apps.googleusercontent.com + API_KEY + AIzaSyDyEXkzZAWpXXe4dAesYaZflt5BEtMn9tA + GCM_SENDER_ID + 933560802882 + PLIST_VERSION + 1 + BUNDLE_ID + com.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:d2b6d743608e2a527757db + + \ No newline at end of file diff --git a/apps/mobile/apps/client/lib/firebase_options.dart b/apps/mobile/apps/client/lib/firebase_options.dart new file mode 100644 index 00000000..01fa9ae4 --- /dev/null +++ b/apps/mobile/apps/client/lib/firebase_options.dart @@ -0,0 +1,38 @@ +// File generated by Krow Coding Agent to fix Web Runtime Error. +// Please update the appId for Web with the correct value from Firebase Console. + +import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; +import 'package:flutter/foundation.dart' + show kIsWeb; + +/// Default [FirebaseOptions] for use with your Firebase apps. +/// +/// Example: +/// ```dart +/// import 'firebase_options.dart'; +/// // ... +/// await Firebase.initializeApp( +/// options: DefaultFirebaseOptions.currentPlatform, +/// ); +/// ``` +class DefaultFirebaseOptions { + static FirebaseOptions get currentPlatform { + if (kIsWeb) { + return web; + } + throw UnsupportedError( + 'DefaultFirebaseOptions are not supported for this platform.', + ); + } + + static const FirebaseOptions web = FirebaseOptions( + apiKey: 'AIzaSyBqRtZPMGU-Sz5x5UnRrunKu5NSWYyPRn8', + // TODO: STOP! You must replace this placeholder with the actual Web App ID from your Firebase Console. + // Go to Project Settings -> General -> Your apps -> Web App -> appId + appId: '1:933560802882:web:173a841992885bb27757db', + messagingSenderId: '933560802882', + projectId: 'krow-workforce-dev', + authDomain: 'krow-workforce-dev.firebaseapp.com', + storageBucket: 'krow-workforce-dev.firebasestorage.app', + ); +} diff --git a/apps/mobile/apps/client/lib/main.dart b/apps/mobile/apps/client/lib/main.dart index 8acdd045..362fe8b3 100644 --- a/apps/mobile/apps/client/lib/main.dart +++ b/apps/mobile/apps/client/lib/main.dart @@ -12,11 +12,15 @@ import 'package:client_hubs/client_hubs.dart' as client_hubs; import 'package:client_create_order/client_create_order.dart' as client_create_order; import 'package:firebase_core/firebase_core.dart'; +import 'package:flutter/foundation.dart'; import 'package:krow_core/core.dart'; +import 'firebase_options.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); - await Firebase.initializeApp(); + await Firebase.initializeApp( + options: kIsWeb ? DefaultFirebaseOptions.currentPlatform : null, + ); runApp(ModularApp(module: AppModule(), child: const AppWidget())); } diff --git a/apps/mobile/apps/client/web/index.html b/apps/mobile/apps/client/web/index.html index 0b6cda5a..998286cb 100644 --- a/apps/mobile/apps/client/web/index.html +++ b/apps/mobile/apps/client/web/index.html @@ -33,6 +33,29 @@ + diff --git a/apps/mobile/apps/staff/android/app/google-services.json b/apps/mobile/apps/staff/android/app/google-services.json index 42bb1f02..f4d57e10 100644 --- a/apps/mobile/apps/staff/android/app/google-services.json +++ b/apps/mobile/apps/staff/android/app/google-services.json @@ -5,42 +5,6 @@ "storage_bucket": "krow-workforce-dev.firebasestorage.app" }, "client": [ - { - "client_info": { - "mobilesdk_app_id": "1:933560802882:android:87d41566f8dda41d7757db", - "android_client_info": { - "package_name": "com.example.krow_workforce" - } - }, - "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-dppsapp5i3lsfrlm1mhob2s21peofg1t.apps.googleusercontent.com", - "client_type": 2, - "ios_info": { - "bundle_id": "com.krow.app.staff.dev" - } - } - ] - } - } - }, { "client_info": { "mobilesdk_app_id": "1:933560802882:android:edcddb83ea4bbb517757db", @@ -149,42 +113,6 @@ } } }, - { - "client_info": { - "mobilesdk_app_id": "1:933560802882:android:d26bde4ee337b0b17757db", - "android_client_info": { - "package_name": "com.krowwithus.krow_workforce.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-dppsapp5i3lsfrlm1mhob2s21peofg1t.apps.googleusercontent.com", - "client_type": 2, - "ios_info": { - "bundle_id": "com.krow.app.staff.dev" - } - } - ] - } - } - }, { "client_info": { "mobilesdk_app_id": "1:933560802882:android:1ae05d85c865f77c7757db", diff --git a/apps/mobile/apps/staff/ios/Runner.xcodeproj/project.pbxproj b/apps/mobile/apps/staff/ios/Runner.xcodeproj/project.pbxproj index 1569b385..8243a8b5 100644 --- a/apps/mobile/apps/staff/ios/Runner.xcodeproj/project.pbxproj +++ b/apps/mobile/apps/staff/ios/Runner.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 1E967D034ADA3A16EF82CB3E /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 9F0B07DEC91B141354438F79 /* GoogleService-Info.plist */; }; 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; @@ -55,6 +56,7 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 9F0B07DEC91B141354438F79 /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.plist"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -94,6 +96,7 @@ 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, 331C8082294A63A400263BE5 /* RunnerTests */, + 9F0B07DEC91B141354438F79 /* GoogleService-Info.plist */, ); sourceTree = ""; }; @@ -216,6 +219,7 @@ 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + 1E967D034ADA3A16EF82CB3E /* GoogleService-Info.plist in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/apps/mobile/apps/staff/ios/Runner/GoogleService-Info.plist b/apps/mobile/apps/staff/ios/Runner/GoogleService-Info.plist new file mode 100644 index 00000000..4a0f6b5d --- /dev/null +++ b/apps/mobile/apps/staff/ios/Runner/GoogleService-Info.plist @@ -0,0 +1,36 @@ + + + + + CLIENT_ID + 933560802882-29olj9ku64jbe9h7flinha6hbi8qrluh.apps.googleusercontent.com + REVERSED_CLIENT_ID + com.googleusercontent.apps.933560802882-29olj9ku64jbe9h7flinha6hbi8qrluh + ANDROID_CLIENT_ID + 933560802882-ikdfv3o5f47g36qqgvfq55o4m19n7gk4.apps.googleusercontent.com + API_KEY + AIzaSyDyEXkzZAWpXXe4dAesYaZflt5BEtMn9tA + GCM_SENDER_ID + 933560802882 + PLIST_VERSION + 1 + BUNDLE_ID + com.krowwithus.staff + 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:fa584205b356de937757db + + \ No newline at end of file diff --git a/apps/mobile/apps/staff/lib/firebase_options.dart b/apps/mobile/apps/staff/lib/firebase_options.dart new file mode 100644 index 00000000..6062a65b --- /dev/null +++ b/apps/mobile/apps/staff/lib/firebase_options.dart @@ -0,0 +1,36 @@ +// File generated by Krow Coding Agent to fix Web Runtime Error. +// Please update the appId for Web with the correct value from Firebase Console. + +import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; +import 'package:flutter/foundation.dart' + show kIsWeb; + +/// Default [FirebaseOptions] for use with your Firebase apps. +/// +/// Example: +/// ```dart +/// import 'firebase_options.dart'; +/// // ... +/// await Firebase.initializeApp( +/// options: DefaultFirebaseOptions.currentPlatform, +/// ); +/// ``` +class DefaultFirebaseOptions { + static FirebaseOptions get currentPlatform { + if (kIsWeb) { + return web; + } + throw UnsupportedError( + 'DefaultFirebaseOptions are not supported for this platform.', + ); + } + + static const FirebaseOptions web = FirebaseOptions( + apiKey: 'AIzaSyBqRtZPMGU-Sz5x5UnRrunKu5NSWYyPRn8', + appId: '1:933560802882:web:4508ef1ee6d4e6907757db', + messagingSenderId: '933560802882', + projectId: 'krow-workforce-dev', + authDomain: 'krow-workforce-dev.firebaseapp.com', + storageBucket: 'krow-workforce-dev.firebasestorage.app', + ); +} diff --git a/apps/mobile/apps/staff/lib/main.dart b/apps/mobile/apps/staff/lib/main.dart index f4c215c6..74f7cf77 100644 --- a/apps/mobile/apps/staff/lib/main.dart +++ b/apps/mobile/apps/staff/lib/main.dart @@ -5,14 +5,16 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_modular/flutter_modular.dart'; +import 'package:krowwithus_staff/firebase_options.dart'; import 'package:staff_authentication/staff_authentication.dart' as staff_authentication; import 'package:staff_main/staff_main.dart' as staff_main; -import 'package:krow_core/core.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); - await Firebase.initializeApp(); + await Firebase.initializeApp( + options: DefaultFirebaseOptions.currentPlatform, + ); runApp(ModularApp(module: AppModule(), child: const AppWidget())); } @@ -35,37 +37,33 @@ class AppWidget extends StatelessWidget { @override Widget build(BuildContext context) { - return WebMobileFrame( - appName: 'KROW Staff\nApplication', - logo: Image.asset('assets/logo.png'), - child: BlocProvider( - create: (BuildContext context) => - Modular.get(), - child: - BlocBuilder< - core_localization.LocaleBloc, - core_localization.LocaleState - >( - builder: - (BuildContext context, core_localization.LocaleState state) { - return core_localization.TranslationProvider( - child: MaterialApp.router( - title: "KROW Staff", - theme: UiTheme.light, - routerConfig: Modular.routerConfig, - locale: state.locale, - supportedLocales: state.supportedLocales, - localizationsDelegates: - const >[ - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - ], - ), - ); - }, - ), - ), + return BlocProvider( + create: (BuildContext context) => + Modular.get(), + child: + BlocBuilder< + core_localization.LocaleBloc, + core_localization.LocaleState + >( + builder: + (BuildContext context, core_localization.LocaleState state) { + return core_localization.TranslationProvider( + child: MaterialApp.router( + title: "KROW Staff", + theme: UiTheme.light, + routerConfig: Modular.routerConfig, + locale: state.locale, + supportedLocales: state.supportedLocales, + localizationsDelegates: + const >[ + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + ), + ); + }, + ), ); } } diff --git a/apps/mobile/apps/staff/pubspec.yaml b/apps/mobile/apps/staff/pubspec.yaml index 7b479547..29c204ad 100644 --- a/apps/mobile/apps/staff/pubspec.yaml +++ b/apps/mobile/apps/staff/pubspec.yaml @@ -11,10 +11,7 @@ dependencies: sdk: flutter flutter_localizations: sdk: flutter - cupertino_icons: ^1.0.8 - flutter_modular: ^6.3.0 - - # Architecture Packages + # Architecture Packages design_system: path: ../../packages/design_system core_localization: @@ -27,6 +24,14 @@ dependencies: path: ../../packages/features/staff/availability staff_clock_in: path: ../../packages/features/staff/clock_in + staff_main: + path: ../../packages/features/staff/staff_main + krow_core: + path: ../../packages/core + cupertino_icons: ^1.0.8 + flutter_modular: ^6.3.0 + firebase_core: ^4.4.0 + flutter_bloc: ^8.1.6 dev_dependencies: flutter_test: diff --git a/apps/mobile/apps/staff/web/index.html b/apps/mobile/apps/staff/web/index.html index e03e65a3..ff5fd9b9 100644 --- a/apps/mobile/apps/staff/web/index.html +++ b/apps/mobile/apps/staff/web/index.html @@ -34,5 +34,28 @@ + From 40c43b06cc3dcdd528175689f0219b18e86bac14 Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Sun, 1 Feb 2026 15:50:04 -0500 Subject: [PATCH 02/15] feat: Integrate krow_core package and update WebMobileFrame widget for improved structure --- apps/mobile/apps/client/pubspec.yaml | 2 + apps/mobile/apps/staff/lib/main.dart | 59 ++++----- .../widgets/web_mobile_frame.dart | 114 ++++++++++-------- apps/mobile/packages/core/pubspec.yaml | 2 + 4 files changed, 100 insertions(+), 77 deletions(-) diff --git a/apps/mobile/apps/client/pubspec.yaml b/apps/mobile/apps/client/pubspec.yaml index 2221f485..77f1325a 100644 --- a/apps/mobile/apps/client/pubspec.yaml +++ b/apps/mobile/apps/client/pubspec.yaml @@ -32,6 +32,8 @@ dependencies: path: ../../packages/features/client/hubs client_create_order: path: ../../packages/features/client/create_order + krow_core: + path: ../../packages/core/krow_core cupertino_icons: ^1.0.8 flutter_modular: ^6.3.2 diff --git a/apps/mobile/apps/staff/lib/main.dart b/apps/mobile/apps/staff/lib/main.dart index 74f7cf77..050ae079 100644 --- a/apps/mobile/apps/staff/lib/main.dart +++ b/apps/mobile/apps/staff/lib/main.dart @@ -9,6 +9,7 @@ import 'package:krowwithus_staff/firebase_options.dart'; import 'package:staff_authentication/staff_authentication.dart' as staff_authentication; import 'package:staff_main/staff_main.dart' as staff_main; +import 'package:krow_core/core.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -37,33 +38,37 @@ class AppWidget extends StatelessWidget { @override Widget build(BuildContext context) { - return BlocProvider( - create: (BuildContext context) => - Modular.get(), - child: - BlocBuilder< - core_localization.LocaleBloc, - core_localization.LocaleState - >( - builder: - (BuildContext context, core_localization.LocaleState state) { - return core_localization.TranslationProvider( - child: MaterialApp.router( - title: "KROW Staff", - theme: UiTheme.light, - routerConfig: Modular.routerConfig, - locale: state.locale, - supportedLocales: state.supportedLocales, - localizationsDelegates: - const >[ - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - ], - ), - ); - }, - ), + return WebMobileFrame( + appName: 'KROW Staff\nApplication', + logo: Image.asset('assets/logo.png'), + child: BlocProvider( + create: (BuildContext context) => + Modular.get(), + child: + BlocBuilder< + core_localization.LocaleBloc, + core_localization.LocaleState + >( + builder: + (BuildContext context, core_localization.LocaleState state) { + return core_localization.TranslationProvider( + child: MaterialApp.router( + title: "KROW Staff", + theme: UiTheme.light, + routerConfig: Modular.routerConfig, + locale: state.locale, + supportedLocales: state.supportedLocales, + localizationsDelegates: + const >[ + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + ), + ); + }, + ), + ), ); } } diff --git a/apps/mobile/packages/core/lib/src/presentation/widgets/web_mobile_frame.dart b/apps/mobile/packages/core/lib/src/presentation/widgets/web_mobile_frame.dart index 9c636294..6a98c784 100644 --- a/apps/mobile/packages/core/lib/src/presentation/widgets/web_mobile_frame.dart +++ b/apps/mobile/packages/core/lib/src/presentation/widgets/web_mobile_frame.dart @@ -1,13 +1,11 @@ +import 'package:design_system/design_system.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; /// A wrapper widget that renders the application inside an iPhone-like frame /// specifically for Flutter Web. On other platforms, it simply returns the child. class WebMobileFrame extends StatelessWidget { - final Widget child; - final Widget logo; - final String appName; - const WebMobileFrame({ super.key, required this.child, @@ -15,6 +13,10 @@ class WebMobileFrame extends StatelessWidget { required this.appName, }); + final Widget child; + final Widget logo; + final String appName; + @override Widget build(BuildContext context) { if (!kIsWeb) return child; @@ -22,26 +24,22 @@ class WebMobileFrame extends StatelessWidget { return MaterialApp( debugShowCheckedModeBanner: false, theme: ThemeData.dark(), - home: _WebFrameContent( - logo: logo, - appName: appName, - child: child, - ), + home: _WebFrameContent(logo: logo, appName: appName, child: child), ); } } class _WebFrameContent extends StatefulWidget { - final Widget child; - final Widget logo; - final String appName; - const _WebFrameContent({ required this.child, required this.logo, required this.appName, }); + final Widget child; + final Widget logo; + final String appName; + @override State<_WebFrameContent> createState() => _WebFrameContentState(); } @@ -61,10 +59,10 @@ class _WebFrameContentState extends State<_WebFrameContent> { const double borderThickness = 12.0; return Scaffold( - backgroundColor: const Color(0xFF121212), + backgroundColor: UiColors.foreground, body: MouseRegion( cursor: SystemMouseCursors.none, - onHover: (event) { + onHover: (PointerHoverEvent event) { setState(() { _cursorPosition = event.position; _isHovering = true; @@ -72,7 +70,7 @@ class _WebFrameContentState extends State<_WebFrameContent> { }, onExit: (_) => setState(() => _isHovering = false), child: Stack( - children: [ + children: [ // Logo and Title on the left (Web only) Positioned( left: 60, @@ -84,28 +82,21 @@ class _WebFrameContentState extends State<_WebFrameContent> { child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: 140, - child: widget.logo, - ), + children: [ + SizedBox(width: 140, child: widget.logo), const SizedBox(height: 12), Text( widget.appName, textAlign: TextAlign.left, - style: const TextStyle( - color: Colors.white, - fontSize: 28, - fontWeight: FontWeight.bold, - letterSpacing: -0.5, - fontFamily: 'Instrument Sans', // Fallback if available or system + style: UiTypography.display1b.copyWith( + color: UiColors.white, ), ), const SizedBox(height: 4), Container( height: 2, width: 40, - color: Colors.white.withOpacity(0.3), + color: UiColors.white.withOpacity(0.3), ), ], ), @@ -116,11 +107,11 @@ class _WebFrameContentState extends State<_WebFrameContent> { // Frame and Content Center( child: LayoutBuilder( - builder: (context, constraints) { + builder: (BuildContext context, BoxConstraints constraints) { // Scale down if screen is too small - double scaleX = constraints.maxWidth / (frameWidth + 80); - double scaleY = constraints.maxHeight / (frameHeight + 80); - double scale = (scaleX < 1 || scaleY < 1) + final double scaleX = constraints.maxWidth / (frameWidth + 80); + final double scaleY = constraints.maxHeight / (frameHeight + 80); + final double scale = (scaleX < 1 || scaleY < 1) ? (scaleX < scaleY ? scaleX : scaleY) : 1.0; @@ -130,11 +121,11 @@ class _WebFrameContentState extends State<_WebFrameContent> { width: frameWidth, height: frameHeight, decoration: BoxDecoration( - color: Colors.black, + color: UiColors.black, borderRadius: BorderRadius.circular(borderRadius), - boxShadow: [ + boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.6), + color: UiColors.black.withOpacity(0.6), blurRadius: 40, spreadRadius: 10, ), @@ -149,10 +140,10 @@ class _WebFrameContentState extends State<_WebFrameContent> { borderRadius - borderThickness, ), child: Stack( - children: [ + children: [ // The actual app + status bar Column( - children: [ + children: [ // Mock iOS Status Bar Container( height: 48, @@ -160,26 +151,26 @@ class _WebFrameContentState extends State<_WebFrameContent> { horizontal: 24, ), decoration: const BoxDecoration( - color: Color(0xFFF9F6EE), + color: UiColors.background, border: Border( bottom: BorderSide( - color: Color(0xFFEEEEEE), + color: UiColors.border, width: 0.5, ), ), ), - child: Row( + child: const Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ + children: [ // Time side - const SizedBox( + SizedBox( width: 80, child: Text( '9:41 PM', textAlign: TextAlign.center, style: TextStyle( - color: Colors.black54, + color: UiColors.black, fontWeight: FontWeight.w700, fontSize: 14, letterSpacing: -0.2, @@ -187,27 +178,27 @@ class _WebFrameContentState extends State<_WebFrameContent> { ), ), // Status Icons side - const SizedBox( + SizedBox( width: 80, child: Row( mainAxisAlignment: MainAxisAlignment.end, spacing: 12, - children: [ + children: [ Icon( Icons.signal_cellular_alt, size: 14, - color: Colors.black54, + color: UiColors.black, ), Icon( Icons.wifi, size: 14, - color: Colors.black54, + color: UiColors.black, ), Icon( Icons.battery_full, size: 14, - color: Colors.black54, + color: UiColors.black, ), ], ), @@ -215,7 +206,7 @@ class _WebFrameContentState extends State<_WebFrameContent> { ], ), ), - // The main app content content + // The main app content Expanded(child: widget.child), ], ), @@ -228,7 +219,7 @@ class _WebFrameContentState extends State<_WebFrameContent> { height: 35, margin: const EdgeInsets.only(top: 10), decoration: BoxDecoration( - color: Colors.black, + color: UiColors.black, borderRadius: BorderRadius.circular(20), ), ), @@ -241,6 +232,29 @@ class _WebFrameContentState extends State<_WebFrameContent> { }, ), ), + if (_isHovering) + Positioned( + left: _cursorPosition.dx - 15, + top: _cursorPosition.dy - 15, + child: IgnorePointer( + child: Container( + width: 30, + height: 30, + decoration: BoxDecoration( + color: UiColors.mutedForeground.withOpacity(0.3), + shape: BoxShape.circle, + border: Border.all(color: UiColors.white.withOpacity(0.7), width: 2), + boxShadow: [ + BoxShadow( + color: UiColors.black.withOpacity(0.2), + blurRadius: 4, + spreadRadius: 1, + ), + ], + ), + ), + ), + ), ], ), ), diff --git a/apps/mobile/packages/core/pubspec.yaml b/apps/mobile/packages/core/pubspec.yaml index 1b14ddda..f0a02c12 100644 --- a/apps/mobile/packages/core/pubspec.yaml +++ b/apps/mobile/packages/core/pubspec.yaml @@ -11,3 +11,5 @@ environment: dependencies: flutter: sdk: flutter + design_system: + path: ../design_system From f339b70f30fde2e331f69ade43bbf541ba81dfb1 Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Sun, 1 Feb 2026 15:54:23 -0500 Subject: [PATCH 03/15] feat: Adjust scaling logic for web frame dimensions to improve layout responsiveness --- .../core/lib/src/presentation/widgets/web_mobile_frame.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/mobile/packages/core/lib/src/presentation/widgets/web_mobile_frame.dart b/apps/mobile/packages/core/lib/src/presentation/widgets/web_mobile_frame.dart index 6a98c784..0c5b1d00 100644 --- a/apps/mobile/packages/core/lib/src/presentation/widgets/web_mobile_frame.dart +++ b/apps/mobile/packages/core/lib/src/presentation/widgets/web_mobile_frame.dart @@ -109,8 +109,8 @@ class _WebFrameContentState extends State<_WebFrameContent> { child: LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { // Scale down if screen is too small - final double scaleX = constraints.maxWidth / (frameWidth + 80); - final double scaleY = constraints.maxHeight / (frameHeight + 80); + final double scaleX = constraints.maxWidth / (frameWidth - 150); + final double scaleY = constraints.maxHeight / (frameHeight - 220); final double scale = (scaleX < 1 || scaleY < 1) ? (scaleX < scaleY ? scaleX : scaleY) : 1.0; From 6bd669fec3ce4aba5ef4835459e8a2611f4ddf60 Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Sun, 1 Feb 2026 19:27:07 -0500 Subject: [PATCH 04/15] Fix krow_core path in client pubspec Update the krow_core dependency path in apps/mobile/apps/client/pubspec.yaml from ../../packages/core/krow_core to ../../packages/core to match the package layout (corrects dependency resolution). --- apps/mobile/apps/client/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/mobile/apps/client/pubspec.yaml b/apps/mobile/apps/client/pubspec.yaml index 77f1325a..1d5e21d6 100644 --- a/apps/mobile/apps/client/pubspec.yaml +++ b/apps/mobile/apps/client/pubspec.yaml @@ -33,7 +33,7 @@ dependencies: client_create_order: path: ../../packages/features/client/create_order krow_core: - path: ../../packages/core/krow_core + path: ../../packages/core cupertino_icons: ^1.0.8 flutter_modular: ^6.3.2 From 3ea7d4352fc5dd971784bf3b89e599f90603bca5 Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Sun, 1 Feb 2026 19:53:00 -0500 Subject: [PATCH 05/15] Refactor code structure for improved readability and maintainability --- .../apps/client/lib/firebase_options.dart | 54 +- .../apps/staff/lib/firebase_options.dart | 52 +- docs/QA_TESTING_CHECKLIST.md | 1665 +++++++++++++++++ 3 files changed, 1755 insertions(+), 16 deletions(-) create mode 100644 docs/QA_TESTING_CHECKLIST.md diff --git a/apps/mobile/apps/client/lib/firebase_options.dart b/apps/mobile/apps/client/lib/firebase_options.dart index 01fa9ae4..08a57ddc 100644 --- a/apps/mobile/apps/client/lib/firebase_options.dart +++ b/apps/mobile/apps/client/lib/firebase_options.dart @@ -1,9 +1,8 @@ -// File generated by Krow Coding Agent to fix Web Runtime Error. -// Please update the appId for Web with the correct value from Firebase Console. +// File generated by FlutterFire CLI. import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; import 'package:flutter/foundation.dart' - show kIsWeb; + show defaultTargetPlatform, kIsWeb, TargetPlatform; /// Default [FirebaseOptions] for use with your Firebase apps. /// @@ -20,19 +19,56 @@ class DefaultFirebaseOptions { if (kIsWeb) { return web; } - throw UnsupportedError( - 'DefaultFirebaseOptions are not supported for this platform.', - ); + switch (defaultTargetPlatform) { + case TargetPlatform.android: + return android; + 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.', + ); + default: + throw UnsupportedError( + 'DefaultFirebaseOptions are not supported for this platform.', + ); + } } static const FirebaseOptions web = FirebaseOptions( apiKey: 'AIzaSyBqRtZPMGU-Sz5x5UnRrunKu5NSWYyPRn8', - // TODO: STOP! You must replace this placeholder with the actual Web App ID from your Firebase Console. - // Go to Project Settings -> General -> Your apps -> Web App -> appId - appId: '1:933560802882:web:173a841992885bb27757db', + appId: '1:933560802882:web:173a841992885bb27757db', messagingSenderId: '933560802882', projectId: 'krow-workforce-dev', authDomain: 'krow-workforce-dev.firebaseapp.com', storageBucket: 'krow-workforce-dev.firebasestorage.app', ); + + 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', + ); + + static const FirebaseOptions ios = FirebaseOptions( + apiKey: 'AIzaSyDyEXkzZAWpXXe4dAesYaZflt5BEtMn9tA', + appId: '1:933560802882:ios:d2b6d743608e2a527757db', + messagingSenderId: '933560802882', + projectId: 'krow-workforce-dev', + storageBucket: 'krow-workforce-dev.firebasestorage.app', + iosBundleId: 'com.krowwithus.client', + ); } diff --git a/apps/mobile/apps/staff/lib/firebase_options.dart b/apps/mobile/apps/staff/lib/firebase_options.dart index 6062a65b..343d9f5e 100644 --- a/apps/mobile/apps/staff/lib/firebase_options.dart +++ b/apps/mobile/apps/staff/lib/firebase_options.dart @@ -1,9 +1,8 @@ -// File generated by Krow Coding Agent to fix Web Runtime Error. -// Please update the appId for Web with the correct value from Firebase Console. +// File generated by FlutterFire CLI. import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; import 'package:flutter/foundation.dart' - show kIsWeb; + show defaultTargetPlatform, kIsWeb, TargetPlatform; /// Default [FirebaseOptions] for use with your Firebase apps. /// @@ -20,17 +19,56 @@ class DefaultFirebaseOptions { if (kIsWeb) { return web; } - throw UnsupportedError( - 'DefaultFirebaseOptions are not supported for this platform.', - ); + switch (defaultTargetPlatform) { + case TargetPlatform.android: + return android; + 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.', + ); + default: + throw UnsupportedError( + 'DefaultFirebaseOptions are not supported for this platform.', + ); + } } static const FirebaseOptions web = FirebaseOptions( apiKey: 'AIzaSyBqRtZPMGU-Sz5x5UnRrunKu5NSWYyPRn8', - appId: '1:933560802882:web:4508ef1ee6d4e6907757db', + appId: '1:933560802882:web:4508ef1ee6d4e6907757db', messagingSenderId: '933560802882', projectId: 'krow-workforce-dev', authDomain: 'krow-workforce-dev.firebaseapp.com', storageBucket: 'krow-workforce-dev.firebasestorage.app', ); + + static const FirebaseOptions android = FirebaseOptions( + apiKey: 'AIzaSyDBYhflhK6DThKnS7RM-9raKdvyKzLUjY4', + appId: '1:933560802882:android:d49b8c0f4d19e95e7757db', + messagingSenderId: '933560802882', + projectId: 'krow-workforce-dev', + storageBucket: 'krow-workforce-dev.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', + iosBundleId: 'com.krowwithus.staff', + ); } diff --git a/docs/QA_TESTING_CHECKLIST.md b/docs/QA_TESTING_CHECKLIST.md new file mode 100644 index 00000000..aedccbc1 --- /dev/null +++ b/docs/QA_TESTING_CHECKLIST.md @@ -0,0 +1,1665 @@ +# ๐Ÿงช KROW Workforce Platform - QA Testing Checklist + +**Version:** 1.0 +**Date:** February 1, 2026 +**Coverage:** Client App + Staff App +**Purpose:** Manual QA and Regression Testing + +--- + +## ๐Ÿ“‹ TABLE OF CONTENTS + +1. [Feature-Level QA Checklist](#1๏ธโƒฃ-feature-level-qa-checklist) + - [Client App Features](#client-app-features) + - [Staff App Features](#staff-app-features) +2. [Cross-Application Test Scenarios](#2๏ธโƒฃ-cross-application-test-scenarios) +3. [Shared Infrastructure Validation](#3๏ธโƒฃ-shared-infrastructure-validation) +4. [Regression & Release Checklist](#4๏ธโƒฃ-regression--release-checklist) + +--- + +## 1๏ธโƒฃ FEATURE-LEVEL QA CHECKLIST + +### CLIENT APP FEATURES + +--- + +#### ๐Ÿ“ฑ CLIENT-001: Authentication + +**Applications:** Client +**Entry Points:** +- Launch app โ†’ Get Started โ†’ Sign In +- Launch app โ†’ Get Started โ†’ Sign Up + +**Happy Path Test Cases:** +- [ ] Sign in with valid email and password displays home dashboard +- [ ] Sign up with business details creates account and navigates to home +- [ ] Sign in with Google OAuth completes authentication flow +- [ ] Sign in with Apple OAuth completes authentication flow +- [ ] Session persists after app restart + +**Validation & Error States:** +- [ ] Invalid email format shows validation error +- [ ] Incorrect password shows authentication error +- [ ] Weak password in sign-up shows strength requirements +- [ ] Duplicate email in sign-up shows "already registered" error +- [ ] Empty fields show required field errors +- [ ] Network error displays retry option + +**Loading & Empty States:** +- [ ] Loading spinner displays during authentication +- [ ] OAuth redirect shows appropriate loading state +- [ ] Backend timeout shows error message + +**State Persistence:** +- [ ] Authenticated session persists after app background โ†’ foreground +- [ ] Session expires appropriately after logout +- [ ] Device restart maintains logged-in state + +**Backend Dependency Validation:** +- [ ] `getUserById` returns user data for authenticated UID +- [ ] `createBusiness` successfully creates business entity +- [ ] `createUser` links user to business +- [ ] `getBusinessesByUserId` retrieves business profile +- [ ] Failed business creation triggers `deleteBusiness` rollback + +--- + +#### ๐Ÿ“ฑ CLIENT-002: Home Dashboard + +**Applications:** Client +**Entry Points:** +- Home tab (bottom navigation) + +**Happy Path Test Cases:** +- [ ] Dashboard displays current day coverage widget +- [ ] Spending analytics widget shows correct totals +- [ ] Recent reorders display completed shift roles +- [ ] Quick action buttons navigate to correct features +- [ ] Drag-and-drop widget reordering works correctly +- [ ] Dashboard refreshes on pull-to-refresh gesture + +**Validation & Error States:** +- [ ] Empty state shows "No data available" when no orders exist +- [ ] API error shows retry option +- [ ] Negative spending values display correctly + +**Loading & Empty States:** +- [ ] Skeleton loaders display while fetching data +- [ ] Empty coverage shows "No shifts today" +- [ ] Empty reorders shows "No recent orders" + +**State Persistence:** +- [ ] Widget order persists after app restart +- [ ] Dashboard data refreshes after returning from background + +**Backend Dependency Validation:** +- [ ] `getCompletedShiftsByBusinessId` returns spending data for date ranges +- [ ] `listShiftRolesByBusinessAndDateRange` returns coverage stats +- [ ] `listShiftRolesByBusinessDateRangeCompletedOrders` returns reorder suggestions +- [ ] Business ID correctly retrieved from session + +--- + +#### ๐Ÿ“ฑ CLIENT-003: Create Order + +**Applications:** Client +**Entry Points:** +- Home โ†’ Create Order button +- Orders tab โ†’ + FAB button +- Order type โ†’ Rapid / One-Time / Recurring / Permanent + +**Happy Path Test Cases:** +- [ ] Order type selection displays all available types +- [ ] Hub selection shows list of business hubs +- [ ] Google Places autocomplete suggests valid addresses +- [ ] Role selection displays vendor roles +- [ ] Position quantity can be incremented/decremented (min 1) +- [ ] Date picker displays correct calendar +- [ ] Time pickers show valid time ranges +- [ ] Break duration affects total hours calculation +- [ ] Cost preview calculates correctly (rate ร— positions ร— hours) +- [ ] Order submission creates order, shift, and shift roles +- [ ] Success confirmation displays after submission +- [ ] New order appears in View Orders list + +**Validation & Error States:** +- [ ] Empty hub field shows validation error +- [ ] Empty role field shows validation error +- [ ] Zero positions shows validation error +- [ ] Invalid date (past) shows validation error +- [ ] Start time after end time shows validation error +- [ ] Missing required fields prevent submission +- [ ] Network error during submission shows retry option +- [ ] Backend validation errors display appropriately + +**Loading & Empty States:** +- [ ] Hub list shows "No hubs" if none exist +- [ ] Role list shows "No roles" if none configured +- [ ] Loading spinner displays during submission +- [ ] Submission progress indicator updates + +**State Persistence:** +- [ ] Form data persists when navigating away and back +- [ ] Draft order data clears after successful submission + +**Backend Dependency Validation:** +- [ ] `createOrder` creates order with ONE_TIME type +- [ ] `createShift` creates shift with location and time details +- [ ] `createShiftRole` creates positions with correct rates +- [ ] `updateOrder` links shift to order +- [ ] All operations complete or rollback on failure + +--- + +#### ๐Ÿ“ฑ CLIENT-004: View Orders + +**Applications:** Client +**Entry Points:** +- Orders tab (bottom navigation) + +**Happy Path Test Cases:** +- [ ] Orders list displays orders for selected date +- [ ] Calendar date selection updates order list +- [ ] Each order card shows hub name and address +- [ ] Each order card shows shift time range +- [ ] Each order card shows role positions (filled/total) +- [ ] Each order card shows hourly rate and total cost +- [ ] Accepted applications section displays confirmed staff +- [ ] Staff names and photos display correctly +- [ ] Order list scrolls smoothly with many orders + +**Validation & Error States:** +- [ ] Invalid date selection shows error +- [ ] Network error shows retry option +- [ ] Missing staff data shows placeholder + +**Loading & Empty States:** +- [ ] Skeleton loaders display while fetching orders +- [ ] Empty date shows "No orders for this date" +- [ ] Empty accepted applications shows "No confirmed staff" + +**State Persistence:** +- [ ] Selected date persists after navigating away +- [ ] Order list refreshes after returning from background + +**Backend Dependency Validation:** +- [ ] `listShiftRolesByBusinessAndDateRange` returns orders for date range +- [ ] `listAcceptedApplicationsByBusinessForDay` returns confirmed staff +- [ ] Business ID correctly filtered in queries + +--- + +#### ๐Ÿ“ฑ CLIENT-005: Coverage Monitoring + +**Applications:** Client +**Entry Points:** +- Coverage tab (bottom navigation) + +**Happy Path Test Cases:** +- [ ] Coverage overview displays current date +- [ ] Coverage stats show needed/confirmed/checked-in counts +- [ ] Shift cards display hub name and time range +- [ ] Worker cards show staff name and photo +- [ ] Check-in status indicators update correctly (late, en-route, checked-in) +- [ ] Late workers display with warning indicator +- [ ] Coverage progress bar updates correctly + +**Validation & Error States:** +- [ ] Missing worker photo shows default avatar +- [ ] Network error shows retry option + +**Loading & Empty States:** +- [ ] Skeleton loaders display while fetching data +- [ ] Empty coverage shows "No shifts today" +- [ ] No workers show "No staff assigned" + +**State Persistence:** +- [ ] Coverage data refreshes automatically every X minutes +- [ ] Manual refresh via pull-to-refresh gesture + +**Backend Dependency Validation:** +- [ ] `listShiftRolesByBusinessAndDateRange` returns shift requirements +- [ ] `listStaffsApplicationsByBusinessForDay` returns staff status +- [ ] Attendance status correctly mapped from backend + +--- + +#### ๐Ÿ“ฑ CLIENT-006: Billing & Invoices + +**Applications:** Client +**Entry Points:** +- Billing tab (bottom navigation) + +**Happy Path Test Cases:** +- [ ] Current bill amount displays correctly +- [ ] Pending invoices list shows open invoices +- [ ] Invoice history shows paid invoices +- [ ] Savings amount displays correctly +- [ ] Spending breakdown shows costs by role +- [ ] Period filter (weekly/monthly) updates data +- [ ] Invoice detail view shows line items +- [ ] Invoice PDF download works (if implemented) + +**Validation & Error States:** +- [ ] Zero billing shows $0.00 (not error) +- [ ] Negative savings shows correctly +- [ ] Missing invoice data shows placeholder +- [ ] Network error shows retry option + +**Loading & Empty States:** +- [ ] Skeleton loaders display while fetching data +- [ ] Empty pending invoices shows "No pending invoices" +- [ ] Empty history shows "No invoice history" +- [ ] Empty spending breakdown shows "No spending data" + +**State Persistence:** +- [ ] Selected period persists after navigating away +- [ ] Billing data refreshes after returning from background + +**Backend Dependency Validation:** +- [ ] `listInvoicesByBusinessId` returns invoice records +- [ ] `listShiftRolesByBusinessAndDatesSummary` returns spending aggregates +- [ ] Period date range correctly calculated +- [ ] Business ID correctly filtered + +--- + +#### ๐Ÿ“ฑ CLIENT-007: Hub Management + +**Applications:** Client +**Entry Points:** +- Settings โ†’ Hubs +- Create Order โ†’ Add Hub button + +**Happy Path Test Cases:** +- [ ] Hubs list displays all business hubs +- [ ] Hub cards show name and full address +- [ ] Add hub button opens creation form +- [ ] Google Places autocomplete suggests addresses +- [ ] Address selection auto-fills all address fields +- [ ] Hub name can be customized +- [ ] Hub creation adds to list immediately +- [ ] Hub deletion removes from list (with confirmation) +- [ ] Team entity auto-created for business if missing + +**Validation & Error States:** +- [ ] Empty hub name shows validation error +- [ ] Empty address shows validation error +- [ ] Invalid address format shows error +- [ ] Duplicate hub name shows warning +- [ ] Hub with active orders prevents deletion (validation error) +- [ ] Network error shows retry option + +**Loading & Empty States:** +- [ ] Skeleton loaders display while fetching hubs +- [ ] Empty hubs list shows "No hubs configured" +- [ ] Address autocomplete shows loading during search +- [ ] Hub creation shows loading spinner + +**State Persistence:** +- [ ] Hub list refreshes after creation/deletion +- [ ] Hub data persists across app sessions + +**Backend Dependency Validation:** +- [ ] `getBusinessesByUserId` retrieves business ID +- [ ] `getTeamsByOwnerId` checks for existing team +- [ ] `createTeam` creates team if missing +- [ ] `getTeamHubsByTeamId` fetches hub list +- [ ] `createTeamHub` creates hub with geocoded data +- [ ] `deleteTeamHub` removes hub entity +- [ ] `listOrdersByBusinessAndTeamHub` validates no active orders +- [ ] Google Places API returns valid address components + +--- + +#### ๐Ÿ“ฑ CLIENT-008: Settings + +**Applications:** Client +**Entry Points:** +- Settings (navigation menu) + +**Happy Path Test Cases:** +- [ ] User profile displays name and email +- [ ] Business name displays correctly +- [ ] Hubs link navigates to hub management +- [ ] Sign out logs out user and returns to auth screen + +**Validation & Error States:** +- [ ] Missing profile photo shows default avatar +- [ ] Sign out error shows retry option + +**Loading & Empty States:** +- [ ] Profile data loads on page mount + +**State Persistence:** +- [ ] User data refreshes on page focus + +**Backend Dependency Validation:** +- [ ] Firebase Auth signOut called +- [ ] Session data cleared + +--- + +#### ๐Ÿ“ฑ CLIENT-009: Client Main Navigation + +**Applications:** Client +**Entry Points:** +- Main app shell after authentication + +**Happy Path Test Cases:** +- [ ] Bottom navigation displays 5 tabs (Home, Coverage, Billing, Orders, Reports) +- [ ] Tab selection updates active indicator +- [ ] Tab selection navigates to correct feature +- [ ] Deep links navigate to correct tab +- [ ] Back button navigates correctly within nested routes +- [ ] Tab state persists after device rotation + +**Validation & Error States:** +- [ ] Invalid route shows 404 or redirects to home +- [ ] Reports tab shows placeholder (not yet implemented) + +**Loading & Empty States:** +- [ ] Navigation bar displays immediately +- [ ] Initial tab loads first + +**State Persistence:** +- [ ] Active tab persists after app background โ†’ foreground +- [ ] Tab state resets to home on app restart + +**Backend Dependency Validation:** +- [ ] No direct backend calls (navigation only) + +--- + +### STAFF APP FEATURES + +--- + +#### ๐Ÿ“ฑ STAFF-001: Authentication + +**Applications:** Staff +**Entry Points:** +- Launch app โ†’ Get Started โ†’ Phone Verification + +**Happy Path Test Cases:** +- [ ] Phone number entry accepts valid formats +- [ ] OTP sent confirmation displays +- [ ] OTP verification succeeds with valid code +- [ ] Profile setup wizard displays for new users +- [ ] Authenticated users bypass auth and show home +- [ ] Session persists after app restart + +**Validation & Error States:** +- [ ] Invalid phone format shows validation error +- [ ] Incorrect OTP shows verification error +- [ ] Expired OTP shows re-send option +- [ ] Empty fields show required field errors +- [ ] Network error displays retry option + +**Loading & Empty States:** +- [ ] Loading spinner displays during phone verification +- [ ] OTP input shows countdown timer +- [ ] Profile setup shows progress indicator + +**State Persistence:** +- [ ] Authenticated session persists after app background โ†’ foreground +- [ ] Session expires appropriately after logout + +**Backend Dependency Validation:** +- [ ] Firebase Auth phone verification flow completes +- [ ] `getUserById` returns user data +- [ ] `getStaffByUserId` retrieves staff profile +- [ ] Staff profile created if missing + +--- + +#### ๐Ÿ“ฑ STAFF-002: Home Dashboard + +**Applications:** Staff +**Entry Points:** +- Home tab (bottom navigation) + +**Happy Path Test Cases:** +- [ ] Today's shifts display with time and location +- [ ] Tomorrow's shifts display correctly +- [ ] Recommended shifts show available opportunities +- [ ] Shift cards show role, location, and pay rate +- [ ] Quick actions navigate to correct features +- [ ] Dashboard refreshes on pull-to-refresh + +**Validation & Error States:** +- [ ] Missing shift data shows placeholder +- [ ] Network error shows retry option + +**Loading & Empty States:** +- [ ] Skeleton loaders display while fetching data +- [ ] Empty today's shifts shows "No shifts today" +- [ ] Empty recommended shows "No available shifts" + +**State Persistence:** +- [ ] Dashboard data refreshes after returning from background +- [ ] Shift status updates reflected immediately + +**Backend Dependency Validation:** +- [ ] `getApplicationsByStaffId` fetches staff assignments +- [ ] `listShifts` returns available shifts +- [ ] Date filtering correctly applied + +--- + +#### ๐Ÿ“ฑ STAFF-003: Profile + +**Applications:** Staff +**Entry Points:** +- Profile tab (bottom navigation) + +**Happy Path Test Cases:** +- [ ] Profile displays name, email, phone, and photo +- [ ] Statistics show total shifts, ratings, reliability score +- [ ] Profile sections list displays all sections +- [ ] Section navigation works correctly +- [ ] Sign out logs out user and returns to auth screen + +**Validation & Error States:** +- [ ] Missing profile photo shows default avatar +- [ ] Missing statistics show 0 or default values +- [ ] Sign out error shows retry option + +**Loading & Empty States:** +- [ ] Profile data loads on page mount +- [ ] Statistics display placeholders while loading + +**State Persistence:** +- [ ] Profile data refreshes on page focus +- [ ] Profile updates reflect immediately + +**Backend Dependency Validation:** +- [ ] `getStaffByUserId` retrieves complete staff profile +- [ ] Firebase Auth signOut called +- [ ] Session data cleared + +--- + +#### ๐Ÿ“ฑ STAFF-004: Shifts Management + +**Applications:** Staff +**Entry Points:** +- Shifts tab (bottom navigation) +- Tab navigation: My Shifts / Available / Pending / Cancelled / History + +**Happy Path Test Cases:** +- [ ] My Shifts tab displays assigned shifts +- [ ] Available Shifts tab shows open positions +- [ ] Pending tab shows applications awaiting approval +- [ ] Cancelled tab shows cancelled shifts +- [ ] History tab shows past shifts +- [ ] Shift detail view displays full information +- [ ] Accept shift updates status to confirmed +- [ ] Decline shift updates status to declined +- [ ] Apply for shift creates application +- [ ] Shift cards show time, location, role, and pay + +**Validation & Error States:** +- [ ] Empty tabs show appropriate empty state messages +- [ ] Network error shows retry option +- [ ] Already applied shift prevents duplicate application +- [ ] Past shifts cannot be applied to +- [ ] Cancelled shifts show cancellation reason + +**Loading & Empty States:** +- [ ] Skeleton loaders display while fetching shifts +- [ ] Empty My Shifts shows "No assigned shifts" +- [ ] Empty Available shows "No open shifts" +- [ ] Empty Pending shows "No pending applications" +- [ ] Empty History shows "No past shifts" + +**State Persistence:** +- [ ] Active tab persists after navigating away +- [ ] Shift list refreshes after status changes +- [ ] Shift data refreshes after returning from background + +**Backend Dependency Validation:** +- [ ] `getApplicationsByStaffId` fetches applications by status +- [ ] `getShiftById` retrieves shift details +- [ ] `updateApplicationStatus` changes application state +- [ ] `createApplication` creates new application +- [ ] `deleteApplication` removes application +- [ ] `updateShift` updates filled count + +--- + +#### ๐Ÿ“ฑ STAFF-005: Availability Management + +**Applications:** Staff +**Entry Points:** +- Worker Main โ†’ Availability +- Profile โ†’ Availability section + +**Happy Path Test Cases:** +- [ ] Weekly grid displays Monday-Sunday +- [ ] Time slots (Morning/Afternoon/Evening) toggle correctly +- [ ] Quick-set buttons work (Weekdays/Weekends/All Week) +- [ ] Individual day/slot updates save correctly +- [ ] Green checkmarks indicate availability +- [ ] Gray states indicate unavailability +- [ ] Changes save automatically + +**Validation & Error States:** +- [ ] Network error shows retry option +- [ ] Save failure shows error message + +**Loading & Empty States:** +- [ ] Loading spinner displays while fetching availability +- [ ] Default state shows all unavailable + +**State Persistence:** +- [ ] Availability persists across app sessions +- [ ] Changes reflect immediately in shift matching + +**Backend Dependency Validation:** +- [ ] `getStaffByUserId` retrieves staff ID +- [ ] `listStaffAvailabilitiesByStaffId` fetches availability records +- [ ] `getStaffAvailabilityByKey` checks existing record +- [ ] `updateStaffAvailability` updates existing slot +- [ ] `createStaffAvailability` creates new slot + +--- + +#### ๐Ÿ“ฑ STAFF-006: Clock In/Out + +**Applications:** Staff +**Entry Points:** +- Clock In tab (bottom navigation) + +**Happy Path Test Cases:** +- [ ] Today's shift displays with clock in button +- [ ] Clock in button creates attendance record +- [ ] Clock in time displays correctly +- [ ] Clock out button appears after clocking in +- [ ] Clock out creates end time record +- [ ] Total hours calculated correctly +- [ ] Attendance status updates immediately + +**Validation & Error States:** +- [ ] No shift today shows "No shifts to clock in" +- [ ] Already clocked in prevents duplicate clock in +- [ ] Network error shows retry option +- [ ] Clock in outside shift time shows warning + +**Loading & Empty States:** +- [ ] Loading spinner displays while fetching shift +- [ ] Empty state shows "No shifts scheduled" + +**State Persistence:** +- [ ] Attendance status persists across app sessions +- [ ] Clock in/out times display correctly + +**Backend Dependency Validation:** +- [ ] `getApplicationsByStaffId` fetches today's shifts +- [ ] `createAttendance` records clock in +- [ ] `updateAttendance` records clock out +- [ ] `listAttendancesByApplicationId` gets attendance status +- [ ] `updateApplicationStatus` updates application state + +--- + +#### ๐Ÿ“ฑ STAFF-007: Payments + +**Applications:** Staff +**Entry Points:** +- Payments tab (bottom navigation) + +**Happy Path Test Cases:** +- [ ] Payment summary displays total earnings +- [ ] Payment history lists all transactions +- [ ] Payment cards show amount, date, and status +- [ ] Payment detail view shows breakdown +- [ ] Filter by date range works correctly + +**Validation & Error States:** +- [ ] Zero earnings show $0.00 (not error) +- [ ] Missing payment data shows placeholder +- [ ] Network error shows retry option + +**Loading & Empty States:** +- [ ] Skeleton loaders display while fetching payments +- [ ] Empty history shows "No payment history" + +**State Persistence:** +- [ ] Payment data refreshes after returning from background +- [ ] Filter state persists after navigating away + +**Backend Dependency Validation:** +- [ ] `getStaffByUserId` retrieves staff ID +- [ ] `getPaymentsByStaffId` fetches payment records +- [ ] Mock summary data calculated correctly + +--- + +#### ๐Ÿ“ฑ STAFF-008: Personal Info (Onboarding) + +**Applications:** Staff +**Entry Points:** +- Profile โ†’ Personal Info +- Onboarding wizard + +**Happy Path Test Cases:** +- [ ] Form displays current profile data +- [ ] Name field allows text input +- [ ] Email field validates email format +- [ ] Phone field validates phone format +- [ ] Photo upload works correctly +- [ ] Preferred locations multi-select works +- [ ] Save button updates profile + +**Validation & Error States:** +- [ ] Empty required fields show validation errors +- [ ] Invalid email format shows error +- [ ] Invalid phone format shows error +- [ ] Network error shows retry option +- [ ] Photo upload failure shows error + +**Loading & Empty States:** +- [ ] Form loads with skeleton placeholders +- [ ] Photo upload shows progress indicator +- [ ] Save button shows loading spinner + +**State Persistence:** +- [ ] Changes persist after save +- [ ] Unsaved changes show confirmation dialog on exit + +**Backend Dependency Validation:** +- [ ] `getStaffByUserId` fetches profile +- [ ] `updateStaff` saves profile changes + +--- + +#### ๐Ÿ“ฑ STAFF-009: Emergency Contact (Onboarding) + +**Applications:** Staff +**Entry Points:** +- Profile โ†’ Emergency Contact +- Onboarding wizard + +**Happy Path Test Cases:** +- [ ] Contact list displays all contacts +- [ ] Add contact button opens form +- [ ] Contact form validates name and phone +- [ ] Relationship dropdown shows options (Family/Spouse/Friend/Other) +- [ ] Remove contact deletes from list +- [ ] Save updates all contacts +- [ ] Multiple contacts supported + +**Validation & Error States:** +- [ ] Empty name shows validation error +- [ ] Invalid phone format shows error +- [ ] At least one contact required (if applicable) +- [ ] Network error shows retry option + +**Loading & Empty States:** +- [ ] Loading spinner displays while fetching contacts +- [ ] Empty state shows "No emergency contacts" +- [ ] Save button shows loading spinner + +**State Persistence:** +- [ ] Contacts persist after save +- [ ] Unsaved changes show confirmation dialog on exit + +**Backend Dependency Validation:** +- [ ] `getStaffByUserId` retrieves staff ID +- [ ] `getEmergencyContactsByStaffId` fetches contacts +- [ ] `deleteEmergencyContact` removes contacts (replace-all pattern) +- [ ] `createEmergencyContact` creates new contacts + +--- + +#### ๐Ÿ“ฑ STAFF-010: Experience & Skills (Onboarding) + +**Applications:** Staff +**Entry Points:** +- Profile โ†’ Experience +- Onboarding wizard + +**Happy Path Test Cases:** +- [ ] Industries multi-select displays options +- [ ] Skills multi-select displays options +- [ ] Selected items show checkmarks +- [ ] Deselection removes items +- [ ] Save updates profile + +**Validation & Error States:** +- [ ] At least one industry required (if applicable) +- [ ] At least one skill required (if applicable) +- [ ] Network error shows retry option + +**Loading & Empty States:** +- [ ] Loading spinner displays while fetching data +- [ ] Save button shows loading spinner + +**State Persistence:** +- [ ] Selections persist after save +- [ ] Unsaved changes show confirmation dialog on exit + +**Backend Dependency Validation:** +- [ ] `getStaffByUserId` fetches profile with industries and skills +- [ ] `updateStaff` updates industries and skills arrays + +--- + +#### ๐Ÿ“ฑ STAFF-011: Attire Selection (Onboarding) + +**Applications:** Staff +**Entry Points:** +- Profile โ†’ Attire +- Onboarding wizard + +**Happy Path Test Cases:** +- [ ] Attire options list displays all items +- [ ] Item selection toggles checkmark +- [ ] Photo upload button opens camera/gallery +- [ ] Photos display in grid +- [ ] Save updates selections and photos + +**Validation & Error States:** +- [ ] At least one attire item required (if applicable) +- [ ] Photo upload failure shows error +- [ ] Network error shows retry option + +**Loading & Empty States:** +- [ ] Loading spinner displays while fetching options +- [ ] Photo upload shows progress indicator +- [ ] Save button shows loading spinner + +**State Persistence:** +- [ ] Selections and photos persist after save +- [ ] Unsaved changes show confirmation dialog on exit + +**Backend Dependency Validation:** +- [ ] `listAttireOptions` fetches available items +- [ ] Photo upload and save mutations (pending implementation) + +--- + +#### ๐Ÿ“ฑ STAFF-012: Bank Account (Finances) + +**Applications:** Staff +**Entry Points:** +- Profile โ†’ Bank Account + +**Happy Path Test Cases:** +- [ ] Account list displays all accounts +- [ ] Add account button opens form +- [ ] Form validates routing and account numbers +- [ ] Account type dropdown shows options (Checking/Savings) +- [ ] First account auto-sets as primary +- [ ] Save adds account to list +- [ ] Primary account indicator displays + +**Validation & Error States:** +- [ ] Empty routing number shows validation error +- [ ] Invalid routing number format shows error +- [ ] Empty account number shows validation error +- [ ] Invalid account number format shows error +- [ ] Duplicate account shows warning +- [ ] Network error shows retry option + +**Loading & Empty States:** +- [ ] Loading spinner displays while fetching accounts +- [ ] Empty state shows "No bank accounts" +- [ ] Save button shows loading spinner + +**State Persistence:** +- [ ] Accounts persist after save +- [ ] Account list refreshes after addition + +**Backend Dependency Validation:** +- [ ] `getAccountsByOwnerId` fetches staff accounts +- [ ] `createAccount` creates new account +- [ ] First account auto-flagged as primary + +--- + +#### ๐Ÿ“ฑ STAFF-013: Time Card History (Finances) + +**Applications:** Staff +**Entry Points:** +- Profile โ†’ Time Card + +**Happy Path Test Cases:** +- [ ] Time card list displays all records +- [ ] Each card shows shift details (date, time, location) +- [ ] Each card shows clock in/out times +- [ ] Each card shows total hours worked +- [ ] Scrolling loads more records (pagination) + +**Validation & Error States:** +- [ ] Missing attendance data shows "Not recorded" +- [ ] Network error shows retry option + +**Loading & Empty States:** +- [ ] Skeleton loaders display while fetching records +- [ ] Empty state shows "No time card history" + +**State Persistence:** +- [ ] Time card data refreshes after returning from background + +**Backend Dependency Validation:** +- [ ] `getStaffByUserId` retrieves staff ID +- [ ] `getApplicationsByStaffId` fetches applications with attendance +- [ ] Attendance records mapped to time card format + +--- + +#### ๐Ÿ“ฑ STAFF-014: Tax Forms (Compliance) + +**Applications:** Staff +**Entry Points:** +- Profile โ†’ Tax Forms + +**Happy Path Test Cases:** +- [ ] Forms list displays required forms (I-9, W-4) +- [ ] Form status shows completed/incomplete +- [ ] I-9 form opens editor +- [ ] I-9 form validates all fields +- [ ] W-4 form opens editor +- [ ] W-4 form validates all fields +- [ ] Form submission updates status to completed +- [ ] Completed forms show edit option + +**Validation & Error States:** +- [ ] Empty required fields show validation errors +- [ ] Invalid SSN format shows error +- [ ] Invalid date format shows error +- [ ] Signature required validation +- [ ] Network error shows retry option + +**Loading & Empty States:** +- [ ] Loading spinner displays while fetching forms +- [ ] Form editor loads with skeleton placeholders +- [ ] Save button shows loading spinner + +**State Persistence:** +- [ ] Form data persists after save +- [ ] Unsaved changes show confirmation dialog on exit +- [ ] Form status updates immediately + +**Backend Dependency Validation:** +- [ ] `getTaxFormsByStaffId` fetches forms +- [ ] `createTaxForm` initializes missing forms +- [ ] `updateTaxForm` saves form data and status + +--- + +#### ๐Ÿ“ฑ STAFF-015: Documents (Compliance) + +**Applications:** Staff +**Entry Points:** +- Profile โ†’ Documents + +**Happy Path Test Cases:** +- [ ] Documents list displays required documents +- [ ] Document status shows verified/pending/expired +- [ ] Document detail view shows requirements +- [ ] Expiry dates display correctly +- [ ] Expired documents highlight in red + +**Validation & Error States:** +- [ ] Missing documents show incomplete status +- [ ] Network error shows retry option + +**Loading & Empty States:** +- [ ] Skeleton loaders display while fetching documents +- [ ] Empty state shows "No documents required" + +**State Persistence:** +- [ ] Document data refreshes after returning from background + +**Backend Dependency Validation:** +- [ ] Mock implementation currently +- [ ] โš ๏ธ Requires clarification: Real Data Connect integration pending + +--- + +#### ๐Ÿ“ฑ STAFF-016: Certificates (Compliance) + +**Applications:** Staff +**Entry Points:** +- Profile โ†’ Certificates + +**Happy Path Test Cases:** +- [ ] Certificates list displays all certificates +- [ ] Certificate cards show name, status, and expiry +- [ ] Certificate detail view shows full information +- [ ] Expired certificates highlight in red +- [ ] Certificate verification status displays + +**Validation & Error States:** +- [ ] Missing certificates show placeholder +- [ ] Network error shows retry option + +**Loading & Empty States:** +- [ ] Skeleton loaders display while fetching certificates +- [ ] Empty state shows "No certificates" + +**State Persistence:** +- [ ] Certificate data refreshes after returning from background + +**Backend Dependency Validation:** +- [ ] `listStaffDocumentsByStaffId` fetches certificate documents +- [ ] Document data mapped to certificate entities + +--- + +#### ๐Ÿ“ฑ STAFF-017: Staff Main Navigation + +**Applications:** Staff +**Entry Points:** +- Main app shell after authentication + +**Happy Path Test Cases:** +- [ ] Bottom navigation displays 5 tabs (Shifts, Payments, Home, Clock In, Profile) +- [ ] Tab selection updates active indicator +- [ ] Tab selection navigates to correct feature +- [ ] Deep links navigate to correct tab and nested route +- [ ] Back button navigates correctly within nested routes +- [ ] Tab state persists after device rotation +- [ ] Nested routes (onboarding, emergency-contact, etc.) accessible + +**Validation & Error States:** +- [ ] Invalid route shows 404 or redirects to home +- [ ] Navigation errors log appropriately + +**Loading & Empty States:** +- [ ] Navigation bar displays immediately +- [ ] Initial tab loads first + +**State Persistence:** +- [ ] Active tab persists after app background โ†’ foreground +- [ ] Tab state resets to home on app restart + +**Backend Dependency Validation:** +- [ ] No direct backend calls (navigation only) + +--- + +## 2๏ธโƒฃ CROSS-APPLICATION TEST SCENARIOS + +### Scenario 1: Order Creation โ†’ Staff Application Flow + +**Preconditions:** +- Client user authenticated +- Staff user authenticated +- At least one hub configured + +**Steps:** +1. **CLIENT APP:** + - [ ] Create one-time order with specific hub, role, date, and time + - [ ] Verify order appears in View Orders list + - [ ] Verify shift shows as unfilled (0/X positions) + +2. **STAFF APP:** + - [ ] Open Shifts tab โ†’ Available tab + - [ ] Verify new shift appears in available list + - [ ] Verify shift details match order (hub, role, time, pay) + - [ ] Apply for shift position + +3. **CLIENT APP:** + - [ ] Refresh View Orders + - [ ] Verify shift shows pending application (0/X filled, pending) + +4. **STAFF APP:** + - [ ] Verify application appears in Pending tab + - [ ] Verify shift removed from Available tab + +**Expected Results:** +- โœ… Order created in Client appears in Staff Available Shifts +- โœ… Application in Staff shows pending in both apps +- โœ… Shift counts update correctly in real-time + +--- + +### Scenario 2: Shift Acceptance โ†’ Coverage Tracking + +**Preconditions:** +- Scenario 1 completed (pending application exists) + +**Steps:** +1. **STAFF APP:** + - [ ] Go to Shifts โ†’ Pending tab + - [ ] Accept pending shift assignment + +2. **CLIENT APP:** + - [ ] Refresh View Orders + - [ ] Verify shift shows as filled (1/X positions) + - [ ] Verify staff name and photo appear in accepted applications + - [ ] Navigate to Coverage tab + - [ ] Verify shift appears with assigned staff + +3. **STAFF APP:** + - [ ] Verify shift moved from Pending to My Shifts tab + - [ ] Verify shift appears on Home dashboard + +**Expected Results:** +- โœ… Accepted shift reflects in Client orders immediately +- โœ… Staff appears in Coverage monitoring +- โœ… Shift moves to My Shifts in Staff app + +--- + +### Scenario 3: Clock In โ†’ Real-Time Coverage Update + +**Preconditions:** +- Scenario 2 completed (staff has accepted shift) +- Current date/time is during shift window + +**Steps:** +1. **STAFF APP:** + - [ ] Navigate to Clock In tab + - [ ] Verify today's shift displays + - [ ] Click Clock In button + - [ ] Verify clock in time recorded + +2. **CLIENT APP:** + - [ ] Navigate to Coverage tab + - [ ] Verify staff status changed to "Checked In" + - [ ] Verify check-in time displays + - [ ] Verify coverage stats updated (checked-in count incremented) + +3. **STAFF APP:** + - [ ] Wait until shift end time + - [ ] Click Clock Out button + - [ ] Verify clock out time recorded + +4. **CLIENT APP:** + - [ ] Refresh Coverage tab + - [ ] Verify staff status changed to "Completed" + +5. **STAFF APP:** + - [ ] Navigate to Time Card + - [ ] Verify attendance record appears with correct times and hours + +**Expected Results:** +- โœ… Clock in updates Coverage status in Client +- โœ… Clock out completes attendance record +- โœ… Time card displays correct hours in Staff app +- โœ… Coverage monitoring reflects real-time status + +--- + +### Scenario 4: Hub Creation โ†’ Order Placement + +**Preconditions:** +- Client user authenticated +- No existing hubs + +**Steps:** +1. **CLIENT APP:** + - [ ] Navigate to Settings โ†’ Hubs + - [ ] Verify empty state "No hubs configured" + - [ ] Click Add Hub button + - [ ] Enter hub name and use Google Places autocomplete + - [ ] Select address from suggestions + - [ ] Verify address fields auto-filled + - [ ] Save hub + +2. **CLIENT APP:** + - [ ] Navigate to Create Order + - [ ] Verify new hub appears in hub selection list + - [ ] Select new hub and complete order creation + +3. **STAFF APP:** + - [ ] Navigate to Shifts โ†’ Available + - [ ] Verify shift shows correct hub name and address + +**Expected Results:** +- โœ… Hub created in Settings appears in order creation +- โœ… Hub address propagates to shift details in Staff app + +--- + +### Scenario 5: Shift Cancellation โ†’ Staff Notification + +**Preconditions:** +- Staff has accepted shift assignment + +**Steps:** +1. **CLIENT APP:** + - [ ] Navigate to View Orders + - [ ] Select order with assigned staff + - [ ] Cancel shift (if feature exists) or delete order + +2. **STAFF APP:** + - [ ] Refresh Shifts tab + - [ ] Verify shift moved to Cancelled tab + - [ ] Verify shift removed from My Shifts + - [ ] Verify cancellation reason displays + +3. **STAFF APP:** + - [ ] Verify shift removed from Home dashboard + +**Expected Results:** +- โœ… Cancelled shift moves to Cancelled tab +- โœ… Shift removed from active assignments +- โš ๏ธ **Requires clarification:** Cancellation feature may not be fully implemented + +--- + +### Scenario 6: Authentication State Sharing + +**Preconditions:** +- Neither app authenticated + +**Steps:** +1. **CLIENT APP:** + - [ ] Sign in with email/password + - [ ] Verify Firebase Auth token generated + +2. **STAFF APP:** + - [ ] Launch app + - [ ] Verify Staff app requires separate authentication + - [ ] Verify Client session does not carry over + +3. **CLIENT APP:** + - [ ] Sign out + +4. **STAFF APP:** + - [ ] Verify Staff app session persists (independent) + +**Expected Results:** +- โœ… Client and Staff apps maintain independent auth sessions +- โœ… Signing out of one app does not affect the other + +--- + +### Scenario 7: Data Created in Client โ†’ Visible in Staff + +**Preconditions:** +- Client creates multiple orders + +**Steps:** +1. **CLIENT APP:** + - [ ] Create 5 orders on different dates + - [ ] Create 3 orders on same date with different hubs + +2. **STAFF APP:** + - [ ] Navigate to Shifts โ†’ Available + - [ ] Verify all 8 shifts appear + - [ ] Verify date grouping correct + - [ ] Verify hub addresses correct + - [ ] Apply for 2 shifts + +3. **CLIENT APP:** + - [ ] Navigate to View Orders + - [ ] Verify 2 shifts show pending applications + - [ ] Navigate to Coverage + - [ ] Verify 0 checked-in (pending acceptance) + +**Expected Results:** +- โœ… All orders visible in both apps +- โœ… Application states sync correctly +- โœ… Data consistency maintained across apps + +--- + +### Scenario 8: Role-Based Access Differences + +**Preconditions:** +- Client user authenticated +- Staff user authenticated + +**Steps:** +1. **CLIENT APP:** + - [ ] Navigate to Billing + - [ ] Verify billing data displays (Client-only feature) + - [ ] Navigate to Create Order + - [ ] Verify order creation available (Client-only feature) + +2. **STAFF APP:** + - [ ] Verify no Billing tab exists + - [ ] Verify no Create Order feature + - [ ] Navigate to Availability + - [ ] Verify availability editing available (Staff-only feature) + +3. **CLIENT APP:** + - [ ] Verify no Availability feature exists + - [ ] Verify no Clock In feature exists + +**Expected Results:** +- โœ… Client app has business management features (orders, billing, hubs) +- โœ… Staff app has worker features (availability, clock in, payments) +- โœ… No feature overlap or unauthorized access + +--- + +### Scenario 9: Race Condition - Concurrent Shift Application + +**Preconditions:** +- One available shift with 1 position +- Two staff users authenticated on separate devices + +**Steps:** +1. **STAFF APP (Device 1):** + - [ ] Navigate to Shifts โ†’ Available + - [ ] View shift details + +2. **STAFF APP (Device 2):** + - [ ] Navigate to Shifts โ†’ Available + - [ ] View same shift details + +3. **STAFF APP (Device 1):** + - [ ] Apply for shift + - [ ] Verify application created + +4. **STAFF APP (Device 2):** + - [ ] Attempt to apply for same shift + - [ ] Verify appropriate behavior (position filled message or pending status) + +5. **CLIENT APP:** + - [ ] Navigate to View Orders + - [ ] Verify only 1 application shows (not 2) + - [ ] Accept Device 1 application + +6. **STAFF APP (Device 2):** + - [ ] Refresh Available shifts + - [ ] Verify shift removed or shows as filled + +**Expected Results:** +- โœ… Only first application succeeds (or both go to pending) +- โœ… No double-booking occurs +- โœ… Race condition handled gracefully +- โš ๏ธ **Requires clarification:** Backend concurrency control behavior + +--- + +### Scenario 10: Network Failure During Critical Operation + +**Preconditions:** +- Staff has pending shift application + +**Steps:** +1. **STAFF APP:** + - [ ] Navigate to Shifts โ†’ Pending + - [ ] Disable network connection + - [ ] Attempt to accept shift + - [ ] Verify offline error message displays + - [ ] Re-enable network + - [ ] Retry accept shift + - [ ] Verify acceptance succeeds + +2. **CLIENT APP:** + - [ ] Verify shift shows as filled after network restored + +**Expected Results:** +- โœ… Offline state handled gracefully with clear messaging +- โœ… Retry succeeds after network restored +- โœ… Data consistency maintained + +--- + +## 3๏ธโƒฃ SHARED INFRASTRUCTURE VALIDATION + +### Domain Entity Consistency + +#### Test: Entity Field Validation + +- [ ] **Staff Entity:** + - [ ] Verify all required fields populate (id, userId, firstName, lastName, email, phone) + - [ ] Verify optional fields handle null correctly (photoUrl, preferredLocations) + - [ ] Verify enum fields map correctly (UserStatus) + +- [ ] **Order Entity:** + - [ ] Verify all required fields populate + - [ ] Verify OrderStatus enum maps correctly + - [ ] Verify OrderType enum maps correctly + +- [ ] **Shift Entity:** + - [ ] Verify date/time fields parse correctly + - [ ] Verify ShiftStatus enum maps correctly + - [ ] Verify location data (hub) links correctly + +- [ ] **Application Entity:** + - [ ] Verify ApplicationStatus enum maps correctly + - [ ] Verify relationships (staff, shift, role) link correctly + +- [ ] **Invoice Entity:** + - [ ] Verify amount calculations correct + - [ ] Verify date fields parse correctly + - [ ] Verify InvoiceStatus enum maps correctly + +- [ ] **Hub Entity:** + - [ ] Verify address components parse correctly + - [ ] Verify geocoding (lat/lng) present and valid + - [ ] Verify placeId populated + +--- + +### Data Connect Schema Alignment + +#### Test: Backend Operation Contracts + +- [ ] **User Operations:** + - [ ] `getUserById(userId)` returns expected fields + - [ ] `createUser(...)` accepts all required parameters + - [ ] `updateUser(...)` updates only specified fields + +- [ ] **Staff Operations:** + - [ ] `getStaffByUserId(userId)` returns staff profile + - [ ] `updateStaff(...)` updates specified fields + - [ ] `listStaffs()` returns paginated results + +- [ ] **Order Operations:** + - [ ] `createOrder(...)` creates order with shifts + - [ ] `listOrdersByBusinessId(...)` filters by business correctly + - [ ] `updateOrder(...)` updates order fields + +- [ ] **Shift Operations:** + - [ ] `createShift(...)` creates shift with location + - [ ] `getShiftById(id)` returns full shift details + - [ ] `listShiftRolesByBusinessAndDateRange(...)` returns correct date range + +- [ ] **Application Operations:** + - [ ] `createApplication(...)` creates pending application + - [ ] `updateApplicationStatus(...)` changes status correctly + - [ ] `getApplicationsByStaffId(...)` filters by staff and date + +- [ ] **Attendance Operations:** + - [ ] `createAttendance(...)` records clock in + - [ ] `updateAttendance(...)` records clock out + - [ ] `listAttendancesByApplicationId(...)` returns attendance records + +- [ ] **Hub Operations:** + - [ ] `createTeamHub(...)` creates hub with location data + - [ ] `getTeamHubsByTeamId(...)` returns hubs for team + - [ ] `deleteTeamHub(id)` removes hub entity + +--- + +### Error Handling Consistency + +#### Test: Standard Error Patterns + +- [ ] **Network Errors:** + - [ ] All features show "Network error" message + - [ ] All features show "Retry" button + - [ ] Retry button re-attempts operation + +- [ ] **Authentication Errors:** + - [ ] Expired token redirects to login + - [ ] Invalid credentials show appropriate message + - [ ] Auth failures log out user + +- [ ] **Validation Errors:** + - [ ] Field-level validation shows inline errors + - [ ] Form-level validation prevents submission + - [ ] Error messages are user-friendly + +- [ ] **Backend Errors:** + - [ ] 400 errors show validation details + - [ ] 404 errors show "Not found" message + - [ ] 500 errors show "Server error, try again" message + +- [ ] **Data Not Found:** + - [ ] Empty lists show appropriate empty state + - [ ] Missing entities show "Not found" message + - [ ] Deleted entities handle gracefully + +--- + +### Version Mismatch Tolerance + +#### Test: App Version Compatibility + +- [ ] **Client App Updated, Staff App Not:** + - [ ] Backend operations remain compatible + - [ ] Shared domain entities parse correctly + - [ ] New fields in Client don't break Staff + +- [ ] **Staff App Updated, Client App Not:** + - [ ] Backend operations remain compatible + - [ ] Shared domain entities parse correctly + - [ ] New fields in Staff don't break Client + +- [ ] **Backend Schema Updated:** + - [ ] Apps handle new optional fields gracefully + - [ ] Apps ignore unknown fields + - [ ] Required fields validated correctly + +--- + +## 4๏ธโƒฃ REGRESSION & RELEASE CHECKLIST + +### Smoke Testing (Critical Path) + +#### Authentication Flow (5 minutes) + +- [ ] **Client App:** + - [ ] Launch app shows Get Started screen + - [ ] Sign in with valid credentials succeeds + - [ ] Home dashboard displays + +- [ ] **Staff App:** + - [ ] Launch app shows Get Started screen + - [ ] Phone verification sends OTP + - [ ] OTP verification succeeds + - [ ] Home dashboard displays + +#### Order Creation & Application (10 minutes) + +- [ ] **Client App:** + - [ ] Create one-time order succeeds + - [ ] Order appears in View Orders list + - [ ] Order details display correctly + +- [ ] **Staff App:** + - [ ] Available shift appears in Shifts tab + - [ ] Apply for shift succeeds + - [ ] Application appears in Pending tab + +- [ ] **Client App:** + - [ ] Pending application displays in View Orders + - [ ] Coverage shows staff as pending + +#### Clock In/Out Flow (5 minutes) + +- [ ] **Staff App:** + - [ ] Accept shift from Pending tab + - [ ] Clock in on Clock In tab + - [ ] Clock in time recorded + +- [ ] **Client App:** + - [ ] Coverage shows staff as checked in + - [ ] Staff status updates in real-time + +- [ ] **Staff App:** + - [ ] Clock out succeeds + - [ ] Time card displays attendance record + +--- + +### Critical Path Validation (Must Pass Before Release) + +#### Client App Critical Features + +- [ ] **Authentication:** + - [ ] Sign in with email/password works + - [ ] Session persists after restart + +- [ ] **Order Management:** + - [ ] Create order succeeds + - [ ] View orders displays correctly + - [ ] Order details accurate + +- [ ] **Coverage Monitoring:** + - [ ] Coverage stats display correctly + - [ ] Staff status updates reflect backend + +- [ ] **Billing:** + - [ ] Invoice list displays + - [ ] Spending breakdown calculates correctly + +#### Staff App Critical Features + +- [ ] **Authentication:** + - [ ] Phone verification works + - [ ] Session persists after restart + +- [ ] **Shift Management:** + - [ ] Available shifts display + - [ ] Apply for shift succeeds + - [ ] Accept shift succeeds + - [ ] My Shifts displays assigned shifts + +- [ ] **Clock In/Out:** + - [ ] Clock in records attendance + - [ ] Clock out completes record + +- [ ] **Profile:** + - [ ] View profile displays data + - [ ] Update personal info succeeds + +--- + +### High-Risk Features (Require Extra Scrutiny) + +#### Payment Processing + +- [ ] **Staff App:** + - [ ] Payment history displays correctly + - [ ] Payment amounts accurate + - [ ] No double-payment scenarios + +- [ ] **Client App:** + - [ ] Invoice amounts correct + - [ ] Billing calculations accurate + - [ ] No overcharging scenarios + +#### Data Integrity + +- [ ] **Order โ†’ Shift โ†’ Application Chain:** + - [ ] Order creation creates shifts + - [ ] Shift deletion cascades correctly + - [ ] Application deletion updates shift counts + +- [ ] **Attendance Records:** + - [ ] Clock in/out times accurate + - [ ] Hours calculation correct + - [ ] No duplicate attendance records + +#### Concurrency Issues + +- [ ] **Multiple Staff Applying:** + - [ ] Race condition handled correctly + - [ ] No double-booking + - [ ] First-come-first-served logic works + +- [ ] **Shift Cancellation:** + - [ ] Staff notified appropriately + - [ ] Applications updated correctly + - [ ] No orphaned assignments + +--- + +### Release-Blocking Failures + +**The following issues MUST be fixed before release:** + +- [ ] **Authentication fails completely** (users cannot log in) +- [ ] **Order creation fails completely** (clients cannot create orders) +- [ ] **Shift application fails completely** (staff cannot apply for shifts) +- [ ] **Clock in/out fails completely** (staff cannot track attendance) +- [ ] **Payment data displays incorrectly** (financial inaccuracies) +- [ ] **Data loss occurs** (orders, shifts, or applications deleted unintentionally) +- [ ] **App crashes on launch** (unrecoverable error) +- [ ] **Backend connection fails** (cannot communicate with Data Connect) +- [ ] **Critical security vulnerability** (unauthorized access, data exposure) + +--- + +## ๐Ÿ“Š TESTING METRICS & REPORTING + +### Test Execution Summary + +**Date:** __________ +**Tester:** __________ +**Build Version:** __________ + +| Category | Total Tests | Passed | Failed | Blocked | Pass Rate | +|----------|-------------|--------|--------|---------|-----------| +| Client Features | __ | __ | __ | __ | __% | +| Staff Features | __ | __ | __ | __ | __% | +| Cross-App Scenarios | __ | __ | __ | __ | __% | +| Infrastructure | __ | __ | __ | __ | __% | +| Smoke Tests | __ | __ | __ | __ | __% | +| **TOTAL** | **__** | **__** | **__** | **__** | **__%** | + +--- + +### Defect Severity Classification + +**Critical (P0):** Release-blocking, affects core functionality +**High (P1):** Major functionality broken, workaround exists +**Medium (P2):** Minor functionality affected, low impact +**Low (P3):** Cosmetic issue, no functional impact + +--- + +### Sign-Off Criteria + +**Release can proceed when:** +- [ ] All P0 defects resolved +- [ ] 95%+ pass rate on Critical Path tests +- [ ] 85%+ pass rate on all Feature tests +- [ ] No unresolved P1 defects in core features +- [ ] Cross-app scenarios pass 90%+ +- [ ] Backend integration stable (no frequent failures) +- [ ] QA lead approval obtained +- [ ] Product owner approval obtained + +--- + +## ๐Ÿ“ NOTES & CLARIFICATIONS NEEDED + +The following items require clarification before full QA execution: + +1. โš ๏ธ **Documents Feature (STAFF-015):** Real Data Connect integration status unclear. Currently using mock implementation. + +2. โš ๏ธ **Shift Cancellation:** Feature existence and behavior not confirmed in current implementation. + +3. โš ๏ธ **Race Condition Handling (Scenario 9):** Backend concurrency control mechanism needs documentation. + +4. โš ๏ธ **Payment Processing:** End-to-end payment flow from shift completion to payment disbursement not fully implemented. + +5. โš ๏ธ **NFC Tag Assignment:** Hub NFC functionality interface exists but implementation status unclear. + +6. โš ๏ธ **Recurring & Permanent Orders:** Placeholder screens exist but full workflow not implemented. + +7. โš ๏ธ **Reports Feature (Client):** Currently shows placeholder, implementation status unknown. + +8. โš ๏ธ **Notification System:** Push notifications for shift assignments, cancellations, and status updates not covered in current analysis. + +--- + +## ๐ŸŽฏ CONCLUSION + +This QA checklist provides comprehensive coverage of all implemented features across both Client and Staff applications. It is designed for manual testing by QA engineers and supports release sign-off decisions based on structured test execution and clear pass/fail criteria. + +**Key Strengths:** +- โœ… Feature-by-feature detailed test cases +- โœ… Cross-application integration scenarios +- โœ… Infrastructure and data consistency validation +- โœ… Clear release-blocking criteria +- โœ… Based on actual implemented code (not speculative) + +**Recommended Usage:** +1. Execute smoke tests before each build +2. Run full feature regression weekly +3. Execute cross-app scenarios before major releases +4. Validate infrastructure after backend schema updates +5. Use sign-off checklist for release go/no-go decisions + +--- + +**Document Maintainer:** KROW QA Team +**Last Updated:** February 1, 2026 +**Next Review:** Upon next major feature release From f8bd19ec52da67506efda61ff99c8cb0b9338960 Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Sun, 1 Feb 2026 19:56:02 -0500 Subject: [PATCH 06/15] Add Mermaid diagram rendering and styles Enable rendering of Mermaid diagrams embedded in markdown on the launchpad page. Adds CSS (.mermaid-diagram-wrapper and svg rules) to ensure responsive, centered diagrams and updates the markdown loader to find code.language-mermaid blocks, render them via mermaid.render, and replace the code block with a styled wrapper. Includes error handling that logs rendering errors and shows a user-facing error box. Changes are localized to internal/launchpad/index.html. --- internal/launchpad/index.html | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/internal/launchpad/index.html b/internal/launchpad/index.html index 55c7fb9e..c8dbdcc2 100644 --- a/internal/launchpad/index.html +++ b/internal/launchpad/index.html @@ -146,6 +146,17 @@ .markdown-content th { background-color: #f9fafb; font-weight: 600; } .markdown-content img { max-width: 100%; height: auto; border-radius: 0.5em; margin: 1em 0; } .markdown-content hr { border: none; border-top: 2px solid #e5e7eb; margin: 2em 0; } + + /* Mermaid diagram styling */ + .mermaid-diagram-wrapper { + display: flex; + justify-content: center; + align-items: center; + } + .mermaid-diagram-wrapper svg { + max-width: 100%; + height: auto; + } /* Loading Overlay */ #auth-loading { @@ -821,6 +832,28 @@ const markdownText = await response.text(); const htmlContent = marked.parse(markdownText); documentContainer.innerHTML = htmlContent; + + // Render Mermaid diagrams embedded in the markdown + const mermaidBlocks = documentContainer.querySelectorAll('code.language-mermaid'); + for (let i = 0; i < mermaidBlocks.length; i++) { + const block = mermaidBlocks[i]; + const mermaidCode = block.textContent; + const pre = block.parentElement; + + try { + const { svg } = await mermaid.render(`mermaid-doc-${Date.now()}-${i}`, mermaidCode); + const wrapper = document.createElement('div'); + wrapper.className = 'mermaid-diagram-wrapper bg-white p-4 rounded-lg border border-gray-200 my-4 overflow-x-auto'; + wrapper.innerHTML = svg; + pre.replaceWith(wrapper); + } catch (err) { + console.error('Mermaid rendering error:', err); + const errorDiv = document.createElement('div'); + errorDiv.className = 'bg-red-50 border border-red-200 rounded-lg p-4 my-4'; + errorDiv.innerHTML = `

Mermaid Error: ${err.message}

`; + pre.replaceWith(errorDiv); + } + } } catch (error) { console.error('Error loading document:', error); documentContainer.innerHTML = ` From 5c15db1695d2e8e80188c01a0ccae33092681c68 Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Sun, 1 Feb 2026 20:22:14 -0500 Subject: [PATCH 07/15] Integrate Google Maps Places Autocomplete for hub address validation and enhance UI button styles --- .../lib/src/widgets/ui_button.dart | 68 +++++++++++++- .../presentation/widgets/reorder_widget.dart | 76 ++++++--------- docs/QA_TESTING_CHECKLIST.md | 92 ++++--------------- 3 files changed, 111 insertions(+), 125 deletions(-) diff --git a/apps/mobile/packages/design_system/lib/src/widgets/ui_button.dart b/apps/mobile/packages/design_system/lib/src/widgets/ui_button.dart index 7867798c..d2dc3abb 100644 --- a/apps/mobile/packages/design_system/lib/src/widgets/ui_button.dart +++ b/apps/mobile/packages/design_system/lib/src/widgets/ui_button.dart @@ -132,10 +132,14 @@ class UiButton extends StatelessWidget { @override /// Builds the button UI. Widget build(BuildContext context) { + final ButtonStyle? mergedStyle = style != null + ? _getSizeStyle().merge(style) + : _getSizeStyle(); + final Widget button = buttonBuilder( context, onPressed, - style, + mergedStyle, _buildButtonContent(), ); @@ -146,6 +150,65 @@ class UiButton extends StatelessWidget { return button; } + /// Gets the style based on the button size. + ButtonStyle _getSizeStyle() { + switch (size) { + case UiButtonSize.extraSmall: + return ButtonStyle( + padding: WidgetStateProperty.all( + const EdgeInsets.symmetric( + horizontal: UiConstants.space2, + vertical: UiConstants.space1, + ), + ), + minimumSize: WidgetStateProperty.all(const Size(0, 28)), + maximumSize: WidgetStateProperty.all(const Size(double.infinity, 28)), + textStyle: WidgetStateProperty.all( + const TextStyle(fontSize: 12, fontWeight: FontWeight.w500), + ), + ); + case UiButtonSize.small: + return ButtonStyle( + padding: WidgetStateProperty.all( + const EdgeInsets.symmetric( + horizontal: UiConstants.space3, + vertical: UiConstants.space2, + ), + ), + minimumSize: WidgetStateProperty.all(const Size(0, 36)), + maximumSize: WidgetStateProperty.all(const Size(double.infinity, 36)), + textStyle: WidgetStateProperty.all( + const TextStyle(fontSize: 13, fontWeight: FontWeight.w500), + ), + ); + case UiButtonSize.medium: + return ButtonStyle( + padding: WidgetStateProperty.all( + const EdgeInsets.symmetric( + horizontal: UiConstants.space4, + vertical: UiConstants.space3, + ), + ), + minimumSize: WidgetStateProperty.all(const Size(0, 44)), + maximumSize: WidgetStateProperty.all(const Size(double.infinity, 44)), + ); + case UiButtonSize.large: + return ButtonStyle( + padding: WidgetStateProperty.all( + const EdgeInsets.symmetric( + horizontal: UiConstants.space6, + vertical: UiConstants.space4, + ), + ), + minimumSize: WidgetStateProperty.all(const Size(0, 52)), + maximumSize: WidgetStateProperty.all(const Size(double.infinity, 52)), + textStyle: WidgetStateProperty.all( + const TextStyle(fontSize: 16, fontWeight: FontWeight.w600), + ), + ); + } + } + /// Builds the button content with optional leading and trailing icons. Widget _buildButtonContent() { if (child != null) { @@ -229,6 +292,9 @@ class UiButton extends StatelessWidget { /// Defines the size of a [UiButton]. enum UiButtonSize { + /// Extra small button (very compact) + extraSmall, + /// Small button (compact) small, diff --git a/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/reorder_widget.dart b/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/reorder_widget.dart index fe9274b8..1dfa8353 100644 --- a/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/reorder_widget.dart +++ b/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/reorder_widget.dart @@ -43,14 +43,11 @@ class ReorderWidget extends StatelessWidget { ), if (subtitle != null) ...[ const SizedBox(height: UiConstants.space1), - Text( - subtitle!, - style: UiTypography.body2r.textSecondary, - ), + Text(subtitle!, style: UiTypography.body2r.textSecondary), ], const SizedBox(height: UiConstants.space2), SizedBox( - height: 140, + height: 164, child: ListView.separated( scrollDirection: Axis.horizontal, itemCount: recentOrders.length, @@ -67,13 +64,7 @@ class ReorderWidget extends StatelessWidget { decoration: BoxDecoration( color: UiColors.white, borderRadius: UiConstants.radiusLg, - border: Border.all(color: UiColors.border), - boxShadow: [ - BoxShadow( - color: UiColors.black.withValues(alpha: 0.02), - blurRadius: 4, - ), - ], + border: Border.all(color: UiColors.border, width: 0.6), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -129,10 +120,7 @@ class ReorderWidget extends StatelessWidget { style: UiTypography.body1b, ), Text( - i18n.per_hr( - amount: order.hourlyRate.toString(), - ) + - ' ยท ${order.hours}h', + '${i18n.per_hr(amount: order.hourlyRate.toString())} ยท ${order.hours}h', style: UiTypography.footnote2r.textSecondary, ), ], @@ -145,49 +133,37 @@ class ReorderWidget extends StatelessWidget { _Badge( icon: UiIcons.success, text: order.type, - color: const Color(0xFF2563EB), - bg: const Color(0xFF2563EB), - textColor: UiColors.white, + color: UiColors.primary, + bg: UiColors.buttonSecondaryStill, + textColor: UiColors.primary, ), const SizedBox(width: UiConstants.space2), _Badge( icon: UiIcons.building, text: '${order.workers}', - color: const Color(0xFF334155), - bg: const Color(0xFFF1F5F9), - textColor: const Color(0xFF334155), + color: UiColors.textSecondary, + bg: UiColors.buttonSecondaryStill, + textColor: UiColors.textSecondary, ), ], ), const Spacer(), - SizedBox( - height: 28, - width: double.infinity, - child: ElevatedButton.icon( - onPressed: () => onReorderPressed({ - 'orderId': order.orderId, - 'title': order.title, - 'location': order.location, - 'hourlyRate': order.hourlyRate, - 'hours': order.hours, - 'workers': order.workers, - 'type': order.type, - }), - style: ElevatedButton.styleFrom( - backgroundColor: UiColors.primary, - foregroundColor: UiColors.white, - padding: EdgeInsets.zero, - shape: RoundedRectangleBorder( - borderRadius: UiConstants.radiusMd, - ), - elevation: 0, - ), - icon: const Icon(UiIcons.zap, size: 12), - label: Text( - i18n.reorder_button, - style: UiTypography.footnote1m, - ), - ), + + UiButton.secondary( + size: UiButtonSize.small, + text: i18n.reorder_button, + leadingIcon: UiIcons.zap, + iconSize: 12, + fullWidth: true, + onPressed: () => onReorderPressed({ + 'orderId': order.orderId, + 'title': order.title, + 'location': order.location, + 'hourlyRate': order.hourlyRate, + 'hours': order.hours, + 'workers': order.workers, + 'type': order.type, + }), ), ], ), diff --git a/docs/QA_TESTING_CHECKLIST.md b/docs/QA_TESTING_CHECKLIST.md index aedccbc1..ff272efa 100644 --- a/docs/QA_TESTING_CHECKLIST.md +++ b/docs/QA_TESTING_CHECKLIST.md @@ -34,9 +34,6 @@ **Happy Path Test Cases:** - [ ] Sign in with valid email and password displays home dashboard - [ ] Sign up with business details creates account and navigates to home -- [ ] Sign in with Google OAuth completes authentication flow -- [ ] Sign in with Apple OAuth completes authentication flow -- [ ] Session persists after app restart **Validation & Error States:** - [ ] Invalid email format shows validation error @@ -44,24 +41,10 @@ - [ ] Weak password in sign-up shows strength requirements - [ ] Duplicate email in sign-up shows "already registered" error - [ ] Empty fields show required field errors -- [ ] Network error displays retry option **Loading & Empty States:** - [ ] Loading spinner displays during authentication - [ ] OAuth redirect shows appropriate loading state -- [ ] Backend timeout shows error message - -**State Persistence:** -- [ ] Authenticated session persists after app background โ†’ foreground -- [ ] Session expires appropriately after logout -- [ ] Device restart maintains logged-in state - -**Backend Dependency Validation:** -- [ ] `getUserById` returns user data for authenticated UID -- [ ] `createBusiness` successfully creates business entity -- [ ] `createUser` links user to business -- [ ] `getBusinessesByUserId` retrieves business profile -- [ ] Failed business creation triggers `deleteBusiness` rollback --- @@ -77,28 +60,14 @@ - [ ] Recent reorders display completed shift roles - [ ] Quick action buttons navigate to correct features - [ ] Drag-and-drop widget reordering works correctly -- [ ] Dashboard refreshes on pull-to-refresh gesture **Validation & Error States:** - [ ] Empty state shows "No data available" when no orders exist -- [ ] API error shows retry option -- [ ] Negative spending values display correctly **Loading & Empty States:** -- [ ] Skeleton loaders display while fetching data - [ ] Empty coverage shows "No shifts today" - [ ] Empty reorders shows "No recent orders" -**State Persistence:** -- [ ] Widget order persists after app restart -- [ ] Dashboard data refreshes after returning from background - -**Backend Dependency Validation:** -- [ ] `getCompletedShiftsByBusinessId` returns spending data for date ranges -- [ ] `listShiftRolesByBusinessAndDateRange` returns coverage stats -- [ ] `listShiftRolesByBusinessDateRangeCompletedOrders` returns reorder suggestions -- [ ] Business ID correctly retrieved from session - --- #### ๐Ÿ“ฑ CLIENT-003: Create Order @@ -107,12 +76,11 @@ **Entry Points:** - Home โ†’ Create Order button - Orders tab โ†’ + FAB button -- Order type โ†’ Rapid / One-Time / Recurring / Permanent +- Order type โ†’ One-Time **Happy Path Test Cases:** -- [ ] Order type selection displays all available types +- [ ] Order type selection displays. - [ ] Hub selection shows list of business hubs -- [ ] Google Places autocomplete suggests valid addresses - [ ] Role selection displays vendor roles - [ ] Position quantity can be incremented/decremented (min 1) - [ ] Date picker displays correct calendar @@ -130,7 +98,6 @@ - [ ] Invalid date (past) shows validation error - [ ] Start time after end time shows validation error - [ ] Missing required fields prevent submission -- [ ] Network error during submission shows retry option - [ ] Backend validation errors display appropriately **Loading & Empty States:** @@ -139,17 +106,6 @@ - [ ] Loading spinner displays during submission - [ ] Submission progress indicator updates -**State Persistence:** -- [ ] Form data persists when navigating away and back -- [ ] Draft order data clears after successful submission - -**Backend Dependency Validation:** -- [ ] `createOrder` creates order with ONE_TIME type -- [ ] `createShift` creates shift with location and time details -- [ ] `createShiftRole` creates positions with correct rates -- [ ] `updateOrder` links shift to order -- [ ] All operations complete or rollback on failure - --- #### ๐Ÿ“ฑ CLIENT-004: View Orders @@ -171,23 +127,13 @@ **Validation & Error States:** - [ ] Invalid date selection shows error -- [ ] Network error shows retry option + - [ ] Missing staff data shows placeholder **Loading & Empty States:** -- [ ] Skeleton loaders display while fetching orders - [ ] Empty date shows "No orders for this date" - [ ] Empty accepted applications shows "No confirmed staff" -**State Persistence:** -- [ ] Selected date persists after navigating away -- [ ] Order list refreshes after returning from background - -**Backend Dependency Validation:** -- [ ] `listShiftRolesByBusinessAndDateRange` returns orders for date range -- [ ] `listAcceptedApplicationsByBusinessForDay` returns confirmed staff -- [ ] Business ID correctly filtered in queries - --- #### ๐Ÿ“ฑ CLIENT-005: Coverage Monitoring @@ -207,7 +153,6 @@ **Validation & Error States:** - [ ] Missing worker photo shows default avatar -- [ ] Network error shows retry option **Loading & Empty States:** - [ ] Skeleton loaders display while fetching data @@ -245,7 +190,6 @@ - [ ] Zero billing shows $0.00 (not error) - [ ] Negative savings shows correctly - [ ] Missing invoice data shows placeholder -- [ ] Network error shows retry option **Loading & Empty States:** - [ ] Skeleton loaders display while fetching data @@ -289,7 +233,7 @@ - [ ] Invalid address format shows error - [ ] Duplicate hub name shows warning - [ ] Hub with active orders prevents deletion (validation error) -- [ ] Network error shows retry option + **Loading & Empty States:** - [ ] Skeleton loaders display while fetching hubs @@ -430,7 +374,7 @@ **Validation & Error States:** - [ ] Missing shift data shows placeholder -- [ ] Network error shows retry option + **Loading & Empty States:** - [ ] Skeleton loaders display while fetching data @@ -502,7 +446,7 @@ **Validation & Error States:** - [ ] Empty tabs show appropriate empty state messages -- [ ] Network error shows retry option + - [ ] Already applied shift prevents duplicate application - [ ] Past shifts cannot be applied to - [ ] Cancelled shifts show cancellation reason @@ -546,7 +490,7 @@ - [ ] Changes save automatically **Validation & Error States:** -- [ ] Network error shows retry option + - [ ] Save failure shows error message **Loading & Empty States:** @@ -584,7 +528,7 @@ **Validation & Error States:** - [ ] No shift today shows "No shifts to clock in" - [ ] Already clocked in prevents duplicate clock in -- [ ] Network error shows retry option + - [ ] Clock in outside shift time shows warning **Loading & Empty States:** @@ -620,7 +564,7 @@ **Validation & Error States:** - [ ] Zero earnings show $0.00 (not error) - [ ] Missing payment data shows placeholder -- [ ] Network error shows retry option + **Loading & Empty States:** - [ ] Skeleton loaders display while fetching payments @@ -657,7 +601,7 @@ - [ ] Empty required fields show validation errors - [ ] Invalid email format shows error - [ ] Invalid phone format shows error -- [ ] Network error shows retry option + - [ ] Photo upload failure shows error **Loading & Empty States:** @@ -695,7 +639,7 @@ - [ ] Empty name shows validation error - [ ] Invalid phone format shows error - [ ] At least one contact required (if applicable) -- [ ] Network error shows retry option + **Loading & Empty States:** - [ ] Loading spinner displays while fetching contacts @@ -731,7 +675,7 @@ **Validation & Error States:** - [ ] At least one industry required (if applicable) - [ ] At least one skill required (if applicable) -- [ ] Network error shows retry option + **Loading & Empty States:** - [ ] Loading spinner displays while fetching data @@ -764,7 +708,7 @@ **Validation & Error States:** - [ ] At least one attire item required (if applicable) - [ ] Photo upload failure shows error -- [ ] Network error shows retry option + **Loading & Empty States:** - [ ] Loading spinner displays while fetching options @@ -802,7 +746,7 @@ - [ ] Empty account number shows validation error - [ ] Invalid account number format shows error - [ ] Duplicate account shows warning -- [ ] Network error shows retry option + **Loading & Empty States:** - [ ] Loading spinner displays while fetching accounts @@ -835,7 +779,7 @@ **Validation & Error States:** - [ ] Missing attendance data shows "Not recorded" -- [ ] Network error shows retry option + **Loading & Empty States:** - [ ] Skeleton loaders display while fetching records @@ -872,7 +816,7 @@ - [ ] Invalid SSN format shows error - [ ] Invalid date format shows error - [ ] Signature required validation -- [ ] Network error shows retry option + **Loading & Empty States:** - [ ] Loading spinner displays while fetching forms @@ -906,7 +850,7 @@ **Validation & Error States:** - [ ] Missing documents show incomplete status -- [ ] Network error shows retry option + **Loading & Empty States:** - [ ] Skeleton loaders display while fetching documents @@ -936,7 +880,7 @@ **Validation & Error States:** - [ ] Missing certificates show placeholder -- [ ] Network error shows retry option + **Loading & Empty States:** - [ ] Skeleton loaders display while fetching certificates From 109ed5375d861ba8c8dba118c97ba8a6e1fb9e5a Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Sun, 1 Feb 2026 20:28:29 -0500 Subject: [PATCH 08/15] Refactor UI components: update button size to large and adjust profile header margin --- .../packages/design_system/lib/src/ui_theme.dart | 3 ++- .../design_system/lib/src/widgets/ui_button.dart | 10 +++++----- .../client_settings_page/settings_profile_header.dart | 2 +- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/apps/mobile/packages/design_system/lib/src/ui_theme.dart b/apps/mobile/packages/design_system/lib/src/ui_theme.dart index ae97f1b6..2b098529 100644 --- a/apps/mobile/packages/design_system/lib/src/ui_theme.dart +++ b/apps/mobile/packages/design_system/lib/src/ui_theme.dart @@ -1,7 +1,8 @@ import 'package:flutter/material.dart'; + import 'ui_colors.dart'; -import 'ui_typography.dart'; import 'ui_constants.dart'; +import 'ui_typography.dart'; /// The main entry point for the Staff Design System theme. /// Assembles colors, typography, and constants into a comprehensive Material 3 theme. diff --git a/apps/mobile/packages/design_system/lib/src/widgets/ui_button.dart b/apps/mobile/packages/design_system/lib/src/widgets/ui_button.dart index d2dc3abb..68f16e49 100644 --- a/apps/mobile/packages/design_system/lib/src/widgets/ui_button.dart +++ b/apps/mobile/packages/design_system/lib/src/widgets/ui_button.dart @@ -50,7 +50,7 @@ class UiButton extends StatelessWidget { this.trailingIcon, this.style, this.iconSize = 20, - this.size = UiButtonSize.medium, + this.size = UiButtonSize.large, this.fullWidth = false, }) : assert( text != null || child != null, @@ -67,7 +67,7 @@ class UiButton extends StatelessWidget { this.trailingIcon, this.style, this.iconSize = 20, - this.size = UiButtonSize.medium, + this.size = UiButtonSize.large, this.fullWidth = false, }) : buttonBuilder = _elevatedButtonBuilder, assert( @@ -85,7 +85,7 @@ class UiButton extends StatelessWidget { this.trailingIcon, this.style, this.iconSize = 20, - this.size = UiButtonSize.medium, + this.size = UiButtonSize.large, this.fullWidth = false, }) : buttonBuilder = _outlinedButtonBuilder, assert( @@ -103,7 +103,7 @@ class UiButton extends StatelessWidget { this.trailingIcon, this.style, this.iconSize = 20, - this.size = UiButtonSize.medium, + this.size = UiButtonSize.large, this.fullWidth = false, }) : buttonBuilder = _textButtonBuilder, assert( @@ -121,7 +121,7 @@ class UiButton extends StatelessWidget { this.trailingIcon, this.style, this.iconSize = 20, - this.size = UiButtonSize.medium, + this.size = UiButtonSize.large, this.fullWidth = false, }) : buttonBuilder = _textButtonBuilder, assert( diff --git a/apps/mobile/packages/features/client/settings/lib/src/presentation/widgets/client_settings_page/settings_profile_header.dart b/apps/mobile/packages/features/client/settings/lib/src/presentation/widgets/client_settings_page/settings_profile_header.dart index f644caf3..5d4deac1 100644 --- a/apps/mobile/packages/features/client/settings/lib/src/presentation/widgets/client_settings_page/settings_profile_header.dart +++ b/apps/mobile/packages/features/client/settings/lib/src/presentation/widgets/client_settings_page/settings_profile_header.dart @@ -35,7 +35,7 @@ class SettingsProfileHeader extends StatelessWidget { flexibleSpace: FlexibleSpaceBar( background: Container( padding: const EdgeInsets.symmetric(horizontal: UiConstants.space8), - margin: const EdgeInsets.only(top: UiConstants.space16), + margin: const EdgeInsets.only(top: UiConstants.space24), child: Row( crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.start, From 08b96cea6f0f3d4e61b107ae160a3232186f8266 Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Sun, 1 Feb 2026 21:05:14 -0500 Subject: [PATCH 09/15] feat: Introduce CoverageWorker entity and update coverage repository - Added CoverageWorker entity to represent worker status and check-in information. - Updated CoverageRepositoryImpl to utilize CoverageWorker and its status. - Removed legacy coverage_entities.dart file and replaced references with krow_domain imports. - Enhanced coverage statistics calculations based on new worker status logic. - Updated UI components to reflect changes in worker status representation. - Modified acceptance of shifts to align with new status definitions. - Cleaned up QA testing checklist to remove outdated items and clarify requirements. --- .../packages/domain/lib/krow_domain.dart | 5 + .../coverage_domain/coverage_shift.dart | 57 ++ .../coverage_domain/coverage_stats.dart | 45 ++ .../coverage_domain/coverage_worker.dart | 55 ++ .../coverage_repository_impl.dart | 51 +- .../repositories/coverage_repository.dart | 2 +- .../domain/ui_entities/coverage_entities.dart | 133 ---- .../usecases/get_coverage_stats_usecase.dart | 3 +- .../usecases/get_shifts_for_date_usecase.dart | 2 +- .../src/presentation/blocs/coverage_bloc.dart | 2 +- .../presentation/blocs/coverage_state.dart | 2 +- .../widgets/coverage_quick_stats.dart | 2 +- .../widgets/coverage_shift_list.dart | 121 ++- .../shifts_repository_impl.dart | 2 +- docs/QA_TESTING_CHECKLIST.md | 703 +----------------- 15 files changed, 288 insertions(+), 897 deletions(-) create mode 100644 apps/mobile/packages/domain/lib/src/entities/coverage_domain/coverage_shift.dart create mode 100644 apps/mobile/packages/domain/lib/src/entities/coverage_domain/coverage_stats.dart create mode 100644 apps/mobile/packages/domain/lib/src/entities/coverage_domain/coverage_worker.dart delete mode 100644 apps/mobile/packages/features/client/client_coverage/lib/src/domain/ui_entities/coverage_entities.dart diff --git a/apps/mobile/packages/domain/lib/krow_domain.dart b/apps/mobile/packages/domain/lib/krow_domain.dart index fc763e64..3dc41679 100644 --- a/apps/mobile/packages/domain/lib/krow_domain.dart +++ b/apps/mobile/packages/domain/lib/krow_domain.dart @@ -87,6 +87,11 @@ export 'src/adapters/clock_in/clock_in_adapter.dart'; export 'src/entities/availability/availability_slot.dart'; export 'src/entities/availability/day_availability.dart'; +// Coverage +export 'src/entities/coverage_domain/coverage_shift.dart'; +export 'src/entities/coverage_domain/coverage_worker.dart'; +export 'src/entities/coverage_domain/coverage_stats.dart'; + // Adapters export 'src/adapters/profile/emergency_contact_adapter.dart'; export 'src/adapters/profile/experience_adapter.dart'; diff --git a/apps/mobile/packages/domain/lib/src/entities/coverage_domain/coverage_shift.dart b/apps/mobile/packages/domain/lib/src/entities/coverage_domain/coverage_shift.dart new file mode 100644 index 00000000..afc10d60 --- /dev/null +++ b/apps/mobile/packages/domain/lib/src/entities/coverage_domain/coverage_shift.dart @@ -0,0 +1,57 @@ +import 'package:equatable/equatable.dart'; +import 'coverage_worker.dart'; + +/// Domain entity representing a shift in the coverage view. +/// +/// This is a feature-specific domain entity that encapsulates shift information +/// including scheduling details and assigned workers. +class CoverageShift extends Equatable { + /// Creates a [CoverageShift]. + const CoverageShift({ + required this.id, + required this.title, + required this.location, + required this.startTime, + required this.workersNeeded, + required this.date, + required this.workers, + }); + + /// The unique identifier for the shift. + final String id; + + /// The title or role of the shift. + final String title; + + /// The location where the shift takes place. + final String location; + + /// The start time of the shift (e.g., "16:00"). + final String startTime; + + /// The number of workers needed for this shift. + final int workersNeeded; + + /// The date of the shift. + final DateTime date; + + /// The list of workers assigned to this shift. + final List workers; + + /// Calculates the coverage percentage for this shift. + int get coveragePercent { + if (workersNeeded == 0) return 0; + return ((workers.length / workersNeeded) * 100).round(); + } + + @override + List get props => [ + id, + title, + location, + startTime, + workersNeeded, + date, + workers, + ]; +} diff --git a/apps/mobile/packages/domain/lib/src/entities/coverage_domain/coverage_stats.dart b/apps/mobile/packages/domain/lib/src/entities/coverage_domain/coverage_stats.dart new file mode 100644 index 00000000..580116a9 --- /dev/null +++ b/apps/mobile/packages/domain/lib/src/entities/coverage_domain/coverage_stats.dart @@ -0,0 +1,45 @@ +import 'package:equatable/equatable.dart'; + +/// Domain entity representing coverage statistics. +/// +/// Aggregates coverage metrics for a specific date. +class CoverageStats extends Equatable { + /// Creates a [CoverageStats]. + const CoverageStats({ + required this.totalNeeded, + required this.totalConfirmed, + required this.checkedIn, + required this.enRoute, + required this.late, + }); + + /// The total number of workers needed. + final int totalNeeded; + + /// The total number of confirmed workers. + final int totalConfirmed; + + /// The number of workers who have checked in. + final int checkedIn; + + /// The number of workers en route. + final int enRoute; + + /// The number of late workers. + final int late; + + /// Calculates the overall coverage percentage. + int get coveragePercent { + if (totalNeeded == 0) return 0; + return ((totalConfirmed / totalNeeded) * 100).round(); + } + + @override + List get props => [ + totalNeeded, + totalConfirmed, + checkedIn, + enRoute, + late, + ]; +} diff --git a/apps/mobile/packages/domain/lib/src/entities/coverage_domain/coverage_worker.dart b/apps/mobile/packages/domain/lib/src/entities/coverage_domain/coverage_worker.dart new file mode 100644 index 00000000..3ade4d9d --- /dev/null +++ b/apps/mobile/packages/domain/lib/src/entities/coverage_domain/coverage_worker.dart @@ -0,0 +1,55 @@ +import 'package:equatable/equatable.dart'; + +/// Worker status enum matching ApplicationStatus from Data Connect. +enum CoverageWorkerStatus { + /// Application is pending approval. + pending, + + /// Application has been accepted. + accepted, + + /// Application has been rejected. + rejected, + + /// Worker has confirmed attendance. + confirmed, + + /// Worker has checked in. + checkedIn, + + /// Worker has checked out. + checkedOut, + + /// Worker is late. + late, + + /// Worker did not show up. + noShow, + + /// Shift is completed. + completed, +} + +/// Domain entity representing a worker in the coverage view. +/// +/// This entity tracks worker status including check-in information. +class CoverageWorker extends Equatable { + /// Creates a [CoverageWorker]. + const CoverageWorker({ + required this.name, + required this.status, + this.checkInTime, + }); + + /// The name of the worker. + final String name; + + /// The status of the worker. + final CoverageWorkerStatus status; + + /// The time the worker checked in, if applicable. + final String? checkInTime; + + @override + List get props => [name, status, checkInTime]; +} diff --git a/apps/mobile/packages/features/client/client_coverage/lib/src/data/repositories_impl/coverage_repository_impl.dart b/apps/mobile/packages/features/client/client_coverage/lib/src/data/repositories_impl/coverage_repository_impl.dart index 305f65e9..9ee38782 100644 --- a/apps/mobile/packages/features/client/client_coverage/lib/src/data/repositories_impl/coverage_repository_impl.dart +++ b/apps/mobile/packages/features/client/client_coverage/lib/src/data/repositories_impl/coverage_repository_impl.dart @@ -1,7 +1,7 @@ import 'package:firebase_data_connect/firebase_data_connect.dart' as fdc; import 'package:krow_data_connect/krow_data_connect.dart' as dc; +import 'package:krow_domain/krow_domain.dart'; import '../../domain/repositories/coverage_repository.dart'; -import '../../domain/ui_entities/coverage_entities.dart'; /// Implementation of [CoverageRepository] in the Data layer. /// @@ -25,18 +25,14 @@ class CoverageRepositoryImpl implements CoverageRepository { Future> getShiftsForDate({required DateTime date}) async { final String? businessId = dc.ClientSessionStore.instance.session?.business?.id; - print('Coverage: now=${DateTime.now().toIso8601String()}'); if (businessId == null || businessId.isEmpty) { - print('Coverage: missing businessId for date=${date.toIso8601String()}'); return []; } final DateTime start = DateTime(date.year, date.month, date.day); final DateTime end = DateTime(date.year, date.month, date.day, 23, 59, 59, 999); - print( - 'Coverage: request businessId=$businessId dayStart=${start.toIso8601String()} dayEnd=${end.toIso8601String()}', - ); + final fdc.QueryResult< dc.ListShiftRolesByBusinessAndDateRangeData, dc.ListShiftRolesByBusinessAndDateRangeVariables> shiftRolesResult = @@ -58,9 +54,6 @@ class CoverageRepositoryImpl implements CoverageRepository { dayEnd: _toTimestamp(end), ) .execute(); - print( - 'Coverage: ${date.toIso8601String()} staffsApplications=${applicationsResult.data.applications.length}', - ); return _mapCoverageShifts( shiftRolesResult.data.shiftRoles, @@ -84,11 +77,16 @@ class CoverageRepositoryImpl implements CoverageRepository { final List allWorkers = shifts.expand((CoverageShift shift) => shift.workers).toList(); final int totalConfirmed = allWorkers.length; - final int checkedIn = - allWorkers.where((CoverageWorker w) => w.isCheckedIn).length; - final int enRoute = - allWorkers.where((CoverageWorker w) => w.isEnRoute).length; - final int late = allWorkers.where((CoverageWorker w) => w.isLate).length; + final int checkedIn = allWorkers + .where((CoverageWorker w) => w.status == CoverageWorkerStatus.checkedIn) + .length; + final int enRoute = allWorkers + .where((CoverageWorker w) => + w.status == CoverageWorkerStatus.confirmed && w.checkInTime == null) + .length; + final int late = allWorkers + .where((CoverageWorker w) => w.status == CoverageWorkerStatus.late) + .length; return CoverageStats( totalNeeded: totalNeeded, @@ -172,25 +170,32 @@ class CoverageRepositoryImpl implements CoverageRepository { .toList(); } - String _mapWorkerStatus( + CoverageWorkerStatus _mapWorkerStatus( dc.EnumValue status, ) { if (status is dc.Known) { switch (status.value) { - case dc.ApplicationStatus.LATE: - return 'late'; - case dc.ApplicationStatus.CHECKED_IN: - case dc.ApplicationStatus.CHECKED_OUT: - case dc.ApplicationStatus.ACCEPTED: - case dc.ApplicationStatus.CONFIRMED: case dc.ApplicationStatus.PENDING: + return CoverageWorkerStatus.pending; + case dc.ApplicationStatus.ACCEPTED: + return CoverageWorkerStatus.accepted; case dc.ApplicationStatus.REJECTED: + return CoverageWorkerStatus.rejected; + case dc.ApplicationStatus.CONFIRMED: + return CoverageWorkerStatus.confirmed; + case dc.ApplicationStatus.CHECKED_IN: + return CoverageWorkerStatus.checkedIn; + case dc.ApplicationStatus.CHECKED_OUT: + return CoverageWorkerStatus.checkedOut; + case dc.ApplicationStatus.LATE: + return CoverageWorkerStatus.late; case dc.ApplicationStatus.NO_SHOW: + return CoverageWorkerStatus.noShow; case dc.ApplicationStatus.COMPLETED: - return 'confirmed'; + return CoverageWorkerStatus.completed; } } - return 'confirmed'; + return CoverageWorkerStatus.pending; } String? _formatTime(fdc.Timestamp? timestamp) { diff --git a/apps/mobile/packages/features/client/client_coverage/lib/src/domain/repositories/coverage_repository.dart b/apps/mobile/packages/features/client/client_coverage/lib/src/domain/repositories/coverage_repository.dart index 6d7de8ba..f5c340b3 100644 --- a/apps/mobile/packages/features/client/client_coverage/lib/src/domain/repositories/coverage_repository.dart +++ b/apps/mobile/packages/features/client/client_coverage/lib/src/domain/repositories/coverage_repository.dart @@ -1,4 +1,4 @@ -import '../ui_entities/coverage_entities.dart'; +import 'package:krow_domain/krow_domain.dart'; /// Repository interface for coverage-related operations. /// diff --git a/apps/mobile/packages/features/client/client_coverage/lib/src/domain/ui_entities/coverage_entities.dart b/apps/mobile/packages/features/client/client_coverage/lib/src/domain/ui_entities/coverage_entities.dart deleted file mode 100644 index bb9249c9..00000000 --- a/apps/mobile/packages/features/client/client_coverage/lib/src/domain/ui_entities/coverage_entities.dart +++ /dev/null @@ -1,133 +0,0 @@ -import 'package:equatable/equatable.dart'; - -/// Domain entity representing a shift in the coverage view. -/// -/// This is a feature-specific domain entity that encapsulates shift information -/// including scheduling details and assigned workers. -class CoverageShift extends Equatable { - /// Creates a [CoverageShift]. - const CoverageShift({ - required this.id, - required this.title, - required this.location, - required this.startTime, - required this.workersNeeded, - required this.date, - required this.workers, - }); - - /// The unique identifier for the shift. - final String id; - - /// The title or role of the shift. - final String title; - - /// The location where the shift takes place. - final String location; - - /// The start time of the shift (e.g., "16:00"). - final String startTime; - - /// The number of workers needed for this shift. - final int workersNeeded; - - /// The date of the shift. - final DateTime date; - - /// The list of workers assigned to this shift. - final List workers; - - /// Calculates the coverage percentage for this shift. - int get coveragePercent { - if (workersNeeded == 0) return 0; - return ((workers.length / workersNeeded) * 100).round(); - } - - @override - List get props => [ - id, - title, - location, - startTime, - workersNeeded, - date, - workers, - ]; -} - -/// Domain entity representing a worker in the coverage view. -/// -/// This entity tracks worker status including check-in information. -class CoverageWorker extends Equatable { - /// Creates a [CoverageWorker]. - const CoverageWorker({ - required this.name, - required this.status, - this.checkInTime, - }); - - /// The name of the worker. - final String name; - - /// The status of the worker ('confirmed', 'late', etc.). - final String status; - - /// The time the worker checked in, if applicable. - final String? checkInTime; - - /// Returns true if the worker is checked in. - bool get isCheckedIn => status == 'confirmed' && checkInTime != null; - - /// Returns true if the worker is en route. - bool get isEnRoute => status == 'confirmed' && checkInTime == null; - - /// Returns true if the worker is late. - bool get isLate => status == 'late'; - - @override - List get props => [name, status, checkInTime]; -} - -/// Domain entity representing coverage statistics. -/// -/// Aggregates coverage metrics for a specific date. -class CoverageStats extends Equatable { - /// Creates a [CoverageStats]. - const CoverageStats({ - required this.totalNeeded, - required this.totalConfirmed, - required this.checkedIn, - required this.enRoute, - required this.late, - }); - - /// The total number of workers needed. - final int totalNeeded; - - /// The total number of confirmed workers. - final int totalConfirmed; - - /// The number of workers who have checked in. - final int checkedIn; - - /// The number of workers en route. - final int enRoute; - - /// The number of late workers. - final int late; - - /// Calculates the overall coverage percentage. - int get coveragePercent { - if (totalNeeded == 0) return 0; - return ((totalConfirmed / totalNeeded) * 100).round(); - } - - @override - List get props => [ - totalNeeded, - totalConfirmed, - checkedIn, - enRoute, - late, - ]; -} diff --git a/apps/mobile/packages/features/client/client_coverage/lib/src/domain/usecases/get_coverage_stats_usecase.dart b/apps/mobile/packages/features/client/client_coverage/lib/src/domain/usecases/get_coverage_stats_usecase.dart index 00cb7c1d..a2fa4a50 100644 --- a/apps/mobile/packages/features/client/client_coverage/lib/src/domain/usecases/get_coverage_stats_usecase.dart +++ b/apps/mobile/packages/features/client/client_coverage/lib/src/domain/usecases/get_coverage_stats_usecase.dart @@ -1,7 +1,8 @@ import 'package:krow_core/core.dart'; +import 'package:krow_domain/krow_domain.dart'; + import '../arguments/get_coverage_stats_arguments.dart'; import '../repositories/coverage_repository.dart'; -import '../ui_entities/coverage_entities.dart'; /// Use case for fetching coverage statistics for a specific date. /// diff --git a/apps/mobile/packages/features/client/client_coverage/lib/src/domain/usecases/get_shifts_for_date_usecase.dart b/apps/mobile/packages/features/client/client_coverage/lib/src/domain/usecases/get_shifts_for_date_usecase.dart index da84506b..1b17c969 100644 --- a/apps/mobile/packages/features/client/client_coverage/lib/src/domain/usecases/get_shifts_for_date_usecase.dart +++ b/apps/mobile/packages/features/client/client_coverage/lib/src/domain/usecases/get_shifts_for_date_usecase.dart @@ -1,7 +1,7 @@ import 'package:krow_core/core.dart'; import '../arguments/get_shifts_for_date_arguments.dart'; import '../repositories/coverage_repository.dart'; -import '../ui_entities/coverage_entities.dart'; +import 'package:krow_domain/krow_domain.dart'; /// Use case for fetching shifts for a specific date. /// diff --git a/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/blocs/coverage_bloc.dart b/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/blocs/coverage_bloc.dart index d8a0a8c3..c218e9a5 100644 --- a/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/blocs/coverage_bloc.dart +++ b/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/blocs/coverage_bloc.dart @@ -1,7 +1,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import '../../domain/arguments/get_coverage_stats_arguments.dart'; import '../../domain/arguments/get_shifts_for_date_arguments.dart'; -import '../../domain/ui_entities/coverage_entities.dart'; +import 'package:krow_domain/krow_domain.dart'; import '../../domain/usecases/get_coverage_stats_usecase.dart'; import '../../domain/usecases/get_shifts_for_date_usecase.dart'; import 'coverage_event.dart'; diff --git a/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/blocs/coverage_state.dart b/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/blocs/coverage_state.dart index 9ca35dad..e6b99656 100644 --- a/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/blocs/coverage_state.dart +++ b/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/blocs/coverage_state.dart @@ -1,5 +1,5 @@ import 'package:equatable/equatable.dart'; -import '../../domain/ui_entities/coverage_entities.dart'; +import 'package:krow_domain/krow_domain.dart'; /// Enum representing the status of coverage data loading. enum CoverageStatus { diff --git a/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/widgets/coverage_quick_stats.dart b/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/widgets/coverage_quick_stats.dart index 56f87c69..31e3fd42 100644 --- a/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/widgets/coverage_quick_stats.dart +++ b/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/widgets/coverage_quick_stats.dart @@ -1,6 +1,6 @@ import 'package:design_system/design_system.dart'; import 'package:flutter/material.dart'; -import '../../domain/ui_entities/coverage_entities.dart'; +import 'package:krow_domain/krow_domain.dart'; /// Quick statistics cards showing coverage metrics. /// diff --git a/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/widgets/coverage_shift_list.dart b/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/widgets/coverage_shift_list.dart index 7ec0c0c5..504828dd 100644 --- a/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/widgets/coverage_shift_list.dart +++ b/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/widgets/coverage_shift_list.dart @@ -1,7 +1,7 @@ import 'package:design_system/design_system.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; -import '../../domain/ui_entities/coverage_entities.dart'; +import 'package:krow_domain/krow_domain.dart'; /// List of shifts with their workers. /// @@ -194,7 +194,8 @@ class _ShiftHeader extends StatelessWidget { size: UiConstants.space3, color: UiColors.iconSecondary, ), - Expanded(child: Text( + Expanded( + child: Text( location, style: UiTypography.body3r.textSecondary, overflow: TextOverflow.ellipsis, @@ -314,36 +315,92 @@ class _WorkerRow extends StatelessWidget { Color badgeText; String badgeLabel; - if (worker.isCheckedIn) { - bg = UiColors.textSuccess.withOpacity(0.1); - border = UiColors.textSuccess; - textBg = UiColors.textSuccess.withOpacity(0.2); - textColor = UiColors.textSuccess; - icon = UiIcons.success; - statusText = 'โœ“ Checked In at ${formatTime(worker.checkInTime)}'; - badgeBg = UiColors.textSuccess; - badgeText = UiColors.primaryForeground; - badgeLabel = 'On Site'; - } else if (worker.isEnRoute) { - bg = UiColors.textWarning.withOpacity(0.1); - border = UiColors.textWarning; - textBg = UiColors.textWarning.withOpacity(0.2); - textColor = UiColors.textWarning; - icon = UiIcons.clock; - statusText = 'En Route - Expected $shiftStartTime'; - badgeBg = UiColors.textWarning; - badgeText = UiColors.primaryForeground; - badgeLabel = 'En Route'; - } else { - bg = UiColors.destructive.withOpacity(0.1); - border = UiColors.destructive; - textBg = UiColors.destructive.withOpacity(0.2); - textColor = UiColors.destructive; - icon = UiIcons.warning; - statusText = 'โš  Running Late'; - badgeBg = UiColors.destructive; - badgeText = UiColors.destructiveForeground; - badgeLabel = 'Late'; + switch (worker.status) { + case CoverageWorkerStatus.checkedIn: + bg = UiColors.textSuccess.withOpacity(0.1); + border = UiColors.textSuccess; + textBg = UiColors.textSuccess.withOpacity(0.2); + textColor = UiColors.textSuccess; + icon = UiIcons.success; + statusText = 'โœ“ Checked In at ${formatTime(worker.checkInTime)}'; + badgeBg = UiColors.textSuccess; + badgeText = UiColors.primaryForeground; + badgeLabel = 'On Site'; + case CoverageWorkerStatus.confirmed: + if (worker.checkInTime == null) { + bg = UiColors.textWarning.withOpacity(0.1); + border = UiColors.textWarning; + textBg = UiColors.textWarning.withOpacity(0.2); + textColor = UiColors.textWarning; + icon = UiIcons.clock; + statusText = 'En Route - Expected $shiftStartTime'; + badgeBg = UiColors.textWarning; + badgeText = UiColors.primaryForeground; + badgeLabel = 'En Route'; + } else { + bg = UiColors.muted.withOpacity(0.1); + border = UiColors.border; + textBg = UiColors.muted.withOpacity(0.2); + textColor = UiColors.textSecondary; + icon = UiIcons.success; + statusText = 'Confirmed'; + badgeBg = UiColors.muted; + badgeText = UiColors.textPrimary; + badgeLabel = 'Confirmed'; + } + case CoverageWorkerStatus.late: + bg = UiColors.destructive.withOpacity(0.1); + border = UiColors.destructive; + textBg = UiColors.destructive.withOpacity(0.2); + textColor = UiColors.destructive; + icon = UiIcons.warning; + statusText = 'โš  Running Late'; + badgeBg = UiColors.destructive; + badgeText = UiColors.destructiveForeground; + badgeLabel = 'Late'; + case CoverageWorkerStatus.checkedOut: + bg = UiColors.muted.withOpacity(0.1); + border = UiColors.border; + textBg = UiColors.muted.withOpacity(0.2); + textColor = UiColors.textSecondary; + icon = UiIcons.success; + statusText = 'Checked Out'; + badgeBg = UiColors.muted; + badgeText = UiColors.textPrimary; + badgeLabel = 'Done'; + case CoverageWorkerStatus.noShow: + bg = UiColors.destructive.withOpacity(0.1); + border = UiColors.destructive; + textBg = UiColors.destructive.withOpacity(0.2); + textColor = UiColors.destructive; + icon = UiIcons.warning; + statusText = 'No Show'; + badgeBg = UiColors.destructive; + badgeText = UiColors.destructiveForeground; + badgeLabel = 'No Show'; + case CoverageWorkerStatus.completed: + bg = UiColors.textSuccess.withOpacity(0.1); + border = UiColors.textSuccess; + textBg = UiColors.textSuccess.withOpacity(0.2); + textColor = UiColors.textSuccess; + icon = UiIcons.success; + statusText = 'Completed'; + badgeBg = UiColors.textSuccess; + badgeText = UiColors.primaryForeground; + badgeLabel = 'Completed'; + case CoverageWorkerStatus.pending: + case CoverageWorkerStatus.accepted: + case CoverageWorkerStatus.rejected: + bg = UiColors.muted.withOpacity(0.1); + border = UiColors.border; + textBg = UiColors.muted.withOpacity(0.2); + textColor = UiColors.textSecondary; + icon = UiIcons.clock; + statusText = worker.status.name.toUpperCase(); + badgeBg = UiColors.muted; + badgeText = UiColors.textPrimary; + badgeLabel = worker.status.name[0].toUpperCase() + + worker.status.name.substring(1); } return Container( diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/data/repositories_impl/shifts_repository_impl.dart b/apps/mobile/packages/features/staff/shifts/lib/src/data/repositories_impl/shifts_repository_impl.dart index d4df32c0..347f389f 100644 --- a/apps/mobile/packages/features/staff/shifts/lib/src/data/repositories_impl/shifts_repository_impl.dart +++ b/apps/mobile/packages/features/staff/shifts/lib/src/data/repositories_impl/shifts_repository_impl.dart @@ -525,7 +525,7 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface { @override Future acceptShift(String shiftId) async { - await _updateApplicationStatus(shiftId, dc.ApplicationStatus.ACCEPTED); + await _updateApplicationStatus(shiftId, dc.ApplicationStatus.CONFIRMED); } @override diff --git a/docs/QA_TESTING_CHECKLIST.md b/docs/QA_TESTING_CHECKLIST.md index ff272efa..49fef7e4 100644 --- a/docs/QA_TESTING_CHECKLIST.md +++ b/docs/QA_TESTING_CHECKLIST.md @@ -155,7 +155,7 @@ - [ ] Missing worker photo shows default avatar **Loading & Empty States:** -- [ ] Skeleton loaders display while fetching data + - [ ] Empty coverage shows "No shifts today" - [ ] No workers show "No staff assigned" @@ -192,20 +192,10 @@ - [ ] Missing invoice data shows placeholder **Loading & Empty States:** -- [ ] Skeleton loaders display while fetching data - [ ] Empty pending invoices shows "No pending invoices" - [ ] Empty history shows "No invoice history" - [ ] Empty spending breakdown shows "No spending data" -**State Persistence:** -- [ ] Selected period persists after navigating away -- [ ] Billing data refreshes after returning from background - -**Backend Dependency Validation:** -- [ ] `listInvoicesByBusinessId` returns invoice records -- [ ] `listShiftRolesByBusinessAndDatesSummary` returns spending aggregates -- [ ] Period date range correctly calculated -- [ ] Business ID correctly filtered --- @@ -234,27 +224,10 @@ - [ ] Duplicate hub name shows warning - [ ] Hub with active orders prevents deletion (validation error) - **Loading & Empty States:** -- [ ] Skeleton loaders display while fetching hubs - [ ] Empty hubs list shows "No hubs configured" -- [ ] Address autocomplete shows loading during search - [ ] Hub creation shows loading spinner -**State Persistence:** -- [ ] Hub list refreshes after creation/deletion -- [ ] Hub data persists across app sessions - -**Backend Dependency Validation:** -- [ ] `getBusinessesByUserId` retrieves business ID -- [ ] `getTeamsByOwnerId` checks for existing team -- [ ] `createTeam` creates team if missing -- [ ] `getTeamHubsByTeamId` fetches hub list -- [ ] `createTeamHub` creates hub with geocoded data -- [ ] `deleteTeamHub` removes hub entity -- [ ] `listOrdersByBusinessAndTeamHub` validates no active orders -- [ ] Google Places API returns valid address components - --- #### ๐Ÿ“ฑ CLIENT-008: Settings @@ -276,13 +249,6 @@ **Loading & Empty States:** - [ ] Profile data loads on page mount -**State Persistence:** -- [ ] User data refreshes on page focus - -**Backend Dependency Validation:** -- [ ] Firebase Auth signOut called -- [ ] Session data cleared - --- #### ๐Ÿ“ฑ CLIENT-009: Client Main Navigation @@ -307,13 +273,6 @@ - [ ] Navigation bar displays immediately - [ ] Initial tab loads first -**State Persistence:** -- [ ] Active tab persists after app background โ†’ foreground -- [ ] Tab state resets to home on app restart - -**Backend Dependency Validation:** -- [ ] No direct backend calls (navigation only) - --- ### STAFF APP FEATURES @@ -332,7 +291,6 @@ - [ ] OTP verification succeeds with valid code - [ ] Profile setup wizard displays for new users - [ ] Authenticated users bypass auth and show home -- [ ] Session persists after app restart **Validation & Error States:** - [ ] Invalid phone format shows validation error @@ -346,16 +304,6 @@ - [ ] OTP input shows countdown timer - [ ] Profile setup shows progress indicator -**State Persistence:** -- [ ] Authenticated session persists after app background โ†’ foreground -- [ ] Session expires appropriately after logout - -**Backend Dependency Validation:** -- [ ] Firebase Auth phone verification flow completes -- [ ] `getUserById` returns user data -- [ ] `getStaffByUserId` retrieves staff profile -- [ ] Staff profile created if missing - --- #### ๐Ÿ“ฑ STAFF-002: Home Dashboard @@ -375,21 +323,10 @@ **Validation & Error States:** - [ ] Missing shift data shows placeholder - **Loading & Empty States:** -- [ ] Skeleton loaders display while fetching data - [ ] Empty today's shifts shows "No shifts today" - [ ] Empty recommended shows "No available shifts" -**State Persistence:** -- [ ] Dashboard data refreshes after returning from background -- [ ] Shift status updates reflected immediately - -**Backend Dependency Validation:** -- [ ] `getApplicationsByStaffId` fetches staff assignments -- [ ] `listShifts` returns available shifts -- [ ] Date filtering correctly applied - --- #### ๐Ÿ“ฑ STAFF-003: Profile @@ -412,16 +349,6 @@ **Loading & Empty States:** - [ ] Profile data loads on page mount -- [ ] Statistics display placeholders while loading - -**State Persistence:** -- [ ] Profile data refreshes on page focus -- [ ] Profile updates reflect immediately - -**Backend Dependency Validation:** -- [ ] `getStaffByUserId` retrieves complete staff profile -- [ ] Firebase Auth signOut called -- [ ] Session data cleared --- @@ -446,31 +373,10 @@ **Validation & Error States:** - [ ] Empty tabs show appropriate empty state messages - - [ ] Already applied shift prevents duplicate application - [ ] Past shifts cannot be applied to - [ ] Cancelled shifts show cancellation reason -**Loading & Empty States:** -- [ ] Skeleton loaders display while fetching shifts -- [ ] Empty My Shifts shows "No assigned shifts" -- [ ] Empty Available shows "No open shifts" -- [ ] Empty Pending shows "No pending applications" -- [ ] Empty History shows "No past shifts" - -**State Persistence:** -- [ ] Active tab persists after navigating away -- [ ] Shift list refreshes after status changes -- [ ] Shift data refreshes after returning from background - -**Backend Dependency Validation:** -- [ ] `getApplicationsByStaffId` fetches applications by status -- [ ] `getShiftById` retrieves shift details -- [ ] `updateApplicationStatus` changes application state -- [ ] `createApplication` creates new application -- [ ] `deleteApplication` removes application -- [ ] `updateShift` updates filled count - --- #### ๐Ÿ“ฑ STAFF-005: Availability Management @@ -497,17 +403,6 @@ - [ ] Loading spinner displays while fetching availability - [ ] Default state shows all unavailable -**State Persistence:** -- [ ] Availability persists across app sessions -- [ ] Changes reflect immediately in shift matching - -**Backend Dependency Validation:** -- [ ] `getStaffByUserId` retrieves staff ID -- [ ] `listStaffAvailabilitiesByStaffId` fetches availability records -- [ ] `getStaffAvailabilityByKey` checks existing record -- [ ] `updateStaffAvailability` updates existing slot -- [ ] `createStaffAvailability` creates new slot - --- #### ๐Ÿ“ฑ STAFF-006: Clock In/Out @@ -535,17 +430,6 @@ - [ ] Loading spinner displays while fetching shift - [ ] Empty state shows "No shifts scheduled" -**State Persistence:** -- [ ] Attendance status persists across app sessions -- [ ] Clock in/out times display correctly - -**Backend Dependency Validation:** -- [ ] `getApplicationsByStaffId` fetches today's shifts -- [ ] `createAttendance` records clock in -- [ ] `updateAttendance` records clock out -- [ ] `listAttendancesByApplicationId` gets attendance status -- [ ] `updateApplicationStatus` updates application state - --- #### ๐Ÿ“ฑ STAFF-007: Payments @@ -565,20 +449,9 @@ - [ ] Zero earnings show $0.00 (not error) - [ ] Missing payment data shows placeholder - **Loading & Empty States:** -- [ ] Skeleton loaders display while fetching payments - [ ] Empty history shows "No payment history" -**State Persistence:** -- [ ] Payment data refreshes after returning from background -- [ ] Filter state persists after navigating away - -**Backend Dependency Validation:** -- [ ] `getStaffByUserId` retrieves staff ID -- [ ] `getPaymentsByStaffId` fetches payment records -- [ ] Mock summary data calculated correctly - --- #### ๐Ÿ“ฑ STAFF-008: Personal Info (Onboarding) @@ -609,14 +482,6 @@ - [ ] Photo upload shows progress indicator - [ ] Save button shows loading spinner -**State Persistence:** -- [ ] Changes persist after save -- [ ] Unsaved changes show confirmation dialog on exit - -**Backend Dependency Validation:** -- [ ] `getStaffByUserId` fetches profile -- [ ] `updateStaff` saves profile changes - --- #### ๐Ÿ“ฑ STAFF-009: Emergency Contact (Onboarding) @@ -640,22 +505,11 @@ - [ ] Invalid phone format shows error - [ ] At least one contact required (if applicable) - **Loading & Empty States:** - [ ] Loading spinner displays while fetching contacts - [ ] Empty state shows "No emergency contacts" - [ ] Save button shows loading spinner -**State Persistence:** -- [ ] Contacts persist after save -- [ ] Unsaved changes show confirmation dialog on exit - -**Backend Dependency Validation:** -- [ ] `getStaffByUserId` retrieves staff ID -- [ ] `getEmergencyContactsByStaffId` fetches contacts -- [ ] `deleteEmergencyContact` removes contacts (replace-all pattern) -- [ ] `createEmergencyContact` creates new contacts - --- #### ๐Ÿ“ฑ STAFF-010: Experience & Skills (Onboarding) @@ -681,48 +535,6 @@ - [ ] Loading spinner displays while fetching data - [ ] Save button shows loading spinner -**State Persistence:** -- [ ] Selections persist after save -- [ ] Unsaved changes show confirmation dialog on exit - -**Backend Dependency Validation:** -- [ ] `getStaffByUserId` fetches profile with industries and skills -- [ ] `updateStaff` updates industries and skills arrays - ---- - -#### ๐Ÿ“ฑ STAFF-011: Attire Selection (Onboarding) - -**Applications:** Staff -**Entry Points:** -- Profile โ†’ Attire -- Onboarding wizard - -**Happy Path Test Cases:** -- [ ] Attire options list displays all items -- [ ] Item selection toggles checkmark -- [ ] Photo upload button opens camera/gallery -- [ ] Photos display in grid -- [ ] Save updates selections and photos - -**Validation & Error States:** -- [ ] At least one attire item required (if applicable) -- [ ] Photo upload failure shows error - - -**Loading & Empty States:** -- [ ] Loading spinner displays while fetching options -- [ ] Photo upload shows progress indicator -- [ ] Save button shows loading spinner - -**State Persistence:** -- [ ] Selections and photos persist after save -- [ ] Unsaved changes show confirmation dialog on exit - -**Backend Dependency Validation:** -- [ ] `listAttireOptions` fetches available items -- [ ] Photo upload and save mutations (pending implementation) - --- #### ๐Ÿ“ฑ STAFF-012: Bank Account (Finances) @@ -780,19 +592,9 @@ **Validation & Error States:** - [ ] Missing attendance data shows "Not recorded" - **Loading & Empty States:** -- [ ] Skeleton loaders display while fetching records - [ ] Empty state shows "No time card history" -**State Persistence:** -- [ ] Time card data refreshes after returning from background - -**Backend Dependency Validation:** -- [ ] `getStaffByUserId` retrieves staff ID -- [ ] `getApplicationsByStaffId` fetches applications with attendance -- [ ] Attendance records mapped to time card format - --- #### ๐Ÿ“ฑ STAFF-014: Tax Forms (Compliance) @@ -817,82 +619,11 @@ - [ ] Invalid date format shows error - [ ] Signature required validation - **Loading & Empty States:** - [ ] Loading spinner displays while fetching forms - [ ] Form editor loads with skeleton placeholders - [ ] Save button shows loading spinner -**State Persistence:** -- [ ] Form data persists after save -- [ ] Unsaved changes show confirmation dialog on exit -- [ ] Form status updates immediately - -**Backend Dependency Validation:** -- [ ] `getTaxFormsByStaffId` fetches forms -- [ ] `createTaxForm` initializes missing forms -- [ ] `updateTaxForm` saves form data and status - ---- - -#### ๐Ÿ“ฑ STAFF-015: Documents (Compliance) - -**Applications:** Staff -**Entry Points:** -- Profile โ†’ Documents - -**Happy Path Test Cases:** -- [ ] Documents list displays required documents -- [ ] Document status shows verified/pending/expired -- [ ] Document detail view shows requirements -- [ ] Expiry dates display correctly -- [ ] Expired documents highlight in red - -**Validation & Error States:** -- [ ] Missing documents show incomplete status - - -**Loading & Empty States:** -- [ ] Skeleton loaders display while fetching documents -- [ ] Empty state shows "No documents required" - -**State Persistence:** -- [ ] Document data refreshes after returning from background - -**Backend Dependency Validation:** -- [ ] Mock implementation currently -- [ ] โš ๏ธ Requires clarification: Real Data Connect integration pending - ---- - -#### ๐Ÿ“ฑ STAFF-016: Certificates (Compliance) - -**Applications:** Staff -**Entry Points:** -- Profile โ†’ Certificates - -**Happy Path Test Cases:** -- [ ] Certificates list displays all certificates -- [ ] Certificate cards show name, status, and expiry -- [ ] Certificate detail view shows full information -- [ ] Expired certificates highlight in red -- [ ] Certificate verification status displays - -**Validation & Error States:** -- [ ] Missing certificates show placeholder - - -**Loading & Empty States:** -- [ ] Skeleton loaders display while fetching certificates -- [ ] Empty state shows "No certificates" - -**State Persistence:** -- [ ] Certificate data refreshes after returning from background - -**Backend Dependency Validation:** -- [ ] `listStaffDocumentsByStaffId` fetches certificate documents -- [ ] Document data mapped to certificate entities - --- #### ๐Ÿ“ฑ STAFF-017: Staff Main Navigation @@ -1175,435 +906,3 @@ - โœ… No feature overlap or unauthorized access --- - -### Scenario 9: Race Condition - Concurrent Shift Application - -**Preconditions:** -- One available shift with 1 position -- Two staff users authenticated on separate devices - -**Steps:** -1. **STAFF APP (Device 1):** - - [ ] Navigate to Shifts โ†’ Available - - [ ] View shift details - -2. **STAFF APP (Device 2):** - - [ ] Navigate to Shifts โ†’ Available - - [ ] View same shift details - -3. **STAFF APP (Device 1):** - - [ ] Apply for shift - - [ ] Verify application created - -4. **STAFF APP (Device 2):** - - [ ] Attempt to apply for same shift - - [ ] Verify appropriate behavior (position filled message or pending status) - -5. **CLIENT APP:** - - [ ] Navigate to View Orders - - [ ] Verify only 1 application shows (not 2) - - [ ] Accept Device 1 application - -6. **STAFF APP (Device 2):** - - [ ] Refresh Available shifts - - [ ] Verify shift removed or shows as filled - -**Expected Results:** -- โœ… Only first application succeeds (or both go to pending) -- โœ… No double-booking occurs -- โœ… Race condition handled gracefully -- โš ๏ธ **Requires clarification:** Backend concurrency control behavior - ---- - -### Scenario 10: Network Failure During Critical Operation - -**Preconditions:** -- Staff has pending shift application - -**Steps:** -1. **STAFF APP:** - - [ ] Navigate to Shifts โ†’ Pending - - [ ] Disable network connection - - [ ] Attempt to accept shift - - [ ] Verify offline error message displays - - [ ] Re-enable network - - [ ] Retry accept shift - - [ ] Verify acceptance succeeds - -2. **CLIENT APP:** - - [ ] Verify shift shows as filled after network restored - -**Expected Results:** -- โœ… Offline state handled gracefully with clear messaging -- โœ… Retry succeeds after network restored -- โœ… Data consistency maintained - ---- - -## 3๏ธโƒฃ SHARED INFRASTRUCTURE VALIDATION - -### Domain Entity Consistency - -#### Test: Entity Field Validation - -- [ ] **Staff Entity:** - - [ ] Verify all required fields populate (id, userId, firstName, lastName, email, phone) - - [ ] Verify optional fields handle null correctly (photoUrl, preferredLocations) - - [ ] Verify enum fields map correctly (UserStatus) - -- [ ] **Order Entity:** - - [ ] Verify all required fields populate - - [ ] Verify OrderStatus enum maps correctly - - [ ] Verify OrderType enum maps correctly - -- [ ] **Shift Entity:** - - [ ] Verify date/time fields parse correctly - - [ ] Verify ShiftStatus enum maps correctly - - [ ] Verify location data (hub) links correctly - -- [ ] **Application Entity:** - - [ ] Verify ApplicationStatus enum maps correctly - - [ ] Verify relationships (staff, shift, role) link correctly - -- [ ] **Invoice Entity:** - - [ ] Verify amount calculations correct - - [ ] Verify date fields parse correctly - - [ ] Verify InvoiceStatus enum maps correctly - -- [ ] **Hub Entity:** - - [ ] Verify address components parse correctly - - [ ] Verify geocoding (lat/lng) present and valid - - [ ] Verify placeId populated - ---- - -### Data Connect Schema Alignment - -#### Test: Backend Operation Contracts - -- [ ] **User Operations:** - - [ ] `getUserById(userId)` returns expected fields - - [ ] `createUser(...)` accepts all required parameters - - [ ] `updateUser(...)` updates only specified fields - -- [ ] **Staff Operations:** - - [ ] `getStaffByUserId(userId)` returns staff profile - - [ ] `updateStaff(...)` updates specified fields - - [ ] `listStaffs()` returns paginated results - -- [ ] **Order Operations:** - - [ ] `createOrder(...)` creates order with shifts - - [ ] `listOrdersByBusinessId(...)` filters by business correctly - - [ ] `updateOrder(...)` updates order fields - -- [ ] **Shift Operations:** - - [ ] `createShift(...)` creates shift with location - - [ ] `getShiftById(id)` returns full shift details - - [ ] `listShiftRolesByBusinessAndDateRange(...)` returns correct date range - -- [ ] **Application Operations:** - - [ ] `createApplication(...)` creates pending application - - [ ] `updateApplicationStatus(...)` changes status correctly - - [ ] `getApplicationsByStaffId(...)` filters by staff and date - -- [ ] **Attendance Operations:** - - [ ] `createAttendance(...)` records clock in - - [ ] `updateAttendance(...)` records clock out - - [ ] `listAttendancesByApplicationId(...)` returns attendance records - -- [ ] **Hub Operations:** - - [ ] `createTeamHub(...)` creates hub with location data - - [ ] `getTeamHubsByTeamId(...)` returns hubs for team - - [ ] `deleteTeamHub(id)` removes hub entity - ---- - -### Error Handling Consistency - -#### Test: Standard Error Patterns - -- [ ] **Network Errors:** - - [ ] All features show "Network error" message - - [ ] All features show "Retry" button - - [ ] Retry button re-attempts operation - -- [ ] **Authentication Errors:** - - [ ] Expired token redirects to login - - [ ] Invalid credentials show appropriate message - - [ ] Auth failures log out user - -- [ ] **Validation Errors:** - - [ ] Field-level validation shows inline errors - - [ ] Form-level validation prevents submission - - [ ] Error messages are user-friendly - -- [ ] **Backend Errors:** - - [ ] 400 errors show validation details - - [ ] 404 errors show "Not found" message - - [ ] 500 errors show "Server error, try again" message - -- [ ] **Data Not Found:** - - [ ] Empty lists show appropriate empty state - - [ ] Missing entities show "Not found" message - - [ ] Deleted entities handle gracefully - ---- - -### Version Mismatch Tolerance - -#### Test: App Version Compatibility - -- [ ] **Client App Updated, Staff App Not:** - - [ ] Backend operations remain compatible - - [ ] Shared domain entities parse correctly - - [ ] New fields in Client don't break Staff - -- [ ] **Staff App Updated, Client App Not:** - - [ ] Backend operations remain compatible - - [ ] Shared domain entities parse correctly - - [ ] New fields in Staff don't break Client - -- [ ] **Backend Schema Updated:** - - [ ] Apps handle new optional fields gracefully - - [ ] Apps ignore unknown fields - - [ ] Required fields validated correctly - ---- - -## 4๏ธโƒฃ REGRESSION & RELEASE CHECKLIST - -### Smoke Testing (Critical Path) - -#### Authentication Flow (5 minutes) - -- [ ] **Client App:** - - [ ] Launch app shows Get Started screen - - [ ] Sign in with valid credentials succeeds - - [ ] Home dashboard displays - -- [ ] **Staff App:** - - [ ] Launch app shows Get Started screen - - [ ] Phone verification sends OTP - - [ ] OTP verification succeeds - - [ ] Home dashboard displays - -#### Order Creation & Application (10 minutes) - -- [ ] **Client App:** - - [ ] Create one-time order succeeds - - [ ] Order appears in View Orders list - - [ ] Order details display correctly - -- [ ] **Staff App:** - - [ ] Available shift appears in Shifts tab - - [ ] Apply for shift succeeds - - [ ] Application appears in Pending tab - -- [ ] **Client App:** - - [ ] Pending application displays in View Orders - - [ ] Coverage shows staff as pending - -#### Clock In/Out Flow (5 minutes) - -- [ ] **Staff App:** - - [ ] Accept shift from Pending tab - - [ ] Clock in on Clock In tab - - [ ] Clock in time recorded - -- [ ] **Client App:** - - [ ] Coverage shows staff as checked in - - [ ] Staff status updates in real-time - -- [ ] **Staff App:** - - [ ] Clock out succeeds - - [ ] Time card displays attendance record - ---- - -### Critical Path Validation (Must Pass Before Release) - -#### Client App Critical Features - -- [ ] **Authentication:** - - [ ] Sign in with email/password works - - [ ] Session persists after restart - -- [ ] **Order Management:** - - [ ] Create order succeeds - - [ ] View orders displays correctly - - [ ] Order details accurate - -- [ ] **Coverage Monitoring:** - - [ ] Coverage stats display correctly - - [ ] Staff status updates reflect backend - -- [ ] **Billing:** - - [ ] Invoice list displays - - [ ] Spending breakdown calculates correctly - -#### Staff App Critical Features - -- [ ] **Authentication:** - - [ ] Phone verification works - - [ ] Session persists after restart - -- [ ] **Shift Management:** - - [ ] Available shifts display - - [ ] Apply for shift succeeds - - [ ] Accept shift succeeds - - [ ] My Shifts displays assigned shifts - -- [ ] **Clock In/Out:** - - [ ] Clock in records attendance - - [ ] Clock out completes record - -- [ ] **Profile:** - - [ ] View profile displays data - - [ ] Update personal info succeeds - ---- - -### High-Risk Features (Require Extra Scrutiny) - -#### Payment Processing - -- [ ] **Staff App:** - - [ ] Payment history displays correctly - - [ ] Payment amounts accurate - - [ ] No double-payment scenarios - -- [ ] **Client App:** - - [ ] Invoice amounts correct - - [ ] Billing calculations accurate - - [ ] No overcharging scenarios - -#### Data Integrity - -- [ ] **Order โ†’ Shift โ†’ Application Chain:** - - [ ] Order creation creates shifts - - [ ] Shift deletion cascades correctly - - [ ] Application deletion updates shift counts - -- [ ] **Attendance Records:** - - [ ] Clock in/out times accurate - - [ ] Hours calculation correct - - [ ] No duplicate attendance records - -#### Concurrency Issues - -- [ ] **Multiple Staff Applying:** - - [ ] Race condition handled correctly - - [ ] No double-booking - - [ ] First-come-first-served logic works - -- [ ] **Shift Cancellation:** - - [ ] Staff notified appropriately - - [ ] Applications updated correctly - - [ ] No orphaned assignments - ---- - -### Release-Blocking Failures - -**The following issues MUST be fixed before release:** - -- [ ] **Authentication fails completely** (users cannot log in) -- [ ] **Order creation fails completely** (clients cannot create orders) -- [ ] **Shift application fails completely** (staff cannot apply for shifts) -- [ ] **Clock in/out fails completely** (staff cannot track attendance) -- [ ] **Payment data displays incorrectly** (financial inaccuracies) -- [ ] **Data loss occurs** (orders, shifts, or applications deleted unintentionally) -- [ ] **App crashes on launch** (unrecoverable error) -- [ ] **Backend connection fails** (cannot communicate with Data Connect) -- [ ] **Critical security vulnerability** (unauthorized access, data exposure) - ---- - -## ๐Ÿ“Š TESTING METRICS & REPORTING - -### Test Execution Summary - -**Date:** __________ -**Tester:** __________ -**Build Version:** __________ - -| Category | Total Tests | Passed | Failed | Blocked | Pass Rate | -|----------|-------------|--------|--------|---------|-----------| -| Client Features | __ | __ | __ | __ | __% | -| Staff Features | __ | __ | __ | __ | __% | -| Cross-App Scenarios | __ | __ | __ | __ | __% | -| Infrastructure | __ | __ | __ | __ | __% | -| Smoke Tests | __ | __ | __ | __ | __% | -| **TOTAL** | **__** | **__** | **__** | **__** | **__%** | - ---- - -### Defect Severity Classification - -**Critical (P0):** Release-blocking, affects core functionality -**High (P1):** Major functionality broken, workaround exists -**Medium (P2):** Minor functionality affected, low impact -**Low (P3):** Cosmetic issue, no functional impact - ---- - -### Sign-Off Criteria - -**Release can proceed when:** -- [ ] All P0 defects resolved -- [ ] 95%+ pass rate on Critical Path tests -- [ ] 85%+ pass rate on all Feature tests -- [ ] No unresolved P1 defects in core features -- [ ] Cross-app scenarios pass 90%+ -- [ ] Backend integration stable (no frequent failures) -- [ ] QA lead approval obtained -- [ ] Product owner approval obtained - ---- - -## ๐Ÿ“ NOTES & CLARIFICATIONS NEEDED - -The following items require clarification before full QA execution: - -1. โš ๏ธ **Documents Feature (STAFF-015):** Real Data Connect integration status unclear. Currently using mock implementation. - -2. โš ๏ธ **Shift Cancellation:** Feature existence and behavior not confirmed in current implementation. - -3. โš ๏ธ **Race Condition Handling (Scenario 9):** Backend concurrency control mechanism needs documentation. - -4. โš ๏ธ **Payment Processing:** End-to-end payment flow from shift completion to payment disbursement not fully implemented. - -5. โš ๏ธ **NFC Tag Assignment:** Hub NFC functionality interface exists but implementation status unclear. - -6. โš ๏ธ **Recurring & Permanent Orders:** Placeholder screens exist but full workflow not implemented. - -7. โš ๏ธ **Reports Feature (Client):** Currently shows placeholder, implementation status unknown. - -8. โš ๏ธ **Notification System:** Push notifications for shift assignments, cancellations, and status updates not covered in current analysis. - ---- - -## ๐ŸŽฏ CONCLUSION - -This QA checklist provides comprehensive coverage of all implemented features across both Client and Staff applications. It is designed for manual testing by QA engineers and supports release sign-off decisions based on structured test execution and clear pass/fail criteria. - -**Key Strengths:** -- โœ… Feature-by-feature detailed test cases -- โœ… Cross-application integration scenarios -- โœ… Infrastructure and data consistency validation -- โœ… Clear release-blocking criteria -- โœ… Based on actual implemented code (not speculative) - -**Recommended Usage:** -1. Execute smoke tests before each build -2. Run full feature regression weekly -3. Execute cross-app scenarios before major releases -4. Validate infrastructure after backend schema updates -5. Use sign-off checklist for release go/no-go decisions - ---- - -**Document Maintainer:** KROW QA Team -**Last Updated:** February 1, 2026 -**Next Review:** Upon next major feature release From 82e479b4c03c83bd4eab7c6023510034d0d0845f Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Sun, 1 Feb 2026 21:07:15 -0500 Subject: [PATCH 10/15] feat: Add spacing between invoice history and spending breakdown card in billing view --- .../client/billing/lib/src/presentation/pages/billing_page.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/mobile/packages/features/client/billing/lib/src/presentation/pages/billing_page.dart b/apps/mobile/packages/features/client/billing/lib/src/presentation/pages/billing_page.dart index 825917ff..bc522cb9 100644 --- a/apps/mobile/packages/features/client/billing/lib/src/presentation/pages/billing_page.dart +++ b/apps/mobile/packages/features/client/billing/lib/src/presentation/pages/billing_page.dart @@ -200,6 +200,8 @@ class _BillingViewState extends State { const SpendingBreakdownCard(), if (state.invoiceHistory.isEmpty) _buildEmptyState(context) else InvoiceHistorySection(invoices: state.invoiceHistory), + + const SizedBox(height: UiConstants.space32), ], ), ); From e7d5c29c000a1e75a6d583d4c86143ccb6bf251a Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Sun, 1 Feb 2026 21:14:01 -0500 Subject: [PATCH 11/15] feat: Integrate ViewOrdersHeader and ViewOrdersFilterTab components for improved UI in ViewOrdersPage --- .../presentation/pages/view_orders_page.dart | 434 +++--------------- .../presentation/widgets/view_order_card.dart | 1 + .../widgets/view_orders_filter_tab.dart | 68 +++ .../widgets/view_orders_header.dart | 259 +++++++++++ 4 files changed, 400 insertions(+), 362 deletions(-) create mode 100644 apps/mobile/packages/features/client/view_orders/lib/src/presentation/widgets/view_orders_filter_tab.dart create mode 100644 apps/mobile/packages/features/client/view_orders/lib/src/presentation/widgets/view_orders_header.dart diff --git a/apps/mobile/packages/features/client/view_orders/lib/src/presentation/pages/view_orders_page.dart b/apps/mobile/packages/features/client/view_orders/lib/src/presentation/pages/view_orders_page.dart index 27ca4dc2..fd256e8c 100644 --- a/apps/mobile/packages/features/client/view_orders/lib/src/presentation/pages/view_orders_page.dart +++ b/apps/mobile/packages/features/client/view_orders/lib/src/presentation/pages/view_orders_page.dart @@ -1,4 +1,3 @@ -import 'dart:ui'; import 'package:design_system/design_system.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -10,6 +9,7 @@ import '../blocs/view_orders_cubit.dart'; import '../blocs/view_orders_state.dart'; import 'package:krow_domain/krow_domain.dart'; import '../widgets/view_order_card.dart'; +import '../widgets/view_orders_header.dart'; import '../navigation/view_orders_navigator.dart'; /// The main page for viewing client orders. @@ -22,6 +22,7 @@ class ViewOrdersPage extends StatelessWidget { /// Creates a [ViewOrdersPage]. const ViewOrdersPage({super.key, this.initialDate}); + /// The initial date to display orders for. final DateTime? initialDate; @override @@ -37,7 +38,8 @@ class ViewOrdersPage extends StatelessWidget { class ViewOrdersView extends StatefulWidget { /// Creates a [ViewOrdersView]. const ViewOrdersView({super.key, this.initialDate}); - + + /// The initial date to display orders for. final DateTime? initialDate; @override @@ -88,376 +90,84 @@ class _ViewOrdersViewState extends State { } return Scaffold( - body: Stack( - children: [ - // Background Gradient - Container( - decoration: const BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [UiColors.bgSecondary, UiColors.white], - stops: [0.0, 0.3], - ), + body: SafeArea( + child: Column( + children: [ + // Header + Filter + Calendar (Sticky behavior) + ViewOrdersHeader( + state: state, + calendarDays: calendarDays, ), - ), - - SafeArea( - child: Column( - children: [ - // Header + Filter + Calendar (Sticky behavior) - _buildHeader( - context: context, - state: state, - calendarDays: calendarDays, - ), - - // Content List - Expanded( - child: filteredOrders.isEmpty - ? _buildEmptyState(context: context, state: state) - : ListView( - padding: const EdgeInsets.fromLTRB( - UiConstants.space5, - UiConstants.space4, - UiConstants.space5, - 100, - ), - children: [ - if (filteredOrders.isNotEmpty) - Padding( - padding: const EdgeInsets.only( - bottom: UiConstants.space3, - ), - child: Row( - children: [ - Container( - width: 8, - height: 8, - decoration: BoxDecoration( - color: dotColor, - shape: BoxShape.circle, - ), - ), - const SizedBox( - width: UiConstants.space2, - ), - Text( - sectionTitle.toUpperCase(), - style: UiTypography.titleUppercase2m - .copyWith( - color: UiColors.textPrimary, - ), - ), - const SizedBox( - width: UiConstants.space1, - ), - Text( - '(${filteredOrders.length})', - style: UiTypography.footnote1r - .copyWith( - color: UiColors.textSecondary, - ), - ), - ], - ), - ), - ...filteredOrders.map( - (OrderItem order) => Padding( - padding: const EdgeInsets.only( - bottom: UiConstants.space3, - ), - child: ViewOrderCard(order: order), - ), + + // Content List + Expanded( + child: filteredOrders.isEmpty + ? _buildEmptyState(context: context, state: state) + : ListView( + padding: const EdgeInsets.fromLTRB( + UiConstants.space5, + UiConstants.space4, + UiConstants.space5, + 100, + ), + children: [ + if (filteredOrders.isNotEmpty) + Padding( + padding: const EdgeInsets.only( + bottom: UiConstants.space3, ), - ], + child: Row( + children: [ + Container( + width: 8, + height: 8, + decoration: BoxDecoration( + color: dotColor, + shape: BoxShape.circle, + ), + ), + const SizedBox( + width: UiConstants.space2, + ), + Text( + sectionTitle.toUpperCase(), + style: UiTypography.titleUppercase2m + .copyWith( + color: UiColors.textPrimary, + ), + ), + const SizedBox( + width: UiConstants.space1, + ), + Text( + '(${filteredOrders.length})', + style: UiTypography.footnote1r + .copyWith( + color: UiColors.textSecondary, + ), + ), + ], + ), + ), + ...filteredOrders.map( + (OrderItem order) => Padding( + padding: const EdgeInsets.only( + bottom: UiConstants.space3, + ), + child: ViewOrderCard(order: order), + ), ), - ), - ], + ], + ), ), - ), - ], + ], + ), ), ); }, ); } - /// Builds the sticky header section. - Widget _buildHeader({ - required BuildContext context, - required ViewOrdersState state, - required List calendarDays, - }) { - return ClipRect( - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), - child: Container( - decoration: const BoxDecoration( - color: Color(0xCCFFFFFF), // White with 0.8 alpha - border: Border( - bottom: BorderSide(color: UiColors.separatorSecondary), - ), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - // Top Bar - Padding( - padding: const EdgeInsets.fromLTRB( - UiConstants.space5, - UiConstants.space5, - UiConstants.space5, - UiConstants.space3, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - t.client_view_orders.title, - style: UiTypography.headline3m.copyWith( - color: UiColors.textPrimary, - fontWeight: FontWeight.bold, - ), - ), - if (state.filteredOrders.isNotEmpty) - UiButton.primary( - text: t.client_view_orders.post_button, - leadingIcon: UiIcons.add, - onPressed: () => Modular.to.navigateToCreateOrder(), - size: UiButtonSize.small, - style: ElevatedButton.styleFrom( - minimumSize: const Size(0, 48), - maximumSize: const Size(0, 48), - ), - ), - ], - ), - ), - - // Filter Tabs - Padding( - padding: const EdgeInsets.only(bottom: UiConstants.space3), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - _buildFilterTab( - context, - label: t.client_view_orders.tabs.up_next, - isSelected: state.filterTab == 'all', - tabId: 'all', - count: state.upNextCount, - ), - const SizedBox(width: UiConstants.space6), - _buildFilterTab( - context, - label: t.client_view_orders.tabs.active, - isSelected: state.filterTab == 'active', - tabId: 'active', - count: state.activeCount, - ), - const SizedBox(width: UiConstants.space6), - _buildFilterTab( - context, - label: t.client_view_orders.tabs.completed, - isSelected: state.filterTab == 'completed', - tabId: 'completed', - count: state.completedCount, - ), - ], - ), - ), - - // Calendar Header controls - Padding( - padding: const EdgeInsets.symmetric( - horizontal: UiConstants.space5, - vertical: UiConstants.space2, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - IconButton( - icon: const Icon( - UiIcons.chevronLeft, - size: 20, - color: UiColors.iconSecondary, - ), - onPressed: () => BlocProvider.of( - context, - ).updateWeekOffset(-1), - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), - splashRadius: 20, - ), - Text( - DateFormat('MMMM yyyy').format(calendarDays.first), - style: UiTypography.body2m.copyWith( - color: UiColors.textSecondary, - ), - ), - IconButton( - icon: const Icon( - UiIcons.chevronRight, - size: 20, - color: UiColors.iconSecondary, - ), - onPressed: () => BlocProvider.of( - context, - ).updateWeekOffset(1), - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), - splashRadius: 20, - ), - ], - ), - ), - - // Calendar Grid - SizedBox( - height: 72, - child: ListView.separated( - padding: const EdgeInsets.symmetric( - horizontal: UiConstants.space5, - ), - scrollDirection: Axis.horizontal, - itemCount: 7, - separatorBuilder: (BuildContext context, int index) => - const SizedBox(width: UiConstants.space2), - itemBuilder: (BuildContext context, int index) { - final DateTime date = calendarDays[index]; - final bool isSelected = - state.selectedDate != null && - date.year == state.selectedDate!.year && - date.month == state.selectedDate!.month && - date.day == state.selectedDate!.day; - - // Check if this date has any shifts - final String dateStr = DateFormat( - 'yyyy-MM-dd', - ).format(date); - final bool hasShifts = state.orders.any( - (OrderItem s) => s.date == dateStr, - ); - - return GestureDetector( - onTap: () => BlocProvider.of( - context, - ).selectDate(date), - child: AnimatedContainer( - duration: const Duration(milliseconds: 200), - width: 48, - decoration: BoxDecoration( - color: isSelected ? UiColors.primary : UiColors.white, - borderRadius: BorderRadius.circular(16), - border: Border.all( - color: isSelected - ? UiColors.primary - : UiColors.separatorPrimary, - ), - boxShadow: isSelected - ? [ - BoxShadow( - color: UiColors.primary.withValues( - alpha: 0.25, - ), - blurRadius: 12, - offset: const Offset(0, 4), - ), - ] - : null, - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - DateFormat('dd').format(date), - style: UiTypography.title2b.copyWith( - fontSize: 18, - color: isSelected - ? UiColors.white - : UiColors.textPrimary, - ), - ), - Text( - DateFormat('E').format(date), - style: UiTypography.footnote2m.copyWith( - color: isSelected - ? UiColors.white.withValues(alpha: 0.8) - : UiColors.textSecondary, - ), - ), - if (hasShifts) ...[ - const SizedBox(height: UiConstants.space1), - Container( - width: 6, - height: 6, - decoration: BoxDecoration( - color: isSelected - ? UiColors.white - : UiColors.primary, - shape: BoxShape.circle, - ), - ), - ], - ], - ), - ), - ); - }, - ), - ), - const SizedBox(height: UiConstants.space4), - ], - ), - ), - ), - ); - } - - /// Builds a single filter tab. - Widget _buildFilterTab( - BuildContext context, { - required String label, - required bool isSelected, - required String tabId, - int? count, - }) { - String text = label; - if (count != null) { - text = '$label ($count)'; - } - - return GestureDetector( - onTap: () => - BlocProvider.of(context).selectFilterTab(tabId), - child: Column( - children: [ - Padding( - padding: const EdgeInsets.only(bottom: UiConstants.space2), - child: Text( - text, - style: UiTypography.body2m.copyWith( - color: isSelected ? UiColors.primary : UiColors.textSecondary, - fontWeight: FontWeight.w600, - ), - ), - ), - AnimatedContainer( - duration: const Duration(milliseconds: 200), - height: 2, - width: isSelected ? 40 : 0, - decoration: BoxDecoration( - color: UiColors.primary, - borderRadius: BorderRadius.circular(2), - ), - ), - if (!isSelected) const SizedBox(height: 2), - ], - ), - ); - } - /// Builds the empty state view. Widget _buildEmptyState({ required BuildContext context, diff --git a/apps/mobile/packages/features/client/view_orders/lib/src/presentation/widgets/view_order_card.dart b/apps/mobile/packages/features/client/view_orders/lib/src/presentation/widgets/view_order_card.dart index 6886cfe0..7ded64a7 100644 --- a/apps/mobile/packages/features/client/view_orders/lib/src/presentation/widgets/view_order_card.dart +++ b/apps/mobile/packages/features/client/view_orders/lib/src/presentation/widgets/view_order_card.dart @@ -8,6 +8,7 @@ import 'package:intl/intl.dart'; import 'package:krow_data_connect/krow_data_connect.dart' as dc; import 'package:krow_domain/krow_domain.dart'; import 'package:url_launcher/url_launcher.dart'; + import '../blocs/view_orders_cubit.dart'; /// A rich card displaying details of a client order/shift. diff --git a/apps/mobile/packages/features/client/view_orders/lib/src/presentation/widgets/view_orders_filter_tab.dart b/apps/mobile/packages/features/client/view_orders/lib/src/presentation/widgets/view_orders_filter_tab.dart new file mode 100644 index 00000000..661face0 --- /dev/null +++ b/apps/mobile/packages/features/client/view_orders/lib/src/presentation/widgets/view_orders_filter_tab.dart @@ -0,0 +1,68 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../blocs/view_orders_cubit.dart'; + +/// A single filter tab for the View Orders page. +/// +/// Displays a label with an optional count and shows a selection indicator +/// when the tab is active. +class ViewOrdersFilterTab extends StatelessWidget { + /// Creates a [ViewOrdersFilterTab]. + const ViewOrdersFilterTab({ + required this.label, + required this.isSelected, + required this.tabId, + this.count, + super.key, + }); + + /// The label text to display. + final String label; + + /// Whether this tab is currently selected. + final bool isSelected; + + /// The unique identifier for this tab. + final String tabId; + + /// Optional count to display next to the label. + final int? count; + + @override + Widget build(BuildContext context) { + String text = label; + if (count != null) { + text = '$label ($count)'; + } + + return GestureDetector( + onTap: () => + BlocProvider.of(context).selectFilterTab(tabId), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.only(bottom: UiConstants.space2), + child: Text( + text, + style: UiTypography.body2m.copyWith( + color: isSelected ? UiColors.primary : UiColors.textSecondary, + fontWeight: FontWeight.w600, + ), + ), + ), + AnimatedContainer( + duration: const Duration(milliseconds: 200), + height: 2, + width: isSelected ? 40 : 0, + decoration: BoxDecoration( + color: UiColors.primary, + borderRadius: BorderRadius.circular(2), + ), + ), + if (!isSelected) const SizedBox(height: 2), + ], + ), + ); + } +} diff --git a/apps/mobile/packages/features/client/view_orders/lib/src/presentation/widgets/view_orders_header.dart b/apps/mobile/packages/features/client/view_orders/lib/src/presentation/widgets/view_orders_header.dart new file mode 100644 index 00000000..c53cf6f0 --- /dev/null +++ b/apps/mobile/packages/features/client/view_orders/lib/src/presentation/widgets/view_orders_header.dart @@ -0,0 +1,259 @@ +import 'dart:ui'; +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_modular/flutter_modular.dart'; +import 'package:intl/intl.dart'; +import 'package:core_localization/core_localization.dart'; +import 'package:krow_domain/krow_domain.dart'; +import '../blocs/view_orders_cubit.dart'; +import '../blocs/view_orders_state.dart'; +import '../navigation/view_orders_navigator.dart'; +import 'view_orders_filter_tab.dart'; + +/// The sticky header section for the View Orders page. +/// +/// This widget contains: +/// - Top bar with title and post button +/// - Filter tabs (Up Next, Active, Completed) +/// - Calendar navigation controls +/// - Horizontal calendar grid +class ViewOrdersHeader extends StatelessWidget { + /// Creates a [ViewOrdersHeader]. + const ViewOrdersHeader({ + required this.state, + required this.calendarDays, + super.key, + }); + + /// The current state of the view orders feature. + final ViewOrdersState state; + + /// The list of calendar days to display. + final List calendarDays; + + @override + Widget build(BuildContext context) { + return ClipRect( + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), + child: Container( + decoration: const BoxDecoration( + color: Color(0xCCFFFFFF), // White with 0.8 alpha + border: Border( + bottom: BorderSide(color: UiColors.separatorSecondary), + ), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Top Bar + Padding( + padding: const EdgeInsets.fromLTRB( + UiConstants.space5, + UiConstants.space5, + UiConstants.space5, + UiConstants.space3, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + t.client_view_orders.title, + style: UiTypography.headline3m.copyWith( + color: UiColors.textPrimary, + fontWeight: FontWeight.bold, + ), + ), + if (state.filteredOrders.isNotEmpty) + UiButton.primary( + text: t.client_view_orders.post_button, + leadingIcon: UiIcons.add, + onPressed: () => Modular.to.navigateToCreateOrder(), + size: UiButtonSize.small, + style: ElevatedButton.styleFrom( + minimumSize: const Size(0, 48), + maximumSize: const Size(0, 48), + ), + ), + ], + ), + ), + + // Filter Tabs + Padding( + padding: const EdgeInsets.only(bottom: UiConstants.space3), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ViewOrdersFilterTab( + label: t.client_view_orders.tabs.up_next, + isSelected: state.filterTab == 'all', + tabId: 'all', + count: state.upNextCount, + ), + const SizedBox(width: UiConstants.space6), + ViewOrdersFilterTab( + label: t.client_view_orders.tabs.active, + isSelected: state.filterTab == 'active', + tabId: 'active', + count: state.activeCount, + ), + const SizedBox(width: UiConstants.space6), + ViewOrdersFilterTab( + label: t.client_view_orders.tabs.completed, + isSelected: state.filterTab == 'completed', + tabId: 'completed', + count: state.completedCount, + ), + ], + ), + ), + + // Calendar Header controls + Padding( + padding: const EdgeInsets.symmetric( + horizontal: UiConstants.space5, + vertical: UiConstants.space2, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + IconButton( + icon: const Icon( + UiIcons.chevronLeft, + size: 20, + color: UiColors.iconSecondary, + ), + onPressed: () => BlocProvider.of( + context, + ).updateWeekOffset(-1), + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + splashRadius: 20, + ), + Text( + DateFormat('MMMM yyyy').format(calendarDays.first), + style: UiTypography.body2m.copyWith( + color: UiColors.textSecondary, + ), + ), + IconButton( + icon: const Icon( + UiIcons.chevronRight, + size: 20, + color: UiColors.iconSecondary, + ), + onPressed: () => BlocProvider.of( + context, + ).updateWeekOffset(1), + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + splashRadius: 20, + ), + ], + ), + ), + + // Calendar Grid + SizedBox( + height: 72, + child: ListView.separated( + padding: const EdgeInsets.symmetric( + horizontal: UiConstants.space5, + ), + scrollDirection: Axis.horizontal, + itemCount: 7, + separatorBuilder: (BuildContext context, int index) => + const SizedBox(width: UiConstants.space2), + itemBuilder: (BuildContext context, int index) { + final DateTime date = calendarDays[index]; + final bool isSelected = + state.selectedDate != null && + date.year == state.selectedDate!.year && + date.month == state.selectedDate!.month && + date.day == state.selectedDate!.day; + + // Check if this date has any shifts + final String dateStr = DateFormat( + 'yyyy-MM-dd', + ).format(date); + final bool hasShifts = state.orders.any( + (OrderItem s) => s.date == dateStr, + ); + + return GestureDetector( + onTap: () => BlocProvider.of( + context, + ).selectDate(date), + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + width: 48, + decoration: BoxDecoration( + color: isSelected ? UiColors.primary : UiColors.white, + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: isSelected + ? UiColors.primary + : UiColors.separatorPrimary, + ), + boxShadow: isSelected + ? [ + BoxShadow( + color: UiColors.primary.withValues( + alpha: 0.25, + ), + blurRadius: 12, + offset: const Offset(0, 4), + ), + ] + : null, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + DateFormat('dd').format(date), + style: UiTypography.title2b.copyWith( + fontSize: 18, + color: isSelected + ? UiColors.white + : UiColors.textPrimary, + ), + ), + Text( + DateFormat('E').format(date), + style: UiTypography.footnote2m.copyWith( + color: isSelected + ? UiColors.white.withValues(alpha: 0.8) + : UiColors.textSecondary, + ), + ), + if (hasShifts) ...[ + const SizedBox(height: UiConstants.space1), + Container( + width: 6, + height: 6, + decoration: BoxDecoration( + color: isSelected + ? UiColors.white + : UiColors.primary, + shape: BoxShape.circle, + ), + ), + ], + ], + ), + ), + ); + }, + ), + ), + const SizedBox(height: UiConstants.space4), + ], + ), + ), + ), + ); + } +} From 3489ae4060ae25e28d9af5606e75a984a1fd20b7 Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Sun, 1 Feb 2026 21:23:58 -0500 Subject: [PATCH 12/15] feat: Add checkCircle icon and update ViewOrderCard and ViewOrdersHeader for improved date handling and UI feedback --- .../design_system/lib/src/ui_icons.dart | 3 + .../presentation/widgets/view_order_card.dart | 12 +- .../widgets/view_orders_header.dart | 119 ++++++++++-------- .../features/client/view_orders/pubspec.yaml | 4 + 4 files changed, 81 insertions(+), 57 deletions(-) diff --git a/apps/mobile/packages/design_system/lib/src/ui_icons.dart b/apps/mobile/packages/design_system/lib/src/ui_icons.dart index e035bf63..6acff6a9 100644 --- a/apps/mobile/packages/design_system/lib/src/ui_icons.dart +++ b/apps/mobile/packages/design_system/lib/src/ui_icons.dart @@ -63,6 +63,9 @@ class UiIcons { /// Checkmark icon static const IconData check = _IconLib.check; + /// Checkmark circle icon + static const IconData checkCircle = _IconLib.checkCircle; + /// X/Cancel icon static const IconData close = _IconLib.x; diff --git a/apps/mobile/packages/features/client/view_orders/lib/src/presentation/widgets/view_order_card.dart b/apps/mobile/packages/features/client/view_orders/lib/src/presentation/widgets/view_order_card.dart index 7ded64a7..76416e5d 100644 --- a/apps/mobile/packages/features/client/view_orders/lib/src/presentation/widgets/view_order_card.dart +++ b/apps/mobile/packages/features/client/view_orders/lib/src/presentation/widgets/view_order_card.dart @@ -326,15 +326,23 @@ class _ViewOrderCardState extends State { children: [ Row( children: [ - if (order.workersNeeded != 0) + if (coveragePercent != 100) const Icon( UiIcons.error, size: 16, color: UiColors.textError, ), + if (coveragePercent == 100) + const Icon( + UiIcons.checkCircle, + size: 16, + color: UiColors.textSuccess, + ), const SizedBox(width: 8), Text( - '${order.workersNeeded} Workers Needed', + coveragePercent == 100 + ? 'All Workers Confirmed' + : '${order.workersNeeded} Workers Needed', style: UiTypography.body2m.textPrimary, ), ], diff --git a/apps/mobile/packages/features/client/view_orders/lib/src/presentation/widgets/view_orders_header.dart b/apps/mobile/packages/features/client/view_orders/lib/src/presentation/widgets/view_orders_header.dart index c53cf6f0..45e72f93 100644 --- a/apps/mobile/packages/features/client/view_orders/lib/src/presentation/widgets/view_orders_header.dart +++ b/apps/mobile/packages/features/client/view_orders/lib/src/presentation/widgets/view_orders_header.dart @@ -182,67 +182,76 @@ class ViewOrdersHeader extends StatelessWidget { (OrderItem s) => s.date == dateStr, ); - return GestureDetector( - onTap: () => BlocProvider.of( - context, - ).selectDate(date), - child: AnimatedContainer( - duration: const Duration(milliseconds: 200), - width: 48, - decoration: BoxDecoration( - color: isSelected ? UiColors.primary : UiColors.white, - borderRadius: BorderRadius.circular(16), - border: Border.all( - color: isSelected - ? UiColors.primary - : UiColors.separatorPrimary, - ), - boxShadow: isSelected - ? [ - BoxShadow( - color: UiColors.primary.withValues( - alpha: 0.25, + // Check if date is in the past + final DateTime now = DateTime.now(); + final DateTime today = DateTime(now.year, now.month, now.day); + final DateTime checkDate = DateTime(date.year, date.month, date.day); + final bool isPast = checkDate.isBefore(today); + + return Opacity( + opacity: isPast && !isSelected ? 0.5 : 1.0, + child: GestureDetector( + onTap: () => BlocProvider.of( + context, + ).selectDate(date), + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + width: 48, + decoration: BoxDecoration( + color: isSelected ? UiColors.primary : UiColors.white, + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: isSelected + ? UiColors.primary + : UiColors.separatorPrimary, + ), + boxShadow: isSelected + ? [ + BoxShadow( + color: UiColors.primary.withValues( + alpha: 0.25, + ), + blurRadius: 12, + offset: const Offset(0, 4), ), - blurRadius: 12, - offset: const Offset(0, 4), - ), - ] - : null, - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - DateFormat('dd').format(date), - style: UiTypography.title2b.copyWith( - fontSize: 18, - color: isSelected - ? UiColors.white - : UiColors.textPrimary, - ), - ), - Text( - DateFormat('E').format(date), - style: UiTypography.footnote2m.copyWith( - color: isSelected - ? UiColors.white.withValues(alpha: 0.8) - : UiColors.textSecondary, - ), - ), - if (hasShifts) ...[ - const SizedBox(height: UiConstants.space1), - Container( - width: 6, - height: 6, - decoration: BoxDecoration( + ] + : null, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + DateFormat('dd').format(date), + style: UiTypography.title2b.copyWith( + fontSize: 18, color: isSelected ? UiColors.white - : UiColors.primary, - shape: BoxShape.circle, + : UiColors.textPrimary, ), ), + Text( + DateFormat('E').format(date), + style: UiTypography.footnote2m.copyWith( + color: isSelected + ? UiColors.white.withValues(alpha: 0.8) + : UiColors.textSecondary, + ), + ), + if (hasShifts) ...[ + const SizedBox(height: UiConstants.space1), + Container( + width: 6, + height: 6, + decoration: BoxDecoration( + color: isSelected + ? UiColors.white + : UiColors.primary, + shape: BoxShape.circle, + ), + ), + ], ], - ], + ), ), ), ); diff --git a/apps/mobile/packages/features/client/view_orders/pubspec.yaml b/apps/mobile/packages/features/client/view_orders/pubspec.yaml index 5c419aa9..46182d70 100644 --- a/apps/mobile/packages/features/client/view_orders/pubspec.yaml +++ b/apps/mobile/packages/features/client/view_orders/pubspec.yaml @@ -25,10 +25,14 @@ dependencies: path: ../../../domain krow_core: path: ../../../core + krow_data_connect: + path: ../../../data_connect # UI lucide_icons: ^0.257.0 intl: ^0.20.1 url_launcher: ^6.3.1 + firebase_data_connect: ^0.2.2+2 + firebase_auth: ^6.1.4 dev_dependencies: flutter_test: From 6d0d7dcbd2805a6f73f664db43e895cd47890fbd Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Sun, 1 Feb 2026 21:45:40 -0500 Subject: [PATCH 13/15] feat: Refactor PhoneInput to use StatefulWidget and improve phone number handling --- .../pages/phone_verification_page.dart | 10 +++--- .../phone_verification_page/phone_input.dart | 35 +++++++++++++------ .../repositories/home_repository_impl.dart | 2 +- .../presentation/pages/worker_home_page.dart | 1 - .../src/presentation/widgets/shift_card.dart | 6 ++-- 5 files changed, 32 insertions(+), 22 deletions(-) diff --git a/apps/mobile/packages/features/staff/authentication/lib/src/presentation/pages/phone_verification_page.dart b/apps/mobile/packages/features/staff/authentication/lib/src/presentation/pages/phone_verification_page.dart index b58ed1bf..0192487c 100644 --- a/apps/mobile/packages/features/staff/authentication/lib/src/presentation/pages/phone_verification_page.dart +++ b/apps/mobile/packages/features/staff/authentication/lib/src/presentation/pages/phone_verification_page.dart @@ -1,15 +1,15 @@ +import 'package:design_system/design_system.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_modular/flutter_modular.dart'; -import 'package:design_system/design_system.dart'; -import 'package:staff_authentication/src/domain/ui_entities/auth_mode.dart'; +import 'package:staff_authentication/src/presentation/blocs/auth_bloc.dart'; import 'package:staff_authentication/src/presentation/blocs/auth_event.dart'; import 'package:staff_authentication/src/presentation/blocs/auth_state.dart'; -import 'package:staff_authentication/src/presentation/blocs/auth_bloc.dart'; -import '../widgets/phone_verification_page/phone_input.dart'; -import '../widgets/phone_verification_page/otp_verification.dart'; import 'package:staff_authentication/staff_authentication.dart'; + import '../navigation/auth_navigator.dart'; // Import the extension +import '../widgets/phone_verification_page/otp_verification.dart'; +import '../widgets/phone_verification_page/phone_input.dart'; /// A combined page for phone number entry and OTP verification. /// diff --git a/apps/mobile/packages/features/staff/authentication/lib/src/presentation/widgets/phone_verification_page/phone_input.dart b/apps/mobile/packages/features/staff/authentication/lib/src/presentation/widgets/phone_verification_page/phone_input.dart index 01be5bf4..9ad647f3 100644 --- a/apps/mobile/packages/features/staff/authentication/lib/src/presentation/widgets/phone_verification_page/phone_input.dart +++ b/apps/mobile/packages/features/staff/authentication/lib/src/presentation/widgets/phone_verification_page/phone_input.dart @@ -9,15 +9,29 @@ import 'phone_input/phone_input_form_field.dart'; import 'phone_input/phone_input_header.dart'; /// A widget that displays the phone number entry UI. -class PhoneInput extends StatelessWidget { +class PhoneInput extends StatefulWidget { + /// Creates a [PhoneInput]. + const PhoneInput({super.key, required this.state, required this.onSendCode}); + /// The current state of the authentication process. final AuthState state; /// Callback for when the "Send Code" action is triggered. final VoidCallback onSendCode; - /// Creates a [PhoneInput]. - const PhoneInput({super.key, required this.state, required this.onSendCode}); + @override + State createState() => _PhoneInputState(); +} + +class _PhoneInputState extends State { + void _handlePhoneChanged(String value) { + if (!mounted) return; + + final AuthBloc bloc = context.read(); + if (!bloc.isClosed) { + bloc.add(AuthPhoneUpdated(value)); + } + } @override Widget build(BuildContext context) { @@ -35,19 +49,18 @@ class PhoneInput extends StatelessWidget { const PhoneInputHeader(), const SizedBox(height: UiConstants.space8), PhoneInputFormField( - initialValue: state.phoneNumber, - error: state.errorMessage ?? '', - onChanged: (String value) { - BlocProvider.of( - context, - ).add(AuthPhoneUpdated(value)); - }, + initialValue: widget.state.phoneNumber, + error: widget.state.errorMessage ?? '', + onChanged: _handlePhoneChanged, ), ], ), ), ), - PhoneInputActions(isLoading: state.isLoading, onSendCode: onSendCode), + PhoneInputActions( + isLoading: widget.state.isLoading, + onSendCode: widget.onSendCode, + ), ], ); } diff --git a/apps/mobile/packages/features/staff/home/lib/src/data/repositories/home_repository_impl.dart b/apps/mobile/packages/features/staff/home/lib/src/data/repositories/home_repository_impl.dart index 508e350a..40e6ddfe 100644 --- a/apps/mobile/packages/features/staff/home/lib/src/data/repositories/home_repository_impl.dart +++ b/apps/mobile/packages/features/staff/home/lib/src/data/repositories/home_repository_impl.dart @@ -44,7 +44,7 @@ class HomeRepositoryImpl implements HomeRepository { if (shiftDate == null) return false; final isDateMatch = DateFormat('yyyy-MM-dd').format(shiftDate) == targetYmd; - final isAssigned = app.status is Known && (app.status as Known).value == ApplicationStatus.ACCEPTED; + final isAssigned = app.status is Known && ((app.status as Known).value == ApplicationStatus.ACCEPTED || (app.status as Known).value == ApplicationStatus.CONFIRMED); return isDateMatch && isAssigned; }) diff --git a/apps/mobile/packages/features/staff/home/lib/src/presentation/pages/worker_home_page.dart b/apps/mobile/packages/features/staff/home/lib/src/presentation/pages/worker_home_page.dart index 1cbb51fc..a9b3f169 100644 --- a/apps/mobile/packages/features/staff/home/lib/src/presentation/pages/worker_home_page.dart +++ b/apps/mobile/packages/features/staff/home/lib/src/presentation/pages/worker_home_page.dart @@ -13,7 +13,6 @@ import 'package:staff_home/src/presentation/widgets/home_page/quick_action_item. import 'package:staff_home/src/presentation/widgets/home_page/recommended_shift_card.dart'; import 'package:staff_home/src/presentation/widgets/home_page/section_header.dart'; import 'package:staff_home/src/presentation/widgets/shift_card.dart'; -import 'package:staff_home/src/presentation/widgets/worker/auto_match_toggle.dart'; /// The home page for the staff worker application. /// diff --git a/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/shift_card.dart b/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/shift_card.dart index f2a95f0d..f223bbcd 100644 --- a/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/shift_card.dart +++ b/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/shift_card.dart @@ -5,6 +5,7 @@ import 'package:intl/intl.dart'; import 'package:design_system/design_system.dart'; import 'package:krow_domain/krow_domain.dart'; +import '../navigation/home_navigator.dart'; class ShiftCard extends StatefulWidget { final Shift shift; @@ -73,10 +74,7 @@ class _ShiftCardState extends State { ? null : () { setState(() => isExpanded = !isExpanded); - Modular.to.pushNamed( - '/shift-details/${widget.shift.id}', - arguments: widget.shift, - ); + Modular.to.pushShiftDetails(widget.shift); }, child: Container( margin: const EdgeInsets.only(bottom: 12), From 476d697dc3000dcb164a8c2be819580ed0cab012 Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Sun, 1 Feb 2026 22:18:07 -0500 Subject: [PATCH 14/15] feat: Integrate ShiftAdapter for mapping application data to Shift entities and update status handling in Coverage and Shifts repositories --- .../src/adapters/shifts/shift_adapter.dart | 59 ++++++++- .../coverage_repository_impl.dart | 2 +- .../repositories/home_repository_impl.dart | 77 +++++++----- .../shifts_repository_impl.dart | 113 +++++++----------- 4 files changed, 146 insertions(+), 105 deletions(-) diff --git a/apps/mobile/packages/domain/lib/src/adapters/shifts/shift_adapter.dart b/apps/mobile/packages/domain/lib/src/adapters/shifts/shift_adapter.dart index 07cab44a..6022d327 100644 --- a/apps/mobile/packages/domain/lib/src/adapters/shifts/shift_adapter.dart +++ b/apps/mobile/packages/domain/lib/src/adapters/shifts/shift_adapter.dart @@ -1,10 +1,59 @@ +import 'package:intl/intl.dart'; import '../../entities/shifts/shift.dart'; /// Adapter for Shift related data. class ShiftAdapter { - - // Note: Conversion logic will likely live in RepoImpl or here if we pass raw objects. - // Given we are dealing with generated types that aren't exported by domain, - // we might put the logic in Repo or make this accept dynamic/Map if strictly required. - // For now, placeholders or simple status helpers. + /// Maps application data to a Shift entity. + /// + /// This method handles the common mapping logic used across different + /// repositories when converting application data from Data Connect to + /// domain Shift entities. + static Shift fromApplicationData({ + required String shiftId, + required String roleId, + required String roleName, + required String businessName, + String? companyLogoUrl, + required double costPerHour, + String? shiftLocation, + required String teamHubName, + DateTime? shiftDate, + DateTime? startTime, + DateTime? endTime, + DateTime? createdAt, + required String status, + String? description, + int? durationDays, + required int count, + int? assigned, + String? eventName, + bool hasApplied = false, + }) { + final String orderName = (eventName ?? '').trim().isNotEmpty + ? eventName! + : businessName; + final String title = '$roleName - $orderName'; + + return Shift( + id: shiftId, + roleId: roleId, + title: title, + clientName: businessName, + logoUrl: companyLogoUrl, + hourlyRate: costPerHour, + location: shiftLocation ?? '', + locationAddress: teamHubName, + date: shiftDate?.toIso8601String() ?? '', + startTime: startTime != null ? DateFormat('HH:mm').format(startTime) : '', + endTime: endTime != null ? DateFormat('HH:mm').format(endTime) : '', + createdDate: createdAt?.toIso8601String() ?? '', + status: status, + description: description, + durationDays: durationDays, + requiredSlots: count, + filledSlots: assigned ?? 0, + hasApplied: hasApplied, + ); + } } + diff --git a/apps/mobile/packages/features/client/client_coverage/lib/src/data/repositories_impl/coverage_repository_impl.dart b/apps/mobile/packages/features/client/client_coverage/lib/src/data/repositories_impl/coverage_repository_impl.dart index 9ee38782..cfecec36 100644 --- a/apps/mobile/packages/features/client/client_coverage/lib/src/data/repositories_impl/coverage_repository_impl.dart +++ b/apps/mobile/packages/features/client/client_coverage/lib/src/data/repositories_impl/coverage_repository_impl.dart @@ -178,7 +178,7 @@ class CoverageRepositoryImpl implements CoverageRepository { case dc.ApplicationStatus.PENDING: return CoverageWorkerStatus.pending; case dc.ApplicationStatus.ACCEPTED: - return CoverageWorkerStatus.accepted; + return CoverageWorkerStatus.confirmed; case dc.ApplicationStatus.REJECTED: return CoverageWorkerStatus.rejected; case dc.ApplicationStatus.CONFIRMED: diff --git a/apps/mobile/packages/features/staff/home/lib/src/data/repositories/home_repository_impl.dart b/apps/mobile/packages/features/staff/home/lib/src/data/repositories/home_repository_impl.dart index 40e6ddfe..6769036f 100644 --- a/apps/mobile/packages/features/staff/home/lib/src/data/repositories/home_repository_impl.dart +++ b/apps/mobile/packages/features/staff/home/lib/src/data/repositories/home_repository_impl.dart @@ -32,28 +32,40 @@ class HomeRepositoryImpl implements HomeRepository { Future> _getShiftsForDate(DateTime date) async { try { + final staffId = _currentStaffId; + + // Create start and end timestamps for the target date + final DateTime start = DateTime(date.year, date.month, date.day); + final DateTime end = DateTime(date.year, date.month, date.day, 23, 59, 59, 999); + final response = await ExampleConnector.instance - .getApplicationsByStaffId(staffId: _currentStaffId) + .getApplicationsByStaffId(staffId: staffId) + .dayStart(_toTimestamp(start)) + .dayEnd(_toTimestamp(end)) .execute(); - final targetYmd = DateFormat('yyyy-MM-dd').format(date); - - return response.data.applications - .where((app) { - final shiftDate = app.shift.date?.toDate(); - if (shiftDate == null) return false; - - final isDateMatch = DateFormat('yyyy-MM-dd').format(shiftDate) == targetYmd; - final isAssigned = app.status is Known && ((app.status as Known).value == ApplicationStatus.ACCEPTED || (app.status as Known).value == ApplicationStatus.CONFIRMED); - - return isDateMatch && isAssigned; - }) - .map((app) => _mapApplicationToShift(app)) - .toList(); + // Filter for ACCEPTED applications (same logic as shifts_repository_impl) + final apps = response.data.applications.where( + (app) => (app.status is Known && (app.status as Known).value == ApplicationStatus.ACCEPTED) || (app.status is Known && (app.status as Known).value == ApplicationStatus.CONFIRMED) + ); + + final List shifts = []; + for (final app in apps) { + shifts.add(_mapApplicationToShift(app)); + } + + return shifts; } catch (e) { return []; } } + + Timestamp _toTimestamp(DateTime dateTime) { + final DateTime utc = dateTime.toUtc(); + final int seconds = utc.millisecondsSinceEpoch ~/ 1000; + final int nanoseconds = (utc.microsecondsSinceEpoch % 1000000) * 1000; + return Timestamp(nanoseconds, seconds); + } @override Future> getRecommendedShifts() async { @@ -93,21 +105,26 @@ class HomeRepositoryImpl implements HomeRepository { final s = app.shift; final r = app.shiftRole; - return Shift( - id: s.id, - title: r.role.name, - clientName: s.order.business.businessName, - hourlyRate: r.role.costPerHour, - location: s.location ?? 'Unknown', - locationAddress: s.location ?? '', - date: s.date?.toDate().toIso8601String() ?? '', - startTime: DateFormat('HH:mm').format(r.startTime?.toDate() ?? DateTime.now()), - endTime: DateFormat('HH:mm').format(r.endTime?.toDate() ?? DateTime.now()), - createdDate: app.createdAt?.toDate().toIso8601String() ?? '', - tipsAvailable: false, // Not in API - mealProvided: false, // Not in API - managers: [], // Not in this query - description: null, + return ShiftAdapter.fromApplicationData( + shiftId: s.id, + roleId: r.roleId, + roleName: r.role.name, + businessName: s.order.business.businessName, + companyLogoUrl: s.order.business.companyLogoUrl, + costPerHour: r.role.costPerHour, + shiftLocation: s.location, + teamHubName: s.order.teamHub.hubName, + shiftDate: s.date?.toDate(), + startTime: r.startTime?.toDate(), + endTime: r.endTime?.toDate(), + createdAt: app.createdAt?.toDate(), + status: 'confirmed', + description: s.description, + durationDays: s.durationDays, + count: r.count, + assigned: r.assigned, + eventName: s.order.eventName, + hasApplied: true, ); } diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/data/repositories_impl/shifts_repository_impl.dart b/apps/mobile/packages/features/staff/shifts/lib/src/data/repositories_impl/shifts_repository_impl.dart index 347f389f..8376b88b 100644 --- a/apps/mobile/packages/features/staff/shifts/lib/src/data/repositories_impl/shifts_repository_impl.dart +++ b/apps/mobile/packages/features/staff/shifts/lib/src/data/repositories_impl/shifts_repository_impl.dart @@ -77,13 +77,42 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface { return null; } + /// Helper method to map Data Connect application to domain Shift using ShiftAdapter. + Shift _mapApplicationToShift( + dynamic app, + String status, { + bool hasApplied = true, + }) { + return ShiftAdapter.fromApplicationData( + shiftId: app.shift.id, + roleId: app.shiftRole.roleId, + roleName: app.shiftRole.role.name, + businessName: app.shift.order.business.businessName, + companyLogoUrl: app.shift.order.business.companyLogoUrl, + costPerHour: app.shiftRole.role.costPerHour, + shiftLocation: app.shift.location, + teamHubName: app.shift.order.teamHub.hubName, + shiftDate: _toDateTime(app.shift.date), + startTime: _toDateTime(app.shiftRole.startTime), + endTime: _toDateTime(app.shiftRole.endTime), + createdAt: _toDateTime(app.createdAt), + status: status, + description: app.shift.description, + durationDays: app.shift.durationDays, + count: app.shiftRole.count, + assigned: app.shiftRole.assigned, + eventName: app.shift.order.eventName, + hasApplied: hasApplied, + ); + } + @override Future> getMyShifts({ required DateTime start, required DateTime end, }) async { return _fetchApplications( - dc.ApplicationStatus.ACCEPTED, + [dc.ApplicationStatus.ACCEPTED, dc.ApplicationStatus.CONFIRMED], start: start, end: end, ); @@ -91,12 +120,12 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface { @override Future> getPendingAssignments() async { - return _fetchApplications(dc.ApplicationStatus.PENDING); + return _fetchApplications([dc.ApplicationStatus.PENDING]); } @override Future> getCancelledShifts() async { - return _fetchApplications(dc.ApplicationStatus.REJECTED); + return _fetchApplications([dc.ApplicationStatus.REJECTED]); } @override @@ -112,37 +141,10 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface { _shiftToAppIdMap[app.shift.id] = app.id; _appToRoleIdMap[app.id] = app.shiftRole.id; - final String roleName = app.shiftRole.role.name; - final String orderName = - (app.shift.order.eventName ?? '').trim().isNotEmpty - ? app.shift.order.eventName! - : app.shift.order.business.businessName; - final String title = '$roleName - $orderName'; - final DateTime? shiftDate = _toDateTime(app.shift.date); - final DateTime? startDt = _toDateTime(app.shiftRole.startTime); - final DateTime? endDt = _toDateTime(app.shiftRole.endTime); - final DateTime? createdDt = _toDateTime(app.createdAt); - shifts.add( - Shift( - id: app.shift.id, - roleId: app.shiftRole.roleId, - title: title, - clientName: app.shift.order.business.businessName, - logoUrl: app.shift.order.business.companyLogoUrl, - hourlyRate: app.shiftRole.role.costPerHour, - location: app.shift.location ?? '', - locationAddress: app.shift.order.teamHub.hubName, - date: shiftDate?.toIso8601String() ?? '', - startTime: startDt != null ? DateFormat('HH:mm').format(startDt) : '', - endTime: endDt != null ? DateFormat('HH:mm').format(endDt) : '', - createdDate: createdDt?.toIso8601String() ?? '', - status: _mapStatus(dc.ApplicationStatus.CHECKED_OUT), - description: app.shift.description, - durationDays: app.shift.durationDays, - requiredSlots: app.shiftRole.count, - filledSlots: app.shiftRole.assigned ?? 0, - hasApplied: true, + _mapApplicationToShift( + app, + _mapStatus(dc.ApplicationStatus.CHECKED_OUT), ), ); } @@ -153,7 +155,7 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface { } Future> _fetchApplications( - dc.ApplicationStatus status, { + List statuses, { DateTime? start, DateTime? end, }) async { @@ -167,8 +169,9 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface { } final response = await query.execute(); + final statusNames = statuses.map((s) => s.name).toSet(); final apps = response.data.applications.where( - (app) => app.status.stringValue == status.name, + (app) => statusNames.contains(app.status.stringValue), ); final List shifts = []; @@ -176,40 +179,12 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface { _shiftToAppIdMap[app.shift.id] = app.id; _appToRoleIdMap[app.id] = app.shiftRole.id; - final String roleName = app.shiftRole.role.name; - final String orderName = - (app.shift.order.eventName ?? '').trim().isNotEmpty - ? app.shift.order.eventName! - : app.shift.order.business.businessName; - final String title = '$roleName - $orderName'; - final DateTime? shiftDate = _toDateTime(app.shift.date); - final DateTime? startDt = _toDateTime(app.shiftRole.startTime); - final DateTime? endDt = _toDateTime(app.shiftRole.endTime); - final DateTime? createdDt = _toDateTime(app.createdAt); - - // Override status to reflect the application state (e.g., CHECKED_OUT, ACCEPTED) - shifts.add( - Shift( - id: app.shift.id, - roleId: app.shiftRole.roleId, - title: title, - clientName: app.shift.order.business.businessName, - logoUrl: app.shift.order.business.companyLogoUrl, - hourlyRate: app.shiftRole.role.costPerHour, - location: app.shift.location ?? '', - locationAddress: app.shift.order.teamHub.hubName, - date: shiftDate?.toIso8601String() ?? '', - startTime: startDt != null ? DateFormat('HH:mm').format(startDt) : '', - endTime: endDt != null ? DateFormat('HH:mm').format(endDt) : '', - createdDate: createdDt?.toIso8601String() ?? '', - status: _mapStatus(status), - description: app.shift.description, - durationDays: app.shift.durationDays, - requiredSlots: app.shiftRole.count, - filledSlots: app.shiftRole.assigned ?? 0, - hasApplied: true, - ), + // Use the first matching status for mapping + final matchingStatus = statuses.firstWhere( + (s) => s.name == app.status.stringValue, + orElse: () => statuses.first, ); + shifts.add(_mapApplicationToShift(app, _mapStatus(matchingStatus))); } return shifts; } catch (e) { @@ -491,7 +466,7 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface { shiftId: shiftId, staffId: staffId, roleId: targetRoleId, - status: dc.ApplicationStatus.ACCEPTED, + status: dc.ApplicationStatus.CONFIRMED, origin: dc.ApplicationOrigin.STAFF, ) // TODO: this should be PENDING so a vendor can accept it. From 439971bfaba16bf8ab7f5a0205c8e8d282ba36c3 Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Sun, 1 Feb 2026 22:28:43 -0500 Subject: [PATCH 15/15] feat: Update BLOCKERS.md to include new staff application session handling and assumptions for shift assignments --- BLOCKERS.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/BLOCKERS.md b/BLOCKERS.md index 8c5ceb33..af8df57d 100644 --- a/BLOCKERS.md +++ b/BLOCKERS.md @@ -46,4 +46,12 @@ - Staff APP: - On app launch, check whether there is an active session. If a valid session exists, skip the auth flow and navigate directly to Home, loading Staff account. - Add an expiration time (TTL) to the session (store expiresAt / expiryTimestamp) and invalidate/clear the session when it has expired. - - For staffs Skills = Roles? thinking in the future for the smart assigned that need to know the roles of staff to assign. \ No newline at end of file + - For staffs Skills = Roles? thinking in the future for the smart assigned that need to know the roles of staff to assign. + +## App +- Staff Application + +### Github issue +- https://github.com/Oloodi/krow-workforce/issues/248 +### Deveations: +- Assumed that a worker can only have one shift per day.