diff --git a/.gitignore b/.gitignore index 9f3989b2..00f2c2ca 100644 --- a/.gitignore +++ b/.gitignore @@ -18,7 +18,6 @@ $RECYCLE.BIN/ # IDE & Editors .idea/ -.vscode/ *.iml *.iws *.swp @@ -111,6 +110,9 @@ vite.config.ts.timestamp-* .flutter-plugins .flutter-plugins-dependencies +# Firebase Data Connect Generated SDK (regenerated via make mobile-install) +**/dataconnect_generated/ + # Android .gradle/ **/android/app/libs/ @@ -127,6 +129,12 @@ build/ **/ios/Pods/ **/ios/.symlinks/ +# Ephemeral files (generated by Flutter for desktop platforms) +**/linux/flutter/ephemeral/ +**/windows/flutter/ephemeral/ +**/macos/Flutter/ephemeral/ +**/ios/Flutter/ephemeral/ + # ============================================================================== # FIREBASE & BACKEND # ============================================================================== diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..437dd654 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,41 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Client (Dev) - Android", + "request": "launch", + "type": "dart", + "program": "apps/mobile/apps/client/lib/main.dart", + "args": [ + "--dart-define-from-file=${workspaceFolder}/apps/mobile/config.dev.json" + ] + }, + { + "name": "Client (Dev) - iOS", + "request": "launch", + "type": "dart", + "program": "apps/mobile/apps/client/lib/main.dart", + "args": [ + "--dart-define-from-file=${workspaceFolder}/apps/mobile/config.dev.json" + ] + }, + { + "name": "Staff (Dev) - Android", + "request": "launch", + "type": "dart", + "program": "apps/mobile/apps/staff/lib/main.dart", + "args": [ + "--dart-define-from-file=${workspaceFolder}/apps/mobile/config.dev.json" + ] + }, + { + "name": "Staff (Dev) - iOS", + "request": "launch", + "type": "dart", + "program": "apps/mobile/apps/staff/lib/main.dart", + "args": [ + "--dart-define-from-file=${workspaceFolder}/apps/mobile/config.dev.json" + ] + } + ] +} \ No newline at end of file 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. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..9403874e --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,129 @@ +# CLAUDE.md - Project Context for AI Assistants + +This file provides context for Claude Code and other AI assistants working on this codebase. + +## Project Overview + +**KROW Workforce** is a workforce management platform connecting businesses with temporary/on-demand workers. It consists of: +- **Client App**: For businesses to create orders, manage hubs, handle billing +- **Staff App**: For workers to manage availability, clock in/out, view earnings +- **Web Dashboard**: Admin portal (React/Vite - WIP) +- **Backend**: Firebase Data Connect + PostgreSQL on Cloud SQL + +## Monorepo Structure + +``` +/apps + /mobile # Flutter apps (managed by Melos) + /apps + /client # krowwithus_client - Business app + /staff # krowwithus_staff - Worker app + /design_system_viewer + /packages + /core # Base utilities + /domain # Business entities, repository interfaces + /data_connect # Data layer, Firebase Data Connect SDK + /design_system # Shared UI components + /core_localization # i18n (Slang) + /features + /client/* # Client-specific features + /staff/* # Staff-specific features + /web-dashboard # React web app (WIP) +/backend + /dataconnect # GraphQL schemas, Firebase Data Connect config + /cloud-functions # Serverless functions (placeholder) +/internal + /launchpad # Internal DevOps portal + /api-harness # API testing tool +/makefiles # Modular Make targets +/docs # Project documentation +``` + +## Key Commands + +### Mobile Development +```bash +# Install dependencies +make mobile-install + +# Run client app (specify your device ID) +make mobile-client-dev-android DEVICE= + +# Run staff app +make mobile-staff-dev-android DEVICE= + +# Find your device ID +flutter devices + +# Build APK +make mobile-client-build PLATFORM=apk +make mobile-staff-build PLATFORM=apk + +# Code generation (localization + build_runner) +cd apps/mobile && melos run gen:all +``` + +### Web Development +```bash +make install # Install web dependencies +make dev # Run web dev server +``` + +### Data Connect +```bash +make dataconnect-sync # Deploy schemas, migrate, regenerate SDK +``` + +## Architecture Patterns + +- **State Management**: BLoC pattern (flutter_bloc) +- **Navigation**: Flutter Modular +- **Architecture**: Clean Architecture (domain/data/presentation layers) +- **Feature Organization**: Each feature is a separate package +- **Value Objects**: Equatable for entity equality + +## Code Conventions + +- Features go in `/apps/mobile/packages/features/{client|staff}/` +- Shared code goes in `/apps/mobile/packages/{core|domain|data_connect}/` +- UI components go in `/apps/mobile/packages/design_system/` +- GraphQL schemas go in `/backend/dataconnect/schema/` +- Documentation language: **English** + +## Important Files + +- `apps/mobile/melos.yaml` - Melos workspace config +- `makefiles/mobile.mk` - Mobile Make targets +- `backend/dataconnect/dataconnect.yaml` - Data Connect config +- `firebase.json` - Firebase hosting/emulator config +- `BLOCKERS.md` - Known blockers and deviations + +## Branch Protection + +- `main` and `dev` branches are protected +- Always create feature branches: `feature/`, `fix/`, `chore/` +- PRs required for merging + +## Testing Mobile Apps + +1. Connect your Android device or start emulator +2. Run `flutter devices` to get device ID +3. Run `make mobile-client-dev-android DEVICE=` + +## Common Issues + +### "No devices found with name 'android'" +The Makefile defaults to device ID `android`. Override with your actual device: +```bash +make mobile-client-dev-android DEVICE=3fb285a7 +``` + +### Dependency resolution issues +```bash +cd apps/mobile && melos clean && melos bootstrap +``` + +### Code generation out of sync +```bash +cd apps/mobile && melos run gen:all +``` diff --git a/GEMINI.md b/GEMINI.md new file mode 100644 index 00000000..1a9c4fe0 --- /dev/null +++ b/GEMINI.md @@ -0,0 +1,138 @@ +# GEMINI.md - Project Context for AI Assistants + +This file provides context for Gemini and other AI assistants working on this codebase. + +## Project Overview + +**KROW Workforce** is a workforce management platform connecting businesses with temporary/on-demand workers. It consists of: +- **Client App**: For businesses to create orders, manage hubs, handle billing +- **Staff App**: For workers to manage availability, clock in/out, view earnings +- **Web Dashboard**: Admin portal (React/Vite - WIP) +- **Backend**: Firebase Data Connect + PostgreSQL on Cloud SQL + +## Monorepo Structure + +``` +/apps + /mobile # Flutter apps (managed by Melos) + /apps + /client # krowwithus_client - Business app + /staff # krowwithus_staff - Worker app + /design_system_viewer + /packages + /core # Base utilities + /domain # Business entities, repository interfaces + /data_connect # Data layer, Firebase Data Connect SDK + /design_system # Shared UI components + /core_localization # i18n (Slang) + /features + /client/* # Client-specific features + /staff/* # Staff-specific features + /web-dashboard # React web app (WIP) +/backend + /dataconnect # GraphQL schemas, Firebase Data Connect config + /cloud-functions # Serverless functions (placeholder) +/internal + /launchpad # Internal DevOps portal + /api-harness # API testing tool +/makefiles # Modular Make targets +/docs # Project documentation +/bugs # Bug reports and screenshots +``` + +## Key Commands + +### Mobile Development +```bash +# Install dependencies +make mobile-install + +# Run client app (specify your device ID) +make mobile-client-dev-android DEVICE= + +# Run staff app +make mobile-staff-dev-android DEVICE= + +# Find your device ID +flutter devices + +# Build APK +make mobile-client-build PLATFORM=apk +make mobile-staff-build PLATFORM=apk + +# Code generation (localization + build_runner) +cd apps/mobile && melos run gen:all +``` + +### Web Development +```bash +make install # Install web dependencies +make dev # Run web dev server +``` + +### Data Connect +```bash +make dataconnect-sync # Deploy schemas, migrate, regenerate SDK +``` + +## Architecture Patterns + +- **State Management**: BLoC pattern (flutter_bloc) +- **Navigation**: Flutter Modular +- **Architecture**: Clean Architecture (domain/data/presentation layers) +- **Feature Organization**: Each feature is a separate package +- **Value Objects**: Equatable for entity equality + +## Code Conventions + +- Features go in `/apps/mobile/packages/features/{client|staff}/` +- Shared code goes in `/apps/mobile/packages/{core|domain|data_connect}/` +- UI components go in `/apps/mobile/packages/design_system/` +- GraphQL schemas go in `/backend/dataconnect/schema/` +- Documentation language: **English** + +## Important Files + +- `apps/mobile/melos.yaml` - Melos workspace config +- `makefiles/mobile.mk` - Mobile Make targets +- `backend/dataconnect/dataconnect.yaml` - Data Connect config +- `firebase.json` - Firebase hosting/emulator config +- `BLOCKERS.md` - Known blockers and deviations +- `bugs/BUG-REPORT-*.md` - Bug reports with analysis + +## Branch Protection + +- `main` and `dev` branches are protected +- Always create feature branches: `feature/`, `fix/`, `chore/` +- PRs required for merging + +## Testing Mobile Apps + +1. Connect your Android device or start emulator +2. Run `flutter devices` to get device ID +3. Run `make mobile-client-dev-android DEVICE=` + +## Common Issues + +### "No devices found with name 'android'" +The Makefile defaults to device ID `android`. Override with your actual device: +```bash +make mobile-client-dev-android DEVICE=3fb285a7 +``` + +### Dependency resolution issues +```bash +cd apps/mobile && melos clean && melos bootstrap +``` + +### Code generation out of sync +```bash +cd apps/mobile && melos run gen:all +``` + +## Known Technical Debt + +See `bugs/BUG-REPORT-*.md` for detailed analysis of: +- Authentication/User sync issues +- Error handling architecture (needs AppException pattern) +- BLoC state management patterns (copyWith null handling) diff --git a/Makefile b/Makefile index 86a23d33..49176fbc 100644 --- a/Makefile +++ b/Makefile @@ -17,9 +17,9 @@ include makefiles/tools.mk .PHONY: help help: - @echo "--------------------------------------------------" - @echo " KROW Workforce - Available Makefile Commands" - @echo "--------------------------------------------------" + @echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + @echo " 🚀 KROW Workforce - Available Makefile Commands" + @echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" @echo "" @echo " --- WEB APP DEVELOPMENT ---" @echo " make web-install - Installs web frontend dependencies." @@ -29,31 +29,53 @@ help: @echo " make web-lint - Runs linter for web frontend." @echo " make web-preview - Previews the web frontend build." @echo " make launchpad-dev - Starts the local launchpad server (Firebase Hosting emulator)." + @echo " 📦 WEB FRONTEND (internal/api-harness)" + @echo " ────────────────────────────────────────────────────────────────────" + @echo " make install Install web frontend dependencies" + @echo " make dev Start local web frontend dev server" + @echo " make build Build web frontend for production" + @echo " make deploy-app [ENV=dev] Build and deploy web app (dev/staging/prod)" @echo "" - @echo " --- MOBILE APP DEVELOPMENT ---" - @echo " make mobile-install - Bootstrap the mobile workspace (Melos)." - @echo " make mobile-info - List custom mobile development commands." - @echo " make mobile-client-dev-android - Run client app in dev mode (Android)." - @echo " make mobile-client-build PLATFORM=apk - Build client app for specified platform." - @echo " make mobile-staff-dev-android - Run staff app in dev mode (Android)." - @echo " make mobile-staff-build PLATFORM=apk - Build staff app for specified platform." + @echo " 🏠 LAUNCHPAD (internal/launchpad)" + @echo " ────────────────────────────────────────────────────────────────────" + @echo " make launchpad-dev Start launchpad dev server (Firebase Hosting)" + @echo " make deploy-launchpad-hosting Deploy launchpad to Firebase Hosting" @echo "" - @echo " --- DEPLOYMENT ---" - @echo " make deploy-launchpad-hosting - Deploys internal launchpad to Firebase Hosting." - @echo " make deploy-app [ENV=staging] - Builds and deploys the main web app (default: dev)." + @echo " 📱 MOBILE APPS (apps/mobile)" + @echo " ────────────────────────────────────────────────────────────────────" + @echo " make mobile-install Bootstrap mobile workspace + SDK" + @echo " make mobile-info List mobile development commands" + @echo " make mobile-client-dev-android [DEVICE=android] Run client app (Android)" + @echo " make mobile-client-build PLATFORM=apk Build client app (apk/ipa/etc)" + @echo " make mobile-staff-dev-android [DEVICE=android] Run staff app (Android)" + @echo " make mobile-staff-build PLATFORM=apk Build staff app (apk/ipa/etc)" + @echo " make mobile-hot-reload Hot reload running Flutter app" + @echo " make mobile-hot-restart Hot restart running Flutter app" @echo "" - @echo " --- DEVELOPMENT TOOLS ---" - @echo " make install-melos - Installs Melos globally if not already present." - @echo " make install-git-hooks - Installs git pre-push hook to protect main/dev branches." - @echo " make sync-prototypes - Builds and copies prototypes from adjacent 'client-krow-poc' repo." + @echo " 🗄️ DATA CONNECT & BACKEND (backend/dataconnect)" + @echo " ────────────────────────────────────────────────────────────────────" + @echo " make dataconnect-init Initialize Firebase Data Connect" + @echo " make dataconnect-deploy Deploy Data Connect schemas to Cloud SQL" + @echo " make dataconnect-sql-migrate Apply pending SQL migrations" + @echo " make dataconnect-generate-sdk Regenerate Data Connect client SDK" + @echo " make dataconnect-sync Full sync: deploy + migrate + generate SDK" + @echo " make dataconnect-seed Seed database with test data" + @echo " make dataconnect-clean Delete all data from Data Connect" + @echo " make dataconnect-test Test Data Connect deployment (dry-run)" + @echo " make dataconnect-enable-apis Enable required GCP APIs" + @echo " make dataconnect-bootstrap-db ONE-TIME: Full Cloud SQL + Data Connect setup" @echo "" - @echo " --- DATA CONNECT MANAGEMENT ---" - @echo " make dataconnect-init - Initializes Firebase Data Connect." - @echo " make dataconnect-deploy - Deploys Data Connect schemas." - @echo " make dataconnect-sql-migrate - Applies SQL migrations." - @echo " make dataconnect-generate-sdk - Regenerates the Data Connect SDK." - @echo " make dataconnect-sync - Runs migrate + deploy + generate-sdk." - @echo " make dataconnect-bootstrap-db - ONE-TIME: Full Cloud SQL + Data Connect setup." + @echo " 🛠️ DEVELOPMENT TOOLS" + @echo " ────────────────────────────────────────────────────────────────────" + @echo " make install-melos Install Melos globally (for mobile dev)" + @echo " make install-git-hooks Install git pre-push hook (protect main/dev)" + @echo " make sync-prototypes Sync prototypes from client-krow-poc repo" @echo "" - @echo " make help - Shows this help message." - @echo "--------------------------------------------------" \ No newline at end of file + @echo " ℹ️ HELP" + @echo " ────────────────────────────────────────────────────────────────────" + @echo " make help Show this help message" + @echo "" + @echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + @echo " 💡 Tip: Run 'make mobile-install' first for mobile development" + @echo " 💡 Tip: Use 'make dataconnect-sync' after schema changes" + @echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" \ No newline at end of file diff --git a/apps/mobile/NEXT_SPRINT_TASKS.md b/apps/mobile/NEXT_SPRINT_TASKS.md new file mode 100644 index 00000000..d35afb90 --- /dev/null +++ b/apps/mobile/NEXT_SPRINT_TASKS.md @@ -0,0 +1,18 @@ +## Recommended tasks for the next sprint + + +* In the mobile applications, since the structure is now finalized (at least for the existing features), we need to **strictly follow best practices while coding**: + + * Break down large widgets into **smaller, reusable widgets** + * Add **doc comments** where necessary to improve readability and maintainability + * **Remove overly complicated or unnecessary logic** introduced by AI and simplify where possible + * **Adhere to the design system** and remove all **hard-coded colors and typography**, using shared tokens instead + +* Improvement points +- apps/mobile/packages/features/client/client_coverage/lib/src/data/repositories_impl/coverage_repository_impl.dart + - Fix the location field in CoverageShiftRole to use the correct fallback logic. + - line 125 remove redundant location values. +- Need to clarify the difference b/w `case dc.ApplicationStatus.ACCEPTED` and `case dc.ApplicationStatus.CONFIRMED`. +- Update the dataconnect docs. +- Track `lat` and `lng` in the staff preferred work locations (for now we are only storing the name). +- Remove "Up Next (x)" counter from orders list in client app as it is confusing, becase the tab already has a badge showing the number of the upcoming orders. diff --git a/apps/mobile/README.md b/apps/mobile/README.md index a708b8eb..77d948ad 100644 --- a/apps/mobile/README.md +++ b/apps/mobile/README.md @@ -27,32 +27,43 @@ The project is organized into modular packages to ensure separation of concerns Ensure you have the Flutter SDK installed and configured. ### 2. Initial Setup -Run the following command from the **project root** to install Melos, bootstrap all packages, and generate localization files: +Run the following command from the **project root** to install Melos, bootstrap all packages, generate localization files, and generate the Firebase Data Connect SDK: ```bash -# Using Makefile +# Using Makefile (Recommended) make mobile-install -# Using Melos -melos bootstrap ``` +This command will: +- Install Melos if not already installed +- Generate the Firebase Data Connect SDK from schema files +- Bootstrap all packages (install dependencies) +- Generate localization files + +**Note:** The Firebase Data Connect SDK files (`dataconnect_generated/`) are auto-generated and not committed to the repository. They will be regenerated automatically when you run `make mobile-install` or any mobile development commands. + ### 3. Running the Apps You can run the applications using Melos scripts or through the `Makefile`: +First, find your device ID: +```bash +flutter devices +``` + #### Client App ```bash # Using Melos -melos run start:client -d android # or ios -# Using Makefile -make mobile-client-dev-android +melos run start:client -- -d +# Using Makefile (DEVICE defaults to 'android' if not specified) +make mobile-client-dev-android DEVICE= ``` #### Staff App ```bash # Using Melos -melos run start:staff -d android # or ios -# Using Makefile -make mobile-staff-dev-android +melos run start:staff -- -d +# Using Makefile (DEVICE defaults to 'android' if not specified) +make mobile-staff-dev-android DEVICE= ``` ## 🛠 Useful Commands diff --git a/apps/mobile/apps/client/android/app/build.gradle.kts b/apps/mobile/apps/client/android/app/build.gradle.kts index 04b25374..202bc20b 100644 --- a/apps/mobile/apps/client/android/app/build.gradle.kts +++ b/apps/mobile/apps/client/android/app/build.gradle.kts @@ -7,7 +7,7 @@ plugins { } android { - namespace = "com.example.krow_client" + namespace = "com.krowwithus.client" compileSdk = flutter.compileSdkVersion ndkVersion = flutter.ndkVersion diff --git a/apps/mobile/apps/client/android/app/google-services.json b/apps/mobile/apps/client/android/app/google-services.json index 7533ddc2..fcd3c0e0 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" } } ] @@ -121,6 +85,14 @@ } }, "oauth_client": [ + { + "client_id": "933560802882-fbqg2icq24bmci3f84evjrbth5huh87f.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "com.krowwithus.client", + "certificate_hash": "c3efbe1642239c599c16ad04c7fac340902fe280" + } + }, { "client_id": "933560802882-grp98a1v7amflnnup68vh01tj06eaem1.apps.googleusercontent.com", "client_type": 3 @@ -139,10 +111,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 +123,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 +155,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/android/app/src/main/kotlin/com/example/krow_client/MainActivity.kt b/apps/mobile/apps/client/android/app/src/main/kotlin/com/example/krow_client/MainActivity.kt index 419b3bd4..3e393b5d 100644 --- a/apps/mobile/apps/client/android/app/src/main/kotlin/com/example/krow_client/MainActivity.kt +++ b/apps/mobile/apps/client/android/app/src/main/kotlin/com/example/krow_client/MainActivity.kt @@ -1,4 +1,4 @@ -package com.example.krow_client +package com.krowwithus.client import io.flutter.embedding.android.FlutterActivity diff --git a/apps/mobile/apps/client/firebase.json b/apps/mobile/apps/client/firebase.json new file mode 100644 index 00000000..09f707ae --- /dev/null +++ b/apps/mobile/apps/client/firebase.json @@ -0,0 +1 @@ +{"flutter":{"platforms":{"android":{"default":{"projectId":"krow-workforce-dev","appId":"1:933560802882:android:da13569105659ead7757db","fileOutput":"android/app/google-services.json"}},"ios":{"default":{"projectId":"krow-workforce-dev","appId":"1:933560802882:ios:d2b6d743608e2a527757db","uploadDebugSymbols":false,"fileOutput":"ios/Runner/GoogleService-Info.plist"}},"dart":{"lib/firebase_options.dart":{"projectId":"krow-workforce-dev","configurations":{"android":"1:933560802882:android:da13569105659ead7757db","ios":"1:933560802882:ios:d2b6d743608e2a527757db","web":"1:933560802882:web:173a841992885bb27757db"}}}}}} \ No newline at end of file diff --git a/apps/mobile/apps/client/ios/Flutter/ephemeral/flutter_lldb_helper.py b/apps/mobile/apps/client/ios/Flutter/ephemeral/flutter_lldb_helper.py deleted file mode 100644 index a88caf99..00000000 --- a/apps/mobile/apps/client/ios/Flutter/ephemeral/flutter_lldb_helper.py +++ /dev/null @@ -1,32 +0,0 @@ -# -# Generated file, do not edit. -# - -import lldb - -def handle_new_rx_page(frame: lldb.SBFrame, bp_loc, extra_args, intern_dict): - """Intercept NOTIFY_DEBUGGER_ABOUT_RX_PAGES and touch the pages.""" - base = frame.register["x0"].GetValueAsAddress() - page_len = frame.register["x1"].GetValueAsUnsigned() - - # Note: NOTIFY_DEBUGGER_ABOUT_RX_PAGES will check contents of the - # first page to see if handled it correctly. This makes diagnosing - # misconfiguration (e.g. missing breakpoint) easier. - data = bytearray(page_len) - data[0:8] = b'IHELPED!' - - error = lldb.SBError() - frame.GetThread().GetProcess().WriteMemory(base, data, error) - if not error.Success(): - print(f'Failed to write into {base}[+{page_len}]', error) - return - -def __lldb_init_module(debugger: lldb.SBDebugger, _): - target = debugger.GetDummyTarget() - # Caveat: must use BreakpointCreateByRegEx here and not - # BreakpointCreateByName. For some reasons callback function does not - # get carried over from dummy target for the later. - bp = target.BreakpointCreateByRegex("^NOTIFY_DEBUGGER_ABOUT_RX_PAGES$") - bp.SetScriptCallbackFunction('{}.handle_new_rx_page'.format(__name__)) - bp.SetAutoContinue(True) - print("-- LLDB integration loaded --") diff --git a/apps/mobile/apps/client/ios/Flutter/ephemeral/flutter_lldbinit b/apps/mobile/apps/client/ios/Flutter/ephemeral/flutter_lldbinit deleted file mode 100644 index e3ba6fbe..00000000 --- a/apps/mobile/apps/client/ios/Flutter/ephemeral/flutter_lldbinit +++ /dev/null @@ -1,5 +0,0 @@ -# -# Generated file, do not edit. -# - -command script import --relative-to-command-file flutter_lldb_helper.py 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..86d280e1 --- /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-fbqg2icq24bmci3f84evjrbth5huh87f.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..f703aa10 --- /dev/null +++ b/apps/mobile/apps/client/lib/firebase_options.dart @@ -0,0 +1,78 @@ +// File generated by FlutterFire CLI. + +import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; +import 'package:flutter/foundation.dart' + show defaultTargetPlatform, kIsWeb, TargetPlatform; + +/// 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; + } + 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:173a841992885bb27757db', + messagingSenderId: '933560802882', + projectId: 'krow-workforce-dev', + authDomain: 'krow-workforce-dev.firebaseapp.com', + storageBucket: 'krow-workforce-dev.firebasestorage.app', + measurementId: 'G-9S7WEQTDKX', + ); + + static const FirebaseOptions android = FirebaseOptions( + apiKey: 'AIzaSyDBYhflhK6DThKnS7RM-9raKdvyKzLUjY4', + appId: '1:933560802882:android:da13569105659ead7757db', + messagingSenderId: '933560802882', + projectId: 'krow-workforce-dev', + storageBucket: 'krow-workforce-dev.firebasestorage.app', + ); + + static const FirebaseOptions ios = FirebaseOptions( + apiKey: 'AIzaSyDyEXkzZAWpXXe4dAesYaZflt5BEtMn9tA', + appId: '1:933560802882:ios:d2b6d743608e2a527757db', + messagingSenderId: '933560802882', + projectId: 'krow-workforce-dev', + storageBucket: 'krow-workforce-dev.firebasestorage.app', + androidClientId: '933560802882-fbqg2icq24bmci3f84evjrbth5huh87f.apps.googleusercontent.com', + iosClientId: '933560802882-jqpv1l3gjmi3m87b2gu1iq4lg46lkdfg.apps.googleusercontent.com', + iosBundleId: 'com.krowwithus.client', + ); + +} \ No newline at end of file diff --git a/apps/mobile/apps/client/lib/main.dart b/apps/mobile/apps/client/lib/main.dart index ba82fce4..362fe8b3 100644 --- a/apps/mobile/apps/client/lib/main.dart +++ b/apps/mobile/apps/client/lib/main.dart @@ -12,10 +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())); } @@ -54,35 +59,38 @@ class AppWidget extends StatelessWidget { @override Widget build(BuildContext context) { - return BlocProvider( - create: (BuildContext context) => - Modular.get() - ..add(const core_localization.LoadLocale()), - child: - BlocBuilder< - core_localization.LocaleBloc, - core_localization.LocaleState - >( - builder: - (BuildContext context, core_localization.LocaleState state) { - return core_localization.TranslationProvider( - child: MaterialApp.router( - debugShowCheckedModeBanner: false, - title: "Krow Client", - theme: UiTheme.light, - routerConfig: Modular.routerConfig, - locale: state.locale, - supportedLocales: state.supportedLocales, - localizationsDelegates: - const >[ - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - ], - ), - ); - }, - ), + return WebMobileFrame( + appName: 'KROW Client\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( + debugShowCheckedModeBanner: false, + title: "Krow Client", + 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/client/linux/CMakeLists.txt b/apps/mobile/apps/client/linux/CMakeLists.txt index 6f1df0fe..350d88d7 100644 --- a/apps/mobile/apps/client/linux/CMakeLists.txt +++ b/apps/mobile/apps/client/linux/CMakeLists.txt @@ -7,7 +7,7 @@ project(runner LANGUAGES CXX) set(BINARY_NAME "krow_client") # The unique GTK application identifier for this application. See: # https://wiki.gnome.org/HowDoI/ChooseApplicationID -set(APPLICATION_ID "com.example.krow_client") +set(APPLICATION_ID "com.krowwithus.client") # Explicitly opt in to modern CMake behaviors to avoid warnings with recent # versions of CMake. diff --git a/apps/mobile/apps/client/linux/flutter/ephemeral/.plugin_symlinks/path_provider_linux b/apps/mobile/apps/client/linux/flutter/ephemeral/.plugin_symlinks/path_provider_linux deleted file mode 120000 index d7e81bb9..00000000 --- a/apps/mobile/apps/client/linux/flutter/ephemeral/.plugin_symlinks/path_provider_linux +++ /dev/null @@ -1 +0,0 @@ -/Users/achinthaisuru/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/ \ No newline at end of file diff --git a/apps/mobile/apps/client/linux/flutter/ephemeral/.plugin_symlinks/shared_preferences_linux b/apps/mobile/apps/client/linux/flutter/ephemeral/.plugin_symlinks/shared_preferences_linux deleted file mode 120000 index 6202480c..00000000 --- a/apps/mobile/apps/client/linux/flutter/ephemeral/.plugin_symlinks/shared_preferences_linux +++ /dev/null @@ -1 +0,0 @@ -/Users/achinthaisuru/.pub-cache/hosted/pub.dev/shared_preferences_linux-2.4.1/ \ No newline at end of file diff --git a/apps/mobile/apps/client/linux/flutter/ephemeral/.plugin_symlinks/url_launcher_linux b/apps/mobile/apps/client/linux/flutter/ephemeral/.plugin_symlinks/url_launcher_linux deleted file mode 120000 index ad8c4158..00000000 --- a/apps/mobile/apps/client/linux/flutter/ephemeral/.plugin_symlinks/url_launcher_linux +++ /dev/null @@ -1 +0,0 @@ -/Users/achinthaisuru/.pub-cache/hosted/pub.dev/url_launcher_linux-3.2.2/ \ No newline at end of file diff --git a/apps/mobile/apps/client/macos/Flutter/ephemeral/Flutter-Generated.xcconfig b/apps/mobile/apps/client/macos/Flutter/ephemeral/Flutter-Generated.xcconfig deleted file mode 100644 index 758a34eb..00000000 --- a/apps/mobile/apps/client/macos/Flutter/ephemeral/Flutter-Generated.xcconfig +++ /dev/null @@ -1,13 +0,0 @@ -// This is a generated file; do not edit or check into version control. -FLUTTER_ROOT=/Users/josesalazar/flutter -FLUTTER_APPLICATION_PATH=/Users/josesalazar/Documents/DEV/krow-workforce/apps/mobile/apps/client -COCOAPODS_PARALLEL_CODE_SIGN=true -FLUTTER_TARGET=/Users/josesalazar/Documents/DEV/krow-workforce/apps/mobile/apps/client/lib/main.dart -FLUTTER_BUILD_DIR=build -FLUTTER_BUILD_NAME=1.0.0 -FLUTTER_BUILD_NUMBER=1 -DART_DEFINES=RkxVVFRFUl9WRVJTSU9OPTMuMzguNw==,RkxVVFRFUl9DSEFOTkVMPXN0YWJsZQ==,RkxVVFRFUl9HSVRfVVJMPWh0dHBzOi8vZ2l0aHViLmNvbS9mbHV0dGVyL2ZsdXR0ZXIuZ2l0,RkxVVFRFUl9GUkFNRVdPUktfUkVWSVNJT049M2I2MmVmYzJhMw==,RkxVVFRFUl9FTkdJTkVfUkVWSVNJT049NzhmYzMwMTJlNA==,RkxVVFRFUl9EQVJUX1ZFUlNJT049My4xMC43 -DART_OBFUSCATION=false -TRACK_WIDGET_CREATION=true -TREE_SHAKE_ICONS=false -PACKAGE_CONFIG=/Users/josesalazar/Documents/DEV/krow-workforce/apps/mobile/.dart_tool/package_config.json diff --git a/apps/mobile/apps/client/macos/Flutter/ephemeral/flutter_export_environment.sh b/apps/mobile/apps/client/macos/Flutter/ephemeral/flutter_export_environment.sh deleted file mode 100755 index 98894259..00000000 --- a/apps/mobile/apps/client/macos/Flutter/ephemeral/flutter_export_environment.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/sh -# This is a generated file; do not edit or check into version control. -export "FLUTTER_ROOT=/Users/josesalazar/flutter" -export "FLUTTER_APPLICATION_PATH=/Users/josesalazar/Documents/DEV/krow-workforce/apps/mobile/apps/client" -export "COCOAPODS_PARALLEL_CODE_SIGN=true" -export "FLUTTER_TARGET=/Users/josesalazar/Documents/DEV/krow-workforce/apps/mobile/apps/client/lib/main.dart" -export "FLUTTER_BUILD_DIR=build" -export "FLUTTER_BUILD_NAME=1.0.0" -export "FLUTTER_BUILD_NUMBER=1" -export "DART_DEFINES=RkxVVFRFUl9WRVJTSU9OPTMuMzguNw==,RkxVVFRFUl9DSEFOTkVMPXN0YWJsZQ==,RkxVVFRFUl9HSVRfVVJMPWh0dHBzOi8vZ2l0aHViLmNvbS9mbHV0dGVyL2ZsdXR0ZXIuZ2l0,RkxVVFRFUl9GUkFNRVdPUktfUkVWSVNJT049M2I2MmVmYzJhMw==,RkxVVFRFUl9FTkdJTkVfUkVWSVNJT049NzhmYzMwMTJlNA==,RkxVVFRFUl9EQVJUX1ZFUlNJT049My4xMC43" -export "DART_OBFUSCATION=false" -export "TRACK_WIDGET_CREATION=true" -export "TREE_SHAKE_ICONS=false" -export "PACKAGE_CONFIG=/Users/josesalazar/Documents/DEV/krow-workforce/apps/mobile/.dart_tool/package_config.json" diff --git a/apps/mobile/apps/client/pubspec.yaml b/apps/mobile/apps/client/pubspec.yaml index c2fa3bd1..c896944d 100644 --- a/apps/mobile/apps/client/pubspec.yaml +++ b/apps/mobile/apps/client/pubspec.yaml @@ -1,11 +1,11 @@ name: krowwithus_client description: "Krow Client Application" publish_to: "none" -version: 0.0.1-M+301 +version: 0.0.1-M3+5 resolution: workspace environment: - sdk: ^3.10.7 + sdk: '>=3.10.0 <4.0.0' dependencies: flutter: @@ -32,6 +32,8 @@ dependencies: path: ../../packages/features/client/hubs client_create_order: path: ../../packages/features/client/create_order + krow_core: + path: ../../packages/core cupertino_icons: ^1.0.8 flutter_modular: ^6.3.2 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/client/windows/flutter/ephemeral/.plugin_symlinks/firebase_auth b/apps/mobile/apps/client/windows/flutter/ephemeral/.plugin_symlinks/firebase_auth deleted file mode 120000 index a05ca7fe..00000000 --- a/apps/mobile/apps/client/windows/flutter/ephemeral/.plugin_symlinks/firebase_auth +++ /dev/null @@ -1 +0,0 @@ -/Users/achinthaisuru/.pub-cache/hosted/pub.dev/firebase_auth-6.1.4/ \ No newline at end of file diff --git a/apps/mobile/apps/client/windows/flutter/ephemeral/.plugin_symlinks/firebase_core b/apps/mobile/apps/client/windows/flutter/ephemeral/.plugin_symlinks/firebase_core deleted file mode 120000 index 1d268465..00000000 --- a/apps/mobile/apps/client/windows/flutter/ephemeral/.plugin_symlinks/firebase_core +++ /dev/null @@ -1 +0,0 @@ -/Users/achinthaisuru/.pub-cache/hosted/pub.dev/firebase_core-4.4.0/ \ No newline at end of file diff --git a/apps/mobile/apps/client/windows/flutter/ephemeral/.plugin_symlinks/path_provider_windows b/apps/mobile/apps/client/windows/flutter/ephemeral/.plugin_symlinks/path_provider_windows deleted file mode 120000 index 2316cfff..00000000 --- a/apps/mobile/apps/client/windows/flutter/ephemeral/.plugin_symlinks/path_provider_windows +++ /dev/null @@ -1 +0,0 @@ -/Users/achinthaisuru/.pub-cache/hosted/pub.dev/path_provider_windows-2.3.0/ \ No newline at end of file diff --git a/apps/mobile/apps/client/windows/flutter/ephemeral/.plugin_symlinks/shared_preferences_windows b/apps/mobile/apps/client/windows/flutter/ephemeral/.plugin_symlinks/shared_preferences_windows deleted file mode 120000 index d567e409..00000000 --- a/apps/mobile/apps/client/windows/flutter/ephemeral/.plugin_symlinks/shared_preferences_windows +++ /dev/null @@ -1 +0,0 @@ -/Users/achinthaisuru/.pub-cache/hosted/pub.dev/shared_preferences_windows-2.4.1/ \ No newline at end of file diff --git a/apps/mobile/apps/client/windows/flutter/ephemeral/.plugin_symlinks/url_launcher_windows b/apps/mobile/apps/client/windows/flutter/ephemeral/.plugin_symlinks/url_launcher_windows deleted file mode 120000 index 7bce5a33..00000000 --- a/apps/mobile/apps/client/windows/flutter/ephemeral/.plugin_symlinks/url_launcher_windows +++ /dev/null @@ -1 +0,0 @@ -/Users/achinthaisuru/.pub-cache/hosted/pub.dev/url_launcher_windows-3.1.5/ \ No newline at end of file diff --git a/apps/mobile/apps/design_system_viewer/ios/Flutter/ephemeral/flutter_lldb_helper.py b/apps/mobile/apps/design_system_viewer/ios/Flutter/ephemeral/flutter_lldb_helper.py deleted file mode 100644 index a88caf99..00000000 --- a/apps/mobile/apps/design_system_viewer/ios/Flutter/ephemeral/flutter_lldb_helper.py +++ /dev/null @@ -1,32 +0,0 @@ -# -# Generated file, do not edit. -# - -import lldb - -def handle_new_rx_page(frame: lldb.SBFrame, bp_loc, extra_args, intern_dict): - """Intercept NOTIFY_DEBUGGER_ABOUT_RX_PAGES and touch the pages.""" - base = frame.register["x0"].GetValueAsAddress() - page_len = frame.register["x1"].GetValueAsUnsigned() - - # Note: NOTIFY_DEBUGGER_ABOUT_RX_PAGES will check contents of the - # first page to see if handled it correctly. This makes diagnosing - # misconfiguration (e.g. missing breakpoint) easier. - data = bytearray(page_len) - data[0:8] = b'IHELPED!' - - error = lldb.SBError() - frame.GetThread().GetProcess().WriteMemory(base, data, error) - if not error.Success(): - print(f'Failed to write into {base}[+{page_len}]', error) - return - -def __lldb_init_module(debugger: lldb.SBDebugger, _): - target = debugger.GetDummyTarget() - # Caveat: must use BreakpointCreateByRegEx here and not - # BreakpointCreateByName. For some reasons callback function does not - # get carried over from dummy target for the later. - bp = target.BreakpointCreateByRegex("^NOTIFY_DEBUGGER_ABOUT_RX_PAGES$") - bp.SetScriptCallbackFunction('{}.handle_new_rx_page'.format(__name__)) - bp.SetAutoContinue(True) - print("-- LLDB integration loaded --") diff --git a/apps/mobile/apps/design_system_viewer/ios/Flutter/ephemeral/flutter_lldbinit b/apps/mobile/apps/design_system_viewer/ios/Flutter/ephemeral/flutter_lldbinit deleted file mode 100644 index e3ba6fbe..00000000 --- a/apps/mobile/apps/design_system_viewer/ios/Flutter/ephemeral/flutter_lldbinit +++ /dev/null @@ -1,5 +0,0 @@ -# -# Generated file, do not edit. -# - -command script import --relative-to-command-file flutter_lldb_helper.py diff --git a/apps/mobile/apps/design_system_viewer/macos/Flutter/ephemeral/Flutter-Generated.xcconfig b/apps/mobile/apps/design_system_viewer/macos/Flutter/ephemeral/Flutter-Generated.xcconfig deleted file mode 100644 index d7e96049..00000000 --- a/apps/mobile/apps/design_system_viewer/macos/Flutter/ephemeral/Flutter-Generated.xcconfig +++ /dev/null @@ -1,11 +0,0 @@ -// This is a generated file; do not edit or check into version control. -FLUTTER_ROOT=/Users/achinthaisuru/Documents/flutter -FLUTTER_APPLICATION_PATH=/Users/achinthaisuru/Documents/Github/krow-workforce/apps/mobile/apps/design_system_viewer -COCOAPODS_PARALLEL_CODE_SIGN=true -FLUTTER_BUILD_DIR=build -FLUTTER_BUILD_NAME=1.0.0 -FLUTTER_BUILD_NUMBER=1 -DART_OBFUSCATION=false -TRACK_WIDGET_CREATION=true -TREE_SHAKE_ICONS=false -PACKAGE_CONFIG=.dart_tool/package_config.json diff --git a/apps/mobile/apps/design_system_viewer/macos/Flutter/ephemeral/flutter_export_environment.sh b/apps/mobile/apps/design_system_viewer/macos/Flutter/ephemeral/flutter_export_environment.sh deleted file mode 100755 index 6b0b50f3..00000000 --- a/apps/mobile/apps/design_system_viewer/macos/Flutter/ephemeral/flutter_export_environment.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/sh -# This is a generated file; do not edit or check into version control. -export "FLUTTER_ROOT=/Users/achinthaisuru/Documents/flutter" -export "FLUTTER_APPLICATION_PATH=/Users/achinthaisuru/Documents/Github/krow-workforce/apps/mobile/apps/design_system_viewer" -export "COCOAPODS_PARALLEL_CODE_SIGN=true" -export "FLUTTER_BUILD_DIR=build" -export "FLUTTER_BUILD_NAME=1.0.0" -export "FLUTTER_BUILD_NUMBER=1" -export "DART_OBFUSCATION=false" -export "TRACK_WIDGET_CREATION=true" -export "TREE_SHAKE_ICONS=false" -export "PACKAGE_CONFIG=.dart_tool/package_config.json" diff --git a/apps/mobile/apps/design_system_viewer/pubspec.yaml b/apps/mobile/apps/design_system_viewer/pubspec.yaml index c96bbd77..dfc73a12 100644 --- a/apps/mobile/apps/design_system_viewer/pubspec.yaml +++ b/apps/mobile/apps/design_system_viewer/pubspec.yaml @@ -16,11 +16,11 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.0.0+1 +version: 0.0.1 resolution: workspace environment: - sdk: ^3.10.7 + sdk: '>=3.10.0 <4.0.0' # Dependencies specify other packages that your package needs in order to work. # To automatically upgrade your package dependencies to the latest versions diff --git a/apps/mobile/apps/staff/android/app/build.gradle.kts b/apps/mobile/apps/staff/android/app/build.gradle.kts index 80f2b222..8764d57b 100644 --- a/apps/mobile/apps/staff/android/app/build.gradle.kts +++ b/apps/mobile/apps/staff/android/app/build.gradle.kts @@ -7,7 +7,7 @@ plugins { } android { - namespace = "com.example.krow_staff" + namespace = "com.krowwithus.staff" compileSdk = flutter.compileSdkVersion ndkVersion = flutter.ndkVersion diff --git a/apps/mobile/apps/staff/android/app/google-services.json b/apps/mobile/apps/staff/android/app/google-services.json index 13b4592b..fcd3c0e0 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", @@ -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" } } ] @@ -122,41 +86,13 @@ }, "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:d26bde4ee337b0b17757db", - "android_client_info": { - "package_name": "com.krowwithus.krow_workforce.dev" - } - }, - "oauth_client": [ + "client_id": "933560802882-fbqg2icq24bmci3f84evjrbth5huh87f.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "com.krowwithus.client", + "certificate_hash": "c3efbe1642239c599c16ad04c7fac340902fe280" + } + }, { "client_id": "933560802882-grp98a1v7amflnnup68vh01tj06eaem1.apps.googleusercontent.com", "client_type": 3 @@ -175,10 +111,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" } } ] @@ -193,6 +129,14 @@ } }, "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 @@ -211,10 +155,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/staff/android/app/google-services.json_back b/apps/mobile/apps/staff/android/app/google-services.json_back new file mode 100644 index 00000000..f4d57e10 --- /dev/null +++ b/apps/mobile/apps/staff/android/app/google-services.json_back @@ -0,0 +1,162 @@ +{ + "project_info": { + "project_number": "933560802882", + "project_id": "krow-workforce-dev", + "storage_bucket": "krow-workforce-dev.firebasestorage.app" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:933560802882:android:edcddb83ea4bbb517757db", + "android_client_info": { + "package_name": "com.krow.app.business.dev" + } + }, + "oauth_client": [ + { + "client_id": "933560802882-grp98a1v7amflnnup68vh01tj06eaem1.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyDBYhflhK6DThKnS7RM-9raKdvyKzLUjY4" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "933560802882-grp98a1v7amflnnup68vh01tj06eaem1.apps.googleusercontent.com", + "client_type": 3 + }, + { + "client_id": "933560802882-dppsapp5i3lsfrlm1mhob2s21peofg1t.apps.googleusercontent.com", + "client_type": 2, + "ios_info": { + "bundle_id": "com.krow.app.staff.dev" + } + } + ] + } + } + }, + { + "client_info": { + "mobilesdk_app_id": "1:933560802882:android:d49b8c0f4d19e95e7757db", + "android_client_info": { + "package_name": "com.krow.app.staff.dev" + } + }, + "oauth_client": [ + { + "client_id": "933560802882-grp98a1v7amflnnup68vh01tj06eaem1.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyDBYhflhK6DThKnS7RM-9raKdvyKzLUjY4" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "933560802882-grp98a1v7amflnnup68vh01tj06eaem1.apps.googleusercontent.com", + "client_type": 3 + }, + { + "client_id": "933560802882-dppsapp5i3lsfrlm1mhob2s21peofg1t.apps.googleusercontent.com", + "client_type": 2, + "ios_info": { + "bundle_id": "com.krow.app.staff.dev" + } + } + ] + } + } + }, + { + "client_info": { + "mobilesdk_app_id": "1:933560802882:android:da13569105659ead7757db", + "android_client_info": { + "package_name": "com.krowwithus.client" + } + }, + "oauth_client": [ + { + "client_id": "933560802882-grp98a1v7amflnnup68vh01tj06eaem1.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyDBYhflhK6DThKnS7RM-9raKdvyKzLUjY4" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "933560802882-grp98a1v7amflnnup68vh01tj06eaem1.apps.googleusercontent.com", + "client_type": 3 + }, + { + "client_id": "933560802882-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", + "android_client_info": { + "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 + } + ], + "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" + } + } + ] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/apps/mobile/apps/staff/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java b/apps/mobile/apps/staff/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java index 899a1487..6614ffb4 100644 --- a/apps/mobile/apps/staff/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java +++ b/apps/mobile/apps/staff/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java @@ -30,11 +30,21 @@ public final class GeneratedPluginRegistrant { } catch (Exception e) { Log.e(TAG, "Error registering plugin firebase_core, io.flutter.plugins.firebase.core.FlutterFirebaseCorePlugin", e); } + try { + flutterEngine.getPlugins().add(new com.baseflow.geolocator.GeolocatorPlugin()); + } catch (Exception e) { + Log.e(TAG, "Error registering plugin geolocator_android, com.baseflow.geolocator.GeolocatorPlugin", e); + } try { flutterEngine.getPlugins().add(new io.flutter.plugins.pathprovider.PathProviderPlugin()); } catch (Exception e) { Log.e(TAG, "Error registering plugin path_provider_android, io.flutter.plugins.pathprovider.PathProviderPlugin", e); } + try { + flutterEngine.getPlugins().add(new com.baseflow.permissionhandler.PermissionHandlerPlugin()); + } catch (Exception e) { + Log.e(TAG, "Error registering plugin permission_handler_android, com.baseflow.permissionhandler.PermissionHandlerPlugin", e); + } try { flutterEngine.getPlugins().add(new io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin()); } catch (Exception e) { diff --git a/apps/mobile/apps/staff/android/app/src/main/kotlin/com/example/krow_staff/MainActivity.kt b/apps/mobile/apps/staff/android/app/src/main/kotlin/com/example/krow_staff/MainActivity.kt index 13520833..b892977d 100644 --- a/apps/mobile/apps/staff/android/app/src/main/kotlin/com/example/krow_staff/MainActivity.kt +++ b/apps/mobile/apps/staff/android/app/src/main/kotlin/com/example/krow_staff/MainActivity.kt @@ -1,4 +1,4 @@ -package com.example.krow_staff +package com.krowwithus.staff import io.flutter.embedding.android.FlutterActivity diff --git a/apps/mobile/apps/staff/ios/Flutter/ephemeral/flutter_lldb_helper.py b/apps/mobile/apps/staff/ios/Flutter/ephemeral/flutter_lldb_helper.py deleted file mode 100644 index a88caf99..00000000 --- a/apps/mobile/apps/staff/ios/Flutter/ephemeral/flutter_lldb_helper.py +++ /dev/null @@ -1,32 +0,0 @@ -# -# Generated file, do not edit. -# - -import lldb - -def handle_new_rx_page(frame: lldb.SBFrame, bp_loc, extra_args, intern_dict): - """Intercept NOTIFY_DEBUGGER_ABOUT_RX_PAGES and touch the pages.""" - base = frame.register["x0"].GetValueAsAddress() - page_len = frame.register["x1"].GetValueAsUnsigned() - - # Note: NOTIFY_DEBUGGER_ABOUT_RX_PAGES will check contents of the - # first page to see if handled it correctly. This makes diagnosing - # misconfiguration (e.g. missing breakpoint) easier. - data = bytearray(page_len) - data[0:8] = b'IHELPED!' - - error = lldb.SBError() - frame.GetThread().GetProcess().WriteMemory(base, data, error) - if not error.Success(): - print(f'Failed to write into {base}[+{page_len}]', error) - return - -def __lldb_init_module(debugger: lldb.SBDebugger, _): - target = debugger.GetDummyTarget() - # Caveat: must use BreakpointCreateByRegEx here and not - # BreakpointCreateByName. For some reasons callback function does not - # get carried over from dummy target for the later. - bp = target.BreakpointCreateByRegex("^NOTIFY_DEBUGGER_ABOUT_RX_PAGES$") - bp.SetScriptCallbackFunction('{}.handle_new_rx_page'.format(__name__)) - bp.SetAutoContinue(True) - print("-- LLDB integration loaded --") diff --git a/apps/mobile/apps/staff/ios/Flutter/ephemeral/flutter_lldbinit b/apps/mobile/apps/staff/ios/Flutter/ephemeral/flutter_lldbinit deleted file mode 100644 index e3ba6fbe..00000000 --- a/apps/mobile/apps/staff/ios/Flutter/ephemeral/flutter_lldbinit +++ /dev/null @@ -1,5 +0,0 @@ -# -# Generated file, do not edit. -# - -command script import --relative-to-command-file flutter_lldb_helper.py 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/GeneratedPluginRegistrant.m b/apps/mobile/apps/staff/ios/Runner/GeneratedPluginRegistrant.m index bde6e93e..5f7e015d 100644 --- a/apps/mobile/apps/staff/ios/Runner/GeneratedPluginRegistrant.m +++ b/apps/mobile/apps/staff/ios/Runner/GeneratedPluginRegistrant.m @@ -24,6 +24,18 @@ @import firebase_core; #endif +#if __has_include() +#import +#else +@import geolocator_apple; +#endif + +#if __has_include() +#import +#else +@import permission_handler_apple; +#endif + #if __has_include() #import #else @@ -36,6 +48,8 @@ [FLTFirebaseAppCheckPlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTFirebaseAppCheckPlugin"]]; [FLTFirebaseAuthPlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTFirebaseAuthPlugin"]]; [FLTFirebaseCorePlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTFirebaseCorePlugin"]]; + [GeolocatorPlugin registerWithRegistrar:[registry registrarForPlugin:@"GeolocatorPlugin"]]; + [PermissionHandlerPlugin registerWithRegistrar:[registry registrarForPlugin:@"PermissionHandlerPlugin"]]; [SharedPreferencesPlugin registerWithRegistrar:[registry registrarForPlugin:@"SharedPreferencesPlugin"]]; } 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..7fc4d7e6 --- /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-fbqg2icq24bmci3f84evjrbth5huh87f.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..3945a3a2 --- /dev/null +++ b/apps/mobile/apps/staff/lib/firebase_options.dart @@ -0,0 +1,78 @@ +// File generated by FlutterFire CLI. + +import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; +import 'package:flutter/foundation.dart' + show defaultTargetPlatform, kIsWeb, TargetPlatform; + +/// 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; + } + 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:173a841992885bb27757db', + messagingSenderId: '933560802882', + projectId: 'krow-workforce-dev', + authDomain: 'krow-workforce-dev.firebaseapp.com', + storageBucket: 'krow-workforce-dev.firebasestorage.app', + measurementId: 'G-9S7WEQTDKX', + ); + + static const FirebaseOptions android = FirebaseOptions( + apiKey: 'AIzaSyDBYhflhK6DThKnS7RM-9raKdvyKzLUjY4', + appId: '1:933560802882:android:1ae05d85c865f77c7757db', + messagingSenderId: '933560802882', + projectId: 'krow-workforce-dev', + storageBucket: 'krow-workforce-dev.firebasestorage.app', + ); + + static const FirebaseOptions ios = FirebaseOptions( + apiKey: 'AIzaSyDyEXkzZAWpXXe4dAesYaZflt5BEtMn9tA', + appId: '1:933560802882:ios:fa584205b356de937757db', + messagingSenderId: '933560802882', + projectId: 'krow-workforce-dev', + storageBucket: 'krow-workforce-dev.firebasestorage.app', + androidClientId: '933560802882-fbqg2icq24bmci3f84evjrbth5huh87f.apps.googleusercontent.com', + iosClientId: '933560802882-29olj9ku64jbe9h7flinha6hbi8qrluh.apps.googleusercontent.com', + iosBundleId: 'com.krowwithus.staff', + ); + +} \ No newline at end of file diff --git a/apps/mobile/apps/staff/lib/main.dart b/apps/mobile/apps/staff/lib/main.dart index 92770719..050ae079 100644 --- a/apps/mobile/apps/staff/lib/main.dart +++ b/apps/mobile/apps/staff/lib/main.dart @@ -1,17 +1,21 @@ import 'package:core_localization/core_localization.dart' as core_localization; import 'package:design_system/design_system.dart'; +import 'package:firebase_core/firebase_core.dart'; 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:firebase_core/firebase_core.dart'; +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())); } @@ -34,31 +38,37 @@ class AppWidget extends StatelessWidget { @override Widget build(BuildContext context) { - return BlocProvider( - create: (BuildContext context) => - Modular.get() - ..add(const core_localization.LoadLocale()), - 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/apps/staff/linux/CMakeLists.txt b/apps/mobile/apps/staff/linux/CMakeLists.txt index b222a83e..56ce18bd 100644 --- a/apps/mobile/apps/staff/linux/CMakeLists.txt +++ b/apps/mobile/apps/staff/linux/CMakeLists.txt @@ -7,7 +7,7 @@ project(runner LANGUAGES CXX) set(BINARY_NAME "krow_staff") # The unique GTK application identifier for this application. See: # https://wiki.gnome.org/HowDoI/ChooseApplicationID -set(APPLICATION_ID "com.example.krow_staff") +set(APPLICATION_ID "com.krowwithus.staff") # Explicitly opt in to modern CMake behaviors to avoid warnings with recent # versions of CMake. diff --git a/apps/mobile/apps/staff/linux/flutter/ephemeral/.plugin_symlinks/path_provider_linux b/apps/mobile/apps/staff/linux/flutter/ephemeral/.plugin_symlinks/path_provider_linux deleted file mode 120000 index d7e81bb9..00000000 --- a/apps/mobile/apps/staff/linux/flutter/ephemeral/.plugin_symlinks/path_provider_linux +++ /dev/null @@ -1 +0,0 @@ -/Users/achinthaisuru/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/ \ No newline at end of file diff --git a/apps/mobile/apps/staff/linux/flutter/ephemeral/.plugin_symlinks/shared_preferences_linux b/apps/mobile/apps/staff/linux/flutter/ephemeral/.plugin_symlinks/shared_preferences_linux deleted file mode 120000 index 6202480c..00000000 --- a/apps/mobile/apps/staff/linux/flutter/ephemeral/.plugin_symlinks/shared_preferences_linux +++ /dev/null @@ -1 +0,0 @@ -/Users/achinthaisuru/.pub-cache/hosted/pub.dev/shared_preferences_linux-2.4.1/ \ No newline at end of file diff --git a/apps/mobile/apps/staff/macos/Flutter/GeneratedPluginRegistrant.swift b/apps/mobile/apps/staff/macos/Flutter/GeneratedPluginRegistrant.swift index 8bd29968..2da91454 100644 --- a/apps/mobile/apps/staff/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/apps/mobile/apps/staff/macos/Flutter/GeneratedPluginRegistrant.swift @@ -8,11 +8,13 @@ import Foundation import firebase_app_check import firebase_auth import firebase_core +import geolocator_apple import shared_preferences_foundation func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FLTFirebaseAppCheckPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAppCheckPlugin")) FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin")) FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) + GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) } diff --git a/apps/mobile/apps/staff/macos/Flutter/ephemeral/Flutter-Generated.xcconfig b/apps/mobile/apps/staff/macos/Flutter/ephemeral/Flutter-Generated.xcconfig deleted file mode 100644 index b27990b2..00000000 --- a/apps/mobile/apps/staff/macos/Flutter/ephemeral/Flutter-Generated.xcconfig +++ /dev/null @@ -1,11 +0,0 @@ -// This is a generated file; do not edit or check into version control. -FLUTTER_ROOT=/Users/achinthaisuru/Documents/flutter -FLUTTER_APPLICATION_PATH=/Users/achinthaisuru/Documents/Github/krow-workforce/apps/mobile/apps/staff -COCOAPODS_PARALLEL_CODE_SIGN=true -FLUTTER_BUILD_DIR=build -FLUTTER_BUILD_NAME=1.0.0 -FLUTTER_BUILD_NUMBER=1 -DART_OBFUSCATION=false -TRACK_WIDGET_CREATION=true -TREE_SHAKE_ICONS=false -PACKAGE_CONFIG=.dart_tool/package_config.json diff --git a/apps/mobile/apps/staff/macos/Flutter/ephemeral/flutter_export_environment.sh b/apps/mobile/apps/staff/macos/Flutter/ephemeral/flutter_export_environment.sh deleted file mode 100755 index a90de9ca..00000000 --- a/apps/mobile/apps/staff/macos/Flutter/ephemeral/flutter_export_environment.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/sh -# This is a generated file; do not edit or check into version control. -export "FLUTTER_ROOT=/Users/achinthaisuru/Documents/flutter" -export "FLUTTER_APPLICATION_PATH=/Users/achinthaisuru/Documents/Github/krow-workforce/apps/mobile/apps/staff" -export "COCOAPODS_PARALLEL_CODE_SIGN=true" -export "FLUTTER_BUILD_DIR=build" -export "FLUTTER_BUILD_NAME=1.0.0" -export "FLUTTER_BUILD_NUMBER=1" -export "DART_OBFUSCATION=false" -export "TRACK_WIDGET_CREATION=true" -export "TREE_SHAKE_ICONS=false" -export "PACKAGE_CONFIG=.dart_tool/package_config.json" diff --git a/apps/mobile/apps/staff/macos/Podfile.lock b/apps/mobile/apps/staff/macos/Podfile.lock index 1385d0fb..29dd6847 100644 --- a/apps/mobile/apps/staff/macos/Podfile.lock +++ b/apps/mobile/apps/staff/macos/Podfile.lock @@ -1,21 +1,143 @@ PODS: + - AppCheckCore (11.2.0): + - GoogleUtilities/Environment (~> 8.0) + - GoogleUtilities/UserDefaults (~> 8.0) + - PromisesObjC (~> 2.4) + - Firebase/AppCheck (12.8.0): + - Firebase/CoreOnly + - FirebaseAppCheck (~> 12.8.0) + - Firebase/Auth (12.8.0): + - Firebase/CoreOnly + - FirebaseAuth (~> 12.8.0) + - Firebase/CoreOnly (12.8.0): + - FirebaseCore (~> 12.8.0) + - firebase_app_check (0.4.1-4): + - Firebase/AppCheck (~> 12.8.0) + - Firebase/CoreOnly (~> 12.8.0) + - firebase_core + - FlutterMacOS + - firebase_auth (6.1.4): + - Firebase/Auth (~> 12.8.0) + - Firebase/CoreOnly (~> 12.8.0) + - firebase_core + - FlutterMacOS + - firebase_core (4.4.0): + - Firebase/CoreOnly (~> 12.8.0) + - FlutterMacOS + - FirebaseAppCheck (12.8.0): + - AppCheckCore (~> 11.0) + - FirebaseAppCheckInterop (~> 12.8.0) + - FirebaseCore (~> 12.8.0) + - GoogleUtilities/Environment (~> 8.1) + - GoogleUtilities/UserDefaults (~> 8.1) + - FirebaseAppCheckInterop (12.8.0) + - FirebaseAuth (12.8.0): + - FirebaseAppCheckInterop (~> 12.8.0) + - FirebaseAuthInterop (~> 12.8.0) + - FirebaseCore (~> 12.8.0) + - FirebaseCoreExtension (~> 12.8.0) + - GoogleUtilities/AppDelegateSwizzler (~> 8.1) + - GoogleUtilities/Environment (~> 8.1) + - GTMSessionFetcher/Core (< 6.0, >= 3.4) + - RecaptchaInterop (~> 101.0) + - FirebaseAuthInterop (12.8.0) + - FirebaseCore (12.8.0): + - FirebaseCoreInternal (~> 12.8.0) + - GoogleUtilities/Environment (~> 8.1) + - GoogleUtilities/Logger (~> 8.1) + - FirebaseCoreExtension (12.8.0): + - FirebaseCore (~> 12.8.0) + - FirebaseCoreInternal (12.8.0): + - "GoogleUtilities/NSData+zlib (~> 8.1)" - FlutterMacOS (1.0.0) + - geolocator_apple (1.2.0): + - Flutter + - FlutterMacOS + - GoogleUtilities/AppDelegateSwizzler (8.1.0): + - GoogleUtilities/Environment + - GoogleUtilities/Logger + - GoogleUtilities/Network + - GoogleUtilities/Privacy + - GoogleUtilities/Environment (8.1.0): + - GoogleUtilities/Privacy + - GoogleUtilities/Logger (8.1.0): + - GoogleUtilities/Environment + - GoogleUtilities/Privacy + - GoogleUtilities/Network (8.1.0): + - GoogleUtilities/Logger + - "GoogleUtilities/NSData+zlib" + - GoogleUtilities/Privacy + - GoogleUtilities/Reachability + - "GoogleUtilities/NSData+zlib (8.1.0)": + - GoogleUtilities/Privacy + - GoogleUtilities/Privacy (8.1.0) + - GoogleUtilities/Reachability (8.1.0): + - GoogleUtilities/Logger + - GoogleUtilities/Privacy + - GoogleUtilities/UserDefaults (8.1.0): + - GoogleUtilities/Logger + - GoogleUtilities/Privacy + - GTMSessionFetcher/Core (5.0.0) + - PromisesObjC (2.4.0) - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS DEPENDENCIES: + - firebase_app_check (from `Flutter/ephemeral/.symlinks/plugins/firebase_app_check/macos`) + - firebase_auth (from `Flutter/ephemeral/.symlinks/plugins/firebase_auth/macos`) + - firebase_core (from `Flutter/ephemeral/.symlinks/plugins/firebase_core/macos`) - FlutterMacOS (from `Flutter/ephemeral`) + - geolocator_apple (from `Flutter/ephemeral/.symlinks/plugins/geolocator_apple/darwin`) - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) +SPEC REPOS: + trunk: + - AppCheckCore + - Firebase + - FirebaseAppCheck + - FirebaseAppCheckInterop + - FirebaseAuth + - FirebaseAuthInterop + - FirebaseCore + - FirebaseCoreExtension + - FirebaseCoreInternal + - GoogleUtilities + - GTMSessionFetcher + - PromisesObjC + EXTERNAL SOURCES: + firebase_app_check: + :path: Flutter/ephemeral/.symlinks/plugins/firebase_app_check/macos + firebase_auth: + :path: Flutter/ephemeral/.symlinks/plugins/firebase_auth/macos + firebase_core: + :path: Flutter/ephemeral/.symlinks/plugins/firebase_core/macos FlutterMacOS: :path: Flutter/ephemeral + geolocator_apple: + :path: Flutter/ephemeral/.symlinks/plugins/geolocator_apple/darwin shared_preferences_foundation: :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin SPEC CHECKSUMS: + AppCheckCore: cc8fd0a3a230ddd401f326489c99990b013f0c4f + Firebase: 9a58fdbc9d8655ed7b79a19cf9690bb007d3d46d + firebase_app_check: daf97f2d7044e28b68d23bc90e16751acee09732 + firebase_auth: 2c2438e41f061c03bd67dcb045dfd7bc843b5f52 + firebase_core: b1697fb64ff2b9ca16baaa821205f8b0c058e5d2 + FirebaseAppCheck: 11da425929a45c677d537adfff3520ccd57c1690 + FirebaseAppCheckInterop: ba3dc604a89815379e61ec2365101608d365cf7d + FirebaseAuth: 4c289b1a43f5955283244a55cf6bd616de344be5 + FirebaseAuthInterop: 95363fe96493cb4f106656666a0768b420cba090 + FirebaseCore: 0dbad74bda10b8fb9ca34ad8f375fb9dd3ebef7c + FirebaseCoreExtension: 6605938d51f765d8b18bfcafd2085276a252bee2 + FirebaseCoreInternal: fe5fa466aeb314787093a7dce9f0beeaad5a2a21 FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 + geolocator_apple: ab36aa0e8b7d7a2d7639b3b4e48308394e8cef5e + GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1 + GTMSessionFetcher: 02d6e866e90bc236f48a703a041dfe43e6221a29 + PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb PODFILE CHECKSUM: 54d867c82ac51cbd61b565781b9fada492027009 diff --git a/apps/mobile/apps/staff/pubspec.yaml b/apps/mobile/apps/staff/pubspec.yaml index 397ace98..62c2ae21 100644 --- a/apps/mobile/apps/staff/pubspec.yaml +++ b/apps/mobile/apps/staff/pubspec.yaml @@ -1,20 +1,17 @@ name: krowwithus_staff description: "Krow Staff Application" publish_to: 'none' -version: 0.0.1+M301 +version: 0.0.1-M3+3 resolution: workspace environment: - sdk: ^3.10.7 + sdk: '>=3.10.0 <4.0.0' dependencies: flutter: 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: @@ -23,6 +20,18 @@ dependencies: # Feature Packages staff_authentication: path: ../../packages/features/staff/authentication + staff_availability: + 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 @@ + diff --git a/apps/mobile/apps/staff/windows/flutter/ephemeral/.plugin_symlinks/firebase_auth b/apps/mobile/apps/staff/windows/flutter/ephemeral/.plugin_symlinks/firebase_auth deleted file mode 120000 index a05ca7fe..00000000 --- a/apps/mobile/apps/staff/windows/flutter/ephemeral/.plugin_symlinks/firebase_auth +++ /dev/null @@ -1 +0,0 @@ -/Users/achinthaisuru/.pub-cache/hosted/pub.dev/firebase_auth-6.1.4/ \ No newline at end of file diff --git a/apps/mobile/apps/staff/windows/flutter/ephemeral/.plugin_symlinks/firebase_core b/apps/mobile/apps/staff/windows/flutter/ephemeral/.plugin_symlinks/firebase_core deleted file mode 120000 index 1d268465..00000000 --- a/apps/mobile/apps/staff/windows/flutter/ephemeral/.plugin_symlinks/firebase_core +++ /dev/null @@ -1 +0,0 @@ -/Users/achinthaisuru/.pub-cache/hosted/pub.dev/firebase_core-4.4.0/ \ No newline at end of file diff --git a/apps/mobile/apps/staff/windows/flutter/ephemeral/.plugin_symlinks/path_provider_windows b/apps/mobile/apps/staff/windows/flutter/ephemeral/.plugin_symlinks/path_provider_windows deleted file mode 120000 index 2316cfff..00000000 --- a/apps/mobile/apps/staff/windows/flutter/ephemeral/.plugin_symlinks/path_provider_windows +++ /dev/null @@ -1 +0,0 @@ -/Users/achinthaisuru/.pub-cache/hosted/pub.dev/path_provider_windows-2.3.0/ \ No newline at end of file diff --git a/apps/mobile/apps/staff/windows/flutter/ephemeral/.plugin_symlinks/shared_preferences_windows b/apps/mobile/apps/staff/windows/flutter/ephemeral/.plugin_symlinks/shared_preferences_windows deleted file mode 120000 index d567e409..00000000 --- a/apps/mobile/apps/staff/windows/flutter/ephemeral/.plugin_symlinks/shared_preferences_windows +++ /dev/null @@ -1 +0,0 @@ -/Users/achinthaisuru/.pub-cache/hosted/pub.dev/shared_preferences_windows-2.4.1/ \ No newline at end of file diff --git a/apps/mobile/apps/staff/windows/flutter/generated_plugin_registrant.cc b/apps/mobile/apps/staff/windows/flutter/generated_plugin_registrant.cc index d141b74f..228d2240 100644 --- a/apps/mobile/apps/staff/windows/flutter/generated_plugin_registrant.cc +++ b/apps/mobile/apps/staff/windows/flutter/generated_plugin_registrant.cc @@ -8,10 +8,16 @@ #include #include +#include +#include void RegisterPlugins(flutter::PluginRegistry* registry) { FirebaseAuthPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("FirebaseAuthPluginCApi")); FirebaseCorePluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("FirebaseCorePluginCApi")); + GeolocatorWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("GeolocatorWindows")); + PermissionHandlerWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); } diff --git a/apps/mobile/apps/staff/windows/flutter/generated_plugins.cmake b/apps/mobile/apps/staff/windows/flutter/generated_plugins.cmake index 29944d5b..22690294 100644 --- a/apps/mobile/apps/staff/windows/flutter/generated_plugins.cmake +++ b/apps/mobile/apps/staff/windows/flutter/generated_plugins.cmake @@ -5,6 +5,8 @@ list(APPEND FLUTTER_PLUGIN_LIST firebase_auth firebase_core + geolocator_windows + permission_handler_windows ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/apps/mobile/config.dev.json b/apps/mobile/config.dev.json new file mode 100644 index 00000000..d2d5fa4c --- /dev/null +++ b/apps/mobile/config.dev.json @@ -0,0 +1,3 @@ +{ + "GOOGLE_PLACES_API_KEY": "AIzaSyAS9yTf4q51_CNSZ7mbmeS9V3l_LZR80lU" +} diff --git a/apps/mobile/packages/core/lib/core.dart b/apps/mobile/packages/core/lib/core.dart index f46af624..0b9e5ccf 100644 --- a/apps/mobile/packages/core/lib/core.dart +++ b/apps/mobile/packages/core/lib/core.dart @@ -2,3 +2,5 @@ library core; export 'src/domain/arguments/usecase_argument.dart'; export 'src/domain/usecases/usecase.dart'; +export 'src/utils/date_time_utils.dart'; +export 'src/presentation/widgets/web_mobile_frame.dart'; 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 new file mode 100644 index 00000000..0c5b1d00 --- /dev/null +++ b/apps/mobile/packages/core/lib/src/presentation/widgets/web_mobile_frame.dart @@ -0,0 +1,263 @@ +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 { + const WebMobileFrame({ + super.key, + required this.child, + required this.logo, + required this.appName, + }); + + final Widget child; + final Widget logo; + final String appName; + + @override + Widget build(BuildContext context) { + if (!kIsWeb) return child; + + return MaterialApp( + debugShowCheckedModeBanner: false, + theme: ThemeData.dark(), + home: _WebFrameContent(logo: logo, appName: appName, child: child), + ); + } +} + +class _WebFrameContent extends StatefulWidget { + 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(); +} + +class _WebFrameContentState extends State<_WebFrameContent> { + // ignore: unused_field + Offset _cursorPosition = Offset.zero; + // ignore: unused_field + bool _isHovering = false; + + @override + Widget build(BuildContext context) { + // iPhone 14 Pro Max-ish dimensions (scaled for frame look) + const double frameWidth = 390 * 1.2; + const double frameHeight = 844 * 1.3; + const double borderRadius = 54.0; + const double borderThickness = 12.0; + + return Scaffold( + backgroundColor: UiColors.foreground, + body: MouseRegion( + cursor: SystemMouseCursors.none, + onHover: (PointerHoverEvent event) { + setState(() { + _cursorPosition = event.position; + _isHovering = true; + }); + }, + onExit: (_) => setState(() => _isHovering = false), + child: Stack( + children: [ + // Logo and Title on the left (Web only) + Positioned( + left: 60, + top: 0, + bottom: 0, + child: Center( + child: Opacity( + opacity: 0.5, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(width: 140, child: widget.logo), + const SizedBox(height: 12), + Text( + widget.appName, + textAlign: TextAlign.left, + style: UiTypography.display1b.copyWith( + color: UiColors.white, + ), + ), + const SizedBox(height: 4), + Container( + height: 2, + width: 40, + color: UiColors.white.withOpacity(0.3), + ), + ], + ), + ), + ), + ), + + // Frame and Content + Center( + child: LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + // Scale down if screen is too small + 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; + + return Transform.scale( + scale: scale, + child: Container( + width: frameWidth, + height: frameHeight, + decoration: BoxDecoration( + color: UiColors.black, + borderRadius: BorderRadius.circular(borderRadius), + boxShadow: [ + BoxShadow( + color: UiColors.black.withOpacity(0.6), + blurRadius: 40, + spreadRadius: 10, + ), + ], + border: Border.all( + color: const Color(0xFF2C2C2C), + width: borderThickness, + ), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular( + borderRadius - borderThickness, + ), + child: Stack( + children: [ + // The actual app + status bar + Column( + children: [ + // Mock iOS Status Bar + Container( + height: 48, + padding: const EdgeInsets.symmetric( + horizontal: 24, + ), + decoration: const BoxDecoration( + color: UiColors.background, + border: Border( + bottom: BorderSide( + color: UiColors.border, + width: 0.5, + ), + ), + ), + child: const Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + // Time side + SizedBox( + width: 80, + child: Text( + '9:41 PM', + textAlign: TextAlign.center, + style: TextStyle( + color: UiColors.black, + fontWeight: FontWeight.w700, + fontSize: 14, + letterSpacing: -0.2, + ), + ), + ), + // Status Icons side + SizedBox( + width: 80, + child: Row( + mainAxisAlignment: + MainAxisAlignment.end, + spacing: 12, + children: [ + Icon( + Icons.signal_cellular_alt, + size: 14, + color: UiColors.black, + ), + Icon( + Icons.wifi, + size: 14, + color: UiColors.black, + ), + Icon( + Icons.battery_full, + size: 14, + color: UiColors.black, + ), + ], + ), + ), + ], + ), + ), + // The main app content + Expanded(child: widget.child), + ], + ), + + // Dynamic Island / Notch Mockup + Align( + alignment: Alignment.topCenter, + child: Container( + width: 120, + height: 35, + margin: const EdgeInsets.only(top: 10), + decoration: BoxDecoration( + color: UiColors.black, + borderRadius: BorderRadius.circular(20), + ), + ), + ), + ], + ), + ), + ), + ); + }, + ), + ), + 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/lib/src/utils/date_time_utils.dart b/apps/mobile/packages/core/lib/src/utils/date_time_utils.dart new file mode 100644 index 00000000..1d142b33 --- /dev/null +++ b/apps/mobile/packages/core/lib/src/utils/date_time_utils.dart @@ -0,0 +1,7 @@ + +class DateTimeUtils { + /// Converts a [DateTime] (assumed UTC if not specified) to the device's local time. + static DateTime toDeviceTime(DateTime date) { + return date.toLocal(); + } +} 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 diff --git a/apps/mobile/packages/core_localization/lib/core_localization.dart b/apps/mobile/packages/core_localization/lib/core_localization.dart index 8c8c71fc..0b0bc657 100644 --- a/apps/mobile/packages/core_localization/lib/core_localization.dart +++ b/apps/mobile/packages/core_localization/lib/core_localization.dart @@ -8,3 +8,4 @@ export 'src/domain/usecases/set_locale_use_case.dart'; export 'src/data/repositories_impl/locale_repository_impl.dart'; export 'src/data/datasources/locale_local_data_source.dart'; export 'src/localization_module.dart'; +export 'src/utils/error_translator.dart'; diff --git a/apps/mobile/packages/core_localization/lib/src/bloc/locale_bloc.dart b/apps/mobile/packages/core_localization/lib/src/bloc/locale_bloc.dart index 5ae60907..59065746 100644 --- a/apps/mobile/packages/core_localization/lib/src/bloc/locale_bloc.dart +++ b/apps/mobile/packages/core_localization/lib/src/bloc/locale_bloc.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import '../domain/usecases/get_default_locale_use_case.dart'; import '../domain/usecases/get_locale_use_case.dart'; +import '../domain/usecases/get_supported_locales_use_case.dart'; import '../domain/usecases/set_locale_use_case.dart'; import '../l10n/strings.g.dart'; import 'locale_event.dart'; @@ -11,27 +13,43 @@ import 'locale_state.dart'; /// It coordinates the flow between user language requests and persistent storage /// using [SetLocaleUseCase] and [GetLocaleUseCase]. class LocaleBloc extends Bloc { - final GetLocaleUseCase getLocaleUseCase; - final SetLocaleUseCase setLocaleUseCase; - /// Creates a [LocaleBloc] with the required use cases. - LocaleBloc({required this.getLocaleUseCase, required this.setLocaleUseCase}) - : super(LocaleState.initial()) { + LocaleBloc({ + required this.getLocaleUseCase, + required this.setLocaleUseCase, + required this.getSupportedLocalesUseCase, + required this.getDefaultLocaleUseCase, + }) : super(LocaleState.initial()) { on(_onChangeLocale); on(_onLoadLocale); + + /// Initial event + add(const LoadLocale()); } + /// Use case for retrieving the saved locale. + final GetLocaleUseCase getLocaleUseCase; + + /// Use case for saving the selected locale. + final SetLocaleUseCase setLocaleUseCase; + + /// Use case for retrieving supported locales. + final GetSupportedLocalesUseCase getSupportedLocalesUseCase; + + /// Use case for retrieving the default locale. + final GetDefaultLocaleUseCase getDefaultLocaleUseCase; + /// Handles the [ChangeLocale] event by saving it via the use case and emitting new state. Future _onChangeLocale( ChangeLocale event, Emitter emit, ) async { // 1. Update slang settings - LocaleSettings.setLocaleRaw(event.locale.languageCode); + await LocaleSettings.setLocaleRaw(event.locale.languageCode); // 2. Persist using Use Case await setLocaleUseCase(event.locale); - + // 3. Emit new state emit( LocaleState( @@ -46,11 +64,14 @@ class LocaleBloc extends Bloc { LoadLocale event, Emitter emit, ) async { - final Locale? savedLocale = await getLocaleUseCase(); - final Locale locale = const Locale('es'); + final Locale savedLocale = await getLocaleUseCase(); + final List supportedLocales = getSupportedLocalesUseCase(); - LocaleSettings.setLocaleRaw(locale.languageCode); + await LocaleSettings.setLocaleRaw(savedLocale.languageCode); - emit(LocaleState(locale: locale, supportedLocales: state.supportedLocales)); + emit(LocaleState( + locale: savedLocale, + supportedLocales: supportedLocales, + )); } } diff --git a/apps/mobile/packages/core_localization/lib/src/bloc/locale_state.dart b/apps/mobile/packages/core_localization/lib/src/bloc/locale_state.dart index 33219cd1..a37288ed 100644 --- a/apps/mobile/packages/core_localization/lib/src/bloc/locale_state.dart +++ b/apps/mobile/packages/core_localization/lib/src/bloc/locale_state.dart @@ -1,20 +1,21 @@ import 'package:flutter/material.dart'; + import '../l10n/strings.g.dart'; /// Represents the current state of the application's localization. class LocaleState { + /// Creates a [LocaleState] with the specified [locale]. + const LocaleState({required this.locale, required this.supportedLocales}); + + /// The initial state of the application, defaulting to English. + factory LocaleState.initial() => LocaleState( + locale: AppLocaleUtils.findDeviceLocale().flutterLocale, + supportedLocales: AppLocaleUtils.supportedLocales, + ); + /// The current active locale. final Locale locale; /// The list of supported locales for the application. final List supportedLocales; - - /// Creates a [LocaleState] with the specified [locale]. - const LocaleState({required this.locale, required this.supportedLocales}); - - /// The initial state of the application, defaulting to English. - factory LocaleState.initial() => LocaleState( - locale: const Locale('es'), - supportedLocales: AppLocaleUtils.supportedLocales, - ); } diff --git a/apps/mobile/packages/core_localization/lib/src/data/repositories_impl/locale_repository_impl.dart b/apps/mobile/packages/core_localization/lib/src/data/repositories_impl/locale_repository_impl.dart index ddda18cb..f3bd8d69 100644 --- a/apps/mobile/packages/core_localization/lib/src/data/repositories_impl/locale_repository_impl.dart +++ b/apps/mobile/packages/core_localization/lib/src/data/repositories_impl/locale_repository_impl.dart @@ -1,4 +1,6 @@ import 'dart:ui'; +import 'package:core_localization/src/l10n/strings.g.dart'; + import '../../domain/repositories/locale_repository_interface.dart'; import '../datasources/locale_local_data_source.dart'; @@ -7,22 +9,36 @@ import '../datasources/locale_local_data_source.dart'; /// This class handles the mapping between domain [Locale] objects and the raw /// strings handled by the [LocaleLocalDataSource]. class LocaleRepositoryImpl implements LocaleRepositoryInterface { - final LocaleLocalDataSource _localDataSource; + /// Creates a [LocaleRepositoryImpl] with the provided [localDataSource]. + LocaleRepositoryImpl({required this.localDataSource}); - /// Creates a [LocaleRepositoryImpl] with the provided [_localDataSource]. - LocaleRepositoryImpl(this._localDataSource); + final LocaleLocalDataSource localDataSource; @override Future saveLocale(Locale locale) { - return _localDataSource.saveLanguageCode(locale.languageCode); + return localDataSource.saveLanguageCode(locale.languageCode); } @override - Future getSavedLocale() async { - final String? languageCode = await _localDataSource.getLanguageCode(); + Future getSavedLocale() async { + return getDefaultLocale(); + + /// TODO: FEATURE_NOT_IMPLEMENTED: Implement saved locale retrieval later + final String? languageCode = await localDataSource.getLanguageCode(); if (languageCode != null) { return Locale(languageCode); } - return null; } + + @override + Locale getDefaultLocale() { + final Locale deviceLocale = AppLocaleUtils.findDeviceLocale().flutterLocale; + if (getSupportedLocales().contains(deviceLocale)) { + return deviceLocale; + } + return const Locale('en'); + } + + @override + List getSupportedLocales() => AppLocaleUtils.supportedLocales; } diff --git a/apps/mobile/packages/core_localization/lib/src/domain/repositories/locale_repository_interface.dart b/apps/mobile/packages/core_localization/lib/src/domain/repositories/locale_repository_interface.dart index 604c2d41..912d8248 100644 --- a/apps/mobile/packages/core_localization/lib/src/domain/repositories/locale_repository_interface.dart +++ b/apps/mobile/packages/core_localization/lib/src/domain/repositories/locale_repository_interface.dart @@ -13,5 +13,11 @@ abstract interface class LocaleRepositoryInterface { /// Retrieves the saved [locale] from persistent storage. /// /// Returns `null` if no locale has been previously saved. - Future getSavedLocale(); + Future getSavedLocale(); + + /// Retrieves the default [Locale] for the application. + Locale getDefaultLocale(); + + /// Retrieves the list of supported [Locale]s. + List getSupportedLocales(); } diff --git a/apps/mobile/packages/core_localization/lib/src/domain/usecases/get_default_locale_use_case.dart b/apps/mobile/packages/core_localization/lib/src/domain/usecases/get_default_locale_use_case.dart new file mode 100644 index 00000000..e416d1cd --- /dev/null +++ b/apps/mobile/packages/core_localization/lib/src/domain/usecases/get_default_locale_use_case.dart @@ -0,0 +1,15 @@ +import 'dart:ui'; +import '../repositories/locale_repository_interface.dart'; + +/// Use case to retrieve the default locale. +class GetDefaultLocaleUseCase { + final LocaleRepositoryInterface _repository; + + /// Creates a [GetDefaultLocaleUseCase] with the required [LocaleRepositoryInterface]. + GetDefaultLocaleUseCase(this._repository); + + /// Retrieves the default locale. + Locale call() { + return _repository.getDefaultLocale(); + } +} diff --git a/apps/mobile/packages/core_localization/lib/src/domain/usecases/get_locale_use_case.dart b/apps/mobile/packages/core_localization/lib/src/domain/usecases/get_locale_use_case.dart index 8d29876e..02256a69 100644 --- a/apps/mobile/packages/core_localization/lib/src/domain/usecases/get_locale_use_case.dart +++ b/apps/mobile/packages/core_localization/lib/src/domain/usecases/get_locale_use_case.dart @@ -13,7 +13,7 @@ class GetLocaleUseCase extends NoInputUseCase { GetLocaleUseCase(this._repository); @override - Future call() { + Future call() { return _repository.getSavedLocale(); } } diff --git a/apps/mobile/packages/core_localization/lib/src/domain/usecases/get_supported_locales_use_case.dart b/apps/mobile/packages/core_localization/lib/src/domain/usecases/get_supported_locales_use_case.dart new file mode 100644 index 00000000..8840b196 --- /dev/null +++ b/apps/mobile/packages/core_localization/lib/src/domain/usecases/get_supported_locales_use_case.dart @@ -0,0 +1,15 @@ +import 'dart:ui'; +import '../repositories/locale_repository_interface.dart'; + +/// Use case to retrieve the list of supported locales. +class GetSupportedLocalesUseCase { + final LocaleRepositoryInterface _repository; + + /// Creates a [GetSupportedLocalesUseCase] with the required [LocaleRepositoryInterface]. + GetSupportedLocalesUseCase(this._repository); + + /// Retrieves the supported locales. + List call() { + return _repository.getSupportedLocales(); + } +} diff --git a/apps/mobile/packages/core_localization/lib/src/l10n/en.i18n.json b/apps/mobile/packages/core_localization/lib/src/l10n/en.i18n.json index 8adab006..2aa6c54f 100644 --- a/apps/mobile/packages/core_localization/lib/src/l10n/en.i18n.json +++ b/apps/mobile/packages/core_localization/lib/src/l10n/en.i18n.json @@ -517,6 +517,8 @@ "secure_subtitle": "Your account details are encrypted and safe.", "primary": "Primary", "add_new_account": "Add New Account", + "bank_name": "Bank Name", + "bank_hint": "Enter bank name", "routing_number": "Routing Number", "routing_hint": "Enter routing number", "account_number": "Account Number", @@ -526,7 +528,8 @@ "savings": "Savings", "cancel": "Cancel", "save": "Save", - "account_ending": "Ending in $last4" + "account_ending": "Ending in $last4", + "account_added_success": "Bank account added successfully!" }, "logout": { "button": "Sign Out" @@ -725,6 +728,60 @@ "paid": "Paid", "pending": "Pending" } + }, + "errors": { + "auth": { + "invalid_credentials": "The email or password you entered is incorrect.", + "account_exists": "An account with this email already exists. Try signing in instead.", + "session_expired": "Your session has expired. Please sign in again.", + "user_not_found": "We couldn't find your account. Please check your email and try again.", + "unauthorized_app": "This account is not authorized for this app.", + "weak_password": "Please choose a stronger password with at least 8 characters.", + "sign_up_failed": "We couldn't create your account. Please try again.", + "sign_in_failed": "We couldn't sign you in. Please try again.", + "not_authenticated": "Please sign in to continue.", + "password_mismatch": "This email is already registered. Please use the correct password or tap 'Forgot Password' to reset it.", + "google_only_account": "This email is registered via Google. Please use 'Forgot Password' to set a password, then try signing up again with the same information." + }, + "hub": { + "has_orders": "This hub has active orders and cannot be deleted.", + "not_found": "The hub you're looking for doesn't exist.", + "creation_failed": "We couldn't create the hub. Please try again." + }, + "order": { + "missing_hub": "Please select a location for your order.", + "missing_vendor": "Please select a vendor for your order.", + "creation_failed": "We couldn't create your order. Please try again.", + "shift_creation_failed": "We couldn't schedule the shift. Please try again.", + "missing_business": "Your business profile couldn't be loaded. Please sign in again." + }, + "profile": { + "staff_not_found": "Your profile couldn't be loaded. Please sign in again.", + "business_not_found": "Your business profile couldn't be loaded. Please sign in again.", + "update_failed": "We couldn't update your profile. Please try again." + }, + "shift": { + "no_open_roles": "There are no open positions available for this shift.", + "application_not_found": "Your application couldn't be found.", + "no_active_shift": "You don't have an active shift to clock out from." + }, + "generic": { + "unknown": "Something went wrong. Please try again.", + "no_connection": "No internet connection. Please check your network and try again." + } + }, + "success": { + "hub": { + "created": "Hub created successfully!", + "deleted": "Hub deleted successfully!", + "nfc_assigned": "NFC tag assigned successfully!" + }, + "order": { + "created": "Order created successfully!" + }, + "profile": { + "updated": "Profile updated successfully!" + } } } diff --git a/apps/mobile/packages/core_localization/lib/src/l10n/es.i18n.json b/apps/mobile/packages/core_localization/lib/src/l10n/es.i18n.json index ea3fc08c..6e7e8f1c 100644 --- a/apps/mobile/packages/core_localization/lib/src/l10n/es.i18n.json +++ b/apps/mobile/packages/core_localization/lib/src/l10n/es.i18n.json @@ -515,6 +515,8 @@ "secure_title": "Seguro y Cifrado", "secure_subtitle": "Su información bancaria está cifrada y almacenada de forma segura. Nunca compartimos sus detalles.", "add_new_account": "Agregar Nueva Cuenta", + "bank_name": "Nombre del Banco", + "bank_hint": "Ingrese nombre del banco", "routing_number": "Número de Ruta", "routing_hint": "9 dígitos", "account_number": "Número de Cuenta", @@ -525,7 +527,8 @@ "cancel": "Cancelar", "save": "Guardar", "primary": "Principal", - "account_ending": "Termina en $last4" + "account_ending": "Termina en $last4", + "account_added_success": "¡Cuenta bancaria agregada exitosamente!" }, "logout": { "button": "Cerrar Sesión" @@ -724,5 +727,59 @@ "paid": "Pagado", "pending": "Pendiente" } + }, + "errors": { + "auth": { + "invalid_credentials": "El correo electrónico o la contraseña que ingresaste es incorrecta.", + "account_exists": "Ya existe una cuenta con este correo electrónico. Intenta iniciar sesión.", + "session_expired": "Tu sesión ha expirado. Por favor, inicia sesión de nuevo.", + "user_not_found": "No pudimos encontrar tu cuenta. Por favor, verifica tu correo electrónico e intenta de nuevo.", + "unauthorized_app": "Esta cuenta no está autorizada para esta aplicación.", + "weak_password": "Por favor, elige una contraseña más segura con al menos 8 caracteres.", + "sign_up_failed": "No pudimos crear tu cuenta. Por favor, intenta de nuevo.", + "sign_in_failed": "No pudimos iniciar sesión. Por favor, intenta de nuevo.", + "not_authenticated": "Por favor, inicia sesión para continuar.", + "password_mismatch": "Este correo ya está registrado. Por favor, usa la contraseña correcta o toca 'Olvidé mi contraseña' para restablecerla.", + "google_only_account": "Este correo está registrado con Google. Por favor, usa 'Olvidé mi contraseña' para establecer una contraseña, luego intenta registrarte de nuevo con la misma información." + }, + "hub": { + "has_orders": "Este hub tiene órdenes activas y no puede ser eliminado.", + "not_found": "El hub que buscas no existe.", + "creation_failed": "No pudimos crear el hub. Por favor, intenta de nuevo." + }, + "order": { + "missing_hub": "Por favor, selecciona una ubicación para tu orden.", + "missing_vendor": "Por favor, selecciona un proveedor para tu orden.", + "creation_failed": "No pudimos crear tu orden. Por favor, intenta de nuevo.", + "shift_creation_failed": "No pudimos programar el turno. Por favor, intenta de nuevo.", + "missing_business": "No se pudo cargar tu perfil de empresa. Por favor, inicia sesión de nuevo." + }, + "profile": { + "staff_not_found": "No se pudo cargar tu perfil. Por favor, inicia sesión de nuevo.", + "business_not_found": "No se pudo cargar tu perfil de empresa. Por favor, inicia sesión de nuevo.", + "update_failed": "No pudimos actualizar tu perfil. Por favor, intenta de nuevo." + }, + "shift": { + "no_open_roles": "No hay posiciones abiertas disponibles para este turno.", + "application_not_found": "No se pudo encontrar tu solicitud.", + "no_active_shift": "No tienes un turno activo para registrar salida." + }, + "generic": { + "unknown": "Algo salió mal. Por favor, intenta de nuevo.", + "no_connection": "Sin conexión a internet. Por favor, verifica tu red e intenta de nuevo." + } + }, + "success": { + "hub": { + "created": "¡Hub creado exitosamente!", + "deleted": "¡Hub eliminado exitosamente!", + "nfc_assigned": "¡Etiqueta NFC asignada exitosamente!" + }, + "order": { + "created": "¡Orden creada exitosamente!" + }, + "profile": { + "updated": "¡Perfil actualizado exitosamente!" + } } } diff --git a/apps/mobile/packages/core_localization/lib/src/l10n/strings.g.dart b/apps/mobile/packages/core_localization/lib/src/l10n/strings.g.dart deleted file mode 100644 index 60de65e3..00000000 --- a/apps/mobile/packages/core_localization/lib/src/l10n/strings.g.dart +++ /dev/null @@ -1,183 +0,0 @@ -/// Generated file. Do not edit. -/// -/// Source: lib/src/l10n -/// To regenerate, run: `dart run slang` -/// -/// Locales: 2 -/// Strings: 1038 (519 per locale) -/// -/// Built on 2026-01-27 at 19:37 UTC - -// coverage:ignore-file -// ignore_for_file: type=lint, unused_import -// dart format off - -import 'package:flutter/widgets.dart'; -import 'package:intl/intl.dart'; -import 'package:slang/generated.dart'; -import 'package:slang_flutter/slang_flutter.dart'; -export 'package:slang_flutter/slang_flutter.dart'; - -import 'strings_es.g.dart' deferred as l_es; -part 'strings_en.g.dart'; - -/// Supported locales. -/// -/// Usage: -/// - LocaleSettings.setLocale(AppLocale.en) // set locale -/// - Locale locale = AppLocale.en.flutterLocale // get flutter locale from enum -/// - if (LocaleSettings.currentLocale == AppLocale.en) // locale check -enum AppLocale with BaseAppLocale { - en(languageCode: 'en'), - es(languageCode: 'es'); - - const AppLocale({ - required this.languageCode, - this.scriptCode, // ignore: unused_element, unused_element_parameter - this.countryCode, // ignore: unused_element, unused_element_parameter - }); - - @override final String languageCode; - @override final String? scriptCode; - @override final String? countryCode; - - @override - Future build({ - Map? overrides, - PluralResolver? cardinalResolver, - PluralResolver? ordinalResolver, - }) async { - switch (this) { - case AppLocale.en: - return TranslationsEn( - overrides: overrides, - cardinalResolver: cardinalResolver, - ordinalResolver: ordinalResolver, - ); - case AppLocale.es: - await l_es.loadLibrary(); - return l_es.TranslationsEs( - overrides: overrides, - cardinalResolver: cardinalResolver, - ordinalResolver: ordinalResolver, - ); - } - } - - @override - Translations buildSync({ - Map? overrides, - PluralResolver? cardinalResolver, - PluralResolver? ordinalResolver, - }) { - switch (this) { - case AppLocale.en: - return TranslationsEn( - overrides: overrides, - cardinalResolver: cardinalResolver, - ordinalResolver: ordinalResolver, - ); - case AppLocale.es: - return l_es.TranslationsEs( - overrides: overrides, - cardinalResolver: cardinalResolver, - ordinalResolver: ordinalResolver, - ); - } - } - - /// Gets current instance managed by [LocaleSettings]. - Translations get translations => LocaleSettings.instance.getTranslations(this); -} - -/// Method A: Simple -/// -/// No rebuild after locale change. -/// Translation happens during initialization of the widget (call of t). -/// Configurable via 'translate_var'. -/// -/// Usage: -/// String a = t.someKey.anotherKey; -/// String b = t['someKey.anotherKey']; // Only for edge cases! -Translations get t => LocaleSettings.instance.currentTranslations; - -/// Method B: Advanced -/// -/// All widgets using this method will trigger a rebuild when locale changes. -/// Use this if you have e.g. a settings page where the user can select the locale during runtime. -/// -/// Step 1: -/// wrap your App with -/// TranslationProvider( -/// child: MyApp() -/// ); -/// -/// Step 2: -/// final t = Translations.of(context); // Get t variable. -/// String a = t.someKey.anotherKey; // Use t variable. -/// String b = t['someKey.anotherKey']; // Only for edge cases! -class TranslationProvider extends BaseTranslationProvider { - TranslationProvider({required super.child}) : super(settings: LocaleSettings.instance); - - static InheritedLocaleData of(BuildContext context) => InheritedLocaleData.of(context); -} - -/// Method B shorthand via [BuildContext] extension method. -/// Configurable via 'translate_var'. -/// -/// Usage (e.g. in a widget's build method): -/// context.t.someKey.anotherKey -extension BuildContextTranslationsExtension on BuildContext { - Translations get t => TranslationProvider.of(this).translations; -} - -/// Manages all translation instances and the current locale -class LocaleSettings extends BaseFlutterLocaleSettings { - LocaleSettings._() : super( - utils: AppLocaleUtils.instance, - lazy: true, - ); - - static final instance = LocaleSettings._(); - - // static aliases (checkout base methods for documentation) - static AppLocale get currentLocale => instance.currentLocale; - static Stream getLocaleStream() => instance.getLocaleStream(); - static Future setLocale(AppLocale locale, {bool? listenToDeviceLocale = false}) => instance.setLocale(locale, listenToDeviceLocale: listenToDeviceLocale); - static Future setLocaleRaw(String rawLocale, {bool? listenToDeviceLocale = false}) => instance.setLocaleRaw(rawLocale, listenToDeviceLocale: listenToDeviceLocale); - static Future useDeviceLocale() => instance.useDeviceLocale(); - static Future setPluralResolver({String? language, AppLocale? locale, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) => instance.setPluralResolver( - language: language, - locale: locale, - cardinalResolver: cardinalResolver, - ordinalResolver: ordinalResolver, - ); - - // synchronous versions - static AppLocale setLocaleSync(AppLocale locale, {bool? listenToDeviceLocale = false}) => instance.setLocaleSync(locale, listenToDeviceLocale: listenToDeviceLocale); - static AppLocale setLocaleRawSync(String rawLocale, {bool? listenToDeviceLocale = false}) => instance.setLocaleRawSync(rawLocale, listenToDeviceLocale: listenToDeviceLocale); - static AppLocale useDeviceLocaleSync() => instance.useDeviceLocaleSync(); - static void setPluralResolverSync({String? language, AppLocale? locale, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) => instance.setPluralResolverSync( - language: language, - locale: locale, - cardinalResolver: cardinalResolver, - ordinalResolver: ordinalResolver, - ); -} - -/// Provides utility functions without any side effects. -class AppLocaleUtils extends BaseAppLocaleUtils { - AppLocaleUtils._() : super( - baseLocale: AppLocale.en, - locales: AppLocale.values, - ); - - static final instance = AppLocaleUtils._(); - - // static aliases (checkout base methods for documentation) - static AppLocale parse(String rawLocale) => instance.parse(rawLocale); - static AppLocale parseLocaleParts({required String languageCode, String? scriptCode, String? countryCode}) => instance.parseLocaleParts(languageCode: languageCode, scriptCode: scriptCode, countryCode: countryCode); - static AppLocale findDeviceLocale() => instance.findDeviceLocale(); - static List get supportedLocales => instance.supportedLocales; - static List get supportedLocalesRaw => instance.supportedLocalesRaw; -} diff --git a/apps/mobile/packages/core_localization/lib/src/l10n/strings_en.g.dart b/apps/mobile/packages/core_localization/lib/src/l10n/strings_en.g.dart deleted file mode 100644 index 3f4a7e81..00000000 --- a/apps/mobile/packages/core_localization/lib/src/l10n/strings_en.g.dart +++ /dev/null @@ -1,3210 +0,0 @@ -/// -/// Generated file. Do not edit. -/// -// coverage:ignore-file -// ignore_for_file: type=lint, unused_import -// dart format off - -part of 'strings.g.dart'; - -// Path: -typedef TranslationsEn = Translations; // ignore: unused_element -class Translations with BaseTranslations { - /// Returns the current translations of the given [context]. - /// - /// Usage: - /// final t = Translations.of(context); - static Translations of(BuildContext context) => InheritedLocaleData.of(context).translations; - - /// You can call this constructor and build your own translation instance of this locale. - /// Constructing via the enum [AppLocale.build] is preferred. - Translations({Map? overrides, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver, TranslationMetadata? meta}) - : assert(overrides == null, 'Set "translation_overrides: true" in order to enable this feature.'), - $meta = meta ?? TranslationMetadata( - locale: AppLocale.en, - overrides: overrides ?? {}, - cardinalResolver: cardinalResolver, - ordinalResolver: ordinalResolver, - ) { - $meta.setFlatMapFunction(_flatMapFunction); - } - - /// Metadata for the translations of . - @override final TranslationMetadata $meta; - - /// Access flat map - dynamic operator[](String key) => $meta.getTranslation(key); - - late final Translations _root = this; // ignore: unused_field - - Translations $copyWith({TranslationMetadata? meta}) => Translations(meta: meta ?? this.$meta); - - // Translations - late final TranslationsCommonEn common = TranslationsCommonEn._(_root); - late final TranslationsSettingsEn settings = TranslationsSettingsEn._(_root); - late final TranslationsStaffAuthenticationEn staff_authentication = TranslationsStaffAuthenticationEn._(_root); - late final TranslationsClientAuthenticationEn client_authentication = TranslationsClientAuthenticationEn._(_root); - late final TranslationsClientHomeEn client_home = TranslationsClientHomeEn._(_root); - late final TranslationsClientSettingsEn client_settings = TranslationsClientSettingsEn._(_root); - late final TranslationsClientHubsEn client_hubs = TranslationsClientHubsEn._(_root); - late final TranslationsClientCreateOrderEn client_create_order = TranslationsClientCreateOrderEn._(_root); - late final TranslationsClientMainEn client_main = TranslationsClientMainEn._(_root); - late final TranslationsClientViewOrdersEn client_view_orders = TranslationsClientViewOrdersEn._(_root); - late final TranslationsClientBillingEn client_billing = TranslationsClientBillingEn._(_root); - late final TranslationsStaffEn staff = TranslationsStaffEn._(_root); - late final TranslationsStaffDocumentsEn staff_documents = TranslationsStaffDocumentsEn._(_root); - late final TranslationsStaffCertificatesEn staff_certificates = TranslationsStaffCertificatesEn._(_root); - late final TranslationsStaffProfileAttireEn staff_profile_attire = TranslationsStaffProfileAttireEn._(_root); - late final TranslationsStaffShiftsEn staff_shifts = TranslationsStaffShiftsEn._(_root); - late final TranslationsStaffTimeCardEn staff_time_card = TranslationsStaffTimeCardEn._(_root); -} - -// Path: common -class TranslationsCommonEn { - TranslationsCommonEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'OK' - String get ok => 'OK'; - - /// en: 'Cancel' - String get cancel => 'Cancel'; - - /// en: 'Save' - String get save => 'Save'; - - /// en: 'Delete' - String get delete => 'Delete'; - - /// en: 'Continue' - String get continue_text => 'Continue'; -} - -// Path: settings -class TranslationsSettingsEn { - TranslationsSettingsEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'Language' - String get language => 'Language'; - - /// en: 'Change Language' - String get change_language => 'Change Language'; -} - -// Path: staff_authentication -class TranslationsStaffAuthenticationEn { - TranslationsStaffAuthenticationEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - late final TranslationsStaffAuthenticationGetStartedPageEn get_started_page = TranslationsStaffAuthenticationGetStartedPageEn._(_root); - late final TranslationsStaffAuthenticationPhoneVerificationPageEn phone_verification_page = TranslationsStaffAuthenticationPhoneVerificationPageEn._(_root); - late final TranslationsStaffAuthenticationPhoneInputEn phone_input = TranslationsStaffAuthenticationPhoneInputEn._(_root); - late final TranslationsStaffAuthenticationOtpVerificationEn otp_verification = TranslationsStaffAuthenticationOtpVerificationEn._(_root); - late final TranslationsStaffAuthenticationProfileSetupPageEn profile_setup_page = TranslationsStaffAuthenticationProfileSetupPageEn._(_root); - late final TranslationsStaffAuthenticationCommonEn common = TranslationsStaffAuthenticationCommonEn._(_root); -} - -// Path: client_authentication -class TranslationsClientAuthenticationEn { - TranslationsClientAuthenticationEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - late final TranslationsClientAuthenticationGetStartedPageEn get_started_page = TranslationsClientAuthenticationGetStartedPageEn._(_root); - late final TranslationsClientAuthenticationSignInPageEn sign_in_page = TranslationsClientAuthenticationSignInPageEn._(_root); - late final TranslationsClientAuthenticationSignUpPageEn sign_up_page = TranslationsClientAuthenticationSignUpPageEn._(_root); -} - -// Path: client_home -class TranslationsClientHomeEn { - TranslationsClientHomeEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - late final TranslationsClientHomeDashboardEn dashboard = TranslationsClientHomeDashboardEn._(_root); - late final TranslationsClientHomeWidgetsEn widgets = TranslationsClientHomeWidgetsEn._(_root); - late final TranslationsClientHomeActionsEn actions = TranslationsClientHomeActionsEn._(_root); - late final TranslationsClientHomeReorderEn reorder = TranslationsClientHomeReorderEn._(_root); - late final TranslationsClientHomeFormEn form = TranslationsClientHomeFormEn._(_root); -} - -// Path: client_settings -class TranslationsClientSettingsEn { - TranslationsClientSettingsEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - late final TranslationsClientSettingsProfileEn profile = TranslationsClientSettingsProfileEn._(_root); -} - -// Path: client_hubs -class TranslationsClientHubsEn { - TranslationsClientHubsEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'Hubs' - String get title => 'Hubs'; - - /// en: 'Manage clock-in locations' - String get subtitle => 'Manage clock-in locations'; - - /// en: 'Add Hub' - String get add_hub => 'Add Hub'; - - late final TranslationsClientHubsEmptyStateEn empty_state = TranslationsClientHubsEmptyStateEn._(_root); - late final TranslationsClientHubsAboutHubsEn about_hubs = TranslationsClientHubsAboutHubsEn._(_root); - late final TranslationsClientHubsHubCardEn hub_card = TranslationsClientHubsHubCardEn._(_root); - late final TranslationsClientHubsAddHubDialogEn add_hub_dialog = TranslationsClientHubsAddHubDialogEn._(_root); - late final TranslationsClientHubsNfcDialogEn nfc_dialog = TranslationsClientHubsNfcDialogEn._(_root); -} - -// Path: client_create_order -class TranslationsClientCreateOrderEn { - TranslationsClientCreateOrderEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'Create Order' - String get title => 'Create Order'; - - /// en: 'ORDER TYPE' - String get section_title => 'ORDER TYPE'; - - late final TranslationsClientCreateOrderTypesEn types = TranslationsClientCreateOrderTypesEn._(_root); - late final TranslationsClientCreateOrderRapidEn rapid = TranslationsClientCreateOrderRapidEn._(_root); - late final TranslationsClientCreateOrderOneTimeEn one_time = TranslationsClientCreateOrderOneTimeEn._(_root); - late final TranslationsClientCreateOrderRecurringEn recurring = TranslationsClientCreateOrderRecurringEn._(_root); - late final TranslationsClientCreateOrderPermanentEn permanent = TranslationsClientCreateOrderPermanentEn._(_root); -} - -// Path: client_main -class TranslationsClientMainEn { - TranslationsClientMainEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - late final TranslationsClientMainTabsEn tabs = TranslationsClientMainTabsEn._(_root); -} - -// Path: client_view_orders -class TranslationsClientViewOrdersEn { - TranslationsClientViewOrdersEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'Orders' - String get title => 'Orders'; - - /// en: 'Post' - String get post_button => 'Post'; - - /// en: 'Post an Order' - String get post_order => 'Post an Order'; - - /// en: 'No orders for $date' - String no_orders({required Object date}) => 'No orders for ${date}'; - - late final TranslationsClientViewOrdersTabsEn tabs = TranslationsClientViewOrdersTabsEn._(_root); - late final TranslationsClientViewOrdersCardEn card = TranslationsClientViewOrdersCardEn._(_root); -} - -// Path: client_billing -class TranslationsClientBillingEn { - TranslationsClientBillingEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'Billing' - String get title => 'Billing'; - - /// en: 'Current Period' - String get current_period => 'Current Period'; - - /// en: '$amount saved' - String saved_amount({required Object amount}) => '${amount} saved'; - - /// en: 'Awaiting Approval' - String get awaiting_approval => 'Awaiting Approval'; - - /// en: 'Payment Method' - String get payment_method => 'Payment Method'; - - /// en: 'Add' - String get add_payment => 'Add'; - - /// en: 'Default' - String get default_badge => 'Default'; - - /// en: 'Expires $date' - String expires({required Object date}) => 'Expires ${date}'; - - /// en: 'This Period Breakdown' - String get period_breakdown => 'This Period Breakdown'; - - /// en: 'Week' - String get week => 'Week'; - - /// en: 'Month' - String get month => 'Month'; - - /// en: 'Total' - String get total => 'Total'; - - /// en: '$count hours' - String hours({required Object count}) => '${count} hours'; - - /// en: 'Rate Optimization' - String get rate_optimization_title => 'Rate Optimization'; - - /// en: 'Save $amount/month by switching 3 shifts' - String rate_optimization_body({required Object amount}) => 'Save ${amount}/month by switching 3 shifts'; - - /// en: 'View Details' - String get view_details => 'View Details'; - - /// en: 'Invoice History' - String get invoice_history => 'Invoice History'; - - /// en: 'View all' - String get view_all => 'View all'; - - /// en: 'Export All Invoices' - String get export_button => 'Export All Invoices'; - - /// en: 'PENDING APPROVAL' - String get pending_badge => 'PENDING APPROVAL'; - - /// en: 'PAID' - String get paid_badge => 'PAID'; -} - -// Path: staff -class TranslationsStaffEn { - TranslationsStaffEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - late final TranslationsStaffMainEn main = TranslationsStaffMainEn._(_root); - late final TranslationsStaffHomeEn home = TranslationsStaffHomeEn._(_root); - late final TranslationsStaffProfileEn profile = TranslationsStaffProfileEn._(_root); - late final TranslationsStaffOnboardingEn onboarding = TranslationsStaffOnboardingEn._(_root); -} - -// Path: staff_documents -class TranslationsStaffDocumentsEn { - TranslationsStaffDocumentsEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'Documents' - String get title => 'Documents'; - - late final TranslationsStaffDocumentsVerificationCardEn verification_card = TranslationsStaffDocumentsVerificationCardEn._(_root); - late final TranslationsStaffDocumentsListEn list = TranslationsStaffDocumentsListEn._(_root); - late final TranslationsStaffDocumentsCardEn card = TranslationsStaffDocumentsCardEn._(_root); -} - -// Path: staff_certificates -class TranslationsStaffCertificatesEn { - TranslationsStaffCertificatesEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'Certificates' - String get title => 'Certificates'; - - late final TranslationsStaffCertificatesProgressEn progress = TranslationsStaffCertificatesProgressEn._(_root); - late final TranslationsStaffCertificatesCardEn card = TranslationsStaffCertificatesCardEn._(_root); - late final TranslationsStaffCertificatesAddMoreEn add_more = TranslationsStaffCertificatesAddMoreEn._(_root); - late final TranslationsStaffCertificatesUploadModalEn upload_modal = TranslationsStaffCertificatesUploadModalEn._(_root); - late final TranslationsStaffCertificatesDeleteModalEn delete_modal = TranslationsStaffCertificatesDeleteModalEn._(_root); -} - -// Path: staff_profile_attire -class TranslationsStaffProfileAttireEn { - TranslationsStaffProfileAttireEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'Attire' - String get title => 'Attire'; - - late final TranslationsStaffProfileAttireInfoCardEn info_card = TranslationsStaffProfileAttireInfoCardEn._(_root); - late final TranslationsStaffProfileAttireStatusEn status = TranslationsStaffProfileAttireStatusEn._(_root); - - /// en: 'I certify that I own these items and will wear them to my shifts. I understand that items are pending manager verification at my first shift.' - String get attestation => 'I certify that I own these items and will wear them to my shifts. I understand that items are pending manager verification at my first shift.'; - - late final TranslationsStaffProfileAttireActionsEn actions = TranslationsStaffProfileAttireActionsEn._(_root); - late final TranslationsStaffProfileAttireValidationEn validation = TranslationsStaffProfileAttireValidationEn._(_root); -} - -// Path: staff_shifts -class TranslationsStaffShiftsEn { - TranslationsStaffShiftsEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'Shifts' - String get title => 'Shifts'; - - late final TranslationsStaffShiftsTabsEn tabs = TranslationsStaffShiftsTabsEn._(_root); - late final TranslationsStaffShiftsListEn list = TranslationsStaffShiftsListEn._(_root); - late final TranslationsStaffShiftsFilterEn filter = TranslationsStaffShiftsFilterEn._(_root); - late final TranslationsStaffShiftsStatusEn status = TranslationsStaffShiftsStatusEn._(_root); - late final TranslationsStaffShiftsActionEn action = TranslationsStaffShiftsActionEn._(_root); - late final TranslationsStaffShiftsDetailsEn details = TranslationsStaffShiftsDetailsEn._(_root); - late final TranslationsStaffShiftsTagsEn tags = TranslationsStaffShiftsTagsEn._(_root); -} - -// Path: staff_time_card -class TranslationsStaffTimeCardEn { - TranslationsStaffTimeCardEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'Timecard' - String get title => 'Timecard'; - - /// en: 'Hours Worked' - String get hours_worked => 'Hours Worked'; - - /// en: 'Total Earnings' - String get total_earnings => 'Total Earnings'; - - /// en: 'Shift History' - String get shift_history => 'Shift History'; - - /// en: 'No shifts for this month' - String get no_shifts => 'No shifts for this month'; - - /// en: 'hours' - String get hours => 'hours'; - - /// en: '/hr' - String get per_hr => '/hr'; - - late final TranslationsStaffTimeCardStatusEn status = TranslationsStaffTimeCardStatusEn._(_root); -} - -// Path: staff_authentication.get_started_page -class TranslationsStaffAuthenticationGetStartedPageEn { - TranslationsStaffAuthenticationGetStartedPageEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'Work, Grow, ' - String get title_part1 => 'Work, Grow, '; - - /// en: 'Elevate' - String get title_part2 => 'Elevate'; - - /// en: 'Build your career in hospitality with flexibility and freedom.' - String get subtitle => 'Build your career in hospitality with \nflexibility and freedom.'; - - /// en: 'Sign Up' - String get sign_up_button => 'Sign Up'; - - /// en: 'Log In' - String get log_in_button => 'Log In'; -} - -// Path: staff_authentication.phone_verification_page -class TranslationsStaffAuthenticationPhoneVerificationPageEn { - TranslationsStaffAuthenticationPhoneVerificationPageEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'Please enter a valid 10-digit phone number' - String get validation_error => 'Please enter a valid 10-digit phone number'; - - /// en: 'Send Code' - String get send_code_button => 'Send Code'; - - /// en: 'Enter verification code' - String get enter_code_title => 'Enter verification code'; - - /// en: 'We sent a 6-digit code to ' - String get code_sent_message => 'We sent a 6-digit code to '; - - /// en: '. Enter it below to verify your account.' - String get code_sent_instruction => '. Enter it below to verify your account.'; -} - -// Path: staff_authentication.phone_input -class TranslationsStaffAuthenticationPhoneInputEn { - TranslationsStaffAuthenticationPhoneInputEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'Verify your phone number' - String get title => 'Verify your phone number'; - - /// en: 'We'll send you a verification code to get started.' - String get subtitle => 'We\'ll send you a verification code to get started.'; - - /// en: 'Phone Number' - String get label => 'Phone Number'; - - /// en: 'Enter your number' - String get hint => 'Enter your number'; -} - -// Path: staff_authentication.otp_verification -class TranslationsStaffAuthenticationOtpVerificationEn { - TranslationsStaffAuthenticationOtpVerificationEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'Didn't get the code ?' - String get did_not_get_code => 'Didn\'t get the code ?'; - - /// en: 'Resend in $seconds s' - String resend_in({required Object seconds}) => 'Resend in ${seconds} s'; - - /// en: 'Resend code' - String get resend_code => 'Resend code'; -} - -// Path: staff_authentication.profile_setup_page -class TranslationsStaffAuthenticationProfileSetupPageEn { - TranslationsStaffAuthenticationProfileSetupPageEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'Step $current of $total' - String step_indicator({required Object current, required Object total}) => 'Step ${current} of ${total}'; - - /// en: 'An error occurred' - String get error_occurred => 'An error occurred'; - - /// en: 'Complete Setup' - String get complete_setup_button => 'Complete Setup'; - - late final TranslationsStaffAuthenticationProfileSetupPageStepsEn steps = TranslationsStaffAuthenticationProfileSetupPageStepsEn._(_root); - late final TranslationsStaffAuthenticationProfileSetupPageBasicInfoEn basic_info = TranslationsStaffAuthenticationProfileSetupPageBasicInfoEn._(_root); - late final TranslationsStaffAuthenticationProfileSetupPageLocationEn location = TranslationsStaffAuthenticationProfileSetupPageLocationEn._(_root); - late final TranslationsStaffAuthenticationProfileSetupPageExperienceEn experience = TranslationsStaffAuthenticationProfileSetupPageExperienceEn._(_root); -} - -// Path: staff_authentication.common -class TranslationsStaffAuthenticationCommonEn { - TranslationsStaffAuthenticationCommonEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'Having trouble? ' - String get trouble_question => 'Having trouble? '; - - /// en: 'Contact Support' - String get contact_support => 'Contact Support'; -} - -// Path: client_authentication.get_started_page -class TranslationsClientAuthenticationGetStartedPageEn { - TranslationsClientAuthenticationGetStartedPageEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'Take Control of Your Shifts and Events' - String get title => 'Take Control of Your\nShifts and Events'; - - /// en: 'Streamline your operations with powerful tools to manage schedules, track performance, and keep your team on the same page—all in one place' - String get subtitle => 'Streamline your operations with powerful tools to manage schedules, track performance, and keep your team on the same page—all in one place'; - - /// en: 'Sign In' - String get sign_in_button => 'Sign In'; - - /// en: 'Create Account' - String get create_account_button => 'Create Account'; -} - -// Path: client_authentication.sign_in_page -class TranslationsClientAuthenticationSignInPageEn { - TranslationsClientAuthenticationSignInPageEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'Welcome Back' - String get title => 'Welcome Back'; - - /// en: 'Sign in to manage your shifts and workers' - String get subtitle => 'Sign in to manage your shifts and workers'; - - /// en: 'Email' - String get email_label => 'Email'; - - /// en: 'Enter your email' - String get email_hint => 'Enter your email'; - - /// en: 'Password' - String get password_label => 'Password'; - - /// en: 'Enter your password' - String get password_hint => 'Enter your password'; - - /// en: 'Forgot Password?' - String get forgot_password => 'Forgot Password?'; - - /// en: 'Sign In' - String get sign_in_button => 'Sign In'; - - /// en: 'or' - String get or_divider => 'or'; - - /// en: 'Sign In with Apple' - String get social_apple => 'Sign In with Apple'; - - /// en: 'Sign In with Google' - String get social_google => 'Sign In with Google'; - - /// en: 'Don't have an account? ' - String get no_account => 'Don\'t have an account? '; - - /// en: 'Sign Up' - String get sign_up_link => 'Sign Up'; -} - -// Path: client_authentication.sign_up_page -class TranslationsClientAuthenticationSignUpPageEn { - TranslationsClientAuthenticationSignUpPageEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'Create Account' - String get title => 'Create Account'; - - /// en: 'Get started with Krow for your business' - String get subtitle => 'Get started with Krow for your business'; - - /// en: 'Company Name' - String get company_label => 'Company Name'; - - /// en: 'Enter company name' - String get company_hint => 'Enter company name'; - - /// en: 'Email' - String get email_label => 'Email'; - - /// en: 'Enter your email' - String get email_hint => 'Enter your email'; - - /// en: 'Password' - String get password_label => 'Password'; - - /// en: 'Create a password' - String get password_hint => 'Create a password'; - - /// en: 'Confirm Password' - String get confirm_password_label => 'Confirm Password'; - - /// en: 'Confirm your password' - String get confirm_password_hint => 'Confirm your password'; - - /// en: 'Create Account' - String get create_account_button => 'Create Account'; - - /// en: 'or' - String get or_divider => 'or'; - - /// en: 'Sign Up with Apple' - String get social_apple => 'Sign Up with Apple'; - - /// en: 'Sign Up with Google' - String get social_google => 'Sign Up with Google'; - - /// en: 'Already have an account? ' - String get has_account => 'Already have an account? '; - - /// en: 'Sign In' - String get sign_in_link => 'Sign In'; -} - -// Path: client_home.dashboard -class TranslationsClientHomeDashboardEn { - TranslationsClientHomeDashboardEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'Welcome back' - String get welcome_back => 'Welcome back'; - - /// en: 'Edit Mode Active' - String get edit_mode_active => 'Edit Mode Active'; - - /// en: 'Drag to reorder, toggle visibility' - String get drag_instruction => 'Drag to reorder, toggle visibility'; - - /// en: 'Reset' - String get reset => 'Reset'; - - /// en: 'Needed' - String get metric_needed => 'Needed'; - - /// en: 'Filled' - String get metric_filled => 'Filled'; - - /// en: 'Open' - String get metric_open => 'Open'; - - /// en: 'View all' - String get view_all => 'View all'; - - /// en: 'Save $amount/month' - String insight_lightbulb({required Object amount}) => 'Save ${amount}/month'; - - /// en: 'Book 48hrs ahead for better rates' - String get insight_tip => 'Book 48hrs ahead for better rates'; -} - -// Path: client_home.widgets -class TranslationsClientHomeWidgetsEn { - TranslationsClientHomeWidgetsEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'Quick Actions' - String get actions => 'Quick Actions'; - - /// en: 'Reorder' - String get reorder => 'Reorder'; - - /// en: 'Today's Coverage' - String get coverage => 'Today\'s Coverage'; - - /// en: 'Spending Insights' - String get spending => 'Spending Insights'; - - /// en: 'Live Activity' - String get live_activity => 'Live Activity'; -} - -// Path: client_home.actions -class TranslationsClientHomeActionsEn { - TranslationsClientHomeActionsEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'RAPID' - String get rapid => 'RAPID'; - - /// en: 'Urgent same-day' - String get rapid_subtitle => 'Urgent same-day'; - - /// en: 'Create Order' - String get create_order => 'Create Order'; - - /// en: 'Schedule shifts' - String get create_order_subtitle => 'Schedule shifts'; - - /// en: 'Hubs' - String get hubs => 'Hubs'; - - /// en: 'Clock-in points' - String get hubs_subtitle => 'Clock-in points'; -} - -// Path: client_home.reorder -class TranslationsClientHomeReorderEn { - TranslationsClientHomeReorderEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'REORDER' - String get title => 'REORDER'; - - /// en: 'Reorder' - String get reorder_button => 'Reorder'; - - /// en: '$amount/hr' - String per_hr({required Object amount}) => '${amount}/hr'; -} - -// Path: client_home.form -class TranslationsClientHomeFormEn { - TranslationsClientHomeFormEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'Edit & Reorder' - String get edit_reorder => 'Edit & Reorder'; - - /// en: 'Post a New Shift' - String get post_new => 'Post a New Shift'; - - /// en: 'Review and edit the details before posting' - String get review_subtitle => 'Review and edit the details before posting'; - - /// en: 'Date *' - String get date_label => 'Date *'; - - /// en: 'mm/dd/yyyy' - String get date_hint => 'mm/dd/yyyy'; - - /// en: 'Location *' - String get location_label => 'Location *'; - - /// en: 'Business address' - String get location_hint => 'Business address'; - - /// en: 'Positions' - String get positions_title => 'Positions'; - - /// en: 'Add Position' - String get add_position => 'Add Position'; - - /// en: 'Role *' - String get role_label => 'Role *'; - - /// en: 'Select role' - String get role_hint => 'Select role'; - - /// en: 'Start Time *' - String get start_time => 'Start Time *'; - - /// en: 'End Time *' - String get end_time => 'End Time *'; - - /// en: 'Workers Needed *' - String get workers_needed => 'Workers Needed *'; - - /// en: 'Hourly Rate (\$) *' - String get hourly_rate => 'Hourly Rate (\$) *'; - - /// en: 'Post Shift' - String get post_shift => 'Post Shift'; -} - -// Path: client_settings.profile -class TranslationsClientSettingsProfileEn { - TranslationsClientSettingsProfileEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'Profile' - String get title => 'Profile'; - - /// en: 'Edit Profile' - String get edit_profile => 'Edit Profile'; - - /// en: 'Hubs' - String get hubs => 'Hubs'; - - /// en: 'Log Out' - String get log_out => 'Log Out'; - - /// en: 'Quick Links' - String get quick_links => 'Quick Links'; - - /// en: 'Clock-In Hubs' - String get clock_in_hubs => 'Clock-In Hubs'; - - /// en: 'Billing & Payments' - String get billing_payments => 'Billing & Payments'; -} - -// Path: client_hubs.empty_state -class TranslationsClientHubsEmptyStateEn { - TranslationsClientHubsEmptyStateEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'No hubs yet' - String get title => 'No hubs yet'; - - /// en: 'Create clock-in stations for your locations' - String get description => 'Create clock-in stations for your locations'; - - /// en: 'Add Your First Hub' - String get button => 'Add Your First Hub'; -} - -// Path: client_hubs.about_hubs -class TranslationsClientHubsAboutHubsEn { - TranslationsClientHubsAboutHubsEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'About Hubs' - String get title => 'About Hubs'; - - /// en: 'Hubs are clock-in stations at your locations. Assign NFC tags to each hub so workers can quickly clock in/out using their phones.' - String get description => 'Hubs are clock-in stations at your locations. Assign NFC tags to each hub so workers can quickly clock in/out using their phones.'; -} - -// Path: client_hubs.hub_card -class TranslationsClientHubsHubCardEn { - TranslationsClientHubsHubCardEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'Tag: $id' - String tag_label({required Object id}) => 'Tag: ${id}'; -} - -// Path: client_hubs.add_hub_dialog -class TranslationsClientHubsAddHubDialogEn { - TranslationsClientHubsAddHubDialogEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'Add New Hub' - String get title => 'Add New Hub'; - - /// en: 'Hub Name *' - String get name_label => 'Hub Name *'; - - /// en: 'e.g., Main Kitchen, Front Desk' - String get name_hint => 'e.g., Main Kitchen, Front Desk'; - - /// en: 'Location Name' - String get location_label => 'Location Name'; - - /// en: 'e.g., Downtown Restaurant' - String get location_hint => 'e.g., Downtown Restaurant'; - - /// en: 'Address' - String get address_label => 'Address'; - - /// en: 'Full address' - String get address_hint => 'Full address'; - - /// en: 'Create Hub' - String get create_button => 'Create Hub'; -} - -// Path: client_hubs.nfc_dialog -class TranslationsClientHubsNfcDialogEn { - TranslationsClientHubsNfcDialogEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'Identify NFC Tag' - String get title => 'Identify NFC Tag'; - - /// en: 'Tap your phone to the NFC tag to identify it' - String get instruction => 'Tap your phone to the NFC tag to identify it'; - - /// en: 'Scan NFC Tag' - String get scan_button => 'Scan NFC Tag'; - - /// en: 'Tag Identified' - String get tag_identified => 'Tag Identified'; - - /// en: 'Assign Tag' - String get assign_button => 'Assign Tag'; -} - -// Path: client_create_order.types -class TranslationsClientCreateOrderTypesEn { - TranslationsClientCreateOrderTypesEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'RAPID' - String get rapid => 'RAPID'; - - /// en: 'URGENT same-day Coverage' - String get rapid_desc => 'URGENT same-day Coverage'; - - /// en: 'One-Time' - String get one_time => 'One-Time'; - - /// en: 'Single Event or Shift Request' - String get one_time_desc => 'Single Event or Shift Request'; - - /// en: 'Recurring' - String get recurring => 'Recurring'; - - /// en: 'Ongoing Weekly / Monthly Coverage' - String get recurring_desc => 'Ongoing Weekly / Monthly Coverage'; - - /// en: 'Permanent' - String get permanent => 'Permanent'; - - /// en: 'Long-Term Staffing Placement' - String get permanent_desc => 'Long-Term Staffing Placement'; -} - -// Path: client_create_order.rapid -class TranslationsClientCreateOrderRapidEn { - TranslationsClientCreateOrderRapidEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'RAPID Order' - String get title => 'RAPID Order'; - - /// en: 'Emergency staffing in minutes' - String get subtitle => 'Emergency staffing in minutes'; - - /// en: 'URGENT' - String get urgent_badge => 'URGENT'; - - /// en: 'Tell us what you need' - String get tell_us => 'Tell us what you need'; - - /// en: 'Need staff urgently?' - String get need_staff => 'Need staff urgently?'; - - /// en: 'Type or speak what you need. I'll handle the rest' - String get type_or_speak => 'Type or speak what you need. I\'ll handle the rest'; - - /// en: 'Example: ' - String get example => 'Example: '; - - /// en: 'Type or speak... (e.g., "Need 5 cooks ASAP until 5am")' - String get hint => 'Type or speak... (e.g., "Need 5 cooks ASAP until 5am")'; - - /// en: 'Speak' - String get speak => 'Speak'; - - /// en: 'Listening...' - String get listening => 'Listening...'; - - /// en: 'Send Message' - String get send => 'Send Message'; - - /// en: 'Sending...' - String get sending => 'Sending...'; - - /// en: 'Request Sent!' - String get success_title => 'Request Sent!'; - - /// en: 'We're finding available workers for you right now. You'll be notified as they accept.' - String get success_message => 'We\'re finding available workers for you right now. You\'ll be notified as they accept.'; - - /// en: 'Back to Orders' - String get back_to_orders => 'Back to Orders'; -} - -// Path: client_create_order.one_time -class TranslationsClientCreateOrderOneTimeEn { - TranslationsClientCreateOrderOneTimeEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'One-Time Order' - String get title => 'One-Time Order'; - - /// en: 'Single event or shift request' - String get subtitle => 'Single event or shift request'; - - /// en: 'Create Your Order' - String get create_your_order => 'Create Your Order'; - - /// en: 'Date' - String get date_label => 'Date'; - - /// en: 'Select date' - String get date_hint => 'Select date'; - - /// en: 'Location' - String get location_label => 'Location'; - - /// en: 'Enter address' - String get location_hint => 'Enter address'; - - /// en: 'Positions' - String get positions_title => 'Positions'; - - /// en: 'Add Position' - String get add_position => 'Add Position'; - - /// en: 'Position $number' - String position_number({required Object number}) => 'Position ${number}'; - - /// en: 'Remove' - String get remove => 'Remove'; - - /// en: 'Select role' - String get select_role => 'Select role'; - - /// en: 'Start' - String get start_label => 'Start'; - - /// en: 'End' - String get end_label => 'End'; - - /// en: 'Workers' - String get workers_label => 'Workers'; - - /// en: 'Lunch Break' - String get lunch_break_label => 'Lunch Break'; - - /// en: 'No break' - String get no_break => 'No break'; - - /// en: 'min (Paid)' - String get paid_break => 'min (Paid)'; - - /// en: 'min (Unpaid)' - String get unpaid_break => 'min (Unpaid)'; - - /// en: 'Use different location for this position' - String get different_location => 'Use different location for this position'; - - /// en: 'Different Location' - String get different_location_title => 'Different Location'; - - /// en: 'Enter different address' - String get different_location_hint => 'Enter different address'; - - /// en: 'Create Order' - String get create_order => 'Create Order'; - - /// en: 'Creating...' - String get creating => 'Creating...'; - - /// en: 'Order Created!' - String get success_title => 'Order Created!'; - - /// en: 'Your shift request has been posted. Workers will start applying soon.' - String get success_message => 'Your shift request has been posted. Workers will start applying soon.'; - - /// en: 'Back to Orders' - String get back_to_orders => 'Back to Orders'; -} - -// Path: client_create_order.recurring -class TranslationsClientCreateOrderRecurringEn { - TranslationsClientCreateOrderRecurringEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'Recurring Order' - String get title => 'Recurring Order'; - - /// en: 'Ongoing weekly/monthly coverage' - String get subtitle => 'Ongoing weekly/monthly coverage'; - - /// en: 'Recurring Order Flow (Work in Progress)' - String get placeholder => 'Recurring Order Flow (Work in Progress)'; -} - -// Path: client_create_order.permanent -class TranslationsClientCreateOrderPermanentEn { - TranslationsClientCreateOrderPermanentEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'Permanent Order' - String get title => 'Permanent Order'; - - /// en: 'Long-term staffing placement' - String get subtitle => 'Long-term staffing placement'; - - /// en: 'Permanent Order Flow (Work in Progress)' - String get placeholder => 'Permanent Order Flow (Work in Progress)'; -} - -// Path: client_main.tabs -class TranslationsClientMainTabsEn { - TranslationsClientMainTabsEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'Coverage' - String get coverage => 'Coverage'; - - /// en: 'Billing' - String get billing => 'Billing'; - - /// en: 'Home' - String get home => 'Home'; - - /// en: 'Orders' - String get orders => 'Orders'; - - /// en: 'Reports' - String get reports => 'Reports'; -} - -// Path: client_view_orders.tabs -class TranslationsClientViewOrdersTabsEn { - TranslationsClientViewOrdersTabsEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'Up Next' - String get up_next => 'Up Next'; - - /// en: 'Active' - String get active => 'Active'; - - /// en: 'Completed' - String get completed => 'Completed'; -} - -// Path: client_view_orders.card -class TranslationsClientViewOrdersCardEn { - TranslationsClientViewOrdersCardEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'OPEN' - String get open => 'OPEN'; - - /// en: 'FILLED' - String get filled => 'FILLED'; - - /// en: 'CONFIRMED' - String get confirmed => 'CONFIRMED'; - - /// en: 'IN PROGRESS' - String get in_progress => 'IN PROGRESS'; - - /// en: 'COMPLETED' - String get completed => 'COMPLETED'; - - /// en: 'CANCELLED' - String get cancelled => 'CANCELLED'; - - /// en: 'Get direction' - String get get_direction => 'Get direction'; - - /// en: 'Total' - String get total => 'Total'; - - /// en: 'HRS' - String get hrs => 'HRS'; - - /// en: '$count workers' - String workers({required Object count}) => '${count} workers'; - - /// en: 'CLOCK IN' - String get clock_in => 'CLOCK IN'; - - /// en: 'CLOCK OUT' - String get clock_out => 'CLOCK OUT'; - - /// en: 'Coverage' - String get coverage => 'Coverage'; - - /// en: '$filled/$needed Workers' - String workers_label({required Object filled, required Object needed}) => '${filled}/${needed} Workers'; - - /// en: 'Workers Confirmed' - String get confirmed_workers => 'Workers Confirmed'; - - /// en: 'No workers confirmed yet.' - String get no_workers => 'No workers confirmed yet.'; -} - -// Path: staff.main -class TranslationsStaffMainEn { - TranslationsStaffMainEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - late final TranslationsStaffMainTabsEn tabs = TranslationsStaffMainTabsEn._(_root); -} - -// Path: staff.home -class TranslationsStaffHomeEn { - TranslationsStaffHomeEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - late final TranslationsStaffHomeHeaderEn header = TranslationsStaffHomeHeaderEn._(_root); - late final TranslationsStaffHomeBannersEn banners = TranslationsStaffHomeBannersEn._(_root); - late final TranslationsStaffHomeQuickActionsEn quick_actions = TranslationsStaffHomeQuickActionsEn._(_root); - late final TranslationsStaffHomeSectionsEn sections = TranslationsStaffHomeSectionsEn._(_root); - late final TranslationsStaffHomeEmptyStatesEn empty_states = TranslationsStaffHomeEmptyStatesEn._(_root); - late final TranslationsStaffHomePendingPaymentEn pending_payment = TranslationsStaffHomePendingPaymentEn._(_root); - late final TranslationsStaffHomeRecommendedCardEn recommended_card = TranslationsStaffHomeRecommendedCardEn._(_root); - late final TranslationsStaffHomeBenefitsEn benefits = TranslationsStaffHomeBenefitsEn._(_root); - late final TranslationsStaffHomeAutoMatchEn auto_match = TranslationsStaffHomeAutoMatchEn._(_root); - late final TranslationsStaffHomeImproveEn improve = TranslationsStaffHomeImproveEn._(_root); - late final TranslationsStaffHomeMoreWaysEn more_ways = TranslationsStaffHomeMoreWaysEn._(_root); -} - -// Path: staff.profile -class TranslationsStaffProfileEn { - TranslationsStaffProfileEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - late final TranslationsStaffProfileHeaderEn header = TranslationsStaffProfileHeaderEn._(_root); - late final TranslationsStaffProfileReliabilityStatsEn reliability_stats = TranslationsStaffProfileReliabilityStatsEn._(_root); - late final TranslationsStaffProfileReliabilityScoreEn reliability_score = TranslationsStaffProfileReliabilityScoreEn._(_root); - late final TranslationsStaffProfileSectionsEn sections = TranslationsStaffProfileSectionsEn._(_root); - late final TranslationsStaffProfileMenuItemsEn menu_items = TranslationsStaffProfileMenuItemsEn._(_root); - late final TranslationsStaffProfileBankAccountPageEn bank_account_page = TranslationsStaffProfileBankAccountPageEn._(_root); - late final TranslationsStaffProfileLogoutEn logout = TranslationsStaffProfileLogoutEn._(_root); -} - -// Path: staff.onboarding -class TranslationsStaffOnboardingEn { - TranslationsStaffOnboardingEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - late final TranslationsStaffOnboardingPersonalInfoEn personal_info = TranslationsStaffOnboardingPersonalInfoEn._(_root); - late final TranslationsStaffOnboardingExperienceEn experience = TranslationsStaffOnboardingExperienceEn._(_root); -} - -// Path: staff_documents.verification_card -class TranslationsStaffDocumentsVerificationCardEn { - TranslationsStaffDocumentsVerificationCardEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'Document Verification' - String get title => 'Document Verification'; - - /// en: '$completed/$total Complete' - String progress({required Object completed, required Object total}) => '${completed}/${total} Complete'; -} - -// Path: staff_documents.list -class TranslationsStaffDocumentsListEn { - TranslationsStaffDocumentsListEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'No documents found' - String get empty => 'No documents found'; - - /// en: 'Error: $message' - String error({required Object message}) => 'Error: ${message}'; -} - -// Path: staff_documents.card -class TranslationsStaffDocumentsCardEn { - TranslationsStaffDocumentsCardEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'View' - String get view => 'View'; - - /// en: 'Upload' - String get upload => 'Upload'; - - /// en: 'Verified' - String get verified => 'Verified'; - - /// en: 'Pending' - String get pending => 'Pending'; - - /// en: 'Missing' - String get missing => 'Missing'; - - /// en: 'Rejected' - String get rejected => 'Rejected'; -} - -// Path: staff_certificates.progress -class TranslationsStaffCertificatesProgressEn { - TranslationsStaffCertificatesProgressEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'Your Progress' - String get title => 'Your Progress'; - - /// en: '$completed of $total verified' - String verified_count({required Object completed, required Object total}) => '${completed} of ${total} verified'; - - /// en: 'Compliance Active' - String get active => 'Compliance Active'; -} - -// Path: staff_certificates.card -class TranslationsStaffCertificatesCardEn { - TranslationsStaffCertificatesCardEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'Expires in $days days - Renew now' - String expires_in_days({required Object days}) => 'Expires in ${days} days - Renew now'; - - /// en: 'Expired - Renew now' - String get expired => 'Expired - Renew now'; - - /// en: 'Verified' - String get verified => 'Verified'; - - /// en: 'Expiring Soon' - String get expiring_soon => 'Expiring Soon'; - - /// en: 'Exp: $date' - String exp({required Object date}) => 'Exp: ${date}'; - - /// en: 'Upload Certificate' - String get upload_button => 'Upload Certificate'; - - /// en: 'Edit Expiration Date' - String get edit_expiry => 'Edit Expiration Date'; - - /// en: 'Remove Certificate' - String get remove => 'Remove Certificate'; - - /// en: 'Renew' - String get renew => 'Renew'; - - /// en: 'Certificate opened in new tab' - String get opened_snackbar => 'Certificate opened in new tab'; -} - -// Path: staff_certificates.add_more -class TranslationsStaffCertificatesAddMoreEn { - TranslationsStaffCertificatesAddMoreEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'Add Another Certificate' - String get title => 'Add Another Certificate'; - - /// en: 'Upload additional certifications' - String get subtitle => 'Upload additional certifications'; -} - -// Path: staff_certificates.upload_modal -class TranslationsStaffCertificatesUploadModalEn { - TranslationsStaffCertificatesUploadModalEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'Upload Certificate' - String get title => 'Upload Certificate'; - - /// en: 'Expiration Date (Optional)' - String get expiry_label => 'Expiration Date (Optional)'; - - /// en: 'Select date' - String get select_date => 'Select date'; - - /// en: 'Upload File' - String get upload_file => 'Upload File'; - - /// en: 'Drag and drop or click to upload' - String get drag_drop => 'Drag and drop or click to upload'; - - /// en: 'PDF, JPG, PNG up to 10MB' - String get supported_formats => 'PDF, JPG, PNG up to 10MB'; - - /// en: 'Cancel' - String get cancel => 'Cancel'; - - /// en: 'Save Certificate' - String get save => 'Save Certificate'; -} - -// Path: staff_certificates.delete_modal -class TranslationsStaffCertificatesDeleteModalEn { - TranslationsStaffCertificatesDeleteModalEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'Remove Certificate?' - String get title => 'Remove Certificate?'; - - /// en: 'This action cannot be undone.' - String get message => 'This action cannot be undone.'; - - /// en: 'Cancel' - String get cancel => 'Cancel'; - - /// en: 'Remove' - String get confirm => 'Remove'; -} - -// Path: staff_profile_attire.info_card -class TranslationsStaffProfileAttireInfoCardEn { - TranslationsStaffProfileAttireInfoCardEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'Your Wardrobe' - String get title => 'Your Wardrobe'; - - /// en: 'Select the attire items you own. This helps us match you with shifts that fit your wardrobe.' - String get description => 'Select the attire items you own. This helps us match you with shifts that fit your wardrobe.'; -} - -// Path: staff_profile_attire.status -class TranslationsStaffProfileAttireStatusEn { - TranslationsStaffProfileAttireStatusEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'REQUIRED' - String get required => 'REQUIRED'; - - /// en: 'Add Photo' - String get add_photo => 'Add Photo'; - - /// en: 'Added' - String get added => 'Added'; - - /// en: '⏳ Pending verification' - String get pending => '⏳ Pending verification'; -} - -// Path: staff_profile_attire.actions -class TranslationsStaffProfileAttireActionsEn { - TranslationsStaffProfileAttireActionsEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'Save Attire' - String get save => 'Save Attire'; -} - -// Path: staff_profile_attire.validation -class TranslationsStaffProfileAttireValidationEn { - TranslationsStaffProfileAttireValidationEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: '✓ Select all required items' - String get select_required => '✓ Select all required items'; - - /// en: '✓ Upload photos of required items' - String get upload_required => '✓ Upload photos of required items'; - - /// en: '✓ Accept attestation' - String get accept_attestation => '✓ Accept attestation'; -} - -// Path: staff_shifts.tabs -class TranslationsStaffShiftsTabsEn { - TranslationsStaffShiftsTabsEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'My Shifts' - String get my_shifts => 'My Shifts'; - - /// en: 'Find Work' - String get find_work => 'Find Work'; -} - -// Path: staff_shifts.list -class TranslationsStaffShiftsListEn { - TranslationsStaffShiftsListEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'No shifts found' - String get no_shifts => 'No shifts found'; - - /// en: 'PENDING OFFERS' - String get pending_offers => 'PENDING OFFERS'; - - /// en: '$count AVAILABLE JOBS' - String available_jobs({required Object count}) => '${count} AVAILABLE JOBS'; - - /// en: 'Search jobs...' - String get search_hint => 'Search jobs...'; -} - -// Path: staff_shifts.filter -class TranslationsStaffShiftsFilterEn { - TranslationsStaffShiftsFilterEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'All Jobs' - String get all => 'All Jobs'; - - /// en: 'One Day' - String get one_day => 'One Day'; - - /// en: 'Multi Day' - String get multi_day => 'Multi Day'; - - /// en: 'Long Term' - String get long_term => 'Long Term'; -} - -// Path: staff_shifts.status -class TranslationsStaffShiftsStatusEn { - TranslationsStaffShiftsStatusEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'CONFIRMED' - String get confirmed => 'CONFIRMED'; - - /// en: 'ACT NOW' - String get act_now => 'ACT NOW'; - - /// en: 'SWAP REQUESTED' - String get swap_requested => 'SWAP REQUESTED'; - - /// en: 'COMPLETED' - String get completed => 'COMPLETED'; - - /// en: 'NO SHOW' - String get no_show => 'NO SHOW'; - - /// en: 'Please confirm assignment' - String get pending_warning => 'Please confirm assignment'; -} - -// Path: staff_shifts.action -class TranslationsStaffShiftsActionEn { - TranslationsStaffShiftsActionEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'Decline' - String get decline => 'Decline'; - - /// en: 'Confirm' - String get confirm => 'Confirm'; - - /// en: 'Request Swap' - String get request_swap => 'Request Swap'; -} - -// Path: staff_shifts.details -class TranslationsStaffShiftsDetailsEn { - TranslationsStaffShiftsDetailsEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'ADDITIONAL DETAILS' - String get additional => 'ADDITIONAL DETAILS'; - - /// en: '$days Days' - String days({required Object days}) => '${days} Days'; - - /// en: '(exp.total \$$amount)' - String exp_total({required Object amount}) => '(exp.total \$${amount})'; - - /// en: 'Pending $time ago' - String pending_time({required Object time}) => 'Pending ${time} ago'; -} - -// Path: staff_shifts.tags -class TranslationsStaffShiftsTagsEn { - TranslationsStaffShiftsTagsEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'Immediate start' - String get immediate_start => 'Immediate start'; - - /// en: 'No experience' - String get no_experience => 'No experience'; -} - -// Path: staff_time_card.status -class TranslationsStaffTimeCardStatusEn { - TranslationsStaffTimeCardStatusEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'Approved' - String get approved => 'Approved'; - - /// en: 'Disputed' - String get disputed => 'Disputed'; - - /// en: 'Paid' - String get paid => 'Paid'; - - /// en: 'Pending' - String get pending => 'Pending'; -} - -// Path: staff_authentication.profile_setup_page.steps -class TranslationsStaffAuthenticationProfileSetupPageStepsEn { - TranslationsStaffAuthenticationProfileSetupPageStepsEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'Basic Info' - String get basic => 'Basic Info'; - - /// en: 'Location' - String get location => 'Location'; - - /// en: 'Experience' - String get experience => 'Experience'; -} - -// Path: staff_authentication.profile_setup_page.basic_info -class TranslationsStaffAuthenticationProfileSetupPageBasicInfoEn { - TranslationsStaffAuthenticationProfileSetupPageBasicInfoEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'Let's get to know you' - String get title => 'Let\'s get to know you'; - - /// en: 'Tell us a bit about yourself' - String get subtitle => 'Tell us a bit about yourself'; - - /// en: 'Full Name *' - String get full_name_label => 'Full Name *'; - - /// en: 'John Smith' - String get full_name_hint => 'John Smith'; - - /// en: 'Short Bio' - String get bio_label => 'Short Bio'; - - /// en: 'Experienced hospitality professional...' - String get bio_hint => 'Experienced hospitality professional...'; -} - -// Path: staff_authentication.profile_setup_page.location -class TranslationsStaffAuthenticationProfileSetupPageLocationEn { - TranslationsStaffAuthenticationProfileSetupPageLocationEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'Where do you want to work?' - String get title => 'Where do you want to work?'; - - /// en: 'Add your preferred work locations' - String get subtitle => 'Add your preferred work locations'; - - /// en: 'Full Name' - String get full_name_label => 'Full Name'; - - /// en: 'Add Location *' - String get add_location_label => 'Add Location *'; - - /// en: 'City or ZIP code' - String get add_location_hint => 'City or ZIP code'; - - /// en: 'Add' - String get add_button => 'Add'; - - /// en: 'Max Distance: $distance miles' - String max_distance({required Object distance}) => 'Max Distance: ${distance} miles'; - - /// en: '5 mi' - String get min_dist_label => '5 mi'; - - /// en: '50 mi' - String get max_dist_label => '50 mi'; -} - -// Path: staff_authentication.profile_setup_page.experience -class TranslationsStaffAuthenticationProfileSetupPageExperienceEn { - TranslationsStaffAuthenticationProfileSetupPageExperienceEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'What are your skills?' - String get title => 'What are your skills?'; - - /// en: 'Select all that apply' - String get subtitle => 'Select all that apply'; - - /// en: 'Skills *' - String get skills_label => 'Skills *'; - - /// en: 'Preferred Industries' - String get industries_label => 'Preferred Industries'; - - late final TranslationsStaffAuthenticationProfileSetupPageExperienceSkillsEn skills = TranslationsStaffAuthenticationProfileSetupPageExperienceSkillsEn._(_root); - late final TranslationsStaffAuthenticationProfileSetupPageExperienceIndustriesEn industries = TranslationsStaffAuthenticationProfileSetupPageExperienceIndustriesEn._(_root); -} - -// Path: staff.main.tabs -class TranslationsStaffMainTabsEn { - TranslationsStaffMainTabsEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'Shifts' - String get shifts => 'Shifts'; - - /// en: 'Payments' - String get payments => 'Payments'; - - /// en: 'Home' - String get home => 'Home'; - - /// en: 'Clock In' - String get clock_in => 'Clock In'; - - /// en: 'Profile' - String get profile => 'Profile'; -} - -// Path: staff.home.header -class TranslationsStaffHomeHeaderEn { - TranslationsStaffHomeHeaderEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'Welcome back' - String get welcome_back => 'Welcome back'; - - /// en: 'Krower' - String get user_name_placeholder => 'Krower'; -} - -// Path: staff.home.banners -class TranslationsStaffHomeBannersEn { - TranslationsStaffHomeBannersEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'Complete Your Profile' - String get complete_profile_title => 'Complete Your Profile'; - - /// en: 'Get verified to see more shifts' - String get complete_profile_subtitle => 'Get verified to see more shifts'; - - /// en: 'Availability' - String get availability_title => 'Availability'; - - /// en: 'Update your availability for next week' - String get availability_subtitle => 'Update your availability for next week'; -} - -// Path: staff.home.quick_actions -class TranslationsStaffHomeQuickActionsEn { - TranslationsStaffHomeQuickActionsEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'Find Shifts' - String get find_shifts => 'Find Shifts'; - - /// en: 'Availability' - String get availability => 'Availability'; - - /// en: 'Messages' - String get messages => 'Messages'; - - /// en: 'Earnings' - String get earnings => 'Earnings'; -} - -// Path: staff.home.sections -class TranslationsStaffHomeSectionsEn { - TranslationsStaffHomeSectionsEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'Today's Shift' - String get todays_shift => 'Today\'s Shift'; - - /// en: '$count scheduled' - String scheduled_count({required Object count}) => '${count} scheduled'; - - /// en: 'Tomorrow' - String get tomorrow => 'Tomorrow'; - - /// en: 'Recommended for You' - String get recommended_for_you => 'Recommended for You'; - - /// en: 'View all' - String get view_all => 'View all'; -} - -// Path: staff.home.empty_states -class TranslationsStaffHomeEmptyStatesEn { - TranslationsStaffHomeEmptyStatesEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'No shifts scheduled for today' - String get no_shifts_today => 'No shifts scheduled for today'; - - /// en: 'Find shifts →' - String get find_shifts_cta => 'Find shifts →'; - - /// en: 'No shifts for tomorrow' - String get no_shifts_tomorrow => 'No shifts for tomorrow'; - - /// en: 'No recommended shifts' - String get no_recommended_shifts => 'No recommended shifts'; -} - -// Path: staff.home.pending_payment -class TranslationsStaffHomePendingPaymentEn { - TranslationsStaffHomePendingPaymentEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'Pending Payment' - String get title => 'Pending Payment'; - - /// en: 'Payment processing' - String get subtitle => 'Payment processing'; - - /// en: '$amount' - String amount({required Object amount}) => '${amount}'; -} - -// Path: staff.home.recommended_card -class TranslationsStaffHomeRecommendedCardEn { - TranslationsStaffHomeRecommendedCardEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: '• ACT NOW' - String get act_now => '• ACT NOW'; - - /// en: 'One Day' - String get one_day => 'One Day'; - - /// en: 'Today' - String get today => 'Today'; - - /// en: 'Applied for $title' - String applied_for({required Object title}) => 'Applied for ${title}'; - - /// en: '$start - $end' - String time_range({required Object start, required Object end}) => '${start} - ${end}'; -} - -// Path: staff.home.benefits -class TranslationsStaffHomeBenefitsEn { - TranslationsStaffHomeBenefitsEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'Your Benefits' - String get title => 'Your Benefits'; - - /// en: 'View all' - String get view_all => 'View all'; - - /// en: 'hours' - String get hours_label => 'hours'; - - late final TranslationsStaffHomeBenefitsItemsEn items = TranslationsStaffHomeBenefitsItemsEn._(_root); -} - -// Path: staff.home.auto_match -class TranslationsStaffHomeAutoMatchEn { - TranslationsStaffHomeAutoMatchEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'Auto-Match' - String get title => 'Auto-Match'; - - /// en: 'Finding shifts for you' - String get finding_shifts => 'Finding shifts for you'; - - /// en: 'Get matched automatically' - String get get_matched => 'Get matched automatically'; - - /// en: 'Matching based on:' - String get matching_based_on => 'Matching based on:'; - - late final TranslationsStaffHomeAutoMatchChipsEn chips = TranslationsStaffHomeAutoMatchChipsEn._(_root); -} - -// Path: staff.home.improve -class TranslationsStaffHomeImproveEn { - TranslationsStaffHomeImproveEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'Improve Yourself' - String get title => 'Improve Yourself'; - - late final TranslationsStaffHomeImproveItemsEn items = TranslationsStaffHomeImproveItemsEn._(_root); -} - -// Path: staff.home.more_ways -class TranslationsStaffHomeMoreWaysEn { - TranslationsStaffHomeMoreWaysEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'More Ways To Use Krow' - String get title => 'More Ways To Use Krow'; - - late final TranslationsStaffHomeMoreWaysItemsEn items = TranslationsStaffHomeMoreWaysItemsEn._(_root); -} - -// Path: staff.profile.header -class TranslationsStaffProfileHeaderEn { - TranslationsStaffProfileHeaderEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'Profile' - String get title => 'Profile'; - - /// en: 'SIGN OUT' - String get sign_out => 'SIGN OUT'; -} - -// Path: staff.profile.reliability_stats -class TranslationsStaffProfileReliabilityStatsEn { - TranslationsStaffProfileReliabilityStatsEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'Shifts' - String get shifts => 'Shifts'; - - /// en: 'Rating' - String get rating => 'Rating'; - - /// en: 'On Time' - String get on_time => 'On Time'; - - /// en: 'No Shows' - String get no_shows => 'No Shows'; - - /// en: 'Cancel.' - String get cancellations => 'Cancel.'; -} - -// Path: staff.profile.reliability_score -class TranslationsStaffProfileReliabilityScoreEn { - TranslationsStaffProfileReliabilityScoreEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'Reliability Score' - String get title => 'Reliability Score'; - - /// en: 'Keep your score above 45% to continue picking up shifts.' - String get description => 'Keep your score above 45% to continue picking up shifts.'; -} - -// Path: staff.profile.sections -class TranslationsStaffProfileSectionsEn { - TranslationsStaffProfileSectionsEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'ONBOARDING' - String get onboarding => 'ONBOARDING'; - - /// en: 'COMPLIANCE' - String get compliance => 'COMPLIANCE'; - - /// en: 'LEVEL UP' - String get level_up => 'LEVEL UP'; - - /// en: 'FINANCE' - String get finance => 'FINANCE'; - - /// en: 'SUPPORT' - String get support => 'SUPPORT'; -} - -// Path: staff.profile.menu_items -class TranslationsStaffProfileMenuItemsEn { - TranslationsStaffProfileMenuItemsEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'Personal Info' - String get personal_info => 'Personal Info'; - - /// en: 'Emergency Contact' - String get emergency_contact => 'Emergency Contact'; - - /// en: 'Experience' - String get experience => 'Experience'; - - /// en: 'Attire' - String get attire => 'Attire'; - - /// en: 'Documents' - String get documents => 'Documents'; - - /// en: 'Certificates' - String get certificates => 'Certificates'; - - /// en: 'Tax Forms' - String get tax_forms => 'Tax Forms'; - - /// en: 'Krow University' - String get krow_university => 'Krow University'; - - /// en: 'Trainings' - String get trainings => 'Trainings'; - - /// en: 'Leaderboard' - String get leaderboard => 'Leaderboard'; - - /// en: 'Bank Account' - String get bank_account => 'Bank Account'; - - /// en: 'Payments' - String get payments => 'Payments'; - - /// en: 'Timecard' - String get timecard => 'Timecard'; - - /// en: 'FAQs' - String get faqs => 'FAQs'; - - /// en: 'Privacy & Security' - String get privacy_security => 'Privacy & Security'; - - /// en: 'Messages' - String get messages => 'Messages'; -} - -// Path: staff.profile.bank_account_page -class TranslationsStaffProfileBankAccountPageEn { - TranslationsStaffProfileBankAccountPageEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'Bank Account' - String get title => 'Bank Account'; - - /// en: 'LINKED ACCOUNTS' - String get linked_accounts => 'LINKED ACCOUNTS'; - - /// en: 'Add New Account' - String get add_account => 'Add New Account'; - - /// en: '100% Secured' - String get secure_title => '100% Secured'; - - /// en: 'Your account details are encrypted and safe.' - String get secure_subtitle => 'Your account details are encrypted and safe.'; - - /// en: 'Primary' - String get primary => 'Primary'; - - /// en: 'Add New Account' - String get add_new_account => 'Add New Account'; - - /// en: 'Routing Number' - String get routing_number => 'Routing Number'; - - /// en: 'Enter routing number' - String get routing_hint => 'Enter routing number'; - - /// en: 'Account Number' - String get account_number => 'Account Number'; - - /// en: 'Enter account number' - String get account_hint => 'Enter account number'; - - /// en: 'Account Type' - String get account_type => 'Account Type'; - - /// en: 'Checking' - String get checking => 'Checking'; - - /// en: 'Savings' - String get savings => 'Savings'; - - /// en: 'Cancel' - String get cancel => 'Cancel'; - - /// en: 'Save' - String get save => 'Save'; - - /// en: 'Ending in $last4' - String account_ending({required Object last4}) => 'Ending in ${last4}'; -} - -// Path: staff.profile.logout -class TranslationsStaffProfileLogoutEn { - TranslationsStaffProfileLogoutEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'Sign Out' - String get button => 'Sign Out'; -} - -// Path: staff.onboarding.personal_info -class TranslationsStaffOnboardingPersonalInfoEn { - TranslationsStaffOnboardingPersonalInfoEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'Personal Info' - String get title => 'Personal Info'; - - /// en: 'Tap to change photo' - String get change_photo_hint => 'Tap to change photo'; - - /// en: 'Full Name' - String get full_name_label => 'Full Name'; - - /// en: 'Email' - String get email_label => 'Email'; - - /// en: 'Phone Number' - String get phone_label => 'Phone Number'; - - /// en: '+1 (555) 000-0000' - String get phone_hint => '+1 (555) 000-0000'; - - /// en: 'Bio' - String get bio_label => 'Bio'; - - /// en: 'Tell clients about yourself...' - String get bio_hint => 'Tell clients about yourself...'; - - /// en: 'Languages' - String get languages_label => 'Languages'; - - /// en: 'English, Spanish, French...' - String get languages_hint => 'English, Spanish, French...'; - - /// en: 'Preferred Locations' - String get locations_label => 'Preferred Locations'; - - /// en: 'Downtown, Midtown, Brooklyn...' - String get locations_hint => 'Downtown, Midtown, Brooklyn...'; - - /// en: 'Save Changes' - String get save_button => 'Save Changes'; - - /// en: 'Personal info saved successfully' - String get save_success => 'Personal info saved successfully'; -} - -// Path: staff.onboarding.experience -class TranslationsStaffOnboardingExperienceEn { - TranslationsStaffOnboardingExperienceEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'Experience & Skills' - String get title => 'Experience & Skills'; - - /// en: 'Industries' - String get industries_title => 'Industries'; - - /// en: 'Select the industries you have experience in' - String get industries_subtitle => 'Select the industries you have experience in'; - - /// en: 'Skills' - String get skills_title => 'Skills'; - - /// en: 'Select your skills or add custom ones' - String get skills_subtitle => 'Select your skills or add custom ones'; - - /// en: 'Custom Skills:' - String get custom_skills_title => 'Custom Skills:'; - - /// en: 'Add custom skill...' - String get custom_skill_hint => 'Add custom skill...'; - - /// en: 'Save & Continue' - String get save_button => 'Save & Continue'; - - late final TranslationsStaffOnboardingExperienceIndustriesEn industries = TranslationsStaffOnboardingExperienceIndustriesEn._(_root); - late final TranslationsStaffOnboardingExperienceSkillsEn skills = TranslationsStaffOnboardingExperienceSkillsEn._(_root); -} - -// Path: staff_authentication.profile_setup_page.experience.skills -class TranslationsStaffAuthenticationProfileSetupPageExperienceSkillsEn { - TranslationsStaffAuthenticationProfileSetupPageExperienceSkillsEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'Food Service' - String get food_service => 'Food Service'; - - /// en: 'Bartending' - String get bartending => 'Bartending'; - - /// en: 'Warehouse' - String get warehouse => 'Warehouse'; - - /// en: 'Retail' - String get retail => 'Retail'; - - /// en: 'Events' - String get events => 'Events'; - - /// en: 'Customer Service' - String get customer_service => 'Customer Service'; - - /// en: 'Cleaning' - String get cleaning => 'Cleaning'; - - /// en: 'Security' - String get security => 'Security'; - - /// en: 'Driving' - String get driving => 'Driving'; - - /// en: 'Cooking' - String get cooking => 'Cooking'; - - /// en: 'Cashier' - String get cashier => 'Cashier'; - - /// en: 'Server' - String get server => 'Server'; - - /// en: 'Barista' - String get barista => 'Barista'; - - /// en: 'Host/Hostess' - String get host_hostess => 'Host/Hostess'; - - /// en: 'Busser' - String get busser => 'Busser'; -} - -// Path: staff_authentication.profile_setup_page.experience.industries -class TranslationsStaffAuthenticationProfileSetupPageExperienceIndustriesEn { - TranslationsStaffAuthenticationProfileSetupPageExperienceIndustriesEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'Hospitality' - String get hospitality => 'Hospitality'; - - /// en: 'Food Service' - String get food_service => 'Food Service'; - - /// en: 'Warehouse' - String get warehouse => 'Warehouse'; - - /// en: 'Events' - String get events => 'Events'; - - /// en: 'Retail' - String get retail => 'Retail'; - - /// en: 'Healthcare' - String get healthcare => 'Healthcare'; -} - -// Path: staff.home.benefits.items -class TranslationsStaffHomeBenefitsItemsEn { - TranslationsStaffHomeBenefitsItemsEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'Sick Days' - String get sick_days => 'Sick Days'; - - /// en: 'Vacation' - String get vacation => 'Vacation'; - - /// en: 'Holidays' - String get holidays => 'Holidays'; -} - -// Path: staff.home.auto_match.chips -class TranslationsStaffHomeAutoMatchChipsEn { - TranslationsStaffHomeAutoMatchChipsEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'Location' - String get location => 'Location'; - - /// en: 'Availability' - String get availability => 'Availability'; - - /// en: 'Skills' - String get skills => 'Skills'; -} - -// Path: staff.home.improve.items -class TranslationsStaffHomeImproveItemsEn { - TranslationsStaffHomeImproveItemsEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - late final TranslationsStaffHomeImproveItemsTrainingEn training = TranslationsStaffHomeImproveItemsTrainingEn._(_root); - late final TranslationsStaffHomeImproveItemsPodcastEn podcast = TranslationsStaffHomeImproveItemsPodcastEn._(_root); -} - -// Path: staff.home.more_ways.items -class TranslationsStaffHomeMoreWaysItemsEn { - TranslationsStaffHomeMoreWaysItemsEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - late final TranslationsStaffHomeMoreWaysItemsBenefitsEn benefits = TranslationsStaffHomeMoreWaysItemsBenefitsEn._(_root); - late final TranslationsStaffHomeMoreWaysItemsReferEn refer = TranslationsStaffHomeMoreWaysItemsReferEn._(_root); -} - -// Path: staff.onboarding.experience.industries -class TranslationsStaffOnboardingExperienceIndustriesEn { - TranslationsStaffOnboardingExperienceIndustriesEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'Hospitality' - String get hospitality => 'Hospitality'; - - /// en: 'Food Service' - String get food_service => 'Food Service'; - - /// en: 'Warehouse' - String get warehouse => 'Warehouse'; - - /// en: 'Events' - String get events => 'Events'; - - /// en: 'Retail' - String get retail => 'Retail'; - - /// en: 'Healthcare' - String get healthcare => 'Healthcare'; - - /// en: 'Other' - String get other => 'Other'; -} - -// Path: staff.onboarding.experience.skills -class TranslationsStaffOnboardingExperienceSkillsEn { - TranslationsStaffOnboardingExperienceSkillsEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'Food Service' - String get food_service => 'Food Service'; - - /// en: 'Bartending' - String get bartending => 'Bartending'; - - /// en: 'Event Setup' - String get event_setup => 'Event Setup'; - - /// en: 'Hospitality' - String get hospitality => 'Hospitality'; - - /// en: 'Warehouse' - String get warehouse => 'Warehouse'; - - /// en: 'Customer Service' - String get customer_service => 'Customer Service'; - - /// en: 'Cleaning' - String get cleaning => 'Cleaning'; - - /// en: 'Security' - String get security => 'Security'; - - /// en: 'Retail' - String get retail => 'Retail'; - - /// en: 'Cooking' - String get cooking => 'Cooking'; - - /// en: 'Cashier' - String get cashier => 'Cashier'; - - /// en: 'Server' - String get server => 'Server'; - - /// en: 'Barista' - String get barista => 'Barista'; - - /// en: 'Host/Hostess' - String get host_hostess => 'Host/Hostess'; - - /// en: 'Busser' - String get busser => 'Busser'; - - /// en: 'Driving' - String get driving => 'Driving'; -} - -// Path: staff.home.improve.items.training -class TranslationsStaffHomeImproveItemsTrainingEn { - TranslationsStaffHomeImproveItemsTrainingEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'Training Section' - String get title => 'Training Section'; - - /// en: 'Improve your skills and get certified.' - String get description => 'Improve your skills and get certified.'; - - /// en: '/krow-university' - String get page => '/krow-university'; -} - -// Path: staff.home.improve.items.podcast -class TranslationsStaffHomeImproveItemsPodcastEn { - TranslationsStaffHomeImproveItemsPodcastEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'Krow Podcast' - String get title => 'Krow Podcast'; - - /// en: 'Listen to tips from top workers.' - String get description => 'Listen to tips from top workers.'; - - /// en: '/krow-university' - String get page => '/krow-university'; -} - -// Path: staff.home.more_ways.items.benefits -class TranslationsStaffHomeMoreWaysItemsBenefitsEn { - TranslationsStaffHomeMoreWaysItemsBenefitsEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'Krow Benefits' - String get title => 'Krow Benefits'; - - /// en: '/benefits' - String get page => '/benefits'; -} - -// Path: staff.home.more_ways.items.refer -class TranslationsStaffHomeMoreWaysItemsReferEn { - TranslationsStaffHomeMoreWaysItemsReferEn._(this._root); - - final Translations _root; // ignore: unused_field - - // Translations - - /// en: 'Refer a Friend' - String get title => 'Refer a Friend'; - - /// en: '/worker-profile' - String get page => '/worker-profile'; -} - -/// The flat map containing all translations for locale . -/// Only for edge cases! For simple maps, use the map function of this library. -/// -/// The Dart AOT compiler has issues with very large switch statements, -/// so the map is split into smaller functions (512 entries each). -extension on Translations { - dynamic _flatMapFunction(String path) { - return switch (path) { - 'common.ok' => 'OK', - 'common.cancel' => 'Cancel', - 'common.save' => 'Save', - 'common.delete' => 'Delete', - 'common.continue_text' => 'Continue', - 'settings.language' => 'Language', - 'settings.change_language' => 'Change Language', - 'staff_authentication.get_started_page.title_part1' => 'Work, Grow, ', - 'staff_authentication.get_started_page.title_part2' => 'Elevate', - 'staff_authentication.get_started_page.subtitle' => 'Build your career in hospitality with \nflexibility and freedom.', - 'staff_authentication.get_started_page.sign_up_button' => 'Sign Up', - 'staff_authentication.get_started_page.log_in_button' => 'Log In', - 'staff_authentication.phone_verification_page.validation_error' => 'Please enter a valid 10-digit phone number', - 'staff_authentication.phone_verification_page.send_code_button' => 'Send Code', - 'staff_authentication.phone_verification_page.enter_code_title' => 'Enter verification code', - 'staff_authentication.phone_verification_page.code_sent_message' => 'We sent a 6-digit code to ', - 'staff_authentication.phone_verification_page.code_sent_instruction' => '. Enter it below to verify your account.', - 'staff_authentication.phone_input.title' => 'Verify your phone number', - 'staff_authentication.phone_input.subtitle' => 'We\'ll send you a verification code to get started.', - 'staff_authentication.phone_input.label' => 'Phone Number', - 'staff_authentication.phone_input.hint' => 'Enter your number', - 'staff_authentication.otp_verification.did_not_get_code' => 'Didn\'t get the code ?', - 'staff_authentication.otp_verification.resend_in' => ({required Object seconds}) => 'Resend in ${seconds} s', - 'staff_authentication.otp_verification.resend_code' => 'Resend code', - 'staff_authentication.profile_setup_page.step_indicator' => ({required Object current, required Object total}) => 'Step ${current} of ${total}', - 'staff_authentication.profile_setup_page.error_occurred' => 'An error occurred', - 'staff_authentication.profile_setup_page.complete_setup_button' => 'Complete Setup', - 'staff_authentication.profile_setup_page.steps.basic' => 'Basic Info', - 'staff_authentication.profile_setup_page.steps.location' => 'Location', - 'staff_authentication.profile_setup_page.steps.experience' => 'Experience', - 'staff_authentication.profile_setup_page.basic_info.title' => 'Let\'s get to know you', - 'staff_authentication.profile_setup_page.basic_info.subtitle' => 'Tell us a bit about yourself', - 'staff_authentication.profile_setup_page.basic_info.full_name_label' => 'Full Name *', - 'staff_authentication.profile_setup_page.basic_info.full_name_hint' => 'John Smith', - 'staff_authentication.profile_setup_page.basic_info.bio_label' => 'Short Bio', - 'staff_authentication.profile_setup_page.basic_info.bio_hint' => 'Experienced hospitality professional...', - 'staff_authentication.profile_setup_page.location.title' => 'Where do you want to work?', - 'staff_authentication.profile_setup_page.location.subtitle' => 'Add your preferred work locations', - 'staff_authentication.profile_setup_page.location.full_name_label' => 'Full Name', - 'staff_authentication.profile_setup_page.location.add_location_label' => 'Add Location *', - 'staff_authentication.profile_setup_page.location.add_location_hint' => 'City or ZIP code', - 'staff_authentication.profile_setup_page.location.add_button' => 'Add', - 'staff_authentication.profile_setup_page.location.max_distance' => ({required Object distance}) => 'Max Distance: ${distance} miles', - 'staff_authentication.profile_setup_page.location.min_dist_label' => '5 mi', - 'staff_authentication.profile_setup_page.location.max_dist_label' => '50 mi', - 'staff_authentication.profile_setup_page.experience.title' => 'What are your skills?', - 'staff_authentication.profile_setup_page.experience.subtitle' => 'Select all that apply', - 'staff_authentication.profile_setup_page.experience.skills_label' => 'Skills *', - 'staff_authentication.profile_setup_page.experience.industries_label' => 'Preferred Industries', - 'staff_authentication.profile_setup_page.experience.skills.food_service' => 'Food Service', - 'staff_authentication.profile_setup_page.experience.skills.bartending' => 'Bartending', - 'staff_authentication.profile_setup_page.experience.skills.warehouse' => 'Warehouse', - 'staff_authentication.profile_setup_page.experience.skills.retail' => 'Retail', - 'staff_authentication.profile_setup_page.experience.skills.events' => 'Events', - 'staff_authentication.profile_setup_page.experience.skills.customer_service' => 'Customer Service', - 'staff_authentication.profile_setup_page.experience.skills.cleaning' => 'Cleaning', - 'staff_authentication.profile_setup_page.experience.skills.security' => 'Security', - 'staff_authentication.profile_setup_page.experience.skills.driving' => 'Driving', - 'staff_authentication.profile_setup_page.experience.skills.cooking' => 'Cooking', - 'staff_authentication.profile_setup_page.experience.skills.cashier' => 'Cashier', - 'staff_authentication.profile_setup_page.experience.skills.server' => 'Server', - 'staff_authentication.profile_setup_page.experience.skills.barista' => 'Barista', - 'staff_authentication.profile_setup_page.experience.skills.host_hostess' => 'Host/Hostess', - 'staff_authentication.profile_setup_page.experience.skills.busser' => 'Busser', - 'staff_authentication.profile_setup_page.experience.industries.hospitality' => 'Hospitality', - 'staff_authentication.profile_setup_page.experience.industries.food_service' => 'Food Service', - 'staff_authentication.profile_setup_page.experience.industries.warehouse' => 'Warehouse', - 'staff_authentication.profile_setup_page.experience.industries.events' => 'Events', - 'staff_authentication.profile_setup_page.experience.industries.retail' => 'Retail', - 'staff_authentication.profile_setup_page.experience.industries.healthcare' => 'Healthcare', - 'staff_authentication.common.trouble_question' => 'Having trouble? ', - 'staff_authentication.common.contact_support' => 'Contact Support', - 'client_authentication.get_started_page.title' => 'Take Control of Your\nShifts and Events', - 'client_authentication.get_started_page.subtitle' => 'Streamline your operations with powerful tools to manage schedules, track performance, and keep your team on the same page—all in one place', - 'client_authentication.get_started_page.sign_in_button' => 'Sign In', - 'client_authentication.get_started_page.create_account_button' => 'Create Account', - 'client_authentication.sign_in_page.title' => 'Welcome Back', - 'client_authentication.sign_in_page.subtitle' => 'Sign in to manage your shifts and workers', - 'client_authentication.sign_in_page.email_label' => 'Email', - 'client_authentication.sign_in_page.email_hint' => 'Enter your email', - 'client_authentication.sign_in_page.password_label' => 'Password', - 'client_authentication.sign_in_page.password_hint' => 'Enter your password', - 'client_authentication.sign_in_page.forgot_password' => 'Forgot Password?', - 'client_authentication.sign_in_page.sign_in_button' => 'Sign In', - 'client_authentication.sign_in_page.or_divider' => 'or', - 'client_authentication.sign_in_page.social_apple' => 'Sign In with Apple', - 'client_authentication.sign_in_page.social_google' => 'Sign In with Google', - 'client_authentication.sign_in_page.no_account' => 'Don\'t have an account? ', - 'client_authentication.sign_in_page.sign_up_link' => 'Sign Up', - 'client_authentication.sign_up_page.title' => 'Create Account', - 'client_authentication.sign_up_page.subtitle' => 'Get started with Krow for your business', - 'client_authentication.sign_up_page.company_label' => 'Company Name', - 'client_authentication.sign_up_page.company_hint' => 'Enter company name', - 'client_authentication.sign_up_page.email_label' => 'Email', - 'client_authentication.sign_up_page.email_hint' => 'Enter your email', - 'client_authentication.sign_up_page.password_label' => 'Password', - 'client_authentication.sign_up_page.password_hint' => 'Create a password', - 'client_authentication.sign_up_page.confirm_password_label' => 'Confirm Password', - 'client_authentication.sign_up_page.confirm_password_hint' => 'Confirm your password', - 'client_authentication.sign_up_page.create_account_button' => 'Create Account', - 'client_authentication.sign_up_page.or_divider' => 'or', - 'client_authentication.sign_up_page.social_apple' => 'Sign Up with Apple', - 'client_authentication.sign_up_page.social_google' => 'Sign Up with Google', - 'client_authentication.sign_up_page.has_account' => 'Already have an account? ', - 'client_authentication.sign_up_page.sign_in_link' => 'Sign In', - 'client_home.dashboard.welcome_back' => 'Welcome back', - 'client_home.dashboard.edit_mode_active' => 'Edit Mode Active', - 'client_home.dashboard.drag_instruction' => 'Drag to reorder, toggle visibility', - 'client_home.dashboard.reset' => 'Reset', - 'client_home.dashboard.metric_needed' => 'Needed', - 'client_home.dashboard.metric_filled' => 'Filled', - 'client_home.dashboard.metric_open' => 'Open', - 'client_home.dashboard.view_all' => 'View all', - 'client_home.dashboard.insight_lightbulb' => ({required Object amount}) => 'Save ${amount}/month', - 'client_home.dashboard.insight_tip' => 'Book 48hrs ahead for better rates', - 'client_home.widgets.actions' => 'Quick Actions', - 'client_home.widgets.reorder' => 'Reorder', - 'client_home.widgets.coverage' => 'Today\'s Coverage', - 'client_home.widgets.spending' => 'Spending Insights', - 'client_home.widgets.live_activity' => 'Live Activity', - 'client_home.actions.rapid' => 'RAPID', - 'client_home.actions.rapid_subtitle' => 'Urgent same-day', - 'client_home.actions.create_order' => 'Create Order', - 'client_home.actions.create_order_subtitle' => 'Schedule shifts', - 'client_home.actions.hubs' => 'Hubs', - 'client_home.actions.hubs_subtitle' => 'Clock-in points', - 'client_home.reorder.title' => 'REORDER', - 'client_home.reorder.reorder_button' => 'Reorder', - 'client_home.reorder.per_hr' => ({required Object amount}) => '${amount}/hr', - 'client_home.form.edit_reorder' => 'Edit & Reorder', - 'client_home.form.post_new' => 'Post a New Shift', - 'client_home.form.review_subtitle' => 'Review and edit the details before posting', - 'client_home.form.date_label' => 'Date *', - 'client_home.form.date_hint' => 'mm/dd/yyyy', - 'client_home.form.location_label' => 'Location *', - 'client_home.form.location_hint' => 'Business address', - 'client_home.form.positions_title' => 'Positions', - 'client_home.form.add_position' => 'Add Position', - 'client_home.form.role_label' => 'Role *', - 'client_home.form.role_hint' => 'Select role', - 'client_home.form.start_time' => 'Start Time *', - 'client_home.form.end_time' => 'End Time *', - 'client_home.form.workers_needed' => 'Workers Needed *', - 'client_home.form.hourly_rate' => 'Hourly Rate (\$) *', - 'client_home.form.post_shift' => 'Post Shift', - 'client_settings.profile.title' => 'Profile', - 'client_settings.profile.edit_profile' => 'Edit Profile', - 'client_settings.profile.hubs' => 'Hubs', - 'client_settings.profile.log_out' => 'Log Out', - 'client_settings.profile.quick_links' => 'Quick Links', - 'client_settings.profile.clock_in_hubs' => 'Clock-In Hubs', - 'client_settings.profile.billing_payments' => 'Billing & Payments', - 'client_hubs.title' => 'Hubs', - 'client_hubs.subtitle' => 'Manage clock-in locations', - 'client_hubs.add_hub' => 'Add Hub', - 'client_hubs.empty_state.title' => 'No hubs yet', - 'client_hubs.empty_state.description' => 'Create clock-in stations for your locations', - 'client_hubs.empty_state.button' => 'Add Your First Hub', - 'client_hubs.about_hubs.title' => 'About Hubs', - 'client_hubs.about_hubs.description' => 'Hubs are clock-in stations at your locations. Assign NFC tags to each hub so workers can quickly clock in/out using their phones.', - 'client_hubs.hub_card.tag_label' => ({required Object id}) => 'Tag: ${id}', - 'client_hubs.add_hub_dialog.title' => 'Add New Hub', - 'client_hubs.add_hub_dialog.name_label' => 'Hub Name *', - 'client_hubs.add_hub_dialog.name_hint' => 'e.g., Main Kitchen, Front Desk', - 'client_hubs.add_hub_dialog.location_label' => 'Location Name', - 'client_hubs.add_hub_dialog.location_hint' => 'e.g., Downtown Restaurant', - 'client_hubs.add_hub_dialog.address_label' => 'Address', - 'client_hubs.add_hub_dialog.address_hint' => 'Full address', - 'client_hubs.add_hub_dialog.create_button' => 'Create Hub', - 'client_hubs.nfc_dialog.title' => 'Identify NFC Tag', - 'client_hubs.nfc_dialog.instruction' => 'Tap your phone to the NFC tag to identify it', - 'client_hubs.nfc_dialog.scan_button' => 'Scan NFC Tag', - 'client_hubs.nfc_dialog.tag_identified' => 'Tag Identified', - 'client_hubs.nfc_dialog.assign_button' => 'Assign Tag', - 'client_create_order.title' => 'Create Order', - 'client_create_order.section_title' => 'ORDER TYPE', - 'client_create_order.types.rapid' => 'RAPID', - 'client_create_order.types.rapid_desc' => 'URGENT same-day Coverage', - 'client_create_order.types.one_time' => 'One-Time', - 'client_create_order.types.one_time_desc' => 'Single Event or Shift Request', - 'client_create_order.types.recurring' => 'Recurring', - 'client_create_order.types.recurring_desc' => 'Ongoing Weekly / Monthly Coverage', - 'client_create_order.types.permanent' => 'Permanent', - 'client_create_order.types.permanent_desc' => 'Long-Term Staffing Placement', - 'client_create_order.rapid.title' => 'RAPID Order', - 'client_create_order.rapid.subtitle' => 'Emergency staffing in minutes', - 'client_create_order.rapid.urgent_badge' => 'URGENT', - 'client_create_order.rapid.tell_us' => 'Tell us what you need', - 'client_create_order.rapid.need_staff' => 'Need staff urgently?', - 'client_create_order.rapid.type_or_speak' => 'Type or speak what you need. I\'ll handle the rest', - 'client_create_order.rapid.example' => 'Example: ', - 'client_create_order.rapid.hint' => 'Type or speak... (e.g., "Need 5 cooks ASAP until 5am")', - 'client_create_order.rapid.speak' => 'Speak', - 'client_create_order.rapid.listening' => 'Listening...', - 'client_create_order.rapid.send' => 'Send Message', - 'client_create_order.rapid.sending' => 'Sending...', - 'client_create_order.rapid.success_title' => 'Request Sent!', - 'client_create_order.rapid.success_message' => 'We\'re finding available workers for you right now. You\'ll be notified as they accept.', - 'client_create_order.rapid.back_to_orders' => 'Back to Orders', - 'client_create_order.one_time.title' => 'One-Time Order', - 'client_create_order.one_time.subtitle' => 'Single event or shift request', - 'client_create_order.one_time.create_your_order' => 'Create Your Order', - 'client_create_order.one_time.date_label' => 'Date', - 'client_create_order.one_time.date_hint' => 'Select date', - 'client_create_order.one_time.location_label' => 'Location', - 'client_create_order.one_time.location_hint' => 'Enter address', - 'client_create_order.one_time.positions_title' => 'Positions', - 'client_create_order.one_time.add_position' => 'Add Position', - 'client_create_order.one_time.position_number' => ({required Object number}) => 'Position ${number}', - 'client_create_order.one_time.remove' => 'Remove', - 'client_create_order.one_time.select_role' => 'Select role', - 'client_create_order.one_time.start_label' => 'Start', - 'client_create_order.one_time.end_label' => 'End', - 'client_create_order.one_time.workers_label' => 'Workers', - 'client_create_order.one_time.lunch_break_label' => 'Lunch Break', - 'client_create_order.one_time.no_break' => 'No break', - 'client_create_order.one_time.paid_break' => 'min (Paid)', - 'client_create_order.one_time.unpaid_break' => 'min (Unpaid)', - 'client_create_order.one_time.different_location' => 'Use different location for this position', - 'client_create_order.one_time.different_location_title' => 'Different Location', - 'client_create_order.one_time.different_location_hint' => 'Enter different address', - 'client_create_order.one_time.create_order' => 'Create Order', - 'client_create_order.one_time.creating' => 'Creating...', - 'client_create_order.one_time.success_title' => 'Order Created!', - 'client_create_order.one_time.success_message' => 'Your shift request has been posted. Workers will start applying soon.', - 'client_create_order.one_time.back_to_orders' => 'Back to Orders', - 'client_create_order.recurring.title' => 'Recurring Order', - 'client_create_order.recurring.subtitle' => 'Ongoing weekly/monthly coverage', - 'client_create_order.recurring.placeholder' => 'Recurring Order Flow (Work in Progress)', - 'client_create_order.permanent.title' => 'Permanent Order', - 'client_create_order.permanent.subtitle' => 'Long-term staffing placement', - 'client_create_order.permanent.placeholder' => 'Permanent Order Flow (Work in Progress)', - 'client_main.tabs.coverage' => 'Coverage', - 'client_main.tabs.billing' => 'Billing', - 'client_main.tabs.home' => 'Home', - 'client_main.tabs.orders' => 'Orders', - 'client_main.tabs.reports' => 'Reports', - 'client_view_orders.title' => 'Orders', - 'client_view_orders.post_button' => 'Post', - 'client_view_orders.post_order' => 'Post an Order', - 'client_view_orders.no_orders' => ({required Object date}) => 'No orders for ${date}', - 'client_view_orders.tabs.up_next' => 'Up Next', - 'client_view_orders.tabs.active' => 'Active', - 'client_view_orders.tabs.completed' => 'Completed', - 'client_view_orders.card.open' => 'OPEN', - 'client_view_orders.card.filled' => 'FILLED', - 'client_view_orders.card.confirmed' => 'CONFIRMED', - 'client_view_orders.card.in_progress' => 'IN PROGRESS', - 'client_view_orders.card.completed' => 'COMPLETED', - 'client_view_orders.card.cancelled' => 'CANCELLED', - 'client_view_orders.card.get_direction' => 'Get direction', - 'client_view_orders.card.total' => 'Total', - 'client_view_orders.card.hrs' => 'HRS', - 'client_view_orders.card.workers' => ({required Object count}) => '${count} workers', - 'client_view_orders.card.clock_in' => 'CLOCK IN', - 'client_view_orders.card.clock_out' => 'CLOCK OUT', - 'client_view_orders.card.coverage' => 'Coverage', - 'client_view_orders.card.workers_label' => ({required Object filled, required Object needed}) => '${filled}/${needed} Workers', - 'client_view_orders.card.confirmed_workers' => 'Workers Confirmed', - 'client_view_orders.card.no_workers' => 'No workers confirmed yet.', - 'client_billing.title' => 'Billing', - 'client_billing.current_period' => 'Current Period', - 'client_billing.saved_amount' => ({required Object amount}) => '${amount} saved', - 'client_billing.awaiting_approval' => 'Awaiting Approval', - 'client_billing.payment_method' => 'Payment Method', - 'client_billing.add_payment' => 'Add', - 'client_billing.default_badge' => 'Default', - 'client_billing.expires' => ({required Object date}) => 'Expires ${date}', - 'client_billing.period_breakdown' => 'This Period Breakdown', - 'client_billing.week' => 'Week', - 'client_billing.month' => 'Month', - 'client_billing.total' => 'Total', - 'client_billing.hours' => ({required Object count}) => '${count} hours', - 'client_billing.rate_optimization_title' => 'Rate Optimization', - 'client_billing.rate_optimization_body' => ({required Object amount}) => 'Save ${amount}/month by switching 3 shifts', - 'client_billing.view_details' => 'View Details', - 'client_billing.invoice_history' => 'Invoice History', - 'client_billing.view_all' => 'View all', - 'client_billing.export_button' => 'Export All Invoices', - 'client_billing.pending_badge' => 'PENDING APPROVAL', - 'client_billing.paid_badge' => 'PAID', - 'staff.main.tabs.shifts' => 'Shifts', - 'staff.main.tabs.payments' => 'Payments', - 'staff.main.tabs.home' => 'Home', - 'staff.main.tabs.clock_in' => 'Clock In', - 'staff.main.tabs.profile' => 'Profile', - 'staff.home.header.welcome_back' => 'Welcome back', - 'staff.home.header.user_name_placeholder' => 'Krower', - 'staff.home.banners.complete_profile_title' => 'Complete Your Profile', - 'staff.home.banners.complete_profile_subtitle' => 'Get verified to see more shifts', - 'staff.home.banners.availability_title' => 'Availability', - 'staff.home.banners.availability_subtitle' => 'Update your availability for next week', - 'staff.home.quick_actions.find_shifts' => 'Find Shifts', - 'staff.home.quick_actions.availability' => 'Availability', - 'staff.home.quick_actions.messages' => 'Messages', - 'staff.home.quick_actions.earnings' => 'Earnings', - 'staff.home.sections.todays_shift' => 'Today\'s Shift', - 'staff.home.sections.scheduled_count' => ({required Object count}) => '${count} scheduled', - 'staff.home.sections.tomorrow' => 'Tomorrow', - 'staff.home.sections.recommended_for_you' => 'Recommended for You', - 'staff.home.sections.view_all' => 'View all', - 'staff.home.empty_states.no_shifts_today' => 'No shifts scheduled for today', - 'staff.home.empty_states.find_shifts_cta' => 'Find shifts →', - 'staff.home.empty_states.no_shifts_tomorrow' => 'No shifts for tomorrow', - 'staff.home.empty_states.no_recommended_shifts' => 'No recommended shifts', - 'staff.home.pending_payment.title' => 'Pending Payment', - 'staff.home.pending_payment.subtitle' => 'Payment processing', - 'staff.home.pending_payment.amount' => ({required Object amount}) => '${amount}', - 'staff.home.recommended_card.act_now' => '• ACT NOW', - 'staff.home.recommended_card.one_day' => 'One Day', - 'staff.home.recommended_card.today' => 'Today', - 'staff.home.recommended_card.applied_for' => ({required Object title}) => 'Applied for ${title}', - 'staff.home.recommended_card.time_range' => ({required Object start, required Object end}) => '${start} - ${end}', - 'staff.home.benefits.title' => 'Your Benefits', - 'staff.home.benefits.view_all' => 'View all', - 'staff.home.benefits.hours_label' => 'hours', - 'staff.home.benefits.items.sick_days' => 'Sick Days', - 'staff.home.benefits.items.vacation' => 'Vacation', - 'staff.home.benefits.items.holidays' => 'Holidays', - 'staff.home.auto_match.title' => 'Auto-Match', - 'staff.home.auto_match.finding_shifts' => 'Finding shifts for you', - 'staff.home.auto_match.get_matched' => 'Get matched automatically', - 'staff.home.auto_match.matching_based_on' => 'Matching based on:', - 'staff.home.auto_match.chips.location' => 'Location', - 'staff.home.auto_match.chips.availability' => 'Availability', - 'staff.home.auto_match.chips.skills' => 'Skills', - 'staff.home.improve.title' => 'Improve Yourself', - 'staff.home.improve.items.training.title' => 'Training Section', - 'staff.home.improve.items.training.description' => 'Improve your skills and get certified.', - 'staff.home.improve.items.training.page' => '/krow-university', - 'staff.home.improve.items.podcast.title' => 'Krow Podcast', - 'staff.home.improve.items.podcast.description' => 'Listen to tips from top workers.', - 'staff.home.improve.items.podcast.page' => '/krow-university', - 'staff.home.more_ways.title' => 'More Ways To Use Krow', - 'staff.home.more_ways.items.benefits.title' => 'Krow Benefits', - 'staff.home.more_ways.items.benefits.page' => '/benefits', - 'staff.home.more_ways.items.refer.title' => 'Refer a Friend', - 'staff.home.more_ways.items.refer.page' => '/worker-profile', - 'staff.profile.header.title' => 'Profile', - 'staff.profile.header.sign_out' => 'SIGN OUT', - 'staff.profile.reliability_stats.shifts' => 'Shifts', - 'staff.profile.reliability_stats.rating' => 'Rating', - 'staff.profile.reliability_stats.on_time' => 'On Time', - 'staff.profile.reliability_stats.no_shows' => 'No Shows', - 'staff.profile.reliability_stats.cancellations' => 'Cancel.', - 'staff.profile.reliability_score.title' => 'Reliability Score', - 'staff.profile.reliability_score.description' => 'Keep your score above 45% to continue picking up shifts.', - 'staff.profile.sections.onboarding' => 'ONBOARDING', - 'staff.profile.sections.compliance' => 'COMPLIANCE', - 'staff.profile.sections.level_up' => 'LEVEL UP', - 'staff.profile.sections.finance' => 'FINANCE', - 'staff.profile.sections.support' => 'SUPPORT', - 'staff.profile.menu_items.personal_info' => 'Personal Info', - 'staff.profile.menu_items.emergency_contact' => 'Emergency Contact', - 'staff.profile.menu_items.experience' => 'Experience', - 'staff.profile.menu_items.attire' => 'Attire', - 'staff.profile.menu_items.documents' => 'Documents', - 'staff.profile.menu_items.certificates' => 'Certificates', - 'staff.profile.menu_items.tax_forms' => 'Tax Forms', - 'staff.profile.menu_items.krow_university' => 'Krow University', - 'staff.profile.menu_items.trainings' => 'Trainings', - 'staff.profile.menu_items.leaderboard' => 'Leaderboard', - 'staff.profile.menu_items.bank_account' => 'Bank Account', - 'staff.profile.menu_items.payments' => 'Payments', - 'staff.profile.menu_items.timecard' => 'Timecard', - 'staff.profile.menu_items.faqs' => 'FAQs', - 'staff.profile.menu_items.privacy_security' => 'Privacy & Security', - 'staff.profile.menu_items.messages' => 'Messages', - 'staff.profile.bank_account_page.title' => 'Bank Account', - 'staff.profile.bank_account_page.linked_accounts' => 'LINKED ACCOUNTS', - 'staff.profile.bank_account_page.add_account' => 'Add New Account', - 'staff.profile.bank_account_page.secure_title' => '100% Secured', - 'staff.profile.bank_account_page.secure_subtitle' => 'Your account details are encrypted and safe.', - 'staff.profile.bank_account_page.primary' => 'Primary', - 'staff.profile.bank_account_page.add_new_account' => 'Add New Account', - 'staff.profile.bank_account_page.routing_number' => 'Routing Number', - 'staff.profile.bank_account_page.routing_hint' => 'Enter routing number', - 'staff.profile.bank_account_page.account_number' => 'Account Number', - 'staff.profile.bank_account_page.account_hint' => 'Enter account number', - 'staff.profile.bank_account_page.account_type' => 'Account Type', - 'staff.profile.bank_account_page.checking' => 'Checking', - 'staff.profile.bank_account_page.savings' => 'Savings', - 'staff.profile.bank_account_page.cancel' => 'Cancel', - 'staff.profile.bank_account_page.save' => 'Save', - 'staff.profile.bank_account_page.account_ending' => ({required Object last4}) => 'Ending in ${last4}', - 'staff.profile.logout.button' => 'Sign Out', - 'staff.onboarding.personal_info.title' => 'Personal Info', - 'staff.onboarding.personal_info.change_photo_hint' => 'Tap to change photo', - 'staff.onboarding.personal_info.full_name_label' => 'Full Name', - 'staff.onboarding.personal_info.email_label' => 'Email', - 'staff.onboarding.personal_info.phone_label' => 'Phone Number', - 'staff.onboarding.personal_info.phone_hint' => '+1 (555) 000-0000', - 'staff.onboarding.personal_info.bio_label' => 'Bio', - 'staff.onboarding.personal_info.bio_hint' => 'Tell clients about yourself...', - 'staff.onboarding.personal_info.languages_label' => 'Languages', - 'staff.onboarding.personal_info.languages_hint' => 'English, Spanish, French...', - 'staff.onboarding.personal_info.locations_label' => 'Preferred Locations', - 'staff.onboarding.personal_info.locations_hint' => 'Downtown, Midtown, Brooklyn...', - 'staff.onboarding.personal_info.save_button' => 'Save Changes', - 'staff.onboarding.personal_info.save_success' => 'Personal info saved successfully', - 'staff.onboarding.experience.title' => 'Experience & Skills', - 'staff.onboarding.experience.industries_title' => 'Industries', - 'staff.onboarding.experience.industries_subtitle' => 'Select the industries you have experience in', - 'staff.onboarding.experience.skills_title' => 'Skills', - 'staff.onboarding.experience.skills_subtitle' => 'Select your skills or add custom ones', - 'staff.onboarding.experience.custom_skills_title' => 'Custom Skills:', - 'staff.onboarding.experience.custom_skill_hint' => 'Add custom skill...', - 'staff.onboarding.experience.save_button' => 'Save & Continue', - 'staff.onboarding.experience.industries.hospitality' => 'Hospitality', - 'staff.onboarding.experience.industries.food_service' => 'Food Service', - 'staff.onboarding.experience.industries.warehouse' => 'Warehouse', - 'staff.onboarding.experience.industries.events' => 'Events', - 'staff.onboarding.experience.industries.retail' => 'Retail', - 'staff.onboarding.experience.industries.healthcare' => 'Healthcare', - 'staff.onboarding.experience.industries.other' => 'Other', - 'staff.onboarding.experience.skills.food_service' => 'Food Service', - 'staff.onboarding.experience.skills.bartending' => 'Bartending', - 'staff.onboarding.experience.skills.event_setup' => 'Event Setup', - 'staff.onboarding.experience.skills.hospitality' => 'Hospitality', - 'staff.onboarding.experience.skills.warehouse' => 'Warehouse', - 'staff.onboarding.experience.skills.customer_service' => 'Customer Service', - 'staff.onboarding.experience.skills.cleaning' => 'Cleaning', - 'staff.onboarding.experience.skills.security' => 'Security', - 'staff.onboarding.experience.skills.retail' => 'Retail', - 'staff.onboarding.experience.skills.cooking' => 'Cooking', - 'staff.onboarding.experience.skills.cashier' => 'Cashier', - 'staff.onboarding.experience.skills.server' => 'Server', - 'staff.onboarding.experience.skills.barista' => 'Barista', - 'staff.onboarding.experience.skills.host_hostess' => 'Host/Hostess', - 'staff.onboarding.experience.skills.busser' => 'Busser', - 'staff.onboarding.experience.skills.driving' => 'Driving', - 'staff_documents.title' => 'Documents', - 'staff_documents.verification_card.title' => 'Document Verification', - 'staff_documents.verification_card.progress' => ({required Object completed, required Object total}) => '${completed}/${total} Complete', - 'staff_documents.list.empty' => 'No documents found', - 'staff_documents.list.error' => ({required Object message}) => 'Error: ${message}', - 'staff_documents.card.view' => 'View', - 'staff_documents.card.upload' => 'Upload', - 'staff_documents.card.verified' => 'Verified', - 'staff_documents.card.pending' => 'Pending', - 'staff_documents.card.missing' => 'Missing', - 'staff_documents.card.rejected' => 'Rejected', - 'staff_certificates.title' => 'Certificates', - 'staff_certificates.progress.title' => 'Your Progress', - 'staff_certificates.progress.verified_count' => ({required Object completed, required Object total}) => '${completed} of ${total} verified', - 'staff_certificates.progress.active' => 'Compliance Active', - 'staff_certificates.card.expires_in_days' => ({required Object days}) => 'Expires in ${days} days - Renew now', - 'staff_certificates.card.expired' => 'Expired - Renew now', - 'staff_certificates.card.verified' => 'Verified', - 'staff_certificates.card.expiring_soon' => 'Expiring Soon', - 'staff_certificates.card.exp' => ({required Object date}) => 'Exp: ${date}', - 'staff_certificates.card.upload_button' => 'Upload Certificate', - 'staff_certificates.card.edit_expiry' => 'Edit Expiration Date', - 'staff_certificates.card.remove' => 'Remove Certificate', - 'staff_certificates.card.renew' => 'Renew', - 'staff_certificates.card.opened_snackbar' => 'Certificate opened in new tab', - 'staff_certificates.add_more.title' => 'Add Another Certificate', - 'staff_certificates.add_more.subtitle' => 'Upload additional certifications', - 'staff_certificates.upload_modal.title' => 'Upload Certificate', - 'staff_certificates.upload_modal.expiry_label' => 'Expiration Date (Optional)', - 'staff_certificates.upload_modal.select_date' => 'Select date', - 'staff_certificates.upload_modal.upload_file' => 'Upload File', - 'staff_certificates.upload_modal.drag_drop' => 'Drag and drop or click to upload', - 'staff_certificates.upload_modal.supported_formats' => 'PDF, JPG, PNG up to 10MB', - 'staff_certificates.upload_modal.cancel' => 'Cancel', - 'staff_certificates.upload_modal.save' => 'Save Certificate', - 'staff_certificates.delete_modal.title' => 'Remove Certificate?', - 'staff_certificates.delete_modal.message' => 'This action cannot be undone.', - 'staff_certificates.delete_modal.cancel' => 'Cancel', - 'staff_certificates.delete_modal.confirm' => 'Remove', - 'staff_profile_attire.title' => 'Attire', - 'staff_profile_attire.info_card.title' => 'Your Wardrobe', - 'staff_profile_attire.info_card.description' => 'Select the attire items you own. This helps us match you with shifts that fit your wardrobe.', - 'staff_profile_attire.status.required' => 'REQUIRED', - 'staff_profile_attire.status.add_photo' => 'Add Photo', - 'staff_profile_attire.status.added' => 'Added', - 'staff_profile_attire.status.pending' => '⏳ Pending verification', - 'staff_profile_attire.attestation' => 'I certify that I own these items and will wear them to my shifts. I understand that items are pending manager verification at my first shift.', - 'staff_profile_attire.actions.save' => 'Save Attire', - 'staff_profile_attire.validation.select_required' => '✓ Select all required items', - 'staff_profile_attire.validation.upload_required' => '✓ Upload photos of required items', - 'staff_profile_attire.validation.accept_attestation' => '✓ Accept attestation', - 'staff_shifts.title' => 'Shifts', - 'staff_shifts.tabs.my_shifts' => 'My Shifts', - 'staff_shifts.tabs.find_work' => 'Find Work', - 'staff_shifts.list.no_shifts' => 'No shifts found', - 'staff_shifts.list.pending_offers' => 'PENDING OFFERS', - 'staff_shifts.list.available_jobs' => ({required Object count}) => '${count} AVAILABLE JOBS', - 'staff_shifts.list.search_hint' => 'Search jobs...', - 'staff_shifts.filter.all' => 'All Jobs', - 'staff_shifts.filter.one_day' => 'One Day', - 'staff_shifts.filter.multi_day' => 'Multi Day', - 'staff_shifts.filter.long_term' => 'Long Term', - 'staff_shifts.status.confirmed' => 'CONFIRMED', - 'staff_shifts.status.act_now' => 'ACT NOW', - 'staff_shifts.status.swap_requested' => 'SWAP REQUESTED', - 'staff_shifts.status.completed' => 'COMPLETED', - 'staff_shifts.status.no_show' => 'NO SHOW', - 'staff_shifts.status.pending_warning' => 'Please confirm assignment', - 'staff_shifts.action.decline' => 'Decline', - 'staff_shifts.action.confirm' => 'Confirm', - 'staff_shifts.action.request_swap' => 'Request Swap', - 'staff_shifts.details.additional' => 'ADDITIONAL DETAILS', - 'staff_shifts.details.days' => ({required Object days}) => '${days} Days', - 'staff_shifts.details.exp_total' => ({required Object amount}) => '(exp.total \$${amount})', - 'staff_shifts.details.pending_time' => ({required Object time}) => 'Pending ${time} ago', - 'staff_shifts.tags.immediate_start' => 'Immediate start', - 'staff_shifts.tags.no_experience' => 'No experience', - 'staff_time_card.title' => 'Timecard', - 'staff_time_card.hours_worked' => 'Hours Worked', - 'staff_time_card.total_earnings' => 'Total Earnings', - 'staff_time_card.shift_history' => 'Shift History', - _ => null, - } ?? switch (path) { - 'staff_time_card.no_shifts' => 'No shifts for this month', - 'staff_time_card.hours' => 'hours', - 'staff_time_card.per_hr' => '/hr', - 'staff_time_card.status.approved' => 'Approved', - 'staff_time_card.status.disputed' => 'Disputed', - 'staff_time_card.status.paid' => 'Paid', - 'staff_time_card.status.pending' => 'Pending', - _ => null, - }; - } -} diff --git a/apps/mobile/packages/core_localization/lib/src/l10n/strings_es.g.dart b/apps/mobile/packages/core_localization/lib/src/l10n/strings_es.g.dart deleted file mode 100644 index 23fb8610..00000000 --- a/apps/mobile/packages/core_localization/lib/src/l10n/strings_es.g.dart +++ /dev/null @@ -1,2153 +0,0 @@ -/// -/// Generated file. Do not edit. -/// -// coverage:ignore-file -// ignore_for_file: type=lint, unused_import -// dart format off - -import 'package:flutter/widgets.dart'; -import 'package:intl/intl.dart'; -import 'package:slang/generated.dart'; -import 'strings.g.dart'; - -// Path: -class TranslationsEs with BaseTranslations implements Translations { - /// You can call this constructor and build your own translation instance of this locale. - /// Constructing via the enum [AppLocale.build] is preferred. - TranslationsEs({Map? overrides, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver, TranslationMetadata? meta}) - : assert(overrides == null, 'Set "translation_overrides: true" in order to enable this feature.'), - $meta = meta ?? TranslationMetadata( - locale: AppLocale.es, - overrides: overrides ?? {}, - cardinalResolver: cardinalResolver, - ordinalResolver: ordinalResolver, - ) { - $meta.setFlatMapFunction(_flatMapFunction); - } - - /// Metadata for the translations of . - @override final TranslationMetadata $meta; - - /// Access flat map - @override dynamic operator[](String key) => $meta.getTranslation(key); - - late final TranslationsEs _root = this; // ignore: unused_field - - @override - TranslationsEs $copyWith({TranslationMetadata? meta}) => TranslationsEs(meta: meta ?? this.$meta); - - // Translations - @override late final _TranslationsCommonEs common = _TranslationsCommonEs._(_root); - @override late final _TranslationsSettingsEs settings = _TranslationsSettingsEs._(_root); - @override late final _TranslationsStaffAuthenticationEs staff_authentication = _TranslationsStaffAuthenticationEs._(_root); - @override late final _TranslationsClientAuthenticationEs client_authentication = _TranslationsClientAuthenticationEs._(_root); - @override late final _TranslationsClientHomeEs client_home = _TranslationsClientHomeEs._(_root); - @override late final _TranslationsClientSettingsEs client_settings = _TranslationsClientSettingsEs._(_root); - @override late final _TranslationsClientHubsEs client_hubs = _TranslationsClientHubsEs._(_root); - @override late final _TranslationsClientCreateOrderEs client_create_order = _TranslationsClientCreateOrderEs._(_root); - @override late final _TranslationsClientMainEs client_main = _TranslationsClientMainEs._(_root); - @override late final _TranslationsClientViewOrdersEs client_view_orders = _TranslationsClientViewOrdersEs._(_root); - @override late final _TranslationsClientBillingEs client_billing = _TranslationsClientBillingEs._(_root); - @override late final _TranslationsStaffEs staff = _TranslationsStaffEs._(_root); - @override late final _TranslationsStaffDocumentsEs staff_documents = _TranslationsStaffDocumentsEs._(_root); - @override late final _TranslationsStaffCertificatesEs staff_certificates = _TranslationsStaffCertificatesEs._(_root); - @override late final _TranslationsStaffProfileAttireEs staff_profile_attire = _TranslationsStaffProfileAttireEs._(_root); - @override late final _TranslationsStaffShiftsEs staff_shifts = _TranslationsStaffShiftsEs._(_root); - @override late final _TranslationsStaffTimeCardEs staff_time_card = _TranslationsStaffTimeCardEs._(_root); -} - -// Path: common -class _TranslationsCommonEs implements TranslationsCommonEn { - _TranslationsCommonEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String get ok => 'Aceptar'; - @override String get cancel => 'Cancelar'; - @override String get save => 'Guardar'; - @override String get delete => 'Eliminar'; - @override String get continue_text => 'Continuar'; -} - -// Path: settings -class _TranslationsSettingsEs implements TranslationsSettingsEn { - _TranslationsSettingsEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String get language => 'Idioma'; - @override String get change_language => 'Cambiar Idioma'; -} - -// Path: staff_authentication -class _TranslationsStaffAuthenticationEs implements TranslationsStaffAuthenticationEn { - _TranslationsStaffAuthenticationEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override late final _TranslationsStaffAuthenticationGetStartedPageEs get_started_page = _TranslationsStaffAuthenticationGetStartedPageEs._(_root); - @override late final _TranslationsStaffAuthenticationPhoneVerificationPageEs phone_verification_page = _TranslationsStaffAuthenticationPhoneVerificationPageEs._(_root); - @override late final _TranslationsStaffAuthenticationPhoneInputEs phone_input = _TranslationsStaffAuthenticationPhoneInputEs._(_root); - @override late final _TranslationsStaffAuthenticationOtpVerificationEs otp_verification = _TranslationsStaffAuthenticationOtpVerificationEs._(_root); - @override late final _TranslationsStaffAuthenticationProfileSetupPageEs profile_setup_page = _TranslationsStaffAuthenticationProfileSetupPageEs._(_root); - @override late final _TranslationsStaffAuthenticationCommonEs common = _TranslationsStaffAuthenticationCommonEs._(_root); -} - -// Path: client_authentication -class _TranslationsClientAuthenticationEs implements TranslationsClientAuthenticationEn { - _TranslationsClientAuthenticationEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override late final _TranslationsClientAuthenticationGetStartedPageEs get_started_page = _TranslationsClientAuthenticationGetStartedPageEs._(_root); - @override late final _TranslationsClientAuthenticationSignInPageEs sign_in_page = _TranslationsClientAuthenticationSignInPageEs._(_root); - @override late final _TranslationsClientAuthenticationSignUpPageEs sign_up_page = _TranslationsClientAuthenticationSignUpPageEs._(_root); -} - -// Path: client_home -class _TranslationsClientHomeEs implements TranslationsClientHomeEn { - _TranslationsClientHomeEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override late final _TranslationsClientHomeDashboardEs dashboard = _TranslationsClientHomeDashboardEs._(_root); - @override late final _TranslationsClientHomeWidgetsEs widgets = _TranslationsClientHomeWidgetsEs._(_root); - @override late final _TranslationsClientHomeActionsEs actions = _TranslationsClientHomeActionsEs._(_root); - @override late final _TranslationsClientHomeReorderEs reorder = _TranslationsClientHomeReorderEs._(_root); - @override late final _TranslationsClientHomeFormEs form = _TranslationsClientHomeFormEs._(_root); -} - -// Path: client_settings -class _TranslationsClientSettingsEs implements TranslationsClientSettingsEn { - _TranslationsClientSettingsEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override late final _TranslationsClientSettingsProfileEs profile = _TranslationsClientSettingsProfileEs._(_root); -} - -// Path: client_hubs -class _TranslationsClientHubsEs implements TranslationsClientHubsEn { - _TranslationsClientHubsEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String get title => 'Hubs'; - @override String get subtitle => 'Gestionar ubicaciones de marcaje'; - @override String get add_hub => 'Añadir Hub'; - @override late final _TranslationsClientHubsEmptyStateEs empty_state = _TranslationsClientHubsEmptyStateEs._(_root); - @override late final _TranslationsClientHubsAboutHubsEs about_hubs = _TranslationsClientHubsAboutHubsEs._(_root); - @override late final _TranslationsClientHubsHubCardEs hub_card = _TranslationsClientHubsHubCardEs._(_root); - @override late final _TranslationsClientHubsAddHubDialogEs add_hub_dialog = _TranslationsClientHubsAddHubDialogEs._(_root); - @override late final _TranslationsClientHubsNfcDialogEs nfc_dialog = _TranslationsClientHubsNfcDialogEs._(_root); -} - -// Path: client_create_order -class _TranslationsClientCreateOrderEs implements TranslationsClientCreateOrderEn { - _TranslationsClientCreateOrderEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String get title => 'Crear Orden'; - @override String get section_title => 'TIPO DE ORDEN'; - @override late final _TranslationsClientCreateOrderTypesEs types = _TranslationsClientCreateOrderTypesEs._(_root); - @override late final _TranslationsClientCreateOrderRapidEs rapid = _TranslationsClientCreateOrderRapidEs._(_root); - @override late final _TranslationsClientCreateOrderOneTimeEs one_time = _TranslationsClientCreateOrderOneTimeEs._(_root); - @override late final _TranslationsClientCreateOrderRecurringEs recurring = _TranslationsClientCreateOrderRecurringEs._(_root); - @override late final _TranslationsClientCreateOrderPermanentEs permanent = _TranslationsClientCreateOrderPermanentEs._(_root); -} - -// Path: client_main -class _TranslationsClientMainEs implements TranslationsClientMainEn { - _TranslationsClientMainEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override late final _TranslationsClientMainTabsEs tabs = _TranslationsClientMainTabsEs._(_root); -} - -// Path: client_view_orders -class _TranslationsClientViewOrdersEs implements TranslationsClientViewOrdersEn { - _TranslationsClientViewOrdersEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String get title => 'Órdenes'; - @override String get post_button => 'Publicar'; - @override String get post_order => 'Publicar una Orden'; - @override String no_orders({required Object date}) => 'No hay órdenes para ${date}'; - @override late final _TranslationsClientViewOrdersTabsEs tabs = _TranslationsClientViewOrdersTabsEs._(_root); - @override late final _TranslationsClientViewOrdersCardEs card = _TranslationsClientViewOrdersCardEs._(_root); -} - -// Path: client_billing -class _TranslationsClientBillingEs implements TranslationsClientBillingEn { - _TranslationsClientBillingEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String get title => 'Facturación'; - @override String get current_period => 'Período Actual'; - @override String saved_amount({required Object amount}) => '${amount} ahorrado'; - @override String get awaiting_approval => 'Esperando Aprobación'; - @override String get payment_method => 'Método de Pago'; - @override String get add_payment => 'Añadir'; - @override String get default_badge => 'Predeterminado'; - @override String expires({required Object date}) => 'Expira ${date}'; - @override String get period_breakdown => 'Desglose de este Período'; - @override String get week => 'Semana'; - @override String get month => 'Mes'; - @override String get total => 'Total'; - @override String hours({required Object count}) => '${count} horas'; - @override String get rate_optimization_title => 'Optimización de Tarifas'; - @override String rate_optimization_body({required Object amount}) => 'Ahorra ${amount}/mes cambiando 3 turnos'; - @override String get view_details => 'Ver Detalles'; - @override String get invoice_history => 'Historial de Facturas'; - @override String get view_all => 'Ver todo'; - @override String get export_button => 'Exportar Todas las Facturas'; - @override String get pending_badge => 'PENDIENTE APROBACIÓN'; - @override String get paid_badge => 'PAGADO'; -} - -// Path: staff -class _TranslationsStaffEs implements TranslationsStaffEn { - _TranslationsStaffEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override late final _TranslationsStaffMainEs main = _TranslationsStaffMainEs._(_root); - @override late final _TranslationsStaffHomeEs home = _TranslationsStaffHomeEs._(_root); - @override late final _TranslationsStaffProfileEs profile = _TranslationsStaffProfileEs._(_root); - @override late final _TranslationsStaffOnboardingEs onboarding = _TranslationsStaffOnboardingEs._(_root); -} - -// Path: staff_documents -class _TranslationsStaffDocumentsEs implements TranslationsStaffDocumentsEn { - _TranslationsStaffDocumentsEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String get title => 'Documents'; - @override late final _TranslationsStaffDocumentsVerificationCardEs verification_card = _TranslationsStaffDocumentsVerificationCardEs._(_root); - @override late final _TranslationsStaffDocumentsListEs list = _TranslationsStaffDocumentsListEs._(_root); - @override late final _TranslationsStaffDocumentsCardEs card = _TranslationsStaffDocumentsCardEs._(_root); -} - -// Path: staff_certificates -class _TranslationsStaffCertificatesEs implements TranslationsStaffCertificatesEn { - _TranslationsStaffCertificatesEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String get title => 'Certificates'; - @override late final _TranslationsStaffCertificatesProgressEs progress = _TranslationsStaffCertificatesProgressEs._(_root); - @override late final _TranslationsStaffCertificatesCardEs card = _TranslationsStaffCertificatesCardEs._(_root); - @override late final _TranslationsStaffCertificatesAddMoreEs add_more = _TranslationsStaffCertificatesAddMoreEs._(_root); - @override late final _TranslationsStaffCertificatesUploadModalEs upload_modal = _TranslationsStaffCertificatesUploadModalEs._(_root); - @override late final _TranslationsStaffCertificatesDeleteModalEs delete_modal = _TranslationsStaffCertificatesDeleteModalEs._(_root); -} - -// Path: staff_profile_attire -class _TranslationsStaffProfileAttireEs implements TranslationsStaffProfileAttireEn { - _TranslationsStaffProfileAttireEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String get title => 'Vestimenta'; - @override late final _TranslationsStaffProfileAttireInfoCardEs info_card = _TranslationsStaffProfileAttireInfoCardEs._(_root); - @override late final _TranslationsStaffProfileAttireStatusEs status = _TranslationsStaffProfileAttireStatusEs._(_root); - @override String get attestation => 'Certifico que poseo estos artículos y los usaré en mis turnos. Entiendo que los artículos están pendientes de verificación por el gerente en mi primer turno.'; - @override late final _TranslationsStaffProfileAttireActionsEs actions = _TranslationsStaffProfileAttireActionsEs._(_root); - @override late final _TranslationsStaffProfileAttireValidationEs validation = _TranslationsStaffProfileAttireValidationEs._(_root); -} - -// Path: staff_shifts -class _TranslationsStaffShiftsEs implements TranslationsStaffShiftsEn { - _TranslationsStaffShiftsEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String get title => 'Shifts'; - @override late final _TranslationsStaffShiftsTabsEs tabs = _TranslationsStaffShiftsTabsEs._(_root); - @override late final _TranslationsStaffShiftsListEs list = _TranslationsStaffShiftsListEs._(_root); - @override late final _TranslationsStaffShiftsFilterEs filter = _TranslationsStaffShiftsFilterEs._(_root); - @override late final _TranslationsStaffShiftsStatusEs status = _TranslationsStaffShiftsStatusEs._(_root); - @override late final _TranslationsStaffShiftsActionEs action = _TranslationsStaffShiftsActionEs._(_root); - @override late final _TranslationsStaffShiftsDetailsEs details = _TranslationsStaffShiftsDetailsEs._(_root); - @override late final _TranslationsStaffShiftsTagsEs tags = _TranslationsStaffShiftsTagsEs._(_root); -} - -// Path: staff_time_card -class _TranslationsStaffTimeCardEs implements TranslationsStaffTimeCardEn { - _TranslationsStaffTimeCardEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String get title => 'Tarjeta de tiempo'; - @override String get hours_worked => 'Horas trabajadas'; - @override String get total_earnings => 'Ganancias totales'; - @override String get shift_history => 'Historial de turnos'; - @override String get no_shifts => 'No hay turnos para este mes'; - @override String get hours => 'horas'; - @override String get per_hr => '/hr'; - @override late final _TranslationsStaffTimeCardStatusEs status = _TranslationsStaffTimeCardStatusEs._(_root); -} - -// Path: staff_authentication.get_started_page -class _TranslationsStaffAuthenticationGetStartedPageEs implements TranslationsStaffAuthenticationGetStartedPageEn { - _TranslationsStaffAuthenticationGetStartedPageEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String get title_part1 => 'Trabaja, Crece, '; - @override String get title_part2 => 'Elévate'; - @override String get subtitle => 'Construye tu carrera en hostelería con \nflexibilidad y libertad.'; - @override String get sign_up_button => 'Registrarse'; - @override String get log_in_button => 'Iniciar sesión'; -} - -// Path: staff_authentication.phone_verification_page -class _TranslationsStaffAuthenticationPhoneVerificationPageEs implements TranslationsStaffAuthenticationPhoneVerificationPageEn { - _TranslationsStaffAuthenticationPhoneVerificationPageEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String get validation_error => 'Por favor, ingresa un número de teléfono válido de 10 dígitos'; - @override String get send_code_button => 'Enviar código'; - @override String get enter_code_title => 'Ingresa el código de verificación'; - @override String get code_sent_message => 'Enviamos un código de 6 dígitos a '; - @override String get code_sent_instruction => '. Ingrésalo a continuación para verificar tu cuenta.'; -} - -// Path: staff_authentication.phone_input -class _TranslationsStaffAuthenticationPhoneInputEs implements TranslationsStaffAuthenticationPhoneInputEn { - _TranslationsStaffAuthenticationPhoneInputEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String get title => 'Verifica tu número de teléfono'; - @override String get subtitle => 'Te enviaremos un código de verificación para comenzar.'; - @override String get label => 'Número de teléfono'; - @override String get hint => 'Ingresa tu número'; -} - -// Path: staff_authentication.otp_verification -class _TranslationsStaffAuthenticationOtpVerificationEs implements TranslationsStaffAuthenticationOtpVerificationEn { - _TranslationsStaffAuthenticationOtpVerificationEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String get did_not_get_code => '¿No recibiste el código?'; - @override String resend_in({required Object seconds}) => 'Reenviar en ${seconds} s'; - @override String get resend_code => 'Reenviar código'; -} - -// Path: staff_authentication.profile_setup_page -class _TranslationsStaffAuthenticationProfileSetupPageEs implements TranslationsStaffAuthenticationProfileSetupPageEn { - _TranslationsStaffAuthenticationProfileSetupPageEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String step_indicator({required Object current, required Object total}) => 'Paso ${current} de ${total}'; - @override String get error_occurred => 'Ocurrió un error'; - @override String get complete_setup_button => 'Completar configuración'; - @override late final _TranslationsStaffAuthenticationProfileSetupPageStepsEs steps = _TranslationsStaffAuthenticationProfileSetupPageStepsEs._(_root); - @override late final _TranslationsStaffAuthenticationProfileSetupPageBasicInfoEs basic_info = _TranslationsStaffAuthenticationProfileSetupPageBasicInfoEs._(_root); - @override late final _TranslationsStaffAuthenticationProfileSetupPageLocationEs location = _TranslationsStaffAuthenticationProfileSetupPageLocationEs._(_root); - @override late final _TranslationsStaffAuthenticationProfileSetupPageExperienceEs experience = _TranslationsStaffAuthenticationProfileSetupPageExperienceEs._(_root); -} - -// Path: staff_authentication.common -class _TranslationsStaffAuthenticationCommonEs implements TranslationsStaffAuthenticationCommonEn { - _TranslationsStaffAuthenticationCommonEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String get trouble_question => '¿Tienes problemas? '; - @override String get contact_support => 'Contactar a soporte'; -} - -// Path: client_authentication.get_started_page -class _TranslationsClientAuthenticationGetStartedPageEs implements TranslationsClientAuthenticationGetStartedPageEn { - _TranslationsClientAuthenticationGetStartedPageEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String get title => 'Toma el control de tus\nturnos y eventos'; - @override String get subtitle => 'Optimiza tus operaciones con potentes herramientas para gestionar horarios, realizar un seguimiento del rendimiento y mantener a tu equipo en la misma página, todo en un solo lugar'; - @override String get sign_in_button => 'Iniciar sesión'; - @override String get create_account_button => 'Crear cuenta'; -} - -// Path: client_authentication.sign_in_page -class _TranslationsClientAuthenticationSignInPageEs implements TranslationsClientAuthenticationSignInPageEn { - _TranslationsClientAuthenticationSignInPageEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String get title => 'Bienvenido de nuevo'; - @override String get subtitle => 'Inicia sesión para gestionar tus turnos y trabajadores'; - @override String get email_label => 'Correo electrónico'; - @override String get email_hint => 'Ingresa tu correo electrónico'; - @override String get password_label => 'Contraseña'; - @override String get password_hint => 'Ingresa tu contraseña'; - @override String get forgot_password => '¿Olvidaste tu contraseña?'; - @override String get sign_in_button => 'Iniciar sesión'; - @override String get or_divider => 'o'; - @override String get social_apple => 'Iniciar sesión con Apple'; - @override String get social_google => 'Iniciar sesión con Google'; - @override String get no_account => '¿No tienes una cuenta? '; - @override String get sign_up_link => 'Regístrate'; -} - -// Path: client_authentication.sign_up_page -class _TranslationsClientAuthenticationSignUpPageEs implements TranslationsClientAuthenticationSignUpPageEn { - _TranslationsClientAuthenticationSignUpPageEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String get title => 'Crear cuenta'; - @override String get subtitle => 'Comienza con Krow para tu negocio'; - @override String get company_label => 'Nombre de la empresa'; - @override String get company_hint => 'Ingresa el nombre de la empresa'; - @override String get email_label => 'Correo electrónico'; - @override String get email_hint => 'Ingresa tu correo electrónico'; - @override String get password_label => 'Contraseña'; - @override String get password_hint => 'Crea una contraseña'; - @override String get confirm_password_label => 'Confirmar contraseña'; - @override String get confirm_password_hint => 'Confirma tu contraseña'; - @override String get create_account_button => 'Crear cuenta'; - @override String get or_divider => 'o'; - @override String get social_apple => 'Regístrate con Apple'; - @override String get social_google => 'Regístrate con Google'; - @override String get has_account => '¿Ya tienes una cuenta? '; - @override String get sign_in_link => 'Iniciar sesión'; -} - -// Path: client_home.dashboard -class _TranslationsClientHomeDashboardEs implements TranslationsClientHomeDashboardEn { - _TranslationsClientHomeDashboardEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String get welcome_back => 'Bienvenido de nuevo'; - @override String get edit_mode_active => 'Modo Edición Activo'; - @override String get drag_instruction => 'Arrastra para reordenar, cambia la visibilidad'; - @override String get reset => 'Restablecer'; - @override String get metric_needed => 'Necesario'; - @override String get metric_filled => 'Lleno'; - @override String get metric_open => 'Abierto'; - @override String get view_all => 'Ver todo'; - @override String insight_lightbulb({required Object amount}) => 'Ahorra ${amount}/mes'; - @override String get insight_tip => 'Reserva con 48h de antelación para mejores tarifas'; -} - -// Path: client_home.widgets -class _TranslationsClientHomeWidgetsEs implements TranslationsClientHomeWidgetsEn { - _TranslationsClientHomeWidgetsEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String get actions => 'Acciones Rápidas'; - @override String get reorder => 'Reordenar'; - @override String get coverage => 'Cobertura de Hoy'; - @override String get spending => 'Información de Gastos'; - @override String get live_activity => 'Actividad en Vivo'; -} - -// Path: client_home.actions -class _TranslationsClientHomeActionsEs implements TranslationsClientHomeActionsEn { - _TranslationsClientHomeActionsEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String get rapid => 'RÁPIDO'; - @override String get rapid_subtitle => 'Urgente mismo día'; - @override String get create_order => 'Crear Orden'; - @override String get create_order_subtitle => 'Programar turnos'; - @override String get hubs => 'Hubs'; - @override String get hubs_subtitle => 'Puntos marcaje'; -} - -// Path: client_home.reorder -class _TranslationsClientHomeReorderEs implements TranslationsClientHomeReorderEn { - _TranslationsClientHomeReorderEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String get title => 'REORDENAR'; - @override String get reorder_button => 'Reordenar'; - @override String per_hr({required Object amount}) => '${amount}/hr'; -} - -// Path: client_home.form -class _TranslationsClientHomeFormEs implements TranslationsClientHomeFormEn { - _TranslationsClientHomeFormEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String get edit_reorder => 'Editar y Reordenar'; - @override String get post_new => 'Publicar un Nuevo Turno'; - @override String get review_subtitle => 'Revisa y edita los detalles antes de publicar'; - @override String get date_label => 'Fecha *'; - @override String get date_hint => 'mm/dd/aaaa'; - @override String get location_label => 'Ubicación *'; - @override String get location_hint => 'Dirección del negocio'; - @override String get positions_title => 'Posiciones'; - @override String get add_position => 'Añadir Posición'; - @override String get role_label => 'Rol *'; - @override String get role_hint => 'Seleccionar rol'; - @override String get start_time => 'Hora de Inicio *'; - @override String get end_time => 'Hora de Fin *'; - @override String get workers_needed => 'Trabajadores Necesarios *'; - @override String get hourly_rate => 'Tarifa por hora (\$) *'; - @override String get post_shift => 'Publicar Turno'; -} - -// Path: client_settings.profile -class _TranslationsClientSettingsProfileEs implements TranslationsClientSettingsProfileEn { - _TranslationsClientSettingsProfileEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String get title => 'Perfil'; - @override String get edit_profile => 'Editar Perfil'; - @override String get hubs => 'Hubs'; - @override String get log_out => 'Cerrar sesión'; - @override String get quick_links => 'Enlaces rápidos'; - @override String get clock_in_hubs => 'Hubs de Marcaje'; - @override String get billing_payments => 'Facturación y Pagos'; -} - -// Path: client_hubs.empty_state -class _TranslationsClientHubsEmptyStateEs implements TranslationsClientHubsEmptyStateEn { - _TranslationsClientHubsEmptyStateEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String get title => 'No hay hubs aún'; - @override String get description => 'Crea estaciones de marcaje para tus ubicaciones'; - @override String get button => 'Añade tu primer Hub'; -} - -// Path: client_hubs.about_hubs -class _TranslationsClientHubsAboutHubsEs implements TranslationsClientHubsAboutHubsEn { - _TranslationsClientHubsAboutHubsEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String get title => 'Sobre los Hubs'; - @override String get description => 'Los Hubs son estaciones de marcaje en tus ubicaciones. Asigna etiquetas NFC a cada hub para que los trabajadores puedan marcar entrada/salida rápidamente usando sus teléfonos.'; -} - -// Path: client_hubs.hub_card -class _TranslationsClientHubsHubCardEs implements TranslationsClientHubsHubCardEn { - _TranslationsClientHubsHubCardEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String tag_label({required Object id}) => 'Etiqueta: ${id}'; -} - -// Path: client_hubs.add_hub_dialog -class _TranslationsClientHubsAddHubDialogEs implements TranslationsClientHubsAddHubDialogEn { - _TranslationsClientHubsAddHubDialogEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String get title => 'Añadir Nuevo Hub'; - @override String get name_label => 'Nombre del Hub *'; - @override String get name_hint => 'ej., Cocina Principal, Recepción'; - @override String get location_label => 'Nombre de la Ubicación'; - @override String get location_hint => 'ej., Restaurante Centro'; - @override String get address_label => 'Dirección'; - @override String get address_hint => 'Dirección completa'; - @override String get create_button => 'Crear Hub'; -} - -// Path: client_hubs.nfc_dialog -class _TranslationsClientHubsNfcDialogEs implements TranslationsClientHubsNfcDialogEn { - _TranslationsClientHubsNfcDialogEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String get title => 'Identificar Etiqueta NFC'; - @override String get instruction => 'Acerque su teléfono a la etiqueta NFC para identificarla'; - @override String get scan_button => 'Escanear Etiqueta NFC'; - @override String get tag_identified => 'Etiqueta Identificada'; - @override String get assign_button => 'Asignar Etiqueta'; -} - -// Path: client_create_order.types -class _TranslationsClientCreateOrderTypesEs implements TranslationsClientCreateOrderTypesEn { - _TranslationsClientCreateOrderTypesEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String get rapid => 'RÁPIDO'; - @override String get rapid_desc => 'Cobertura URGENTE mismo día'; - @override String get one_time => 'Única Vez'; - @override String get one_time_desc => 'Evento Único o Petición de Turno'; - @override String get recurring => 'Recurrente'; - @override String get recurring_desc => 'Cobertura Continua Semanal / Mensual'; - @override String get permanent => 'Permanente'; - @override String get permanent_desc => 'Colocación de Personal a Largo Plazo'; -} - -// Path: client_create_order.rapid -class _TranslationsClientCreateOrderRapidEs implements TranslationsClientCreateOrderRapidEn { - _TranslationsClientCreateOrderRapidEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String get title => 'Orden RÁPIDA'; - @override String get subtitle => 'Personal de emergencia en minutos'; - @override String get urgent_badge => 'URGENTE'; - @override String get tell_us => 'Dinos qué necesitas'; - @override String get need_staff => '¿Necesitas personal urgentemente?'; - @override String get type_or_speak => 'Escribe o habla lo que necesitas. Yo me encargo del resto'; - @override String get example => 'Ejemplo: '; - @override String get hint => 'Escribe o habla... (ej., "Necesito 5 cocineros YA hasta las 5am")'; - @override String get speak => 'Hablar'; - @override String get listening => 'Escuchando...'; - @override String get send => 'Enviar Mensaje'; - @override String get sending => 'Enviando...'; - @override String get success_title => '¡Solicitud Enviada!'; - @override String get success_message => 'Estamos encontrando trabajadores disponibles para ti ahora mismo. Te notificaremos cuando acepten.'; - @override String get back_to_orders => 'Volver a Órdenes'; -} - -// Path: client_create_order.one_time -class _TranslationsClientCreateOrderOneTimeEs implements TranslationsClientCreateOrderOneTimeEn { - _TranslationsClientCreateOrderOneTimeEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String get title => 'Orden Única Vez'; - @override String get subtitle => 'Evento único o petición de turno'; - @override String get create_your_order => 'Crea Tu Orden'; - @override String get date_label => 'Fecha'; - @override String get date_hint => 'Seleccionar fecha'; - @override String get location_label => 'Ubicación'; - @override String get location_hint => 'Ingresar dirección'; - @override String get positions_title => 'Posiciones'; - @override String get add_position => 'Añadir Posición'; - @override String position_number({required Object number}) => 'Posición ${number}'; - @override String get remove => 'Eliminar'; - @override String get select_role => 'Seleccionar rol'; - @override String get start_label => 'Inicio'; - @override String get end_label => 'Fin'; - @override String get workers_label => 'Trabajadores'; - @override String get lunch_break_label => 'Descanso para Almuerzo'; - @override String get different_location => 'Usar ubicación diferente para esta posición'; - @override String get different_location_title => 'Ubicación Diferente'; - @override String get different_location_hint => 'Ingresar dirección diferente'; - @override String get create_order => 'Crear Orden'; - @override String get creating => 'Creando...'; - @override String get success_title => '¡Orden Creada!'; - @override String get success_message => 'Tu solicitud de turno ha sido publicada. Los trabajadores comenzarán a postularse pronto.'; - @override String get back_to_orders => 'Volver a Órdenes'; - @override String get no_break => 'Sin descanso'; - @override String get paid_break => 'min (Pagado)'; - @override String get unpaid_break => 'min (No pagado)'; -} - -// Path: client_create_order.recurring -class _TranslationsClientCreateOrderRecurringEs implements TranslationsClientCreateOrderRecurringEn { - _TranslationsClientCreateOrderRecurringEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String get title => 'Orden Recurrente'; - @override String get subtitle => 'Cobertura continua semanal/mensual'; - @override String get placeholder => 'Flujo de Orden Recurrente (Trabajo en Progreso)'; -} - -// Path: client_create_order.permanent -class _TranslationsClientCreateOrderPermanentEs implements TranslationsClientCreateOrderPermanentEn { - _TranslationsClientCreateOrderPermanentEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String get title => 'Orden Permanente'; - @override String get subtitle => 'Colocación de personal a largo plazo'; - @override String get placeholder => 'Flujo de Orden Permanente (Trabajo en Progreso)'; -} - -// Path: client_main.tabs -class _TranslationsClientMainTabsEs implements TranslationsClientMainTabsEn { - _TranslationsClientMainTabsEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String get coverage => 'Cobertura'; - @override String get billing => 'Facturación'; - @override String get home => 'Inicio'; - @override String get orders => 'Órdenes'; - @override String get reports => 'Reportes'; -} - -// Path: client_view_orders.tabs -class _TranslationsClientViewOrdersTabsEs implements TranslationsClientViewOrdersTabsEn { - _TranslationsClientViewOrdersTabsEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String get up_next => 'Próximos'; - @override String get active => 'Activos'; - @override String get completed => 'Completados'; -} - -// Path: client_view_orders.card -class _TranslationsClientViewOrdersCardEs implements TranslationsClientViewOrdersCardEn { - _TranslationsClientViewOrdersCardEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String get open => 'ABIERTO'; - @override String get filled => 'LLENO'; - @override String get confirmed => 'CONFIRMADO'; - @override String get in_progress => 'EN PROGRESO'; - @override String get completed => 'COMPLETADO'; - @override String get cancelled => 'CANCELADO'; - @override String get get_direction => 'Obtener dirección'; - @override String get total => 'Total'; - @override String get hrs => 'HRS'; - @override String workers({required Object count}) => '${count} trabajadores'; - @override String get clock_in => 'ENTRADA'; - @override String get clock_out => 'SALIDA'; - @override String get coverage => 'Cobertura'; - @override String workers_label({required Object filled, required Object needed}) => '${filled}/${needed} Trabajadores'; - @override String get confirmed_workers => 'Trabajadores Confirmados'; - @override String get no_workers => 'Ningún trabajador confirmado aún.'; -} - -// Path: staff.main -class _TranslationsStaffMainEs implements TranslationsStaffMainEn { - _TranslationsStaffMainEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override late final _TranslationsStaffMainTabsEs tabs = _TranslationsStaffMainTabsEs._(_root); -} - -// Path: staff.home -class _TranslationsStaffHomeEs implements TranslationsStaffHomeEn { - _TranslationsStaffHomeEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override late final _TranslationsStaffHomeHeaderEs header = _TranslationsStaffHomeHeaderEs._(_root); - @override late final _TranslationsStaffHomeBannersEs banners = _TranslationsStaffHomeBannersEs._(_root); - @override late final _TranslationsStaffHomeQuickActionsEs quick_actions = _TranslationsStaffHomeQuickActionsEs._(_root); - @override late final _TranslationsStaffHomeSectionsEs sections = _TranslationsStaffHomeSectionsEs._(_root); - @override late final _TranslationsStaffHomeEmptyStatesEs empty_states = _TranslationsStaffHomeEmptyStatesEs._(_root); - @override late final _TranslationsStaffHomePendingPaymentEs pending_payment = _TranslationsStaffHomePendingPaymentEs._(_root); - @override late final _TranslationsStaffHomeRecommendedCardEs recommended_card = _TranslationsStaffHomeRecommendedCardEs._(_root); - @override late final _TranslationsStaffHomeBenefitsEs benefits = _TranslationsStaffHomeBenefitsEs._(_root); - @override late final _TranslationsStaffHomeAutoMatchEs auto_match = _TranslationsStaffHomeAutoMatchEs._(_root); - @override late final _TranslationsStaffHomeImproveEs improve = _TranslationsStaffHomeImproveEs._(_root); - @override late final _TranslationsStaffHomeMoreWaysEs more_ways = _TranslationsStaffHomeMoreWaysEs._(_root); -} - -// Path: staff.profile -class _TranslationsStaffProfileEs implements TranslationsStaffProfileEn { - _TranslationsStaffProfileEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override late final _TranslationsStaffProfileHeaderEs header = _TranslationsStaffProfileHeaderEs._(_root); - @override late final _TranslationsStaffProfileReliabilityStatsEs reliability_stats = _TranslationsStaffProfileReliabilityStatsEs._(_root); - @override late final _TranslationsStaffProfileReliabilityScoreEs reliability_score = _TranslationsStaffProfileReliabilityScoreEs._(_root); - @override late final _TranslationsStaffProfileSectionsEs sections = _TranslationsStaffProfileSectionsEs._(_root); - @override late final _TranslationsStaffProfileMenuItemsEs menu_items = _TranslationsStaffProfileMenuItemsEs._(_root); - @override late final _TranslationsStaffProfileBankAccountPageEs bank_account_page = _TranslationsStaffProfileBankAccountPageEs._(_root); - @override late final _TranslationsStaffProfileLogoutEs logout = _TranslationsStaffProfileLogoutEs._(_root); -} - -// Path: staff.onboarding -class _TranslationsStaffOnboardingEs implements TranslationsStaffOnboardingEn { - _TranslationsStaffOnboardingEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override late final _TranslationsStaffOnboardingPersonalInfoEs personal_info = _TranslationsStaffOnboardingPersonalInfoEs._(_root); - @override late final _TranslationsStaffOnboardingExperienceEs experience = _TranslationsStaffOnboardingExperienceEs._(_root); -} - -// Path: staff_documents.verification_card -class _TranslationsStaffDocumentsVerificationCardEs implements TranslationsStaffDocumentsVerificationCardEn { - _TranslationsStaffDocumentsVerificationCardEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String get title => 'Document Verification'; - @override String progress({required Object completed, required Object total}) => '${completed}/${total} Complete'; -} - -// Path: staff_documents.list -class _TranslationsStaffDocumentsListEs implements TranslationsStaffDocumentsListEn { - _TranslationsStaffDocumentsListEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String get empty => 'No documents found'; - @override String error({required Object message}) => 'Error: ${message}'; -} - -// Path: staff_documents.card -class _TranslationsStaffDocumentsCardEs implements TranslationsStaffDocumentsCardEn { - _TranslationsStaffDocumentsCardEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String get view => 'View'; - @override String get upload => 'Upload'; - @override String get verified => 'Verified'; - @override String get pending => 'Pending'; - @override String get missing => 'Missing'; - @override String get rejected => 'Rejected'; -} - -// Path: staff_certificates.progress -class _TranslationsStaffCertificatesProgressEs implements TranslationsStaffCertificatesProgressEn { - _TranslationsStaffCertificatesProgressEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String get title => 'Your Progress'; - @override String verified_count({required Object completed, required Object total}) => '${completed} of ${total} verified'; - @override String get active => 'Compliance Active'; -} - -// Path: staff_certificates.card -class _TranslationsStaffCertificatesCardEs implements TranslationsStaffCertificatesCardEn { - _TranslationsStaffCertificatesCardEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String expires_in_days({required Object days}) => 'Expires in ${days} days - Renew now'; - @override String get expired => 'Expired - Renew now'; - @override String get verified => 'Verified'; - @override String get expiring_soon => 'Expiring Soon'; - @override String exp({required Object date}) => 'Exp: ${date}'; - @override String get upload_button => 'Upload Certificate'; - @override String get edit_expiry => 'Edit Expiration Date'; - @override String get remove => 'Remove Certificate'; - @override String get renew => 'Renew'; - @override String get opened_snackbar => 'Certificate opened in new tab'; -} - -// Path: staff_certificates.add_more -class _TranslationsStaffCertificatesAddMoreEs implements TranslationsStaffCertificatesAddMoreEn { - _TranslationsStaffCertificatesAddMoreEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String get title => 'Add Another Certificate'; - @override String get subtitle => 'Upload additional certifications'; -} - -// Path: staff_certificates.upload_modal -class _TranslationsStaffCertificatesUploadModalEs implements TranslationsStaffCertificatesUploadModalEn { - _TranslationsStaffCertificatesUploadModalEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String get title => 'Upload Certificate'; - @override String get expiry_label => 'Expiration Date (Optional)'; - @override String get select_date => 'Select date'; - @override String get upload_file => 'Upload File'; - @override String get drag_drop => 'Drag and drop or click to upload'; - @override String get supported_formats => 'PDF, JPG, PNG up to 10MB'; - @override String get cancel => 'Cancel'; - @override String get save => 'Save Certificate'; -} - -// Path: staff_certificates.delete_modal -class _TranslationsStaffCertificatesDeleteModalEs implements TranslationsStaffCertificatesDeleteModalEn { - _TranslationsStaffCertificatesDeleteModalEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String get title => 'Remove Certificate?'; - @override String get message => 'This action cannot be undone.'; - @override String get cancel => 'Cancel'; - @override String get confirm => 'Remove'; -} - -// Path: staff_profile_attire.info_card -class _TranslationsStaffProfileAttireInfoCardEs implements TranslationsStaffProfileAttireInfoCardEn { - _TranslationsStaffProfileAttireInfoCardEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String get title => 'Tu Vestuario'; - @override String get description => 'Selecciona los artículos de vestimenta que posees. Esto nos ayuda a asignarte turnos que se ajusten a tu vestuario.'; -} - -// Path: staff_profile_attire.status -class _TranslationsStaffProfileAttireStatusEs implements TranslationsStaffProfileAttireStatusEn { - _TranslationsStaffProfileAttireStatusEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String get required => 'REQUERIDO'; - @override String get add_photo => 'Añadir Foto'; - @override String get added => 'Añadido'; - @override String get pending => '⏳ Verificación pendiente'; -} - -// Path: staff_profile_attire.actions -class _TranslationsStaffProfileAttireActionsEs implements TranslationsStaffProfileAttireActionsEn { - _TranslationsStaffProfileAttireActionsEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String get save => 'Guardar Vestimenta'; -} - -// Path: staff_profile_attire.validation -class _TranslationsStaffProfileAttireValidationEs implements TranslationsStaffProfileAttireValidationEn { - _TranslationsStaffProfileAttireValidationEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String get select_required => '✓ Seleccionar todos los artículos requeridos'; - @override String get upload_required => '✓ Subir fotos de artículos requeridos'; - @override String get accept_attestation => '✓ Aceptar certificación'; -} - -// Path: staff_shifts.tabs -class _TranslationsStaffShiftsTabsEs implements TranslationsStaffShiftsTabsEn { - _TranslationsStaffShiftsTabsEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String get my_shifts => 'My Shifts'; - @override String get find_work => 'Find Work'; -} - -// Path: staff_shifts.list -class _TranslationsStaffShiftsListEs implements TranslationsStaffShiftsListEn { - _TranslationsStaffShiftsListEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String get no_shifts => 'No shifts found'; - @override String get pending_offers => 'PENDING OFFERS'; - @override String available_jobs({required Object count}) => '${count} AVAILABLE JOBS'; - @override String get search_hint => 'Search jobs...'; -} - -// Path: staff_shifts.filter -class _TranslationsStaffShiftsFilterEs implements TranslationsStaffShiftsFilterEn { - _TranslationsStaffShiftsFilterEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String get all => 'All Jobs'; - @override String get one_day => 'One Day'; - @override String get multi_day => 'Multi Day'; - @override String get long_term => 'Long Term'; -} - -// Path: staff_shifts.status -class _TranslationsStaffShiftsStatusEs implements TranslationsStaffShiftsStatusEn { - _TranslationsStaffShiftsStatusEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String get confirmed => 'CONFIRMED'; - @override String get act_now => 'ACT NOW'; - @override String get swap_requested => 'SWAP REQUESTED'; - @override String get completed => 'COMPLETED'; - @override String get no_show => 'NO SHOW'; - @override String get pending_warning => 'Please confirm assignment'; -} - -// Path: staff_shifts.action -class _TranslationsStaffShiftsActionEs implements TranslationsStaffShiftsActionEn { - _TranslationsStaffShiftsActionEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String get decline => 'Decline'; - @override String get confirm => 'Confirm'; - @override String get request_swap => 'Request Swap'; -} - -// Path: staff_shifts.details -class _TranslationsStaffShiftsDetailsEs implements TranslationsStaffShiftsDetailsEn { - _TranslationsStaffShiftsDetailsEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String get additional => 'ADDITIONAL DETAILS'; - @override String days({required Object days}) => '${days} Days'; - @override String exp_total({required Object amount}) => '(exp.total \$${amount})'; - @override String pending_time({required Object time}) => 'Pending ${time} ago'; -} - -// Path: staff_shifts.tags -class _TranslationsStaffShiftsTagsEs implements TranslationsStaffShiftsTagsEn { - _TranslationsStaffShiftsTagsEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String get immediate_start => 'Immediate start'; - @override String get no_experience => 'No experience'; -} - -// Path: staff_time_card.status -class _TranslationsStaffTimeCardStatusEs implements TranslationsStaffTimeCardStatusEn { - _TranslationsStaffTimeCardStatusEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String get approved => 'Aprobado'; - @override String get disputed => 'Disputado'; - @override String get paid => 'Pagado'; - @override String get pending => 'Pendiente'; -} - -// Path: staff_authentication.profile_setup_page.steps -class _TranslationsStaffAuthenticationProfileSetupPageStepsEs implements TranslationsStaffAuthenticationProfileSetupPageStepsEn { - _TranslationsStaffAuthenticationProfileSetupPageStepsEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String get basic => 'Información básica'; - @override String get location => 'Ubicación'; - @override String get experience => 'Experiencia'; -} - -// Path: staff_authentication.profile_setup_page.basic_info -class _TranslationsStaffAuthenticationProfileSetupPageBasicInfoEs implements TranslationsStaffAuthenticationProfileSetupPageBasicInfoEn { - _TranslationsStaffAuthenticationProfileSetupPageBasicInfoEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String get title => 'Conozcámonos'; - @override String get subtitle => 'Cuéntanos un poco sobre ti'; - @override String get full_name_label => 'Nombre completo *'; - @override String get full_name_hint => 'Juan Pérez'; - @override String get bio_label => 'Biografía corta'; - @override String get bio_hint => 'Profesional experimentado en hostelería...'; -} - -// Path: staff_authentication.profile_setup_page.location -class _TranslationsStaffAuthenticationProfileSetupPageLocationEs implements TranslationsStaffAuthenticationProfileSetupPageLocationEn { - _TranslationsStaffAuthenticationProfileSetupPageLocationEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String get title => '¿Dónde quieres trabajar?'; - @override String get subtitle => 'Agrega tus ubicaciones de trabajo preferidas'; - @override String get full_name_label => 'Nombre completo'; - @override String get add_location_label => 'Agregar ubicación *'; - @override String get add_location_hint => 'Ciudad o código postal'; - @override String get add_button => 'Agregar'; - @override String max_distance({required Object distance}) => 'Distancia máxima: ${distance} millas'; - @override String get min_dist_label => '5 mi'; - @override String get max_dist_label => '50 mi'; -} - -// Path: staff_authentication.profile_setup_page.experience -class _TranslationsStaffAuthenticationProfileSetupPageExperienceEs implements TranslationsStaffAuthenticationProfileSetupPageExperienceEn { - _TranslationsStaffAuthenticationProfileSetupPageExperienceEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String get title => '¿Cuáles son tus habilidades?'; - @override String get subtitle => 'Selecciona todas las que correspondan'; - @override String get skills_label => 'Habilidades *'; - @override String get industries_label => 'Industrias preferidas'; - @override late final _TranslationsStaffAuthenticationProfileSetupPageExperienceSkillsEs skills = _TranslationsStaffAuthenticationProfileSetupPageExperienceSkillsEs._(_root); - @override late final _TranslationsStaffAuthenticationProfileSetupPageExperienceIndustriesEs industries = _TranslationsStaffAuthenticationProfileSetupPageExperienceIndustriesEs._(_root); -} - -// Path: staff.main.tabs -class _TranslationsStaffMainTabsEs implements TranslationsStaffMainTabsEn { - _TranslationsStaffMainTabsEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String get shifts => 'Turnos'; - @override String get payments => 'Pagos'; - @override String get home => 'Inicio'; - @override String get clock_in => 'Marcar Entrada'; - @override String get profile => 'Perfil'; -} - -// Path: staff.home.header -class _TranslationsStaffHomeHeaderEs implements TranslationsStaffHomeHeaderEn { - _TranslationsStaffHomeHeaderEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String get welcome_back => 'Welcome back'; - @override String get user_name_placeholder => 'Krower'; -} - -// Path: staff.home.banners -class _TranslationsStaffHomeBannersEs implements TranslationsStaffHomeBannersEn { - _TranslationsStaffHomeBannersEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String get complete_profile_title => 'Complete Your Profile'; - @override String get complete_profile_subtitle => 'Get verified to see more shifts'; - @override String get availability_title => 'Availability'; - @override String get availability_subtitle => 'Update your availability for next week'; -} - -// Path: staff.home.quick_actions -class _TranslationsStaffHomeQuickActionsEs implements TranslationsStaffHomeQuickActionsEn { - _TranslationsStaffHomeQuickActionsEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String get find_shifts => 'Find Shifts'; - @override String get availability => 'Availability'; - @override String get messages => 'Messages'; - @override String get earnings => 'Earnings'; -} - -// Path: staff.home.sections -class _TranslationsStaffHomeSectionsEs implements TranslationsStaffHomeSectionsEn { - _TranslationsStaffHomeSectionsEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String get todays_shift => 'Today\'s Shift'; - @override String scheduled_count({required Object count}) => '${count} scheduled'; - @override String get tomorrow => 'Tomorrow'; - @override String get recommended_for_you => 'Recommended for You'; - @override String get view_all => 'View all'; -} - -// Path: staff.home.empty_states -class _TranslationsStaffHomeEmptyStatesEs implements TranslationsStaffHomeEmptyStatesEn { - _TranslationsStaffHomeEmptyStatesEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String get no_shifts_today => 'No shifts scheduled for today'; - @override String get find_shifts_cta => 'Find shifts →'; - @override String get no_shifts_tomorrow => 'No shifts for tomorrow'; - @override String get no_recommended_shifts => 'No recommended shifts'; -} - -// Path: staff.home.pending_payment -class _TranslationsStaffHomePendingPaymentEs implements TranslationsStaffHomePendingPaymentEn { - _TranslationsStaffHomePendingPaymentEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String get title => 'Pending Payment'; - @override String get subtitle => 'Payment processing'; - @override String amount({required Object amount}) => '${amount}'; -} - -// Path: staff.home.recommended_card -class _TranslationsStaffHomeRecommendedCardEs implements TranslationsStaffHomeRecommendedCardEn { - _TranslationsStaffHomeRecommendedCardEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String get act_now => '• ACT NOW'; - @override String get one_day => 'One Day'; - @override String get today => 'Today'; - @override String applied_for({required Object title}) => 'Applied for ${title}'; - @override String time_range({required Object start, required Object end}) => '${start} - ${end}'; -} - -// Path: staff.home.benefits -class _TranslationsStaffHomeBenefitsEs implements TranslationsStaffHomeBenefitsEn { - _TranslationsStaffHomeBenefitsEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String get title => 'Your Benefits'; - @override String get view_all => 'View all'; - @override String get hours_label => 'hours'; - @override late final _TranslationsStaffHomeBenefitsItemsEs items = _TranslationsStaffHomeBenefitsItemsEs._(_root); -} - -// Path: staff.home.auto_match -class _TranslationsStaffHomeAutoMatchEs implements TranslationsStaffHomeAutoMatchEn { - _TranslationsStaffHomeAutoMatchEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String get title => 'Auto-Match'; - @override String get finding_shifts => 'Finding shifts for you'; - @override String get get_matched => 'Get matched automatically'; - @override String get matching_based_on => 'Matching based on:'; - @override late final _TranslationsStaffHomeAutoMatchChipsEs chips = _TranslationsStaffHomeAutoMatchChipsEs._(_root); -} - -// Path: staff.home.improve -class _TranslationsStaffHomeImproveEs implements TranslationsStaffHomeImproveEn { - _TranslationsStaffHomeImproveEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String get title => 'Improve Yourself'; - @override late final _TranslationsStaffHomeImproveItemsEs items = _TranslationsStaffHomeImproveItemsEs._(_root); -} - -// Path: staff.home.more_ways -class _TranslationsStaffHomeMoreWaysEs implements TranslationsStaffHomeMoreWaysEn { - _TranslationsStaffHomeMoreWaysEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String get title => 'More Ways To Use Krow'; - @override late final _TranslationsStaffHomeMoreWaysItemsEs items = _TranslationsStaffHomeMoreWaysItemsEs._(_root); -} - -// Path: staff.profile.header -class _TranslationsStaffProfileHeaderEs implements TranslationsStaffProfileHeaderEn { - _TranslationsStaffProfileHeaderEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String get title => 'Perfil'; - @override String get sign_out => 'CERRAR SESIÓN'; -} - -// Path: staff.profile.reliability_stats -class _TranslationsStaffProfileReliabilityStatsEs implements TranslationsStaffProfileReliabilityStatsEn { - _TranslationsStaffProfileReliabilityStatsEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String get shifts => 'Turnos'; - @override String get rating => 'Calificación'; - @override String get on_time => 'A Tiempo'; - @override String get no_shows => 'Faltas'; - @override String get cancellations => 'Cancel.'; -} - -// Path: staff.profile.reliability_score -class _TranslationsStaffProfileReliabilityScoreEs implements TranslationsStaffProfileReliabilityScoreEn { - _TranslationsStaffProfileReliabilityScoreEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String get title => 'Puntuación de Confiabilidad'; - @override String get description => 'Mantén tu puntuación por encima del 45% para continuar aceptando turnos.'; -} - -// Path: staff.profile.sections -class _TranslationsStaffProfileSectionsEs implements TranslationsStaffProfileSectionsEn { - _TranslationsStaffProfileSectionsEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String get onboarding => 'INCORPORACIÓN'; - @override String get compliance => 'CUMPLIMIENTO'; - @override String get level_up => 'MEJORAR NIVEL'; - @override String get finance => 'FINANZAS'; - @override String get support => 'SOPORTE'; -} - -// Path: staff.profile.menu_items -class _TranslationsStaffProfileMenuItemsEs implements TranslationsStaffProfileMenuItemsEn { - _TranslationsStaffProfileMenuItemsEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String get personal_info => 'Información Personal'; - @override String get emergency_contact => 'Contacto de Emergencia'; - @override String get experience => 'Experiencia'; - @override String get attire => 'Vestimenta'; - @override String get documents => 'Documentos'; - @override String get certificates => 'Certificados'; - @override String get tax_forms => 'Formularios Fiscales'; - @override String get krow_university => 'Krow University'; - @override String get trainings => 'Capacitaciones'; - @override String get leaderboard => 'Tabla de Clasificación'; - @override String get bank_account => 'Cuenta Bancaria'; - @override String get payments => 'Pagos'; - @override String get timecard => 'Tarjeta de Tiempo'; - @override String get faqs => 'Preguntas Frecuentes'; - @override String get privacy_security => 'Privacidad y Seguridad'; - @override String get messages => 'Mensajes'; -} - -// Path: staff.profile.bank_account_page -class _TranslationsStaffProfileBankAccountPageEs implements TranslationsStaffProfileBankAccountPageEn { - _TranslationsStaffProfileBankAccountPageEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String get title => 'Cuenta Bancaria'; - @override String get linked_accounts => 'Cuentas Vinculadas'; - @override String get add_account => 'Agregar Cuenta Bancaria'; - @override String get secure_title => 'Seguro y Cifrado'; - @override String get secure_subtitle => 'Su información bancaria está cifrada y almacenada de forma segura. Nunca compartimos sus detalles.'; - @override String get add_new_account => 'Agregar Nueva Cuenta'; - @override String get routing_number => 'Número de Ruta'; - @override String get routing_hint => '9 dígitos'; - @override String get account_number => 'Número de Cuenta'; - @override String get account_hint => 'Ingrese número de cuenta'; - @override String get account_type => 'Tipo de Cuenta'; - @override String get checking => 'CORRIENTE'; - @override String get savings => 'AHORROS'; - @override String get cancel => 'Cancelar'; - @override String get save => 'Guardar'; - @override String get primary => 'Principal'; - @override String account_ending({required Object last4}) => 'Termina en ${last4}'; -} - -// Path: staff.profile.logout -class _TranslationsStaffProfileLogoutEs implements TranslationsStaffProfileLogoutEn { - _TranslationsStaffProfileLogoutEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String get button => 'Cerrar Sesión'; -} - -// Path: staff.onboarding.personal_info -class _TranslationsStaffOnboardingPersonalInfoEs implements TranslationsStaffOnboardingPersonalInfoEn { - _TranslationsStaffOnboardingPersonalInfoEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String get title => 'Información Personal'; - @override String get change_photo_hint => 'Toca para cambiar foto'; - @override String get full_name_label => 'Nombre Completo'; - @override String get email_label => 'Correo Electrónico'; - @override String get phone_label => 'Número de Teléfono'; - @override String get phone_hint => '+1 (555) 000-0000'; - @override String get bio_label => 'Biografía'; - @override String get bio_hint => 'Cuéntales a los clientes sobre ti...'; - @override String get languages_label => 'Idiomas'; - @override String get languages_hint => 'Inglés, Español, Francés...'; - @override String get locations_label => 'Ubicaciones Preferidas'; - @override String get locations_hint => 'Centro, Midtown, Brooklyn...'; - @override String get save_button => 'Guardar Cambios'; - @override String get save_success => 'Información personal guardada exitosamente'; -} - -// Path: staff.onboarding.experience -class _TranslationsStaffOnboardingExperienceEs implements TranslationsStaffOnboardingExperienceEn { - _TranslationsStaffOnboardingExperienceEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String get title => 'Experience & Skills'; - @override String get industries_title => 'Industries'; - @override String get industries_subtitle => 'Select the industries you have experience in'; - @override String get skills_title => 'Skills'; - @override String get skills_subtitle => 'Select your skills or add custom ones'; - @override String get custom_skills_title => 'Custom Skills:'; - @override String get custom_skill_hint => 'Add custom skill...'; - @override String get save_button => 'Save & Continue'; - @override late final _TranslationsStaffOnboardingExperienceIndustriesEs industries = _TranslationsStaffOnboardingExperienceIndustriesEs._(_root); - @override late final _TranslationsStaffOnboardingExperienceSkillsEs skills = _TranslationsStaffOnboardingExperienceSkillsEs._(_root); -} - -// Path: staff_authentication.profile_setup_page.experience.skills -class _TranslationsStaffAuthenticationProfileSetupPageExperienceSkillsEs implements TranslationsStaffAuthenticationProfileSetupPageExperienceSkillsEn { - _TranslationsStaffAuthenticationProfileSetupPageExperienceSkillsEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String get food_service => 'Servicio de comida'; - @override String get bartending => 'Preparación de bebidas'; - @override String get warehouse => 'Almacén'; - @override String get retail => 'Venta minorista'; - @override String get events => 'Eventos'; - @override String get customer_service => 'Servicio al cliente'; - @override String get cleaning => 'Limpieza'; - @override String get security => 'Seguridad'; - @override String get driving => 'Conducción'; - @override String get cooking => 'Cocina'; - @override String get cashier => 'Cajero'; - @override String get server => 'Mesero'; - @override String get barista => 'Barista'; - @override String get host_hostess => 'Anfitrión'; - @override String get busser => 'Ayudante de mesero'; -} - -// Path: staff_authentication.profile_setup_page.experience.industries -class _TranslationsStaffAuthenticationProfileSetupPageExperienceIndustriesEs implements TranslationsStaffAuthenticationProfileSetupPageExperienceIndustriesEn { - _TranslationsStaffAuthenticationProfileSetupPageExperienceIndustriesEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String get hospitality => 'Hostelería'; - @override String get food_service => 'Servicio de comida'; - @override String get warehouse => 'Almacén'; - @override String get events => 'Eventos'; - @override String get retail => 'Venta minorista'; - @override String get healthcare => 'Atención médica'; -} - -// Path: staff.home.benefits.items -class _TranslationsStaffHomeBenefitsItemsEs implements TranslationsStaffHomeBenefitsItemsEn { - _TranslationsStaffHomeBenefitsItemsEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String get sick_days => 'Sick Days'; - @override String get vacation => 'Vacation'; - @override String get holidays => 'Holidays'; -} - -// Path: staff.home.auto_match.chips -class _TranslationsStaffHomeAutoMatchChipsEs implements TranslationsStaffHomeAutoMatchChipsEn { - _TranslationsStaffHomeAutoMatchChipsEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String get location => 'Location'; - @override String get availability => 'Availability'; - @override String get skills => 'Skills'; -} - -// Path: staff.home.improve.items -class _TranslationsStaffHomeImproveItemsEs implements TranslationsStaffHomeImproveItemsEn { - _TranslationsStaffHomeImproveItemsEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override late final _TranslationsStaffHomeImproveItemsTrainingEs training = _TranslationsStaffHomeImproveItemsTrainingEs._(_root); - @override late final _TranslationsStaffHomeImproveItemsPodcastEs podcast = _TranslationsStaffHomeImproveItemsPodcastEs._(_root); -} - -// Path: staff.home.more_ways.items -class _TranslationsStaffHomeMoreWaysItemsEs implements TranslationsStaffHomeMoreWaysItemsEn { - _TranslationsStaffHomeMoreWaysItemsEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override late final _TranslationsStaffHomeMoreWaysItemsBenefitsEs benefits = _TranslationsStaffHomeMoreWaysItemsBenefitsEs._(_root); - @override late final _TranslationsStaffHomeMoreWaysItemsReferEs refer = _TranslationsStaffHomeMoreWaysItemsReferEs._(_root); -} - -// Path: staff.onboarding.experience.industries -class _TranslationsStaffOnboardingExperienceIndustriesEs implements TranslationsStaffOnboardingExperienceIndustriesEn { - _TranslationsStaffOnboardingExperienceIndustriesEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String get hospitality => 'Hospitality'; - @override String get food_service => 'Food Service'; - @override String get warehouse => 'Warehouse'; - @override String get events => 'Events'; - @override String get retail => 'Retail'; - @override String get healthcare => 'Healthcare'; - @override String get other => 'Other'; -} - -// Path: staff.onboarding.experience.skills -class _TranslationsStaffOnboardingExperienceSkillsEs implements TranslationsStaffOnboardingExperienceSkillsEn { - _TranslationsStaffOnboardingExperienceSkillsEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String get food_service => 'Food Service'; - @override String get bartending => 'Bartending'; - @override String get event_setup => 'Event Setup'; - @override String get hospitality => 'Hospitality'; - @override String get warehouse => 'Warehouse'; - @override String get customer_service => 'Customer Service'; - @override String get cleaning => 'Cleaning'; - @override String get security => 'Security'; - @override String get retail => 'Retail'; - @override String get cooking => 'Cooking'; - @override String get cashier => 'Cashier'; - @override String get server => 'Server'; - @override String get barista => 'Barista'; - @override String get host_hostess => 'Host/Hostess'; - @override String get busser => 'Busser'; - @override String get driving => 'Driving'; -} - -// Path: staff.home.improve.items.training -class _TranslationsStaffHomeImproveItemsTrainingEs implements TranslationsStaffHomeImproveItemsTrainingEn { - _TranslationsStaffHomeImproveItemsTrainingEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String get title => 'Training Section'; - @override String get description => 'Improve your skills and get certified.'; - @override String get page => '/krow-university'; -} - -// Path: staff.home.improve.items.podcast -class _TranslationsStaffHomeImproveItemsPodcastEs implements TranslationsStaffHomeImproveItemsPodcastEn { - _TranslationsStaffHomeImproveItemsPodcastEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String get title => 'Krow Podcast'; - @override String get description => 'Listen to tips from top workers.'; - @override String get page => '/krow-university'; -} - -// Path: staff.home.more_ways.items.benefits -class _TranslationsStaffHomeMoreWaysItemsBenefitsEs implements TranslationsStaffHomeMoreWaysItemsBenefitsEn { - _TranslationsStaffHomeMoreWaysItemsBenefitsEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String get title => 'Krow Benefits'; - @override String get page => '/benefits'; -} - -// Path: staff.home.more_ways.items.refer -class _TranslationsStaffHomeMoreWaysItemsReferEs implements TranslationsStaffHomeMoreWaysItemsReferEn { - _TranslationsStaffHomeMoreWaysItemsReferEs._(this._root); - - final TranslationsEs _root; // ignore: unused_field - - // Translations - @override String get title => 'Refer a Friend'; - @override String get page => '/worker-profile'; -} - -/// The flat map containing all translations for locale . -/// Only for edge cases! For simple maps, use the map function of this library. -/// -/// The Dart AOT compiler has issues with very large switch statements, -/// so the map is split into smaller functions (512 entries each). -extension on TranslationsEs { - dynamic _flatMapFunction(String path) { - return switch (path) { - 'common.ok' => 'Aceptar', - 'common.cancel' => 'Cancelar', - 'common.save' => 'Guardar', - 'common.delete' => 'Eliminar', - 'common.continue_text' => 'Continuar', - 'settings.language' => 'Idioma', - 'settings.change_language' => 'Cambiar Idioma', - 'staff_authentication.get_started_page.title_part1' => 'Trabaja, Crece, ', - 'staff_authentication.get_started_page.title_part2' => 'Elévate', - 'staff_authentication.get_started_page.subtitle' => 'Construye tu carrera en hostelería con \nflexibilidad y libertad.', - 'staff_authentication.get_started_page.sign_up_button' => 'Registrarse', - 'staff_authentication.get_started_page.log_in_button' => 'Iniciar sesión', - 'staff_authentication.phone_verification_page.validation_error' => 'Por favor, ingresa un número de teléfono válido de 10 dígitos', - 'staff_authentication.phone_verification_page.send_code_button' => 'Enviar código', - 'staff_authentication.phone_verification_page.enter_code_title' => 'Ingresa el código de verificación', - 'staff_authentication.phone_verification_page.code_sent_message' => 'Enviamos un código de 6 dígitos a ', - 'staff_authentication.phone_verification_page.code_sent_instruction' => '. Ingrésalo a continuación para verificar tu cuenta.', - 'staff_authentication.phone_input.title' => 'Verifica tu número de teléfono', - 'staff_authentication.phone_input.subtitle' => 'Te enviaremos un código de verificación para comenzar.', - 'staff_authentication.phone_input.label' => 'Número de teléfono', - 'staff_authentication.phone_input.hint' => 'Ingresa tu número', - 'staff_authentication.otp_verification.did_not_get_code' => '¿No recibiste el código?', - 'staff_authentication.otp_verification.resend_in' => ({required Object seconds}) => 'Reenviar en ${seconds} s', - 'staff_authentication.otp_verification.resend_code' => 'Reenviar código', - 'staff_authentication.profile_setup_page.step_indicator' => ({required Object current, required Object total}) => 'Paso ${current} de ${total}', - 'staff_authentication.profile_setup_page.error_occurred' => 'Ocurrió un error', - 'staff_authentication.profile_setup_page.complete_setup_button' => 'Completar configuración', - 'staff_authentication.profile_setup_page.steps.basic' => 'Información básica', - 'staff_authentication.profile_setup_page.steps.location' => 'Ubicación', - 'staff_authentication.profile_setup_page.steps.experience' => 'Experiencia', - 'staff_authentication.profile_setup_page.basic_info.title' => 'Conozcámonos', - 'staff_authentication.profile_setup_page.basic_info.subtitle' => 'Cuéntanos un poco sobre ti', - 'staff_authentication.profile_setup_page.basic_info.full_name_label' => 'Nombre completo *', - 'staff_authentication.profile_setup_page.basic_info.full_name_hint' => 'Juan Pérez', - 'staff_authentication.profile_setup_page.basic_info.bio_label' => 'Biografía corta', - 'staff_authentication.profile_setup_page.basic_info.bio_hint' => 'Profesional experimentado en hostelería...', - 'staff_authentication.profile_setup_page.location.title' => '¿Dónde quieres trabajar?', - 'staff_authentication.profile_setup_page.location.subtitle' => 'Agrega tus ubicaciones de trabajo preferidas', - 'staff_authentication.profile_setup_page.location.full_name_label' => 'Nombre completo', - 'staff_authentication.profile_setup_page.location.add_location_label' => 'Agregar ubicación *', - 'staff_authentication.profile_setup_page.location.add_location_hint' => 'Ciudad o código postal', - 'staff_authentication.profile_setup_page.location.add_button' => 'Agregar', - 'staff_authentication.profile_setup_page.location.max_distance' => ({required Object distance}) => 'Distancia máxima: ${distance} millas', - 'staff_authentication.profile_setup_page.location.min_dist_label' => '5 mi', - 'staff_authentication.profile_setup_page.location.max_dist_label' => '50 mi', - 'staff_authentication.profile_setup_page.experience.title' => '¿Cuáles son tus habilidades?', - 'staff_authentication.profile_setup_page.experience.subtitle' => 'Selecciona todas las que correspondan', - 'staff_authentication.profile_setup_page.experience.skills_label' => 'Habilidades *', - 'staff_authentication.profile_setup_page.experience.industries_label' => 'Industrias preferidas', - 'staff_authentication.profile_setup_page.experience.skills.food_service' => 'Servicio de comida', - 'staff_authentication.profile_setup_page.experience.skills.bartending' => 'Preparación de bebidas', - 'staff_authentication.profile_setup_page.experience.skills.warehouse' => 'Almacén', - 'staff_authentication.profile_setup_page.experience.skills.retail' => 'Venta minorista', - 'staff_authentication.profile_setup_page.experience.skills.events' => 'Eventos', - 'staff_authentication.profile_setup_page.experience.skills.customer_service' => 'Servicio al cliente', - 'staff_authentication.profile_setup_page.experience.skills.cleaning' => 'Limpieza', - 'staff_authentication.profile_setup_page.experience.skills.security' => 'Seguridad', - 'staff_authentication.profile_setup_page.experience.skills.driving' => 'Conducción', - 'staff_authentication.profile_setup_page.experience.skills.cooking' => 'Cocina', - 'staff_authentication.profile_setup_page.experience.skills.cashier' => 'Cajero', - 'staff_authentication.profile_setup_page.experience.skills.server' => 'Mesero', - 'staff_authentication.profile_setup_page.experience.skills.barista' => 'Barista', - 'staff_authentication.profile_setup_page.experience.skills.host_hostess' => 'Anfitrión', - 'staff_authentication.profile_setup_page.experience.skills.busser' => 'Ayudante de mesero', - 'staff_authentication.profile_setup_page.experience.industries.hospitality' => 'Hostelería', - 'staff_authentication.profile_setup_page.experience.industries.food_service' => 'Servicio de comida', - 'staff_authentication.profile_setup_page.experience.industries.warehouse' => 'Almacén', - 'staff_authentication.profile_setup_page.experience.industries.events' => 'Eventos', - 'staff_authentication.profile_setup_page.experience.industries.retail' => 'Venta minorista', - 'staff_authentication.profile_setup_page.experience.industries.healthcare' => 'Atención médica', - 'staff_authentication.common.trouble_question' => '¿Tienes problemas? ', - 'staff_authentication.common.contact_support' => 'Contactar a soporte', - 'client_authentication.get_started_page.title' => 'Toma el control de tus\nturnos y eventos', - 'client_authentication.get_started_page.subtitle' => 'Optimiza tus operaciones con potentes herramientas para gestionar horarios, realizar un seguimiento del rendimiento y mantener a tu equipo en la misma página, todo en un solo lugar', - 'client_authentication.get_started_page.sign_in_button' => 'Iniciar sesión', - 'client_authentication.get_started_page.create_account_button' => 'Crear cuenta', - 'client_authentication.sign_in_page.title' => 'Bienvenido de nuevo', - 'client_authentication.sign_in_page.subtitle' => 'Inicia sesión para gestionar tus turnos y trabajadores', - 'client_authentication.sign_in_page.email_label' => 'Correo electrónico', - 'client_authentication.sign_in_page.email_hint' => 'Ingresa tu correo electrónico', - 'client_authentication.sign_in_page.password_label' => 'Contraseña', - 'client_authentication.sign_in_page.password_hint' => 'Ingresa tu contraseña', - 'client_authentication.sign_in_page.forgot_password' => '¿Olvidaste tu contraseña?', - 'client_authentication.sign_in_page.sign_in_button' => 'Iniciar sesión', - 'client_authentication.sign_in_page.or_divider' => 'o', - 'client_authentication.sign_in_page.social_apple' => 'Iniciar sesión con Apple', - 'client_authentication.sign_in_page.social_google' => 'Iniciar sesión con Google', - 'client_authentication.sign_in_page.no_account' => '¿No tienes una cuenta? ', - 'client_authentication.sign_in_page.sign_up_link' => 'Regístrate', - 'client_authentication.sign_up_page.title' => 'Crear cuenta', - 'client_authentication.sign_up_page.subtitle' => 'Comienza con Krow para tu negocio', - 'client_authentication.sign_up_page.company_label' => 'Nombre de la empresa', - 'client_authentication.sign_up_page.company_hint' => 'Ingresa el nombre de la empresa', - 'client_authentication.sign_up_page.email_label' => 'Correo electrónico', - 'client_authentication.sign_up_page.email_hint' => 'Ingresa tu correo electrónico', - 'client_authentication.sign_up_page.password_label' => 'Contraseña', - 'client_authentication.sign_up_page.password_hint' => 'Crea una contraseña', - 'client_authentication.sign_up_page.confirm_password_label' => 'Confirmar contraseña', - 'client_authentication.sign_up_page.confirm_password_hint' => 'Confirma tu contraseña', - 'client_authentication.sign_up_page.create_account_button' => 'Crear cuenta', - 'client_authentication.sign_up_page.or_divider' => 'o', - 'client_authentication.sign_up_page.social_apple' => 'Regístrate con Apple', - 'client_authentication.sign_up_page.social_google' => 'Regístrate con Google', - 'client_authentication.sign_up_page.has_account' => '¿Ya tienes una cuenta? ', - 'client_authentication.sign_up_page.sign_in_link' => 'Iniciar sesión', - 'client_home.dashboard.welcome_back' => 'Bienvenido de nuevo', - 'client_home.dashboard.edit_mode_active' => 'Modo Edición Activo', - 'client_home.dashboard.drag_instruction' => 'Arrastra para reordenar, cambia la visibilidad', - 'client_home.dashboard.reset' => 'Restablecer', - 'client_home.dashboard.metric_needed' => 'Necesario', - 'client_home.dashboard.metric_filled' => 'Lleno', - 'client_home.dashboard.metric_open' => 'Abierto', - 'client_home.dashboard.view_all' => 'Ver todo', - 'client_home.dashboard.insight_lightbulb' => ({required Object amount}) => 'Ahorra ${amount}/mes', - 'client_home.dashboard.insight_tip' => 'Reserva con 48h de antelación para mejores tarifas', - 'client_home.widgets.actions' => 'Acciones Rápidas', - 'client_home.widgets.reorder' => 'Reordenar', - 'client_home.widgets.coverage' => 'Cobertura de Hoy', - 'client_home.widgets.spending' => 'Información de Gastos', - 'client_home.widgets.live_activity' => 'Actividad en Vivo', - 'client_home.actions.rapid' => 'RÁPIDO', - 'client_home.actions.rapid_subtitle' => 'Urgente mismo día', - 'client_home.actions.create_order' => 'Crear Orden', - 'client_home.actions.create_order_subtitle' => 'Programar turnos', - 'client_home.actions.hubs' => 'Hubs', - 'client_home.actions.hubs_subtitle' => 'Puntos marcaje', - 'client_home.reorder.title' => 'REORDENAR', - 'client_home.reorder.reorder_button' => 'Reordenar', - 'client_home.reorder.per_hr' => ({required Object amount}) => '${amount}/hr', - 'client_home.form.edit_reorder' => 'Editar y Reordenar', - 'client_home.form.post_new' => 'Publicar un Nuevo Turno', - 'client_home.form.review_subtitle' => 'Revisa y edita los detalles antes de publicar', - 'client_home.form.date_label' => 'Fecha *', - 'client_home.form.date_hint' => 'mm/dd/aaaa', - 'client_home.form.location_label' => 'Ubicación *', - 'client_home.form.location_hint' => 'Dirección del negocio', - 'client_home.form.positions_title' => 'Posiciones', - 'client_home.form.add_position' => 'Añadir Posición', - 'client_home.form.role_label' => 'Rol *', - 'client_home.form.role_hint' => 'Seleccionar rol', - 'client_home.form.start_time' => 'Hora de Inicio *', - 'client_home.form.end_time' => 'Hora de Fin *', - 'client_home.form.workers_needed' => 'Trabajadores Necesarios *', - 'client_home.form.hourly_rate' => 'Tarifa por hora (\$) *', - 'client_home.form.post_shift' => 'Publicar Turno', - 'client_settings.profile.title' => 'Perfil', - 'client_settings.profile.edit_profile' => 'Editar Perfil', - 'client_settings.profile.hubs' => 'Hubs', - 'client_settings.profile.log_out' => 'Cerrar sesión', - 'client_settings.profile.quick_links' => 'Enlaces rápidos', - 'client_settings.profile.clock_in_hubs' => 'Hubs de Marcaje', - 'client_settings.profile.billing_payments' => 'Facturación y Pagos', - 'client_hubs.title' => 'Hubs', - 'client_hubs.subtitle' => 'Gestionar ubicaciones de marcaje', - 'client_hubs.add_hub' => 'Añadir Hub', - 'client_hubs.empty_state.title' => 'No hay hubs aún', - 'client_hubs.empty_state.description' => 'Crea estaciones de marcaje para tus ubicaciones', - 'client_hubs.empty_state.button' => 'Añade tu primer Hub', - 'client_hubs.about_hubs.title' => 'Sobre los Hubs', - 'client_hubs.about_hubs.description' => 'Los Hubs son estaciones de marcaje en tus ubicaciones. Asigna etiquetas NFC a cada hub para que los trabajadores puedan marcar entrada/salida rápidamente usando sus teléfonos.', - 'client_hubs.hub_card.tag_label' => ({required Object id}) => 'Etiqueta: ${id}', - 'client_hubs.add_hub_dialog.title' => 'Añadir Nuevo Hub', - 'client_hubs.add_hub_dialog.name_label' => 'Nombre del Hub *', - 'client_hubs.add_hub_dialog.name_hint' => 'ej., Cocina Principal, Recepción', - 'client_hubs.add_hub_dialog.location_label' => 'Nombre de la Ubicación', - 'client_hubs.add_hub_dialog.location_hint' => 'ej., Restaurante Centro', - 'client_hubs.add_hub_dialog.address_label' => 'Dirección', - 'client_hubs.add_hub_dialog.address_hint' => 'Dirección completa', - 'client_hubs.add_hub_dialog.create_button' => 'Crear Hub', - 'client_hubs.nfc_dialog.title' => 'Identificar Etiqueta NFC', - 'client_hubs.nfc_dialog.instruction' => 'Acerque su teléfono a la etiqueta NFC para identificarla', - 'client_hubs.nfc_dialog.scan_button' => 'Escanear Etiqueta NFC', - 'client_hubs.nfc_dialog.tag_identified' => 'Etiqueta Identificada', - 'client_hubs.nfc_dialog.assign_button' => 'Asignar Etiqueta', - 'client_create_order.title' => 'Crear Orden', - 'client_create_order.section_title' => 'TIPO DE ORDEN', - 'client_create_order.types.rapid' => 'RÁPIDO', - 'client_create_order.types.rapid_desc' => 'Cobertura URGENTE mismo día', - 'client_create_order.types.one_time' => 'Única Vez', - 'client_create_order.types.one_time_desc' => 'Evento Único o Petición de Turno', - 'client_create_order.types.recurring' => 'Recurrente', - 'client_create_order.types.recurring_desc' => 'Cobertura Continua Semanal / Mensual', - 'client_create_order.types.permanent' => 'Permanente', - 'client_create_order.types.permanent_desc' => 'Colocación de Personal a Largo Plazo', - 'client_create_order.rapid.title' => 'Orden RÁPIDA', - 'client_create_order.rapid.subtitle' => 'Personal de emergencia en minutos', - 'client_create_order.rapid.urgent_badge' => 'URGENTE', - 'client_create_order.rapid.tell_us' => 'Dinos qué necesitas', - 'client_create_order.rapid.need_staff' => '¿Necesitas personal urgentemente?', - 'client_create_order.rapid.type_or_speak' => 'Escribe o habla lo que necesitas. Yo me encargo del resto', - 'client_create_order.rapid.example' => 'Ejemplo: ', - 'client_create_order.rapid.hint' => 'Escribe o habla... (ej., "Necesito 5 cocineros YA hasta las 5am")', - 'client_create_order.rapid.speak' => 'Hablar', - 'client_create_order.rapid.listening' => 'Escuchando...', - 'client_create_order.rapid.send' => 'Enviar Mensaje', - 'client_create_order.rapid.sending' => 'Enviando...', - 'client_create_order.rapid.success_title' => '¡Solicitud Enviada!', - 'client_create_order.rapid.success_message' => 'Estamos encontrando trabajadores disponibles para ti ahora mismo. Te notificaremos cuando acepten.', - 'client_create_order.rapid.back_to_orders' => 'Volver a Órdenes', - 'client_create_order.one_time.title' => 'Orden Única Vez', - 'client_create_order.one_time.subtitle' => 'Evento único o petición de turno', - 'client_create_order.one_time.create_your_order' => 'Crea Tu Orden', - 'client_create_order.one_time.date_label' => 'Fecha', - 'client_create_order.one_time.date_hint' => 'Seleccionar fecha', - 'client_create_order.one_time.location_label' => 'Ubicación', - 'client_create_order.one_time.location_hint' => 'Ingresar dirección', - 'client_create_order.one_time.positions_title' => 'Posiciones', - 'client_create_order.one_time.add_position' => 'Añadir Posición', - 'client_create_order.one_time.position_number' => ({required Object number}) => 'Posición ${number}', - 'client_create_order.one_time.remove' => 'Eliminar', - 'client_create_order.one_time.select_role' => 'Seleccionar rol', - 'client_create_order.one_time.start_label' => 'Inicio', - 'client_create_order.one_time.end_label' => 'Fin', - 'client_create_order.one_time.workers_label' => 'Trabajadores', - 'client_create_order.one_time.lunch_break_label' => 'Descanso para Almuerzo', - 'client_create_order.one_time.different_location' => 'Usar ubicación diferente para esta posición', - 'client_create_order.one_time.different_location_title' => 'Ubicación Diferente', - 'client_create_order.one_time.different_location_hint' => 'Ingresar dirección diferente', - 'client_create_order.one_time.create_order' => 'Crear Orden', - 'client_create_order.one_time.creating' => 'Creando...', - 'client_create_order.one_time.success_title' => '¡Orden Creada!', - 'client_create_order.one_time.success_message' => 'Tu solicitud de turno ha sido publicada. Los trabajadores comenzarán a postularse pronto.', - 'client_create_order.one_time.back_to_orders' => 'Volver a Órdenes', - 'client_create_order.one_time.no_break' => 'Sin descanso', - 'client_create_order.one_time.paid_break' => 'min (Pagado)', - 'client_create_order.one_time.unpaid_break' => 'min (No pagado)', - 'client_create_order.recurring.title' => 'Orden Recurrente', - 'client_create_order.recurring.subtitle' => 'Cobertura continua semanal/mensual', - 'client_create_order.recurring.placeholder' => 'Flujo de Orden Recurrente (Trabajo en Progreso)', - 'client_create_order.permanent.title' => 'Orden Permanente', - 'client_create_order.permanent.subtitle' => 'Colocación de personal a largo plazo', - 'client_create_order.permanent.placeholder' => 'Flujo de Orden Permanente (Trabajo en Progreso)', - 'client_main.tabs.coverage' => 'Cobertura', - 'client_main.tabs.billing' => 'Facturación', - 'client_main.tabs.home' => 'Inicio', - 'client_main.tabs.orders' => 'Órdenes', - 'client_main.tabs.reports' => 'Reportes', - 'client_view_orders.title' => 'Órdenes', - 'client_view_orders.post_button' => 'Publicar', - 'client_view_orders.post_order' => 'Publicar una Orden', - 'client_view_orders.no_orders' => ({required Object date}) => 'No hay órdenes para ${date}', - 'client_view_orders.tabs.up_next' => 'Próximos', - 'client_view_orders.tabs.active' => 'Activos', - 'client_view_orders.tabs.completed' => 'Completados', - 'client_view_orders.card.open' => 'ABIERTO', - 'client_view_orders.card.filled' => 'LLENO', - 'client_view_orders.card.confirmed' => 'CONFIRMADO', - 'client_view_orders.card.in_progress' => 'EN PROGRESO', - 'client_view_orders.card.completed' => 'COMPLETADO', - 'client_view_orders.card.cancelled' => 'CANCELADO', - 'client_view_orders.card.get_direction' => 'Obtener dirección', - 'client_view_orders.card.total' => 'Total', - 'client_view_orders.card.hrs' => 'HRS', - 'client_view_orders.card.workers' => ({required Object count}) => '${count} trabajadores', - 'client_view_orders.card.clock_in' => 'ENTRADA', - 'client_view_orders.card.clock_out' => 'SALIDA', - 'client_view_orders.card.coverage' => 'Cobertura', - 'client_view_orders.card.workers_label' => ({required Object filled, required Object needed}) => '${filled}/${needed} Trabajadores', - 'client_view_orders.card.confirmed_workers' => 'Trabajadores Confirmados', - 'client_view_orders.card.no_workers' => 'Ningún trabajador confirmado aún.', - 'client_billing.title' => 'Facturación', - 'client_billing.current_period' => 'Período Actual', - 'client_billing.saved_amount' => ({required Object amount}) => '${amount} ahorrado', - 'client_billing.awaiting_approval' => 'Esperando Aprobación', - 'client_billing.payment_method' => 'Método de Pago', - 'client_billing.add_payment' => 'Añadir', - 'client_billing.default_badge' => 'Predeterminado', - 'client_billing.expires' => ({required Object date}) => 'Expira ${date}', - 'client_billing.period_breakdown' => 'Desglose de este Período', - 'client_billing.week' => 'Semana', - 'client_billing.month' => 'Mes', - 'client_billing.total' => 'Total', - 'client_billing.hours' => ({required Object count}) => '${count} horas', - 'client_billing.rate_optimization_title' => 'Optimización de Tarifas', - 'client_billing.rate_optimization_body' => ({required Object amount}) => 'Ahorra ${amount}/mes cambiando 3 turnos', - 'client_billing.view_details' => 'Ver Detalles', - 'client_billing.invoice_history' => 'Historial de Facturas', - 'client_billing.view_all' => 'Ver todo', - 'client_billing.export_button' => 'Exportar Todas las Facturas', - 'client_billing.pending_badge' => 'PENDIENTE APROBACIÓN', - 'client_billing.paid_badge' => 'PAGADO', - 'staff.main.tabs.shifts' => 'Turnos', - 'staff.main.tabs.payments' => 'Pagos', - 'staff.main.tabs.home' => 'Inicio', - 'staff.main.tabs.clock_in' => 'Marcar Entrada', - 'staff.main.tabs.profile' => 'Perfil', - 'staff.home.header.welcome_back' => 'Welcome back', - 'staff.home.header.user_name_placeholder' => 'Krower', - 'staff.home.banners.complete_profile_title' => 'Complete Your Profile', - 'staff.home.banners.complete_profile_subtitle' => 'Get verified to see more shifts', - 'staff.home.banners.availability_title' => 'Availability', - 'staff.home.banners.availability_subtitle' => 'Update your availability for next week', - 'staff.home.quick_actions.find_shifts' => 'Find Shifts', - 'staff.home.quick_actions.availability' => 'Availability', - 'staff.home.quick_actions.messages' => 'Messages', - 'staff.home.quick_actions.earnings' => 'Earnings', - 'staff.home.sections.todays_shift' => 'Today\'s Shift', - 'staff.home.sections.scheduled_count' => ({required Object count}) => '${count} scheduled', - 'staff.home.sections.tomorrow' => 'Tomorrow', - 'staff.home.sections.recommended_for_you' => 'Recommended for You', - 'staff.home.sections.view_all' => 'View all', - 'staff.home.empty_states.no_shifts_today' => 'No shifts scheduled for today', - 'staff.home.empty_states.find_shifts_cta' => 'Find shifts →', - 'staff.home.empty_states.no_shifts_tomorrow' => 'No shifts for tomorrow', - 'staff.home.empty_states.no_recommended_shifts' => 'No recommended shifts', - 'staff.home.pending_payment.title' => 'Pending Payment', - 'staff.home.pending_payment.subtitle' => 'Payment processing', - 'staff.home.pending_payment.amount' => ({required Object amount}) => '${amount}', - 'staff.home.recommended_card.act_now' => '• ACT NOW', - 'staff.home.recommended_card.one_day' => 'One Day', - 'staff.home.recommended_card.today' => 'Today', - 'staff.home.recommended_card.applied_for' => ({required Object title}) => 'Applied for ${title}', - 'staff.home.recommended_card.time_range' => ({required Object start, required Object end}) => '${start} - ${end}', - 'staff.home.benefits.title' => 'Your Benefits', - 'staff.home.benefits.view_all' => 'View all', - 'staff.home.benefits.hours_label' => 'hours', - 'staff.home.benefits.items.sick_days' => 'Sick Days', - 'staff.home.benefits.items.vacation' => 'Vacation', - 'staff.home.benefits.items.holidays' => 'Holidays', - 'staff.home.auto_match.title' => 'Auto-Match', - 'staff.home.auto_match.finding_shifts' => 'Finding shifts for you', - 'staff.home.auto_match.get_matched' => 'Get matched automatically', - 'staff.home.auto_match.matching_based_on' => 'Matching based on:', - 'staff.home.auto_match.chips.location' => 'Location', - 'staff.home.auto_match.chips.availability' => 'Availability', - 'staff.home.auto_match.chips.skills' => 'Skills', - 'staff.home.improve.title' => 'Improve Yourself', - 'staff.home.improve.items.training.title' => 'Training Section', - 'staff.home.improve.items.training.description' => 'Improve your skills and get certified.', - 'staff.home.improve.items.training.page' => '/krow-university', - 'staff.home.improve.items.podcast.title' => 'Krow Podcast', - 'staff.home.improve.items.podcast.description' => 'Listen to tips from top workers.', - 'staff.home.improve.items.podcast.page' => '/krow-university', - 'staff.home.more_ways.title' => 'More Ways To Use Krow', - 'staff.home.more_ways.items.benefits.title' => 'Krow Benefits', - 'staff.home.more_ways.items.benefits.page' => '/benefits', - 'staff.home.more_ways.items.refer.title' => 'Refer a Friend', - 'staff.home.more_ways.items.refer.page' => '/worker-profile', - 'staff.profile.header.title' => 'Perfil', - 'staff.profile.header.sign_out' => 'CERRAR SESIÓN', - 'staff.profile.reliability_stats.shifts' => 'Turnos', - 'staff.profile.reliability_stats.rating' => 'Calificación', - 'staff.profile.reliability_stats.on_time' => 'A Tiempo', - 'staff.profile.reliability_stats.no_shows' => 'Faltas', - 'staff.profile.reliability_stats.cancellations' => 'Cancel.', - 'staff.profile.reliability_score.title' => 'Puntuación de Confiabilidad', - 'staff.profile.reliability_score.description' => 'Mantén tu puntuación por encima del 45% para continuar aceptando turnos.', - 'staff.profile.sections.onboarding' => 'INCORPORACIÓN', - 'staff.profile.sections.compliance' => 'CUMPLIMIENTO', - 'staff.profile.sections.level_up' => 'MEJORAR NIVEL', - 'staff.profile.sections.finance' => 'FINANZAS', - 'staff.profile.sections.support' => 'SOPORTE', - 'staff.profile.menu_items.personal_info' => 'Información Personal', - 'staff.profile.menu_items.emergency_contact' => 'Contacto de Emergencia', - 'staff.profile.menu_items.experience' => 'Experiencia', - 'staff.profile.menu_items.attire' => 'Vestimenta', - 'staff.profile.menu_items.documents' => 'Documentos', - 'staff.profile.menu_items.certificates' => 'Certificados', - 'staff.profile.menu_items.tax_forms' => 'Formularios Fiscales', - 'staff.profile.menu_items.krow_university' => 'Krow University', - 'staff.profile.menu_items.trainings' => 'Capacitaciones', - 'staff.profile.menu_items.leaderboard' => 'Tabla de Clasificación', - 'staff.profile.menu_items.bank_account' => 'Cuenta Bancaria', - 'staff.profile.menu_items.payments' => 'Pagos', - 'staff.profile.menu_items.timecard' => 'Tarjeta de Tiempo', - 'staff.profile.menu_items.faqs' => 'Preguntas Frecuentes', - 'staff.profile.menu_items.privacy_security' => 'Privacidad y Seguridad', - 'staff.profile.menu_items.messages' => 'Mensajes', - 'staff.profile.bank_account_page.title' => 'Cuenta Bancaria', - 'staff.profile.bank_account_page.linked_accounts' => 'Cuentas Vinculadas', - 'staff.profile.bank_account_page.add_account' => 'Agregar Cuenta Bancaria', - 'staff.profile.bank_account_page.secure_title' => 'Seguro y Cifrado', - 'staff.profile.bank_account_page.secure_subtitle' => 'Su información bancaria está cifrada y almacenada de forma segura. Nunca compartimos sus detalles.', - 'staff.profile.bank_account_page.add_new_account' => 'Agregar Nueva Cuenta', - 'staff.profile.bank_account_page.routing_number' => 'Número de Ruta', - 'staff.profile.bank_account_page.routing_hint' => '9 dígitos', - 'staff.profile.bank_account_page.account_number' => 'Número de Cuenta', - 'staff.profile.bank_account_page.account_hint' => 'Ingrese número de cuenta', - 'staff.profile.bank_account_page.account_type' => 'Tipo de Cuenta', - 'staff.profile.bank_account_page.checking' => 'CORRIENTE', - 'staff.profile.bank_account_page.savings' => 'AHORROS', - 'staff.profile.bank_account_page.cancel' => 'Cancelar', - 'staff.profile.bank_account_page.save' => 'Guardar', - 'staff.profile.bank_account_page.primary' => 'Principal', - 'staff.profile.bank_account_page.account_ending' => ({required Object last4}) => 'Termina en ${last4}', - 'staff.profile.logout.button' => 'Cerrar Sesión', - 'staff.onboarding.personal_info.title' => 'Información Personal', - 'staff.onboarding.personal_info.change_photo_hint' => 'Toca para cambiar foto', - 'staff.onboarding.personal_info.full_name_label' => 'Nombre Completo', - 'staff.onboarding.personal_info.email_label' => 'Correo Electrónico', - 'staff.onboarding.personal_info.phone_label' => 'Número de Teléfono', - 'staff.onboarding.personal_info.phone_hint' => '+1 (555) 000-0000', - 'staff.onboarding.personal_info.bio_label' => 'Biografía', - 'staff.onboarding.personal_info.bio_hint' => 'Cuéntales a los clientes sobre ti...', - 'staff.onboarding.personal_info.languages_label' => 'Idiomas', - 'staff.onboarding.personal_info.languages_hint' => 'Inglés, Español, Francés...', - 'staff.onboarding.personal_info.locations_label' => 'Ubicaciones Preferidas', - 'staff.onboarding.personal_info.locations_hint' => 'Centro, Midtown, Brooklyn...', - 'staff.onboarding.personal_info.save_button' => 'Guardar Cambios', - 'staff.onboarding.personal_info.save_success' => 'Información personal guardada exitosamente', - 'staff.onboarding.experience.title' => 'Experience & Skills', - 'staff.onboarding.experience.industries_title' => 'Industries', - 'staff.onboarding.experience.industries_subtitle' => 'Select the industries you have experience in', - 'staff.onboarding.experience.skills_title' => 'Skills', - 'staff.onboarding.experience.skills_subtitle' => 'Select your skills or add custom ones', - 'staff.onboarding.experience.custom_skills_title' => 'Custom Skills:', - 'staff.onboarding.experience.custom_skill_hint' => 'Add custom skill...', - 'staff.onboarding.experience.save_button' => 'Save & Continue', - 'staff.onboarding.experience.industries.hospitality' => 'Hospitality', - 'staff.onboarding.experience.industries.food_service' => 'Food Service', - 'staff.onboarding.experience.industries.warehouse' => 'Warehouse', - 'staff.onboarding.experience.industries.events' => 'Events', - 'staff.onboarding.experience.industries.retail' => 'Retail', - 'staff.onboarding.experience.industries.healthcare' => 'Healthcare', - 'staff.onboarding.experience.industries.other' => 'Other', - 'staff.onboarding.experience.skills.food_service' => 'Food Service', - 'staff.onboarding.experience.skills.bartending' => 'Bartending', - 'staff.onboarding.experience.skills.event_setup' => 'Event Setup', - 'staff.onboarding.experience.skills.hospitality' => 'Hospitality', - 'staff.onboarding.experience.skills.warehouse' => 'Warehouse', - 'staff.onboarding.experience.skills.customer_service' => 'Customer Service', - 'staff.onboarding.experience.skills.cleaning' => 'Cleaning', - 'staff.onboarding.experience.skills.security' => 'Security', - 'staff.onboarding.experience.skills.retail' => 'Retail', - 'staff.onboarding.experience.skills.cooking' => 'Cooking', - 'staff.onboarding.experience.skills.cashier' => 'Cashier', - 'staff.onboarding.experience.skills.server' => 'Server', - 'staff.onboarding.experience.skills.barista' => 'Barista', - 'staff.onboarding.experience.skills.host_hostess' => 'Host/Hostess', - 'staff.onboarding.experience.skills.busser' => 'Busser', - 'staff.onboarding.experience.skills.driving' => 'Driving', - 'staff_documents.title' => 'Documents', - 'staff_documents.verification_card.title' => 'Document Verification', - 'staff_documents.verification_card.progress' => ({required Object completed, required Object total}) => '${completed}/${total} Complete', - 'staff_documents.list.empty' => 'No documents found', - 'staff_documents.list.error' => ({required Object message}) => 'Error: ${message}', - 'staff_documents.card.view' => 'View', - 'staff_documents.card.upload' => 'Upload', - 'staff_documents.card.verified' => 'Verified', - 'staff_documents.card.pending' => 'Pending', - 'staff_documents.card.missing' => 'Missing', - 'staff_documents.card.rejected' => 'Rejected', - 'staff_certificates.title' => 'Certificates', - 'staff_certificates.progress.title' => 'Your Progress', - 'staff_certificates.progress.verified_count' => ({required Object completed, required Object total}) => '${completed} of ${total} verified', - 'staff_certificates.progress.active' => 'Compliance Active', - 'staff_certificates.card.expires_in_days' => ({required Object days}) => 'Expires in ${days} days - Renew now', - 'staff_certificates.card.expired' => 'Expired - Renew now', - 'staff_certificates.card.verified' => 'Verified', - 'staff_certificates.card.expiring_soon' => 'Expiring Soon', - 'staff_certificates.card.exp' => ({required Object date}) => 'Exp: ${date}', - 'staff_certificates.card.upload_button' => 'Upload Certificate', - 'staff_certificates.card.edit_expiry' => 'Edit Expiration Date', - 'staff_certificates.card.remove' => 'Remove Certificate', - 'staff_certificates.card.renew' => 'Renew', - 'staff_certificates.card.opened_snackbar' => 'Certificate opened in new tab', - 'staff_certificates.add_more.title' => 'Add Another Certificate', - 'staff_certificates.add_more.subtitle' => 'Upload additional certifications', - 'staff_certificates.upload_modal.title' => 'Upload Certificate', - 'staff_certificates.upload_modal.expiry_label' => 'Expiration Date (Optional)', - 'staff_certificates.upload_modal.select_date' => 'Select date', - 'staff_certificates.upload_modal.upload_file' => 'Upload File', - 'staff_certificates.upload_modal.drag_drop' => 'Drag and drop or click to upload', - 'staff_certificates.upload_modal.supported_formats' => 'PDF, JPG, PNG up to 10MB', - 'staff_certificates.upload_modal.cancel' => 'Cancel', - 'staff_certificates.upload_modal.save' => 'Save Certificate', - 'staff_certificates.delete_modal.title' => 'Remove Certificate?', - 'staff_certificates.delete_modal.message' => 'This action cannot be undone.', - 'staff_certificates.delete_modal.cancel' => 'Cancel', - 'staff_certificates.delete_modal.confirm' => 'Remove', - 'staff_profile_attire.title' => 'Vestimenta', - 'staff_profile_attire.info_card.title' => 'Tu Vestuario', - 'staff_profile_attire.info_card.description' => 'Selecciona los artículos de vestimenta que posees. Esto nos ayuda a asignarte turnos que se ajusten a tu vestuario.', - 'staff_profile_attire.status.required' => 'REQUERIDO', - 'staff_profile_attire.status.add_photo' => 'Añadir Foto', - 'staff_profile_attire.status.added' => 'Añadido', - 'staff_profile_attire.status.pending' => '⏳ Verificación pendiente', - 'staff_profile_attire.attestation' => 'Certifico que poseo estos artículos y los usaré en mis turnos. Entiendo que los artículos están pendientes de verificación por el gerente en mi primer turno.', - 'staff_profile_attire.actions.save' => 'Guardar Vestimenta', - 'staff_profile_attire.validation.select_required' => '✓ Seleccionar todos los artículos requeridos', - 'staff_profile_attire.validation.upload_required' => '✓ Subir fotos de artículos requeridos', - 'staff_profile_attire.validation.accept_attestation' => '✓ Aceptar certificación', - 'staff_shifts.title' => 'Shifts', - 'staff_shifts.tabs.my_shifts' => 'My Shifts', - 'staff_shifts.tabs.find_work' => 'Find Work', - 'staff_shifts.list.no_shifts' => 'No shifts found', - 'staff_shifts.list.pending_offers' => 'PENDING OFFERS', - 'staff_shifts.list.available_jobs' => ({required Object count}) => '${count} AVAILABLE JOBS', - 'staff_shifts.list.search_hint' => 'Search jobs...', - 'staff_shifts.filter.all' => 'All Jobs', - 'staff_shifts.filter.one_day' => 'One Day', - 'staff_shifts.filter.multi_day' => 'Multi Day', - 'staff_shifts.filter.long_term' => 'Long Term', - 'staff_shifts.status.confirmed' => 'CONFIRMED', - 'staff_shifts.status.act_now' => 'ACT NOW', - 'staff_shifts.status.swap_requested' => 'SWAP REQUESTED', - 'staff_shifts.status.completed' => 'COMPLETED', - 'staff_shifts.status.no_show' => 'NO SHOW', - 'staff_shifts.status.pending_warning' => 'Please confirm assignment', - 'staff_shifts.action.decline' => 'Decline', - 'staff_shifts.action.confirm' => 'Confirm', - 'staff_shifts.action.request_swap' => 'Request Swap', - 'staff_shifts.details.additional' => 'ADDITIONAL DETAILS', - 'staff_shifts.details.days' => ({required Object days}) => '${days} Days', - 'staff_shifts.details.exp_total' => ({required Object amount}) => '(exp.total \$${amount})', - 'staff_shifts.details.pending_time' => ({required Object time}) => 'Pending ${time} ago', - 'staff_shifts.tags.immediate_start' => 'Immediate start', - 'staff_shifts.tags.no_experience' => 'No experience', - 'staff_time_card.title' => 'Tarjeta de tiempo', - 'staff_time_card.hours_worked' => 'Horas trabajadas', - 'staff_time_card.total_earnings' => 'Ganancias totales', - 'staff_time_card.shift_history' => 'Historial de turnos', - _ => null, - } ?? switch (path) { - 'staff_time_card.no_shifts' => 'No hay turnos para este mes', - 'staff_time_card.hours' => 'horas', - 'staff_time_card.per_hr' => '/hr', - 'staff_time_card.status.approved' => 'Aprobado', - 'staff_time_card.status.disputed' => 'Disputado', - 'staff_time_card.status.paid' => 'Pagado', - 'staff_time_card.status.pending' => 'Pendiente', - _ => null, - }; - } -} diff --git a/apps/mobile/packages/core_localization/lib/src/localization_module.dart b/apps/mobile/packages/core_localization/lib/src/localization_module.dart index bbc87c6d..42dd5b71 100644 --- a/apps/mobile/packages/core_localization/lib/src/localization_module.dart +++ b/apps/mobile/packages/core_localization/lib/src/localization_module.dart @@ -3,7 +3,9 @@ import 'package:shared_preferences/shared_preferences.dart'; import 'data/datasources/locale_local_data_source.dart'; import 'data/repositories_impl/locale_repository_impl.dart'; import 'domain/repositories/locale_repository_interface.dart'; +import 'domain/usecases/get_default_locale_use_case.dart'; import 'domain/usecases/get_locale_use_case.dart'; +import 'domain/usecases/get_supported_locales_use_case.dart'; import 'domain/usecases/set_locale_use_case.dart'; import 'bloc/locale_bloc.dart'; @@ -18,28 +20,36 @@ class LocalizationModule extends Module { i.addInstance(SharedPreferencesAsync()); // Data Sources - i.addSingleton( + i.addLazySingleton( () => LocaleLocalDataSourceImpl(i.get()), ); // Repositories - i.addSingleton( - () => LocaleRepositoryImpl(i.get()), + i.addLazySingleton( + () => LocaleRepositoryImpl(localDataSource: i.get()), ); // Use Cases - i.addSingleton( + i.addLazySingleton( () => GetLocaleUseCase(i.get()), ); - i.addSingleton( + i.addLazySingleton( () => SetLocaleUseCase(i.get()), ); + i.addLazySingleton( + () => GetSupportedLocalesUseCase(i.get()), + ); + i.addLazySingleton( + () => GetDefaultLocaleUseCase(i.get()), + ); // BLoCs - i.addSingleton( + i.add( () => LocaleBloc( getLocaleUseCase: i.get(), setLocaleUseCase: i.get(), + getSupportedLocalesUseCase: i.get(), + getDefaultLocaleUseCase: i.get(), ), ); } diff --git a/apps/mobile/packages/core_localization/lib/src/utils/error_translator.dart b/apps/mobile/packages/core_localization/lib/src/utils/error_translator.dart new file mode 100644 index 00000000..5e7df68d --- /dev/null +++ b/apps/mobile/packages/core_localization/lib/src/utils/error_translator.dart @@ -0,0 +1,139 @@ +import '../l10n/strings.g.dart'; + +/// Translates error message keys to localized strings. +/// +/// This utility function takes a dot-notation key like 'errors.auth.account_exists' +/// and returns the corresponding localized string from the translation system. +/// +/// If the key is not found or doesn't match the expected format, the original +/// key is returned as a fallback. +/// +/// Example: +/// ```dart +/// final message = translateErrorKey('errors.auth.account_exists'); +/// // Returns: "An account with this email already exists. Try signing in instead." +/// ``` +String translateErrorKey(String key) { + final List parts = key.split('.'); + + // Expected format: errors.{category}.{error_type} + if (parts.length != 3 || parts[0] != 'errors') { + return key; + } + + final String category = parts[1]; + final String errorType = parts[2]; + + switch (category) { + case 'auth': + return _translateAuthError(errorType); + case 'hub': + return _translateHubError(errorType); + case 'order': + return _translateOrderError(errorType); + case 'profile': + return _translateProfileError(errorType); + case 'shift': + return _translateShiftError(errorType); + case 'generic': + return _translateGenericError(errorType); + default: + return key; + } +} + +String _translateAuthError(String errorType) { + switch (errorType) { + case 'invalid_credentials': + return t.errors.auth.invalid_credentials; + case 'account_exists': + return t.errors.auth.account_exists; + case 'session_expired': + return t.errors.auth.session_expired; + case 'user_not_found': + return t.errors.auth.user_not_found; + case 'unauthorized_app': + return t.errors.auth.unauthorized_app; + case 'weak_password': + return t.errors.auth.weak_password; + case 'sign_up_failed': + return t.errors.auth.sign_up_failed; + case 'sign_in_failed': + return t.errors.auth.sign_in_failed; + case 'not_authenticated': + return t.errors.auth.not_authenticated; + case 'password_mismatch': + return t.errors.auth.password_mismatch; + case 'google_only_account': + return t.errors.auth.google_only_account; + default: + return t.errors.generic.unknown; + } +} + +String _translateHubError(String errorType) { + switch (errorType) { + case 'has_orders': + return t.errors.hub.has_orders; + case 'not_found': + return t.errors.hub.not_found; + case 'creation_failed': + return t.errors.hub.creation_failed; + default: + return t.errors.generic.unknown; + } +} + +String _translateOrderError(String errorType) { + switch (errorType) { + case 'missing_hub': + return t.errors.order.missing_hub; + case 'missing_vendor': + return t.errors.order.missing_vendor; + case 'creation_failed': + return t.errors.order.creation_failed; + case 'shift_creation_failed': + return t.errors.order.shift_creation_failed; + case 'missing_business': + return t.errors.order.missing_business; + default: + return t.errors.generic.unknown; + } +} + +String _translateProfileError(String errorType) { + switch (errorType) { + case 'staff_not_found': + return t.errors.profile.staff_not_found; + case 'business_not_found': + return t.errors.profile.business_not_found; + case 'update_failed': + return t.errors.profile.update_failed; + default: + return t.errors.generic.unknown; + } +} + +String _translateShiftError(String errorType) { + switch (errorType) { + case 'no_open_roles': + return t.errors.shift.no_open_roles; + case 'application_not_found': + return t.errors.shift.application_not_found; + case 'no_active_shift': + return t.errors.shift.no_active_shift; + default: + return t.errors.generic.unknown; + } +} + +String _translateGenericError(String errorType) { + switch (errorType) { + case 'unknown': + return t.errors.generic.unknown; + case 'no_connection': + return t.errors.generic.no_connection; + default: + return t.errors.generic.unknown; + } +} diff --git a/apps/mobile/packages/data_connect/lib/src/session/staff_session_store.dart b/apps/mobile/packages/data_connect/lib/src/session/staff_session_store.dart index 835e11fc..06be4aef 100644 --- a/apps/mobile/packages/data_connect/lib/src/session/staff_session_store.dart +++ b/apps/mobile/packages/data_connect/lib/src/session/staff_session_store.dart @@ -3,10 +3,12 @@ import 'package:krow_domain/krow_domain.dart' as domain; class StaffSession { final domain.User user; final domain.Staff? staff; + final String? ownerId; const StaffSession({ required this.user, this.staff, + this.ownerId, }); } diff --git a/apps/mobile/packages/data_connect/pubspec.yaml b/apps/mobile/packages/data_connect/pubspec.yaml index 45610427..9795fcb7 100644 --- a/apps/mobile/packages/data_connect/pubspec.yaml +++ b/apps/mobile/packages/data_connect/pubspec.yaml @@ -14,3 +14,4 @@ dependencies: krow_domain: path: ../domain flutter_modular: ^6.3.0 + firebase_data_connect: ^0.2.2+2 diff --git a/apps/mobile/packages/design_system/lib/src/ui_constants.dart b/apps/mobile/packages/design_system/lib/src/ui_constants.dart index 819699b1..a13a28c9 100644 --- a/apps/mobile/packages/design_system/lib/src/ui_constants.dart +++ b/apps/mobile/packages/design_system/lib/src/ui_constants.dart @@ -37,4 +37,7 @@ class UiConstants { static const double space12 = 48.0; static const double space14 = 56.0; static const double space16 = 64.0; + static const double space20 = 80.0; + static const double space24 = 96.0; + static const double space32 = 128.0; } 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/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 7867798c..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( @@ -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/design_system/pubspec.yaml b/apps/mobile/packages/design_system/pubspec.yaml index fb4e10c9..ae3c4b60 100644 --- a/apps/mobile/packages/design_system/pubspec.yaml +++ b/apps/mobile/packages/design_system/pubspec.yaml @@ -5,7 +5,7 @@ homepage: resolution: workspace environment: - sdk: ^3.10.7 + sdk: '>=3.10.0 <4.0.0' flutter: ">=1.17.0" dependencies: diff --git a/apps/mobile/packages/domain/lib/krow_domain.dart b/apps/mobile/packages/domain/lib/krow_domain.dart index fc4d87f9..3dc41679 100644 --- a/apps/mobile/packages/domain/lib/krow_domain.dart +++ b/apps/mobile/packages/domain/lib/krow_domain.dart @@ -29,6 +29,7 @@ export 'src/entities/events/work_session.dart'; // Shifts export 'src/entities/shifts/shift.dart'; +export 'src/adapters/shifts/shift_adapter.dart'; // Orders & Requests export 'src/entities/orders/order_type.dart'; @@ -45,9 +46,11 @@ export 'src/entities/skills/skill_kit.dart'; // Financial & Payroll export 'src/entities/financial/invoice.dart'; +export 'src/entities/financial/time_card.dart'; export 'src/entities/financial/invoice_item.dart'; export 'src/entities/financial/invoice_decline.dart'; export 'src/entities/financial/staff_payment.dart'; +export 'src/entities/financial/payment_summary.dart'; // Profile export 'src/entities/profile/staff_document.dart'; @@ -78,12 +81,24 @@ export 'src/entities/home/home_dashboard_data.dart'; export 'src/entities/home/reorder_item.dart'; // Availability +export 'src/adapters/availability/availability_adapter.dart'; +export 'src/entities/clock_in/attendance_status.dart'; +export 'src/adapters/clock_in/clock_in_adapter.dart'; export 'src/entities/availability/availability_slot.dart'; export 'src/entities/availability/day_availability.dart'; +// 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'; export 'src/entities/profile/experience_skill.dart'; export 'src/adapters/profile/bank_account_adapter.dart'; export 'src/adapters/profile/tax_form_adapter.dart'; +export 'src/adapters/financial/payment_adapter.dart'; + +// Exceptions +export 'src/exceptions/app_exception.dart'; diff --git a/apps/mobile/packages/domain/lib/src/adapters/availability/availability_adapter.dart b/apps/mobile/packages/domain/lib/src/adapters/availability/availability_adapter.dart new file mode 100644 index 00000000..f32724f1 --- /dev/null +++ b/apps/mobile/packages/domain/lib/src/adapters/availability/availability_adapter.dart @@ -0,0 +1,33 @@ +import '../../entities/availability/availability_slot.dart'; + +/// Adapter for [AvailabilitySlot] domain entity. +class AvailabilityAdapter { + static const Map> _slotDefinitions = { + 'MORNING': { + 'id': 'morning', + 'label': 'Morning', + 'timeRange': '4:00 AM - 12:00 PM', + }, + 'AFTERNOON': { + 'id': 'afternoon', + 'label': 'Afternoon', + 'timeRange': '12:00 PM - 6:00 PM', + }, + 'EVENING': { + 'id': 'evening', + 'label': 'Evening', + 'timeRange': '6:00 PM - 12:00 AM', + }, + }; + + /// Converts a backend slot name (e.g. 'MORNING') to a Domain [AvailabilitySlot]. + static AvailabilitySlot fromPrimitive(String slotName, {bool isAvailable = false}) { + final def = _slotDefinitions[slotName.toUpperCase()] ?? _slotDefinitions['MORNING']!; + return AvailabilitySlot( + id: def['id']!, + label: def['label']!, + timeRange: def['timeRange']!, + isAvailable: isAvailable, + ); + } +} diff --git a/apps/mobile/packages/domain/lib/src/adapters/clock_in/clock_in_adapter.dart b/apps/mobile/packages/domain/lib/src/adapters/clock_in/clock_in_adapter.dart new file mode 100644 index 00000000..3ebfad03 --- /dev/null +++ b/apps/mobile/packages/domain/lib/src/adapters/clock_in/clock_in_adapter.dart @@ -0,0 +1,28 @@ +import '../../entities/shifts/shift.dart'; +import '../../entities/clock_in/attendance_status.dart'; + +/// Adapter for Clock In related data. +class ClockInAdapter { + + /// Converts primitive attendance data to [AttendanceStatus]. + static AttendanceStatus toAttendanceStatus({ + required String status, + DateTime? checkInTime, + DateTime? checkOutTime, + String? activeShiftId, + String? activeApplicationId, + }) { + final bool isCheckedIn = status == 'CHECKED_IN' || status == 'LATE'; // Assuming LATE is also checked in? + + // Statuses that imply active attendance: CHECKED_IN, LATE. + // Statuses that imply completed: CHECKED_OUT. + + return AttendanceStatus( + isCheckedIn: isCheckedIn, + checkInTime: checkInTime, + checkOutTime: checkOutTime, + activeShiftId: activeShiftId, + activeApplicationId: activeApplicationId, + ); + } +} diff --git a/apps/mobile/packages/domain/lib/src/adapters/financial/payment_adapter.dart b/apps/mobile/packages/domain/lib/src/adapters/financial/payment_adapter.dart new file mode 100644 index 00000000..66446058 --- /dev/null +++ b/apps/mobile/packages/domain/lib/src/adapters/financial/payment_adapter.dart @@ -0,0 +1,19 @@ +import '../../entities/financial/staff_payment.dart'; + +/// Adapter for Payment related data. +class PaymentAdapter { + + /// Converts string status to [PaymentStatus]. + static PaymentStatus toPaymentStatus(String status) { + switch (status) { + case 'PAID': + return PaymentStatus.paid; + case 'PENDING': + return PaymentStatus.pending; + case 'FAILED': + return PaymentStatus.failed; + default: + return PaymentStatus.unknown; + } + } +} diff --git a/apps/mobile/packages/domain/lib/src/adapters/financial/time_card_adapter.dart b/apps/mobile/packages/domain/lib/src/adapters/financial/time_card_adapter.dart new file mode 100644 index 00000000..572d74a1 --- /dev/null +++ b/apps/mobile/packages/domain/lib/src/adapters/financial/time_card_adapter.dart @@ -0,0 +1,50 @@ +import '../../entities/financial/time_card.dart'; + +/// Adapter for [TimeCard] to map data layer values to domain entity. +class TimeCardAdapter { + /// Maps primitive values to [TimeCard]. + static TimeCard fromPrimitives({ + required String id, + required String shiftTitle, + required String clientName, + required DateTime date, + required String startTime, + required String endTime, + required double totalHours, + required double hourlyRate, + required double totalPay, + required String status, + String? location, + }) { + return TimeCard( + id: id, + shiftTitle: shiftTitle, + clientName: clientName, + date: date, + startTime: startTime, + endTime: endTime, + totalHours: totalHours, + hourlyRate: hourlyRate, + totalPay: totalPay, + status: _stringToStatus(status), + location: location, + ); + } + + static TimeCardStatus _stringToStatus(String status) { + switch (status.toUpperCase()) { + case 'CHECKED_OUT': + case 'COMPLETED': + return TimeCardStatus.approved; // Assuming completed = approved for now + case 'PAID': + return TimeCardStatus.paid; // If this status exists + case 'DISPUTED': + return TimeCardStatus.disputed; + case 'CHECKED_IN': + case 'ACCEPTED': + case 'CONFIRMED': + default: + return TimeCardStatus.pending; + } + } +} diff --git a/apps/mobile/packages/domain/lib/src/adapters/profile/tax_form_adapter.dart b/apps/mobile/packages/domain/lib/src/adapters/profile/tax_form_adapter.dart index 41f85479..8c070da4 100644 --- a/apps/mobile/packages/domain/lib/src/adapters/profile/tax_form_adapter.dart +++ b/apps/mobile/packages/domain/lib/src/adapters/profile/tax_form_adapter.dart @@ -15,18 +15,36 @@ class TaxFormAdapter { DateTime? createdAt, DateTime? updatedAt, }) { - return TaxForm( - id: id, - type: _stringToType(type), - title: title, - subtitle: subtitle, - description: description, - status: _stringToStatus(status), - staffId: staffId, - formData: formData is Map ? Map.from(formData) : null, - createdAt: createdAt, - updatedAt: updatedAt, - ); + final TaxFormType formType = _stringToType(type); + final TaxFormStatus formStatus = _stringToStatus(status); + final Map formDetails = + formData is Map ? Map.from(formData as Map) : {}; + + if (formType == TaxFormType.i9) { + return I9TaxForm( + id: id, + title: title, + subtitle: subtitle, + description: description, + status: formStatus, + staffId: staffId, + formData: formDetails, + createdAt: createdAt, + updatedAt: updatedAt, + ); + } else { + return W4TaxForm( + id: id, + title: title, + subtitle: subtitle, + description: description, + status: formStatus, + staffId: staffId, + formData: formDetails, + createdAt: createdAt, + updatedAt: updatedAt, + ); + } } static TaxFormType _stringToType(String? value) { 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 new file mode 100644 index 00000000..6022d327 --- /dev/null +++ b/apps/mobile/packages/domain/lib/src/adapters/shifts/shift_adapter.dart @@ -0,0 +1,59 @@ +import 'package:intl/intl.dart'; +import '../../entities/shifts/shift.dart'; + +/// Adapter for Shift related data. +class ShiftAdapter { + /// Maps application data to a Shift entity. + /// + /// This method handles the common mapping logic used across different + /// repositories when converting application data from Data Connect to + /// domain Shift entities. + static Shift fromApplicationData({ + required String shiftId, + required String roleId, + required String roleName, + required String businessName, + String? companyLogoUrl, + required double costPerHour, + String? shiftLocation, + required String teamHubName, + DateTime? shiftDate, + DateTime? startTime, + DateTime? endTime, + DateTime? createdAt, + required String status, + String? description, + int? durationDays, + required int count, + int? assigned, + String? eventName, + bool hasApplied = false, + }) { + final String orderName = (eventName ?? '').trim().isNotEmpty + ? eventName! + : businessName; + final String title = '$roleName - $orderName'; + + return Shift( + id: shiftId, + roleId: roleId, + title: title, + clientName: businessName, + logoUrl: companyLogoUrl, + hourlyRate: costPerHour, + location: shiftLocation ?? '', + locationAddress: teamHubName, + date: shiftDate?.toIso8601String() ?? '', + startTime: startTime != null ? DateFormat('HH:mm').format(startTime) : '', + endTime: endTime != null ? DateFormat('HH:mm').format(endTime) : '', + createdDate: createdAt?.toIso8601String() ?? '', + status: status, + description: description, + durationDays: durationDays, + requiredSlots: count, + filledSlots: assigned ?? 0, + hasApplied: hasApplied, + ); + } +} + diff --git a/apps/mobile/packages/domain/lib/src/entities/clock_in/attendance_status.dart b/apps/mobile/packages/domain/lib/src/entities/clock_in/attendance_status.dart new file mode 100644 index 00000000..84acf58e --- /dev/null +++ b/apps/mobile/packages/domain/lib/src/entities/clock_in/attendance_status.dart @@ -0,0 +1,27 @@ +import 'package:equatable/equatable.dart'; + +/// Simple entity to hold attendance state +class AttendanceStatus extends Equatable { + final bool isCheckedIn; + final DateTime? checkInTime; + final DateTime? checkOutTime; + final String? activeShiftId; + final String? activeApplicationId; + + const AttendanceStatus({ + this.isCheckedIn = false, + this.checkInTime, + this.checkOutTime, + this.activeShiftId, + this.activeApplicationId, + }); + + @override + List get props => [ + isCheckedIn, + checkInTime, + checkOutTime, + activeShiftId, + activeApplicationId, + ]; +} 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/domain/lib/src/entities/financial/payment_summary.dart b/apps/mobile/packages/domain/lib/src/entities/financial/payment_summary.dart new file mode 100644 index 00000000..0a202449 --- /dev/null +++ b/apps/mobile/packages/domain/lib/src/entities/financial/payment_summary.dart @@ -0,0 +1,24 @@ +import 'package:equatable/equatable.dart'; + +/// Summary of staff earnings. +class PaymentSummary extends Equatable { + final double weeklyEarnings; + final double monthlyEarnings; + final double pendingEarnings; + final double totalEarnings; + + const PaymentSummary({ + required this.weeklyEarnings, + required this.monthlyEarnings, + required this.pendingEarnings, + required this.totalEarnings, + }); + + @override + List get props => [ + weeklyEarnings, + monthlyEarnings, + pendingEarnings, + totalEarnings, + ]; +} diff --git a/apps/mobile/packages/domain/lib/src/entities/financial/staff_payment.dart b/apps/mobile/packages/domain/lib/src/entities/financial/staff_payment.dart index bd890a77..d6126de8 100644 --- a/apps/mobile/packages/domain/lib/src/entities/financial/staff_payment.dart +++ b/apps/mobile/packages/domain/lib/src/entities/financial/staff_payment.dart @@ -13,6 +13,9 @@ enum PaymentStatus { /// Transfer failed. failed, + + /// Status unknown. + unknown, } /// Represents a payout to a [Staff] member for a completed [Assignment]. diff --git a/apps/mobile/packages/features/staff/profile_sections/finances/time_card/lib/src/domain/entities/time_card.dart b/apps/mobile/packages/domain/lib/src/entities/financial/time_card.dart similarity index 62% rename from apps/mobile/packages/features/staff/profile_sections/finances/time_card/lib/src/domain/entities/time_card.dart rename to apps/mobile/packages/domain/lib/src/entities/financial/time_card.dart index 2654ccf0..77bcb4ae 100644 --- a/apps/mobile/packages/features/staff/profile_sections/finances/time_card/lib/src/domain/entities/time_card.dart +++ b/apps/mobile/packages/domain/lib/src/entities/financial/time_card.dart @@ -1,30 +1,52 @@ import 'package:equatable/equatable.dart'; +/// Status of a time card. enum TimeCardStatus { + /// Waiting for approval or payment. pending, + /// Approved by manager. approved, + /// Payment has been issued. paid, + /// Disputed by staff or client. disputed; + /// Whether the card is approved. bool get isApproved => this == TimeCardStatus.approved; + /// Whether the card is paid. bool get isPaid => this == TimeCardStatus.paid; + /// Whether the card is disputed. bool get isDisputed => this == TimeCardStatus.disputed; + /// Whether the card is pending. bool get isPending => this == TimeCardStatus.pending; } +/// Represents a time card for a staff member. class TimeCard extends Equatable { + /// Unique identifier of the time card (often matches Application ID). final String id; + /// Title of the shift. final String shiftTitle; + /// Name of the client business. final String clientName; + /// Date of the shift. final DateTime date; + /// Actual or scheduled start time. final String startTime; + /// Actual or scheduled end time. final String endTime; + /// Total hours worked. final double totalHours; + /// Hourly pay rate. final double hourlyRate; + /// Total pay amount. final double totalPay; + /// Current status of the time card. final TimeCardStatus status; + /// Location name. final String? location; + /// Creates a [TimeCard]. const TimeCard({ required this.id, required this.shiftTitle, diff --git a/apps/mobile/packages/domain/lib/src/entities/orders/one_time_order.dart b/apps/mobile/packages/domain/lib/src/entities/orders/one_time_order.dart index 1e035a49..e0e7ca67 100644 --- a/apps/mobile/packages/domain/lib/src/entities/orders/one_time_order.dart +++ b/apps/mobile/packages/domain/lib/src/entities/orders/one_time_order.dart @@ -10,6 +10,8 @@ class OneTimeOrder extends Equatable { required this.date, required this.location, required this.positions, + this.hub, + this.eventName, this.vendorId, this.roleRates = const {}, }); @@ -22,6 +24,12 @@ class OneTimeOrder extends Equatable { /// The list of positions and headcounts required for this order. final List positions; + /// Selected hub details for this order. + final OneTimeOrderHubDetails? hub; + + /// Optional order name. + final String? eventName; + /// Selected vendor id for this order. final String? vendorId; @@ -33,7 +41,53 @@ class OneTimeOrder extends Equatable { date, location, positions, + hub, + eventName, vendorId, roleRates, ]; } + +/// Minimal hub details used during order creation. +class OneTimeOrderHubDetails extends Equatable { + const OneTimeOrderHubDetails({ + required this.id, + required this.name, + required this.address, + this.placeId, + this.latitude, + this.longitude, + this.city, + this.state, + this.street, + this.country, + this.zipCode, + }); + + final String id; + final String name; + final String address; + final String? placeId; + final double? latitude; + final double? longitude; + final String? city; + final String? state; + final String? street; + final String? country; + final String? zipCode; + + @override + List get props => [ + id, + name, + address, + placeId, + latitude, + longitude, + city, + state, + street, + country, + zipCode, + ]; +} diff --git a/apps/mobile/packages/domain/lib/src/entities/profile/tax_form.dart b/apps/mobile/packages/domain/lib/src/entities/profile/tax_form.dart index 096380c5..bdb07d7b 100644 --- a/apps/mobile/packages/domain/lib/src/entities/profile/tax_form.dart +++ b/apps/mobile/packages/domain/lib/src/entities/profile/tax_form.dart @@ -4,27 +4,26 @@ enum TaxFormType { i9, w4 } enum TaxFormStatus { notStarted, inProgress, submitted, approved, rejected } -class TaxForm extends Equatable { +abstract class TaxForm extends Equatable { final String id; - final TaxFormType type; + TaxFormType get type; final String title; final String? subtitle; final String? description; final TaxFormStatus status; final String? staffId; - final Map? formData; + final Map formData; final DateTime? createdAt; final DateTime? updatedAt; const TaxForm({ required this.id, - required this.type, required this.title, this.subtitle, this.description, this.status = TaxFormStatus.notStarted, this.staffId, - this.formData, + this.formData = const {}, this.createdAt, this.updatedAt, }); @@ -43,3 +42,37 @@ class TaxForm extends Equatable { updatedAt, ]; } + +class I9TaxForm extends TaxForm { + const I9TaxForm({ + required super.id, + required super.title, + super.subtitle, + super.description, + super.status, + super.staffId, + super.formData, + super.createdAt, + super.updatedAt, + }); + + @override + TaxFormType get type => TaxFormType.i9; +} + +class W4TaxForm extends TaxForm { + const W4TaxForm({ + required super.id, + required super.title, + super.subtitle, + super.description, + super.status, + super.staffId, + super.formData, + super.createdAt, + super.updatedAt, + }); + + @override + TaxFormType get type => TaxFormType.w4; +} diff --git a/apps/mobile/packages/domain/lib/src/entities/shifts/shift.dart b/apps/mobile/packages/domain/lib/src/entities/shifts/shift.dart index 4998c45b..5ef22e1e 100644 --- a/apps/mobile/packages/domain/lib/src/entities/shifts/shift.dart +++ b/apps/mobile/packages/domain/lib/src/entities/shifts/shift.dart @@ -24,6 +24,11 @@ class Shift extends Equatable { final double? longitude; final String? status; final int? durationDays; // For multi-day shifts + final int? requiredSlots; + final int? filledSlots; + final String? roleId; + final bool? hasApplied; + final double? totalValue; const Shift({ required this.id, @@ -49,6 +54,11 @@ class Shift extends Equatable { this.longitude, this.status, this.durationDays, + this.requiredSlots, + this.filledSlots, + this.roleId, + this.hasApplied, + this.totalValue, }); @override @@ -76,6 +86,11 @@ class Shift extends Equatable { longitude, status, durationDays, + requiredSlots, + filledSlots, + roleId, + hasApplied, + totalValue, ]; } diff --git a/apps/mobile/packages/domain/lib/src/exceptions/app_exception.dart b/apps/mobile/packages/domain/lib/src/exceptions/app_exception.dart new file mode 100644 index 00000000..bf9ae481 --- /dev/null +++ b/apps/mobile/packages/domain/lib/src/exceptions/app_exception.dart @@ -0,0 +1,314 @@ +/// Base sealed class for all application exceptions. +/// +/// Provides type-safe error handling with user-friendly message keys. +/// Technical details are captured for logging but never shown to users. +sealed class AppException implements Exception { + const AppException({ + required this.code, + this.technicalMessage, + }); + + /// Unique error code for logging/tracking (e.g., "AUTH_001") + final String code; + + /// Technical details for developers (never shown to users) + final String? technicalMessage; + + /// Returns the localization key for user-friendly message + String get messageKey; + + @override + String toString() => 'AppException($code): $technicalMessage'; +} + +// ============================================================ +// AUTH EXCEPTIONS +// ============================================================ + +/// Base class for authentication-related exceptions. +sealed class AuthException extends AppException { + const AuthException({required super.code, super.technicalMessage}); +} + +/// Thrown when email/password combination is incorrect. +class InvalidCredentialsException extends AuthException { + const InvalidCredentialsException({String? technicalMessage}) + : super(code: 'AUTH_001', technicalMessage: technicalMessage); + + @override + String get messageKey => 'errors.auth.invalid_credentials'; +} + +/// Thrown when attempting to register with an email that already exists. +class AccountExistsException extends AuthException { + const AccountExistsException({String? technicalMessage}) + : super(code: 'AUTH_002', technicalMessage: technicalMessage); + + @override + String get messageKey => 'errors.auth.account_exists'; +} + +/// Thrown when the user session has expired. +class SessionExpiredException extends AuthException { + const SessionExpiredException({String? technicalMessage}) + : super(code: 'AUTH_003', technicalMessage: technicalMessage); + + @override + String get messageKey => 'errors.auth.session_expired'; +} + +/// Thrown when user profile is not found in database after Firebase auth. +class UserNotFoundException extends AuthException { + const UserNotFoundException({String? technicalMessage}) + : super(code: 'AUTH_004', technicalMessage: technicalMessage); + + @override + String get messageKey => 'errors.auth.user_not_found'; +} + +/// Thrown when user is not authorized for the current app (wrong role). +class UnauthorizedAppException extends AuthException { + const UnauthorizedAppException({String? technicalMessage}) + : super(code: 'AUTH_005', technicalMessage: technicalMessage); + + @override + String get messageKey => 'errors.auth.unauthorized_app'; +} + +/// Thrown when password doesn't meet security requirements. +class WeakPasswordException extends AuthException { + const WeakPasswordException({String? technicalMessage}) + : super(code: 'AUTH_006', technicalMessage: technicalMessage); + + @override + String get messageKey => 'errors.auth.weak_password'; +} + +/// Thrown when sign-up process fails. +class SignUpFailedException extends AuthException { + const SignUpFailedException({String? technicalMessage}) + : super(code: 'AUTH_007', technicalMessage: technicalMessage); + + @override + String get messageKey => 'errors.auth.sign_up_failed'; +} + +/// Thrown when sign-in process fails. +class SignInFailedException extends AuthException { + const SignInFailedException({String? technicalMessage}) + : super(code: 'AUTH_008', technicalMessage: technicalMessage); + + @override + String get messageKey => 'errors.auth.sign_in_failed'; +} + +/// Thrown when email exists but password doesn't match. +class PasswordMismatchException extends AuthException { + const PasswordMismatchException({String? technicalMessage}) + : super(code: 'AUTH_009', technicalMessage: technicalMessage); + + @override + String get messageKey => 'errors.auth.password_mismatch'; +} + +/// Thrown when account exists only with Google provider (no password). +class GoogleOnlyAccountException extends AuthException { + const GoogleOnlyAccountException({String? technicalMessage}) + : super(code: 'AUTH_010', technicalMessage: technicalMessage); + + @override + String get messageKey => 'errors.auth.google_only_account'; +} + +// ============================================================ +// HUB EXCEPTIONS +// ============================================================ + +/// Base class for hub-related exceptions. +sealed class HubException extends AppException { + const HubException({required super.code, super.technicalMessage}); +} + +/// Thrown when attempting to delete a hub that has active orders. +class HubHasOrdersException extends HubException { + const HubHasOrdersException({String? technicalMessage}) + : super(code: 'HUB_001', technicalMessage: technicalMessage); + + @override + String get messageKey => 'errors.hub.has_orders'; +} + +/// Thrown when hub is not found. +class HubNotFoundException extends HubException { + const HubNotFoundException({String? technicalMessage}) + : super(code: 'HUB_002', technicalMessage: technicalMessage); + + @override + String get messageKey => 'errors.hub.not_found'; +} + +/// Thrown when hub creation fails. +class HubCreationFailedException extends HubException { + const HubCreationFailedException({String? technicalMessage}) + : super(code: 'HUB_003', technicalMessage: technicalMessage); + + @override + String get messageKey => 'errors.hub.creation_failed'; +} + +// ============================================================ +// ORDER EXCEPTIONS +// ============================================================ + +/// Base class for order-related exceptions. +sealed class OrderException extends AppException { + const OrderException({required super.code, super.technicalMessage}); +} + +/// Thrown when order creation is attempted without a hub. +class OrderMissingHubException extends OrderException { + const OrderMissingHubException({String? technicalMessage}) + : super(code: 'ORDER_001', technicalMessage: technicalMessage); + + @override + String get messageKey => 'errors.order.missing_hub'; +} + +/// Thrown when order creation is attempted without a vendor. +class OrderMissingVendorException extends OrderException { + const OrderMissingVendorException({String? technicalMessage}) + : super(code: 'ORDER_002', technicalMessage: technicalMessage); + + @override + String get messageKey => 'errors.order.missing_vendor'; +} + +/// Thrown when order creation fails. +class OrderCreationFailedException extends OrderException { + const OrderCreationFailedException({String? technicalMessage}) + : super(code: 'ORDER_003', technicalMessage: technicalMessage); + + @override + String get messageKey => 'errors.order.creation_failed'; +} + +/// Thrown when shift creation fails. +class ShiftCreationFailedException extends OrderException { + const ShiftCreationFailedException({String? technicalMessage}) + : super(code: 'ORDER_004', technicalMessage: technicalMessage); + + @override + String get messageKey => 'errors.order.shift_creation_failed'; +} + +/// Thrown when order is missing required business context. +class OrderMissingBusinessException extends OrderException { + const OrderMissingBusinessException({String? technicalMessage}) + : super(code: 'ORDER_005', technicalMessage: technicalMessage); + + @override + String get messageKey => 'errors.order.missing_business'; +} + +// ============================================================ +// PROFILE EXCEPTIONS +// ============================================================ + +/// Base class for profile-related exceptions. +sealed class ProfileException extends AppException { + const ProfileException({required super.code, super.technicalMessage}); +} + +/// Thrown when staff profile is not found. +class StaffProfileNotFoundException extends ProfileException { + const StaffProfileNotFoundException({String? technicalMessage}) + : super(code: 'PROFILE_001', technicalMessage: technicalMessage); + + @override + String get messageKey => 'errors.profile.staff_not_found'; +} + +/// Thrown when business profile is not found. +class BusinessNotFoundException extends ProfileException { + const BusinessNotFoundException({String? technicalMessage}) + : super(code: 'PROFILE_002', technicalMessage: technicalMessage); + + @override + String get messageKey => 'errors.profile.business_not_found'; +} + +/// Thrown when profile update fails. +class ProfileUpdateFailedException extends ProfileException { + const ProfileUpdateFailedException({String? technicalMessage}) + : super(code: 'PROFILE_003', technicalMessage: technicalMessage); + + @override + String get messageKey => 'errors.profile.update_failed'; +} + +// ============================================================ +// SHIFT EXCEPTIONS +// ============================================================ + +/// Base class for shift-related exceptions. +sealed class ShiftException extends AppException { + const ShiftException({required super.code, super.technicalMessage}); +} + +/// Thrown when no open roles are available for a shift. +class NoOpenRolesException extends ShiftException { + const NoOpenRolesException({String? technicalMessage}) + : super(code: 'SHIFT_001', technicalMessage: technicalMessage); + + @override + String get messageKey => 'errors.shift.no_open_roles'; +} + +/// Thrown when application for shift is not found. +class ApplicationNotFoundException extends ShiftException { + const ApplicationNotFoundException({String? technicalMessage}) + : super(code: 'SHIFT_002', technicalMessage: technicalMessage); + + @override + String get messageKey => 'errors.shift.application_not_found'; +} + +/// Thrown when no active shift is found for clock out. +class NoActiveShiftException extends ShiftException { + const NoActiveShiftException({String? technicalMessage}) + : super(code: 'SHIFT_003', technicalMessage: technicalMessage); + + @override + String get messageKey => 'errors.shift.no_active_shift'; +} + +// ============================================================ +// NETWORK/GENERIC EXCEPTIONS +// ============================================================ + +/// Thrown when there is no network connection. +class NetworkException extends AppException { + const NetworkException({String? technicalMessage}) + : super(code: 'NET_001', technicalMessage: technicalMessage); + + @override + String get messageKey => 'errors.generic.no_connection'; +} + +/// Thrown when an unexpected error occurs. +class UnknownException extends AppException { + const UnknownException({String? technicalMessage}) + : super(code: 'UNKNOWN', technicalMessage: technicalMessage); + + @override + String get messageKey => 'errors.generic.unknown'; +} + +/// Thrown when user is not authenticated. +class NotAuthenticatedException extends AppException { + const NotAuthenticatedException({String? technicalMessage}) + : super(code: 'AUTH_NOT_LOGGED', technicalMessage: technicalMessage); + + @override + String get messageKey => 'errors.auth.not_authenticated'; +} diff --git a/apps/mobile/packages/features/client/authentication/lib/src/data/repositories_impl/auth_repository_impl.dart b/apps/mobile/packages/features/client/authentication/lib/src/data/repositories_impl/auth_repository_impl.dart index 3c7d387a..87146306 100644 --- a/apps/mobile/packages/features/client/authentication/lib/src/data/repositories_impl/auth_repository_impl.dart +++ b/apps/mobile/packages/features/client/authentication/lib/src/data/repositories_impl/auth_repository_impl.dart @@ -1,6 +1,19 @@ +import 'dart:developer' as developer; + import 'package:firebase_auth/firebase_auth.dart' as firebase; import 'package:firebase_data_connect/firebase_data_connect.dart'; import 'package:krow_data_connect/krow_data_connect.dart' as dc; +import 'package:krow_domain/krow_domain.dart' + show + InvalidCredentialsException, + SignInFailedException, + SignUpFailedException, + WeakPasswordException, + AccountExistsException, + UserNotFoundException, + UnauthorizedAppException, + PasswordMismatchException, + GoogleOnlyAccountException; import 'package:krow_domain/krow_domain.dart' as domain; import '../../domain/repositories/auth_repository_interface.dart'; @@ -33,24 +46,33 @@ class AuthRepositoryImpl implements AuthRepositoryInterface { final firebase.User? firebaseUser = credential.user; if (firebaseUser == null) { - throw Exception('Sign-in failed, no Firebase user received.'); + throw const SignInFailedException( + technicalMessage: 'No Firebase user received after sign-in', + ); } return _getUserProfile( firebaseUserId: firebaseUser.uid, fallbackEmail: firebaseUser.email ?? email, + requireBusinessRole: true, ); - //TO-DO: validate that user is business role and has business account - } on firebase.FirebaseAuthException catch (e) { if (e.code == 'invalid-credential' || e.code == 'wrong-password') { - throw Exception('Incorrect email or password.'); + throw InvalidCredentialsException( + technicalMessage: 'Firebase error code: ${e.code}', + ); } else { - throw Exception('Authentication error: ${e.message}'); + throw SignInFailedException( + technicalMessage: 'Firebase auth error: ${e.message}', + ); } + } on domain.AppException { + rethrow; } catch (e) { - throw Exception('Failed to sign in and fetch user data: ${e.toString()}'); + throw SignInFailedException( + technicalMessage: 'Unexpected error: $e', + ); } } @@ -60,63 +82,225 @@ class AuthRepositoryImpl implements AuthRepositoryInterface { required String email, required String password, }) async { + firebase.User? firebaseUser; + String? createdBusinessId; + try { + // Step 1: Try to create Firebase Auth user final firebase.UserCredential credential = await _firebaseAuth.createUserWithEmailAndPassword( email: email, password: password, ); + firebaseUser = credential.user; + if (firebaseUser == null) { + throw const SignUpFailedException( + technicalMessage: 'Firebase user could not be created', + ); + } + + // New user created successfully, proceed to create PostgreSQL entities + return await _createBusinessAndUser( + firebaseUser: firebaseUser, + companyName: companyName, + email: email, + onBusinessCreated: (String businessId) => createdBusinessId = businessId, + ); + + } on firebase.FirebaseAuthException catch (e) { + if (e.code == 'weak-password') { + throw WeakPasswordException( + technicalMessage: 'Firebase: ${e.message}', + ); + } else if (e.code == 'email-already-in-use') { + // Email exists in Firebase Auth - try to sign in and complete registration + return await _handleExistingFirebaseAccount( + email: email, + password: password, + companyName: companyName, + ); + } else { + throw SignUpFailedException( + technicalMessage: 'Firebase auth error: ${e.message}', + ); + } + } on domain.AppException { + // Rollback for our known exceptions + await _rollbackSignUp(firebaseUser: firebaseUser, businessId: createdBusinessId); + rethrow; + } catch (e) { + // Rollback: Clean up any partially created resources + await _rollbackSignUp(firebaseUser: firebaseUser, businessId: createdBusinessId); + throw SignUpFailedException( + technicalMessage: 'Unexpected error: $e', + ); + } + } + + /// Handles the case where email already exists in Firebase Auth. + /// + /// This can happen when: + /// 1. User signed up with Google in another app sharing the same Firebase project + /// 2. User already has a KROW account + /// + /// The flow: + /// 1. Try to sign in with provided password + /// 2. If sign-in succeeds, check if BUSINESS user exists in PostgreSQL + /// 3. If not, create Business + User (user is new to KROW) + /// 4. If yes, they already have a KROW account + Future _handleExistingFirebaseAccount({ + required String email, + required String password, + required String companyName, + }) async { + developer.log('Email exists in Firebase, attempting sign-in: $email', name: 'AuthRepository'); + + try { + // Try to sign in with the provided password + final firebase.UserCredential credential = await _firebaseAuth.signInWithEmailAndPassword( + email: email, + password: password, + ); + final firebase.User? firebaseUser = credential.user; if (firebaseUser == null) { - throw Exception('Sign-up failed, Firebase user could not be created.'); + throw const SignUpFailedException( + technicalMessage: 'Sign-in succeeded but no user returned', + ); } - // Client-specific business logic: - // 1. Create a `Business` entity. - // 2. Create a `User` entity associated with the business. - final OperationResult createBusinessResponse = await _dataConnect.createBusiness( - businessName: companyName, - userId: firebaseUser.uid, - rateGroup: dc.BusinessRateGroup.STANDARD, - status: dc.BusinessStatus.PENDING, - ).execute(); + // Sign-in succeeded! Check if user already has a BUSINESS account in PostgreSQL + final bool hasBusinessAccount = await _checkBusinessUserExists(firebaseUser.uid); - final dc.CreateBusinessBusinessInsert? businessData = createBusinessResponse.data?.business_insert; - if (businessData == null) { - await firebaseUser.delete(); // Rollback if business creation fails - throw Exception('Business creation failed after Firebase user registration.'); + if (hasBusinessAccount) { + // User already has a KROW Client account + developer.log('User already has BUSINESS account: ${firebaseUser.uid}', name: 'AuthRepository'); + throw AccountExistsException( + technicalMessage: 'User ${firebaseUser.uid} already has BUSINESS role', + ); } - final OperationResult createUserResponse = await _dataConnect.createUser( - id: firebaseUser.uid, - role: dc.UserBaseRole.USER, - ) - .email(email) - .userRole('BUSINESS') - .execute(); - - final dc.CreateUserUserInsert? newUserData = createUserResponse.data?.user_insert; - if (newUserData == null) { - await firebaseUser.delete(); // Rollback if user profile creation fails - // TO-DO: Also delete the created Business if this fails - throw Exception('User profile creation failed after Firebase user registration.'); - } - - return _getUserProfile( - firebaseUserId: firebaseUser.uid, - fallbackEmail: firebaseUser.email ?? email, + // User exists in Firebase but not in KROW PostgreSQL - create the entities + developer.log('Creating BUSINESS account for existing Firebase user: ${firebaseUser.uid}', name: 'AuthRepository'); + return await _createBusinessAndUser( + firebaseUser: firebaseUser, + companyName: companyName, + email: email, + onBusinessCreated: (_) {}, // No rollback needed for existing Firebase user ); } on firebase.FirebaseAuthException catch (e) { - if (e.code == 'weak-password') { - throw Exception('The password provided is too weak.'); - } else if (e.code == 'email-already-in-use') { - throw Exception('An account already exists for that email address.'); + // Sign-in failed - check why + developer.log('Sign-in failed with code: ${e.code}', name: 'AuthRepository'); + + if (e.code == 'wrong-password' || e.code == 'invalid-credential') { + // Password doesn't match - check what providers are available + return await _handlePasswordMismatch(email); } else { - throw Exception('Sign-up error: ${e.message}'); + throw SignUpFailedException( + technicalMessage: 'Firebase sign-in error: ${e.message}', + ); } + } on domain.AppException { + rethrow; + } + } + + /// Handles the case where the password doesn't match the existing account. + /// + /// Note: fetchSignInMethodsForEmail was deprecated by Firebase for security + /// reasons (email enumeration). We show a combined message that covers both + /// cases: wrong password OR account uses different sign-in method (Google). + Future _handlePasswordMismatch(String email) async { + // We can't distinguish between "wrong password" and "no password provider" + // due to Firebase deprecating fetchSignInMethodsForEmail. + // The PasswordMismatchException message covers both scenarios. + developer.log('Password mismatch or different provider for: $email', name: 'AuthRepository'); + throw PasswordMismatchException( + technicalMessage: 'Email $email: password mismatch or different auth provider', + ); + } + + /// Checks if a user with BUSINESS role exists in PostgreSQL. + Future _checkBusinessUserExists(String firebaseUserId) async { + try { + final QueryResult response = + await _dataConnect.getUserById(id: firebaseUserId).execute(); + final dc.GetUserByIdUser? user = response.data?.user; + return user != null && user.userRole == 'BUSINESS'; } catch (e) { - throw Exception('Failed to sign up and create user data: ${e.toString()}'); + developer.log('Error checking business user: $e', name: 'AuthRepository'); + return false; + } + } + + /// Creates Business and User entities in PostgreSQL for a Firebase user. + Future _createBusinessAndUser({ + required firebase.User firebaseUser, + required String companyName, + required String email, + required void Function(String businessId) onBusinessCreated, + }) async { + // Create Business entity in PostgreSQL + final OperationResult createBusinessResponse = + await _dataConnect.createBusiness( + businessName: companyName, + userId: firebaseUser.uid, + rateGroup: dc.BusinessRateGroup.STANDARD, + status: dc.BusinessStatus.PENDING, + ).execute(); + + final dc.CreateBusinessBusinessInsert? businessData = createBusinessResponse.data?.business_insert; + if (businessData == null) { + throw const SignUpFailedException( + technicalMessage: 'Business creation failed in PostgreSQL', + ); + } + onBusinessCreated(businessData.id); + + // Create User entity in PostgreSQL + final OperationResult createUserResponse = + await _dataConnect.createUser( + id: firebaseUser.uid, + role: dc.UserBaseRole.USER, + ) + .email(email) + .userRole('BUSINESS') + .execute(); + + final dc.CreateUserUserInsert? newUserData = createUserResponse.data?.user_insert; + if (newUserData == null) { + throw const SignUpFailedException( + technicalMessage: 'User profile creation failed in PostgreSQL', + ); + } + + return _getUserProfile( + firebaseUserId: firebaseUser.uid, + fallbackEmail: firebaseUser.email ?? email, + ); + } + + /// Rollback helper to clean up partially created resources during sign-up. + Future _rollbackSignUp({ + firebase.User? firebaseUser, + String? businessId, + }) async { + // Delete business first (if created) + if (businessId != null) { + try { + await _dataConnect.deleteBusiness(id: businessId).execute(); + } catch (_) { + // Log but don't throw - we're already in error recovery + } + } + // Delete Firebase user (if created) + if (firebaseUser != null) { + try { + await firebaseUser.delete(); + } catch (_) { + // Log but don't throw - we're already in error recovery + } } } @@ -138,16 +322,28 @@ class AuthRepositoryImpl implements AuthRepositoryInterface { Future _getUserProfile({ required String firebaseUserId, required String? fallbackEmail, + bool requireBusinessRole = false, }) async { final QueryResult response = await _dataConnect.getUserById(id: firebaseUserId).execute(); final dc.GetUserByIdUser? user = response.data?.user; if (user == null) { - throw Exception('Authenticated user profile not found in database.'); + throw UserNotFoundException( + technicalMessage: 'Firebase UID $firebaseUserId not found in users table', + ); + } + if (requireBusinessRole && user.userRole != 'BUSINESS') { + await _firebaseAuth.signOut(); + dc.ClientSessionStore.instance.clear(); + throw UnauthorizedAppException( + technicalMessage: 'User role is ${user.userRole}, expected BUSINESS', + ); } final String? email = user.email ?? fallbackEmail; if (email == null || email.isEmpty) { - throw Exception('User email is missing in profile data.'); + throw UserNotFoundException( + technicalMessage: 'User email missing for UID $firebaseUserId', + ); } final domain.User domainUser = domain.User( diff --git a/apps/mobile/packages/features/client/authentication/lib/src/presentation/blocs/client_auth_bloc.dart b/apps/mobile/packages/features/client/authentication/lib/src/presentation/blocs/client_auth_bloc.dart index e1c39429..b264922c 100644 --- a/apps/mobile/packages/features/client/authentication/lib/src/presentation/blocs/client_auth_bloc.dart +++ b/apps/mobile/packages/features/client/authentication/lib/src/presentation/blocs/client_auth_bloc.dart @@ -1,3 +1,5 @@ +import 'dart:developer' as developer; + import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:krow_domain/krow_domain.dart'; @@ -56,11 +58,20 @@ class ClientAuthBloc extends Bloc { SignInWithEmailArguments(email: event.email, password: event.password), ); emit(state.copyWith(status: ClientAuthStatus.authenticated, user: user)); - } catch (e) { + } on AppException catch (e) { + developer.log('Error ${e.code}: ${e.technicalMessage}', name: 'ClientAuthBloc'); emit( state.copyWith( status: ClientAuthStatus.error, - errorMessage: e.toString(), + errorMessage: e.messageKey, + ), + ); + } catch (e) { + developer.log('Unexpected error: $e', name: 'ClientAuthBloc'); + emit( + state.copyWith( + status: ClientAuthStatus.error, + errorMessage: 'errors.generic.unknown', ), ); } @@ -81,11 +92,20 @@ class ClientAuthBloc extends Bloc { ), ); emit(state.copyWith(status: ClientAuthStatus.authenticated, user: user)); - } catch (e) { + } on AppException catch (e) { + developer.log('Error ${e.code}: ${e.technicalMessage}', name: 'ClientAuthBloc'); emit( state.copyWith( status: ClientAuthStatus.error, - errorMessage: e.toString(), + errorMessage: e.messageKey, + ), + ); + } catch (e) { + developer.log('Unexpected error: $e', name: 'ClientAuthBloc'); + emit( + state.copyWith( + status: ClientAuthStatus.error, + errorMessage: 'errors.generic.unknown', ), ); } @@ -102,11 +122,20 @@ class ClientAuthBloc extends Bloc { SignInWithSocialArguments(provider: event.provider), ); emit(state.copyWith(status: ClientAuthStatus.authenticated, user: user)); - } catch (e) { + } on AppException catch (e) { + developer.log('Error ${e.code}: ${e.technicalMessage}', name: 'ClientAuthBloc'); emit( state.copyWith( status: ClientAuthStatus.error, - errorMessage: e.toString(), + errorMessage: e.messageKey, + ), + ); + } catch (e) { + developer.log('Unexpected error: $e', name: 'ClientAuthBloc'); + emit( + state.copyWith( + status: ClientAuthStatus.error, + errorMessage: 'errors.generic.unknown', ), ); } @@ -121,11 +150,20 @@ class ClientAuthBloc extends Bloc { try { await _signOut(); emit(state.copyWith(status: ClientAuthStatus.signedOut, user: null)); - } catch (e) { + } on AppException catch (e) { + developer.log('Error ${e.code}: ${e.technicalMessage}', name: 'ClientAuthBloc'); emit( state.copyWith( status: ClientAuthStatus.error, - errorMessage: e.toString(), + errorMessage: e.messageKey, + ), + ); + } catch (e) { + developer.log('Unexpected error: $e', name: 'ClientAuthBloc'); + emit( + state.copyWith( + status: ClientAuthStatus.error, + errorMessage: 'errors.generic.unknown', ), ); } diff --git a/apps/mobile/packages/features/client/authentication/lib/src/presentation/pages/client_sign_in_page.dart b/apps/mobile/packages/features/client/authentication/lib/src/presentation/pages/client_sign_in_page.dart index 6bde0059..33df7cbe 100644 --- a/apps/mobile/packages/features/client/authentication/lib/src/presentation/pages/client_sign_in_page.dart +++ b/apps/mobile/packages/features/client/authentication/lib/src/presentation/pages/client_sign_in_page.dart @@ -45,10 +45,11 @@ class ClientSignInPage extends StatelessWidget { if (state.status == ClientAuthStatus.authenticated) { Modular.to.navigateClientHome(); } else if (state.status == ClientAuthStatus.error) { + final String errorMessage = state.errorMessage != null + ? translateErrorKey(state.errorMessage!) + : t.errors.generic.unknown; ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(state.errorMessage ?? 'Authentication Error'), - ), + SnackBar(content: Text(errorMessage)), ); } }, diff --git a/apps/mobile/packages/features/client/authentication/lib/src/presentation/pages/client_sign_up_page.dart b/apps/mobile/packages/features/client/authentication/lib/src/presentation/pages/client_sign_up_page.dart index 0df74969..2453b486 100644 --- a/apps/mobile/packages/features/client/authentication/lib/src/presentation/pages/client_sign_up_page.dart +++ b/apps/mobile/packages/features/client/authentication/lib/src/presentation/pages/client_sign_up_page.dart @@ -49,10 +49,11 @@ class ClientSignUpPage extends StatelessWidget { if (state.status == ClientAuthStatus.authenticated) { Modular.to.navigateClientHome(); } else if (state.status == ClientAuthStatus.error) { + final String errorMessage = state.errorMessage != null + ? translateErrorKey(state.errorMessage!) + : t.errors.generic.unknown; ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(state.errorMessage ?? 'Authentication Error'), - ), + SnackBar(content: Text(errorMessage)), ); } }, diff --git a/apps/mobile/packages/features/client/billing/lib/src/billing_module.dart b/apps/mobile/packages/features/client/billing/lib/src/billing_module.dart index 39ab732d..2a7f9677 100644 --- a/apps/mobile/packages/features/client/billing/lib/src/billing_module.dart +++ b/apps/mobile/packages/features/client/billing/lib/src/billing_module.dart @@ -15,9 +15,8 @@ import 'presentation/pages/billing_page.dart'; class BillingModule extends Module { @override void binds(Injector i) { - // External Dependencies (Mocks from data_connect) - // In a real app, these would likely be provided by a Core module or similar. - i.addSingleton(FinancialRepositoryMock.new); + // Mock repositories (TODO: Replace with real implementations) + i.addSingleton(FinancialRepositoryMock.new); // Repositories i.addSingleton( diff --git a/apps/mobile/packages/features/client/billing/lib/src/data/repositories_impl/billing_repository_impl.dart b/apps/mobile/packages/features/client/billing/lib/src/data/repositories_impl/billing_repository_impl.dart index 4006f92c..9fa8d5cb 100644 --- a/apps/mobile/packages/features/client/billing/lib/src/data/repositories_impl/billing_repository_impl.dart +++ b/apps/mobile/packages/features/client/billing/lib/src/data/repositories_impl/billing_repository_impl.dart @@ -166,9 +166,10 @@ class BillingRepositoryImpl implements BillingRepository { } fdc.Timestamp _toTimestamp(DateTime dateTime) { - final int seconds = dateTime.millisecondsSinceEpoch ~/ 1000; + final DateTime utc = dateTime.toUtc(); + final int seconds = utc.millisecondsSinceEpoch ~/ 1000; final int nanoseconds = - (dateTime.millisecondsSinceEpoch % 1000) * 1000000; + (utc.millisecondsSinceEpoch % 1000) * 1000000; return fdc.Timestamp(nanoseconds, seconds); } 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 8fb39115..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 @@ -1,17 +1,16 @@ +import 'package:core_localization/core_localization.dart'; import 'package:design_system/design_system.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_modular/flutter_modular.dart'; + import '../blocs/billing_bloc.dart'; import '../blocs/billing_event.dart'; import '../blocs/billing_state.dart'; -import '../widgets/billing_header.dart'; -import '../widgets/pending_invoices_section.dart'; -import '../widgets/payment_method_card.dart'; -import '../widgets/spending_breakdown_card.dart'; -import '../widgets/savings_card.dart'; import '../widgets/invoice_history_section.dart'; -import '../widgets/export_invoices_button.dart'; +import '../widgets/payment_method_card.dart'; +import '../widgets/pending_invoices_section.dart'; +import '../widgets/spending_breakdown_card.dart'; /// The entry point page for the client billing feature. /// @@ -34,24 +33,136 @@ class BillingPage extends StatelessWidget { /// /// This widget displays the billing dashboard content based on the current /// state of the [BillingBloc]. -class BillingView extends StatelessWidget { +class BillingView extends StatefulWidget { /// Creates a [BillingView]. const BillingView({super.key}); + @override + State createState() => _BillingViewState(); +} + +class _BillingViewState extends State { + late ScrollController _scrollController; + bool _isScrolled = false; + + @override + void initState() { + super.initState(); + _scrollController = ScrollController(); + _scrollController.addListener(_onScroll); + } + + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } + + void _onScroll() { + if (_scrollController.hasClients) { + if (_scrollController.offset > 140 && !_isScrolled) { + setState(() => _isScrolled = true); + } else if (_scrollController.offset <= 140 && _isScrolled) { + setState(() => _isScrolled = false); + } + } + } + @override Widget build(BuildContext context) { return BlocBuilder( builder: (BuildContext context, BillingState state) { return Scaffold( - backgroundColor: UiColors.bgPrimary, - body: Column( - children: [ - BillingHeader( - currentBill: state.currentBill, - savings: state.savings, - onBack: () => Modular.to.pop(), + body: CustomScrollView( + controller: _scrollController, + slivers: [ + SliverAppBar( + pinned: true, + expandedHeight: 200.0, + backgroundColor: UiColors.primary, + leading: Center( + child: UiIconButton.secondary( + icon: UiIcons.arrowLeft, + onTap: () => Modular.to.navigate('/client-main/home/'), + ), + ), + title: AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + child: Text( + _isScrolled + ? '\$${state.currentBill.toStringAsFixed(2)}' + : t.client_billing.title, + key: ValueKey(_isScrolled), + style: UiTypography.headline4m.copyWith( + color: UiColors.white, + ), + ), + ), + flexibleSpace: FlexibleSpaceBar( + background: Padding( + padding: const EdgeInsets.only( + top: UiConstants.space0, + left: UiConstants.space5, + right: UiConstants.space5, + bottom: UiConstants.space10, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Text( + t.client_billing.current_period, + style: UiTypography.footnote2r.copyWith( + color: UiColors.white.withValues(alpha: 0.7), + ), + ), + const SizedBox(height: UiConstants.space1), + Text( + '\$${state.currentBill.toStringAsFixed(2)}', + style: UiTypography.display1b + .copyWith(color: UiColors.white), + ), + const SizedBox(height: UiConstants.space2), + Container( + padding: const EdgeInsets.symmetric( + horizontal: UiConstants.space2, + vertical: UiConstants.space1, + ), + decoration: BoxDecoration( + color: UiColors.accent, + borderRadius: BorderRadius.circular(100), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon( + UiIcons.trendingDown, + size: 12, + color: UiColors.foreground, + ), + const SizedBox(width: UiConstants.space1), + Text( + t.client_billing.saved_amount( + amount: state.savings.toStringAsFixed(0), + ), + style: UiTypography.footnote2b.copyWith( + color: UiColors.foreground, + ), + ), + ], + ), + ), + ], + ), + ), + ), + ), + SliverList( + delegate: SliverChildListDelegate( + [ + _buildContent(context, state), + ], + ), ), - Expanded(child: _buildContent(context, state)), ], ), ); @@ -61,7 +172,10 @@ class BillingView extends StatelessWidget { Widget _buildContent(BuildContext context, BillingState state) { if (state.status == BillingStatus.loading) { - return const Center(child: CircularProgressIndicator()); + return const Padding( + padding: EdgeInsets.all(UiConstants.space10), + child: Center(child: CircularProgressIndicator()), + ); } if (state.status == BillingStatus.failure) { @@ -73,25 +187,51 @@ class BillingView extends StatelessWidget { ); } - return SingleChildScrollView( + return Padding( padding: const EdgeInsets.all(UiConstants.space5), child: Column( crossAxisAlignment: CrossAxisAlignment.start, + spacing: UiConstants.space4, children: [ if (state.pendingInvoices.isNotEmpty) ...[ PendingInvoicesSection(invoices: state.pendingInvoices), - const SizedBox(height: UiConstants.space4), ], const PaymentMethodCard(), - const SizedBox(height: UiConstants.space4), const SpendingBreakdownCard(), + if (state.invoiceHistory.isEmpty) _buildEmptyState(context) + else InvoiceHistorySection(invoices: state.invoiceHistory), + + const SizedBox(height: UiConstants.space32), + ], + ), + ); + } + + Widget _buildEmptyState(BuildContext context) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox(height: UiConstants.space12), + Container( + padding: const EdgeInsets.all(UiConstants.space6), + decoration: BoxDecoration( + color: UiColors.bgPopup, + shape: BoxShape.circle, + border: Border.all(color: UiColors.border), + ), + child: const Icon( + UiIcons.file, + size: 48, + color: UiColors.textSecondary, + ), + ), const SizedBox(height: UiConstants.space4), - SavingsCard(savings: state.savings), - const SizedBox(height: UiConstants.space6), - InvoiceHistorySection(invoices: state.invoiceHistory), - const SizedBox(height: UiConstants.space6), - const ExportInvoicesButton(), - const SizedBox(height: UiConstants.space6), + Text( + 'No Invoices for the selected period', + style: UiTypography.body1m.textSecondary, + textAlign: TextAlign.center, + ), ], ), ); diff --git a/apps/mobile/packages/features/client/billing/lib/src/presentation/widgets/payment_method_card.dart b/apps/mobile/packages/features/client/billing/lib/src/presentation/widgets/payment_method_card.dart index 6c846212..6deda772 100644 --- a/apps/mobile/packages/features/client/billing/lib/src/presentation/widgets/payment_method_card.dart +++ b/apps/mobile/packages/features/client/billing/lib/src/presentation/widgets/payment_method_card.dart @@ -24,11 +24,13 @@ class _PaymentMethodCardState extends State { return null; } - final fdc.QueryResult result = - await dc.ExampleConnector.instance - .getAccountsByOwnerId(ownerId: businessId) - .execute(); + final fdc.QueryResult< + dc.GetAccountsByOwnerIdData, + dc.GetAccountsByOwnerIdVariables + > + result = await dc.ExampleConnector.instance + .getAccountsByOwnerId(ownerId: businessId) + .execute(); return result.data; } @@ -36,115 +38,123 @@ class _PaymentMethodCardState extends State { Widget build(BuildContext context) { return FutureBuilder( future: _accountsFuture, - builder: (BuildContext context, - AsyncSnapshot snapshot) { - final List accounts = - snapshot.data?.accounts ?? - []; - final dc.GetAccountsByOwnerIdAccounts? account = - accounts.isNotEmpty ? accounts.first : null; - final String bankLabel = - account?.bank.isNotEmpty == true ? account!.bank : '----'; - final String last4 = - account?.last4.isNotEmpty == true ? account!.last4 : '----'; - final bool isPrimary = account?.isPrimary ?? false; - final String expiryLabel = _formatExpiry(account?.expiryTime); + builder: + ( + BuildContext context, + AsyncSnapshot snapshot, + ) { + final List accounts = + snapshot.data?.accounts ?? []; + final dc.GetAccountsByOwnerIdAccounts? account = accounts.isNotEmpty + ? accounts.first + : null; - return Container( - padding: const EdgeInsets.all(UiConstants.space4), - decoration: BoxDecoration( - color: UiColors.white, - borderRadius: UiConstants.radiusLg, - border: Border.all(color: UiColors.border), - boxShadow: [ - BoxShadow( - color: UiColors.black.withValues(alpha: 0.04), - blurRadius: 8, - offset: const Offset(0, 2), - ), - ], - ), - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - t.client_billing.payment_method, - style: UiTypography.title2b.textPrimary, - ), - const SizedBox.shrink(), - ], - ), - if (account != null) ...[ - const SizedBox(height: UiConstants.space3), - Container( - padding: const EdgeInsets.all(UiConstants.space3), - decoration: BoxDecoration( - color: UiColors.bgSecondary, - borderRadius: UiConstants.radiusMd, + if (account == null) { + return const SizedBox.shrink(); + } + + final String bankLabel = account.bank.isNotEmpty == true + ? account.bank + : '----'; + final String last4 = account.last4.isNotEmpty == true + ? account.last4 + : '----'; + final bool isPrimary = account.isPrimary ?? false; + final String expiryLabel = _formatExpiry(account.expiryTime); + + return Container( + padding: const EdgeInsets.all(UiConstants.space4), + decoration: BoxDecoration( + color: UiColors.white, + borderRadius: UiConstants.radiusLg, + border: Border.all(color: UiColors.border), + boxShadow: [ + BoxShadow( + color: UiColors.black.withValues(alpha: 0.04), + blurRadius: 8, + offset: const Offset(0, 2), ), - child: Row( + ], + ), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Container( - width: 40, - height: 28, - decoration: BoxDecoration( - color: UiColors.primary, - borderRadius: BorderRadius.circular(4), - ), - child: Center( - child: Text( - bankLabel, - style: const TextStyle( - color: UiColors.white, - fontSize: 10, - fontWeight: FontWeight.bold, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), + Text( + t.client_billing.payment_method, + style: UiTypography.title2b.textPrimary, ), - const SizedBox(width: UiConstants.space3), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '•••• $last4', - style: UiTypography.body2b.textPrimary, - ), - Text( - t.client_billing.expires(date: expiryLabel), - style: UiTypography.footnote2r.textSecondary, - ), - ], - ), - ), - if (isPrimary) - Container( - padding: const EdgeInsets.symmetric( - horizontal: 6, - vertical: 2, - ), - decoration: BoxDecoration( - color: UiColors.accent, - borderRadius: BorderRadius.circular(4), - ), - child: Text( - t.client_billing.default_badge, - style: UiTypography.titleUppercase4b.textPrimary, - ), - ), + const SizedBox.shrink(), ], ), - ), - ], - ], - ), - ); - }, + const SizedBox(height: UiConstants.space3), + Container( + padding: const EdgeInsets.all(UiConstants.space3), + decoration: BoxDecoration( + color: UiColors.bgSecondary, + borderRadius: UiConstants.radiusMd, + ), + child: Row( + children: [ + Container( + width: 40, + height: 28, + decoration: BoxDecoration( + color: UiColors.primary, + borderRadius: BorderRadius.circular(4), + ), + child: Center( + child: Text( + bankLabel, + style: const TextStyle( + color: UiColors.white, + fontSize: 10, + fontWeight: FontWeight.bold, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ), + const SizedBox(width: UiConstants.space3), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '•••• $last4', + style: UiTypography.body2b.textPrimary, + ), + Text( + t.client_billing.expires(date: expiryLabel), + style: UiTypography.footnote2r.textSecondary, + ), + ], + ), + ), + if (isPrimary) + Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 2, + ), + decoration: BoxDecoration( + color: UiColors.accent, + borderRadius: BorderRadius.circular(4), + ), + child: Text( + t.client_billing.default_badge, + style: UiTypography.titleUppercase4b.textPrimary, + ), + ), + ], + ), + ), + ], + ), + ); + }, ); } diff --git a/apps/mobile/packages/features/client/billing/pubspec.yaml b/apps/mobile/packages/features/client/billing/pubspec.yaml index d8165fe9..74ec711f 100644 --- a/apps/mobile/packages/features/client/billing/pubspec.yaml +++ b/apps/mobile/packages/features/client/billing/pubspec.yaml @@ -1,7 +1,7 @@ name: billing description: Client Billing feature package publish_to: 'none' -version: 1.0.0+1 +version: 0.0.1 resolution: workspace environment: 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 fa29d536..b79c7ddc 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, @@ -100,9 +98,10 @@ class CoverageRepositoryImpl implements CoverageRepository { } fdc.Timestamp _toTimestamp(DateTime dateTime) { - final int seconds = dateTime.millisecondsSinceEpoch ~/ 1000; + final DateTime utc = dateTime.toUtc(); + final int seconds = utc.millisecondsSinceEpoch ~/ 1000; final int nanoseconds = - (dateTime.millisecondsSinceEpoch % 1000) * 1000000; + (utc.millisecondsSinceEpoch % 1000) * 1000000; return fdc.Timestamp(nanoseconds, seconds); } @@ -123,7 +122,7 @@ class CoverageRepositoryImpl implements CoverageRepository { shiftId: shiftRole.shiftId, roleId: shiftRole.roleId, title: shiftRole.role.name, - location: shiftRole.shift.location ?? '', + location: shiftRole.shift.location ?? shiftRole.shift.locationAddress ?? '', startTime: _formatTime(shiftRole.startTime) ?? '00:00', workersNeeded: shiftRole.count, date: shiftRole.shift.date?.toDateTime() ?? date, @@ -171,31 +170,39 @@ 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.confirmed; 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 'confirmed'; + return CoverageWorkerStatus.noShow; + case dc.ApplicationStatus.COMPLETED: + return CoverageWorkerStatus.completed; } } - return 'confirmed'; + return CoverageWorkerStatus.pending; } String? _formatTime(fdc.Timestamp? timestamp) { if (timestamp == null) { return null; } - final DateTime date = timestamp.toDateTime(); + final DateTime date = timestamp.toDateTime().toLocal(); final String hour = date.hour.toString().padLeft(2, '0'); final String minute = date.minute.toString().padLeft(2, '0'); return '$hour:$minute'; 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 50758e8c..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 100; - 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 100; - 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/pages/coverage_page.dart b/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/pages/coverage_page.dart index 441c6040..52f5388f 100644 --- a/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/pages/coverage_page.dart +++ b/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/pages/coverage_page.dart @@ -2,11 +2,12 @@ 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 '../blocs/coverage_bloc.dart'; import '../blocs/coverage_event.dart'; import '../blocs/coverage_state.dart'; -import '../widgets/coverage_header.dart'; +import '../widgets/coverage_calendar_selector.dart'; import '../widgets/coverage_quick_stats.dart'; import '../widgets/coverage_shift_list.dart'; import '../widgets/late_workers_alert.dart'; @@ -14,39 +15,202 @@ import '../widgets/late_workers_alert.dart'; /// Page for displaying daily coverage information. /// /// Shows shifts, worker statuses, and coverage statistics for a selected date. -class CoveragePage extends StatelessWidget { +class CoveragePage extends StatefulWidget { /// Creates a [CoveragePage]. const CoveragePage({super.key}); + @override + State createState() => _CoveragePageState(); +} + +class _CoveragePageState extends State { + late ScrollController _scrollController; + bool _isScrolled = false; + + @override + void initState() { + super.initState(); + _scrollController = ScrollController(); + _scrollController.addListener(_onScroll); + } + + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } + + void _onScroll() { + if (_scrollController.hasClients) { + if (_scrollController.offset > 180 && !_isScrolled) { + setState(() => _isScrolled = true); + } else if (_scrollController.offset <= 180 && _isScrolled) { + setState(() => _isScrolled = false); + } + } + } + @override Widget build(BuildContext context) { return BlocProvider( create: (BuildContext context) => Modular.get() ..add(CoverageLoadRequested(date: DateTime.now())), child: Scaffold( - backgroundColor: UiColors.background, body: BlocBuilder( builder: (BuildContext context, CoverageState state) { - return Column( - children: [ - CoverageHeader( - selectedDate: state.selectedDate ?? DateTime.now(), - coveragePercent: state.stats?.coveragePercent ?? 0, - totalConfirmed: state.stats?.totalConfirmed ?? 0, - totalNeeded: state.stats?.totalNeeded ?? 0, - onDateSelected: (DateTime date) { - BlocProvider.of(context).add( - CoverageLoadRequested(date: date), - ); - }, - onRefresh: () { - BlocProvider.of(context).add( - const CoverageRefreshRequested(), - ); - }, + final DateTime selectedDate = state.selectedDate ?? DateTime.now(); + + return CustomScrollView( + controller: _scrollController, + slivers: [ + SliverAppBar( + pinned: true, + expandedHeight: 300.0, + backgroundColor: UiColors.primary, + leading: IconButton( + onPressed: () => Modular.to.navigate('/client-main/home/'), + icon: Container( + padding: const EdgeInsets.all(UiConstants.space2), + decoration: BoxDecoration( + color: UiColors.primaryForeground.withOpacity(0.2), + shape: BoxShape.circle, + ), + child: const Icon( + UiIcons.arrowLeft, + color: UiColors.primaryForeground, + size: UiConstants.space4, + ), + ), + ), + title: AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + child: Text( + _isScrolled + ? DateFormat('MMMM d').format(selectedDate) + : 'Daily Coverage', + key: ValueKey(_isScrolled), + style: UiTypography.title2m.copyWith( + color: UiColors.primaryForeground, + ), + ), + ), + actions: [ + IconButton( + onPressed: () { + BlocProvider.of(context).add( + const CoverageRefreshRequested(), + ); + }, + icon: Container( + padding: const EdgeInsets.all(UiConstants.space2), + decoration: BoxDecoration( + color: UiColors.primaryForeground.withOpacity(0.2), + borderRadius: UiConstants.radiusMd, + ), + child: const Icon( + UiIcons.rotateCcw, + color: UiColors.primaryForeground, + size: UiConstants.space4, + ), + ), + ), + const SizedBox(width: UiConstants.space4), + ], + flexibleSpace: Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + colors: [ + UiColors.primary, + UiColors.primary, + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + ), + child: FlexibleSpaceBar( + background: Padding( + padding: const EdgeInsets.fromLTRB( + UiConstants.space5, + 100, // Top padding to clear AppBar + UiConstants.space5, + UiConstants.space4, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + CoverageCalendarSelector( + selectedDate: selectedDate, + onDateSelected: (DateTime date) { + BlocProvider.of(context).add( + CoverageLoadRequested(date: date), + ); + }, + ), + const SizedBox(height: UiConstants.space4), + // Coverage Stats Container + Container( + padding: const EdgeInsets.all(UiConstants.space4), + decoration: BoxDecoration( + color: + UiColors.primaryForeground.withOpacity(0.1), + borderRadius: UiConstants.radiusLg, + ), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + 'Coverage Status', + style: UiTypography.body2r.copyWith( + color: UiColors.primaryForeground + .withOpacity(0.7), + ), + ), + Text( + '${state.stats?.coveragePercent ?? 0}%', + style: UiTypography.display1b.copyWith( + color: UiColors.primaryForeground, + ), + ), + ], + ), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + 'Workers', + style: UiTypography.body2r.copyWith( + color: UiColors.primaryForeground + .withOpacity(0.7), + ), + ), + Text( + '${state.stats?.totalConfirmed ?? 0}/${state.stats?.totalNeeded ?? 0}', + style: UiTypography.title2m.copyWith( + color: UiColors.primaryForeground, + ), + ), + ], + ), + ], + ), + ), + ], + ), + ), + ), + ), ), - Expanded( - child: _buildBody(context: context, state: state), + SliverList( + delegate: SliverChildListDelegate( + [ + _buildBody(context: context, state: state), + ], + ), ), ], ); @@ -100,7 +264,7 @@ class CoveragePage extends StatelessWidget { ); } - return SingleChildScrollView( + return Padding( padding: const EdgeInsets.all(UiConstants.space5), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -114,13 +278,16 @@ class CoveragePage extends StatelessWidget { const SizedBox(height: UiConstants.space5), ], Text( - 'Shifts', + 'Shifts (${state.shifts.length})', style: UiTypography.title2b.copyWith( color: UiColors.textPrimary, ), ), const SizedBox(height: UiConstants.space3), CoverageShiftList(shifts: state.shifts), + const SizedBox( + height: UiConstants.space24, + ), ], ), ); diff --git a/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/widgets/coverage_header.dart b/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/widgets/coverage_header.dart index 6c9513bb..f346e8fd 100644 --- a/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/widgets/coverage_header.dart +++ b/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/widgets/coverage_header.dart @@ -67,7 +67,7 @@ class CoverageHeader extends StatelessWidget { Row( children: [ GestureDetector( - onTap: () => Modular.to.pop(), + onTap: () => Modular.to.navigate('/client-main/home/'), child: Container( width: UiConstants.space10, height: UiConstants.space10, 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 0732c389..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. /// @@ -35,24 +35,23 @@ class CoverageShiftList extends StatelessWidget { if (shifts.isEmpty) { return Container( padding: const EdgeInsets.all(UiConstants.space8), + width: double.infinity, decoration: BoxDecoration( color: UiColors.bgPopup, borderRadius: UiConstants.radiusLg, border: Border.all(color: UiColors.border), ), child: Column( + spacing: UiConstants.space4, children: [ const Icon( UiIcons.users, size: UiConstants.space12, - color: UiColors.mutedForeground, + color: UiColors.textSecondary, ), - const SizedBox(height: UiConstants.space3), Text( 'No shifts scheduled for this day', - style: UiTypography.body2r.copyWith( - color: UiColors.mutedForeground, - ), + style: UiTypography.body2r.textSecondary, ), ], ), @@ -160,12 +159,15 @@ class _ShiftHeader extends StatelessWidget { ), ), child: Row( + spacing: UiConstants.space4, children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, + spacing: UiConstants.space2, children: [ Row( + spacing: UiConstants.space2, children: [ Container( width: UiConstants.space2, @@ -175,42 +177,44 @@ class _ShiftHeader extends StatelessWidget { shape: BoxShape.circle, ), ), - const SizedBox(width: UiConstants.space2), Text( title, - style: UiTypography.body1b.copyWith( - color: UiColors.textPrimary, - ), + style: UiTypography.body1b.textPrimary, ), ], ), - const SizedBox(height: UiConstants.space2), - Row( + Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Icon( - UiIcons.mapPin, - size: UiConstants.space3, - color: UiColors.mutedForeground, + Row( + spacing: UiConstants.space1, + children: [ + const Icon( + UiIcons.mapPin, + size: UiConstants.space3, + color: UiColors.iconSecondary, + ), + Expanded( + child: Text( + location, + style: UiTypography.body3r.textSecondary, + overflow: TextOverflow.ellipsis, + )), + ], ), - const SizedBox(width: UiConstants.space1), - Text( - location, - style: UiTypography.body3r.copyWith( - color: UiColors.mutedForeground, - ), - ), - const SizedBox(width: UiConstants.space3), - const Icon( - UiIcons.clock, - size: UiConstants.space3, - color: UiColors.mutedForeground, - ), - const SizedBox(width: UiConstants.space1), - Text( - startTime, - style: UiTypography.body3r.copyWith( - color: UiColors.mutedForeground, - ), + Row( + spacing: UiConstants.space1, + children: [ + const Icon( + UiIcons.clock, + size: UiConstants.space3, + color: UiColors.iconSecondary, + ), + Text( + startTime, + style: UiTypography.body3r.textSecondary, + ), + ], ), ], ), @@ -311,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/client/client_coverage/pubspec.lock b/apps/mobile/packages/features/client/client_coverage/pubspec.lock deleted file mode 100644 index 6dd6fbaf..00000000 --- a/apps/mobile/packages/features/client/client_coverage/pubspec.lock +++ /dev/null @@ -1,650 +0,0 @@ -# Generated by pub -# See https://dart.dev/tools/pub/glossary#lockfile -packages: - async: - dependency: transitive - description: - name: async - sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" - url: "https://pub.dev" - source: hosted - version: "2.13.0" - auto_injector: - dependency: transitive - description: - name: auto_injector - sha256: "1fc2624898e92485122eb2b1698dd42511d7ff6574f84a3a8606fc4549a1e8f8" - url: "https://pub.dev" - source: hosted - version: "2.1.1" - bloc: - dependency: transitive - description: - name: bloc - sha256: "106842ad6569f0b60297619e9e0b1885c2fb9bf84812935490e6c5275777804e" - url: "https://pub.dev" - source: hosted - version: "8.1.4" - boolean_selector: - dependency: transitive - description: - name: boolean_selector - sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" - url: "https://pub.dev" - source: hosted - version: "2.1.2" - characters: - dependency: transitive - description: - name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 - url: "https://pub.dev" - source: hosted - version: "1.4.0" - clock: - dependency: transitive - description: - name: clock - sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b - url: "https://pub.dev" - source: hosted - version: "1.1.2" - code_assets: - dependency: transitive - description: - name: code_assets - sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687" - url: "https://pub.dev" - source: hosted - version: "1.0.0" - collection: - dependency: transitive - description: - name: collection - sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" - url: "https://pub.dev" - source: hosted - version: "1.19.1" - core_localization: - dependency: "direct main" - description: - path: "../../../core_localization" - relative: true - source: path - version: "0.0.1" - crypto: - dependency: transitive - description: - name: crypto - sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf - url: "https://pub.dev" - source: hosted - version: "3.0.7" - csv: - dependency: transitive - description: - name: csv - sha256: c6aa2679b2a18cb57652920f674488d89712efaf4d3fdf2e537215b35fc19d6c - url: "https://pub.dev" - source: hosted - version: "6.0.0" - design_system: - dependency: "direct main" - description: - path: "../../../design_system" - relative: true - source: path - version: "0.0.1" - equatable: - dependency: "direct main" - description: - name: equatable - sha256: "3e0141505477fd8ad55d6eb4e7776d3fe8430be8e497ccb1521370c3f21a3e2b" - url: "https://pub.dev" - source: hosted - version: "2.0.8" - fake_async: - dependency: transitive - description: - name: fake_async - sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" - url: "https://pub.dev" - source: hosted - version: "1.3.3" - ffi: - dependency: transitive - description: - name: ffi - sha256: d07d37192dbf97461359c1518788f203b0c9102cfd2c35a716b823741219542c - url: "https://pub.dev" - source: hosted - version: "2.1.5" - file: - dependency: transitive - description: - name: file - sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 - url: "https://pub.dev" - source: hosted - version: "7.0.1" - fixnum: - dependency: transitive - description: - name: fixnum - sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be - url: "https://pub.dev" - source: hosted - version: "1.1.1" - flutter: - dependency: "direct main" - description: flutter - source: sdk - version: "0.0.0" - flutter_bloc: - dependency: "direct main" - description: - name: flutter_bloc - sha256: b594505eac31a0518bdcb4b5b79573b8d9117b193cc80cc12e17d639b10aa27a - url: "https://pub.dev" - source: hosted - version: "8.1.6" - flutter_lints: - dependency: "direct dev" - description: - name: flutter_lints - sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" - url: "https://pub.dev" - source: hosted - version: "5.0.0" - flutter_localizations: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" - flutter_modular: - dependency: "direct main" - description: - name: flutter_modular - sha256: "33a63d9fe61429d12b3dfa04795ed890f17d179d3d38e988ba7969651fcd5586" - url: "https://pub.dev" - source: hosted - version: "6.4.1" - flutter_test: - dependency: "direct dev" - description: flutter - source: sdk - version: "0.0.0" - flutter_web_plugins: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" - font_awesome_flutter: - dependency: transitive - description: - name: font_awesome_flutter - sha256: b9011df3a1fa02993630b8fb83526368cf2206a711259830325bab2f1d2a4eb0 - url: "https://pub.dev" - source: hosted - version: "10.12.0" - glob: - dependency: transitive - description: - name: glob - sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de - url: "https://pub.dev" - source: hosted - version: "2.1.3" - google_fonts: - dependency: transitive - description: - name: google_fonts - sha256: "6996212014b996eaa17074e02b1b925b212f5e053832d9048970dc27255a8fb3" - url: "https://pub.dev" - source: hosted - version: "7.1.0" - hooks: - dependency: transitive - description: - name: hooks - sha256: "5d309c86e7ce34cd8e37aa71cb30cb652d3829b900ab145e4d9da564b31d59f7" - url: "https://pub.dev" - source: hosted - version: "1.0.0" - http: - dependency: transitive - description: - name: http - sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" - url: "https://pub.dev" - source: hosted - version: "1.6.0" - http_parser: - dependency: transitive - description: - name: http_parser - sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" - url: "https://pub.dev" - source: hosted - version: "4.1.2" - intl: - dependency: "direct main" - description: - name: intl - sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" - url: "https://pub.dev" - source: hosted - version: "0.20.2" - krow_core: - dependency: "direct main" - description: - path: "../../../core" - relative: true - source: path - version: "0.0.1" - krow_data_connect: - dependency: "direct main" - description: - path: "../../../data_connect" - relative: true - source: path - version: "0.0.1" - krow_domain: - dependency: "direct main" - description: - path: "../../../domain" - relative: true - source: path - version: "0.0.1" - leak_tracker: - dependency: transitive - description: - name: leak_tracker - sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" - url: "https://pub.dev" - source: hosted - version: "11.0.2" - leak_tracker_flutter_testing: - dependency: transitive - description: - name: leak_tracker_flutter_testing - sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" - url: "https://pub.dev" - source: hosted - version: "3.0.10" - leak_tracker_testing: - dependency: transitive - description: - name: leak_tracker_testing - sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" - url: "https://pub.dev" - source: hosted - version: "3.0.2" - lints: - dependency: transitive - description: - name: lints - sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 - url: "https://pub.dev" - source: hosted - version: "5.1.1" - logging: - dependency: transitive - description: - name: logging - sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 - url: "https://pub.dev" - source: hosted - version: "1.3.0" - lucide_icons: - dependency: transitive - description: - name: lucide_icons - sha256: ad24d0fd65707e48add30bebada7d90bff2a1bba0a72d6e9b19d44246b0e83c4 - url: "https://pub.dev" - source: hosted - version: "0.257.0" - matcher: - dependency: transitive - description: - name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 - url: "https://pub.dev" - source: hosted - version: "0.12.17" - material_color_utilities: - dependency: transitive - description: - name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec - url: "https://pub.dev" - source: hosted - version: "0.11.1" - meta: - dependency: transitive - description: - name: meta - sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" - url: "https://pub.dev" - source: hosted - version: "1.17.0" - modular_core: - dependency: transitive - description: - name: modular_core - sha256: "1db0420a0dfb8a2c6dca846e7cbaa4ffeb778e247916dbcb27fb25aa566e5436" - url: "https://pub.dev" - source: hosted - version: "3.4.1" - native_toolchain_c: - dependency: transitive - description: - name: native_toolchain_c - sha256: "89e83885ba09da5fdf2cdacc8002a712ca238c28b7f717910b34bcd27b0d03ac" - url: "https://pub.dev" - source: hosted - version: "0.17.4" - nested: - dependency: transitive - description: - name: nested - sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" - url: "https://pub.dev" - source: hosted - version: "1.0.0" - objective_c: - dependency: transitive - description: - name: objective_c - sha256: "7fd0c4d8ac8980011753b9bdaed2bf15111365924cdeeeaeb596214ea2b03537" - url: "https://pub.dev" - source: hosted - version: "9.2.4" - path: - dependency: transitive - description: - name: path - sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" - url: "https://pub.dev" - source: hosted - version: "1.9.1" - path_provider: - dependency: transitive - description: - name: path_provider - sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" - url: "https://pub.dev" - source: hosted - version: "2.1.5" - path_provider_android: - dependency: transitive - description: - name: path_provider_android - sha256: f2c65e21139ce2c3dad46922be8272bb5963516045659e71bb16e151c93b580e - url: "https://pub.dev" - source: hosted - version: "2.2.22" - path_provider_foundation: - dependency: transitive - description: - name: path_provider_foundation - sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699" - url: "https://pub.dev" - source: hosted - version: "2.6.0" - path_provider_linux: - dependency: transitive - description: - name: path_provider_linux - sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 - url: "https://pub.dev" - source: hosted - version: "2.2.1" - path_provider_platform_interface: - dependency: transitive - description: - name: path_provider_platform_interface - sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" - url: "https://pub.dev" - source: hosted - version: "2.1.2" - path_provider_windows: - dependency: transitive - description: - name: path_provider_windows - sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 - url: "https://pub.dev" - source: hosted - version: "2.3.0" - platform: - dependency: transitive - description: - name: platform - sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" - url: "https://pub.dev" - source: hosted - version: "3.1.6" - plugin_platform_interface: - dependency: transitive - description: - name: plugin_platform_interface - sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" - url: "https://pub.dev" - source: hosted - version: "2.1.8" - provider: - dependency: transitive - description: - name: provider - sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272" - url: "https://pub.dev" - source: hosted - version: "6.1.5+1" - pub_semver: - dependency: transitive - description: - name: pub_semver - sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" - url: "https://pub.dev" - source: hosted - version: "2.2.0" - result_dart: - dependency: transitive - description: - name: result_dart - sha256: "0666b21fbdf697b3bdd9986348a380aa204b3ebe7c146d8e4cdaa7ce735e6054" - url: "https://pub.dev" - source: hosted - version: "2.1.1" - shared_preferences: - dependency: transitive - description: - name: shared_preferences - sha256: "2939ae520c9024cb197fc20dee269cd8cdbf564c8b5746374ec6cacdc5169e64" - url: "https://pub.dev" - source: hosted - version: "2.5.4" - shared_preferences_android: - dependency: transitive - description: - name: shared_preferences_android - sha256: "83af5c682796c0f7719c2bbf74792d113e40ae97981b8f266fa84574573556bc" - url: "https://pub.dev" - source: hosted - version: "2.4.18" - shared_preferences_foundation: - dependency: transitive - description: - name: shared_preferences_foundation - sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f" - url: "https://pub.dev" - source: hosted - version: "2.5.6" - shared_preferences_linux: - dependency: transitive - description: - name: shared_preferences_linux - sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" - url: "https://pub.dev" - source: hosted - version: "2.4.1" - shared_preferences_platform_interface: - dependency: transitive - description: - name: shared_preferences_platform_interface - sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" - url: "https://pub.dev" - source: hosted - version: "2.4.1" - shared_preferences_web: - dependency: transitive - description: - name: shared_preferences_web - sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 - url: "https://pub.dev" - source: hosted - version: "2.4.3" - shared_preferences_windows: - dependency: transitive - description: - name: shared_preferences_windows - sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" - url: "https://pub.dev" - source: hosted - version: "2.4.1" - sky_engine: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" - slang: - dependency: transitive - description: - name: slang - sha256: "13e3b6f07adc51ab751e7889647774d294cbce7a3382f81d9e5029acfe9c37b2" - url: "https://pub.dev" - source: hosted - version: "4.12.0" - slang_flutter: - dependency: transitive - description: - name: slang_flutter - sha256: "0a4545cca5404d6b7487cf61cf1fe56c52daeb08de56a7574ee8381fbad035a0" - url: "https://pub.dev" - source: hosted - version: "4.12.0" - source_span: - dependency: transitive - description: - name: source_span - sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" - url: "https://pub.dev" - source: hosted - version: "1.10.1" - stack_trace: - dependency: transitive - description: - name: stack_trace - sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" - url: "https://pub.dev" - source: hosted - version: "1.12.1" - stream_channel: - dependency: transitive - description: - name: stream_channel - sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" - url: "https://pub.dev" - source: hosted - version: "2.1.4" - string_scanner: - dependency: transitive - description: - name: string_scanner - sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" - url: "https://pub.dev" - source: hosted - version: "1.4.1" - term_glyph: - dependency: transitive - description: - name: term_glyph - sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" - url: "https://pub.dev" - source: hosted - version: "1.2.2" - test_api: - dependency: transitive - description: - name: test_api - sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 - url: "https://pub.dev" - source: hosted - version: "0.7.7" - typed_data: - dependency: transitive - description: - name: typed_data - sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 - url: "https://pub.dev" - source: hosted - version: "1.4.0" - uuid: - dependency: transitive - description: - name: uuid - sha256: a11b666489b1954e01d992f3d601b1804a33937b5a8fe677bd26b8a9f96f96e8 - url: "https://pub.dev" - source: hosted - version: "4.5.2" - vector_math: - dependency: transitive - description: - name: vector_math - sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b - url: "https://pub.dev" - source: hosted - version: "2.2.0" - vm_service: - dependency: transitive - description: - name: vm_service - sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" - url: "https://pub.dev" - source: hosted - version: "15.0.2" - watcher: - dependency: transitive - description: - name: watcher - sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635" - url: "https://pub.dev" - source: hosted - version: "1.2.1" - web: - dependency: transitive - description: - name: web - sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" - url: "https://pub.dev" - source: hosted - version: "1.1.1" - xdg_directories: - dependency: transitive - description: - name: xdg_directories - sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" - url: "https://pub.dev" - source: hosted - version: "1.1.0" - yaml: - dependency: transitive - description: - name: yaml - sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce - url: "https://pub.dev" - source: hosted - version: "3.1.3" -sdks: - dart: ">=3.10.7 <4.0.0" - flutter: ">=3.38.4" diff --git a/apps/mobile/packages/features/client/client_coverage/pubspec.yaml b/apps/mobile/packages/features/client/client_coverage/pubspec.yaml index 35422870..107ef9bf 100644 --- a/apps/mobile/packages/features/client/client_coverage/pubspec.yaml +++ b/apps/mobile/packages/features/client/client_coverage/pubspec.yaml @@ -1,7 +1,8 @@ name: client_coverage description: Client coverage feature for tracking daily shift coverage and worker status -version: 1.0.0 +version: 0.0.1 publish_to: none +resolution: workspace environment: sdk: ^3.6.0 @@ -26,9 +27,10 @@ dependencies: flutter_modular: ^6.3.4 flutter_bloc: ^8.1.6 equatable: ^2.0.7 - intl: ^0.20.1 + intl: ^0.20.0 + firebase_data_connect: ^0.2.2+1 dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^5.0.0 + flutter_lints: ^6.0.0 diff --git a/apps/mobile/packages/features/client/client_main/lib/src/presentation/pages/client_main_page.dart b/apps/mobile/packages/features/client/client_main/lib/src/presentation/pages/client_main_page.dart index 1429a78f..b01d1c9b 100644 --- a/apps/mobile/packages/features/client/client_main/lib/src/presentation/pages/client_main_page.dart +++ b/apps/mobile/packages/features/client/client_main/lib/src/presentation/pages/client_main_page.dart @@ -30,7 +30,7 @@ class ClientMainPage extends StatelessWidget { BlocProvider.of(context).navigateToTab(index); }, ); - }, + }, ), ), ); diff --git a/apps/mobile/packages/features/client/client_main/lib/src/presentation/widgets/client_main_bottom_bar.dart b/apps/mobile/packages/features/client/client_main/lib/src/presentation/widgets/client_main_bottom_bar.dart index e59987cf..d7d18428 100644 --- a/apps/mobile/packages/features/client/client_main/lib/src/presentation/widgets/client_main_bottom_bar.dart +++ b/apps/mobile/packages/features/client/client_main/lib/src/presentation/widgets/client_main_bottom_bar.dart @@ -99,13 +99,6 @@ class ClientMainBottomBar extends StatelessWidget { activeColor: activeColor, inactiveColor: inactiveColor, ), - _buildNavItem( - index: 4, - icon: UiIcons.chart, - label: t.client_main.tabs.reports, - activeColor: activeColor, - inactiveColor: inactiveColor, - ), ], ), ), diff --git a/apps/mobile/packages/features/client/create_order/lib/src/data/repositories_impl/client_create_order_repository_impl.dart b/apps/mobile/packages/features/client/create_order/lib/src/data/repositories_impl/client_create_order_repository_impl.dart index 0318b8b7..25b962a4 100644 --- a/apps/mobile/packages/features/client/create_order/lib/src/data/repositories_impl/client_create_order_repository_impl.dart +++ b/apps/mobile/packages/features/client/create_order/lib/src/data/repositories_impl/client_create_order_repository_impl.dart @@ -27,26 +27,28 @@ class ClientCreateOrderRepositoryImpl @override Future> getOrderTypes() { return Future.value(const [ - domain.OrderType( - id: 'rapid', - titleKey: 'client_create_order.types.rapid', - descriptionKey: 'client_create_order.types.rapid_desc', - ), domain.OrderType( id: 'one-time', titleKey: 'client_create_order.types.one_time', descriptionKey: 'client_create_order.types.one_time_desc', ), - domain.OrderType( - id: 'recurring', - titleKey: 'client_create_order.types.recurring', - descriptionKey: 'client_create_order.types.recurring_desc', - ), - domain.OrderType( - id: 'permanent', - titleKey: 'client_create_order.types.permanent', - descriptionKey: 'client_create_order.types.permanent_desc', - ), + + /// TODO: FEATURE_NOT_YET_IMPLEMENTED + // domain.OrderType( + // id: 'rapid', + // titleKey: 'client_create_order.types.rapid', + // descriptionKey: 'client_create_order.types.rapid_desc', + // ), + // domain.OrderType( + // id: 'recurring', + // titleKey: 'client_create_order.types.recurring', + // descriptionKey: 'client_create_order.types.recurring_desc', + // ), + // domain.OrderType( + // id: 'permanent', + // titleKey: 'client_create_order.types.permanent', + // descriptionKey: 'client_create_order.types.permanent_desc', + // ), ]); } @@ -61,12 +63,25 @@ class ClientCreateOrderRepositoryImpl if (vendorId == null || vendorId.isEmpty) { throw Exception('Vendor is missing.'); } + final domain.OneTimeOrderHubDetails? hub = order.hub; + if (hub == null || hub.id.isEmpty) { + throw Exception('Hub is missing.'); + } - final fdc.Timestamp orderTimestamp = _toTimestamp(order.date); + final DateTime orderDateOnly = DateTime( + order.date.year, + order.date.month, + order.date.day, + ); + final fdc.Timestamp orderTimestamp = _toTimestamp(orderDateOnly); final fdc.OperationResult orderResult = await _dataConnect - .createOrder(businessId: businessId, orderType: dc.OrderType.ONE_TIME) + .createOrder( + businessId: businessId, + orderType: dc.OrderType.ONE_TIME, + teamHubId: hub.id, + ) .vendorId(vendorId) - .location(order.location) + .eventName(order.eventName) .status(dc.OrderStatus.POSTED) .date(orderTimestamp) .execute(); @@ -86,8 +101,15 @@ class ClientCreateOrderRepositoryImpl final fdc.OperationResult shiftResult = await _dataConnect .createShift(title: shiftTitle, orderId: orderId) .date(orderTimestamp) - .location(order.location) - .locationAddress(order.location) + .location(hub.name) + .locationAddress(hub.address) + .latitude(hub.latitude) + .longitude(hub.longitude) + .placeId(hub.placeId) + .city(hub.city) + .state(hub.state) + .street(hub.street) + .country(hub.country) .status(dc.ShiftStatus.PENDING) .workersNeeded(workersNeeded) .filled(0) @@ -109,6 +131,10 @@ class ClientCreateOrderRepositoryImpl final double rate = order.roleRates[position.role] ?? 0; final double totalValue = rate * hours * position.count; + print( + 'CreateOneTimeOrder shiftRole: start=${start.toIso8601String()} end=${normalizedEnd.toIso8601String()}', + ); + await _dataConnect .createShiftRole( shiftId: shiftId, @@ -124,7 +150,7 @@ class ClientCreateOrderRepositoryImpl } await _dataConnect - .updateOrder(id: orderId) + .updateOrder(id: orderId, teamHubId: hub.id) .shifts(fdc.AnyValue([shiftId])) .execute(); } diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/one_time_order_bloc.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/one_time_order_bloc.dart index 20d7fea4..65822ff3 100644 --- a/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/one_time_order_bloc.dart +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/one_time_order_bloc.dart @@ -13,14 +13,17 @@ class OneTimeOrderBloc extends Bloc { : super(OneTimeOrderState.initial()) { on(_onVendorsLoaded); on(_onVendorChanged); + on(_onHubsLoaded); + on(_onHubChanged); + on(_onEventNameChanged); on(_onDateChanged); - on(_onLocationChanged); on(_onPositionAdded); on(_onPositionRemoved); on(_onPositionUpdated); on(_onSubmitted); _loadVendors(); + _loadHubs(); } final CreateOneTimeOrderUseCase _createOneTimeOrderUseCase; final dc.ExampleConnector _dataConnect; @@ -63,6 +66,38 @@ class OneTimeOrderBloc extends Bloc { } } + Future _loadHubs() async { + try { + final String? businessId = dc.ClientSessionStore.instance.session?.business?.id; + if (businessId == null || businessId.isEmpty) { + add(const OneTimeOrderHubsLoaded([])); + return; + } + final QueryResult + result = await _dataConnect.listTeamHubsByOwnerId(ownerId: businessId).execute(); + final List hubs = result.data.teamHubs + .map( + (dc.ListTeamHubsByOwnerIdTeamHubs hub) => OneTimeOrderHubOption( + id: hub.id, + name: hub.hubName, + address: hub.address, + placeId: hub.placeId, + latitude: hub.latitude, + longitude: hub.longitude, + city: hub.city, + state: hub.state, + street: hub.street, + country: hub.country, + zipCode: hub.zipCode, + ), + ) + .toList(); + add(OneTimeOrderHubsLoaded(hubs)); + } catch (_) { + add(const OneTimeOrderHubsLoaded([])); + } + } + void _onVendorsLoaded( OneTimeOrderVendorsLoaded event, Emitter emit, @@ -88,6 +123,40 @@ class OneTimeOrderBloc extends Bloc { _loadRolesForVendor(event.vendor.id); } + void _onHubsLoaded( + OneTimeOrderHubsLoaded event, + Emitter emit, + ) { + final OneTimeOrderHubOption? selectedHub = + event.hubs.isNotEmpty ? event.hubs.first : null; + emit( + state.copyWith( + hubs: event.hubs, + selectedHub: selectedHub, + location: selectedHub?.name ?? '', + ), + ); + } + + void _onHubChanged( + OneTimeOrderHubChanged event, + Emitter emit, + ) { + emit( + state.copyWith( + selectedHub: event.hub, + location: event.hub.name, + ), + ); + } + + void _onEventNameChanged( + OneTimeOrderEventNameChanged event, + Emitter emit, + ) { + emit(state.copyWith(eventName: event.eventName)); + } + void _onDateChanged( OneTimeOrderDateChanged event, Emitter emit, @@ -95,13 +164,6 @@ class OneTimeOrderBloc extends Bloc { emit(state.copyWith(date: event.date)); } - void _onLocationChanged( - OneTimeOrderLocationChanged event, - Emitter emit, - ) { - emit(state.copyWith(location: event.location)); - } - void _onPositionAdded( OneTimeOrderPositionAdded event, Emitter emit, @@ -149,10 +211,28 @@ class OneTimeOrderBloc extends Bloc { final Map roleRates = { for (final OneTimeOrderRoleOption role in state.roles) role.id: role.costPerHour, }; + final OneTimeOrderHubOption? selectedHub = state.selectedHub; + if (selectedHub == null) { + throw Exception('Hub is missing.'); + } final OneTimeOrder order = OneTimeOrder( date: state.date, - location: state.location, + location: selectedHub.name, positions: state.positions, + hub: OneTimeOrderHubDetails( + id: selectedHub.id, + name: selectedHub.name, + address: selectedHub.address, + placeId: selectedHub.placeId, + latitude: selectedHub.latitude, + longitude: selectedHub.longitude, + city: selectedHub.city, + state: selectedHub.state, + street: selectedHub.street, + country: selectedHub.country, + zipCode: selectedHub.zipCode, + ), + eventName: state.eventName, vendorId: state.selectedVendor?.id, roleRates: roleRates, ); diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/one_time_order_event.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/one_time_order_event.dart index ec9d4fcd..7258c2d0 100644 --- a/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/one_time_order_event.dart +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/one_time_order_event.dart @@ -1,5 +1,6 @@ import 'package:equatable/equatable.dart'; import 'package:krow_domain/krow_domain.dart'; +import 'one_time_order_state.dart'; abstract class OneTimeOrderEvent extends Equatable { const OneTimeOrderEvent(); @@ -24,6 +25,30 @@ class OneTimeOrderVendorChanged extends OneTimeOrderEvent { List get props => [vendor]; } +class OneTimeOrderHubsLoaded extends OneTimeOrderEvent { + const OneTimeOrderHubsLoaded(this.hubs); + final List hubs; + + @override + List get props => [hubs]; +} + +class OneTimeOrderHubChanged extends OneTimeOrderEvent { + const OneTimeOrderHubChanged(this.hub); + final OneTimeOrderHubOption hub; + + @override + List get props => [hub]; +} + +class OneTimeOrderEventNameChanged extends OneTimeOrderEvent { + const OneTimeOrderEventNameChanged(this.eventName); + final String eventName; + + @override + List get props => [eventName]; +} + class OneTimeOrderDateChanged extends OneTimeOrderEvent { const OneTimeOrderDateChanged(this.date); final DateTime date; @@ -32,14 +57,6 @@ class OneTimeOrderDateChanged extends OneTimeOrderEvent { List get props => [date]; } -class OneTimeOrderLocationChanged extends OneTimeOrderEvent { - const OneTimeOrderLocationChanged(this.location); - final String location; - - @override - List get props => [location]; -} - class OneTimeOrderPositionAdded extends OneTimeOrderEvent { const OneTimeOrderPositionAdded(); } diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/one_time_order_state.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/one_time_order_state.dart index a6d7a06d..872723bc 100644 --- a/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/one_time_order_state.dart +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/one_time_order_state.dart @@ -7,11 +7,14 @@ class OneTimeOrderState extends Equatable { const OneTimeOrderState({ required this.date, required this.location, + required this.eventName, required this.positions, this.status = OneTimeOrderStatus.initial, this.errorMessage, this.vendors = const [], this.selectedVendor, + this.hubs = const [], + this.selectedHub, this.roles = const [], }); @@ -19,40 +22,51 @@ class OneTimeOrderState extends Equatable { return OneTimeOrderState( date: DateTime.now(), location: '', + eventName: '', positions: const [ OneTimeOrderPosition(role: '', count: 1, startTime: '', endTime: ''), ], vendors: const [], + hubs: const [], roles: const [], ); } final DateTime date; final String location; + final String eventName; final List positions; final OneTimeOrderStatus status; final String? errorMessage; final List vendors; final Vendor? selectedVendor; + final List hubs; + final OneTimeOrderHubOption? selectedHub; final List roles; OneTimeOrderState copyWith({ DateTime? date, String? location, + String? eventName, List? positions, OneTimeOrderStatus? status, String? errorMessage, List? vendors, Vendor? selectedVendor, + List? hubs, + OneTimeOrderHubOption? selectedHub, List? roles, }) { return OneTimeOrderState( date: date ?? this.date, location: location ?? this.location, + eventName: eventName ?? this.eventName, positions: positions ?? this.positions, status: status ?? this.status, errorMessage: errorMessage ?? this.errorMessage, vendors: vendors ?? this.vendors, selectedVendor: selectedVendor ?? this.selectedVendor, + hubs: hubs ?? this.hubs, + selectedHub: selectedHub ?? this.selectedHub, roles: roles ?? this.roles, ); } @@ -61,15 +75,61 @@ class OneTimeOrderState extends Equatable { List get props => [ date, location, + eventName, positions, status, errorMessage, vendors, selectedVendor, + hubs, + selectedHub, roles, ]; } +class OneTimeOrderHubOption extends Equatable { + const OneTimeOrderHubOption({ + required this.id, + required this.name, + required this.address, + this.placeId, + this.latitude, + this.longitude, + this.city, + this.state, + this.street, + this.country, + this.zipCode, + }); + + final String id; + final String name; + final String address; + final String? placeId; + final double? latitude; + final double? longitude; + final String? city; + final String? state; + final String? street; + final String? country; + final String? zipCode; + + @override + List get props => [ + id, + name, + address, + placeId, + latitude, + longitude, + city, + state, + street, + country, + zipCode, + ]; +} + class OneTimeOrderRoleOption extends Equatable { const OneTimeOrderRoleOption({ required this.id, diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/pages/permanent_order_page.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/pages/permanent_order_page.dart index fd38a142..fae4d2d1 100644 --- a/apps/mobile/packages/features/client/create_order/lib/src/presentation/pages/permanent_order_page.dart +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/pages/permanent_order_page.dart @@ -18,7 +18,7 @@ class PermanentOrderPage extends StatelessWidget { backgroundColor: UiColors.bgPrimary, appBar: UiAppBar( title: labels.title, - onLeadingPressed: () => Modular.to.pop(), + onLeadingPressed: () => Modular.to.navigate('/client/create-order/'), ), body: Center( child: Padding( diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/pages/recurring_order_page.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/pages/recurring_order_page.dart index 64324b46..2f15cf70 100644 --- a/apps/mobile/packages/features/client/create_order/lib/src/presentation/pages/recurring_order_page.dart +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/pages/recurring_order_page.dart @@ -18,7 +18,7 @@ class RecurringOrderPage extends StatelessWidget { backgroundColor: UiColors.bgPrimary, appBar: UiAppBar( title: labels.title, - onLeadingPressed: () => Modular.to.pop(), + onLeadingPressed: () => Modular.to.navigate('/client/create-order/'), ), body: Center( child: Padding( diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/create_order/create_order_view.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/create_order/create_order_view.dart index eb1775fb..290165fc 100644 --- a/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/create_order/create_order_view.dart +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/create_order/create_order_view.dart @@ -43,7 +43,7 @@ class CreateOrderView extends StatelessWidget { backgroundColor: UiColors.bgPrimary, appBar: UiAppBar( title: t.client_create_order.title, - onLeadingPressed: () => Modular.to.pop(), + onLeadingPressed: () => Modular.to.navigate('/client-main/home/'), ), body: SafeArea( child: Padding( diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_event_name_input.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_event_name_input.dart new file mode 100644 index 00000000..2fe608d0 --- /dev/null +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_event_name_input.dart @@ -0,0 +1,56 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// A text input for the order name in the one-time order form. +class OneTimeOrderEventNameInput extends StatefulWidget { + const OneTimeOrderEventNameInput({ + required this.label, + required this.value, + required this.onChanged, + super.key, + }); + + final String label; + final String value; + final ValueChanged onChanged; + + @override + State createState() => + _OneTimeOrderEventNameInputState(); +} + +class _OneTimeOrderEventNameInputState + extends State { + late final TextEditingController _controller; + + @override + void initState() { + super.initState(); + _controller = TextEditingController(text: widget.value); + } + + @override + void didUpdateWidget(OneTimeOrderEventNameInput oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.value != _controller.text) { + _controller.text = widget.value; + } + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return UiTextField( + label: widget.label, + controller: _controller, + onChanged: widget.onChanged, + hintText: 'Order name', + prefixIcon: UiIcons.briefcase, + ); + } +} diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_view.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_view.dart index f2fba146..895c4ce1 100644 --- a/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_view.dart +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_view.dart @@ -8,8 +8,8 @@ import '../../blocs/one_time_order_bloc.dart'; import '../../blocs/one_time_order_event.dart'; import '../../blocs/one_time_order_state.dart'; import 'one_time_order_date_picker.dart'; +import 'one_time_order_event_name_input.dart'; import 'one_time_order_header.dart'; -import 'one_time_order_location_input.dart'; import 'one_time_order_position_card.dart'; import 'one_time_order_section_header.dart'; import 'one_time_order_success_view.dart'; @@ -31,7 +31,13 @@ class OneTimeOrderView extends StatelessWidget { title: labels.success_title, message: labels.success_message, buttonLabel: labels.back_to_orders, - onDone: () => Modular.to.pop(), + onDone: () => Modular.to.pushNamedAndRemoveUntil( + '/client-main/orders/', + (_) => false, + arguments: { + 'initialDate': state.date.toIso8601String(), + }, + ), ); } @@ -44,7 +50,7 @@ class OneTimeOrderView extends StatelessWidget { OneTimeOrderHeader( title: labels.title, subtitle: labels.subtitle, - onBack: () => Modular.to.pop(), + onBack: () => Modular.to.navigate('/client/create-order/'), ), Expanded( child: Center( @@ -83,7 +89,7 @@ class OneTimeOrderView extends StatelessWidget { OneTimeOrderHeader( title: labels.title, subtitle: labels.subtitle, - onBack: () => Modular.to.pop(), + onBack: () => Modular.to.navigate('/client/create-order/'), ), Expanded( child: Stack( @@ -129,6 +135,15 @@ class _OneTimeOrderForm extends StatelessWidget { ), const SizedBox(height: UiConstants.space4), + OneTimeOrderEventNameInput( + label: 'ORDER NAME', + value: state.eventName, + onChanged: (String value) => BlocProvider.of( + context, + ).add(OneTimeOrderEventNameChanged(value)), + ), + const SizedBox(height: UiConstants.space4), + // Vendor Selection Text('SELECT VENDOR', style: UiTypography.footnote2r.textSecondary), const SizedBox(height: 8), @@ -179,12 +194,43 @@ class _OneTimeOrderForm extends StatelessWidget { ), const SizedBox(height: UiConstants.space4), - OneTimeOrderLocationInput( - label: labels.location_label, - value: state.location, - onChanged: (String location) => BlocProvider.of( - context, - ).add(OneTimeOrderLocationChanged(location)), + Text('HUB', style: UiTypography.footnote2r.textSecondary), + const SizedBox(height: 8), + Container( + padding: const EdgeInsets.symmetric(horizontal: UiConstants.space3), + height: 48, + decoration: BoxDecoration( + color: UiColors.white, + borderRadius: UiConstants.radiusMd, + border: Border.all(color: UiColors.border), + ), + child: DropdownButtonHideUnderline( + child: DropdownButton( + isExpanded: true, + value: state.selectedHub, + icon: const Icon( + UiIcons.chevronDown, + size: 18, + color: UiColors.iconSecondary, + ), + onChanged: (OneTimeOrderHubOption? hub) { + if (hub != null) { + BlocProvider.of( + context, + ).add(OneTimeOrderHubChanged(hub)); + } + }, + items: state.hubs.map((OneTimeOrderHubOption hub) { + return DropdownMenuItem( + value: hub, + child: Text( + hub.name, + style: UiTypography.body2m.textPrimary, + ), + ); + }).toList(), + ), + ), ), const SizedBox(height: UiConstants.space6), diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/rapid_order/rapid_order_view.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/rapid_order/rapid_order_view.dart index 093ec39d..95713729 100644 --- a/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/rapid_order/rapid_order_view.dart +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/rapid_order/rapid_order_view.dart @@ -28,7 +28,7 @@ class RapidOrderView extends StatelessWidget { title: labels.success_title, message: labels.success_message, buttonLabel: labels.back_to_orders, - onDone: () => Modular.to.pop(), + onDone: () => Modular.to.navigate('/client-main/orders/'), ); } @@ -82,7 +82,7 @@ class _RapidOrderFormState extends State<_RapidOrderForm> { subtitle: labels.subtitle, date: dateStr, time: timeStr, - onBack: () => Modular.to.pop(), + onBack: () => Modular.to.navigate('/client/create-order/'), ), // Content diff --git a/apps/mobile/packages/features/client/create_order/pubspec.yaml b/apps/mobile/packages/features/client/create_order/pubspec.yaml index bf4c9bc4..b1091732 100644 --- a/apps/mobile/packages/features/client/create_order/pubspec.yaml +++ b/apps/mobile/packages/features/client/create_order/pubspec.yaml @@ -25,6 +25,7 @@ dependencies: krow_data_connect: path: ../../../data_connect firebase_data_connect: ^0.2.2+2 + firebase_auth: ^6.1.4 dev_dependencies: flutter_test: diff --git a/apps/mobile/packages/features/client/home/lib/client_home.dart b/apps/mobile/packages/features/client/home/lib/client_home.dart index 5f75c860..ce9dfa18 100644 --- a/apps/mobile/packages/features/client/home/lib/client_home.dart +++ b/apps/mobile/packages/features/client/home/lib/client_home.dart @@ -1,5 +1,3 @@ -library client_home; - import 'package:flutter_modular/flutter_modular.dart'; import 'package:krow_data_connect/krow_data_connect.dart'; import 'src/data/repositories_impl/home_repository_impl.dart'; diff --git a/apps/mobile/packages/features/client/home/lib/src/data/repositories_impl/home_repository_impl.dart b/apps/mobile/packages/features/client/home/lib/src/data/repositories_impl/home_repository_impl.dart index 19b08b29..7c014fc2 100644 --- a/apps/mobile/packages/features/client/home/lib/src/data/repositories_impl/home_repository_impl.dart +++ b/apps/mobile/packages/features/client/home/lib/src/data/repositories_impl/home_repository_impl.dart @@ -89,6 +89,14 @@ class HomeRepositoryImpl implements HomeRepositoryInterface { end: _toTimestamp(end), ) .execute(); + print( + 'Home coverage: businessId=$businessId ' + 'startLocal=${start.toIso8601String()} ' + 'endLocal=${end.toIso8601String()} ' + 'startUtc=${_toTimestamp(start).toJson()} ' + 'endUtc=${_toTimestamp(end).toJson()} ' + 'shiftRoles=${result.data.shiftRoles.length}', + ); int totalNeeded = 0; int totalFilled = 0; @@ -169,7 +177,8 @@ class HomeRepositoryImpl implements HomeRepositoryInterface { } fdc.Timestamp _toTimestamp(DateTime date) { - final int millis = date.millisecondsSinceEpoch; + final DateTime utc = date.toUtc(); + final int millis = utc.millisecondsSinceEpoch; final int seconds = millis ~/ 1000; final int nanos = (millis % 1000) * 1000000; return fdc.Timestamp(nanos, seconds); diff --git a/apps/mobile/packages/features/client/home/lib/src/presentation/blocs/client_home_bloc.dart b/apps/mobile/packages/features/client/home/lib/src/presentation/blocs/client_home_bloc.dart index 23ff6846..24e96bb3 100644 --- a/apps/mobile/packages/features/client/home/lib/src/presentation/blocs/client_home_bloc.dart +++ b/apps/mobile/packages/features/client/home/lib/src/presentation/blocs/client_home_bloc.dart @@ -26,6 +26,8 @@ class ClientHomeBloc extends Bloc { on(_onWidgetVisibilityToggled); on(_onWidgetReordered); on(_onLayoutReset); + + add(ClientHomeStarted()); } Future _onStarted( diff --git a/apps/mobile/packages/features/client/home/lib/src/presentation/blocs/client_home_state.dart b/apps/mobile/packages/features/client/home/lib/src/presentation/blocs/client_home_state.dart index f92cc85d..e86bd5f7 100644 --- a/apps/mobile/packages/features/client/home/lib/src/presentation/blocs/client_home_state.dart +++ b/apps/mobile/packages/features/client/home/lib/src/presentation/blocs/client_home_state.dart @@ -35,12 +35,12 @@ class ClientHomeState extends Equatable { this.isEditMode = false, this.errorMessage, this.dashboardData = const HomeDashboardData( - weeklySpending: 4250.0, - next7DaysSpending: 6100.0, - weeklyShifts: 12, - next7DaysScheduled: 18, - totalNeeded: 10, - totalFilled: 8, + weeklySpending: 0.0, + next7DaysSpending: 0.0, + weeklyShifts: 0, + next7DaysScheduled: 0, + totalNeeded: 0, + totalFilled: 0, ), this.reorderItems = const [], this.businessName = 'Your Company', diff --git a/apps/mobile/packages/features/client/home/lib/src/presentation/navigation/client_home_navigator.dart b/apps/mobile/packages/features/client/home/lib/src/presentation/navigation/client_home_navigator.dart index 1c421a36..afb166e3 100644 --- a/apps/mobile/packages/features/client/home/lib/src/presentation/navigation/client_home_navigator.dart +++ b/apps/mobile/packages/features/client/home/lib/src/presentation/navigation/client_home_navigator.dart @@ -34,10 +34,7 @@ class ClientHomeSheets { builder: (BuildContext context) { return ShiftOrderFormSheet( initialData: initialData, - onSubmit: (Map data) { - Navigator.pop(context); - onSubmit(data); - }, + onSubmit: onSubmit, ); }, ); diff --git a/apps/mobile/packages/features/client/home/lib/src/presentation/pages/client_home_page.dart b/apps/mobile/packages/features/client/home/lib/src/presentation/pages/client_home_page.dart index e4e30728..62357ee4 100644 --- a/apps/mobile/packages/features/client/home/lib/src/presentation/pages/client_home_page.dart +++ b/apps/mobile/packages/features/client/home/lib/src/presentation/pages/client_home_page.dart @@ -24,8 +24,7 @@ class ClientHomePage extends StatelessWidget { final TranslationsClientHomeEn i18n = t.client_home; return BlocProvider( - create: (BuildContext context) => - Modular.get()..add(ClientHomeStarted()), + create: (BuildContext context) => Modular.get(), child: Scaffold( body: SafeArea( child: Column( @@ -59,19 +58,15 @@ class ClientHomePage extends StatelessWidget { 100, ), onReorder: (int oldIndex, int newIndex) { - BlocProvider.of(context).add( - ClientHomeWidgetReordered(oldIndex, newIndex), - ); + BlocProvider.of( + context, + ).add(ClientHomeWidgetReordered(oldIndex, newIndex)); }, children: state.widgetOrder.map((String id) { return Container( - key: ValueKey(id), + key: ValueKey(id), margin: const EdgeInsets.only(bottom: UiConstants.space4), - child: DashboardWidgetBuilder( - id: id, - state: state, - isEditMode: true, - ), + child: DashboardWidgetBuilder(id: id, state: state, isEditMode: true), ); }).toList(), ); @@ -79,23 +74,38 @@ class ClientHomePage extends StatelessWidget { /// Builds the widget list in normal mode with visibility filters. Widget _buildNormalModeList(ClientHomeState state) { - return ListView( - padding: const EdgeInsets.fromLTRB( - UiConstants.space4, - 0, - UiConstants.space4, - 100, - ), - children: state.widgetOrder.map((String id) { - return Padding( - padding: const EdgeInsets.only(bottom: UiConstants.space4), - child: DashboardWidgetBuilder( - id: id, - state: state, - isEditMode: false, - ), + final List visibleWidgets = state.widgetOrder.where((String id) { + if (id == 'reorder' && state.reorderItems.isEmpty) { + return false; + } + return state.widgetVisibility[id] ?? true; + }).toList(); + + return ListView.separated( + separatorBuilder: (BuildContext context, int index) { + return const Divider(color: UiColors.border, height: 0.1); + }, + itemCount: visibleWidgets.length, + itemBuilder: (BuildContext context, int index) { + final String id = visibleWidgets[index]; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (index != 0) const SizedBox(height: UiConstants.space8), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: UiConstants.space4, + ), + child: DashboardWidgetBuilder( + id: id, + state: state, + isEditMode: false, + ), + ), + const SizedBox(height: UiConstants.space8), + ], ); - }).toList(), + }, ); } } diff --git a/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/actions_widget.dart b/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/actions_widget.dart index eeebff38..c5384950 100644 --- a/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/actions_widget.dart +++ b/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/actions_widget.dart @@ -10,11 +10,15 @@ class ActionsWidget extends StatelessWidget { /// Callback when Create Order is pressed. final VoidCallback onCreateOrderPressed; + /// Optional subtitle for the section. + final String? subtitle; + /// Creates an [ActionsWidget]. const ActionsWidget({ super.key, required this.onRapidPressed, required this.onCreateOrderPressed, + this.subtitle, }); @override @@ -22,23 +26,27 @@ class ActionsWidget extends StatelessWidget { // Check if client_home exists in t final TranslationsClientHomeActionsEn i18n = t.client_home.actions; - return Row( + return Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Expanded( - child: _ActionCard( - title: i18n.rapid, - subtitle: i18n.rapid_subtitle, - icon: UiIcons.zap, - color: const Color(0xFFFEF2F2), - borderColor: const Color(0xFFFECACA), - iconBgColor: const Color(0xFFFEE2E2), - iconColor: const Color(0xFFDC2626), - textColor: const Color(0xFF7F1D1D), - subtitleColor: const Color(0xFFB91C1C), - onTap: onRapidPressed, - ), - ), - const SizedBox(width: UiConstants.space2), + Row( + children: [ + /// TODO: FEATURE_NOT_YET_IMPLEMENTED + // Expanded( + // child: _ActionCard( + // title: i18n.rapid, + // subtitle: i18n.rapid_subtitle, + // icon: UiIcons.zap, + // color: const Color(0xFFFEF2F2), + // borderColor: const Color(0xFFFECACA), + // iconBgColor: const Color(0xFFFEE2E2), + // iconColor: const Color(0xFFDC2626), + // textColor: const Color(0xFF7F1D1D), + // subtitleColor: const Color(0xFFB91C1C), + // onTap: onRapidPressed, + // ), + // ), + // const SizedBox(width: UiConstants.space2), Expanded( child: _ActionCard( title: i18n.create_order, @@ -54,6 +62,8 @@ class ActionsWidget extends StatelessWidget { ), ), ], + ), + ], ); } } diff --git a/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/coverage_dashboard.dart b/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/coverage_dashboard.dart index c40f0202..20272bdd 100644 --- a/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/coverage_dashboard.dart +++ b/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/coverage_dashboard.dart @@ -25,11 +25,12 @@ class CoverageDashboard extends StatelessWidget { for (final s in shifts) { final int needed = s['workersNeeded'] as int? ?? 0; final int confirmed = s['filled'] as int? ?? 0; - final double rate = s['hourlyRate'] as double? ?? 20.0; + final double rate = s['hourlyRate'] as double? ?? 0.0; + final double hours = s['hours'] as double? ?? 0.0; totalNeeded += needed; totalConfirmed += confirmed; - todayCost += rate * 8 * confirmed; + todayCost += rate * hours; } final int coveragePercent = totalNeeded > 0 @@ -72,22 +73,23 @@ class CoverageDashboard extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text("Today's Status", style: UiTypography.body1m.textSecondary), - Container( - padding: const EdgeInsets.symmetric( - horizontal: UiConstants.space2, - vertical: 2.0, - ), - decoration: BoxDecoration( - color: coverageBadgeColor, - borderRadius: UiConstants.radiusMd, - ), - child: Text( - '$coveragePercent% Covered', - style: UiTypography.footnote1b.copyWith( - color: coverageTextColor, + if (totalNeeded > 0 || totalConfirmed > 0) + Container( + padding: const EdgeInsets.symmetric( + horizontal: UiConstants.space2, + vertical: 2.0, + ), + decoration: BoxDecoration( + color: coverageBadgeColor, + borderRadius: UiConstants.radiusMd, + ), + child: Text( + '$coveragePercent% Covered', + style: UiTypography.footnote1b.copyWith( + color: coverageTextColor, + ), ), ), - ), ], ), const SizedBox(height: UiConstants.space4), @@ -104,15 +106,13 @@ class CoverageDashboard extends StatelessWidget { icon: UiIcons.warning, isWarning: unfilledPositions > 0, ), - if (lateWorkersCount > 0) ...[ - const SizedBox(height: UiConstants.space2), - _StatusCard( - label: 'Running Late', - value: '$lateWorkersCount', - icon: UiIcons.error, - isError: true, - ), - ], + const SizedBox(height: UiConstants.space2), + _StatusCard( + label: 'Running Late', + value: '$lateWorkersCount', + icon: UiIcons.error, + isError: true, + ), ], ), ), @@ -122,7 +122,7 @@ class CoverageDashboard extends StatelessWidget { children: [ _StatusCard( label: 'Checked In', - value: '$checkedInCount/$totalConfirmed', + value: '$checkedInCount/$totalNeeded', icon: UiIcons.success, isInfo: true, ), diff --git a/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/coverage_widget.dart b/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/coverage_widget.dart index 9e812804..3dfaf5f7 100644 --- a/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/coverage_widget.dart +++ b/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/coverage_widget.dart @@ -12,17 +12,36 @@ class CoverageWidget extends StatelessWidget { /// The percentage of coverage (0-100). final int coveragePercent; + /// Optional subtitle for the section. + final String? subtitle; + /// Creates a [CoverageWidget]. const CoverageWidget({ super.key, - this.totalNeeded = 10, - this.totalConfirmed = 8, - this.coveragePercent = 80, + this.totalNeeded = 0, + this.totalConfirmed = 0, + this.coveragePercent = 0, + this.subtitle, }); @override Widget build(BuildContext context) { + Color backgroundColor; + Color textColor; + + if (coveragePercent == 100) { + backgroundColor = UiColors.tagActive; + textColor = UiColors.textSuccess; + } else if (coveragePercent >= 40) { + backgroundColor = UiColors.tagPending; + textColor = UiColors.textWarning; + } else { + backgroundColor = UiColors.tagError; + textColor = UiColors.textError; + } + return Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -34,26 +53,28 @@ class CoverageWidget extends StatelessWidget { letterSpacing: 0.5, ), ), - Container( - padding: const EdgeInsets.symmetric( - horizontal: UiConstants.space2, - vertical: - 2, // 2px is not in metrics, using hardcoded for small tweaks or space0/space1 - ), - decoration: BoxDecoration( - color: UiColors.tagActive, - borderRadius: UiConstants.radiusLg, - ), - child: Text( - '$coveragePercent% Covered', - style: UiTypography.footnote2b.copyWith( - color: UiColors.textSuccess, + if (totalNeeded > 0 || totalConfirmed > 0 || coveragePercent > 0) + Container( + padding: const EdgeInsets.symmetric( + horizontal: UiConstants.space2, + vertical: + 2, // 2px is not in metrics, using hardcoded for small tweaks or space0/space1 + ), + decoration: BoxDecoration( + color: backgroundColor, + borderRadius: UiConstants.radiusLg, + ), + child: Text( + '$coveragePercent% Covered', + style: UiTypography.footnote2b.copyWith(color: textColor), ), ), - ), ], ), - const SizedBox(height: UiConstants.space2), + if (subtitle != null) ...[ + Text(subtitle!, style: UiTypography.body2r.textSecondary), + ], + const SizedBox(height: UiConstants.space6), Row( children: [ Expanded( @@ -65,15 +86,16 @@ class CoverageWidget extends StatelessWidget { ), ), const SizedBox(width: UiConstants.space2), - Expanded( - child: _MetricCard( - icon: UiIcons.success, - iconColor: UiColors.iconSuccess, - label: 'Filled', - value: '$totalConfirmed', - valueColor: UiColors.textSuccess, + if (totalConfirmed != 0) + Expanded( + child: _MetricCard( + icon: UiIcons.success, + iconColor: UiColors.iconSuccess, + label: 'Filled', + value: '$totalConfirmed', + valueColor: UiColors.textSuccess, + ), ), - ), const SizedBox(width: UiConstants.space2), Expanded( child: _MetricCard( diff --git a/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/dashboard_widget_builder.dart b/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/dashboard_widget_builder.dart index 83a333d7..db0e237c 100644 --- a/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/dashboard_widget_builder.dart +++ b/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/dashboard_widget_builder.dart @@ -56,11 +56,15 @@ class DashboardWidgetBuilder extends StatelessWidget { /// Builds the actual widget content based on the widget ID. Widget _buildWidgetContent(BuildContext context) { + // Only show subtitle in normal mode + final String? subtitle = !isEditMode ? _getWidgetSubtitle(id) : null; + switch (id) { case 'actions': return ActionsWidget( onRapidPressed: () => Modular.to.pushRapidOrder(), onCreateOrderPressed: () => Modular.to.pushCreateOrder(), + subtitle: subtitle, ); case 'reorder': return ReorderWidget( @@ -70,10 +74,25 @@ class DashboardWidgetBuilder extends StatelessWidget { context, data, onSubmit: (Map submittedData) { - // Handle form submission if needed + final String? dateStr = + submittedData['date']?.toString(); + if (dateStr == null || dateStr.isEmpty) { + return; + } + final DateTime? initialDate = DateTime.tryParse(dateStr); + if (initialDate == null) { + return; + } + Modular.to.navigate( + '/client-main/orders/', + arguments: { + 'initialDate': initialDate.toIso8601String(), + }, + ); }, ); }, + subtitle: subtitle, ); case 'spending': return SpendingWidget( @@ -81,6 +100,7 @@ class DashboardWidgetBuilder extends StatelessWidget { next7DaysSpending: state.dashboardData.next7DaysSpending, weeklyShifts: state.dashboardData.weeklyShifts, next7DaysScheduled: state.dashboardData.next7DaysScheduled, + subtitle: subtitle, ); case 'coverage': return CoverageWidget( @@ -92,10 +112,12 @@ class DashboardWidgetBuilder extends StatelessWidget { 100) .toInt() : 0, + subtitle: subtitle, ); case 'liveActivity': return LiveActivityWidget( onViewAllPressed: () => Modular.to.navigate('/client-main/coverage/'), + subtitle: subtitle, ); default: return const SizedBox.shrink(); @@ -119,4 +141,21 @@ class DashboardWidgetBuilder extends StatelessWidget { return ''; } } + + String _getWidgetSubtitle(String id) { + switch (id) { + case 'actions': + return 'Quick access to create and manage orders'; + case 'reorder': + return 'Easily reorder from your past activity'; + case 'spending': + return 'Track your spending and budget in real-time'; + case 'coverage': + return 'Overview of your current shift coverage'; + case 'liveActivity': + return 'Real-time updates on your active shifts'; + default: + return ''; + } + } } diff --git a/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/live_activity_widget.dart b/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/live_activity_widget.dart index 1c91a655..7efa461f 100644 --- a/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/live_activity_widget.dart +++ b/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/live_activity_widget.dart @@ -10,8 +10,15 @@ class LiveActivityWidget extends StatefulWidget { /// Callback when "View all" is pressed. final VoidCallback onViewAllPressed; + /// Optional subtitle for the section. + final String? subtitle; + /// Creates a [LiveActivityWidget]. - const LiveActivityWidget({super.key, required this.onViewAllPressed}); + const LiveActivityWidget({ + super.key, + required this.onViewAllPressed, + this.subtitle + }); @override State createState() => _LiveActivityWidgetState(); @@ -31,6 +38,15 @@ class _LiveActivityWidgetState extends State { final DateTime now = DateTime.now(); final DateTime start = DateTime(now.year, now.month, now.day); final DateTime end = DateTime(now.year, now.month, now.day, 23, 59, 59, 999); + final fdc.QueryResult shiftRolesResult = + await dc.ExampleConnector.instance + .listShiftRolesByBusinessAndDateRange( + businessId: businessId, + start: _toTimestamp(start), + end: _toTimestamp(end), + ) + .execute(); final fdc.QueryResult result = await dc.ExampleConnector.instance @@ -41,33 +57,20 @@ class _LiveActivityWidgetState extends State { ) .execute(); - if (result.data.applications.isEmpty) { + if (shiftRolesResult.data.shiftRoles.isEmpty && + result.data.applications.isEmpty) { return _LiveActivityData.empty(); } - final Map aggregates = - {}; - for (final dc.ListStaffsApplicationsByBusinessForDayApplications app - in result.data.applications) { - final String key = '${app.shiftId}:${app.roleId}'; - final _LiveShiftAggregate aggregate = aggregates[key] ?? - _LiveShiftAggregate( - workersNeeded: app.shiftRole.count, - assigned: app.shiftRole.assigned ?? 0, - cost: app.shiftRole.shift.cost ?? 0, - ); - aggregates[key] = aggregate; - } - int totalNeeded = 0; - int totalAssigned = 0; double totalCost = 0; - for (final _LiveShiftAggregate aggregate in aggregates.values) { - totalNeeded += aggregate.workersNeeded; - totalAssigned += aggregate.assigned; - totalCost += aggregate.cost; + for (final dc.ListShiftRolesByBusinessAndDateRangeShiftRoles shiftRole + in shiftRolesResult.data.shiftRoles) { + totalNeeded += shiftRole.count; + totalCost += shiftRole.totalValue ?? 0; } + final int totalAssigned = result.data.applications.length; int lateCount = 0; int checkedInCount = 0; for (final dc.ListStaffsApplicationsByBusinessForDayApplications app @@ -92,9 +95,10 @@ class _LiveActivityWidgetState extends State { } fdc.Timestamp _toTimestamp(DateTime dateTime) { - final int seconds = dateTime.millisecondsSinceEpoch ~/ 1000; + final DateTime utc = dateTime.toUtc(); + final int seconds = utc.millisecondsSinceEpoch ~/ 1000; final int nanoseconds = - (dateTime.millisecondsSinceEpoch % 1000) * 1000000; + (utc.millisecondsSinceEpoch % 1000) * 1000000; return fdc.Timestamp(nanoseconds, seconds); } @@ -103,6 +107,7 @@ class _LiveActivityWidgetState extends State { final TranslationsClientHomeEn i18n = t.client_home; return Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -124,7 +129,13 @@ class _LiveActivityWidgetState extends State { ), ], ), - const SizedBox(height: UiConstants.space2), + if (widget.subtitle != null) ...[ + Text( + widget.subtitle!, + style: UiTypography.body2r.textSecondary, + ), + ], + const SizedBox(height: UiConstants.space6), FutureBuilder<_LiveActivityData>( future: _liveActivityFuture, builder: (BuildContext context, @@ -136,9 +147,8 @@ class _LiveActivityWidgetState extends State { { 'workersNeeded': data.totalNeeded, 'filled': data.totalAssigned, - 'hourlyRate': data.totalAssigned == 0 - ? 0.0 - : data.totalCost / data.totalAssigned, + 'hourlyRate': 1.0, + 'hours': data.totalCost, 'status': 'OPEN', 'date': DateTime.now().toIso8601String().split('T')[0], }, @@ -192,15 +202,3 @@ class _LiveActivityData { ); } } - -class _LiveShiftAggregate { - _LiveShiftAggregate({ - required this.workersNeeded, - required this.assigned, - required this.cost, - }); - - final int workersNeeded; - final int assigned; - final double cost; -} 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 b0147414..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 @@ -11,15 +11,23 @@ class ReorderWidget extends StatelessWidget { /// Callback when a reorder button is pressed. final Function(Map shiftData) onReorderPressed; + /// Optional subtitle for the section. + final String? subtitle; + /// Creates a [ReorderWidget]. const ReorderWidget({ super.key, required this.orders, required this.onReorderPressed, + this.subtitle, }); @override Widget build(BuildContext context) { + if (orders.isEmpty) { + return const SizedBox.shrink(); + } + final TranslationsClientHomeReorderEn i18n = t.client_home.reorder; final List recentOrders = orders; @@ -33,9 +41,13 @@ class ReorderWidget extends StatelessWidget { letterSpacing: 0.5, ), ), + if (subtitle != null) ...[ + const SizedBox(height: UiConstants.space1), + 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, @@ -52,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, @@ -114,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, ), ], @@ -130,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/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/shift_order_form_sheet.dart b/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/shift_order_form_sheet.dart index 7df94dfd..5fbb81f0 100644 --- a/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/shift_order_form_sheet.dart +++ b/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/shift_order_form_sheet.dart @@ -1,3 +1,4 @@ +import 'package:core_localization/core_localization.dart'; import 'package:design_system/design_system.dart'; import 'package:firebase_data_connect/firebase_data_connect.dart' as fdc; import 'package:flutter/material.dart'; @@ -52,6 +53,7 @@ class ShiftOrderFormSheet extends StatefulWidget { class _ShiftOrderFormSheetState extends State { late TextEditingController _dateController; late TextEditingController _globalLocationController; + late TextEditingController _orderNameController; late List> _positions; @@ -59,6 +61,10 @@ class _ShiftOrderFormSheetState extends State { List<_VendorOption> _vendors = const <_VendorOption>[]; List<_RoleOption> _roles = const <_RoleOption>[]; String? _selectedVendorId; + List _hubs = const []; + dc.ListTeamHubsByOwnerIdTeamHubs? _selectedHub; + bool _showSuccess = false; + Map? _submitData; @override void initState() { @@ -75,6 +81,9 @@ class _ShiftOrderFormSheetState extends State { widget.initialData?['locationAddress'] ?? '', ); + _orderNameController = TextEditingController( + text: widget.initialData?['eventName']?.toString() ?? '', + ); // Initialize positions _positions = >[ @@ -96,6 +105,7 @@ class _ShiftOrderFormSheetState extends State { ]; _loadVendors(); + _loadHubs(); _loadOrderDetails(); } @@ -103,6 +113,7 @@ class _ShiftOrderFormSheetState extends State { void dispose() { _dateController.dispose(); _globalLocationController.dispose(); + _orderNameController.dispose(); super.dispose(); } @@ -187,9 +198,14 @@ class _ShiftOrderFormSheetState extends State { if (businessId == null || businessId.isEmpty) { return; } + final dc.ListTeamHubsByOwnerIdTeamHubs? selectedHub = _selectedHub; + if (selectedHub == null) { + return; + } final DateTime date = DateTime.parse(_dateController.text); - final fdc.Timestamp orderTimestamp = _toTimestamp(date); + final DateTime dateOnly = DateTime.utc(date.year, date.month, date.day); + final fdc.Timestamp orderTimestamp = _toTimestamp(dateOnly); final dc.OrderType orderType = _orderTypeFromValue(widget.initialData?['type']?.toString()); @@ -198,9 +214,10 @@ class _ShiftOrderFormSheetState extends State { .createOrder( businessId: businessId, orderType: orderType, + teamHubId: selectedHub.id, ) .vendorId(_selectedVendorId) - .location(_globalLocationController.text) + .eventName(_orderNameController.text) .status(dc.OrderStatus.POSTED) .date(orderTimestamp) .execute(); @@ -222,8 +239,15 @@ class _ShiftOrderFormSheetState extends State { shiftResult = await _dataConnect .createShift(title: shiftTitle, orderId: orderId) .date(orderTimestamp) - .location(_globalLocationController.text) - .locationAddress(_globalLocationController.text) + .location(selectedHub.hubName) + .locationAddress(selectedHub.address) + .latitude(selectedHub.latitude) + .longitude(selectedHub.longitude) + .placeId(selectedHub.placeId) + .city(selectedHub.city) + .state(selectedHub.state) + .street(selectedHub.street) + .country(selectedHub.country) .status(dc.ShiftStatus.PENDING) .workersNeeded(workersNeeded) .filled(0) @@ -266,12 +290,17 @@ class _ShiftOrderFormSheetState extends State { } await _dataConnect - .updateOrder(id: orderId) + .updateOrder(id: orderId, teamHubId: selectedHub.id) .shifts(fdc.AnyValue([shiftId])) .execute(); - widget.onSubmit({ - 'orderId': orderId, + if (!mounted) return; + setState(() { + _submitData = { + 'orderId': orderId, + 'date': _dateController.text, + }; + _showSuccess = true; }); } @@ -306,6 +335,35 @@ class _ShiftOrderFormSheetState extends State { } } + Future _loadHubs() async { + final String? businessId = dc.ClientSessionStore.instance.session?.business?.id; + if (businessId == null || businessId.isEmpty) { + return; + } + + try { + final fdc.QueryResult< + dc.ListTeamHubsByOwnerIdData, + dc.ListTeamHubsByOwnerIdVariables> result = + await _dataConnect.listTeamHubsByOwnerId(ownerId: businessId).execute(); + final List hubs = result.data.teamHubs; + if (!mounted) return; + setState(() { + _hubs = hubs; + _selectedHub = hubs.isNotEmpty ? hubs.first : null; + if (_selectedHub != null) { + _globalLocationController.text = _selectedHub!.address; + } + }); + } catch (_) { + if (!mounted) return; + setState(() { + _hubs = const []; + _selectedHub = null; + }); + } + } + Future _loadRolesForVendor(String vendorId) async { try { final fdc.QueryResult @@ -357,10 +415,14 @@ class _ShiftOrderFormSheetState extends State { final dc.ListShiftRolesByBusinessAndOrderShiftRolesShift firstShift = shiftRoles.first.shift; - _globalLocationController.text = firstShift.order.location ?? - firstShift.locationAddress ?? - firstShift.location ?? - _globalLocationController.text; + final dc.ListShiftRolesByBusinessAndOrderShiftRolesShiftOrderTeamHub? + teamHub = firstShift.order.teamHub; + await _loadHubsAndSelect( + placeId: teamHub?.placeId, + hubName: teamHub?.hubName, + address: teamHub?.address, + ); + _orderNameController.text = firstShift.order.eventName ?? ''; final String? vendorId = firstShift.order.vendorId; if (mounted) { @@ -394,6 +456,70 @@ class _ShiftOrderFormSheetState extends State { } } + Future _loadHubsAndSelect({ + String? placeId, + String? hubName, + String? address, + }) async { + final String? businessId = dc.ClientSessionStore.instance.session?.business?.id; + if (businessId == null || businessId.isEmpty) { + return; + } + + try { + final fdc.QueryResult< + dc.ListTeamHubsByOwnerIdData, + dc.ListTeamHubsByOwnerIdVariables> result = + await _dataConnect.listTeamHubsByOwnerId(ownerId: businessId).execute(); + final List hubs = result.data.teamHubs; + dc.ListTeamHubsByOwnerIdTeamHubs? selected; + + if (placeId != null && placeId.isNotEmpty) { + for (final dc.ListTeamHubsByOwnerIdTeamHubs hub in hubs) { + if (hub.placeId == placeId) { + selected = hub; + break; + } + } + } + + if (selected == null && hubName != null && hubName.isNotEmpty) { + for (final dc.ListTeamHubsByOwnerIdTeamHubs hub in hubs) { + if (hub.hubName == hubName) { + selected = hub; + break; + } + } + } + + if (selected == null && address != null && address.isNotEmpty) { + for (final dc.ListTeamHubsByOwnerIdTeamHubs hub in hubs) { + if (hub.address == address) { + selected = hub; + break; + } + } + } + + selected ??= hubs.isNotEmpty ? hubs.first : null; + + if (!mounted) return; + setState(() { + _hubs = hubs; + _selectedHub = selected; + if (selected != null) { + _globalLocationController.text = selected.address; + } + }); + } catch (_) { + if (!mounted) return; + setState(() { + _hubs = const []; + _selectedHub = null; + }); + } + } + String _formatTimeForField(fdc.Timestamp? value) { if (value == null) return ''; try { @@ -472,7 +598,8 @@ class _ShiftOrderFormSheetState extends State { } fdc.Timestamp _toTimestamp(DateTime date) { - final int millis = date.millisecondsSinceEpoch; + final DateTime utc = date.toUtc(); + final int millis = utc.millisecondsSinceEpoch; final int seconds = millis ~/ 1000; final int nanos = (millis % 1000) * 1000000; return fdc.Timestamp(nanos, seconds); @@ -480,6 +607,16 @@ class _ShiftOrderFormSheetState extends State { @override Widget build(BuildContext context) { + if (_showSuccess) { + final TranslationsClientCreateOrderOneTimeEn labels = + t.client_create_order.one_time; + return _buildSuccessView( + title: labels.success_title, + message: labels.success_message, + buttonLabel: labels.back_to_orders, + ); + } + return Container( height: MediaQuery.of(context).size.height * 0.95, decoration: const BoxDecoration( @@ -546,12 +683,16 @@ class _ShiftOrderFormSheetState extends State { _buildVendorDropdown(), const SizedBox(height: UiConstants.space4), + _buildSectionHeader('ORDER NAME'), + _buildOrderNameField(), + const SizedBox(height: UiConstants.space4), + _buildSectionHeader('DATE'), _buildDateField(), const SizedBox(height: UiConstants.space4), - _buildSectionHeader('LOCATION'), - _buildLocationField(), + _buildSectionHeader('HUB'), + _buildHubField(), const SizedBox(height: UiConstants.space5), Row( @@ -783,7 +924,7 @@ class _ShiftOrderFormSheetState extends State { ); } - Widget _buildLocationField() { + Widget _buildHubField() { return Container( padding: const EdgeInsets.symmetric(horizontal: UiConstants.space3), decoration: BoxDecoration( @@ -791,21 +932,52 @@ class _ShiftOrderFormSheetState extends State { borderRadius: UiConstants.radiusMd, border: Border.all(color: UiColors.border), ), - child: Row( - children: [ - const Icon(UiIcons.mapPin, size: 20, color: UiColors.iconSecondary), - const SizedBox(width: UiConstants.space2), - Expanded( - child: TextField( - controller: _globalLocationController, - decoration: const InputDecoration( - hintText: 'Enter location address', - border: InputBorder.none, - ), - style: UiTypography.body2r.textPrimary, - ), + child: DropdownButtonHideUnderline( + child: DropdownButton( + isExpanded: true, + value: _selectedHub, + icon: const Icon( + UiIcons.chevronDown, + size: 18, + color: UiColors.iconSecondary, ), - ], + onChanged: (dc.ListTeamHubsByOwnerIdTeamHubs? hub) { + if (hub != null) { + setState(() { + _selectedHub = hub; + _globalLocationController.text = hub.address; + }); + } + }, + items: _hubs.map((dc.ListTeamHubsByOwnerIdTeamHubs hub) { + return DropdownMenuItem( + value: hub, + child: Text( + hub.hubName, + style: UiTypography.body2r.textPrimary, + ), + ); + }).toList(), + ), + ), + ); + } + + Widget _buildOrderNameField() { + return Container( + padding: const EdgeInsets.symmetric(horizontal: UiConstants.space3), + decoration: BoxDecoration( + color: UiColors.white, + borderRadius: UiConstants.radiusMd, + border: Border.all(color: UiColors.border), + ), + child: TextField( + controller: _orderNameController, + decoration: const InputDecoration( + hintText: 'Order name', + border: InputBorder.none, + ), + style: UiTypography.body2r.textPrimary, ), ); } @@ -1109,6 +1281,90 @@ class _ShiftOrderFormSheetState extends State { ); } + Widget _buildSuccessView({ + required String title, + required String message, + required String buttonLabel, + }) { + return Container( + width: double.infinity, + height: MediaQuery.of(context).size.height * 0.95, + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [UiColors.primary, UiColors.buttonPrimaryHover], + ), + borderRadius: BorderRadius.vertical(top: Radius.circular(24)), + ), + child: SafeArea( + child: Center( + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 40), + padding: const EdgeInsets.all(UiConstants.space8), + decoration: BoxDecoration( + color: UiColors.white, + borderRadius: UiConstants.radiusLg * 1.5, + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.2), + blurRadius: 20, + offset: const Offset(0, 10), + ), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 64, + height: 64, + decoration: const BoxDecoration( + color: UiColors.accent, + shape: BoxShape.circle, + ), + child: const Center( + child: Icon( + UiIcons.check, + color: UiColors.black, + size: 32, + ), + ), + ), + const SizedBox(height: UiConstants.space6), + Text( + title, + style: UiTypography.headline2m.textPrimary, + textAlign: TextAlign.center, + ), + const SizedBox(height: UiConstants.space3), + Text( + message, + textAlign: TextAlign.center, + style: UiTypography.body2r.textSecondary.copyWith( + height: 1.5, + ), + ), + const SizedBox(height: UiConstants.space8), + SizedBox( + width: double.infinity, + child: UiButton.primary( + text: buttonLabel, + onPressed: () { + widget.onSubmit(_submitData ?? {}); + Navigator.pop(context); + }, + size: UiButtonSize.large, + ), + ), + ], + ), + ), + ), + ), + ); + } + Widget _buildInlineTimeInput({ required String label, required String value, diff --git a/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/spending_widget.dart b/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/spending_widget.dart index 18ee5cd7..1d20ab63 100644 --- a/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/spending_widget.dart +++ b/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/spending_widget.dart @@ -16,6 +16,9 @@ class SpendingWidget extends StatelessWidget { /// The number of scheduled shifts for next 7 days. final int next7DaysScheduled; + /// Optional subtitle for the section. + final String? subtitle; + /// Creates a [SpendingWidget]. const SpendingWidget({ super.key, @@ -23,6 +26,7 @@ class SpendingWidget extends StatelessWidget { required this.next7DaysSpending, required this.weeklyShifts, required this.next7DaysScheduled, + this.subtitle, }); @override @@ -38,7 +42,13 @@ class SpendingWidget extends StatelessWidget { letterSpacing: 0.5, ), ), - const SizedBox(height: UiConstants.space2), + if (subtitle != null) ...[ + Text( + subtitle!, + style: UiTypography.body2r.textSecondary, + ), + ], + const SizedBox(height: UiConstants.space6), Container( padding: const EdgeInsets.all(UiConstants.space3), decoration: BoxDecoration( @@ -114,58 +124,6 @@ class SpendingWidget extends StatelessWidget { ), ], ), - const SizedBox(height: UiConstants.space3), - Container( - padding: const EdgeInsets.only(top: UiConstants.space3), - decoration: const BoxDecoration( - border: Border(top: BorderSide(color: Colors.white24)), - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - width: 24, - height: 24, - decoration: BoxDecoration( - color: Colors.white.withValues(alpha: 0.2), - shape: BoxShape.circle, - ), - child: const Center( - child: Icon( - UiIcons.sparkles, - color: UiColors.white, - size: 14, - ), - ), - ), - const SizedBox(width: UiConstants.space2), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '💡 ' + - i18n.dashboard.insight_lightbulb(amount: '180'), - style: const TextStyle( - color: Colors.white, - fontSize: 10, - fontWeight: FontWeight.w600, - ), - ), - const SizedBox(height: 1), - Text( - i18n.dashboard.insight_tip, - style: TextStyle( - color: Colors.white.withValues(alpha: 0.8), - fontSize: 9, - ), - ), - ], - ), - ), - ], - ), - ), ], ), ), diff --git a/apps/mobile/packages/features/client/home/pubspec.yaml b/apps/mobile/packages/features/client/home/pubspec.yaml index 7566f837..e75de091 100644 --- a/apps/mobile/packages/features/client/home/pubspec.yaml +++ b/apps/mobile/packages/features/client/home/pubspec.yaml @@ -22,6 +22,7 @@ dependencies: core_localization: path: ../../../core_localization krow_domain: ^0.0.1 + krow_data_connect: ^0.0.1 dev_dependencies: flutter_test: diff --git a/apps/mobile/packages/features/client/hubs/lib/src/data/repositories_impl/hub_repository_impl.dart b/apps/mobile/packages/features/client/hubs/lib/src/data/repositories_impl/hub_repository_impl.dart index 190bc0ad..45b8b8f8 100644 --- a/apps/mobile/packages/features/client/hubs/lib/src/data/repositories_impl/hub_repository_impl.dart +++ b/apps/mobile/packages/features/client/hubs/lib/src/data/repositories_impl/hub_repository_impl.dart @@ -1,9 +1,19 @@ +import 'dart:convert'; + import 'package:firebase_auth/firebase_auth.dart' as firebase; import 'package:firebase_data_connect/firebase_data_connect.dart'; +import 'package:http/http.dart' as http; import 'package:krow_data_connect/krow_data_connect.dart' as dc; import 'package:krow_domain/krow_domain.dart' as domain; +import 'package:krow_domain/krow_domain.dart' + show + HubHasOrdersException, + HubCreationFailedException, + BusinessNotFoundException, + NotAuthenticatedException; import '../../domain/repositories/hub_repository_interface.dart'; +import '../../util/hubs_constants.dart'; /// Implementation of [HubRepositoryInterface] backed by Data Connect. class HubRepositoryImpl implements HubRepositoryInterface { @@ -27,10 +37,24 @@ class HubRepositoryImpl implements HubRepositoryInterface { Future createHub({ required String name, required String address, + String? placeId, + double? latitude, + double? longitude, + String? city, + String? state, + String? street, + String? country, + String? zipCode, }) async { final dc.GetBusinessesByUserIdBusinesses business = await _getBusinessForCurrentUser(); final String teamId = await _getOrCreateTeamId(business); - final String? city = business.city; + final _PlaceAddress? placeAddress = + placeId == null || placeId.isEmpty ? null : await _fetchPlaceAddress(placeId); + final String? cityValue = city ?? placeAddress?.city ?? business.city; + final String? stateValue = state ?? placeAddress?.state; + final String? streetValue = street ?? placeAddress?.street; + final String? countryValue = country ?? placeAddress?.country; + final String? zipCodeValue = zipCode ?? placeAddress?.zipCode; final OperationResult result = await _dataConnect .createTeamHub( @@ -38,11 +62,20 @@ class HubRepositoryImpl implements HubRepositoryInterface { hubName: name, address: address, ) - .city(city?.isNotEmpty == true ? city : '') + .placeId(placeId) + .latitude(latitude) + .longitude(longitude) + .city(cityValue?.isNotEmpty == true ? cityValue : '') + .state(stateValue) + .street(streetValue) + .country(countryValue) + .zipCode(zipCodeValue) .execute(); final String? createdId = result.data?.teamHub_insert.id; if (createdId == null) { - throw Exception('Hub creation failed.'); + throw HubCreationFailedException( + technicalMessage: 'teamHub_insert returned null for hub: $name', + ); } final List hubs = await _fetchHubsForTeam( @@ -69,6 +102,29 @@ class HubRepositoryImpl implements HubRepositoryInterface { @override Future deleteHub(String id) async { + final String? businessId = dc.ClientSessionStore.instance.session?.business?.id; + if (businessId == null || businessId.isEmpty) { + await _firebaseAuth.signOut(); + throw const BusinessNotFoundException( + technicalMessage: 'Business ID missing from session', + ); + } + + final QueryResult< + dc.ListOrdersByBusinessAndTeamHubData, + dc.ListOrdersByBusinessAndTeamHubVariables> result = await _dataConnect + .listOrdersByBusinessAndTeamHub( + businessId: businessId, + teamHubId: id, + ) + .execute(); + + if (result.data.orders.isNotEmpty) { + throw HubHasOrdersException( + technicalMessage: 'Hub $id has ${result.data.orders.length} orders', + ); + } + await _dataConnect.deleteTeamHub(id: id).execute(); } @@ -107,7 +163,9 @@ class HubRepositoryImpl implements HubRepositoryInterface { final firebase.User? user = _firebaseAuth.currentUser; if (user == null) { - throw Exception('User is not authenticated.'); + throw const NotAuthenticatedException( + technicalMessage: 'No Firebase user in currentUser', + ); } final QueryResult result = await _dataConnect.getBusinessesByUserId( @@ -115,7 +173,9 @@ class HubRepositoryImpl implements HubRepositoryInterface { ).execute(); if (result.data.businesses.isEmpty) { await _firebaseAuth.signOut(); - throw Exception('No business found for this user. Please sign in again.'); + throw BusinessNotFoundException( + technicalMessage: 'No business found for user ${user.uid}', + ); } final dc.GetBusinessesByUserIdBusinesses business = result.data.businesses.first; @@ -162,7 +222,9 @@ class HubRepositoryImpl implements HubRepositoryInterface { final OperationResult createTeamResult = await createTeamBuilder.execute(); final String? teamId = createTeamResult.data?.team_insert.id; if (teamId == null) { - throw Exception('Team creation failed.'); + throw HubCreationFailedException( + technicalMessage: 'Team creation failed for business ${business.id}', + ); } return teamId; @@ -192,4 +254,99 @@ class HubRepositoryImpl implements HubRepositoryInterface { ) .toList(); } + + Future<_PlaceAddress?> _fetchPlaceAddress(String placeId) async { + final Uri uri = Uri.https( + 'maps.googleapis.com', + '/maps/api/place/details/json', + { + 'place_id': placeId, + 'fields': 'address_component', + 'key': HubsConstants.googlePlacesApiKey, + }, + ); + try { + final http.Response response = await http.get(uri); + if (response.statusCode != 200) { + return null; + } + + final Map payload = + json.decode(response.body) as Map; + if (payload['status'] != 'OK') { + return null; + } + + final Map? result = + payload['result'] as Map?; + final List? components = + result?['address_components'] as List?; + if (components == null || components.isEmpty) { + return null; + } + + String? streetNumber; + String? route; + String? city; + String? state; + String? country; + String? zipCode; + + for (final dynamic entry in components) { + final Map component = entry as Map; + final List types = component['types'] as List? ?? []; + final String? longName = component['long_name'] as String?; + final String? shortName = component['short_name'] as String?; + + if (types.contains('street_number')) { + streetNumber = longName; + } else if (types.contains('route')) { + route = longName; + } else if (types.contains('locality')) { + city = longName; + } else if (types.contains('postal_town')) { + city ??= longName; + } else if (types.contains('administrative_area_level_2')) { + city ??= longName; + } else if (types.contains('administrative_area_level_1')) { + state = shortName ?? longName; + } else if (types.contains('country')) { + country = shortName ?? longName; + } else if (types.contains('postal_code')) { + zipCode = longName; + } + } + + final String? streetValue = [streetNumber, route] + .where((String? value) => value != null && value!.isNotEmpty) + .join(' ') + .trim(); + + return _PlaceAddress( + street: streetValue?.isEmpty == true ? null : streetValue, + city: city, + state: state, + country: country, + zipCode: zipCode, + ); + } catch (_) { + return null; + } + } +} + +class _PlaceAddress { + const _PlaceAddress({ + this.street, + this.city, + this.state, + this.country, + this.zipCode, + }); + + final String? street; + final String? city; + final String? state; + final String? country; + final String? zipCode; } diff --git a/apps/mobile/packages/features/client/hubs/lib/src/domain/arguments/create_hub_arguments.dart b/apps/mobile/packages/features/client/hubs/lib/src/domain/arguments/create_hub_arguments.dart index a978f3a2..8518d9f0 100644 --- a/apps/mobile/packages/features/client/hubs/lib/src/domain/arguments/create_hub_arguments.dart +++ b/apps/mobile/packages/features/client/hubs/lib/src/domain/arguments/create_hub_arguments.dart @@ -10,11 +10,42 @@ class CreateHubArguments extends UseCaseArgument { /// The physical address of the hub. final String address; + final String? placeId; + final double? latitude; + final double? longitude; + final String? city; + final String? state; + final String? street; + final String? country; + final String? zipCode; + /// Creates a [CreateHubArguments] instance. /// /// Both [name] and [address] are required. - const CreateHubArguments({required this.name, required this.address}); + const CreateHubArguments({ + required this.name, + required this.address, + this.placeId, + this.latitude, + this.longitude, + this.city, + this.state, + this.street, + this.country, + this.zipCode, + }); @override - List get props => [name, address]; + List get props => [ + name, + address, + placeId, + latitude, + longitude, + city, + state, + street, + country, + zipCode, + ]; } diff --git a/apps/mobile/packages/features/client/hubs/lib/src/domain/repositories/hub_repository_interface.dart b/apps/mobile/packages/features/client/hubs/lib/src/domain/repositories/hub_repository_interface.dart index 5b03fced..5580e6e4 100644 --- a/apps/mobile/packages/features/client/hubs/lib/src/domain/repositories/hub_repository_interface.dart +++ b/apps/mobile/packages/features/client/hubs/lib/src/domain/repositories/hub_repository_interface.dart @@ -15,7 +15,18 @@ abstract interface class HubRepositoryInterface { /// /// Takes the [name] and [address] of the new hub. /// Returns the created [Hub] entity. - Future createHub({required String name, required String address}); + Future createHub({ + required String name, + required String address, + String? placeId, + double? latitude, + double? longitude, + String? city, + String? state, + String? street, + String? country, + String? zipCode, + }); /// Deletes a hub by its [id]. Future deleteHub(String id); diff --git a/apps/mobile/packages/features/client/hubs/lib/src/domain/usecases/create_hub_usecase.dart b/apps/mobile/packages/features/client/hubs/lib/src/domain/usecases/create_hub_usecase.dart index bbfc1403..50550bc1 100644 --- a/apps/mobile/packages/features/client/hubs/lib/src/domain/usecases/create_hub_usecase.dart +++ b/apps/mobile/packages/features/client/hubs/lib/src/domain/usecases/create_hub_usecase.dart @@ -21,6 +21,14 @@ class CreateHubUseCase implements UseCase { return _repository.createHub( name: arguments.name, address: arguments.address, + placeId: arguments.placeId, + latitude: arguments.latitude, + longitude: arguments.longitude, + city: arguments.city, + state: arguments.state, + street: arguments.street, + country: arguments.country, + zipCode: arguments.zipCode, ); } } diff --git a/apps/mobile/packages/features/client/hubs/lib/src/presentation/blocs/client_hubs_bloc.dart b/apps/mobile/packages/features/client/hubs/lib/src/presentation/blocs/client_hubs_bloc.dart index be1ecc42..becc3e8c 100644 --- a/apps/mobile/packages/features/client/hubs/lib/src/presentation/blocs/client_hubs_bloc.dart +++ b/apps/mobile/packages/features/client/hubs/lib/src/presentation/blocs/client_hubs_bloc.dart @@ -1,3 +1,5 @@ +import 'dart:developer' as developer; + import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_modular/flutter_modular.dart'; import 'package:krow_domain/krow_domain.dart'; @@ -67,11 +69,20 @@ class ClientHubsBloc extends Bloc try { final List hubs = await _getHubsUseCase(); emit(state.copyWith(status: ClientHubsStatus.success, hubs: hubs)); - } catch (e) { + } on AppException catch (e) { + developer.log('Error ${e.code}: ${e.technicalMessage}', name: 'ClientHubsBloc'); emit( state.copyWith( status: ClientHubsStatus.failure, - errorMessage: e.toString(), + errorMessage: e.messageKey, + ), + ); + } catch (e) { + developer.log('Unexpected error: $e', name: 'ClientHubsBloc'); + emit( + state.copyWith( + status: ClientHubsStatus.failure, + errorMessage: 'errors.generic.unknown', ), ); } @@ -84,7 +95,18 @@ class ClientHubsBloc extends Bloc emit(state.copyWith(status: ClientHubsStatus.actionInProgress)); try { await _createHubUseCase( - CreateHubArguments(name: event.name, address: event.address), + CreateHubArguments( + name: event.name, + address: event.address, + placeId: event.placeId, + latitude: event.latitude, + longitude: event.longitude, + city: event.city, + state: event.state, + street: event.street, + country: event.country, + zipCode: event.zipCode, + ), ); final List hubs = await _getHubsUseCase(); emit( @@ -95,11 +117,20 @@ class ClientHubsBloc extends Bloc showAddHubDialog: false, ), ); - } catch (e) { + } on AppException catch (e) { + developer.log('Error ${e.code}: ${e.technicalMessage}', name: 'ClientHubsBloc'); emit( state.copyWith( status: ClientHubsStatus.actionFailure, - errorMessage: e.toString(), + errorMessage: e.messageKey, + ), + ); + } catch (e) { + developer.log('Unexpected error: $e', name: 'ClientHubsBloc'); + emit( + state.copyWith( + status: ClientHubsStatus.actionFailure, + errorMessage: 'errors.generic.unknown', ), ); } @@ -120,11 +151,20 @@ class ClientHubsBloc extends Bloc successMessage: 'Hub deleted successfully', ), ); - } catch (e) { + } on AppException catch (e) { + developer.log('Error ${e.code}: ${e.technicalMessage}', name: 'ClientHubsBloc'); emit( state.copyWith( status: ClientHubsStatus.actionFailure, - errorMessage: e.toString(), + errorMessage: e.messageKey, + ), + ); + } catch (e) { + developer.log('Unexpected error: $e', name: 'ClientHubsBloc'); + emit( + state.copyWith( + status: ClientHubsStatus.actionFailure, + errorMessage: 'errors.generic.unknown', ), ); } @@ -148,11 +188,20 @@ class ClientHubsBloc extends Bloc clearHubToIdentify: true, ), ); - } catch (e) { + } on AppException catch (e) { + developer.log('Error ${e.code}: ${e.technicalMessage}', name: 'ClientHubsBloc'); emit( state.copyWith( status: ClientHubsStatus.actionFailure, - errorMessage: e.toString(), + errorMessage: e.messageKey, + ), + ); + } catch (e) { + developer.log('Unexpected error: $e', name: 'ClientHubsBloc'); + emit( + state.copyWith( + status: ClientHubsStatus.actionFailure, + errorMessage: 'errors.generic.unknown', ), ); } @@ -164,8 +213,8 @@ class ClientHubsBloc extends Bloc ) { emit( state.copyWith( - errorMessage: null, - successMessage: null, + clearErrorMessage: true, + clearSuccessMessage: true, status: state.status == ClientHubsStatus.actionSuccess || state.status == ClientHubsStatus.actionFailure diff --git a/apps/mobile/packages/features/client/hubs/lib/src/presentation/blocs/client_hubs_event.dart b/apps/mobile/packages/features/client/hubs/lib/src/presentation/blocs/client_hubs_event.dart index a42a4843..428eb774 100644 --- a/apps/mobile/packages/features/client/hubs/lib/src/presentation/blocs/client_hubs_event.dart +++ b/apps/mobile/packages/features/client/hubs/lib/src/presentation/blocs/client_hubs_event.dart @@ -18,11 +18,41 @@ class ClientHubsFetched extends ClientHubsEvent { class ClientHubsAddRequested extends ClientHubsEvent { final String name; final String address; + final String? placeId; + final double? latitude; + final double? longitude; + final String? city; + final String? state; + final String? street; + final String? country; + final String? zipCode; - const ClientHubsAddRequested({required this.name, required this.address}); + const ClientHubsAddRequested({ + required this.name, + required this.address, + this.placeId, + this.latitude, + this.longitude, + this.city, + this.state, + this.street, + this.country, + this.zipCode, + }); @override - List get props => [name, address]; + List get props => [ + name, + address, + placeId, + latitude, + longitude, + city, + state, + street, + country, + zipCode, + ]; } /// Event triggered to delete a hub. diff --git a/apps/mobile/packages/features/client/hubs/lib/src/presentation/blocs/client_hubs_state.dart b/apps/mobile/packages/features/client/hubs/lib/src/presentation/blocs/client_hubs_state.dart index efccca99..4d592df8 100644 --- a/apps/mobile/packages/features/client/hubs/lib/src/presentation/blocs/client_hubs_state.dart +++ b/apps/mobile/packages/features/client/hubs/lib/src/presentation/blocs/client_hubs_state.dart @@ -43,12 +43,18 @@ class ClientHubsState extends Equatable { bool? showAddHubDialog, Hub? hubToIdentify, bool clearHubToIdentify = false, + bool clearErrorMessage = false, + bool clearSuccessMessage = false, }) { return ClientHubsState( status: status ?? this.status, hubs: hubs ?? this.hubs, - errorMessage: errorMessage ?? this.errorMessage, - successMessage: successMessage ?? this.successMessage, + errorMessage: clearErrorMessage + ? null + : (errorMessage ?? this.errorMessage), + successMessage: clearSuccessMessage + ? null + : (successMessage ?? this.successMessage), showAddHubDialog: showAddHubDialog ?? this.showAddHubDialog, hubToIdentify: clearHubToIdentify ? null diff --git a/apps/mobile/packages/features/client/hubs/lib/src/presentation/pages/client_hubs_page.dart b/apps/mobile/packages/features/client/hubs/lib/src/presentation/pages/client_hubs_page.dart index 67e84b41..aa3de3e2 100644 --- a/apps/mobile/packages/features/client/hubs/lib/src/presentation/pages/client_hubs_page.dart +++ b/apps/mobile/packages/features/client/hubs/lib/src/presentation/pages/client_hubs_page.dart @@ -27,16 +27,21 @@ class ClientHubsPage extends StatelessWidget { create: (BuildContext context) => Modular.get()..add(const ClientHubsFetched()), child: BlocConsumer( + listenWhen: (ClientHubsState previous, ClientHubsState current) { + return previous.errorMessage != current.errorMessage || + previous.successMessage != current.successMessage; + }, listener: (BuildContext context, ClientHubsState state) { - if (state.errorMessage != null) { - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text(state.errorMessage!))); + if (state.errorMessage != null && state.errorMessage!.isNotEmpty) { + final String errorMessage = translateErrorKey(state.errorMessage!); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(errorMessage)), + ); BlocProvider.of( context, ).add(const ClientHubsMessageCleared()); } - if (state.successMessage != null) { + if (state.successMessage != null && state.successMessage!.isNotEmpty) { ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text(state.successMessage!))); @@ -90,10 +95,10 @@ class ClientHubsPage extends StatelessWidget { ).add( ClientHubsIdentifyDialogToggled(hub: hub), ), - onDeletePressed: () => - BlocProvider.of( - context, - ).add(ClientHubsDeleteRequested(hub.id)), + onDeletePressed: () => _confirmDeleteHub( + context, + hub, + ), ), ), ], @@ -106,9 +111,21 @@ class ClientHubsPage extends StatelessWidget { ), if (state.showAddHubDialog) AddHubDialog( - onCreate: (String name, String address) { + onCreate: ( + String name, + String address, { + String? placeId, + double? latitude, + double? longitude, + }) { BlocProvider.of(context).add( - ClientHubsAddRequested(name: name, address: address), + ClientHubsAddRequested( + name: name, + address: address, + placeId: placeId, + latitude: latitude, + longitude: longitude, + ), ); }, onCancel: () => BlocProvider.of( @@ -162,7 +179,7 @@ class ClientHubsPage extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ GestureDetector( - onTap: () => Modular.to.pop(), + onTap: () => Modular.to.navigate('/client-main/home/'), child: Container( width: 40, height: 40, @@ -204,4 +221,51 @@ class ClientHubsPage extends StatelessWidget { ), ); } + + Future _confirmDeleteHub(BuildContext context, Hub hub) async { + final String hubName = hub.name.isEmpty ? 'this hub' : hub.name; + return showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext dialogContext) { + return AlertDialog( + title: const Text('Confirm Hub Deletion'), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Are you sure you want to delete "$hubName"?'), + const SizedBox(height: UiConstants.space2), + const Text('This action cannot be undone.'), + const SizedBox(height: UiConstants.space2), + Text( + 'Note that if there are any shifts/orders assigned to this hub we shouldn\'t be able to delete the hub.', + style: UiTypography.footnote1r.copyWith( + color: UiColors.textSecondary, + ), + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Modular.to.pop(), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () { + BlocProvider.of( + context, + ).add(ClientHubsDeleteRequested(hub.id)); + Modular.to.pop(); + }, + style: TextButton.styleFrom( + foregroundColor: UiColors.destructive, + ), + child: const Text('Delete'), + ), + ], + ); + }, + ); + } } diff --git a/apps/mobile/packages/features/client/hubs/lib/src/presentation/widgets/add_hub_dialog.dart b/apps/mobile/packages/features/client/hubs/lib/src/presentation/widgets/add_hub_dialog.dart index 2a4dd8e9..7d95c749 100644 --- a/apps/mobile/packages/features/client/hubs/lib/src/presentation/widgets/add_hub_dialog.dart +++ b/apps/mobile/packages/features/client/hubs/lib/src/presentation/widgets/add_hub_dialog.dart @@ -1,11 +1,20 @@ import 'package:design_system/design_system.dart'; import 'package:flutter/material.dart'; import 'package:core_localization/core_localization.dart'; +import 'package:google_places_flutter/model/prediction.dart'; + +import 'hub_address_autocomplete.dart'; /// A dialog for adding a new hub. class AddHubDialog extends StatefulWidget { /// Callback when the "Create Hub" button is pressed. - final Function(String name, String address) onCreate; + final void Function( + String name, + String address, { + String? placeId, + double? latitude, + double? longitude, + }) onCreate; /// Callback when the dialog is cancelled. final VoidCallback onCancel; @@ -24,18 +33,22 @@ class AddHubDialog extends StatefulWidget { class _AddHubDialogState extends State { late final TextEditingController _nameController; late final TextEditingController _addressController; + late final FocusNode _addressFocusNode; + Prediction? _selectedPrediction; @override void initState() { super.initState(); _nameController = TextEditingController(); _addressController = TextEditingController(); + _addressFocusNode = FocusNode(); } @override void dispose() { _nameController.dispose(); _addressController.dispose(); + _addressFocusNode.dispose(); super.dispose(); } @@ -74,12 +87,13 @@ class _AddHubDialogState extends State { ), const SizedBox(height: UiConstants.space4), _buildFieldLabel(t.client_hubs.add_hub_dialog.address_label), - TextField( + HubAddressAutocomplete( controller: _addressController, - style: UiTypography.body1r.textPrimary, - decoration: _buildInputDecoration( - t.client_hubs.add_hub_dialog.address_hint, - ), + hintText: t.client_hubs.add_hub_dialog.address_hint, + focusNode: _addressFocusNode, + onSelected: (Prediction prediction) { + _selectedPrediction = prediction; + }, ), const SizedBox(height: UiConstants.space8), Row( @@ -98,6 +112,13 @@ class _AddHubDialogState extends State { widget.onCreate( _nameController.text, _addressController.text, + placeId: _selectedPrediction?.placeId, + latitude: double.tryParse( + _selectedPrediction?.lat ?? '', + ), + longitude: double.tryParse( + _selectedPrediction?.lng ?? '', + ), ); } }, diff --git a/apps/mobile/packages/features/client/hubs/lib/src/presentation/widgets/hub_address_autocomplete.dart b/apps/mobile/packages/features/client/hubs/lib/src/presentation/widgets/hub_address_autocomplete.dart new file mode 100644 index 00000000..784cf094 --- /dev/null +++ b/apps/mobile/packages/features/client/hubs/lib/src/presentation/widgets/hub_address_autocomplete.dart @@ -0,0 +1,61 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:google_places_flutter/google_places_flutter.dart'; +import 'package:google_places_flutter/model/prediction.dart'; + +import '../../util/hubs_constants.dart'; + +class HubAddressAutocomplete extends StatelessWidget { + const HubAddressAutocomplete({ + required this.controller, + required this.hintText, + this.focusNode, + this.onSelected, + super.key, + }); + + final TextEditingController controller; + final String hintText; + final FocusNode? focusNode; + final void Function(Prediction prediction)? onSelected; + + @override + Widget build(BuildContext context) { + return GooglePlaceAutoCompleteTextField( + textEditingController: controller, + focusNode: focusNode, + googleAPIKey: HubsConstants.googlePlacesApiKey, + debounceTime: 500, + countries: HubsConstants.supportedCountries, + isLatLngRequired: true, + getPlaceDetailWithLatLng: (Prediction prediction) { + onSelected?.call(prediction); + }, + itemClick: (Prediction prediction) { + controller.text = prediction.description ?? ''; + controller.selection = TextSelection.fromPosition( + TextPosition(offset: controller.text.length), + ); + onSelected?.call(prediction); + }, + itemBuilder: (_, _, Prediction prediction) { + return Padding( + padding: const EdgeInsets.all(UiConstants.space2), + child: Row( + spacing: UiConstants.space1, + children: [ + const Icon(UiIcons.mapPin, color: UiColors.iconSecondary), + Expanded( + child: Text( + prediction.description ?? "", + style: UiTypography.body1r.textSecondary, + ), + ), + ], + ), + ); + }, + textStyle: UiTypography.body1r.textPrimary, + ); + } +} diff --git a/apps/mobile/packages/features/client/hubs/lib/src/util/hubs_constants.dart b/apps/mobile/packages/features/client/hubs/lib/src/util/hubs_constants.dart new file mode 100644 index 00000000..23d706bc --- /dev/null +++ b/apps/mobile/packages/features/client/hubs/lib/src/util/hubs_constants.dart @@ -0,0 +1,4 @@ +class HubsConstants { + static const String googlePlacesApiKey = String.fromEnvironment('GOOGLE_PLACES_API_KEY'); + static const List supportedCountries = ['us']; +} diff --git a/apps/mobile/packages/features/client/hubs/pubspec.yaml b/apps/mobile/packages/features/client/hubs/pubspec.yaml index 3c578989..1eaf1911 100644 --- a/apps/mobile/packages/features/client/hubs/pubspec.yaml +++ b/apps/mobile/packages/features/client/hubs/pubspec.yaml @@ -11,11 +11,7 @@ environment: dependencies: flutter: sdk: flutter - flutter_bloc: ^8.1.0 - flutter_modular: ^6.3.2 - equatable: ^2.0.5 - lucide_icons: ^0.257.0 - + # Architecture Packages krow_core: path: ../../../core @@ -27,8 +23,15 @@ dependencies: path: ../../../design_system core_localization: path: ../../../core_localization + + flutter_bloc: ^8.1.0 + flutter_modular: ^6.3.2 + equatable: ^2.0.5 + lucide_icons: ^0.257.0 firebase_auth: ^6.1.4 firebase_data_connect: ^0.2.2+2 + google_places_flutter: ^2.1.1 + http: ^1.2.2 dev_dependencies: flutter_test: diff --git a/apps/mobile/packages/features/client/settings/lib/src/presentation/widgets/client_settings_page/settings_actions.dart b/apps/mobile/packages/features/client/settings/lib/src/presentation/widgets/client_settings_page/settings_actions.dart index 99c77655..e044d1ec 100644 --- a/apps/mobile/packages/features/client/settings/lib/src/presentation/widgets/client_settings_page/settings_actions.dart +++ b/apps/mobile/packages/features/client/settings/lib/src/presentation/widgets/client_settings_page/settings_actions.dart @@ -24,8 +24,8 @@ class SettingsActions extends StatelessWidget { delegate: SliverChildListDelegate([ const SizedBox(height: UiConstants.space5), - /// TODO: FEATURE_NOT_YET_IMPLEMENTED - // Edit profile is not yet implemented + /// TODO: FEATURE_NOT_YET_IMPLEMENTED + // Edit profile is not yet implemented // Hubs button UiButton.primary( @@ -49,12 +49,19 @@ class SettingsActions extends StatelessWidget { ), ); } + + /// Handles the sign-out button click event. + void _onSignoutClicked(BuildContext context) { + ReadContext( + context, + ).read().add(const ClientSettingsSignOutRequested()); + } /// Shows a confirmation dialog for signing out. Future _showSignOutDialog(BuildContext context) { return showDialog( context: context, - builder: (BuildContext context) => AlertDialog( + builder: (BuildContext dialogContext) => AlertDialog( backgroundColor: UiColors.bgPopup, elevation: 0, shape: RoundedRectangleBorder(borderRadius: UiConstants.radiusLg), @@ -70,12 +77,7 @@ class SettingsActions extends StatelessWidget { // Log out button UiButton.secondary( text: t.client_settings.profile.log_out, - onPressed: () { - Modular.to.pop(); - BlocProvider.of( - context, - ).add(const ClientSettingsSignOutRequested()); - }, + onPressed: () => _onSignoutClicked(context), ), // Cancel button 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 fc05ccf6..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 @@ -30,12 +30,12 @@ class SettingsProfileHeader extends StatelessWidget { shape: const Border(bottom: BorderSide(color: UiColors.border, width: 1)), leading: IconButton( icon: const Icon(UiIcons.chevronLeft, color: UiColors.textSecondary), - onPressed: () => Modular.to.pop(), + onPressed: () => Modular.to.navigate('/client-main/home/'), ), 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, diff --git a/apps/mobile/packages/features/client/view_orders/lib/src/data/repositories/view_orders_repository_impl.dart b/apps/mobile/packages/features/client/view_orders/lib/src/data/repositories/view_orders_repository_impl.dart index 3a9e9fe2..c7e8f653 100644 --- a/apps/mobile/packages/features/client/view_orders/lib/src/data/repositories/view_orders_repository_impl.dart +++ b/apps/mobile/packages/features/client/view_orders/lib/src/data/repositories/view_orders_repository_impl.dart @@ -36,13 +36,16 @@ class ViewOrdersRepositoryImpl implements IViewOrdersRepository { end: endTimestamp, ) .execute(); + print( + 'ViewOrders range start=${start.toIso8601String()} end=${end.toIso8601String()} shiftRoles=${result.data.shiftRoles.length}', + ); final String businessName = dc.ClientSessionStore.instance.session?.business?.businessName ?? 'Your Company'; return result.data.shiftRoles.map((dc.ListShiftRolesByBusinessAndDateRangeShiftRoles shiftRole) { - final DateTime? shiftDate = shiftRole.shift.date?.toDateTime(); + final DateTime? shiftDate = shiftRole.shift.date?.toDateTime().toLocal(); final String dateStr = shiftDate == null ? '' : DateFormat('yyyy-MM-dd').format(shiftDate); @@ -62,10 +65,13 @@ class ViewOrdersRepositoryImpl implements IViewOrdersRepository { 'end=${shiftRole.endTime?.toJson()} hours=$hours totalValue=$totalValue', ); + final String eventName = + shiftRole.shift.order.eventName ?? shiftRole.shift.title; + return domain.OrderItem( id: _shiftRoleKey(shiftRole.shiftId, shiftRole.roleId), orderId: shiftRole.shift.order.id, - title: '${shiftRole.role.name} - ${shiftRole.shift.title}', + title: '${shiftRole.role.name} - $eventName', clientName: businessName, status: status, date: dateStr, @@ -103,6 +109,10 @@ class ViewOrdersRepositoryImpl implements IViewOrdersRepository { ) .execute(); + print( + 'ViewOrders day=${day.toIso8601String()} applications=${result.data.applications.length}', + ); + final Map>> grouped = >>{}; for (final dc.ListAcceptedApplicationsByBusinessForDayApplications application in result.data.applications) { print( @@ -117,6 +127,8 @@ class ViewOrdersRepositoryImpl implements IViewOrdersRepository { 'worker_name': application.staff.fullName, 'status': 'confirmed', 'photo_url': application.staff.photoUrl, + 'phone': application.staff.phone, + 'rating': application.staff.averageRating, }); } return grouped; @@ -138,14 +150,26 @@ class ViewOrdersRepositoryImpl implements IViewOrdersRepository { } DateTime _endOfDay(DateTime dateTime) { - return DateTime(dateTime.year, dateTime.month, dateTime.day, 23, 59, 59); + // We add the current microseconds to ensure the query variables are unique + // each time we fetch, bypassing any potential Data Connect caching. + final DateTime now = DateTime.now(); + return DateTime( + dateTime.year, + dateTime.month, + dateTime.day, + 23, + 59, + 59, + now.millisecond, + now.microsecond, + ); } String _formatTime(fdc.Timestamp? timestamp) { if (timestamp == null) { return ''; } - final DateTime dateTime = timestamp.toDateTime(); + final DateTime dateTime = timestamp.toDateTime().toLocal(); return DateFormat('HH:mm').format(dateTime); } diff --git a/apps/mobile/packages/features/client/view_orders/lib/src/presentation/blocs/view_orders_cubit.dart b/apps/mobile/packages/features/client/view_orders/lib/src/presentation/blocs/view_orders_cubit.dart index 727268af..f65f4964 100644 --- a/apps/mobile/packages/features/client/view_orders/lib/src/presentation/blocs/view_orders_cubit.dart +++ b/apps/mobile/packages/features/client/view_orders/lib/src/presentation/blocs/view_orders_cubit.dart @@ -23,6 +23,7 @@ class ViewOrdersCubit extends Cubit { final GetOrdersUseCase _getOrdersUseCase; final GetAcceptedApplicationsForDayUseCase _getAcceptedAppsUseCase; + int _requestId = 0; void _init() { updateWeekOffset(0); // Initialize calendar days @@ -33,6 +34,7 @@ class ViewOrdersCubit extends Cubit { required DateTime rangeEnd, required DateTime dayForApps, }) async { + final int requestId = ++_requestId; emit(state.copyWith(status: ViewOrdersStatus.loading)); try { final List orders = await _getOrdersUseCase( @@ -42,6 +44,9 @@ class ViewOrdersCubit extends Cubit { OrdersDayArguments(day: dayForApps), ); final List updatedOrders = _applyApplications(orders, apps); + if (requestId != _requestId) { + return; + } emit( state.copyWith( status: ViewOrdersStatus.success, @@ -50,6 +55,9 @@ class ViewOrdersCubit extends Cubit { ); _updateDerivedState(); } catch (_) { + if (requestId != _requestId) { + return; + } emit(state.copyWith(status: ViewOrdersStatus.failure)); } } @@ -88,6 +96,28 @@ class ViewOrdersCubit extends Cubit { ); } + void jumpToDate(DateTime date) { + final DateTime target = DateTime(date.year, date.month, date.day); + final DateTime startDate = _calculateCalendarDays(0).first; + final int diffDays = target.difference(startDate).inDays; + final int targetOffset = (diffDays / 7).floor(); + final List calendarDays = _calculateCalendarDays(targetOffset); + + emit( + state.copyWith( + weekOffset: targetOffset, + calendarDays: calendarDays, + selectedDate: target, + ), + ); + + _loadOrdersForRange( + rangeStart: calendarDays.first, + rangeEnd: calendarDays.last, + dayForApps: target, + ); + } + void _updateDerivedState() { final List filteredOrders = _calculateFilteredOrders(state); final int activeCount = _calculateCategoryCount('active'); @@ -130,7 +160,7 @@ class ViewOrdersCubit extends Cubit { final int filled = confirmed.length; final String status = - filled >= order.workersNeeded ? 'filled' : order.status; + filled >= order.workersNeeded ? 'FILLED' : order.status; return OrderItem( id: order.id, orderId: order.orderId, @@ -182,6 +212,9 @@ class ViewOrdersCubit extends Cubit { final List ordersOnDate = state.orders .where((OrderItem s) => s.date == selectedDateStr) .toList(); + print( + 'ViewOrders selectedDate=$selectedDateStr ordersOnDate=${ordersOnDate.length}', + ); // Sort by start time ordersOnDate.sort( @@ -189,22 +222,34 @@ class ViewOrdersCubit extends Cubit { ); if (state.filterTab == 'all') { - return ordersOnDate + final List filtered = ordersOnDate .where( (OrderItem s) => // TODO(orders): move PENDING to its own tab once available. - ['OPEN', 'FILLED', 'CONFIRMED', 'PENDING'] + ['OPEN', 'FILLED', 'CONFIRMED', 'PENDING', 'ASSIGNED'] .contains(s.status), ) .toList(); + print( + 'ViewOrders tab=all statuses=${ordersOnDate.map((OrderItem s) => s.status).toList()} filtered=${filtered.length}', + ); + return filtered; } else if (state.filterTab == 'active') { - return ordersOnDate + final List filtered = ordersOnDate .where((OrderItem s) => s.status == 'IN_PROGRESS') .toList(); + print( + 'ViewOrders tab=active statuses=${ordersOnDate.map((OrderItem s) => s.status).toList()} filtered=${filtered.length}', + ); + return filtered; } else if (state.filterTab == 'completed') { - return ordersOnDate + final List filtered = ordersOnDate .where((OrderItem s) => s.status == 'COMPLETED') .toList(); + print( + 'ViewOrders tab=completed statuses=${ordersOnDate.map((OrderItem s) => s.status).toList()} filtered=${filtered.length}', + ); + return filtered; } return []; } @@ -242,7 +287,7 @@ class ViewOrdersCubit extends Cubit { .where( (OrderItem s) => // TODO(orders): move PENDING to its own tab once available. - ['OPEN', 'FILLED', 'CONFIRMED', 'PENDING'] + ['OPEN', 'FILLED', 'CONFIRMED', 'PENDING', 'ASSIGNED'] .contains(s.status), ) .length; diff --git a/apps/mobile/packages/features/client/view_orders/lib/src/presentation/navigation/view_orders_navigator.dart b/apps/mobile/packages/features/client/view_orders/lib/src/presentation/navigation/view_orders_navigator.dart index 7160bb59..78575ccf 100644 --- a/apps/mobile/packages/features/client/view_orders/lib/src/presentation/navigation/view_orders_navigator.dart +++ b/apps/mobile/packages/features/client/view_orders/lib/src/presentation/navigation/view_orders_navigator.dart @@ -4,7 +4,7 @@ import 'package:flutter_modular/flutter_modular.dart'; extension ViewOrdersNavigator on IModularNavigator { /// Navigates to the Create Order feature. void navigateToCreateOrder() { - pushNamed('/client/create-order/'); + navigate('/client/create-order/'); } /// Navigates to the Order Details (placeholder for now). 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 c47b8518..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. @@ -20,21 +20,51 @@ import '../navigation/view_orders_navigator.dart'; /// - Adhering to the project's Design System. class ViewOrdersPage extends StatelessWidget { /// Creates a [ViewOrdersPage]. - const ViewOrdersPage({super.key}); + const ViewOrdersPage({super.key, this.initialDate}); + + /// The initial date to display orders for. + final DateTime? initialDate; @override Widget build(BuildContext context) { return BlocProvider( create: (BuildContext context) => Modular.get(), - child: const ViewOrdersView(), + child: ViewOrdersView(initialDate: initialDate), ); } } /// The internal view implementation for [ViewOrdersPage]. -class ViewOrdersView extends StatelessWidget { +class ViewOrdersView extends StatefulWidget { /// Creates a [ViewOrdersView]. - const ViewOrdersView({super.key}); + const ViewOrdersView({super.key, this.initialDate}); + + /// The initial date to display orders for. + final DateTime? initialDate; + + @override + State createState() => _ViewOrdersViewState(); +} + +class _ViewOrdersViewState extends State { + bool _didInitialJump = false; + ViewOrdersCubit? _cubit; + + @override + void initState() { + super.initState(); + // Force initialization of cubit immediately + _cubit = BlocProvider.of(context, listen: false); + + if (widget.initialDate != null) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; + if (_didInitialJump) return; + _didInitialJump = true; + _cubit?.jumpToDate(widget.initialDate!); + }); + } + } @override Widget build(BuildContext context) { @@ -60,375 +90,84 @@ class ViewOrdersView extends StatelessWidget { } return Scaffold( - backgroundColor: UiColors.white, - 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, - ), - ), - 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', - ), - const SizedBox(width: UiConstants.space6), - _buildFilterTab( - context, - label: t.client_view_orders.tabs.active, - isSelected: state.filterTab == 'active', - tabId: 'active', - count: state.activeCount + state.upNextCount, - ), - 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 a5eee120..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 @@ -3,9 +3,13 @@ import 'package:design_system/design_system.dart'; import 'package:firebase_auth/firebase_auth.dart' as firebase; import 'package:firebase_data_connect/firebase_data_connect.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; 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. /// @@ -30,7 +34,10 @@ class _ViewOrderCardState extends State { context: context, isScrollControlled: true, backgroundColor: Colors.transparent, - builder: (BuildContext context) => _OrderEditSheet(order: order), + builder: (BuildContext context) => _OrderEditSheet( + order: order, + onUpdated: () => this.context.read().updateWeekOffset(0), + ), ); } @@ -192,37 +199,42 @@ class _ViewOrderCardState extends State { order.clientName, style: UiTypography.body3r.textSecondary, ), - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 6, - ), - child: Text( - '•', - style: UiTypography.body3r.textInactive, - ), - ), - Text( - _formatDate(dateStr: order.date), - style: UiTypography.body3m.textSecondary, - ), + const SizedBox(width: 0), ], ), const SizedBox(height: UiConstants.space2), - // Address + // Location (Hub name + Address) Row( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Icon( - UiIcons.mapPin, - size: 14, - color: UiColors.iconSecondary, + const Padding( + padding: EdgeInsets.only(top: 2), + child: Icon( + UiIcons.mapPin, + size: 14, + color: UiColors.iconSecondary, + ), ), const SizedBox(width: 4), Expanded( - child: Text( - order.locationAddress, - style: UiTypography.footnote2r.textSecondary, - maxLines: 1, - overflow: TextOverflow.ellipsis, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (order.location.isNotEmpty) + Text( + order.location, + style: UiTypography.footnote1b.textPrimary, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + if (order.locationAddress.isNotEmpty) + Text( + order.locationAddress, + style: UiTypography.footnote2r.textSecondary, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], ), ), ], @@ -241,14 +253,16 @@ class _ViewOrderCardState extends State { onTap: () => _openEditSheet(order: order), ), const SizedBox(width: UiConstants.space2), - _buildHeaderIconButton( - icon: _expanded - ? UiIcons.chevronUp - : UiIcons.chevronDown, - color: UiColors.iconSecondary, - bgColor: UiColors.bgSecondary, - onTap: () => setState(() => _expanded = !_expanded), - ), + if (order.confirmedApps.isNotEmpty) + _buildHeaderIconButton( + icon: _expanded + ? UiIcons.chevronUp + : UiIcons.chevronDown, + color: UiColors.iconSecondary, + bgColor: UiColors.bgSecondary, + onTap: () => + setState(() => _expanded = !_expanded), + ), ], ), ], @@ -276,8 +290,7 @@ class _ViewOrderCardState extends State { _buildStatDivider(), _buildStatItem( icon: UiIcons.users, - value: - '${order.filled > 0 ? order.filled : order.workersNeeded}', + value: '${order.workersNeeded}', label: 'Workers', ), ], @@ -313,14 +326,23 @@ class _ViewOrderCardState extends State { children: [ Row( children: [ + if (coveragePercent != 100) const Icon( - UiIcons.success, + 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.filled}/${order.workersNeeded} Workers Filled', + coveragePercent == 100 + ? 'All Workers Confirmed' + : '${order.workersNeeded} Workers Needed', style: UiTypography.body2m.textPrimary, ), ], @@ -492,6 +514,7 @@ class _ViewOrderCardState extends State { /// Builds a detailed row for a worker. Widget _buildWorkerRow(Map app) { + final String? phone = app['phone'] as String?; return Container( margin: const EdgeInsets.only(bottom: 12), padding: const EdgeInsets.all(12), @@ -522,9 +545,19 @@ class _ViewOrderCardState extends State { const SizedBox(height: 2), Row( children: [ - const Icon(UiIcons.star, size: 10, color: UiColors.accent), - const SizedBox(width: 2), - Text('4.8', style: UiTypography.footnote2r.textSecondary), + if ((app['rating'] as num?) != null && + (app['rating'] as num) > 0) ...[ + const Icon( + UiIcons.star, + size: 10, + color: UiColors.accent, + ), + const SizedBox(width: 2), + Text( + (app['rating'] as num).toStringAsFixed(1), + style: UiTypography.footnote2r.textSecondary, + ), + ], if (app['check_in_time'] != null) ...[ const SizedBox(width: 8), Container( @@ -543,20 +576,70 @@ class _ViewOrderCardState extends State { ), ), ), + ] else if ((app['status'] as String?)?.isNotEmpty ?? false) ...[ + const SizedBox(width: 8), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 4, + vertical: 1, + ), + decoration: BoxDecoration( + color: UiColors.bgSecondary, + borderRadius: BorderRadius.circular(4), + ), + child: Text( + (app['status'] as String).toUpperCase(), + style: UiTypography.titleUppercase4m.copyWith( + color: UiColors.textSecondary, + ), + ), + ), ], ], ), ], ), ), - _buildActionIconButton(icon: UiIcons.phone, onTap: () {}), - const SizedBox(width: 8), - _buildActionIconButton(icon: UiIcons.messageCircle, onTap: () {}), + if (phone != null && phone.isNotEmpty) ...[ + _buildActionIconButton( + icon: UiIcons.phone, + onTap: () => _confirmAndCall(phone), + ), + ], ], ), ); } + Future _confirmAndCall(String phone) async { + final bool? shouldCall = await showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Call'), + content: Text('Do you want to call $phone?'), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(true), + child: const Text('Call'), + ), + ], + ); + }, + ); + + if (shouldCall != true) { + return; + } + + final Uri uri = Uri(scheme: 'tel', path: phone); + await launchUrl(uri); + } + /// Specialized action button for worker rows. Widget _buildActionIconButton({ required IconData icon, @@ -644,9 +727,13 @@ class _ShiftRoleKey { /// A sophisticated bottom sheet for editing an existing order, /// following the Unified Order Flow prototype and matching OneTimeOrderView. class _OrderEditSheet extends StatefulWidget { - const _OrderEditSheet({required this.order}); + const _OrderEditSheet({ + required this.order, + this.onUpdated, + }); final OrderItem order; + final VoidCallback? onUpdated; @override State<_OrderEditSheet> createState() => _OrderEditSheetState(); @@ -658,6 +745,7 @@ class _OrderEditSheetState extends State<_OrderEditSheet> { late TextEditingController _dateController; late TextEditingController _globalLocationController; + late TextEditingController _orderNameController; late List> _positions; @@ -667,6 +755,8 @@ class _OrderEditSheetState extends State<_OrderEditSheet> { List _vendors = const []; Vendor? _selectedVendor; List<_RoleOption> _roles = const <_RoleOption>[]; + List _hubs = const []; + dc.ListTeamHubsByOwnerIdTeamHubs? _selectedHub; String? _shiftId; List<_ShiftRoleKey> _originalShiftRoles = const <_ShiftRoleKey>[]; @@ -678,6 +768,7 @@ class _OrderEditSheetState extends State<_OrderEditSheet> { _globalLocationController = TextEditingController( text: widget.order.locationAddress, ); + _orderNameController = TextEditingController(); _positions = >[ { @@ -700,6 +791,7 @@ class _OrderEditSheetState extends State<_OrderEditSheet> { void dispose() { _dateController.dispose(); _globalLocationController.dispose(); + _orderNameController.dispose(); super.dispose(); } @@ -728,6 +820,7 @@ class _OrderEditSheetState extends State<_OrderEditSheet> { final List shiftRoles = result.data.shiftRoles; if (shiftRoles.isEmpty) { + await _loadHubsAndSelect(); return; } @@ -737,13 +830,14 @@ class _OrderEditSheetState extends State<_OrderEditSheet> { final String dateText = orderDate == null ? widget.order.date : DateFormat('yyyy-MM-dd').format(orderDate); - final String location = firstShift.order.location ?? + final String location = firstShift.order.teamHub?.hubName ?? firstShift.locationAddress ?? firstShift.location ?? widget.order.locationAddress; _dateController.text = dateText; _globalLocationController.text = location; + _orderNameController.text = firstShift.order.eventName ?? ''; _shiftId = shiftRoles.first.shiftId; final List> positions = @@ -774,6 +868,13 @@ class _OrderEditSheetState extends State<_OrderEditSheet> { .toList(); await _loadVendorsAndSelect(firstShift.order.vendorId); + final dc.ListShiftRolesByBusinessAndOrderShiftRolesShiftOrderTeamHub? + teamHub = firstShift.order.teamHub; + await _loadHubsAndSelect( + placeId: teamHub?.placeId, + hubName: teamHub?.hubName, + address: teamHub?.address, + ); if (mounted) { setState(() { @@ -786,6 +887,75 @@ class _OrderEditSheetState extends State<_OrderEditSheet> { } } + Future _loadHubsAndSelect({ + String? placeId, + String? hubName, + String? address, + }) async { + final String? businessId = + dc.ClientSessionStore.instance.session?.business?.id; + if (businessId == null || businessId.isEmpty) { + return; + } + + try { + final QueryResult< + dc.ListTeamHubsByOwnerIdData, + dc.ListTeamHubsByOwnerIdVariables> result = await _dataConnect + .listTeamHubsByOwnerId(ownerId: businessId) + .execute(); + + final List hubs = result.data.teamHubs; + dc.ListTeamHubsByOwnerIdTeamHubs? selected; + + if (placeId != null && placeId.isNotEmpty) { + for (final dc.ListTeamHubsByOwnerIdTeamHubs hub in hubs) { + if (hub.placeId == placeId) { + selected = hub; + break; + } + } + } + + if (selected == null && hubName != null && hubName.isNotEmpty) { + for (final dc.ListTeamHubsByOwnerIdTeamHubs hub in hubs) { + if (hub.hubName == hubName) { + selected = hub; + break; + } + } + } + + if (selected == null && address != null && address.isNotEmpty) { + for (final dc.ListTeamHubsByOwnerIdTeamHubs hub in hubs) { + if (hub.address == address) { + selected = hub; + break; + } + } + } + + selected ??= hubs.isNotEmpty ? hubs.first : null; + + if (mounted) { + setState(() { + _hubs = hubs; + _selectedHub = selected; + if (selected != null) { + _globalLocationController.text = selected.address; + } + }); + } + } catch (_) { + if (mounted) { + setState(() { + _hubs = const []; + _selectedHub = null; + }); + } + } + } + Future _loadVendorsAndSelect(String? selectedVendorId) async { try { final QueryResult result = @@ -874,7 +1044,7 @@ class _OrderEditSheetState extends State<_OrderEditSheet> { String _formatTimeForField(Timestamp? value) { if (value == null) return ''; try { - return DateFormat('HH:mm').format(value.toDateTime()); + return DateFormat('HH:mm').format(value.toDateTime().toLocal()); } catch (_) { return ''; } @@ -948,7 +1118,8 @@ class _OrderEditSheetState extends State<_OrderEditSheet> { } Timestamp _toTimestamp(DateTime date) { - final int millis = date.millisecondsSinceEpoch; + final DateTime utc = date.toUtc(); + final int millis = utc.millisecondsSinceEpoch; final int seconds = millis ~/ 1000; final int nanos = (millis % 1000) * 1000000; return Timestamp(nanos, seconds); @@ -987,7 +1158,10 @@ class _OrderEditSheetState extends State<_OrderEditSheet> { } final DateTime orderDate = _parseDate(_dateController.text); - final String location = _globalLocationController.text; + final dc.ListTeamHubsByOwnerIdTeamHubs? selectedHub = _selectedHub; + if (selectedHub == null) { + return; + } int totalWorkers = 0; double shiftCost = 0; @@ -1071,19 +1245,32 @@ class _OrderEditSheetState extends State<_OrderEditSheet> { .execute(); } + final DateTime orderDateOnly = DateTime.utc( + orderDate.year, + orderDate.month, + orderDate.day, + ); + await _dataConnect - .updateOrder(id: widget.order.orderId) + .updateOrder(id: widget.order.orderId, teamHubId: selectedHub.id) .vendorId(_selectedVendor?.id) - .location(location) - .date(_toTimestamp(orderDate)) + .date(_toTimestamp(orderDateOnly)) + .eventName(_orderNameController.text) .execute(); await _dataConnect .updateShift(id: _shiftId!) .title('shift 1 ${DateFormat('yyyy-MM-dd').format(orderDate)}') - .date(_toTimestamp(orderDate)) - .location(location) - .locationAddress(location) + .date(_toTimestamp(orderDateOnly)) + .location(selectedHub.hubName) + .locationAddress(selectedHub.address) + .latitude(selectedHub.latitude) + .longitude(selectedHub.longitude) + .placeId(selectedHub.placeId) + .city(selectedHub.city) + .state(selectedHub.state) + .street(selectedHub.street) + .country(selectedHub.country) .workersNeeded(totalWorkers) .cost(shiftCost) .durationDays(1) @@ -1185,11 +1372,57 @@ class _OrderEditSheetState extends State<_OrderEditSheet> { ), const SizedBox(height: UiConstants.space4), - _buildSectionHeader('LOCATION'), + _buildSectionHeader('ORDER NAME'), UiTextField( - controller: _globalLocationController, - hintText: 'Business address', - prefixIcon: UiIcons.mapPin, + controller: _orderNameController, + hintText: 'Order name', + prefixIcon: UiIcons.briefcase, + ), + const SizedBox(height: UiConstants.space4), + + _buildSectionHeader('HUB'), + Container( + padding: const EdgeInsets.symmetric( + horizontal: UiConstants.space3, + ), + height: 48, + decoration: BoxDecoration( + color: UiColors.white, + borderRadius: UiConstants.radiusMd, + border: Border.all(color: UiColors.border), + ), + child: DropdownButtonHideUnderline( + child: DropdownButton( + isExpanded: true, + value: _selectedHub, + icon: const Icon( + UiIcons.chevronDown, + size: 18, + color: UiColors.iconSecondary, + ), + onChanged: + (dc.ListTeamHubsByOwnerIdTeamHubs? hub) { + if (hub != null) { + setState(() { + _selectedHub = hub; + _globalLocationController.text = hub.address; + }); + } + }, + items: _hubs.map( + (dc.ListTeamHubsByOwnerIdTeamHubs hub) { + return DropdownMenuItem< + dc.ListTeamHubsByOwnerIdTeamHubs>( + value: hub, + child: Text( + hub.hubName, + style: UiTypography.body2m.textPrimary, + ), + ); + }, + ).toList(), + ), + ), ), const SizedBox(height: UiConstants.space6), @@ -1370,7 +1603,19 @@ class _OrderEditSheetState extends State<_OrderEditSheet> { child: _buildInlineTimeInput( label: 'Start', value: pos['start_time'], - onTap: () {}, + onTap: () async { + final TimeOfDay? picked = await showTimePicker( + context: context, + initialTime: TimeOfDay.now(), + ); + if (picked != null && context.mounted) { + _updatePosition( + index, + 'start_time', + picked.format(context), + ); + } + }, ), ), const SizedBox(width: UiConstants.space2), @@ -1378,7 +1623,19 @@ class _OrderEditSheetState extends State<_OrderEditSheet> { child: _buildInlineTimeInput( label: 'End', value: pos['end_time'], - onTap: () {}, + onTap: () async { + final TimeOfDay? picked = await showTimePicker( + context: context, + initialTime: TimeOfDay.now(), + ); + if (picked != null && context.mounted) { + _updatePosition( + index, + 'end_time', + picked.format(context), + ); + } + }, ), ), const SizedBox(width: UiConstants.space2), @@ -1766,7 +2023,10 @@ class _OrderEditSheetState extends State<_OrderEditSheet> { onPressed: () async { setState(() => _isLoading = true); await _saveOrderChanges(); - if (mounted) Navigator.pop(context); + if (mounted) { + widget.onUpdated?.call(); + Navigator.pop(context); + } }, ), ), 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..45e72f93 --- /dev/null +++ b/apps/mobile/packages/features/client/view_orders/lib/src/presentation/widgets/view_orders_header.dart @@ -0,0 +1,268 @@ +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, + ); + + // 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), + ), + ] + : 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), + ], + ), + ), + ), + ); + } +} diff --git a/apps/mobile/packages/features/client/view_orders/lib/src/view_orders_module.dart b/apps/mobile/packages/features/client/view_orders/lib/src/view_orders_module.dart index 3579ca65..ceac0b36 100644 --- a/apps/mobile/packages/features/client/view_orders/lib/src/view_orders_module.dart +++ b/apps/mobile/packages/features/client/view_orders/lib/src/view_orders_module.dart @@ -21,7 +21,7 @@ class ViewOrdersModule extends Module { @override void binds(Injector i) { // Repositories - i.addLazySingleton( + i.add( () => ViewOrdersRepositoryImpl( firebaseAuth: firebase.FirebaseAuth.instance, dataConnect: ExampleConnector.instance, @@ -29,11 +29,11 @@ class ViewOrdersModule extends Module { ); // UseCases - i.addLazySingleton(GetOrdersUseCase.new); - i.addLazySingleton(GetAcceptedApplicationsForDayUseCase.new); + i.add(GetOrdersUseCase.new); + i.add(GetAcceptedApplicationsForDayUseCase.new); // BLoCs - i.addSingleton( + i.add( () => ViewOrdersCubit( getOrdersUseCase: i.get(), getAcceptedAppsUseCase: i.get(), @@ -43,6 +43,23 @@ class ViewOrdersModule extends Module { @override void routes(RouteManager r) { - r.child('/', child: (BuildContext context) => const ViewOrdersPage()); + r.child( + '/', + child: (BuildContext context) { + final Object? args = Modular.args.data; + DateTime? initialDate; + if (args is DateTime) { + initialDate = args; + } else if (args is Map) { + final Object? rawDate = args['initialDate']; + if (rawDate is DateTime) { + initialDate = rawDate; + } else if (rawDate is String) { + initialDate = DateTime.tryParse(rawDate); + } + } + return ViewOrdersPage(initialDate: initialDate); + }, + ); } } diff --git a/apps/mobile/packages/features/client/view_orders/pubspec.yaml b/apps/mobile/packages/features/client/view_orders/pubspec.yaml index dbf26cc2..46182d70 100644 --- a/apps/mobile/packages/features/client/view_orders/pubspec.yaml +++ b/apps/mobile/packages/features/client/view_orders/pubspec.yaml @@ -1,7 +1,7 @@ name: view_orders description: Client View Orders feature package publish_to: 'none' -version: 1.0.0+1 +version: 0.0.1 resolution: workspace environment: @@ -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: diff --git a/apps/mobile/packages/features/staff/authentication/lib/src/data/repositories_impl/auth_repository_impl.dart b/apps/mobile/packages/features/staff/authentication/lib/src/data/repositories_impl/auth_repository_impl.dart index 3b618cd3..742714fc 100644 --- a/apps/mobile/packages/features/staff/authentication/lib/src/data/repositories_impl/auth_repository_impl.dart +++ b/apps/mobile/packages/features/staff/authentication/lib/src/data/repositories_impl/auth_repository_impl.dart @@ -4,6 +4,7 @@ import 'package:firebase_auth/firebase_auth.dart'; import 'package:firebase_data_connect/firebase_data_connect.dart'; import 'package:krow_data_connect/krow_data_connect.dart'; import 'package:krow_domain/krow_domain.dart' as domain; +import '../../utils/test_phone_numbers.dart'; import '../../domain/ui_entities/auth_mode.dart'; import '../../domain/repositories/auth_repository_interface.dart'; @@ -41,7 +42,17 @@ class AuthRepositoryImpl implements AuthRepositoryInterface { await firebaseAuth.verifyPhoneNumber( phoneNumber: phoneNumber, - verificationCompleted: (_) { + verificationCompleted: (PhoneAuthCredential credential) { + // Skip auto-verification for test numbers to allow manual code entry + if (TestPhoneNumbers.isTestNumber(phoneNumber)) { + return; + } + + // For real numbers, we can support auto-verification if desired. + // But since this method returns a verificationId for manual OTP entry, + // we might not handle direct sign-in here unless the architecture changes. + // Currently, we just ignore it for the completer flow, + // or we could sign in directly if the credential is provided. }, verificationFailed: (FirebaseAuthException e) { if (!completer.isCompleted) { @@ -168,7 +179,11 @@ class AuthRepositoryImpl implements AuthRepositoryInterface { avatar: staffRecord.photoUrl, ); StaffSessionStore.instance.setSession( - StaffSession(user: domainUser, staff: domainStaff), + StaffSession( + user: domainUser, + staff: domainStaff, + ownerId: staffRecord?.ownerId, + ), ); return domainUser; } diff --git a/apps/mobile/packages/features/staff/authentication/lib/src/data/repositories_impl/profile_setup_repository_impl.dart b/apps/mobile/packages/features/staff/authentication/lib/src/data/repositories_impl/profile_setup_repository_impl.dart new file mode 100644 index 00000000..1aeabdc2 --- /dev/null +++ b/apps/mobile/packages/features/staff/authentication/lib/src/data/repositories_impl/profile_setup_repository_impl.dart @@ -0,0 +1,67 @@ +import 'package:firebase_auth/firebase_auth.dart' as auth; +import 'package:krow_data_connect/krow_data_connect.dart'; +import 'package:firebase_data_connect/firebase_data_connect.dart' as fdc; +import 'package:krow_domain/krow_domain.dart'; +import '../../domain/repositories/profile_setup_repository.dart'; + +class ProfileSetupRepositoryImpl implements ProfileSetupRepository { + final auth.FirebaseAuth _firebaseAuth; + final ExampleConnector _dataConnect; + + ProfileSetupRepositoryImpl({ + required auth.FirebaseAuth firebaseAuth, + required ExampleConnector dataConnect, + }) : _firebaseAuth = firebaseAuth, + _dataConnect = dataConnect; + + @override + Future submitProfile({ + required String fullName, + String? bio, + required List preferredLocations, + required double maxDistanceMiles, + required List industries, + required List skills, + }) async { + final auth.User? firebaseUser = _firebaseAuth.currentUser; + if (firebaseUser == null) { + throw Exception('User not authenticated.'); + } + + final StaffSession? session = StaffSessionStore.instance.session; + final String email = session?.user.email ?? ''; + final String? phone = firebaseUser.phoneNumber; + + final fdc.OperationResult + result = await _dataConnect + .createStaff( + userId: firebaseUser.uid, + fullName: fullName, + ) + .bio(bio) + .preferredLocations(preferredLocations) + .maxDistanceMiles(maxDistanceMiles.toInt()) + .industries(industries) + .skills(skills) + .email(email.isEmpty ? null : email) + .phone(phone) + .execute(); + + final String staffId = result.data.staff_insert.id; + + final Staff staff = Staff( + id: staffId, + authProviderId: firebaseUser.uid, + name: fullName, + email: email, + phone: phone, + status: StaffStatus.completedProfile, + ); + + if (session != null) { + StaffSessionStore.instance.setSession( + StaffSession(user: session.user, staff: staff, ownerId: session.ownerId), + ); + } + } +} diff --git a/apps/mobile/packages/features/staff/authentication/lib/src/domain/repositories/profile_setup_repository.dart b/apps/mobile/packages/features/staff/authentication/lib/src/domain/repositories/profile_setup_repository.dart new file mode 100644 index 00000000..8b99f0f9 --- /dev/null +++ b/apps/mobile/packages/features/staff/authentication/lib/src/domain/repositories/profile_setup_repository.dart @@ -0,0 +1,12 @@ +import 'package:krow_domain/krow_domain.dart'; + +abstract class ProfileSetupRepository { + Future submitProfile({ + required String fullName, + String? bio, + required List preferredLocations, + required double maxDistanceMiles, + required List industries, + required List skills, + }); +} diff --git a/apps/mobile/packages/features/staff/authentication/lib/src/domain/usecases/submit_profile_setup_usecase.dart b/apps/mobile/packages/features/staff/authentication/lib/src/domain/usecases/submit_profile_setup_usecase.dart new file mode 100644 index 00000000..b69f5fe6 --- /dev/null +++ b/apps/mobile/packages/features/staff/authentication/lib/src/domain/usecases/submit_profile_setup_usecase.dart @@ -0,0 +1,25 @@ +import '../repositories/profile_setup_repository.dart'; + +class SubmitProfileSetup { + final ProfileSetupRepository repository; + + SubmitProfileSetup(this.repository); + + Future call({ + required String fullName, + String? bio, + required List preferredLocations, + required double maxDistanceMiles, + required List industries, + required List skills, + }) { + return repository.submitProfile( + fullName: fullName, + bio: bio, + preferredLocations: preferredLocations, + maxDistanceMiles: maxDistanceMiles, + industries: industries, + skills: skills, + ); + } +} diff --git a/apps/mobile/packages/features/staff/authentication/lib/src/presentation/blocs/profile_setup/profile_setup_bloc.dart b/apps/mobile/packages/features/staff/authentication/lib/src/presentation/blocs/profile_setup/profile_setup_bloc.dart index 93d8b44f..324ea906 100644 --- a/apps/mobile/packages/features/staff/authentication/lib/src/presentation/blocs/profile_setup/profile_setup_bloc.dart +++ b/apps/mobile/packages/features/staff/authentication/lib/src/presentation/blocs/profile_setup/profile_setup_bloc.dart @@ -1,8 +1,5 @@ -import 'package:firebase_auth/firebase_auth.dart' as auth; -import 'package:firebase_data_connect/firebase_data_connect.dart' as fdc; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:krow_data_connect/krow_data_connect.dart' as dc; -import 'package:krow_domain/krow_domain.dart'; +import '../../../domain/usecases/submit_profile_setup_usecase.dart'; import 'profile_setup_event.dart'; import 'profile_setup_state.dart'; @@ -13,10 +10,8 @@ export 'profile_setup_state.dart'; /// BLoC responsible for managing the profile setup state and logic. class ProfileSetupBloc extends Bloc { ProfileSetupBloc({ - required auth.FirebaseAuth firebaseAuth, - required dc.ExampleConnector dataConnect, - }) : _firebaseAuth = firebaseAuth, - _dataConnect = dataConnect, + required SubmitProfileSetup submitProfileSetup, + }) : _submitProfileSetup = submitProfileSetup, super(const ProfileSetupState()) { on(_onFullNameChanged); on(_onBioChanged); @@ -27,8 +22,7 @@ class ProfileSetupBloc extends Bloc { on(_onSubmitted); } - final auth.FirebaseAuth _firebaseAuth; - final dc.ExampleConnector _dataConnect; + final SubmitProfileSetup _submitProfileSetup; /// Handles the [ProfileSetupFullNameChanged] event. void _onFullNameChanged( @@ -86,44 +80,14 @@ class ProfileSetupBloc extends Bloc { emit(state.copyWith(status: ProfileSetupStatus.loading)); try { - final auth.User? firebaseUser = _firebaseAuth.currentUser; - if (firebaseUser == null) { - throw Exception('User not authenticated.'); - } - - final dc.StaffSession? session = dc.StaffSessionStore.instance.session; - final String email = session?.user.email ?? ''; - final String? phone = firebaseUser.phoneNumber; - - final fdc.OperationResult - result = await _dataConnect - .createStaff( - userId: firebaseUser.uid, - fullName: state.fullName, - ) - .bio(state.bio.isEmpty ? null : state.bio) - .preferredLocations(state.preferredLocations) - .maxDistanceMiles(state.maxDistanceMiles.toInt()) - .industries(state.industries) - .skills(state.skills) - .email(email.isEmpty ? null : email) - .phone(phone) - .execute(); - - final String staffId = result.data.staff_insert.id ; - final Staff staff = Staff( - id: staffId, - authProviderId: firebaseUser.uid, - name: state.fullName, - email: email, - phone: phone, - status: StaffStatus.completedProfile, + await _submitProfileSetup( + fullName: state.fullName, + bio: state.bio.isEmpty ? null : state.bio, + preferredLocations: state.preferredLocations, + maxDistanceMiles: state.maxDistanceMiles, + industries: state.industries, + skills: state.skills, ); - if (session != null) { - dc.StaffSessionStore.instance.setSession( - dc.StaffSession(user: session.user, staff: staff), - ); - } emit(state.copyWith(status: ProfileSetupStatus.success)); } catch (e) { diff --git a/apps/mobile/packages/features/staff/authentication/lib/src/presentation/navigation/auth_navigator.dart b/apps/mobile/packages/features/staff/authentication/lib/src/presentation/navigation/auth_navigator.dart index 5c201e2c..2034bc04 100644 --- a/apps/mobile/packages/features/staff/authentication/lib/src/presentation/navigation/auth_navigator.dart +++ b/apps/mobile/packages/features/staff/authentication/lib/src/presentation/navigation/auth_navigator.dart @@ -16,6 +16,6 @@ extension AuthNavigator on IModularNavigator { /// Navigates to the worker home (external to this module). void pushWorkerHome() { - pushNamed('/worker-main/home/'); + pushNamed('/worker-main/home'); } } 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..2a1bc849 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. /// @@ -27,6 +27,7 @@ class PhoneVerificationPage extends StatelessWidget { required BuildContext context, required String phoneNumber, }) { + print('Phone verification input: "$phoneNumber" len=${phoneNumber.length}'); if (phoneNumber.length == 10) { BlocProvider.of( context, diff --git a/apps/mobile/packages/features/staff/authentication/lib/src/presentation/widgets/get_started_page/get_started_actions.dart b/apps/mobile/packages/features/staff/authentication/lib/src/presentation/widgets/get_started_page/get_started_actions.dart index 8d1ac228..7e7ead4b 100644 --- a/apps/mobile/packages/features/staff/authentication/lib/src/presentation/widgets/get_started_page/get_started_actions.dart +++ b/apps/mobile/packages/features/staff/authentication/lib/src/presentation/widgets/get_started_page/get_started_actions.dart @@ -1,17 +1,11 @@ -import 'package:design_system/design_system.dart'; import 'package:flutter/material.dart'; -import 'package:staff_authentication/staff_authentication.dart'; +import 'package:design_system/design_system.dart'; +import 'package:core_localization/core_localization.dart'; -/// A widget that displays the primary action buttons (Sign Up and Log In) -/// for the Get Started page. class GetStartedActions extends StatelessWidget { - /// Void callback for when the Sign Up button is pressed. final VoidCallback onSignUpPressed; - - /// Void callback for when the Log In button is pressed. final VoidCallback onLoginPressed; - /// Creates a [GetStartedActions]. const GetStartedActions({ super.key, required this.onSignUpPressed, @@ -20,22 +14,15 @@ class GetStartedActions extends StatelessWidget { @override Widget build(BuildContext context) { + final TranslationsStaffAuthenticationGetStartedPageEn i18n = + t.staff_authentication.get_started_page; + return Column( crossAxisAlignment: CrossAxisAlignment.stretch, + spacing: UiConstants.space4, children: [ - // Sign Up Button - UiButton.primary( - text: t.staff_authentication.get_started_page.sign_up_button, - onPressed: onSignUpPressed, - ), - - const SizedBox(height: 12), - - // Log In Button - UiButton.secondary( - text: t.staff_authentication.get_started_page.log_in_button, - onPressed: onLoginPressed, - ), + UiButton.primary(onPressed: onSignUpPressed, text: i18n.sign_up_button), + UiButton.secondary(onPressed: onLoginPressed, text: i18n.log_in_button), ], ); } diff --git a/apps/mobile/packages/features/staff/authentication/lib/src/presentation/widgets/get_started_page/get_started_background.dart b/apps/mobile/packages/features/staff/authentication/lib/src/presentation/widgets/get_started_page/get_started_background.dart index 18cc18c6..a3c0bafa 100644 --- a/apps/mobile/packages/features/staff/authentication/lib/src/presentation/widgets/get_started_page/get_started_background.dart +++ b/apps/mobile/packages/features/staff/authentication/lib/src/presentation/widgets/get_started_page/get_started_background.dart @@ -1,49 +1,74 @@ -import 'package:design_system/design_system.dart'; import 'package:flutter/material.dart'; +import 'package:design_system/design_system.dart'; -/// A widget that displays the background for the Get Started page. class GetStartedBackground extends StatelessWidget { - /// Creates a [GetStartedBackground]. const GetStartedBackground({super.key}); @override Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.only(top: 24.0), + return Container( child: Column( - children: [ + children: [ + const SizedBox(height: 32), // Logo - Image.asset(UiImageAssets.logoBlue, height: 40), - + Image.asset( + UiImageAssets.logoBlue, + height: 40, + ), Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - // Hero Image - Container( - width: 288, - height: 288, - margin: const EdgeInsets.only(bottom: 32), - decoration: BoxDecoration( - shape: BoxShape.circle, - color: UiColors.secondaryForeground.withAlpha( - 64, - ), // 0.5 opacity - ), - child: Padding( - padding: const EdgeInsets.all(8.0), - child: ClipOval( - child: Image.network( - 'https://images.unsplash.com/photo-1577219491135-ce391730fb2c?w=400&h=400&fit=crop&crop=faces', - fit: BoxFit.cover, - ), - ), + child: Center( + child: Container( + width: 288, + height: 288, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: const Color(0xFF3A4A5A).withOpacity(0.05), + ), + padding: const EdgeInsets.all(8.0), + child: ClipOval( + child: Image.network( + 'https://images.unsplash.com/photo-1577219491135-ce391730fb2c?w=400&h=400&fit=crop&crop=faces', + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return Image.asset(UiImageAssets.logoBlue); + }, ), ), - const SizedBox(height: 32), - ], + ), ), ), + // Pagination dots (Visual only) + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + width: 24, + height: 8, + decoration: BoxDecoration( + color: UiColors.primary, + borderRadius: BorderRadius.circular(4), + ), + ), + const SizedBox(width: 8), + Container( + width: 8, + height: 8, + decoration: BoxDecoration( + color: UiColors.primary.withOpacity(0.2), + borderRadius: BorderRadius.circular(4), + ), + ), + const SizedBox(width: 8), + Container( + width: 8, + height: 8, + decoration: BoxDecoration( + color: UiColors.primary.withOpacity(0.2), + borderRadius: BorderRadius.circular(4), + ), + ), + ], + ), ], ), ); diff --git a/apps/mobile/packages/features/staff/authentication/lib/src/presentation/widgets/get_started_page/get_started_header.dart b/apps/mobile/packages/features/staff/authentication/lib/src/presentation/widgets/get_started_page/get_started_header.dart index 94528237..e2b37211 100644 --- a/apps/mobile/packages/features/staff/authentication/lib/src/presentation/widgets/get_started_page/get_started_header.dart +++ b/apps/mobile/packages/features/staff/authentication/lib/src/presentation/widgets/get_started_page/get_started_header.dart @@ -9,6 +9,9 @@ class GetStartedHeader extends StatelessWidget { @override Widget build(BuildContext context) { + final TranslationsStaffAuthenticationGetStartedPageEn i18n = + t.staff_authentication.get_started_page; + return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ @@ -18,10 +21,10 @@ class GetStartedHeader extends StatelessWidget { style: UiTypography.displayM, children: [ TextSpan( - text: t.staff_authentication.get_started_page.title_part1, + text: i18n.title_part1, ), TextSpan( - text: t.staff_authentication.get_started_page.title_part2, + text: i18n.title_part2, style: UiTypography.displayMb.textLink, ), ], @@ -29,11 +32,11 @@ class GetStartedHeader extends StatelessWidget { ), const SizedBox(height: 16), Text( - t.staff_authentication.get_started_page.subtitle, + i18n.subtitle, textAlign: TextAlign.center, style: UiTypography.body1r.textSecondary, ), ], ); } -} +} \ No newline at end of file diff --git a/apps/mobile/packages/features/staff/authentication/lib/src/presentation/widgets/phone_verification_page/otp_verification/otp_input_field.dart b/apps/mobile/packages/features/staff/authentication/lib/src/presentation/widgets/phone_verification_page/otp_verification/otp_input_field.dart index 70b11165..2eda5bd1 100644 --- a/apps/mobile/packages/features/staff/authentication/lib/src/presentation/widgets/phone_verification_page/otp_verification/otp_input_field.dart +++ b/apps/mobile/packages/features/staff/authentication/lib/src/presentation/widgets/phone_verification_page/otp_verification/otp_input_field.dart @@ -74,7 +74,7 @@ class _OtpInputFieldState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: List.generate(6, (int index) { return SizedBox( - width: 56, + width: 45, height: 56, child: TextField( controller: _controllers[index], diff --git a/apps/mobile/packages/features/staff/authentication/lib/src/presentation/widgets/phone_verification_page/otp_verification/otp_verification_header.dart b/apps/mobile/packages/features/staff/authentication/lib/src/presentation/widgets/phone_verification_page/otp_verification/otp_verification_header.dart index 50837e68..ec4ff79c 100644 --- a/apps/mobile/packages/features/staff/authentication/lib/src/presentation/widgets/phone_verification_page/otp_verification/otp_verification_header.dart +++ b/apps/mobile/packages/features/staff/authentication/lib/src/presentation/widgets/phone_verification_page/otp_verification/otp_verification_header.dart @@ -28,7 +28,7 @@ class OtpVerificationHeader extends StatelessWidget { .code_sent_message, style: UiTypography.body2r.textSecondary, children: [ - TextSpan(text: '+1 $phoneNumber', style: UiTypography.body2b), + TextSpan(text: phoneNumber, style: UiTypography.body2b), TextSpan( text: t .staff_authentication 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/authentication/lib/src/utils/test_phone_numbers.dart b/apps/mobile/packages/features/staff/authentication/lib/src/utils/test_phone_numbers.dart new file mode 100644 index 00000000..c5e8fb00 --- /dev/null +++ b/apps/mobile/packages/features/staff/authentication/lib/src/utils/test_phone_numbers.dart @@ -0,0 +1,9 @@ +class TestPhoneNumbers { + static const List values = [ + '+15145912311', // Test User 1 + ]; + + static bool isTestNumber(String phoneNumber) { + return values.contains(phoneNumber); + } +} diff --git a/apps/mobile/packages/features/staff/authentication/lib/staff_authentication.dart b/apps/mobile/packages/features/staff/authentication/lib/staff_authentication.dart index 19421456..b98c5356 100644 --- a/apps/mobile/packages/features/staff/authentication/lib/staff_authentication.dart +++ b/apps/mobile/packages/features/staff/authentication/lib/staff_authentication.dart @@ -8,6 +8,9 @@ import 'package:staff_authentication/src/data/repositories_impl/auth_repository_ import 'package:staff_authentication/src/domain/repositories/auth_repository_interface.dart'; import 'package:staff_authentication/src/domain/usecases/sign_in_with_phone_usecase.dart'; import 'package:staff_authentication/src/domain/usecases/verify_otp_usecase.dart'; +import 'package:staff_authentication/src/domain/repositories/profile_setup_repository.dart'; +import 'package:staff_authentication/src/data/repositories_impl/profile_setup_repository_impl.dart'; +import 'package:staff_authentication/src/domain/usecases/submit_profile_setup_usecase.dart'; import 'package:staff_authentication/src/presentation/blocs/auth_bloc.dart'; import 'package:staff_authentication/src/presentation/blocs/profile_setup/profile_setup_bloc.dart'; import 'package:staff_authentication/src/presentation/pages/get_started_page.dart'; @@ -35,10 +38,17 @@ class StaffAuthenticationModule extends Module { dataConnect: ExampleConnector.instance, ), ); + i.addLazySingleton( + () => ProfileSetupRepositoryImpl( + firebaseAuth: firebase.FirebaseAuth.instance, + dataConnect: ExampleConnector.instance, + ), + ); // UseCases i.addLazySingleton(SignInWithPhoneUseCase.new); i.addLazySingleton(VerifyOtpUseCase.new); + i.addLazySingleton(SubmitProfileSetup.new); // BLoCs i.addLazySingleton( @@ -49,8 +59,7 @@ class StaffAuthenticationModule extends Module { ); i.add( () => ProfileSetupBloc( - firebaseAuth: firebase.FirebaseAuth.instance, - dataConnect: ExampleConnector.instance, + submitProfileSetup: i.get(), ), ); } diff --git a/apps/mobile/packages/features/staff/availability/lib/src/data/repositories/availability_repository_impl.dart b/apps/mobile/packages/features/staff/availability/lib/src/data/repositories/availability_repository_impl.dart deleted file mode 100644 index 69d7594c..00000000 --- a/apps/mobile/packages/features/staff/availability/lib/src/data/repositories/availability_repository_impl.dart +++ /dev/null @@ -1,164 +0,0 @@ -import 'package:krow_data_connect/krow_data_connect.dart' hide AvailabilitySlot; -import 'package:krow_domain/krow_domain.dart'; -import '../../domain/repositories/availability_repository.dart'; -import 'package:intl/intl.dart'; - -/// Implementation of [AvailabilityRepository]. -/// -/// Uses [StaffRepositoryMock] from data_connect to fetch and store data. -class AvailabilityRepositoryImpl implements AvailabilityRepository { - final StaffRepositoryMock _dataSource; - - // Mock User ID - in real app invoke AuthUseCase to get current user - final String _userId = 'mock_user_123'; - - static const List> _slotDefinitions = [ - { - 'id': 'morning', - 'label': 'Morning', - 'timeRange': '4:00 AM - 12:00 PM', - }, - { - 'id': 'afternoon', - 'label': 'Afternoon', - 'timeRange': '12:00 PM - 6:00 PM', - }, - { - 'id': 'evening', - 'label': 'Evening', - 'timeRange': '6:00 PM - 12:00 AM', - }, - ]; - - AvailabilityRepositoryImpl({StaffRepositoryMock? dataSource}) - : _dataSource = dataSource ?? StaffRepositoryMock(); - - @override - Future> getAvailability( - DateTime start, DateTime end) async { - final rawData = await _dataSource.getAvailability(_userId, start, end); - final List days = []; - - // Loop through each day in range - for (int i = 0; i <= end.difference(start).inDays; i++) { - final date = start.add(Duration(days: i)); - final dateKey = DateFormat('yyyy-MM-dd').format(date); - - final dayData = rawData[dateKey]; - - if (dayData != null) { - days.add(_mapFromData(date, dayData)); - } else { - // Default: Available M-F, Not Sat-Sun (matching prototype logic) - final isWeekend = date.weekday == DateTime.saturday || date.weekday == DateTime.sunday; - // Prototype: Sat/Sun false - - days.add(DayAvailability( - date: date, - isAvailable: !isWeekend, - slots: _generateDefaultSlots(isEnabled: !isWeekend), - )); - } - } - return days; - } - - @override - Future updateDayAvailability( - DayAvailability availability) async { - final dateKey = DateFormat('yyyy-MM-dd').format(availability.date); - final data = _mapToData(availability); - - await _dataSource.updateAvailability(_userId, dateKey, data); - return availability; - } - - @override - Future> applyQuickSet( - DateTime start, DateTime end, String type) async { - final List updatedDays = []; - - for (int i = 0; i <= end.difference(start).inDays; i++) { - final date = start.add(Duration(days: i)); - bool isAvailable = false; - - switch (type) { - case 'all': - isAvailable = true; - break; - case 'weekdays': - isAvailable = date.weekday != DateTime.saturday && date.weekday != DateTime.sunday; - break; - case 'weekends': - isAvailable = date.weekday == DateTime.saturday || date.weekday == DateTime.sunday; - break; - case 'clear': - isAvailable = false; - break; - } - - // Keep existing slot preferences, just toggle main switch? - // Or reset slots too? Prototype behavior: just sets map[day] = bool. - // But it implies slots are active if day is active? - // For now, allow slots to be default true if day is enabled. - - final day = DayAvailability( - date: date, - isAvailable: isAvailable, - slots: _generateDefaultSlots(isEnabled: isAvailable), - ); - - await updateDayAvailability(day); - updatedDays.add(day); - } - return updatedDays; - } - - // --- Helpers --- - - List _generateDefaultSlots({bool isEnabled = true}) { - return _slotDefinitions.map((def) { - return AvailabilitySlot( - id: def['id']!, - label: def['label']!, - timeRange: def['timeRange']!, - isAvailable: true, // Default slots to true - ); - }).toList(); - } - - DayAvailability _mapFromData(DateTime date, Map data) { - final isAvailable = data['isAvailable'] as bool? ?? false; - final Map slotsMap = data['slots'] ?? {}; - - final slots = _slotDefinitions.map((def) { - final slotId = def['id']!; - final slotEnabled = slotsMap[slotId] as bool? ?? true; // Default true if not stored - - return AvailabilitySlot( - id: slotId, - label: def['label']!, - timeRange: def['timeRange']!, - isAvailable: slotEnabled, - ); - }).toList(); - - return DayAvailability( - date: date, - isAvailable: isAvailable, - slots: slots, - ); - } - - Map _mapToData(DayAvailability day) { - Map slotsMap = {}; - for (var slot in day.slots) { - slotsMap[slot.id] = slot.isAvailable; - } - - return { - 'isAvailable': day.isAvailable, - 'slots': slotsMap, - }; - } -} diff --git a/apps/mobile/packages/features/staff/availability/lib/src/data/repositories_impl/availability_repository_impl.dart b/apps/mobile/packages/features/staff/availability/lib/src/data/repositories_impl/availability_repository_impl.dart new file mode 100644 index 00000000..0de2fce2 --- /dev/null +++ b/apps/mobile/packages/features/staff/availability/lib/src/data/repositories_impl/availability_repository_impl.dart @@ -0,0 +1,243 @@ +import 'package:firebase_auth/firebase_auth.dart' as firebase; +import 'package:firebase_data_connect/firebase_data_connect.dart'; +import 'package:krow_data_connect/krow_data_connect.dart' as dc; +import 'package:krow_domain/krow_domain.dart'; +import '../../domain/repositories/availability_repository.dart'; + +/// Implementation of [AvailabilityRepository] using Firebase Data Connect. +/// +/// Note: The backend schema supports recurring availablity (Weekly/DayOfWeek), +/// not specific date availability. Therefore, updating availability for a specific +/// date will update the availability for that Day of Week globally (Recurring). +class AvailabilityRepositoryImpl implements AvailabilityRepository { + final dc.ExampleConnector _dataConnect; + final firebase.FirebaseAuth _firebaseAuth; + + AvailabilityRepositoryImpl({ + required dc.ExampleConnector dataConnect, + required firebase.FirebaseAuth firebaseAuth, + }) : _dataConnect = dataConnect, + _firebaseAuth = firebaseAuth; + + Future _getStaffId() async { + final firebase.User? user = _firebaseAuth.currentUser; + if (user == null) throw Exception('User not authenticated'); + + final QueryResult result = + await _dataConnect.getStaffByUserId(userId: user.uid).execute(); + if (result.data.staffs.isEmpty) { + throw Exception('Staff profile not found'); + } + return result.data.staffs.first.id; + } + + @override + Future> getAvailability(DateTime start, DateTime end) async { + final String staffId = await _getStaffId(); + + // 1. Fetch Weekly recurring availability + final QueryResult result = + await _dataConnect.listStaffAvailabilitiesByStaffId(staffId: staffId).limit(100).execute(); + + final List items = result.data.staffAvailabilities; + + // 2. Map to lookup: DayOfWeek -> Map + final Map> weeklyMap = {}; + + for (final item in items) { + dc.DayOfWeek day; + try { + day = dc.DayOfWeek.values.byName(item.day.stringValue); + } catch (_) { + continue; + } + + dc.AvailabilitySlot slot; + try { + slot = dc.AvailabilitySlot.values.byName(item.slot.stringValue); + } catch (_) { + continue; + } + + bool isAvailable = false; + try { + final dc.AvailabilityStatus status = dc.AvailabilityStatus.values.byName(item.status.stringValue); + isAvailable = _statusToBool(status); + } catch (_) { + isAvailable = false; + } + + if (!weeklyMap.containsKey(day)) { + weeklyMap[day] = {}; + } + weeklyMap[day]![slot] = isAvailable; + } + + // 3. Generate DayAvailability for requested range + final List days = []; + final int dayCount = end.difference(start).inDays; + + for (int i = 0; i <= dayCount; i++) { + final DateTime date = start.add(Duration(days: i)); + final dc.DayOfWeek dow = _toBackendDay(date.weekday); + + final Map daySlots = weeklyMap[dow] ?? {}; + + // We define 3 standard slots for every day + final List slots = [ + _createSlot(date, dow, daySlots, dc.AvailabilitySlot.MORNING), + _createSlot(date, dow, daySlots, dc.AvailabilitySlot.AFTERNOON), + _createSlot(date, dow, daySlots, dc.AvailabilitySlot.EVENING), + ]; + + final bool isDayAvailable = slots.any((s) => s.isAvailable); + + days.add(DayAvailability( + date: date, + isAvailable: isDayAvailable, + slots: slots, + )); + } + return days; + } + + AvailabilitySlot _createSlot( + DateTime date, + dc.DayOfWeek dow, + Map existingSlots, + dc.AvailabilitySlot slotEnum, + ) { + final bool isAvailable = existingSlots[slotEnum] ?? false; + return AvailabilityAdapter.fromPrimitive(slotEnum.name, isAvailable: isAvailable); + } + + @override + Future updateDayAvailability(DayAvailability availability) async { + final String staffId = await _getStaffId(); + final dc.DayOfWeek dow = _toBackendDay(availability.date.weekday); + + // Update each slot in the backend. + // This updates the recurring rule for this DayOfWeek. + for (final AvailabilitySlot slot in availability.slots) { + final dc.AvailabilitySlot slotEnum = _toBackendSlot(slot.id); + final dc.AvailabilityStatus status = _boolToStatus(slot.isAvailable); + + await _upsertSlot(staffId, dow, slotEnum, status); + } + + return availability; + } + + @override + Future> applyQuickSet(DateTime start, DateTime end, String type) async { + final String staffId = await _getStaffId(); + + // QuickSet updates the Recurring schedule for all days involved. + // However, if the user selects a range that covers e.g. Mon-Fri, we update Mon-Fri. + + final int dayCount = end.difference(start).inDays; + final Set processedDays = {}; + final List resultDays = []; + + for (int i = 0; i <= dayCount; i++) { + final DateTime date = start.add(Duration(days: i)); + final dc.DayOfWeek dow = _toBackendDay(date.weekday); + + // Logic to determine if enabled based on type + bool enableDay = false; + if (type == 'all') enableDay = true; + else if (type == 'clear') enableDay = false; + else if (type == 'weekdays') { + enableDay = (dow != dc.DayOfWeek.SATURDAY && dow != dc.DayOfWeek.SUNDAY); + } else if (type == 'weekends') { + enableDay = (dow == dc.DayOfWeek.SATURDAY || dow == dc.DayOfWeek.SUNDAY); + } + + // Only update backend once per DayOfWeek (since it's recurring) + // to avoid redundant calls if range > 1 week. + if (!processedDays.contains(dow)) { + processedDays.add(dow); + + final dc.AvailabilityStatus status = _boolToStatus(enableDay); + + await Future.wait([ + _upsertSlot(staffId, dow, dc.AvailabilitySlot.MORNING, status), + _upsertSlot(staffId, dow, dc.AvailabilitySlot.AFTERNOON, status), + _upsertSlot(staffId, dow, dc.AvailabilitySlot.EVENING, status), + ]); + } + + // Prepare return object + final slots = [ + AvailabilityAdapter.fromPrimitive('MORNING', isAvailable: enableDay), + AvailabilityAdapter.fromPrimitive('AFTERNOON', isAvailable: enableDay), + AvailabilityAdapter.fromPrimitive('EVENING', isAvailable: enableDay), + ]; + + resultDays.add(DayAvailability( + date: date, + isAvailable: enableDay, + slots: slots, + )); + } + + return resultDays; + } + + Future _upsertSlot(String staffId, dc.DayOfWeek day, dc.AvailabilitySlot slot, dc.AvailabilityStatus status) async { + // Check if exists + final result = await _dataConnect.getStaffAvailabilityByKey( + staffId: staffId, + day: day, + slot: slot, + ).execute(); + + if (result.data.staffAvailability != null) { + // Update + await _dataConnect.updateStaffAvailability( + staffId: staffId, + day: day, + slot: slot, + ).status(status).execute(); + } else { + // Create + await _dataConnect.createStaffAvailability( + staffId: staffId, + day: day, + slot: slot, + ).status(status).execute(); + } + } + + // --- Private Helpers --- + + dc.DayOfWeek _toBackendDay(int weekday) { + switch (weekday) { + case DateTime.monday: return dc.DayOfWeek.MONDAY; + case DateTime.tuesday: return dc.DayOfWeek.TUESDAY; + case DateTime.wednesday: return dc.DayOfWeek.WEDNESDAY; + case DateTime.thursday: return dc.DayOfWeek.THURSDAY; + case DateTime.friday: return dc.DayOfWeek.FRIDAY; + case DateTime.saturday: return dc.DayOfWeek.SATURDAY; + case DateTime.sunday: return dc.DayOfWeek.SUNDAY; + default: return dc.DayOfWeek.MONDAY; + } + } + + dc.AvailabilitySlot _toBackendSlot(String id) { + switch (id.toLowerCase()) { + case 'morning': return dc.AvailabilitySlot.MORNING; + case 'afternoon': return dc.AvailabilitySlot.AFTERNOON; + case 'evening': return dc.AvailabilitySlot.EVENING; + default: return dc.AvailabilitySlot.MORNING; + } + } + + bool _statusToBool(dc.AvailabilityStatus status) { + return status == dc.AvailabilityStatus.CONFIRMED_AVAILABLE; + } + + dc.AvailabilityStatus _boolToStatus(bool isAvailable) { + return isAvailable ? dc.AvailabilityStatus.CONFIRMED_AVAILABLE : dc.AvailabilityStatus.BLOCKED; + } +} diff --git a/apps/mobile/packages/features/staff/availability/lib/src/presentation/blocs/availability_bloc.dart b/apps/mobile/packages/features/staff/availability/lib/src/presentation/blocs/availability_bloc.dart index 4073db48..2e1f32a3 100644 --- a/apps/mobile/packages/features/staff/availability/lib/src/presentation/blocs/availability_bloc.dart +++ b/apps/mobile/packages/features/staff/availability/lib/src/presentation/blocs/availability_bloc.dart @@ -1,5 +1,4 @@ import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:krow_domain/krow_domain.dart'; import '../../domain/usecases/apply_quick_set_usecase.dart'; import '../../domain/usecases/get_weekly_availability_usecase.dart'; import '../../domain/usecases/update_day_availability_usecase.dart'; @@ -45,7 +44,11 @@ class AvailabilityBloc extends Bloc { void _onSelectDate(SelectDate event, Emitter emit) { if (state is AvailabilityLoaded) { - emit((state as AvailabilityLoaded).copyWith(selectedDate: event.date)); + // Clear success message on navigation + emit((state as AvailabilityLoaded).copyWith( + selectedDate: event.date, + clearSuccessMessage: true, + )); } } @@ -55,6 +58,10 @@ class AvailabilityBloc extends Bloc { ) async { if (state is AvailabilityLoaded) { final currentState = state as AvailabilityLoaded; + + // Clear message + emit(currentState.copyWith(clearSuccessMessage: true)); + final newWeekStart = currentState.currentWeekStart .add(Duration(days: event.direction * 7)); @@ -77,12 +84,23 @@ class AvailabilityBloc extends Bloc { return d.date == event.day.date ? newDay : d; }).toList(); - emit(currentState.copyWith(days: updatedDays)); + // Optimistic update + emit(currentState.copyWith( + days: updatedDays, + clearSuccessMessage: true, + )); try { await updateDayAvailability(UpdateDayAvailabilityParams(newDay)); + // Success feedback + if (state is AvailabilityLoaded) { + emit((state as AvailabilityLoaded).copyWith(successMessage: 'Availability updated')); + } } catch (e) { - emit(currentState.copyWith(days: currentState.days)); + // Revert + if (state is AvailabilityLoaded) { + emit((state as AvailabilityLoaded).copyWith(days: currentState.days)); + } } } } @@ -107,12 +125,23 @@ class AvailabilityBloc extends Bloc { return d.date == event.day.date ? newDay : d; }).toList(); - emit(currentState.copyWith(days: updatedDays)); + // Optimistic update + emit(currentState.copyWith( + days: updatedDays, + clearSuccessMessage: true, + )); try { await updateDayAvailability(UpdateDayAvailabilityParams(newDay)); + // Success feedback + if (state is AvailabilityLoaded) { + emit((state as AvailabilityLoaded).copyWith(successMessage: 'Availability updated')); + } } catch (e) { - emit(currentState.copyWith(days: currentState.days)); + // Revert + if (state is AvailabilityLoaded) { + emit((state as AvailabilityLoaded).copyWith(days: currentState.days)); + } } } } @@ -124,12 +153,26 @@ class AvailabilityBloc extends Bloc { if (state is AvailabilityLoaded) { final currentState = state as AvailabilityLoaded; + emit(currentState.copyWith( + isActionInProgress: true, + clearSuccessMessage: true, + )); + try { final newDays = await applyQuickSet( ApplyQuickSetParams(currentState.currentWeekStart, event.type)); - emit(currentState.copyWith(days: newDays)); + + emit(currentState.copyWith( + days: newDays, + isActionInProgress: false, + successMessage: 'Availability updated', + )); } catch (e) { - // Handle error + emit(currentState.copyWith( + isActionInProgress: false, + // Could set error message here if we had a field for it, or emit AvailabilityError + // But emitting AvailabilityError would replace the whole screen. + )); } } } diff --git a/apps/mobile/packages/features/staff/availability/lib/src/presentation/blocs/availability_cubit.dart b/apps/mobile/packages/features/staff/availability/lib/src/presentation/blocs/availability_cubit.dart new file mode 100644 index 00000000..2175a7e1 --- /dev/null +++ b/apps/mobile/packages/features/staff/availability/lib/src/presentation/blocs/availability_cubit.dart @@ -0,0 +1,130 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:equatable/equatable.dart'; + +// --- State --- +class AvailabilityState extends Equatable { + final DateTime currentWeekStart; + final DateTime selectedDate; + final Map dayAvailability; + final Map> timeSlotAvailability; + + const AvailabilityState({ + required this.currentWeekStart, + required this.selectedDate, + required this.dayAvailability, + required this.timeSlotAvailability, + }); + + AvailabilityState copyWith({ + DateTime? currentWeekStart, + DateTime? selectedDate, + Map? dayAvailability, + Map>? timeSlotAvailability, + }) { + return AvailabilityState( + currentWeekStart: currentWeekStart ?? this.currentWeekStart, + selectedDate: selectedDate ?? this.selectedDate, + dayAvailability: dayAvailability ?? this.dayAvailability, + timeSlotAvailability: timeSlotAvailability ?? this.timeSlotAvailability, + ); + } + + @override + List get props => [ + currentWeekStart, + selectedDate, + dayAvailability, + timeSlotAvailability, + ]; +} + +// --- Cubit --- +class AvailabilityCubit extends Cubit { + AvailabilityCubit() + : super(AvailabilityState( + currentWeekStart: _getStartOfWeek(DateTime.now()), + selectedDate: DateTime.now(), + dayAvailability: { + 'monday': true, + 'tuesday': true, + 'wednesday': true, + 'thursday': true, + 'friday': true, + 'saturday': false, + 'sunday': false, + }, + timeSlotAvailability: { + 'monday': {'morning': true, 'afternoon': true, 'evening': true}, + 'tuesday': {'morning': true, 'afternoon': true, 'evening': true}, + 'wednesday': {'morning': true, 'afternoon': true, 'evening': true}, + 'thursday': {'morning': true, 'afternoon': true, 'evening': true}, + 'friday': {'morning': true, 'afternoon': true, 'evening': true}, + 'saturday': {'morning': false, 'afternoon': false, 'evening': false}, + 'sunday': {'morning': false, 'afternoon': false, 'evening': false}, + }, + )); + + static DateTime _getStartOfWeek(DateTime date) { + final diff = date.weekday - 1; // Mon=1 -> 0 + final start = date.subtract(Duration(days: diff)); + return DateTime(start.year, start.month, start.day); + } + + void selectDate(DateTime date) { + emit(state.copyWith(selectedDate: date)); + } + + void navigateWeek(int weeks) { + emit(state.copyWith( + currentWeekStart: state.currentWeekStart.add(Duration(days: weeks * 7)), + )); + } + + void toggleDay(String dayKey) { + final currentObj = Map.from(state.dayAvailability); + currentObj[dayKey] = !(currentObj[dayKey] ?? false); + emit(state.copyWith(dayAvailability: currentObj)); + } + + void toggleSlot(String dayKey, String slotId) { + final allSlots = Map>.from(state.timeSlotAvailability); + final daySlots = Map.from(allSlots[dayKey] ?? {}); + + // Default to true if missing, so we toggle to false + final currentVal = daySlots[slotId] ?? true; + daySlots[slotId] = !currentVal; + + allSlots[dayKey] = daySlots; + emit(state.copyWith(timeSlotAvailability: allSlots)); + } + + void quickSet(String type) { + final newAvailability = {}; + final days = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday']; + + switch (type) { + case 'all': + for (var d in days) { + newAvailability[d] = true; + } + break; + case 'weekdays': + for (var d in days) { + newAvailability[d] = (d != 'saturday' && d != 'sunday'); + } + break; + case 'weekends': + for (var d in days) { + newAvailability[d] = (d == 'saturday' || d == 'sunday'); + } + break; + case 'clear': + for (var d in days) { + newAvailability[d] = false; + } + break; + } + + emit(state.copyWith(dayAvailability: newAvailability)); + } +} diff --git a/apps/mobile/packages/features/staff/availability/lib/src/presentation/blocs/availability_state.dart b/apps/mobile/packages/features/staff/availability/lib/src/presentation/blocs/availability_state.dart index 5c8b52ba..e48fed83 100644 --- a/apps/mobile/packages/features/staff/availability/lib/src/presentation/blocs/availability_state.dart +++ b/apps/mobile/packages/features/staff/availability/lib/src/presentation/blocs/availability_state.dart @@ -15,11 +15,15 @@ class AvailabilityLoaded extends AvailabilityState { final List days; final DateTime currentWeekStart; final DateTime selectedDate; + final bool isActionInProgress; + final String? successMessage; const AvailabilityLoaded({ required this.days, required this.currentWeekStart, required this.selectedDate, + this.isActionInProgress = false, + this.successMessage, }); /// Helper to get the currently selected day's availability object @@ -34,11 +38,16 @@ class AvailabilityLoaded extends AvailabilityState { List? days, DateTime? currentWeekStart, DateTime? selectedDate, + bool? isActionInProgress, + String? successMessage, // Nullable override + bool clearSuccessMessage = false, }) { return AvailabilityLoaded( days: days ?? this.days, currentWeekStart: currentWeekStart ?? this.currentWeekStart, selectedDate: selectedDate ?? this.selectedDate, + isActionInProgress: isActionInProgress ?? this.isActionInProgress, + successMessage: clearSuccessMessage ? null : (successMessage ?? this.successMessage), ); } @@ -47,7 +56,7 @@ class AvailabilityLoaded extends AvailabilityState { } @override - List get props => [days, currentWeekStart, selectedDate]; + List get props => [days, currentWeekStart, selectedDate, isActionInProgress, successMessage]; } class AvailabilityError extends AvailabilityState { diff --git a/apps/mobile/packages/features/staff/availability/lib/src/presentation/pages/availability_page.dart b/apps/mobile/packages/features/staff/availability/lib/src/presentation/pages/availability_page.dart index 97c43cd4..91fc33ee 100644 --- a/apps/mobile/packages/features/staff/availability/lib/src/presentation/pages/availability_page.dart +++ b/apps/mobile/packages/features/staff/availability/lib/src/presentation/pages/availability_page.dart @@ -1,8 +1,15 @@ +import 'package:design_system/design_system.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_modular/flutter_modular.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_modular/flutter_modular.dart' hide ModularWatchExtension; import 'package:intl/intl.dart'; import 'package:lucide_icons/lucide_icons.dart'; +import '../blocs/availability_bloc.dart'; +import '../blocs/availability_event.dart'; +import '../blocs/availability_state.dart'; +import 'package:krow_domain/krow_domain.dart'; + class AvailabilityPage extends StatefulWidget { const AvailabilityPage({super.key}); @@ -11,314 +18,104 @@ class AvailabilityPage extends StatefulWidget { } class _AvailabilityPageState extends State { - late DateTime _currentWeekStart; - late DateTime _selectedDate; - - // Mock Availability State - // Map of day name (lowercase) to availability status - Map _availability = { - 'monday': true, - 'tuesday': true, - 'wednesday': true, - 'thursday': true, - 'friday': true, - 'saturday': false, - 'sunday': false, - }; - - // Map of day name to time slot map - Map> _timeSlotAvailability = { - 'monday': {'morning': true, 'afternoon': true, 'evening': true}, - 'tuesday': {'morning': true, 'afternoon': true, 'evening': true}, - 'wednesday': {'morning': true, 'afternoon': true, 'evening': true}, - 'thursday': {'morning': true, 'afternoon': true, 'evening': true}, - 'friday': {'morning': true, 'afternoon': true, 'evening': true}, - 'saturday': {'morning': false, 'afternoon': false, 'evening': false}, - 'sunday': {'morning': false, 'afternoon': false, 'evening': false}, - }; - - final List _dayNames = [ - 'sunday', - 'monday', - 'tuesday', - 'wednesday', - 'thursday', - 'friday', - 'saturday', - ]; - - final List> _timeSlots = [ - { - 'slotId': 'morning', - 'label': 'Morning', - 'timeRange': '4:00 AM - 12:00 PM', - 'icon': LucideIcons.sunrise, - 'bg': const Color(0xFFE6EBF9), // bg-[#0032A0]/10 - 'iconColor': const Color(0xFF0032A0), - }, - { - 'slotId': 'afternoon', - 'label': 'Afternoon', - 'timeRange': '12:00 PM - 6:00 PM', - 'icon': LucideIcons.sun, - 'bg': const Color(0xFFCCD6EC), // bg-[#0032A0]/20 - 'iconColor': const Color(0xFF0032A0), - }, - { - 'slotId': 'evening', - 'label': 'Evening', - 'timeRange': '6:00 PM - 12:00 AM', - 'icon': LucideIcons.moon, - 'bg': const Color(0xFFEBEDEE), // bg-[#333F48]/10 - 'iconColor': const Color(0xFF333F48), - }, - ]; + final AvailabilityBloc _bloc = Modular.get(); @override void initState() { super.initState(); + _calculateInitialWeek(); + } + + void _calculateInitialWeek() { final today = DateTime.now(); - - // Dart equivalent for Monday start: final day = today.weekday; // Mon=1, Sun=7 - final diff = day - 1; - _currentWeekStart = today.subtract(Duration(days: diff)); - // Reset time to midnight - _currentWeekStart = DateTime( - _currentWeekStart.year, - _currentWeekStart.month, - _currentWeekStart.day, + final diff = day - 1; // Assuming Monday start + DateTime currentWeekStart = today.subtract(Duration(days: diff)); + currentWeekStart = DateTime( + currentWeekStart.year, + currentWeekStart.month, + currentWeekStart.day, ); - - _selectedDate = today; - } - - List _getWeekDates() { - return List.generate( - 7, - (index) => _currentWeekStart.add(Duration(days: index)), - ); - } - - String _formatDay(DateTime date) { - return DateFormat('EEE').format(date); - } - - bool _isToday(DateTime date) { - final now = DateTime.now(); - return date.year == now.year && - date.month == now.month && - date.day == now.day; - } - - bool _isSelected(DateTime date) { - return date.year == _selectedDate.year && - date.month == _selectedDate.month && - date.day == _selectedDate.day; - } - - void _navigateWeek(int direction) { - setState(() { - _currentWeekStart = _currentWeekStart.add(Duration(days: direction * 7)); - }); - } - - void _toggleDayAvailability(String dayName) { - setState(() { - _availability[dayName] = !(_availability[dayName] ?? false); - // React code also updates mutation. We mock this. - // NOTE: In prototype we mock it. Refactor will move this to BLoC. - }); - } - - String _getDayKey(DateTime date) { - // DateTime.weekday: Mon=1...Sun=7. - // _dayNames array: 0=Sun, 1=Mon... - // Dart weekday: 7 is Sunday. 7 % 7 = 0. - return _dayNames[date.weekday % 7]; - } - - void _toggleTimeSlot(String slotId) { - final dayKey = _getDayKey(_selectedDate); - final currentDaySlots = - _timeSlotAvailability[dayKey] ?? - {'morning': true, 'afternoon': true, 'evening': true}; - final newValue = !(currentDaySlots[slotId] ?? true); - - setState(() { - _timeSlotAvailability[dayKey] = {...currentDaySlots, slotId: newValue}; - }); - } - - bool _isTimeSlotActive(String slotId) { - final dayKey = _getDayKey(_selectedDate); - final daySlots = _timeSlotAvailability[dayKey]; - if (daySlots == null) return true; - return daySlots[slotId] != false; - } - - String _getMonthYear() { - final middleDate = _currentWeekStart.add(const Duration(days: 3)); - return DateFormat('MMMM yyyy').format(middleDate); - } - - void _quickSet(String type) { - Map newAvailability = {}; - - switch (type) { - case 'all': - for (var day in _dayNames) newAvailability[day] = true; - break; - case 'weekdays': - for (var day in _dayNames) - newAvailability[day] = (day != 'saturday' && day != 'sunday'); - break; - case 'weekends': - for (var day in _dayNames) - newAvailability[day] = (day == 'saturday' || day == 'sunday'); - break; - case 'clear': - for (var day in _dayNames) newAvailability[day] = false; - break; - } - - setState(() { - _availability = newAvailability; - }); + _bloc.add(LoadAvailability(currentWeekStart)); } @override Widget build(BuildContext context) { - final selectedDayKey = _getDayKey(_selectedDate); - final isSelectedDayAvailable = _availability[selectedDayKey] ?? false; - final weekDates = _getWeekDates(); - - return Scaffold( - backgroundColor: const Color( - 0xFFFAFBFC, - ), // slate-50 to white gradient approximation - body: SingleChildScrollView( - padding: const EdgeInsets.only(bottom: 100), - child: Column( - children: [ - _buildHeader(), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - _buildQuickSet(), - const SizedBox(height: 24), - _buildWeekNavigation(weekDates), - const SizedBox(height: 24), - _buildSelectedDayAvailability( - selectedDayKey, - isSelectedDayAvailable, - ), - const SizedBox(height: 24), - _buildInfoCard(), - ], - ), - ), - ], + return BlocProvider.value( + value: _bloc, + child: Scaffold( + backgroundColor: AppColors.krowBackground, + appBar: UiAppBar( + title: 'My Availability', + centerTitle: false, + showBackButton: true, + ), + body: BlocListener( + listener: (context, state) { + if (state is AvailabilityLoaded && state.successMessage != null) { + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(state.successMessage!), + backgroundColor: Colors.green, + behavior: SnackBarBehavior.floating, + ), + ); + } + }, + child: BlocBuilder( + builder: (context, state) { + if (state is AvailabilityLoading) { + return const Center(child: CircularProgressIndicator()); + } else if (state is AvailabilityLoaded) { + return Stack( + children: [ + SingleChildScrollView( + padding: const EdgeInsets.only(bottom: 100), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _buildQuickSet(context), + const SizedBox(height: 24), + _buildWeekNavigation(context, state), + const SizedBox(height: 24), + _buildSelectedDayAvailability( + context, + state.selectedDayAvailability, + ), + const SizedBox(height: 24), + _buildInfoCard(), + ], + ), + ), + ], + ), + ), + if (state.isActionInProgress) + Container( + color: Colors.black.withOpacity(0.3), + child: const Center( + child: CircularProgressIndicator(), + ), + ), + ], + ); + } else if (state is AvailabilityError) { + return Center(child: Text('Error: ${state.message}')); + } + return const SizedBox.shrink(); + }, + ), ), ), ); } - Widget _buildHeader() { - return Container( - padding: const EdgeInsets.fromLTRB(20, 60, 20, 20), - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - IconButton( - icon: const Icon( - LucideIcons.arrowLeft, - color: AppColors.krowCharcoal, - ), - onPressed: () => Modular.to.pop(), - ), - const SizedBox(width: 12), - Row( - children: [ - Container( - width: 40, - height: 40, - decoration: BoxDecoration( - border: Border.all( - color: AppColors.krowBlue.withOpacity(0.2), - width: 2, - ), - shape: BoxShape.circle, - ), - child: Center( - child: CircleAvatar( - backgroundColor: AppColors.krowBlue.withOpacity( - 0.1, - ), - radius: 18, - child: const Text( - 'K', // Mock initial - style: TextStyle( - color: AppColors.krowBlue, - fontWeight: FontWeight.bold, - fontSize: 14, - ), - ), - ), - ), - ), - const SizedBox(width: 12), - const Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'My Availability', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - color: AppColors.krowCharcoal, - ), - ), - Text( - 'Set when you can work', - style: TextStyle( - fontSize: 14, - color: AppColors.krowMuted, - ), - ), - ], - ), - ], - ), - ], - ), - Container( - width: 40, - height: 40, - decoration: BoxDecoration( - color: AppColors.krowBlue.withOpacity(0.1), - shape: BoxShape.circle, - ), - child: const Icon( - LucideIcons.calendar, - color: AppColors.krowBlue, - size: 20, - ), - ), - ], - ), - ], - ), - ); - } - - Widget _buildQuickSet() { + Widget _buildQuickSet(BuildContext context) { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( @@ -340,27 +137,34 @@ class _AvailabilityPageState extends State { Row( children: [ Expanded( - child: _buildQuickSetButton('All Week', () => _quickSet('all')), + child: _buildQuickSetButton( + context, + 'All Week', + 'all', + ), ), const SizedBox(width: 8), Expanded( child: _buildQuickSetButton( + context, 'Weekdays', - () => _quickSet('weekdays'), + 'weekdays', ), ), const SizedBox(width: 8), Expanded( child: _buildQuickSetButton( + context, 'Weekends', - () => _quickSet('weekends'), + 'weekends', ), ), const SizedBox(width: 8), Expanded( child: _buildQuickSetButton( + context, 'Clear All', - () => _quickSet('clear'), + 'clear', isDestructive: true, ), ), @@ -372,14 +176,15 @@ class _AvailabilityPageState extends State { } Widget _buildQuickSetButton( + BuildContext context, String label, - VoidCallback onTap, { + String type, { bool isDestructive = false, }) { return SizedBox( height: 32, child: OutlinedButton( - onPressed: onTap, + onPressed: () => context.read().add(PerformQuickSet(type)), style: OutlinedButton.styleFrom( padding: EdgeInsets.zero, side: BorderSide( @@ -387,8 +192,7 @@ class _AvailabilityPageState extends State { ? Colors.red.withOpacity(0.2) : AppColors.krowBlue.withOpacity(0.2), ), - backgroundColor: - Colors.transparent, + backgroundColor: Colors.transparent, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), @@ -404,7 +208,11 @@ class _AvailabilityPageState extends State { ); } - Widget _buildWeekNavigation(List weekDates) { + Widget _buildWeekNavigation(BuildContext context, AvailabilityLoaded state) { + // Middle date for month display + final middleDate = state.currentWeekStart.add(const Duration(days: 3)); + final monthYear = DateFormat('MMMM yyyy').format(middleDate); + return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( @@ -429,10 +237,10 @@ class _AvailabilityPageState extends State { children: [ _buildNavButton( LucideIcons.chevronLeft, - () => _navigateWeek(-1), + () => context.read().add(const NavigateWeek(-1)), ), Text( - _getMonthYear(), + monthYear, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w600, @@ -441,7 +249,7 @@ class _AvailabilityPageState extends State { ), _buildNavButton( LucideIcons.chevronRight, - () => _navigateWeek(1), + () => context.read().add(const NavigateWeek(1)), ), ], ), @@ -449,7 +257,7 @@ class _AvailabilityPageState extends State { // Days Row Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: weekDates.map((date) => _buildDayItem(date)).toList(), + children: state.days.map((day) => _buildDayItem(context, day, state.selectedDate)).toList(), ), ], ), @@ -471,15 +279,14 @@ class _AvailabilityPageState extends State { ); } - Widget _buildDayItem(DateTime date) { - final isSelected = _isSelected(date); - final dayKey = _getDayKey(date); - final isAvailable = _availability[dayKey] ?? false; - final isToday = _isToday(date); + Widget _buildDayItem(BuildContext context, DayAvailability day, DateTime selectedDate) { + final isSelected = AvailabilityLoaded.isSameDay(day.date, selectedDate); + final isAvailable = day.isAvailable; + final isToday = AvailabilityLoaded.isSameDay(day.date, DateTime.now()); return Expanded( child: GestureDetector( - onTap: () => setState(() => _selectedDate = date), + onTap: () => context.read().add(SelectDate(day.date)), child: Container( margin: const EdgeInsets.symmetric(horizontal: 2), padding: const EdgeInsets.symmetric(vertical: 12), @@ -514,7 +321,7 @@ class _AvailabilityPageState extends State { Column( children: [ Text( - date.day.toString().padLeft(2, '0'), + day.date.day.toString().padLeft(2, '0'), style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, @@ -527,7 +334,7 @@ class _AvailabilityPageState extends State { ), const SizedBox(height: 2), Text( - _formatDay(date), + DateFormat('EEE').format(day.date), style: TextStyle( fontSize: 10, color: isSelected @@ -559,10 +366,11 @@ class _AvailabilityPageState extends State { } Widget _buildSelectedDayAvailability( - String selectedDayKey, - bool isAvailable, + BuildContext context, + DayAvailability day, ) { - final dateStr = DateFormat('EEEE, MMM d').format(_selectedDate); + final dateStr = DateFormat('EEEE, MMM d').format(day.date); + final isAvailable = day.isAvailable; return Container( padding: const EdgeInsets.all(20), @@ -606,7 +414,7 @@ class _AvailabilityPageState extends State { ), Switch( value: isAvailable, - onChanged: (val) => _toggleDayAvailability(selectedDayKey), + onChanged: (val) => context.read().add(ToggleDayStatus(day)), activeColor: AppColors.krowBlue, ), ], @@ -614,123 +422,163 @@ class _AvailabilityPageState extends State { const SizedBox(height: 16), - // Time Slots - ..._timeSlots.map((slot) { - final isActive = _isTimeSlotActive(slot['slotId']); - // Determine styles based on state - final isEnabled = - isAvailable; // If day is off, slots are disabled visually - - // Container style - Color bgColor; - Color borderColor; - - if (!isEnabled) { - bgColor = const Color(0xFFF8FAFC); // slate-50 - borderColor = const Color(0xFFF1F5F9); // slate-100 - } else if (isActive) { - bgColor = AppColors.krowBlue.withOpacity(0.05); - borderColor = AppColors.krowBlue.withOpacity(0.2); - } else { - bgColor = const Color(0xFFF8FAFC); // slate-50 - borderColor = const Color(0xFFE2E8F0); // slate-200 - } - - // Text colors - final titleColor = (isEnabled && isActive) - ? AppColors.krowCharcoal - : AppColors.krowMuted; - final subtitleColor = (isEnabled && isActive) - ? AppColors.krowMuted - : Colors.grey.shade400; - - return GestureDetector( - onTap: isEnabled ? () => _toggleTimeSlot(slot['slotId']) : null, - child: AnimatedContainer( - duration: const Duration(milliseconds: 200), - margin: const EdgeInsets.only(bottom: 12), - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: bgColor, - borderRadius: BorderRadius.circular(12), - border: Border.all(color: borderColor, width: 2), - ), - child: Row( - children: [ - // Icon - Container( - width: 40, - height: 40, - decoration: BoxDecoration( - color: slot['bg'], - borderRadius: BorderRadius.circular(12), - ), - child: Icon( - slot['icon'], - color: slot['iconColor'], - size: 20, - ), - ), - const SizedBox(width: 12), - // Text - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - slot['label'], - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - color: titleColor, - ), - ), - Text( - slot['timeRange'], - style: TextStyle( - fontSize: 12, - color: subtitleColor, - ), - ), - ], - ), - ), - // Checkbox indicator - if (isEnabled && isActive) - Container( - width: 24, - height: 24, - decoration: const BoxDecoration( - color: AppColors.krowBlue, - shape: BoxShape.circle, - ), - child: const Icon( - LucideIcons.check, - size: 16, - color: Colors.white, - ), - ) - else if (isEnabled && !isActive) - Container( - width: 24, - height: 24, - decoration: BoxDecoration( - shape: BoxShape.circle, - border: Border.all( - color: const Color(0xFFCBD5E1), - width: 2, - ), // slate-300 - ), - ), - ], - ), - ), - ); + // Time Slots (only from Domain) + ...day.slots.map((slot) { + // Get UI config for this slot ID + final uiConfig = _getSlotUiConfig(slot.id); + + return _buildTimeSlotItem(context, day, slot, uiConfig); }).toList(), ], ), ); } + + Map _getSlotUiConfig(String slotId) { + switch (slotId) { + case 'morning': + return { + 'icon': LucideIcons.sunrise, + 'bg': const Color(0xFFE6EBF9), // bg-[#0032A0]/10 + 'iconColor': const Color(0xFF0032A0), + }; + case 'afternoon': + return { + 'icon': LucideIcons.sun, + 'bg': const Color(0xFFCCD6EC), // bg-[#0032A0]/20 + 'iconColor': const Color(0xFF0032A0), + }; + case 'evening': + return { + 'icon': LucideIcons.moon, + 'bg': const Color(0xFFEBEDEE), // bg-[#333F48]/10 + 'iconColor': const Color(0xFF333F48), + }; + default: + return { + 'icon': LucideIcons.clock, + 'bg': Colors.grey.shade100, + 'iconColor': Colors.grey, + }; + } + } + + Widget _buildTimeSlotItem( + BuildContext context, + DayAvailability day, + AvailabilitySlot slot, + Map uiConfig + ) { + // Determine styles based on state + final isEnabled = day.isAvailable; + final isActive = slot.isAvailable; + + // Container style + Color bgColor; + Color borderColor; + + if (!isEnabled) { + bgColor = const Color(0xFFF8FAFC); // slate-50 + borderColor = const Color(0xFFF1F5F9); // slate-100 + } else if (isActive) { + bgColor = AppColors.krowBlue.withOpacity(0.05); + borderColor = AppColors.krowBlue.withOpacity(0.2); + } else { + bgColor = const Color(0xFFF8FAFC); // slate-50 + borderColor = const Color(0xFFE2E8F0); // slate-200 + } + + // Text colors + final titleColor = (isEnabled && isActive) + ? AppColors.krowCharcoal + : AppColors.krowMuted; + final subtitleColor = (isEnabled && isActive) + ? AppColors.krowMuted + : Colors.grey.shade400; + + return GestureDetector( + onTap: isEnabled ? () => context.read().add(ToggleSlotStatus(day, slot.id)) : null, + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + margin: const EdgeInsets.only(bottom: 12), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: bgColor, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: borderColor, width: 2), + ), + child: Row( + children: [ + // Icon + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: uiConfig['bg'], + borderRadius: BorderRadius.circular(12), + ), + child: Icon( + uiConfig['icon'], + color: uiConfig['iconColor'], + size: 20, + ), + ), + const SizedBox(width: 12), + // Text + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + slot.label, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: titleColor, + ), + ), + Text( + slot.timeRange, + style: TextStyle( + fontSize: 12, + color: subtitleColor, + ), + ), + ], + ), + ), + // Checkbox indicator + if (isEnabled && isActive) + Container( + width: 24, + height: 24, + decoration: const BoxDecoration( + color: AppColors.krowBlue, + shape: BoxShape.circle, + ), + child: const Icon( + LucideIcons.check, + size: 16, + color: Colors.white, + ), + ) + else if (isEnabled && !isActive) + Container( + width: 24, + height: 24, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: const Color(0xFFCBD5E1), + width: 2, + ), // slate-300 + ), + ), + ], + ), + ), + ); + } Widget _buildInfoCard() { return Container( diff --git a/apps/mobile/packages/features/staff/availability/lib/src/presentation/pages/availability_page_new.dart b/apps/mobile/packages/features/staff/availability/lib/src/presentation/pages/availability_page_new.dart deleted file mode 100644 index ef684b8b..00000000 --- a/apps/mobile/packages/features/staff/availability/lib/src/presentation/pages/availability_page_new.dart +++ /dev/null @@ -1,693 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_modular/flutter_modular.dart' hide ModularWatchExtension; -import 'package:intl/intl.dart'; -import 'package:lucide_icons/lucide_icons.dart'; - -import '../blocs/availability_bloc.dart'; -import '../blocs/availability_event.dart'; -import '../blocs/availability_state.dart'; -import 'package:krow_domain/krow_domain.dart'; - -class AvailabilityPage extends StatefulWidget { - const AvailabilityPage({super.key}); - - @override - State createState() => _AvailabilityPageState(); -} - -class _AvailabilityPageState extends State { - final AvailabilityBloc _bloc = Modular.get(); - - @override - void initState() { - super.initState(); - _calculateInitialWeek(); - } - - void _calculateInitialWeek() { - final today = DateTime.now(); - final day = today.weekday; // Mon=1, Sun=7 - final diff = day - 1; // Assuming Monday start - DateTime currentWeekStart = today.subtract(Duration(days: diff)); - currentWeekStart = DateTime( - currentWeekStart.year, - currentWeekStart.month, - currentWeekStart.day, - ); - _bloc.add(LoadAvailability(currentWeekStart)); - } - - @override - Widget build(BuildContext context) { - return BlocProvider.value( - value: _bloc, - child: Scaffold( - backgroundColor: AppColors.krowBackground, - body: BlocBuilder( - builder: (context, state) { - if (state is AvailabilityLoading) { - return const Center(child: CircularProgressIndicator()); - } else if (state is AvailabilityLoaded) { - return SingleChildScrollView( - padding: const EdgeInsets.only(bottom: 100), - child: Column( - children: [ - _buildHeader(), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - _buildQuickSet(context), - const SizedBox(height: 24), - _buildWeekNavigation(context, state), - const SizedBox(height: 24), - _buildSelectedDayAvailability( - context, - state.selectedDayAvailability, - ), - const SizedBox(height: 24), - _buildInfoCard(), - ], - ), - ), - ], - ), - ); - } else if (state is AvailabilityError) { - return Center(child: Text('Error: ${state.message}')); - } - return const SizedBox.shrink(); - }, - ), - ), - ); - } - - Widget _buildHeader() { - return Container( - padding: const EdgeInsets.fromLTRB(20, 60, 20, 20), - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - IconButton( - icon: const Icon( - LucideIcons.arrowLeft, - color: AppColors.krowCharcoal, - ), - onPressed: () => Modular.to.pop(), - ), - const SizedBox(width: 12), - Row( - children: [ - Container( - width: 40, - height: 40, - decoration: BoxDecoration( - border: Border.all( - color: AppColors.krowBlue.withOpacity(0.2), - width: 2, - ), - shape: BoxShape.circle, - ), - child: Center( - child: CircleAvatar( - backgroundColor: AppColors.krowBlue.withOpacity( - 0.1, - ), - radius: 18, - child: const Text( - 'K', // Mock initial - style: TextStyle( - color: AppColors.krowBlue, - fontWeight: FontWeight.bold, - fontSize: 14, - ), - ), - ), - ), - ), - const SizedBox(width: 12), - const Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'My Availability', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - color: AppColors.krowCharcoal, - ), - ), - Text( - 'Set when you can work', - style: TextStyle( - fontSize: 14, - color: AppColors.krowMuted, - ), - ), - ], - ), - ], - ), - ], - ), - Container( - width: 40, - height: 40, - decoration: BoxDecoration( - color: AppColors.krowBlue.withOpacity(0.1), - shape: BoxShape.circle, - ), - child: const Icon( - LucideIcons.calendar, - color: AppColors.krowBlue, - size: 20, - ), - ), - ], - ), - ], - ), - ); - } - - Widget _buildQuickSet(BuildContext context) { - return Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: AppColors.krowBlue.withOpacity(0.1), - borderRadius: BorderRadius.circular(16), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'Quick Set Availability', - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - color: Color(0xFF333F48), - ), - ), - const SizedBox(height: 12), - Row( - children: [ - Expanded( - child: _buildQuickSetButton( - context, - 'All Week', - 'all', - ), - ), - const SizedBox(width: 8), - Expanded( - child: _buildQuickSetButton( - context, - 'Weekdays', - 'weekdays', - ), - ), - const SizedBox(width: 8), - Expanded( - child: _buildQuickSetButton( - context, - 'Weekends', - 'weekends', - ), - ), - const SizedBox(width: 8), - Expanded( - child: _buildQuickSetButton( - context, - 'Clear All', - 'clear', - isDestructive: true, - ), - ), - ], - ), - ], - ), - ); - } - - Widget _buildQuickSetButton( - BuildContext context, - String label, - String type, { - bool isDestructive = false, - }) { - return SizedBox( - height: 32, - child: OutlinedButton( - onPressed: () => context.read().add(PerformQuickSet(type)), - style: OutlinedButton.styleFrom( - padding: EdgeInsets.zero, - side: BorderSide( - color: isDestructive - ? Colors.red.withOpacity(0.2) - : AppColors.krowBlue.withOpacity(0.2), - ), - backgroundColor: Colors.transparent, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - foregroundColor: isDestructive ? Colors.red : AppColors.krowBlue, - ), - child: Text( - label, - style: const TextStyle(fontSize: 10, fontWeight: FontWeight.w500), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), - ); - } - - Widget _buildWeekNavigation(BuildContext context, AvailabilityLoaded state) { - // Middle date for month display - final middleDate = state.currentWeekStart.add(const Duration(days: 3)); - final monthYear = DateFormat('MMMM yyyy').format(middleDate); - - return Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(16), - border: Border.all(color: Colors.grey.shade100), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.05), - blurRadius: 2, - offset: const Offset(0, 1), - ), - ], - ), - child: Column( - children: [ - // Nav Header - Padding( - padding: const EdgeInsets.only(bottom: 16), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - _buildNavButton( - LucideIcons.chevronLeft, - () => context.read().add(const NavigateWeek(-1)), - ), - Text( - monthYear, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - color: AppColors.krowCharcoal, - ), - ), - _buildNavButton( - LucideIcons.chevronRight, - () => context.read().add(const NavigateWeek(1)), - ), - ], - ), - ), - // Days Row - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: state.days.map((day) => _buildDayItem(context, day, state.selectedDate)).toList(), - ), - ], - ), - ); - } - - Widget _buildNavButton(IconData icon, VoidCallback onTap) { - return GestureDetector( - onTap: onTap, - child: Container( - width: 32, - height: 32, - decoration: const BoxDecoration( - color: Color(0xFFF1F5F9), // slate-100 - shape: BoxShape.circle, - ), - child: Icon(icon, size: 20, color: AppColors.krowMuted), - ), - ); - } - - Widget _buildDayItem(BuildContext context, DayAvailability day, DateTime selectedDate) { - final isSelected = AvailabilityLoaded.isSameDay(day.date, selectedDate); - final isAvailable = day.isAvailable; - final isToday = AvailabilityLoaded.isSameDay(day.date, DateTime.now()); - - return Expanded( - child: GestureDetector( - onTap: () => context.read().add(SelectDate(day.date)), - child: Container( - margin: const EdgeInsets.symmetric(horizontal: 2), - padding: const EdgeInsets.symmetric(vertical: 12), - decoration: BoxDecoration( - color: isSelected - ? AppColors.krowBlue - : (isAvailable - ? const Color(0xFFECFDF5) - : const Color(0xFFF8FAFC)), // emerald-50 or slate-50 - borderRadius: BorderRadius.circular(16), - border: Border.all( - color: isSelected - ? AppColors.krowBlue - : (isAvailable - ? const Color(0xFFA7F3D0) - : Colors.transparent), // emerald-200 - ), - boxShadow: isSelected - ? [ - BoxShadow( - color: AppColors.krowBlue.withOpacity(0.3), - blurRadius: 8, - offset: const Offset(0, 4), - ), - ] - : null, - ), - child: Stack( - clipBehavior: Clip.none, - alignment: Alignment.center, - children: [ - Column( - children: [ - Text( - day.date.day.toString().padLeft(2, '0'), - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: isSelected - ? Colors.white - : (isAvailable - ? const Color(0xFF047857) - : AppColors.krowMuted), // emerald-700 - ), - ), - const SizedBox(height: 2), - Text( - DateFormat('EEE').format(day.date), - style: TextStyle( - fontSize: 10, - color: isSelected - ? Colors.white.withOpacity(0.8) - : (isAvailable - ? const Color(0xFF047857) - : AppColors.krowMuted), - ), - ), - ], - ), - if (isToday && !isSelected) - Positioned( - bottom: -8, - child: Container( - width: 6, - height: 6, - decoration: const BoxDecoration( - color: AppColors.krowBlue, - shape: BoxShape.circle, - ), - ), - ), - ], - ), - ), - ), - ); - } - - Widget _buildSelectedDayAvailability( - BuildContext context, - DayAvailability day, - ) { - final dateStr = DateFormat('EEEE, MMM d').format(day.date); - final isAvailable = day.isAvailable; - - return Container( - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(16), - border: Border.all(color: Colors.grey.shade100), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.05), - blurRadius: 2, - offset: const Offset(0, 1), - ), - ], - ), - child: Column( - children: [ - // Header Row - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - dateStr, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - color: AppColors.krowCharcoal, - ), - ), - Text( - isAvailable ? 'You are available' : 'Not available', - style: const TextStyle( - fontSize: 14, - color: AppColors.krowMuted, - ), - ), - ], - ), - Switch( - value: isAvailable, - onChanged: (val) => context.read().add(ToggleDayStatus(day)), - activeColor: AppColors.krowBlue, - ), - ], - ), - - const SizedBox(height: 16), - - // Time Slots (only from Domain) - ...day.slots.map((slot) { - // Get UI config for this slot ID - final uiConfig = _getSlotUiConfig(slot.id); - - return _buildTimeSlotItem(context, day, slot, uiConfig); - }).toList(), - ], - ), - ); - } - - Map _getSlotUiConfig(String slotId) { - switch (slotId) { - case 'morning': - return { - 'icon': LucideIcons.sunrise, - 'bg': const Color(0xFFE6EBF9), // bg-[#0032A0]/10 - 'iconColor': const Color(0xFF0032A0), - }; - case 'afternoon': - return { - 'icon': LucideIcons.sun, - 'bg': const Color(0xFFCCD6EC), // bg-[#0032A0]/20 - 'iconColor': const Color(0xFF0032A0), - }; - case 'evening': - return { - 'icon': LucideIcons.moon, - 'bg': const Color(0xFFEBEDEE), // bg-[#333F48]/10 - 'iconColor': const Color(0xFF333F48), - }; - default: - return { - 'icon': LucideIcons.clock, - 'bg': Colors.grey.shade100, - 'iconColor': Colors.grey, - }; - } - } - - Widget _buildTimeSlotItem( - BuildContext context, - DayAvailability day, - AvailabilitySlot slot, - Map uiConfig - ) { - // Determine styles based on state - final isEnabled = day.isAvailable; - final isActive = slot.isAvailable; - - // Container style - Color bgColor; - Color borderColor; - - if (!isEnabled) { - bgColor = const Color(0xFFF8FAFC); // slate-50 - borderColor = const Color(0xFFF1F5F9); // slate-100 - } else if (isActive) { - bgColor = AppColors.krowBlue.withOpacity(0.05); - borderColor = AppColors.krowBlue.withOpacity(0.2); - } else { - bgColor = const Color(0xFFF8FAFC); // slate-50 - borderColor = const Color(0xFFE2E8F0); // slate-200 - } - - // Text colors - final titleColor = (isEnabled && isActive) - ? AppColors.krowCharcoal - : AppColors.krowMuted; - final subtitleColor = (isEnabled && isActive) - ? AppColors.krowMuted - : Colors.grey.shade400; - - return GestureDetector( - onTap: isEnabled ? () => context.read().add(ToggleSlotStatus(day, slot.id)) : null, - child: AnimatedContainer( - duration: const Duration(milliseconds: 200), - margin: const EdgeInsets.only(bottom: 12), - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: bgColor, - borderRadius: BorderRadius.circular(12), - border: Border.all(color: borderColor, width: 2), - ), - child: Row( - children: [ - // Icon - Container( - width: 40, - height: 40, - decoration: BoxDecoration( - color: uiConfig['bg'], - borderRadius: BorderRadius.circular(12), - ), - child: Icon( - uiConfig['icon'], - color: uiConfig['iconColor'], - size: 20, - ), - ), - const SizedBox(width: 12), - // Text - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - slot.label, - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - color: titleColor, - ), - ), - Text( - slot.timeRange, - style: TextStyle( - fontSize: 12, - color: subtitleColor, - ), - ), - ], - ), - ), - // Checkbox indicator - if (isEnabled && isActive) - Container( - width: 24, - height: 24, - decoration: const BoxDecoration( - color: AppColors.krowBlue, - shape: BoxShape.circle, - ), - child: const Icon( - LucideIcons.check, - size: 16, - color: Colors.white, - ), - ) - else if (isEnabled && !isActive) - Container( - width: 24, - height: 24, - decoration: BoxDecoration( - shape: BoxShape.circle, - border: Border.all( - color: const Color(0xFFCBD5E1), - width: 2, - ), // slate-300 - ), - ), - ], - ), - ), - ); - } - - Widget _buildInfoCard() { - return Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: AppColors.krowBlue.withOpacity(0.05), - borderRadius: BorderRadius.circular(12), - ), - child: const Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Icon(LucideIcons.clock, size: 20, color: AppColors.krowBlue), - SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Auto-Match uses your availability', - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - color: AppColors.krowCharcoal, - ), - ), - SizedBox(height: 2), - Text( - "When enabled, you'll only be matched with shifts during your available times.", - style: TextStyle(fontSize: 12, color: AppColors.krowMuted), - ), - ], - ), - ), - ], - ), - ); - } -} - -class AppColors { - static const Color krowBlue = Color(0xFF0A39DF); - static const Color krowYellow = Color(0xFFFFED4A); - static const Color krowCharcoal = Color(0xFF121826); - static const Color krowMuted = Color(0xFF6A7382); - static const Color krowBorder = Color(0xFFE3E6E9); - static const Color krowBackground = Color(0xFFFAFBFC); - - static const Color white = Colors.white; - static const Color black = Colors.black; -} diff --git a/apps/mobile/packages/features/staff/availability/lib/src/staff_availability_module.dart b/apps/mobile/packages/features/staff/availability/lib/src/staff_availability_module.dart index 199f0d10..35aba337 100644 --- a/apps/mobile/packages/features/staff/availability/lib/src/staff_availability_module.dart +++ b/apps/mobile/packages/features/staff/availability/lib/src/staff_availability_module.dart @@ -1,21 +1,28 @@ +import 'package:firebase_auth/firebase_auth.dart'; import 'package:flutter_modular/flutter_modular.dart'; import 'package:krow_data_connect/krow_data_connect.dart'; -import 'data/repositories/availability_repository_impl.dart'; +import 'package:staff_availability/src/presentation/pages/availability_page.dart'; + +import 'data/repositories_impl/availability_repository_impl.dart'; import 'domain/repositories/availability_repository.dart'; import 'domain/usecases/apply_quick_set_usecase.dart'; import 'domain/usecases/get_weekly_availability_usecase.dart'; import 'domain/usecases/update_day_availability_usecase.dart'; import 'presentation/blocs/availability_bloc.dart'; -import 'presentation/pages/availability_page.dart'; class StaffAvailabilityModule extends Module { @override - void binds(i) { - // Data Sources - i.add(StaffRepositoryMock.new); + List get imports => [DataConnectModule()]; + @override + void binds(Injector i) { // Repository - i.add(AvailabilityRepositoryImpl.new); + i.add( + () => AvailabilityRepositoryImpl( + dataConnect: ExampleConnector.instance, + firebaseAuth: FirebaseAuth.instance, + ), + ); // UseCases i.add(GetWeeklyAvailabilityUseCase.new); @@ -27,7 +34,7 @@ class StaffAvailabilityModule extends Module { } @override - void routes(r) { + void routes(RouteManager r) { r.child('/', child: (_) => const AvailabilityPage()); } } diff --git a/apps/mobile/packages/features/staff/availability/pubspec.yaml b/apps/mobile/packages/features/staff/availability/pubspec.yaml index 1b20e6bd..fe5ef376 100644 --- a/apps/mobile/packages/features/staff/availability/pubspec.yaml +++ b/apps/mobile/packages/features/staff/availability/pubspec.yaml @@ -5,7 +5,7 @@ publish_to: 'none' resolution: workspace environment: - sdk: '>=3.0.0 <4.0.0' + sdk: '>=3.10.0 <4.0.0' flutter: ">=1.17.0" dependencies: @@ -28,8 +28,10 @@ dependencies: path: ../../../data_connect krow_core: path: ../../../core + firebase_data_connect: ^0.2.2+2 + firebase_auth: ^6.1.4 dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^3.0.0 + flutter_lints: ^6.0.0 diff --git a/apps/mobile/packages/features/staff/clock_in/lib/src/data/repositories/clock_in_repository_impl.dart b/apps/mobile/packages/features/staff/clock_in/lib/src/data/repositories/clock_in_repository_impl.dart deleted file mode 100644 index ef8e4211..00000000 --- a/apps/mobile/packages/features/staff/clock_in/lib/src/data/repositories/clock_in_repository_impl.dart +++ /dev/null @@ -1,115 +0,0 @@ -import 'package:krow_data_connect/krow_data_connect.dart'; -import 'package:krow_domain/krow_domain.dart'; -import 'package:intl/intl.dart'; -import '../../domain/repositories/clock_in_repository_interface.dart'; - -/// Implementation of [ClockInRepositoryInterface]. -/// -/// Delegates shift data retrieval to [ShiftsRepositoryMock] and manages purely -/// local state for attendance (check-in/out) for the prototype phase. -class ClockInRepositoryImpl implements ClockInRepositoryInterface { - final ShiftsRepositoryMock _shiftsMock; - - // Local state for the session (mocking backend state) - bool _isCheckedIn = false; - DateTime? _checkInTime; - DateTime? _checkOutTime; - String? _activeShiftId; - - ClockInRepositoryImpl({ShiftsRepositoryMock? shiftsMock}) - : _shiftsMock = shiftsMock ?? ShiftsRepositoryMock(); - - @override - Future getTodaysShift() async { - final shifts = await _shiftsMock.getMyShifts(); - - if (shifts.isEmpty) return null; - - final now = DateTime.now(); - final todayStr = DateFormat('yyyy-MM-dd').format(now); - - // Find a shift effectively for today, or mock one - try { - return shifts.firstWhere((s) => s.date == todayStr); - } catch (_) { - final original = shifts.first; - // Mock "today's" shift based on the first available shift - return Shift( - id: original.id, - title: original.title, - clientName: original.clientName, - logoUrl: original.logoUrl, - hourlyRate: original.hourlyRate, - location: original.location, - locationAddress: original.locationAddress, - date: todayStr, - startTime: original.startTime, // Use original times or calculate - endTime: original.endTime, - createdDate: original.createdDate, - status: 'assigned', - latitude: original.latitude, - longitude: original.longitude, - description: original.description, - managers: original.managers, - ); - } - } - - @override - Future> getAttendanceStatus() async { - await Future.delayed(const Duration(milliseconds: 300)); - return _getCurrentStatusMap(); - } - - @override - Future> clockIn({required String shiftId, String? notes}) async { - await Future.delayed(const Duration(seconds: 1)); // Simulate network - - _isCheckedIn = true; - _checkInTime = DateTime.now(); - _activeShiftId = shiftId; - _checkOutTime = null; // Reset for new check-in? Or keep for history? - // Simple mock logic: reset check-out on new check-in. - - return _getCurrentStatusMap(); - } - - @override - Future> clockOut({String? notes, int? breakTimeMinutes}) async { - await Future.delayed(const Duration(seconds: 1)); // Simulate network - - _isCheckedIn = false; - _checkOutTime = DateTime.now(); - - return _getCurrentStatusMap(); - } - - @override - Future>> getActivityLog() async { - await Future.delayed(const Duration(milliseconds: 500)); - // Mock data - return [ - { - 'date': DateTime.now().subtract(const Duration(days: 1)), - 'start': '09:00 AM', - 'end': '05:00 PM', - 'hours': '8h', - }, - { - 'date': DateTime.now().subtract(const Duration(days: 2)), - 'start': '09:00 AM', - 'end': '05:00 PM', - 'hours': '8h', - }, - ]; - } - - Map _getCurrentStatusMap() { - return { - 'isCheckedIn': _isCheckedIn, - 'checkInTime': _checkInTime, - 'checkOutTime': _checkOutTime, - 'activeShiftId': _activeShiftId, - }; - } -} diff --git a/apps/mobile/packages/features/staff/clock_in/lib/src/data/repositories_impl/clock_in_repository_impl.dart b/apps/mobile/packages/features/staff/clock_in/lib/src/data/repositories_impl/clock_in_repository_impl.dart new file mode 100644 index 00000000..ba1f14e8 --- /dev/null +++ b/apps/mobile/packages/features/staff/clock_in/lib/src/data/repositories_impl/clock_in_repository_impl.dart @@ -0,0 +1,308 @@ +import 'package:firebase_data_connect/firebase_data_connect.dart'; +import 'package:krow_data_connect/krow_data_connect.dart' as dc; +import 'package:krow_domain/krow_domain.dart'; +import 'package:krow_core/core.dart'; +import 'package:krow_data_connect/src/session/staff_session_store.dart'; +import '../../domain/repositories/clock_in_repository_interface.dart'; + +/// Implementation of [ClockInRepositoryInterface] using Firebase Data Connect. +class ClockInRepositoryImpl implements ClockInRepositoryInterface { + final dc.ExampleConnector _dataConnect; + final Map _shiftToApplicationId = {}; + String? _activeApplicationId; + + ClockInRepositoryImpl({ + required dc.ExampleConnector dataConnect, + }) : _dataConnect = dataConnect; + + Future _getStaffId() async { + final StaffSession? session = StaffSessionStore.instance.session; + final String? staffId = session?.staff?.id; + if (staffId != null && staffId.isNotEmpty) { + return staffId; + } + throw Exception('Staff session not found'); + } + + /// Helper to convert Data Connect Timestamp to DateTime + DateTime? _toDateTime(dynamic t) { + if (t == null) return null; + DateTime? dt; + if (t is DateTime) { + dt = t; + } else if (t is String) { + dt = DateTime.tryParse(t); + } else { + try { + if (t is Timestamp) { + dt = t.toDateTime(); + } + } catch (_) {} + + try { + if (dt == null && t.runtimeType.toString().contains('Timestamp')) { + dt = (t as dynamic).toDate(); + } + } catch (_) {} + + try { + if (dt == null) { + dt = DateTime.tryParse(t.toString()); + } + } catch (_) {} + } + + if (dt != null) { + return DateTimeUtils.toDeviceTime(dt); + } + return null; + } + + /// Helper to create Timestamp from DateTime + Timestamp _fromDateTime(DateTime d) { + // Assuming Timestamp.fromJson takes an ISO string + return Timestamp.fromJson(d.toUtc().toIso8601String()); + } + + ({Timestamp start, Timestamp end}) _utcDayRange(DateTime localDay) { + final DateTime dayStartUtc = DateTime.utc( + localDay.year, + localDay.month, + localDay.day, + ); + final DateTime dayEndUtc = DateTime.utc( + localDay.year, + localDay.month, + localDay.day, + 23, + 59, + 59, + 999, + 999, + ); + return ( + start: _fromDateTime(dayStartUtc), + end: _fromDateTime(dayEndUtc), + ); + } + + /// Helper to find today's applications ordered with the closest at the end. + Future> _getTodaysApplications( + String staffId, + ) async { + final DateTime now = DateTime.now(); + final range = _utcDayRange(now); + final QueryResult + result = await _dataConnect + .getApplicationsByStaffId(staffId: staffId) + .dayStart(range.start) + .dayEnd(range.end) + .execute(); + + final apps = result.data.applications; + if (apps.isEmpty) return const []; + + _shiftToApplicationId + ..clear() + ..addEntries(apps.map((app) => MapEntry(app.shiftId, app.id))); + + apps.sort((a, b) { + final DateTime? aTime = + _toDateTime(a.shift.startTime) ?? _toDateTime(a.shift.date); + final DateTime? bTime = + _toDateTime(b.shift.startTime) ?? _toDateTime(b.shift.date); + if (aTime == null && bTime == null) return 0; + if (aTime == null) return -1; + if (bTime == null) return 1; + final Duration aDiff = aTime.difference(now).abs(); + final Duration bDiff = bTime.difference(now).abs(); + return bDiff.compareTo(aDiff); // closest at the end + }); + + return apps; + } + + dc.GetApplicationsByStaffIdApplications? _getActiveApplication( + List apps, + ) { + try { + return apps.firstWhere((app) { + final status = app.status.stringValue; + return status == 'CHECKED_IN' || status == 'LATE'; + }); + } catch (_) { + return null; + } + } + + @override + Future> getTodaysShifts() async { + final String staffId = await _getStaffId(); + final List apps = + await _getTodaysApplications(staffId); + if (apps.isEmpty) return const []; + + final List shifts = []; + for (final app in apps) { + final dc.GetApplicationsByStaffIdApplicationsShift shift = app.shift; + final DateTime? startDt = _toDateTime(app.shiftRole.startTime); + final DateTime? endDt = _toDateTime(app.shiftRole.endTime); + final DateTime? createdDt = _toDateTime(app.createdAt); + + final String roleName = app.shiftRole.role.name; + final String orderName = + (shift.order.eventName ?? '').trim().isNotEmpty + ? shift.order.eventName! + : shift.order.business.businessName; + final String title = '$roleName - $orderName'; + shifts.add( + Shift( + id: shift.id, + title: title, + clientName: shift.order.business.businessName, + logoUrl: shift.order.business.companyLogoUrl ?? '', + hourlyRate: app.shiftRole.role.costPerHour, + location: shift.location ?? '', + locationAddress: shift.order.teamHub.hubName, + date: startDt?.toIso8601String() ?? '', + startTime: startDt?.toIso8601String() ?? '', + endTime: endDt?.toIso8601String() ?? '', + createdDate: createdDt?.toIso8601String() ?? '', + status: shift.status?.stringValue, + description: shift.description, + latitude: shift.latitude, + longitude: shift.longitude, + ), + ); + } + + return shifts; + } + + @override + Future getAttendanceStatus() async { + final String staffId = await _getStaffId(); + final List apps = + await _getTodaysApplications(staffId); + if (apps.isEmpty) { + return const AttendanceStatus(isCheckedIn: false); + } + + dc.GetApplicationsByStaffIdApplications? activeApp; + for (final app in apps) { + if (app.checkInTime != null && app.checkOutTime == null) { + if (activeApp == null) { + activeApp = app; + } else { + final DateTime? current = _toDateTime(activeApp.checkInTime); + final DateTime? next = _toDateTime(app.checkInTime); + if (current == null || (next != null && next.isAfter(current))) { + activeApp = app; + } + } + } + } + + if (activeApp == null) { + _activeApplicationId = null; + return const AttendanceStatus(isCheckedIn: false); + } + + _activeApplicationId = activeApp.id; + print('Active check-in appId=$_activeApplicationId'); + return AttendanceStatus( + isCheckedIn: true, + checkInTime: _toDateTime(activeApp.checkInTime), + checkOutTime: _toDateTime(activeApp.checkOutTime), + activeShiftId: activeApp.shiftId, + activeApplicationId: activeApp.id, + ); + } + + @override + Future clockIn({required String shiftId, String? notes}) async { + final String staffId = await _getStaffId(); + + final String? cachedAppId = _shiftToApplicationId[shiftId]; + dc.GetApplicationsByStaffIdApplications? app; + if (cachedAppId != null) { + try { + final apps = await _getTodaysApplications(staffId); + app = apps.firstWhere((a) => a.id == cachedAppId); + } catch (_) {} + } + app ??= (await _getTodaysApplications(staffId)) + .firstWhere((a) => a.shiftId == shiftId); + + final Timestamp checkInTs = _fromDateTime(DateTime.now()); + print( + 'ClockIn request: appId=${app.id} shiftId=$shiftId ' + 'checkInTime=${checkInTs.toJson()}', + ); + try { + await _dataConnect + .updateApplicationStatus( + id: app.id, + ) + .checkInTime(checkInTs) + .execute(); + _activeApplicationId = app.id; + } catch (e) { + print('ClockIn updateApplicationStatus error: $e'); + print('ClockIn error type: ${e.runtimeType}'); + try { + final dynamic err = e; + final dynamic details = + err.details ?? err.response ?? err.data ?? err.message; + if (details != null) { + print('ClockIn error details: $details'); + } + } catch (_) {} + rethrow; + } + + return getAttendanceStatus(); + } + + @override + Future clockOut({ + String? notes, + int? breakTimeMinutes, + String? applicationId, + }) async { + final String staffId = await _getStaffId(); + + print( + 'ClockOut request: applicationId=$applicationId ' + 'activeApplicationId=$_activeApplicationId', + ); + final String? targetAppId = applicationId ?? _activeApplicationId; + if (targetAppId == null || targetAppId.isEmpty) { + throw Exception('No active application id for checkout'); + } + final appResult = await _dataConnect + .getApplicationById(id: targetAppId) + .execute(); + final app = appResult.data.application; + print( + 'ClockOut getApplicationById: id=${app?.id} ' + 'checkIn=${app?.checkInTime?.toJson()} ' + 'checkOut=${app?.checkOutTime?.toJson()}', + ); + if (app == null) { + throw Exception('Application not found for checkout'); + } + if (app.checkInTime == null || app.checkOutTime != null) { + throw Exception('No active shift found to clock out'); + } + + await _dataConnect + .updateApplicationStatus( + id: targetAppId, + ) + .checkOutTime(_fromDateTime(DateTime.now())) + .execute(); + + return getAttendanceStatus(); + } +} diff --git a/apps/mobile/packages/features/staff/clock_in/lib/src/domain/arguments/clock_out_arguments.dart b/apps/mobile/packages/features/staff/clock_in/lib/src/domain/arguments/clock_out_arguments.dart index 9b7fd324..04cb55fc 100644 --- a/apps/mobile/packages/features/staff/clock_in/lib/src/domain/arguments/clock_out_arguments.dart +++ b/apps/mobile/packages/features/staff/clock_in/lib/src/domain/arguments/clock_out_arguments.dart @@ -8,12 +8,16 @@ class ClockOutArguments extends UseCaseArgument { /// Optional break time in minutes. final int? breakTimeMinutes; + /// Optional application id for checkout. + final String? applicationId; + /// Creates a [ClockOutArguments] instance. const ClockOutArguments({ this.notes, this.breakTimeMinutes, + this.applicationId, }); @override - List get props => [notes, breakTimeMinutes]; + List get props => [notes, breakTimeMinutes, applicationId]; } diff --git a/apps/mobile/packages/features/staff/clock_in/lib/src/domain/repositories/clock_in_repository.dart b/apps/mobile/packages/features/staff/clock_in/lib/src/domain/repositories/clock_in_repository.dart deleted file mode 100644 index c27c665f..00000000 --- a/apps/mobile/packages/features/staff/clock_in/lib/src/domain/repositories/clock_in_repository.dart +++ /dev/null @@ -1,39 +0,0 @@ -import 'package:krow_domain/krow_domain.dart'; - -/// Repository interface for Clock In/Out functionality -abstract class ClockInRepository { - - /// Retrieves the shift assigned to the user for the current day. - /// Returns null if no shift is assigned for today. - Future getTodaysShift(); - - /// Gets the current attendance status (e.g., checked in or not, times). - /// This helps in restoring the UI state if the app was killed. - Future getAttendanceStatus(); - - /// Checks the user in for the specified [shiftId]. - /// Returns the updated [AttendanceStatus]. - Future clockIn({required String shiftId, String? notes}); - - /// Checks the user out for the currently active shift. - /// Optionally accepts [breakTimeMinutes] if tracked. - Future clockOut({String? notes, int? breakTimeMinutes}); - - /// Retrieves a list of recent clock-in/out activities. - Future>> getActivityLog(); -} - -/// Simple entity to hold attendance state -class AttendanceStatus { - final bool isCheckedIn; - final DateTime? checkInTime; - final DateTime? checkOutTime; - final String? activeShiftId; - - const AttendanceStatus({ - this.isCheckedIn = false, - this.checkInTime, - this.checkOutTime, - this.activeShiftId, - }); -} diff --git a/apps/mobile/packages/features/staff/clock_in/lib/src/domain/repositories/clock_in_repository_interface.dart b/apps/mobile/packages/features/staff/clock_in/lib/src/domain/repositories/clock_in_repository_interface.dart index 5049987e..3d4795bd 100644 --- a/apps/mobile/packages/features/staff/clock_in/lib/src/domain/repositories/clock_in_repository_interface.dart +++ b/apps/mobile/packages/features/staff/clock_in/lib/src/domain/repositories/clock_in_repository_interface.dart @@ -1,35 +1,25 @@ import 'package:krow_domain/krow_domain.dart'; -/// Interface for the Clock In feature repository. -/// -/// Defines the methods for managing clock-in/out operations and retrieving -/// related shift and attendance data. -abstract interface class ClockInRepositoryInterface { - /// Retrieves the shift scheduled for today. - Future getTodaysShift(); +/// Repository interface for Clock In/Out functionality +abstract class ClockInRepositoryInterface { + + /// Retrieves the shifts assigned to the user for the current day. + /// Returns empty list if no shift is assigned for today. + Future> getTodaysShifts(); - /// Retrieves the current attendance status (check-in time, check-out time, etc.). - /// - /// Returns a Map containing: - /// - 'isCheckedIn': bool - /// - 'checkInTime': DateTime? - /// - 'checkOutTime': DateTime? - Future> getAttendanceStatus(); + /// Gets the current attendance status (e.g., checked in or not, times). + /// This helps in restoring the UI state if the app was killed. + Future getAttendanceStatus(); - /// Clocks the user in for a specific shift. - Future> clockIn({ - required String shiftId, - String? notes, - }); + /// Checks the user in for the specified [shiftId]. + /// Returns the updated [AttendanceStatus]. + Future clockIn({required String shiftId, String? notes}); - /// Clocks the user out of the current shift. - Future> clockOut({ + /// Checks the user out for the currently active shift. + /// Optionally accepts [breakTimeMinutes] if tracked. + Future clockOut({ String? notes, int? breakTimeMinutes, + String? applicationId, }); - - /// Retrieves the history of clock-in/out activity. - /// - /// Returns a list of maps, where each map represents an activity entry. - Future>> getActivityLog(); } diff --git a/apps/mobile/packages/features/staff/clock_in/lib/src/domain/usecases/clock_in_usecase.dart b/apps/mobile/packages/features/staff/clock_in/lib/src/domain/usecases/clock_in_usecase.dart index a99ae43e..c4535129 100644 --- a/apps/mobile/packages/features/staff/clock_in/lib/src/domain/usecases/clock_in_usecase.dart +++ b/apps/mobile/packages/features/staff/clock_in/lib/src/domain/usecases/clock_in_usecase.dart @@ -1,15 +1,16 @@ import 'package:krow_core/core.dart'; +import 'package:krow_domain/krow_domain.dart'; import '../repositories/clock_in_repository_interface.dart'; import '../arguments/clock_in_arguments.dart'; /// Use case for clocking in a user. -class ClockInUseCase implements UseCase> { +class ClockInUseCase implements UseCase { final ClockInRepositoryInterface _repository; ClockInUseCase(this._repository); @override - Future> call(ClockInArguments arguments) { + Future call(ClockInArguments arguments) { return _repository.clockIn( shiftId: arguments.shiftId, notes: arguments.notes, diff --git a/apps/mobile/packages/features/staff/clock_in/lib/src/domain/usecases/clock_out_usecase.dart b/apps/mobile/packages/features/staff/clock_in/lib/src/domain/usecases/clock_out_usecase.dart index dbea2b26..f5b0b14a 100644 --- a/apps/mobile/packages/features/staff/clock_in/lib/src/domain/usecases/clock_out_usecase.dart +++ b/apps/mobile/packages/features/staff/clock_in/lib/src/domain/usecases/clock_out_usecase.dart @@ -1,18 +1,20 @@ import 'package:krow_core/core.dart'; +import 'package:krow_domain/krow_domain.dart'; import '../repositories/clock_in_repository_interface.dart'; import '../arguments/clock_out_arguments.dart'; /// Use case for clocking out a user. -class ClockOutUseCase implements UseCase> { +class ClockOutUseCase implements UseCase { final ClockInRepositoryInterface _repository; ClockOutUseCase(this._repository); @override - Future> call(ClockOutArguments arguments) { + Future call(ClockOutArguments arguments) { return _repository.clockOut( notes: arguments.notes, breakTimeMinutes: arguments.breakTimeMinutes, + applicationId: arguments.applicationId, ); } } diff --git a/apps/mobile/packages/features/staff/clock_in/lib/src/domain/usecases/get_activity_log_usecase.dart b/apps/mobile/packages/features/staff/clock_in/lib/src/domain/usecases/get_activity_log_usecase.dart deleted file mode 100644 index 04b908dc..00000000 --- a/apps/mobile/packages/features/staff/clock_in/lib/src/domain/usecases/get_activity_log_usecase.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'package:krow_core/core.dart'; -import '../repositories/clock_in_repository_interface.dart'; - -/// Use case for retrieving the activity log. -class GetActivityLogUseCase implements NoInputUseCase>> { - final ClockInRepositoryInterface _repository; - - GetActivityLogUseCase(this._repository); - - @override - Future>> call() { - return _repository.getActivityLog(); - } -} diff --git a/apps/mobile/packages/features/staff/clock_in/lib/src/domain/usecases/get_attendance_status_usecase.dart b/apps/mobile/packages/features/staff/clock_in/lib/src/domain/usecases/get_attendance_status_usecase.dart index e0722339..1f80da69 100644 --- a/apps/mobile/packages/features/staff/clock_in/lib/src/domain/usecases/get_attendance_status_usecase.dart +++ b/apps/mobile/packages/features/staff/clock_in/lib/src/domain/usecases/get_attendance_status_usecase.dart @@ -1,14 +1,15 @@ import 'package:krow_core/core.dart'; +import 'package:krow_domain/krow_domain.dart'; import '../repositories/clock_in_repository_interface.dart'; /// Use case for getting the current attendance status (check-in/out times). -class GetAttendanceStatusUseCase implements NoInputUseCase> { +class GetAttendanceStatusUseCase implements NoInputUseCase { final ClockInRepositoryInterface _repository; GetAttendanceStatusUseCase(this._repository); @override - Future> call() { + Future call() { return _repository.getAttendanceStatus(); } } diff --git a/apps/mobile/packages/features/staff/clock_in/lib/src/domain/usecases/get_todays_shift_usecase.dart b/apps/mobile/packages/features/staff/clock_in/lib/src/domain/usecases/get_todays_shift_usecase.dart index 54477dc0..3df3a14b 100644 --- a/apps/mobile/packages/features/staff/clock_in/lib/src/domain/usecases/get_todays_shift_usecase.dart +++ b/apps/mobile/packages/features/staff/clock_in/lib/src/domain/usecases/get_todays_shift_usecase.dart @@ -2,14 +2,14 @@ import 'package:krow_core/core.dart'; import 'package:krow_domain/krow_domain.dart'; import '../repositories/clock_in_repository_interface.dart'; -/// Use case for retrieving the user's scheduled shift for today. -class GetTodaysShiftUseCase implements NoInputUseCase { +/// Use case for retrieving the user's scheduled shifts for today. +class GetTodaysShiftUseCase implements NoInputUseCase> { final ClockInRepositoryInterface _repository; GetTodaysShiftUseCase(this._repository); @override - Future call() { - return _repository.getTodaysShift(); + Future> call() { + return _repository.getTodaysShifts(); } } diff --git a/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/bloc/clock_in_bloc.dart b/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/bloc/clock_in_bloc.dart index e934e636..98c9a078 100644 --- a/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/bloc/clock_in_bloc.dart +++ b/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/bloc/clock_in_bloc.dart @@ -1,9 +1,10 @@ import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:geolocator/geolocator.dart'; +import 'package:krow_domain/krow_domain.dart'; import '../../domain/usecases/get_todays_shift_usecase.dart'; import '../../domain/usecases/get_attendance_status_usecase.dart'; import '../../domain/usecases/clock_in_usecase.dart'; import '../../domain/usecases/clock_out_usecase.dart'; -import '../../domain/usecases/get_activity_log_usecase.dart'; import '../../domain/arguments/clock_in_arguments.dart'; import '../../domain/arguments/clock_out_arguments.dart'; import 'clock_in_event.dart'; @@ -14,34 +15,31 @@ class ClockInBloc extends Bloc { final GetAttendanceStatusUseCase _getAttendanceStatus; final ClockInUseCase _clockIn; final ClockOutUseCase _clockOut; - final GetActivityLogUseCase _getActivityLog; + + // Mock Venue Location (e.g., Grand Hotel, NYC) + static const double allowedRadiusMeters = 500; ClockInBloc({ required GetTodaysShiftUseCase getTodaysShift, required GetAttendanceStatusUseCase getAttendanceStatus, required ClockInUseCase clockIn, required ClockOutUseCase clockOut, - required GetActivityLogUseCase getActivityLog, }) : _getTodaysShift = getTodaysShift, _getAttendanceStatus = getAttendanceStatus, _clockIn = clockIn, _clockOut = clockOut, - _getActivityLog = getActivityLog, super(ClockInState(selectedDate: DateTime.now())) { on(_onLoaded); + on(_onShiftSelected); on(_onDateSelected); on(_onCheckIn); on(_onCheckOut); on(_onModeChanged); - } + on(_onRequestLocationPermission); + on(_onCommuteModeToggled); + on(_onLocationUpdated); - AttendanceStatus _mapToStatus(Map map) { - return AttendanceStatus( - isCheckedIn: map['isCheckedIn'] as bool? ?? false, - checkInTime: map['checkInTime'] as DateTime?, - checkOutTime: map['checkOutTime'] as DateTime?, - activeShiftId: map['activeShiftId'] as String?, - ); + add(ClockInPageLoaded()); } Future _onLoaded( @@ -50,16 +48,33 @@ class ClockInBloc extends Bloc { ) async { emit(state.copyWith(status: ClockInStatus.loading)); try { - final shift = await _getTodaysShift(); - final statusMap = await _getAttendanceStatus(); - final activity = await _getActivityLog(); + final shifts = await _getTodaysShift(); + final status = await _getAttendanceStatus(); + + // Check permissions silently on load? Maybe better to wait for user interaction or specific event + // However, if shift exists, we might want to check permission state + Shift? selectedShift; + if (shifts.isNotEmpty) { + if (status.activeShiftId != null) { + try { + selectedShift = + shifts.firstWhere((s) => s.id == status.activeShiftId); + } catch (_) {} + } + selectedShift ??= shifts.last; + } emit(state.copyWith( status: ClockInStatus.success, - todayShift: shift, - attendance: _mapToStatus(statusMap), - activityLog: activity, + todayShifts: shifts, + selectedShift: selectedShift, + attendance: status, )); + + if (selectedShift != null && !status.isCheckedIn) { + add(RequestLocationPermission()); + } + } catch (e) { emit(state.copyWith( status: ClockInStatus.failure, @@ -68,6 +83,91 @@ class ClockInBloc extends Bloc { } } + Future _onRequestLocationPermission( + RequestLocationPermission event, + Emitter emit, + ) async { + try { + LocationPermission permission = await Geolocator.checkPermission(); + if (permission == LocationPermission.denied) { + permission = await Geolocator.requestPermission(); + } + + final hasConsent = permission == LocationPermission.always || permission == LocationPermission.whileInUse; + + emit(state.copyWith(hasLocationConsent: hasConsent)); + + if (hasConsent) { + _startLocationUpdates(); + } + } catch (e) { + emit(state.copyWith(errorMessage: "Location error: $e")); + } + } + + void _startLocationUpdates() async { + try { + final position = await Geolocator.getCurrentPosition(desiredAccuracy: LocationAccuracy.high); + + double distance = 0; + bool isVerified = false; // Require location match by default if shift has location + + if (state.selectedShift != null && + state.selectedShift!.latitude != null && + state.selectedShift!.longitude != null) { + distance = Geolocator.distanceBetween( + position.latitude, + position.longitude, + state.selectedShift!.latitude!, + state.selectedShift!.longitude!, + ); + isVerified = distance <= allowedRadiusMeters; + } else { + // If no shift location, assume verified or don't restrict? + // For strict clock-in, maybe false? but let's default to verified to avoid blocking if data missing + isVerified = true; + } + + if (!isClosed) { + add(LocationUpdated(position: position, distance: distance, isVerified: isVerified)); + } + } catch (e) { + // Handle error silently or via state + } + } + + void _onLocationUpdated( + LocationUpdated event, + Emitter emit, + ) { + emit(state.copyWith( + currentLocation: event.position, + distanceFromVenue: event.distance, + isLocationVerified: event.isVerified, + etaMinutes: (event.distance / 80).round(), // Rough estimate: 80m/min walking speed + )); + } + + void _onCommuteModeToggled( + CommuteModeToggled event, + Emitter emit, + ) { + emit(state.copyWith(isCommuteModeOn: event.isEnabled)); + if (event.isEnabled) { + add(RequestLocationPermission()); + } + } + + void _onShiftSelected( + ShiftSelected event, + Emitter emit, + ) { + emit(state.copyWith(selectedShift: event.shift)); + if (!state.attendance.isCheckedIn) { + _startLocationUpdates(); + } + } + void _onDateSelected( DateSelected event, Emitter emit, @@ -86,14 +186,21 @@ class ClockInBloc extends Bloc { CheckInRequested event, Emitter emit, ) async { + // Only verify location if not using NFC (or depending on requirements) - enforcing for swipe + if (state.checkInMode == 'swipe' && !state.isLocationVerified) { + // Allow for now since coordinates are hardcoded and might not match user location + // emit(state.copyWith(errorMessage: "You must be at the location to clock in.")); + // return; + } + emit(state.copyWith(status: ClockInStatus.actionInProgress)); try { - final newStatusMap = await _clockIn( + final newStatus = await _clockIn( ClockInArguments(shiftId: event.shiftId, notes: event.notes), ); emit(state.copyWith( status: ClockInStatus.success, - attendance: _mapToStatus(newStatusMap), + attendance: newStatus, )); } catch (e) { emit(state.copyWith( @@ -109,15 +216,16 @@ class ClockInBloc extends Bloc { ) async { emit(state.copyWith(status: ClockInStatus.actionInProgress)); try { - final newStatusMap = await _clockOut( + final newStatus = await _clockOut( ClockOutArguments( notes: event.notes, breakTimeMinutes: 0, // Should be passed from event if supported + applicationId: state.attendance.activeApplicationId, ), ); emit(state.copyWith( status: ClockInStatus.success, - attendance: _mapToStatus(newStatusMap), + attendance: newStatus, )); } catch (e) { emit(state.copyWith( diff --git a/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/bloc/clock_in_event.dart b/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/bloc/clock_in_event.dart index d35647fb..acc5c13c 100644 --- a/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/bloc/clock_in_event.dart +++ b/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/bloc/clock_in_event.dart @@ -1,4 +1,6 @@ import 'package:equatable/equatable.dart'; +import 'package:geolocator/geolocator.dart'; +import 'package:krow_domain/krow_domain.dart'; abstract class ClockInEvent extends Equatable { const ClockInEvent(); @@ -9,6 +11,14 @@ abstract class ClockInEvent extends Equatable { class ClockInPageLoaded extends ClockInEvent {} +class ShiftSelected extends ClockInEvent { + final Shift shift; + const ShiftSelected(this.shift); + + @override + List get props => [shift]; +} + class DateSelected extends ClockInEvent { final DateTime date; @@ -46,3 +56,25 @@ class CheckInModeChanged extends ClockInEvent { @override List get props => [mode]; } + +class CommuteModeToggled extends ClockInEvent { + final bool isEnabled; + + const CommuteModeToggled(this.isEnabled); + + @override + List get props => [isEnabled]; +} + +class RequestLocationPermission extends ClockInEvent {} + +class LocationUpdated extends ClockInEvent { + final Position position; + final double distance; + final bool isVerified; + + const LocationUpdated({required this.position, required this.distance, required this.isVerified}); + + @override + List get props => [position, distance, isVerified]; +} diff --git a/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/bloc/clock_in_state.dart b/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/bloc/clock_in_state.dart index 8e6fe30c..dc01582e 100644 --- a/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/bloc/clock_in_state.dart +++ b/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/bloc/clock_in_state.dart @@ -1,73 +1,88 @@ import 'package:equatable/equatable.dart'; import 'package:krow_domain/krow_domain.dart'; +import 'package:geolocator/geolocator.dart'; + enum ClockInStatus { initial, loading, success, failure, actionInProgress } -/// View model representing the user's current attendance state. -class AttendanceStatus extends Equatable { - final bool isCheckedIn; - final DateTime? checkInTime; - final DateTime? checkOutTime; - final String? activeShiftId; - - const AttendanceStatus({ - this.isCheckedIn = false, - this.checkInTime, - this.checkOutTime, - this.activeShiftId, - }); - - @override - List get props => [isCheckedIn, checkInTime, checkOutTime, activeShiftId]; -} - class ClockInState extends Equatable { final ClockInStatus status; - final Shift? todayShift; + final List todayShifts; + final Shift? selectedShift; final AttendanceStatus attendance; - final List> activityLog; final DateTime selectedDate; final String checkInMode; final String? errorMessage; + final Position? currentLocation; + final double? distanceFromVenue; + final bool isLocationVerified; + final bool isCommuteModeOn; + final bool hasLocationConsent; + final int? etaMinutes; + const ClockInState({ this.status = ClockInStatus.initial, - this.todayShift, + this.todayShifts = const [], + this.selectedShift, this.attendance = const AttendanceStatus(), - this.activityLog = const [], required this.selectedDate, this.checkInMode = 'swipe', this.errorMessage, + this.currentLocation, + this.distanceFromVenue, + this.isLocationVerified = false, + this.isCommuteModeOn = false, + this.hasLocationConsent = false, + this.etaMinutes, }); ClockInState copyWith({ ClockInStatus? status, - Shift? todayShift, + List? todayShifts, + Shift? selectedShift, AttendanceStatus? attendance, - List>? activityLog, DateTime? selectedDate, String? checkInMode, String? errorMessage, + Position? currentLocation, + double? distanceFromVenue, + bool? isLocationVerified, + bool? isCommuteModeOn, + bool? hasLocationConsent, + int? etaMinutes, }) { return ClockInState( status: status ?? this.status, - todayShift: todayShift ?? this.todayShift, + todayShifts: todayShifts ?? this.todayShifts, + selectedShift: selectedShift ?? this.selectedShift, attendance: attendance ?? this.attendance, - activityLog: activityLog ?? this.activityLog, selectedDate: selectedDate ?? this.selectedDate, checkInMode: checkInMode ?? this.checkInMode, errorMessage: errorMessage, + currentLocation: currentLocation ?? this.currentLocation, + distanceFromVenue: distanceFromVenue ?? this.distanceFromVenue, + isLocationVerified: isLocationVerified ?? this.isLocationVerified, + isCommuteModeOn: isCommuteModeOn ?? this.isCommuteModeOn, + hasLocationConsent: hasLocationConsent ?? this.hasLocationConsent, + etaMinutes: etaMinutes ?? this.etaMinutes, ); } @override List get props => [ status, - todayShift, + todayShifts, + selectedShift, attendance, - activityLog, selectedDate, checkInMode, errorMessage, + currentLocation, + distanceFromVenue, + isLocationVerified, + isCommuteModeOn, + hasLocationConsent, + etaMinutes, ]; } diff --git a/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/blocs/clock_in_cubit.dart b/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/blocs/clock_in_cubit.dart new file mode 100644 index 00000000..366b1652 --- /dev/null +++ b/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/blocs/clock_in_cubit.dart @@ -0,0 +1,156 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:geolocator/geolocator.dart'; +import 'package:permission_handler/permission_handler.dart'; + +// --- State --- +class ClockInState extends Equatable { + final bool isLoading; + final bool isLocationVerified; + final String? error; + final Position? currentLocation; + final double? distanceFromVenue; + final bool isClockedIn; + final DateTime? clockInTime; + + const ClockInState({ + this.isLoading = false, + this.isLocationVerified = false, + this.error, + this.currentLocation, + this.distanceFromVenue, + this.isClockedIn = false, + this.clockInTime, + }); + + ClockInState copyWith({ + bool? isLoading, + bool? isLocationVerified, + String? error, + Position? currentLocation, + double? distanceFromVenue, + bool? isClockedIn, + DateTime? clockInTime, + }) { + return ClockInState( + isLoading: isLoading ?? this.isLoading, + isLocationVerified: isLocationVerified ?? this.isLocationVerified, + error: error, // Clear error if not provided + currentLocation: currentLocation ?? this.currentLocation, + distanceFromVenue: distanceFromVenue ?? this.distanceFromVenue, + isClockedIn: isClockedIn ?? this.isClockedIn, + clockInTime: clockInTime ?? this.clockInTime, + ); + } + + @override + List get props => [ + isLoading, + isLocationVerified, + error, + currentLocation, + distanceFromVenue, + isClockedIn, + clockInTime, + ]; +} + +// --- Cubit --- +class ClockInCubit extends Cubit { + // Mock Venue Location (e.g., Grand Hotel, NYC) + static const double venueLat = 40.7128; + static const double venueLng = -74.0060; + static const double allowedRadiusMeters = 500; // 500m radius + + ClockInCubit() : super(const ClockInState()); + + Future checkLocationPermission() async { + emit(state.copyWith(isLoading: true, error: null)); + try { + LocationPermission permission = await Geolocator.checkPermission(); + if (permission == LocationPermission.denied) { + permission = await Geolocator.requestPermission(); + if (permission == LocationPermission.denied) { + emit(state.copyWith( + isLoading: false, + error: 'Location permissions are denied', + )); + return; + } + } + + if (permission == LocationPermission.deniedForever) { + emit(state.copyWith( + isLoading: false, + error: 'Location permissions are permanently denied, we cannot request permissions.', + )); + return; + } + + _getCurrentLocation(); + } catch (e) { + emit(state.copyWith(isLoading: false, error: e.toString())); + } + } + + Future _getCurrentLocation() async { + try { + final position = await Geolocator.getCurrentPosition( + desiredAccuracy: LocationAccuracy.high, + ); + + final distance = Geolocator.distanceBetween( + position.latitude, + position.longitude, + venueLat, + venueLng, + ); + + final isWithinRadius = distance <= allowedRadiusMeters; + + emit(state.copyWith( + isLoading: false, + currentLocation: position, + distanceFromVenue: distance, + isLocationVerified: isWithinRadius, + error: isWithinRadius ? null : 'You are ${distance.toStringAsFixed(0)}m away. You must be within ${allowedRadiusMeters}m.', + )); + } catch (e) { + emit(state.copyWith(isLoading: false, error: 'Failed to get location: $e')); + } + } + + Future clockIn() async { + if (state.currentLocation == null) { + await checkLocationPermission(); + if (state.currentLocation == null) return; + } + + emit(state.copyWith(isLoading: true)); + + await Future.delayed(const Duration(seconds: 2)); + + emit(state.copyWith( + isLoading: false, + isClockedIn: true, + clockInTime: DateTime.now(), + )); + } + + Future clockOut() async { + if (state.currentLocation == null) { + await checkLocationPermission(); + if (state.currentLocation == null) return; + } + + emit(state.copyWith(isLoading: true)); + + await Future.delayed(const Duration(seconds: 2)); + + emit(state.copyWith( + isLoading: false, + isClockedIn: false, + clockInTime: null, + )); + } +} diff --git a/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/pages/clock_in_page.dart b/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/pages/clock_in_page.dart index 1ca438d7..f04cc1a5 100644 --- a/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/pages/clock_in_page.dart +++ b/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/pages/clock_in_page.dart @@ -1,18 +1,19 @@ +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:lucide_icons/lucide_icons.dart'; import 'package:intl/intl.dart'; import 'package:krow_domain/krow_domain.dart'; -import '../theme/app_colors.dart'; +import 'package:lucide_icons/lucide_icons.dart'; + import '../bloc/clock_in_bloc.dart'; import '../bloc/clock_in_event.dart'; import '../bloc/clock_in_state.dart'; -import '../widgets/attendance_card.dart'; -import '../widgets/date_selector.dart'; -import '../widgets/swipe_to_check_in.dart'; -import '../widgets/lunch_break_modal.dart'; +import '../theme/app_colors.dart'; import '../widgets/commute_tracker.dart'; +import '../widgets/date_selector.dart'; +import '../widgets/lunch_break_modal.dart'; +import '../widgets/swipe_to_check_in.dart'; class ClockInPage extends StatefulWidget { const ClockInPage({super.key}); @@ -28,32 +29,40 @@ class _ClockInPageState extends State { void initState() { super.initState(); _bloc = Modular.get(); - _bloc.add(ClockInPageLoaded()); } @override Widget build(BuildContext context) { - return BlocProvider.value( + return BlocProvider.value( value: _bloc, child: BlocConsumer( listener: (context, state) { - if (state.status == ClockInStatus.failure && state.errorMessage != null) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(state.errorMessage!)), - ); + if (state.status == ClockInStatus.failure && + state.errorMessage != null) { + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(state.errorMessage!))); } }, builder: (context, state) { - if (state.status == ClockInStatus.loading && state.todayShift == null) { + if (state.status == ClockInStatus.loading && + state.todayShifts.isEmpty) { return const Scaffold( body: Center(child: CircularProgressIndicator()), ); } - final todayShift = state.todayShift; - final checkInTime = state.attendance.checkInTime; - final checkOutTime = state.attendance.checkOutTime; - final isCheckedIn = state.attendance.isCheckedIn; + final todayShifts = state.todayShifts; + final selectedShift = state.selectedShift; + final activeShiftId = state.attendance.activeShiftId; + final bool isActiveSelected = + selectedShift != null && selectedShift.id == activeShiftId; + final checkInTime = + isActiveSelected ? state.attendance.checkInTime : null; + final checkOutTime = + isActiveSelected ? state.attendance.checkOutTime : null; + final isCheckedIn = + state.attendance.isCheckedIn && isActiveSelected; // Format times for display final checkInStr = checkInTime != null @@ -64,260 +73,224 @@ class _ClockInPageState extends State { : '--:-- --'; return Scaffold( - body: Container( - decoration: const BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - Color(0xFFF8FAFC), // slate-50 - Colors.white, - ], - ), + appBar: UiAppBar( + titleWidget: Text( + 'Clock In to your Shift', + style: UiTypography.title1m.textPrimary, ), - child: SafeArea( - child: SingleChildScrollView( - padding: const EdgeInsets.only(bottom: 100), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildHeader(), - - Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: Column( - children: [ - // Commute Tracker (shows before date selector when applicable) - if (todayShift != null) - CommuteTracker( - shift: todayShift, - hasLocationConsent: false, // Mock value - isCommuteModeOn: false, // Mock value - distanceMeters: 500, // Mock value for demo - etaMinutes: 8, // Mock value for demo - ), - - // Date Selector - DateSelector( - selectedDate: state.selectedDate, - onSelect: (date) => _bloc.add(DateSelected(date)), - shiftDates: [ - DateFormat('yyyy-MM-dd').format(DateTime.now()), - ], + showBackButton: false, + centerTitle: false, + ), + body: SafeArea( + child: SingleChildScrollView( + padding: const EdgeInsets.only( + bottom: UiConstants.space24, + top: UiConstants.space6, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Commute Tracker (shows before date selector when applicable) + if (selectedShift != null) + CommuteTracker( + shift: selectedShift, + hasLocationConsent: state.hasLocationConsent, + isCommuteModeOn: state.isCommuteModeOn, + distanceMeters: state.distanceFromVenue, + etaMinutes: state.etaMinutes, + onCommuteToggled: (value) { + _bloc.add(CommuteModeToggled(value)); + }, ), - const SizedBox(height: 20), + // Date Selector + DateSelector( + selectedDate: state.selectedDate, + onSelect: (date) => _bloc.add(DateSelected(date)), + shiftDates: [ + DateFormat('yyyy-MM-dd').format(DateTime.now()), + ], + ), + const SizedBox(height: 20), - // Today Attendance Section - const Align( - alignment: Alignment.centerLeft, - child: Text( - "Today Attendance", - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: AppColors.krowCharcoal, - ), - ), - ), - const SizedBox(height: 12), - GridView.count( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - crossAxisCount: 2, - mainAxisSpacing: 12, - crossAxisSpacing: 12, - childAspectRatio: 1.0, - children: [ - AttendanceCard( - type: AttendanceType.checkin, - title: "Check In", - value: checkInStr, - subtitle: checkInTime != null - ? "On Time" - : "Pending", - scheduledTime: "09:00 AM", - ), - AttendanceCard( - type: AttendanceType.checkout, - title: "Check Out", - value: checkOutStr, - subtitle: checkOutTime != null - ? "Go Home" - : "Pending", - scheduledTime: "05:00 PM", - ), - AttendanceCard( - type: AttendanceType.breaks, - title: "Break Time", - value: "00:30 min", - subtitle: "Scheduled 00:30 min", - ), - const AttendanceCard( - type: AttendanceType.days, - title: "Total Days", - value: "28", - subtitle: "Working Days", - ), - ], - ), - const SizedBox(height: 24), - // Your Activity Header - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text( - "Your Activity", - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: AppColors.krowCharcoal, - ), - ), - GestureDetector( - onTap: () { - debugPrint('Navigating to shifts...'); - }, - child: const Row( - children: [ - Text( - "View all", - style: TextStyle( - color: AppColors.krowBlue, - fontWeight: FontWeight.w500, - ), - ), - Icon( - LucideIcons.chevronRight, - size: 16, - color: AppColors.krowBlue, - ), - ], - ), - ), - ], + // Your Activity Header + const Text( + "Your Activity", + textAlign: TextAlign.start, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: AppColors.krowCharcoal, ), - const SizedBox(height: 12), + ), - // Check-in Mode Toggle - const Align( - alignment: Alignment.centerLeft, - child: Text( - "Check-in Method", - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - color: Color(0xFF334155), // slate-700 - ), - ), - ), - const SizedBox(height: 8), - Container( - padding: const EdgeInsets.all(4), - decoration: BoxDecoration( - color: const Color(0xFFF1F5F9), // slate-100 - borderRadius: BorderRadius.circular(12), - ), - child: Row( - children: [ - _buildModeTab("Swipe", LucideIcons.mapPin, 'swipe', state.checkInMode), - _buildModeTab("NFC Tap", LucideIcons.wifi, 'nfc', state.checkInMode), - ], - ), - ), - const SizedBox(height: 16), + const SizedBox(height: 16), - // Selected Shift Info Card - if (todayShift != null) - Container( - padding: const EdgeInsets.all(12), - margin: const EdgeInsets.only(bottom: 16), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: const Color(0xFFE2E8F0), - ), // slate-200 - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.05), - blurRadius: 2, - offset: const Offset(0, 1), - ), - ], - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - "TODAY'S SHIFT", - style: TextStyle( - fontSize: 10, - fontWeight: FontWeight.w600, - color: AppColors.krowBlue, - letterSpacing: 0.5, - ), + // Selected Shift Info Card + if (todayShifts.isNotEmpty) + Column( + children: todayShifts + .map( + (shift) => GestureDetector( + onTap: () => + _bloc.add(ShiftSelected(shift)), + child: Container( + padding: const EdgeInsets.all(12), + margin: + const EdgeInsets.only(bottom: 12), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular( + 12, ), - const SizedBox(height: 2), - Text( - todayShift.title, - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w600, - color: Color(0xFF1E293B), // slate-800 - ), - ), - Text( - "${todayShift.clientName} • ${todayShift.location}", - style: const TextStyle( - fontSize: 12, - color: Color(0xFF64748B), // slate-500 - ), - ), - ], - ), - ), - Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - const Text( - "9:00 AM - 5:00 PM", - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w500, - color: Color(0xFF475569), // slate-600 + border: Border.all( + color: shift.id == + selectedShift?.id + ? AppColors.krowBlue + : const Color(0xFFE2E8F0), + width: + shift.id == selectedShift?.id + ? 2 + : 1, ), ), - Text( - "\$${todayShift.hourlyRate}/hr", - style: const TextStyle( - fontSize: 12, - fontWeight: FontWeight.w600, - color: AppColors.krowBlue, - ), + child: Row( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + shift.id == + selectedShift?.id + ? "SELECTED SHIFT" + : "TODAY'S SHIFT", + style: TextStyle( + fontSize: 10, + fontWeight: + FontWeight.w600, + color: shift.id == + selectedShift?.id + ? AppColors.krowBlue + : AppColors + .krowCharcoal, + letterSpacing: 0.5, + ), + ), + const SizedBox(height: 2), + Text( + shift.title, + style: const TextStyle( + fontSize: 14, + fontWeight: + FontWeight.w600, + color: Color(0xFF1E293B), + ), + ), + Text( + "${shift.clientName} • ${shift.location}", + style: const TextStyle( + fontSize: 12, + color: Color(0xFF64748B), + ), + ), + ], + ), + ), + Column( + crossAxisAlignment: + CrossAxisAlignment.end, + children: [ + Text( + "${_formatTime(shift.startTime)} - ${_formatTime(shift.endTime)}", + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + color: Color(0xFF475569), + ), + ), + Text( + "\$${shift.hourlyRate}/hr", + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: AppColors.krowBlue, + ), + ), + ], + ), + ], ), - ], + ), ), - ], - ), - ), + ) + .toList(), + ), - // Swipe To Check In / Checked Out State / No Shift State - if (todayShift != null && checkOutTime == null) ...[ - SwipeToCheckIn( + // Swipe To Check In / Checked Out State / No Shift State + if (selectedShift != null && checkOutTime == null) ...[ + if (!isCheckedIn && + !_isCheckInAllowed(selectedShift)) + Container( + width: double.infinity, + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + color: const Color(0xFFF1F5F9), // slate-100 + borderRadius: BorderRadius.circular(16), + ), + child: Column( + children: [ + const Icon( + LucideIcons.clock, + size: 48, + color: Color(0xFF94A3B8), // slate-400 + ), + const SizedBox(height: 16), + const Text( + "You're early!", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Color(0xFF475569), // slate-600 + ), + ), + const SizedBox(height: 4), + Text( + "Check-in available at ${_getCheckInAvailabilityTime(selectedShift)}", + style: const TextStyle( + fontSize: 14, + color: Color(0xFF64748B), // slate-500 + ), + textAlign: TextAlign.center, + ), + ], + ), + ) + else + SwipeToCheckIn( isCheckedIn: isCheckedIn, mode: state.checkInMode, - isLoading: state.status == ClockInStatus.actionInProgress, + isLoading: + state.status == + ClockInStatus.actionInProgress, onCheckIn: () async { // Show NFC dialog if mode is 'nfc' if (state.checkInMode == 'nfc') { await _showNFCDialog(context); } else { - _bloc.add(CheckInRequested(shiftId: todayShift.id)); + _bloc.add( + CheckInRequested( + shiftId: selectedShift.id, + ), + ); } }, onCheckOut: () { @@ -325,216 +298,161 @@ class _ClockInPageState extends State { context: context, builder: (context) => LunchBreakDialog( onComplete: () { - Navigator.of(context).pop(); // Close dialog first + Navigator.of( + context, + ).pop(); // Close dialog first _bloc.add(const CheckOutRequested()); }, ), ); }, ), - ] else if (todayShift != null && checkOutTime != null) ...[ - // Shift Completed State - Container( - padding: const EdgeInsets.all(24), - decoration: BoxDecoration( - color: const Color(0xFFECFDF5), // emerald-50 - borderRadius: BorderRadius.circular(16), - border: Border.all( - color: const Color(0xFFA7F3D0), - ), // emerald-200 - ), - child: Column( - children: [ - Container( - width: 48, - height: 48, - decoration: const BoxDecoration( - color: Color(0xFFD1FAE5), // emerald-100 - shape: BoxShape.circle, - ), - child: const Icon( - LucideIcons.check, - color: Color(0xFF059669), // emerald-600 - size: 24, - ), - ), - const SizedBox(height: 12), - const Text( - "Shift Completed!", - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - color: Color(0xFF065F46), // emerald-800 - ), - ), - const SizedBox(height: 4), - const Text( - "Great work today", - style: TextStyle( - fontSize: 14, - color: Color(0xFF059669), // emerald-600 - ), - ), - ], - ), + ] else if (selectedShift != null && + checkOutTime != null) ...[ + // Shift Completed State + Container( + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + color: const Color(0xFFECFDF5), // emerald-50 + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: const Color(0xFFA7F3D0), + ), // emerald-200 ), - ] else ...[ - // No Shift State - Container( - padding: const EdgeInsets.all(24), - decoration: BoxDecoration( - color: const Color(0xFFF1F5F9), // slate-100 - borderRadius: BorderRadius.circular(16), - ), - child: Column( - children: [ - const Text( - "No confirmed shifts for today", - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - color: Color(0xFF475569), // slate-600 - ), - textAlign: TextAlign.center, + child: Column( + children: [ + Container( + width: 48, + height: 48, + decoration: const BoxDecoration( + color: Color(0xFFD1FAE5), // emerald-100 + shape: BoxShape.circle, ), - const SizedBox(height: 4), - const Text( - "Accept a shift to clock in", - style: TextStyle( - fontSize: 14, - color: Color(0xFF64748B), // slate-500 - ), - textAlign: TextAlign.center, + child: const Icon( + LucideIcons.check, + color: Color(0xFF059669), // emerald-600 + size: 24, ), - ], - ), + ), + const SizedBox(height: 12), + const Text( + "Shift Completed!", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Color(0xFF065F46), // emerald-800 + ), + ), + const SizedBox(height: 4), + const Text( + "Great work today", + style: TextStyle( + fontSize: 14, + color: Color(0xFF059669), // emerald-600 + ), + ), + ], ), - ], - - // Checked In Banner - if (isCheckedIn && checkInTime != null) ...[ - const SizedBox(height: 12), - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: const Color(0xFFECFDF5), // emerald-50 - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: const Color(0xFFA7F3D0), - ), // emerald-200 - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - "Checked in at", - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w500, - color: Color(0xFF059669), - ), - ), - Text( - DateFormat('h:mm a').format(checkInTime), - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: Color(0xFF065F46), - ), - ), - ], - ), - Container( - width: 40, - height: 40, - decoration: const BoxDecoration( - color: Color(0xFFD1FAE5), - shape: BoxShape.circle, - ), - child: const Icon( - LucideIcons.check, - color: Color(0xFF059669), - ), - ), - ], - ), + ), + ] else ...[ + // No Shift State + Container( + width: double.infinity, + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + color: const Color(0xFFF1F5F9), // slate-100 + borderRadius: BorderRadius.circular(16), ), - ], - - const SizedBox(height: 16), - - // Recent Activity List - ...state.activityLog.map( - (activity) => Container( - margin: const EdgeInsets.only(bottom: 12), - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: const Color(0xFFF8F9FA), - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: const Color(0xFFF1F5F9), - ), // slate-100 - ), - child: Row( - children: [ - Container( - width: 40, - height: 40, - decoration: BoxDecoration( - color: AppColors.krowBlue.withOpacity(0.1), - borderRadius: BorderRadius.circular(12), - ), - child: const Icon( - LucideIcons.mapPin, - color: AppColors.krowBlue, - size: 20, - ), + child: const Column( + children: [ + const Text( + "No confirmed shifts for today", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Color(0xFF475569), // slate-600 ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - DateFormat( - 'MMM d', - ).format(activity['date'] as DateTime), - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - color: Color(0xFF0F172A), // slate-900 - ), - ), - Text( - "${activity['start']} - ${activity['end']}", - style: const TextStyle( - fontSize: 12, - color: Color(0xFF64748B), // slate-500 - ), - ), - ], - ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 4), + const Text( + "Accept a shift to clock in", + style: TextStyle( + fontSize: 14, + color: Color(0xFF64748B), // slate-500 ), - Text( - activity['hours'] as String, - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w600, - color: AppColors.krowBlue, - ), - ), - ], - ), + textAlign: TextAlign.center, + ), + ], ), ), ], - ), + + // Checked In Banner + if (isCheckedIn && checkInTime != null) ...[ + const SizedBox(height: 12), + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: const Color(0xFFECFDF5), // emerald-50 + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: const Color(0xFFA7F3D0), + ), // emerald-200 + ), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + const Text( + "Checked in at", + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + color: Color(0xFF059669), + ), + ), + Text( + DateFormat( + 'h:mm a', + ).format(checkInTime), + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Color(0xFF065F46), + ), + ), + ], + ), + Container( + width: 40, + height: 40, + decoration: const BoxDecoration( + color: Color(0xFFD1FAE5), + shape: BoxShape.circle, + ), + child: const Icon( + LucideIcons.check, + color: Color(0xFF059669), + ), + ), + ], + ), + ), + ], + + const SizedBox(height: 16), + + // Recent Activity List (Temporarily removed) + const SizedBox(height: 16), + ], ), - ], - ), + ), + ], ), ), ), @@ -544,7 +462,12 @@ class _ClockInPageState extends State { ); } - Widget _buildModeTab(String label, IconData icon, String value, String currentMode) { + Widget _buildModeTab( + String label, + IconData icon, + String value, + String currentMode, + ) { final isSelected = currentMode == value; return Expanded( child: GestureDetector( @@ -588,90 +511,9 @@ class _ClockInPageState extends State { ); } - Widget _buildHeader() { - return Padding( - padding: const EdgeInsets.fromLTRB(20, 24, 20, 16), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - Container( - width: 48, - height: 48, - decoration: BoxDecoration( - shape: BoxShape.circle, - border: Border.all( - color: AppColors.krowBlue.withOpacity(0.2), - width: 2, - ), - ), - child: CircleAvatar( - backgroundColor: AppColors.krowBlue.withOpacity(0.1), - child: const Text( - 'K', - style: TextStyle( - color: AppColors.krowBlue, - fontWeight: FontWeight.bold, - ), - ), - ), - ), - const SizedBox(width: 12), - const Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Good Morning', - style: TextStyle(color: AppColors.krowMuted, fontSize: 12), - ), - Text( - 'Krower', - style: TextStyle( - color: AppColors.krowCharcoal, - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - Text( - 'Warehouse Assistant', - style: TextStyle(color: AppColors.krowMuted, fontSize: 12), - ), - ], - ), - ], - ), - Container( - width: 40, - height: 40, - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular( - 20, - ), // Rounded full for this page per design - border: Border.all(color: Colors.grey.shade100), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.05), - blurRadius: 2, - offset: const Offset(0, 1), - ), - ], - ), - child: const Icon( - LucideIcons.bell, - color: AppColors.krowMuted, - size: 20, - ), - ), - ], - ), - ); - } - Future _showNFCDialog(BuildContext context) async { bool scanned = false; - + // Using a local navigator context since we are in a dialog await showDialog( context: context, @@ -764,11 +606,59 @@ class _ClockInPageState extends State { ); }, ); - + // After dialog closes, trigger the event if scan was successful (simulated) // In real app, we would check the dialog result - if (scanned && _bloc.state.todayShift != null) { - _bloc.add(CheckInRequested(shiftId: _bloc.state.todayShift!.id)); + if (scanned && _bloc.state.selectedShift != null) { + _bloc.add(CheckInRequested(shiftId: _bloc.state.selectedShift!.id)); } } + + // --- Helper Methods --- + + String _formatTime(String timeStr) { + if (timeStr.isEmpty) return ''; + try { + // Try parsing as ISO string first (which contains date) + final dt = DateTime.parse(timeStr); + return DateFormat('h:mm a').format(dt); + } catch (_) { + // Fallback for strict "HH:mm" or "HH:mm:ss" strings + try { + final parts = timeStr.split(':'); + if (parts.length >= 2) { + final dt = DateTime(2022, 1, 1, int.parse(parts[0]), int.parse(parts[1])); + return DateFormat('h:mm a').format(dt); + } + return timeStr; + } catch (e) { + return timeStr; + } + } + } + + bool _isCheckInAllowed(Shift shift) { + if (shift == null) return false; + try { + // Parse shift date (e.g. 2024-01-31T09:00:00) + // The Shift entity has 'date' which is the start DateTime string + final shiftStart = DateTime.parse(shift.startTime); + final windowStart = shiftStart.subtract(const Duration(minutes: 15)); + return DateTime.now().isAfter(windowStart); + } catch (e) { + // Fallback: If parsing fails, allow check in to avoid blocking. + return true; + } + } + + String _getCheckInAvailabilityTime(Shift shift) { + if (shift == null) return ''; + try { + final shiftStart = DateTime.parse(shift.startTime.trim()); + final windowStart = shiftStart.subtract(const Duration(minutes: 15)); + return DateFormat('h:mm a').format(windowStart); + } catch (e) { + return 'soon'; + } + } } diff --git a/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/widgets/attendance_card.dart b/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/widgets/attendance_card.dart index 4efc0f62..5b67effe 100644 --- a/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/widgets/attendance_card.dart +++ b/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/widgets/attendance_card.dart @@ -24,7 +24,7 @@ class AttendanceCard extends StatelessWidget { final styles = _getStyles(type); return Container( - padding: const EdgeInsets.all(16), + padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), @@ -39,31 +39,37 @@ class AttendanceCard extends StatelessWidget { ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, children: [ Container( - width: 36, - height: 36, + width: 32, + height: 32, decoration: BoxDecoration( color: styles.bgColor, - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.circular(8), ), child: Icon(styles.icon, size: 16, color: styles.iconColor), ), - const SizedBox(height: 12), + const SizedBox(height: 8), Text( title, style: const TextStyle( - fontSize: 12, + fontSize: 11, color: Color(0xFF64748B), // slate-500 ), + maxLines: 1, + overflow: TextOverflow.ellipsis, ), - const SizedBox(height: 4), - Text( - value, - style: const TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - color: Color(0xFF0F172A), // slate-900 + const SizedBox(height: 2), + FittedBox( + fit: BoxFit.scaleDown, + child: Text( + value, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Color(0xFF0F172A), // slate-900 + ), ), ), if (scheduledTime != null) ...[ diff --git a/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/widgets/commute_tracker.dart b/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/widgets/commute_tracker.dart index e9701fe0..f431b285 100644 --- a/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/widgets/commute_tracker.dart +++ b/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/widgets/commute_tracker.dart @@ -14,6 +14,7 @@ enum CommuteMode { class CommuteTracker extends StatefulWidget { final Shift? shift; final Function(CommuteMode)? onModeChange; + final ValueChanged? onCommuteToggled; final bool hasLocationConsent; final bool isCommuteModeOn; final double? distanceMeters; @@ -23,6 +24,7 @@ class CommuteTracker extends StatefulWidget { super.key, this.shift, this.onModeChange, + this.onCommuteToggled, this.hasLocationConsent = false, this.isCommuteModeOn = false, this.distanceMeters, @@ -44,14 +46,41 @@ class _CommuteTrackerState extends State { _localIsCommuteOn = widget.isCommuteModeOn; } + @override + void didUpdateWidget(CommuteTracker oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.isCommuteModeOn != oldWidget.isCommuteModeOn) { + setState(() { + _localIsCommuteOn = widget.isCommuteModeOn; + }); + } + if (widget.hasLocationConsent != oldWidget.hasLocationConsent) { + setState(() { + _localHasConsent = widget.hasLocationConsent; + }); + } + } + CommuteMode _getAppMode() { if (widget.shift == null) return CommuteMode.lockedNoShift; // For demo purposes, check if we're within 24 hours of shift final now = DateTime.now(); - final shiftStart = DateTime.parse( - '${widget.shift!.date} ${widget.shift!.startTime}', - ); + DateTime shiftStart; + try { + // Try parsing startTime as full datetime first + shiftStart = DateTime.parse(widget.shift!.startTime); + } catch (_) { + try { + // Try parsing date as full datetime + shiftStart = DateTime.parse(widget.shift!.date); + } catch (_) { + // Fall back to combining date and time + shiftStart = DateTime.parse( + '${widget.shift!.date} ${widget.shift!.startTime}', + ); + } + } final hoursUntilShift = shiftStart.difference(now).inHours; final inCommuteWindow = hoursUntilShift <= 24 && hoursUntilShift >= 0; @@ -82,9 +111,21 @@ class _CommuteTrackerState extends State { int _getMinutesUntilShift() { if (widget.shift == null) return 0; final now = DateTime.now(); - final shiftStart = DateTime.parse( - '${widget.shift!.date} ${widget.shift!.startTime}', - ); + DateTime shiftStart; + try { + // Try parsing startTime as full datetime first + shiftStart = DateTime.parse(widget.shift!.startTime); + } catch (_) { + try { + // Try parsing date as full datetime + shiftStart = DateTime.parse(widget.shift!.date); + } catch (_) { + // Fall back to combining date and time + shiftStart = DateTime.parse( + '${widget.shift!.date} ${widget.shift!.startTime}', + ); + } + } return shiftStart.difference(now).inMinutes; } @@ -299,6 +340,7 @@ class _CommuteTrackerState extends State { value: _localIsCommuteOn, onChanged: (value) { setState(() => _localIsCommuteOn = value); + widget.onCommuteToggled?.call(value); }, activeColor: AppColors.krowBlue, ), diff --git a/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/widgets/location_map_placeholder.dart b/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/widgets/location_map_placeholder.dart new file mode 100644 index 00000000..f0b482a1 --- /dev/null +++ b/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/widgets/location_map_placeholder.dart @@ -0,0 +1,97 @@ +import 'package:flutter/material.dart'; +import 'package:design_system/design_system.dart'; +import 'package:lucide_icons/lucide_icons.dart'; + +class LocationMapPlaceholder extends StatelessWidget { + final bool isVerified; + final double? distance; + + const LocationMapPlaceholder({ + super.key, + required this.isVerified, + this.distance, + }); + + @override + Widget build(BuildContext context) { + return Container( + height: 200, + width: double.infinity, + decoration: BoxDecoration( + color: const Color(0xFFE2E8F0), + borderRadius: BorderRadius.circular(16), + image: DecorationImage( + image: const NetworkImage( + 'https://maps.googleapis.com/maps/api/staticmap?center=40.7128,-74.0060&zoom=15&size=600x300&maptype=roadmap&markers=color:red%7C40.7128,-74.0060&key=YOUR_API_KEY', + ), + // In a real app with keys, this would verify visually. + // For now we use a generic placeholder color/icon to avoid broken images. + fit: BoxFit.cover, + onError: (_, __) {}, + ), + ), + child: Stack( + children: [ + // Fallback UI if image fails (which it will without key) + const Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(LucideIcons.mapPin, size: 48, color: UiColors.iconSecondary), + SizedBox(height: 8), + Text('Map View (GPS)', style: TextStyle(color: UiColors.textSecondary)), + ], + ), + ), + + // Status Overlay + Positioned( + bottom: 16, + left: 16, + right: 16, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 8, + offset: const Offset(0, 4), + ), + ], + ), + child: Row( + children: [ + Icon( + isVerified ? LucideIcons.checkCircle : LucideIcons.alertCircle, + color: isVerified ? UiColors.textSuccess : UiColors.destructive, + size: 20, + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + isVerified ? 'Location Verified' : 'Location Check', + style: UiTypography.body1b.copyWith(color: UiColors.textPrimary), + ), + if (distance != null) + Text( + '${distance!.toStringAsFixed(0)}m from venue', + style: UiTypography.body2r.copyWith(color: UiColors.textSecondary), + ), + ], + ), + ), + ], + ), + ), + ), + ], + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/widgets/lunch_break_modal.dart b/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/widgets/lunch_break_modal.dart index 3061de20..c0f1b897 100644 --- a/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/widgets/lunch_break_modal.dart +++ b/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/widgets/lunch_break_modal.dart @@ -106,26 +106,28 @@ class _LunchBreakDialogState extends State { Row( children: [ Expanded( - child: OutlinedButton( - onPressed: () { + child: GestureDetector( + onTap: () { setState(() { _tookLunch = false; _step = 102; // Go to No Lunch Reason }); }, - style: OutlinedButton.styleFrom( + child: Container( padding: const EdgeInsets.symmetric(vertical: 16), - side: BorderSide(color: Colors.grey.shade300), - shape: RoundedRectangleBorder( + decoration: BoxDecoration( + border: Border.all(color: Colors.grey.shade300), borderRadius: BorderRadius.circular(12), + color: Colors.transparent, ), - ), - child: const Text( - "No", - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - color: Color(0xFF121826), + alignment: Alignment.center, + child: const Text( + "No", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Color(0xFF121826), + ), ), ), ), @@ -180,19 +182,27 @@ class _LunchBreakDialogState extends State { children: [ Expanded( child: DropdownButtonFormField( + isExpanded: true, value: _breakStart, - items: _timeOptions.map((t) => DropdownMenuItem(value: t, child: Text(t))).toList(), + items: _timeOptions.map((t) => DropdownMenuItem(value: t, child: Text(t, style: const TextStyle(fontSize: 13)))).toList(), onChanged: (v) => setState(() => _breakStart = v), - decoration: const InputDecoration(labelText: 'Start'), + decoration: const InputDecoration( + labelText: 'Start', + contentPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 8), + ), ), ), - const SizedBox(width: 16), + const SizedBox(width: 10), Expanded( child: DropdownButtonFormField( + isExpanded: true, value: _breakEnd, - items: _timeOptions.map((t) => DropdownMenuItem(value: t, child: Text(t))).toList(), + items: _timeOptions.map((t) => DropdownMenuItem(value: t, child: Text(t, style: const TextStyle(fontSize: 13)))).toList(), onChanged: (v) => setState(() => _breakEnd = v), - decoration: const InputDecoration(labelText: 'End'), + decoration: const InputDecoration( + labelText: 'End', + contentPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 8), + ), ), ), ], diff --git a/apps/mobile/packages/features/staff/clock_in/lib/src/staff_clock_in_module.dart b/apps/mobile/packages/features/staff/clock_in/lib/src/staff_clock_in_module.dart index f7062597..1af0bce2 100644 --- a/apps/mobile/packages/features/staff/clock_in/lib/src/staff_clock_in_module.dart +++ b/apps/mobile/packages/features/staff/clock_in/lib/src/staff_clock_in_module.dart @@ -1,10 +1,10 @@ import 'package:flutter_modular/flutter_modular.dart'; import 'package:krow_data_connect/krow_data_connect.dart'; -import 'data/repositories/clock_in_repository_impl.dart'; + +import 'data/repositories_impl/clock_in_repository_impl.dart'; import 'domain/repositories/clock_in_repository_interface.dart'; import 'domain/usecases/clock_in_usecase.dart'; import 'domain/usecases/clock_out_usecase.dart'; -import 'domain/usecases/get_activity_log_usecase.dart'; import 'domain/usecases/get_attendance_status_usecase.dart'; import 'domain/usecases/get_todays_shift_usecase.dart'; import 'presentation/bloc/clock_in_bloc.dart'; @@ -13,18 +13,18 @@ import 'presentation/pages/clock_in_page.dart'; class StaffClockInModule extends Module { @override void binds(Injector i) { - // Data Sources (Mocks from data_connect) - i.add(ShiftsRepositoryMock.new); - // Repositories - i.add(ClockInRepositoryImpl.new); + i.add( + () => ClockInRepositoryImpl( + dataConnect: ExampleConnector.instance, + ), + ); // Use Cases i.add(GetTodaysShiftUseCase.new); i.add(GetAttendanceStatusUseCase.new); i.add(ClockInUseCase.new); i.add(ClockOutUseCase.new); - i.add(GetActivityLogUseCase.new); // BLoC i.add(ClockInBloc.new); diff --git a/apps/mobile/packages/features/staff/clock_in/pubspec.yaml b/apps/mobile/packages/features/staff/clock_in/pubspec.yaml index 47781140..28ceb6b9 100644 --- a/apps/mobile/packages/features/staff/clock_in/pubspec.yaml +++ b/apps/mobile/packages/features/staff/clock_in/pubspec.yaml @@ -5,7 +5,7 @@ publish_to: 'none' resolution: workspace environment: - sdk: '>=3.0.0 <4.0.0' + sdk: '>=3.10.0 <4.0.0' flutter: ">=1.17.0" dependencies: @@ -28,3 +28,7 @@ dependencies: path: ../../../data_connect krow_core: path: ../../../core + firebase_data_connect: ^0.2.2+2 + geolocator: ^10.1.0 + permission_handler: ^11.0.1 + firebase_auth: ^6.1.4 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 0ea0bd34..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 @@ -1,18 +1,150 @@ -import 'package:staff_home/src/domain/entities/shift.dart'; +import 'package:firebase_data_connect/firebase_data_connect.dart'; +import 'package:intl/intl.dart'; +import 'package:krow_data_connect/krow_data_connect.dart'; +import 'package:krow_domain/krow_domain.dart'; +import 'package:krow_core/core.dart'; import 'package:staff_home/src/domain/repositories/home_repository.dart'; -import 'package:staff_home/src/data/services/mock_service.dart'; + +extension TimestampExt on Timestamp { + DateTime toDate() { + return DateTimeUtils.toDeviceTime(toDateTime()); + } +} class HomeRepositoryImpl implements HomeRepository { - final MockService _service; + HomeRepositoryImpl(); - HomeRepositoryImpl(this._service); + String get _currentStaffId { + final session = StaffSessionStore.instance.session; + if (session?.staff?.id == null) throw Exception('User not logged in'); + return session!.staff!.id; + } @override - Future> getTodayShifts() => _service.getTodayShifts(); + Future> getTodayShifts() async { + return _getShiftsForDate(DateTime.now()); + } @override - Future> getTomorrowShifts() => _service.getTomorrowShifts(); + Future> getTomorrowShifts() async { + return _getShiftsForDate(DateTime.now().add(const Duration(days: 1))); + } + + 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: staffId) + .dayStart(_toTimestamp(start)) + .dayEnd(_toTimestamp(end)) + .execute(); + + // 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() => _service.getRecommendedShifts(); + Future> getRecommendedShifts() async { + try { + // Logic: List ALL open shifts (simple recommendation engine) + // Limitation: listShifts might return ALL shifts. We should ideally filter by status=PUBLISHED. + final response = await ExampleConnector.instance.listShifts().execute(); + + return response.data.shifts + .where((s) { + final isOpen = s.status is Known && (s.status as Known).value == ShiftStatus.OPEN; + if (!isOpen) return false; + + final start = s.startTime?.toDate(); + if (start == null) return false; + + return start.isAfter(DateTime.now()); + }) + .take(10) + .map((s) => _mapConnectorShiftToDomain(s)) + .toList(); + } catch (e) { + return []; + } + } + + @override + Future getStaffName() async { + final session = StaffSessionStore.instance.session; + return session?.staff?.name; + } + + // Mappers specific to Home's Domain Entity 'Shift' + // Note: Home's 'Shift' entity might differ slightly from 'StaffPayment' Shift. + + Shift _mapApplicationToShift(GetApplicationsByStaffIdApplications app) { + final s = app.shift; + final r = app.shiftRole; + + 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, + ); + } + + Shift _mapConnectorShiftToDomain(ListShiftsShifts s) { + return Shift( + id: s.id, + title: s.title, + clientName: s.order.business.businessName, + hourlyRate: s.cost ?? 0.0, + location: s.location ?? 'Unknown', + locationAddress: s.locationAddress ?? '', + date: s.date?.toDate().toIso8601String() ?? '', + startTime: DateFormat('HH:mm').format(s.startTime?.toDate() ?? DateTime.now()), + endTime: DateFormat('HH:mm').format(s.endTime?.toDate() ?? DateTime.now()), + createdDate: s.createdAt?.toDate().toIso8601String() ?? '', + tipsAvailable: false, + mealProvided: false, + managers: [], + description: s.description, + ); + } } + diff --git a/apps/mobile/packages/features/staff/home/lib/src/data/services/mock_service.dart b/apps/mobile/packages/features/staff/home/lib/src/data/services/mock_service.dart deleted file mode 100644 index 89dca0f8..00000000 --- a/apps/mobile/packages/features/staff/home/lib/src/data/services/mock_service.dart +++ /dev/null @@ -1,76 +0,0 @@ -import 'package:staff_home/src/domain/entities/shift.dart'; - -class MockService { - static final Shift _sampleShift1 = Shift( - id: '1', - title: 'Line Cook', - clientName: 'The Burger Joint', - hourlyRate: 22.50, - location: 'Downtown, NY', - locationAddress: '123 Main St, New York, NY 10001', - date: DateTime.now().toIso8601String(), - startTime: '16:00', - endTime: '22:00', - createdDate: DateTime.now() - .subtract(const Duration(hours: 2)) - .toIso8601String(), - tipsAvailable: true, - mealProvided: true, - managers: [ShiftManager(name: 'John Doe', phone: '+1 555 0101')], - description: 'Help with dinner service. Must be experienced with grill.', - ); - - static final Shift _sampleShift2 = Shift( - id: '2', - title: 'Dishwasher', - clientName: 'Pasta Place', - hourlyRate: 18.00, - location: 'Brooklyn, NY', - locationAddress: '456 Bedford Ave, Brooklyn, NY 11211', - date: DateTime.now().add(const Duration(days: 1)).toIso8601String(), - startTime: '18:00', - endTime: '23:00', - createdDate: DateTime.now() - .subtract(const Duration(hours: 5)) - .toIso8601String(), - tipsAvailable: false, - mealProvided: true, - ); - - static final Shift _sampleShift3 = Shift( - id: '3', - title: 'Bartender', - clientName: 'Rooftop Bar', - hourlyRate: 25.00, - location: 'Manhattan, NY', - locationAddress: '789 5th Ave, New York, NY 10022', - date: DateTime.now().add(const Duration(days: 2)).toIso8601String(), - startTime: '19:00', - endTime: '02:00', - createdDate: DateTime.now() - .subtract(const Duration(hours: 1)) - .toIso8601String(), - tipsAvailable: true, - parkingAvailable: true, - description: 'High volume bar. Mixology experience required.', - ); - - Future> getTodayShifts() async { - await Future.delayed(const Duration(milliseconds: 500)); - return [_sampleShift1]; - } - - Future> getTomorrowShifts() async { - await Future.delayed(const Duration(milliseconds: 500)); - return [_sampleShift2]; - } - - Future> getRecommendedShifts() async { - await Future.delayed(const Duration(milliseconds: 500)); - return [_sampleShift3, _sampleShift1, _sampleShift2]; - } - - Future createWorkerProfile(Map data) async { - await Future.delayed(const Duration(seconds: 1)); - } -} diff --git a/apps/mobile/packages/features/staff/home/lib/src/domain/repositories/home_repository.dart b/apps/mobile/packages/features/staff/home/lib/src/domain/repositories/home_repository.dart index a280f4cf..df35f9d2 100644 --- a/apps/mobile/packages/features/staff/home/lib/src/domain/repositories/home_repository.dart +++ b/apps/mobile/packages/features/staff/home/lib/src/domain/repositories/home_repository.dart @@ -1,4 +1,4 @@ -import 'package:staff_home/src/domain/entities/shift.dart'; +import 'package:krow_domain/krow_domain.dart'; /// Repository interface for home screen data operations. /// @@ -14,4 +14,7 @@ abstract class HomeRepository { /// Retrieves shifts recommended for the worker based on their profile. Future> getRecommendedShifts(); + + /// Retrieves the current staff member's name. + Future getStaffName(); } diff --git a/apps/mobile/packages/features/staff/home/lib/src/domain/usecases/get_home_shifts.dart b/apps/mobile/packages/features/staff/home/lib/src/domain/usecases/get_home_shifts.dart index 2e9ed17c..dd8d7958 100644 --- a/apps/mobile/packages/features/staff/home/lib/src/domain/usecases/get_home_shifts.dart +++ b/apps/mobile/packages/features/staff/home/lib/src/domain/usecases/get_home_shifts.dart @@ -1,4 +1,4 @@ -import 'package:staff_home/src/domain/entities/shift.dart'; +import 'package:krow_domain/krow_domain.dart'; import 'package:staff_home/src/domain/repositories/home_repository.dart'; /// Use case for fetching all shifts displayed on the home screen. diff --git a/apps/mobile/packages/features/staff/home/lib/src/presentation/blocs/home_cubit.dart b/apps/mobile/packages/features/staff/home/lib/src/presentation/blocs/home_cubit.dart index 7eed68f4..792a32eb 100644 --- a/apps/mobile/packages/features/staff/home/lib/src/presentation/blocs/home_cubit.dart +++ b/apps/mobile/packages/features/staff/home/lib/src/presentation/blocs/home_cubit.dart @@ -1,7 +1,7 @@ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; +import 'package:krow_domain/krow_domain.dart'; -import 'package:staff_home/src/domain/entities/shift.dart'; import 'package:staff_home/src/domain/usecases/get_home_shifts.dart'; import 'package:staff_home/src/domain/repositories/home_repository.dart'; @@ -10,26 +10,33 @@ part 'home_state.dart'; /// Simple Cubit to manage home page state (shifts + loading/error). class HomeCubit extends Cubit { final GetHomeShifts _getHomeShifts; + final HomeRepository _repository; HomeCubit(HomeRepository repository) : _getHomeShifts = GetHomeShifts(repository), + _repository = repository, super(const HomeState.initial()); Future loadShifts() async { + if (isClosed) return; emit(state.copyWith(status: HomeStatus.loading)); try { final result = await _getHomeShifts.call(); + final name = await _repository.getStaffName(); + if (isClosed) return; emit( state.copyWith( status: HomeStatus.loaded, todayShifts: result.today, tomorrowShifts: result.tomorrow, recommendedShifts: result.recommended, + staffName: name, // Mock profile status for now, ideally fetched from a user repository isProfileComplete: false, ), ); } catch (e) { + if (isClosed) return; emit( state.copyWith(status: HomeStatus.error, errorMessage: e.toString()), ); diff --git a/apps/mobile/packages/features/staff/home/lib/src/presentation/blocs/home_state.dart b/apps/mobile/packages/features/staff/home/lib/src/presentation/blocs/home_state.dart index e67f454b..0713d7a1 100644 --- a/apps/mobile/packages/features/staff/home/lib/src/presentation/blocs/home_state.dart +++ b/apps/mobile/packages/features/staff/home/lib/src/presentation/blocs/home_state.dart @@ -9,6 +9,7 @@ class HomeState extends Equatable { final List recommendedShifts; final bool autoMatchEnabled; final bool isProfileComplete; + final String? staffName; final String? errorMessage; const HomeState({ @@ -18,6 +19,7 @@ class HomeState extends Equatable { this.recommendedShifts = const [], this.autoMatchEnabled = false, this.isProfileComplete = false, + this.staffName, this.errorMessage, }); @@ -30,6 +32,7 @@ class HomeState extends Equatable { List? recommendedShifts, bool? autoMatchEnabled, bool? isProfileComplete, + String? staffName, String? errorMessage, }) { return HomeState( @@ -39,18 +42,20 @@ class HomeState extends Equatable { recommendedShifts: recommendedShifts ?? this.recommendedShifts, autoMatchEnabled: autoMatchEnabled ?? this.autoMatchEnabled, isProfileComplete: isProfileComplete ?? this.isProfileComplete, + staffName: staffName ?? this.staffName, errorMessage: errorMessage ?? this.errorMessage, ); } @override List get props => [ - status, - todayShifts, - tomorrowShifts, - recommendedShifts, - autoMatchEnabled, - isProfileComplete, - errorMessage, - ]; -} + status, + todayShifts, + tomorrowShifts, + recommendedShifts, + autoMatchEnabled, + isProfileComplete, + staffName, + errorMessage, + ]; +} \ No newline at end of file diff --git a/apps/mobile/packages/features/staff/home/lib/src/presentation/navigation/home_navigator.dart b/apps/mobile/packages/features/staff/home/lib/src/presentation/navigation/home_navigator.dart index 056a5636..cd9da6f6 100644 --- a/apps/mobile/packages/features/staff/home/lib/src/presentation/navigation/home_navigator.dart +++ b/apps/mobile/packages/features/staff/home/lib/src/presentation/navigation/home_navigator.dart @@ -1,4 +1,5 @@ import 'package:flutter_modular/flutter_modular.dart'; +import 'package:krow_domain/krow_domain.dart'; /// Extension on [IModularNavigator] providing typed navigation helpers /// for the Staff Home feature (worker home screen). @@ -8,7 +9,7 @@ import 'package:flutter_modular/flutter_modular.dart'; extension HomeNavigator on IModularNavigator { /// Navigates to the worker profile page. void pushWorkerProfile() { - pushNamed('/worker-profile'); + pushNamed('/worker-main/profile'); } /// Navigates to the availability page. @@ -30,9 +31,11 @@ extension HomeNavigator on IModularNavigator { /// Optionally provide a [tab] query param (e.g. `find`). void pushShifts({String? tab}) { if (tab == null) { - pushNamed('/shifts'); + navigate('/worker-main/shifts'); } else { - pushNamed('/shifts?tab=$tab'); + navigate('/worker-main/shifts', arguments: { + 'initialTab': tab, + }); } } @@ -40,4 +43,9 @@ extension HomeNavigator on IModularNavigator { void pushSettings() { pushNamed('/settings'); } + + /// Navigates to the shift details page for the given [shift]. + void pushShiftDetails(Shift shift) { + pushNamed('/worker-main/shift-details/${shift.id}', arguments: shift); + } } 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 4b323dba..777cbf14 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 @@ -1,24 +1,18 @@ +import 'package:core_localization/core_localization.dart'; +import 'package:design_system/design_system.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_modular/flutter_modular.dart'; import 'package:lucide_icons/lucide_icons.dart'; -import 'package:core_localization/core_localization.dart'; -import 'package:design_system/design_system.dart'; - import 'package:staff_home/src/presentation/blocs/home_cubit.dart'; import 'package:staff_home/src/presentation/navigation/home_navigator.dart'; import 'package:staff_home/src/presentation/widgets/home_page/empty_state_widget.dart'; import 'package:staff_home/src/presentation/widgets/home_page/home_header.dart'; -import 'package:staff_home/src/presentation/widgets/home_page/pending_payment_card.dart'; import 'package:staff_home/src/presentation/widgets/home_page/placeholder_banner.dart'; import 'package:staff_home/src/presentation/widgets/home_page/quick_action_item.dart'; 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'; -import 'package:staff_home/src/presentation/widgets/worker/benefits_widget.dart'; -import 'package:staff_home/src/presentation/widgets/worker/improve_yourself_widget.dart'; -import 'package:staff_home/src/presentation/widgets/worker/more_ways_widget.dart'; /// The home page for the staff worker application. /// @@ -44,8 +38,8 @@ class WorkerHomePage extends StatelessWidget { final sectionsI18n = i18n.sections; final emptyI18n = i18n.empty_states; - return BlocProvider( - create: (context) => Modular.get()..loadShifts(), + return BlocProvider.value( + value: Modular.get()..loadShifts(), child: Scaffold( body: SafeArea( child: SingleChildScrollView( @@ -53,7 +47,12 @@ class WorkerHomePage extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const HomeHeader(), + BlocBuilder( + buildWhen: (previous, current) => previous.staffName != current.staffName, + builder: (context, state) { + return HomeHeader(userName: state.staffName); + }, + ), Padding( padding: const EdgeInsets.symmetric(horizontal: UiConstants.space4), child: Column( @@ -75,31 +74,7 @@ class WorkerHomePage extends StatelessWidget { ); }, ), - const SizedBox(height: UiConstants.space6), - PlaceholderBanner( - title: bannersI18n.availability_title, - subtitle: bannersI18n.availability_subtitle, - bg: UiColors.accent.withOpacity(0.1), - accent: UiColors.accent, - onTap: () => Modular.to.pushAvailability(), - ), - const SizedBox(height: UiConstants.space6), - // Auto Match Toggle - BlocBuilder( - buildWhen: (previous, current) => - previous.autoMatchEnabled != - current.autoMatchEnabled, - builder: (context, state) { - return AutoMatchToggle( - enabled: state.autoMatchEnabled, - onToggle: (val) => BlocProvider.of( - context, - listen: false, - ).toggleAutoMatch(val), - ); - }, - ), const SizedBox(height: UiConstants.space6), // Quick Actions @@ -120,13 +95,6 @@ class WorkerHomePage extends StatelessWidget { onTap: () => Modular.to.pushAvailability(), ), ), - Expanded( - child: QuickActionItem( - icon: LucideIcons.messageSquare, - label: quickI18n.messages, - onTap: () => Modular.to.pushMessages(), - ), - ), Expanded( child: QuickActionItem( icon: LucideIcons.dollarSign, @@ -164,8 +132,7 @@ class WorkerHomePage extends StatelessWidget { EmptyStateWidget( message: emptyI18n.no_shifts_today, actionLink: emptyI18n.find_shifts_cta, - onAction: () => - Modular.to.pushShifts(tab: 'find'), + onAction: () => Modular.to.pushShifts(tab: 'find'), ) else Column( @@ -212,15 +179,9 @@ class WorkerHomePage extends StatelessWidget { ), const SizedBox(height: 24), - // Pending Payment Card - const PendingPaymentCard(), - const SizedBox(height: 24), - // Recommended Shifts SectionHeader( title: sectionsI18n.recommended_for_you, - action: sectionsI18n.view_all, - onAction: () => Modular.to.pushShifts(tab: 'find'), ), BlocBuilder( builder: (context, state) { @@ -246,14 +207,6 @@ class WorkerHomePage extends StatelessWidget { }, ), const SizedBox(height: 24), - - const BenefitsWidget(), - const SizedBox(height: 24), - - const ImproveYourselfWidget(), - const SizedBox(height: 24), - - const MoreWaysToUseKrowWidget(), ], ), ), diff --git a/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/home_header.dart b/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/home_header.dart index ea85d499..17127ce5 100644 --- a/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/home_header.dart +++ b/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/home_header.dart @@ -2,15 +2,21 @@ import 'package:core_localization/core_localization.dart'; import 'package:design_system/design_system.dart'; import 'package:flutter/material.dart'; - /// Header widget for the staff home page, using design system tokens. class HomeHeader extends StatelessWidget { + final String? userName; + /// Creates a [HomeHeader]. - const HomeHeader({super.key}); + const HomeHeader({super.key, this.userName}); @override Widget build(BuildContext context) { final headerI18n = t.staff.home.header; + final nameToDisplay = userName ?? headerI18n.user_name_placeholder; + final initial = nameToDisplay.isNotEmpty + ? nameToDisplay[0].toUpperCase() + : 'K'; + return Padding( padding: EdgeInsets.fromLTRB( UiConstants.space4, @@ -18,45 +24,42 @@ class HomeHeader extends StatelessWidget { UiConstants.space4, UiConstants.space3, ), - child:Row( + child: Row( + spacing: UiConstants.space3, + children: [ + Container( + width: 48, + height: 48, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: UiColors.primary.withOpacity(0.2), + width: 2, + ), + ), + child: CircleAvatar( + backgroundColor: UiColors.primary.withOpacity(0.1), + child: Text( + initial, + style: const TextStyle( + color: UiColors.primary, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Container( - width: 48, - height: 48, - decoration: BoxDecoration( - shape: BoxShape.circle, - border: Border.all( - color: UiColors.primary.withOpacity(0.2), - width: 2, - ), - ), - child: CircleAvatar( - backgroundColor: UiColors.primary.withOpacity(0.1), - child: const Text( - 'K', - style: TextStyle( - color: UiColors.primary, - fontWeight: FontWeight.bold, - ), - ), - ), - ), - const SizedBox(width: UiConstants.space3), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - headerI18n.welcome_back, - style: UiTypography.body3r.copyWith(color: UiColors.mutedForeground), - ), - Text( - headerI18n.user_name_placeholder, - style: UiTypography.headline4m, - ), - ], + Text( + headerI18n.welcome_back, + style: UiTypography.body3r.textSecondary, ), + Text(nameToDisplay, style: UiTypography.headline4m), ], ), + ], + ), ); } } diff --git a/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/recommended_shift_card.dart b/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/recommended_shift_card.dart index c3400340..3a4ef59d 100644 --- a/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/recommended_shift_card.dart +++ b/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/recommended_shift_card.dart @@ -1,9 +1,10 @@ -import 'package:flutter/material.dart'; -import 'package:lucide_icons/lucide_icons.dart'; - -import 'package:design_system/design_system.dart'; -import 'package:staff_home/src/domain/entities/shift.dart'; import 'package:core_localization/core_localization.dart'; +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_modular/flutter_modular.dart'; +import 'package:krow_domain/krow_domain.dart'; +import 'package:lucide_icons/lucide_icons.dart'; +import 'package:staff_home/src/presentation/navigation/home_navigator.dart'; class RecommendedShiftCard extends StatelessWidget { final Shift shift; @@ -18,13 +19,7 @@ class RecommendedShiftCard extends StatelessWidget { return GestureDetector( onTap: () { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(recI18n.applied_for(title: shift.title)), - backgroundColor: Colors.green, - duration: const Duration(seconds: 2), - ), - ); + Modular.to.pushShiftDetails(shift); }, child: Container( width: 300, @@ -41,171 +36,173 @@ class RecommendedShiftCard extends StatelessWidget { ), ], ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Row( - children: [ - Text( - recI18n.act_now, - style: const TextStyle( - fontSize: 10, - fontWeight: FontWeight.bold, - color: Color(0xFFDC2626), - ), - ), - const SizedBox(width: 8), - Container( - padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 2, - ), - decoration: BoxDecoration( - color: const Color(0xFFE8F0FF), - borderRadius: BorderRadius.circular(999), - ), - child: Text( - recI18n.one_day, + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + Text( + recI18n.act_now, style: const TextStyle( fontSize: 10, - fontWeight: FontWeight.w500, - color: Color(0xFF0047FF), + fontWeight: FontWeight.bold, + color: Color(0xFFDC2626), ), ), - ), - ], - ), - const SizedBox(height: 12), - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - width: 44, - height: 44, - decoration: BoxDecoration( - color: const Color(0xFFE8F0FF), - borderRadius: BorderRadius.circular(12), + const SizedBox(width: 8), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 2, + ), + decoration: BoxDecoration( + color: const Color(0xFFE8F0FF), + borderRadius: BorderRadius.circular(999), + ), + child: Text( + recI18n.one_day, + style: const TextStyle( + fontSize: 10, + fontWeight: FontWeight.w500, + color: Color(0xFF0047FF), + ), + ), ), - child: const Icon( - LucideIcons.calendar, - color: Color(0xFF0047FF), - size: 20, + ], + ), + const SizedBox(height: 12), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 44, + height: 44, + decoration: BoxDecoration( + color: const Color(0xFFE8F0FF), + borderRadius: BorderRadius.circular(12), + ), + child: const Icon( + LucideIcons.calendar, + color: Color(0xFF0047FF), + size: 20, + ), ), - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Text( - shift.title, + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + shift.title, + style: const TextStyle( + fontWeight: FontWeight.w600, + fontSize: 16, + color: UiColors.foreground, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + Text( + '\$${totalPay.round()}', style: const TextStyle( - fontWeight: FontWeight.w600, - fontSize: 16, + fontSize: 18, + fontWeight: FontWeight.bold, color: UiColors.foreground, ), - maxLines: 1, - overflow: TextOverflow.ellipsis, ), - ), - Text( - '\$${totalPay.round()}', - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: UiColors.foreground, + ], + ), + const SizedBox(height: 2), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + shift.clientName, + style: const TextStyle( + fontSize: 12, + color: UiColors.mutedForeground, + ), ), - ), - ], - ), - const SizedBox(height: 2), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - shift.clientName, - style: const TextStyle( - fontSize: 12, - color: UiColors.mutedForeground, + Text( + '\$${shift.hourlyRate.toStringAsFixed(0)}/hr • ${duration}h', + style: const TextStyle( + fontSize: 10, + color: UiColors.mutedForeground, + ), ), - ), - Text( - '\$${shift.hourlyRate.toStringAsFixed(0)}/hr • ${duration}h', - style: const TextStyle( - fontSize: 10, - color: UiColors.mutedForeground, - ), - ), - ], - ), - ], + ], + ), + ], + ), ), - ), - ], - ), - const SizedBox(height: 12), - Row( - children: [ - const Icon( - LucideIcons.calendar, - size: 14, - color: UiColors.mutedForeground, - ), - const SizedBox(width: 4), - Text( - recI18n.today, - style: const TextStyle( - fontSize: 12, + ], + ), + const SizedBox(height: 12), + Row( + children: [ + const Icon( + LucideIcons.calendar, + size: 14, color: UiColors.mutedForeground, ), - ), - const SizedBox(width: 12), - const Icon( - LucideIcons.clock, - size: 14, - color: UiColors.mutedForeground, - ), - const SizedBox(width: 4), - Text( - recI18n.time_range( - start: shift.startTime, - end: shift.endTime, - ), - style: const TextStyle( - fontSize: 12, - color: UiColors.mutedForeground, - ), - ), - ], - ), - const SizedBox(height: 4), - Row( - children: [ - const Icon( - LucideIcons.mapPin, - size: 14, - color: UiColors.mutedForeground, - ), - const SizedBox(width: 4), - Expanded( - child: Text( - shift.locationAddress ?? shift.location, + const SizedBox(width: 4), + Text( + recI18n.today, style: const TextStyle( fontSize: 12, color: UiColors.mutedForeground, ), - maxLines: 1, - overflow: TextOverflow.ellipsis, ), - ), - ], - ), - ], + const SizedBox(width: 12), + const Icon( + LucideIcons.clock, + size: 14, + color: UiColors.mutedForeground, + ), + const SizedBox(width: 4), + Text( + recI18n.time_range( + start: shift.startTime, + end: shift.endTime, + ), + style: const TextStyle( + fontSize: 12, + color: UiColors.mutedForeground, + ), + ), + ], + ), + const SizedBox(height: 4), + Row( + children: [ + const Icon( + LucideIcons.mapPin, + size: 14, + color: UiColors.mutedForeground, + ), + const SizedBox(width: 4), + Expanded( + child: Text( + shift.locationAddress ?? shift.location, + style: const TextStyle( + fontSize: 12, + color: UiColors.mutedForeground, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ], + ), ), ), ); 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 3990fe9c..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 @@ -4,7 +4,8 @@ import 'package:lucide_icons/lucide_icons.dart'; import 'package:intl/intl.dart'; import 'package:design_system/design_system.dart'; -import 'package:staff_home/src/domain/entities/shift.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), diff --git a/apps/mobile/packages/features/staff/home/lib/src/staff_home_module.dart b/apps/mobile/packages/features/staff/home/lib/src/staff_home_module.dart index f04002fd..8eeab6bb 100644 --- a/apps/mobile/packages/features/staff/home/lib/src/staff_home_module.dart +++ b/apps/mobile/packages/features/staff/home/lib/src/staff_home_module.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_modular/flutter_modular.dart'; import 'package:staff_home/src/data/repositories/home_repository_impl.dart'; -import 'package:staff_home/src/data/services/mock_service.dart'; import 'package:staff_home/src/domain/repositories/home_repository.dart'; import 'package:staff_home/src/presentation/blocs/home_cubit.dart'; import 'package:staff_home/src/presentation/pages/worker_home_page.dart'; @@ -14,12 +13,9 @@ import 'package:staff_home/src/presentation/pages/worker_home_page.dart'; class StaffHomeModule extends Module { @override void binds(Injector i) { - // Data layer - Mock service (will be replaced with real implementation) - i.addLazySingleton(MockService.new); - // Repository i.addLazySingleton( - () => HomeRepositoryImpl(i.get()), + () => HomeRepositoryImpl(), ); // Presentation layer - Cubit diff --git a/apps/mobile/packages/features/staff/home/pubspec.yaml b/apps/mobile/packages/features/staff/home/pubspec.yaml index 6bd6a880..8d6afcfd 100644 --- a/apps/mobile/packages/features/staff/home/pubspec.yaml +++ b/apps/mobile/packages/features/staff/home/pubspec.yaml @@ -28,6 +28,11 @@ dependencies: path: ../../../core krow_domain: path: ../../../domain + staff_shifts: + path: ../shifts + krow_data_connect: + path: ../../../data_connect + firebase_data_connect: dev_dependencies: flutter_test: diff --git a/apps/mobile/packages/features/staff/payments/lib/src/data/datasources/payments_mock_datasource.dart b/apps/mobile/packages/features/staff/payments/lib/src/data/datasources/payments_mock_datasource.dart deleted file mode 100644 index 2b084c46..00000000 --- a/apps/mobile/packages/features/staff/payments/lib/src/data/datasources/payments_mock_datasource.dart +++ /dev/null @@ -1,67 +0,0 @@ -// ignore: depend_on_referenced_packages -import 'package:flutter/foundation.dart'; -import '../../domain/entities/payment_summary.dart'; -import '../../domain/entities/payment_transaction.dart'; -import 'payments_remote_datasource.dart'; - -class PaymentsMockDataSource implements PaymentsRemoteDataSource { - @override - Future fetchPaymentSummary() async { - // Simulate network delay - await Future.delayed(const Duration(milliseconds: 800)); - - // Mock data matching the prototype - return const PaymentSummary( - weeklyEarnings: 847.50, - monthlyEarnings: 3240.0, - pendingEarnings: 285.0, - totalEarnings: 12450.0, - ); - } - - @override - Future> fetchPaymentHistory(String period) async { - await Future.delayed(const Duration(milliseconds: 1000)); - - // Mock data matching the prototype - // In a real scenario, this would filter by 'period' (week/month/year) - return [ - PaymentTransaction( - id: '1', - title: 'Cook', - location: 'LA Convention Center', - address: '1201 S Figueroa St, Los Angeles, CA 90015', - workedTime: '2:00 PM - 10:00 PM', - amount: 160.00, - status: 'PAID', - hours: 8, - rate: 20.0, - date: DateTime(2025, 12, 6), // "Sat, Dec 6" (Using future date to match context if needed, but keeping it simple) - ), - PaymentTransaction( - id: '2', - title: 'Server', - location: 'The Grand Hotel', - address: '456 Main St, Los Angeles, CA 90012', - workedTime: '5:00 PM - 11:00 PM', - amount: 176.00, - status: 'PAID', - hours: 8, - rate: 22.0, - date: DateTime(2025, 12, 5), // "Fri, Dec 5" - ), - PaymentTransaction( - id: '3', - title: 'Bartender', - location: 'Club Luxe', - address: '789 Sunset Blvd, Los Angeles, CA 90028', - workedTime: '6:00 PM - 2:00 AM', - amount: 225.00, - status: 'PAID', - hours: 9, - rate: 25.0, - date: DateTime(2025, 12, 4), // "Thu, Dec 4" - ), - ]; - } -} diff --git a/apps/mobile/packages/features/staff/payments/lib/src/data/datasources/payments_remote_datasource.dart b/apps/mobile/packages/features/staff/payments/lib/src/data/datasources/payments_remote_datasource.dart deleted file mode 100644 index 37f24a9e..00000000 --- a/apps/mobile/packages/features/staff/payments/lib/src/data/datasources/payments_remote_datasource.dart +++ /dev/null @@ -1,7 +0,0 @@ -import '../../domain/entities/payment_summary.dart'; -import '../../domain/entities/payment_transaction.dart'; - -abstract class PaymentsRemoteDataSource { - Future fetchPaymentSummary(); - Future> fetchPaymentHistory(String period); -} diff --git a/apps/mobile/packages/features/staff/payments/lib/src/data/repositories/payments_repository_impl.dart b/apps/mobile/packages/features/staff/payments/lib/src/data/repositories/payments_repository_impl.dart index 303fca7f..f1b4afc4 100644 --- a/apps/mobile/packages/features/staff/payments/lib/src/data/repositories/payments_repository_impl.dart +++ b/apps/mobile/packages/features/staff/payments/lib/src/data/repositories/payments_repository_impl.dart @@ -1,22 +1,152 @@ -// ignore: unused_import -// import 'package:data_connect/data_connect.dart'; -import '../../domain/entities/payment_summary.dart'; -import '../../domain/entities/payment_transaction.dart'; +import 'package:firebase_data_connect/firebase_data_connect.dart'; +import 'package:krow_data_connect/krow_data_connect.dart' as dc; +import 'package:krow_data_connect/src/session/staff_session_store.dart'; +import 'package:krow_domain/krow_domain.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:krow_core/core.dart'; import '../../domain/repositories/payments_repository.dart'; -import '../datasources/payments_remote_datasource.dart'; class PaymentsRepositoryImpl implements PaymentsRepository { - final PaymentsRemoteDataSource remoteDataSource; + final dc.ExampleConnector _dataConnect; + final FirebaseAuth _auth = FirebaseAuth.instance; - PaymentsRepositoryImpl({required this.remoteDataSource}); + PaymentsRepositoryImpl() : _dataConnect = dc.ExampleConnector.instance; + + String? _cachedStaffId; + + Future _getStaffId() async { + // 1. Check Session Store + final StaffSession? session = StaffSessionStore.instance.session; + if (session?.staff?.id != null) { + return session!.staff!.id; + } + + // 2. Check Cache + if (_cachedStaffId != null) return _cachedStaffId!; + + // 3. Fetch from Data Connect using Firebase UID + final user = _auth.currentUser; + if (user == null) { + throw Exception('User is not authenticated'); + } + + try { + final response = await _dataConnect.getStaffByUserId(userId: user.uid).execute(); + if (response.data.staffs.isNotEmpty) { + _cachedStaffId = response.data.staffs.first.id; + return _cachedStaffId!; + } + } catch (e) { + // Log or handle error + } + + // 4. Fallback + return user.uid; + } + + /// Helper to convert Data Connect Timestamp to DateTime + DateTime? _toDateTime(dynamic t) { + if (t == null) return null; + DateTime? dt; + if (t is DateTime) { + dt = t; + } else if (t is String) { + dt = DateTime.tryParse(t); + } else { + try { + if (t is Timestamp) { + dt = t.toDateTime(); + } + } catch (_) {} + + try { + if (dt == null && t.runtimeType.toString().contains('Timestamp')) { + dt = (t as dynamic).toDate(); + } + } catch (_) {} + + try { + if (dt == null) { + dt = DateTime.tryParse(t.toString()); + } + } catch (_) {} + } + + if (dt != null) { + return DateTimeUtils.toDeviceTime(dt); + } + return null; + } @override Future getPaymentSummary() async { - return await remoteDataSource.fetchPaymentSummary(); + final String currentStaffId = await _getStaffId(); + + // Fetch recent payments with a limit + // Note: limit is chained on the query builder + final QueryResult result = + await _dataConnect.listRecentPaymentsByStaffId( + staffId: currentStaffId, + ).limit(100).execute(); + + final List payments = result.data.recentPayments; + + double weekly = 0; + double monthly = 0; + double pending = 0; + double total = 0; + + final DateTime now = DateTime.now(); + final DateTime startOfWeek = now.subtract(const Duration(days: 7)); + final DateTime startOfMonth = DateTime(now.year, now.month, 1); + + for (final dc.ListRecentPaymentsByStaffIdRecentPayments p in payments) { + final DateTime? date = _toDateTime(p.invoice.issueDate) ?? _toDateTime(p.createdAt); + final double amount = p.invoice.amount; + final String? status = p.status?.stringValue; + + if (status == 'PENDING') { + pending += amount; + } else if (status == 'PAID') { + total += amount; + if (date != null) { + if (date.isAfter(startOfWeek)) weekly += amount; + if (date.isAfter(startOfMonth)) monthly += amount; + } + } + } + + return PaymentSummary( + weeklyEarnings: weekly, + monthlyEarnings: monthly, + pendingEarnings: pending, + totalEarnings: total, + ); } @override - Future> getPaymentHistory(String period) async { - return await remoteDataSource.fetchPaymentHistory(period); + Future> getPaymentHistory(String period) async { + final String currentStaffId = await _getStaffId(); + + try { + final QueryResult response = + await _dataConnect + .listRecentPaymentsByStaffId(staffId: currentStaffId) + .execute(); + + return response.data.recentPayments.map((dc.ListRecentPaymentsByStaffIdRecentPayments payment) { + return StaffPayment( + id: payment.id, + staffId: payment.staffId, + assignmentId: payment.applicationId, + amount: payment.invoice.amount, + status: PaymentAdapter.toPaymentStatus(payment.status?.stringValue ?? 'UNKNOWN'), + paidAt: _toDateTime(payment.invoice.issueDate), + ); + }).toList(); + } catch (e) { + return []; + } } } + diff --git a/apps/mobile/packages/features/staff/payments/lib/src/data/repositories_impl/payments_repository_impl.dart b/apps/mobile/packages/features/staff/payments/lib/src/data/repositories_impl/payments_repository_impl.dart deleted file mode 100644 index 5abcd80b..00000000 --- a/apps/mobile/packages/features/staff/payments/lib/src/data/repositories_impl/payments_repository_impl.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'package:krow_data_connect/krow_data_connect.dart'; -import 'package:krow_domain/krow_domain.dart'; -import '../../domain/repositories/payments_repository.dart'; - -/// Implementation of [PaymentsRepository]. -/// -/// This class handles the retrieval of payment data by delegating to the -/// [FinancialRepositoryMock] from the data connect package. -/// -/// It resides in the data layer and depends on the domain layer for the repository interface. -class PaymentsRepositoryImpl implements PaymentsRepository { - final FinancialRepositoryMock financialRepository; - - /// Creates a [PaymentsRepositoryImpl] with the given [financialRepository]. - PaymentsRepositoryImpl({required this.financialRepository}); - - @override - Future> getPayments() async { - // TODO: Get actual logged in staff ID - return await financialRepository.getStaffPayments('staff_1'); - } -} diff --git a/apps/mobile/packages/features/staff/payments/lib/src/domain/repositories/payments_repository.dart b/apps/mobile/packages/features/staff/payments/lib/src/domain/repositories/payments_repository.dart index 71546c9e..227c783e 100644 --- a/apps/mobile/packages/features/staff/payments/lib/src/domain/repositories/payments_repository.dart +++ b/apps/mobile/packages/features/staff/payments/lib/src/domain/repositories/payments_repository.dart @@ -5,6 +5,9 @@ import 'package:krow_domain/krow_domain.dart'; /// Defines the contract for data access related to staff payments. /// Implementations of this interface should reside in the data layer. abstract class PaymentsRepository { - /// Fetches the list of payments for the current staff member. - Future> getPayments(); + /// Fetches the payment summary (earnings). + Future getPaymentSummary(); + + /// Fetches the payment history for a specific period. + Future> getPaymentHistory(String period); } diff --git a/apps/mobile/packages/features/staff/payments/lib/src/domain/usecases/get_payment_history_usecase.dart b/apps/mobile/packages/features/staff/payments/lib/src/domain/usecases/get_payment_history_usecase.dart index d5a3a3a8..c43296f0 100644 --- a/apps/mobile/packages/features/staff/payments/lib/src/domain/usecases/get_payment_history_usecase.dart +++ b/apps/mobile/packages/features/staff/payments/lib/src/domain/usecases/get_payment_history_usecase.dart @@ -14,7 +14,6 @@ class GetPaymentHistoryUseCase extends UseCase> call(GetPaymentHistoryArguments arguments) async { - // TODO: Implement filtering by period - return await repository.getPayments(); + return await repository.getPaymentHistory(arguments.period); } } diff --git a/apps/mobile/packages/features/staff/payments/lib/src/domain/usecases/get_payment_summary_usecase.dart b/apps/mobile/packages/features/staff/payments/lib/src/domain/usecases/get_payment_summary_usecase.dart index 27b74290..84c54d59 100644 --- a/apps/mobile/packages/features/staff/payments/lib/src/domain/usecases/get_payment_summary_usecase.dart +++ b/apps/mobile/packages/features/staff/payments/lib/src/domain/usecases/get_payment_summary_usecase.dart @@ -3,17 +3,14 @@ import 'package:krow_domain/krow_domain.dart'; import '../repositories/payments_repository.dart'; /// Use case to retrieve payment summary information. -/// -/// It fetches the full list of payments, which ideally should be aggregated -/// by the presentation layer or a specific data source method. -class GetPaymentSummaryUseCase extends NoInputUseCase> { +class GetPaymentSummaryUseCase extends NoInputUseCase { final PaymentsRepository repository; /// Creates a [GetPaymentSummaryUseCase]. GetPaymentSummaryUseCase(this.repository); @override - Future> call() async { - return await repository.getPayments(); + Future call() async { + return await repository.getPaymentSummary(); } } diff --git a/apps/mobile/packages/features/staff/payments/lib/src/payments_module.dart b/apps/mobile/packages/features/staff/payments/lib/src/payments_module.dart index 3108e8c8..e7cbf17d 100644 --- a/apps/mobile/packages/features/staff/payments/lib/src/payments_module.dart +++ b/apps/mobile/packages/features/staff/payments/lib/src/payments_module.dart @@ -3,17 +3,14 @@ import 'package:krow_data_connect/krow_data_connect.dart'; import 'domain/repositories/payments_repository.dart'; import 'domain/usecases/get_payment_summary_usecase.dart'; import 'domain/usecases/get_payment_history_usecase.dart'; -import 'data/repositories_impl/payments_repository_impl.dart'; +import 'data/repositories/payments_repository_impl.dart'; import 'presentation/blocs/payments/payments_bloc.dart'; import 'presentation/pages/payments_page.dart'; class StaffPaymentsModule extends Module { @override void binds(Injector i) { - // Data Connect Mocks - i.add(FinancialRepositoryMock.new); - - // Repositories + // Repositories i.add(PaymentsRepositoryImpl.new); // Use Cases diff --git a/apps/mobile/packages/features/staff/payments/lib/src/presentation/blocs/payments/payments_bloc.dart b/apps/mobile/packages/features/staff/payments/lib/src/presentation/blocs/payments/payments_bloc.dart index 33887032..c25e98e8 100644 --- a/apps/mobile/packages/features/staff/payments/lib/src/presentation/blocs/payments/payments_bloc.dart +++ b/apps/mobile/packages/features/staff/payments/lib/src/presentation/blocs/payments/payments_bloc.dart @@ -1,9 +1,8 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:krow_domain/krow_domain.dart'; import '../../../domain/arguments/get_payment_history_arguments.dart'; -import '../../../domain/usecases/get_payment_summary_usecase.dart'; import '../../../domain/usecases/get_payment_history_usecase.dart'; -import '../../models/payment_stats.dart'; +import '../../../domain/usecases/get_payment_summary_usecase.dart'; import 'payments_event.dart'; import 'payments_state.dart'; @@ -25,14 +24,13 @@ class PaymentsBloc extends Bloc { ) async { emit(PaymentsLoading()); try { - final List allPayments = await getPaymentSummary(); - final PaymentStats stats = _calculateStats(allPayments); + final PaymentSummary currentSummary = await getPaymentSummary(); final List history = await getPaymentHistory( const GetPaymentHistoryArguments('week'), ); emit(PaymentsLoaded( - summary: stats, + summary: currentSummary, history: history, activePeriod: 'week', )); @@ -60,38 +58,4 @@ class PaymentsBloc extends Bloc { } } } - - PaymentStats _calculateStats(List payments) { - double total = 0; - double pending = 0; - double weekly = 0; - double monthly = 0; - - final DateTime now = DateTime.now(); - - for (final StaffPayment p in payments) { - // Assuming all payments count towards total history - total += p.amount; - - if (p.status == PaymentStatus.pending) { - pending += p.amount; - } - - if (p.paidAt != null) { - if (now.difference(p.paidAt!).inDays < 7) { - weekly += p.amount; - } - if (now.month == p.paidAt!.month && now.year == p.paidAt!.year) { - monthly += p.amount; - } - } - } - - return PaymentStats( - totalEarnings: total, - pendingEarnings: pending, - weeklyEarnings: weekly, - monthlyEarnings: monthly, - ); - } } diff --git a/apps/mobile/packages/features/staff/payments/lib/src/presentation/blocs/payments/payments_state.dart b/apps/mobile/packages/features/staff/payments/lib/src/presentation/blocs/payments/payments_state.dart index 14e3af61..6e100f83 100644 --- a/apps/mobile/packages/features/staff/payments/lib/src/presentation/blocs/payments/payments_state.dart +++ b/apps/mobile/packages/features/staff/payments/lib/src/presentation/blocs/payments/payments_state.dart @@ -1,6 +1,5 @@ import 'package:equatable/equatable.dart'; import 'package:krow_domain/krow_domain.dart'; -import '../../models/payment_stats.dart'; abstract class PaymentsState extends Equatable { const PaymentsState(); @@ -14,7 +13,7 @@ class PaymentsInitial extends PaymentsState {} class PaymentsLoading extends PaymentsState {} class PaymentsLoaded extends PaymentsState { - final PaymentStats summary; + final PaymentSummary summary; final List history; final String activePeriod; @@ -25,7 +24,7 @@ class PaymentsLoaded extends PaymentsState { }); PaymentsLoaded copyWith({ - PaymentStats? summary, + PaymentSummary? summary, List? history, String? activePeriod, }) { diff --git a/apps/mobile/packages/features/staff/payments/lib/src/presentation/pages/payments_page.dart b/apps/mobile/packages/features/staff/payments/lib/src/presentation/pages/payments_page.dart index d82f3588..0df77068 100644 --- a/apps/mobile/packages/features/staff/payments/lib/src/presentation/pages/payments_page.dart +++ b/apps/mobile/packages/features/staff/payments/lib/src/presentation/pages/payments_page.dart @@ -10,6 +10,7 @@ import '../blocs/payments/payments_state.dart'; import '../widgets/payment_stats_card.dart'; import '../widgets/pending_pay_card.dart'; import '../widgets/payment_history_item.dart'; +import '../widgets/earnings_graph.dart'; class PaymentsPage extends StatefulWidget { const PaymentsPage({super.key}); @@ -133,6 +134,12 @@ class _PaymentsPageState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + // Earnings Graph + EarningsGraph( + payments: state.history, + period: state.activePeriod, + ), + const SizedBox(height: 24), // Quick Stats Row( children: [ @@ -158,7 +165,7 @@ class _PaymentsPageState extends State { const SizedBox(height: 16), // Pending Pay - PendingPayCard( + if(state.summary.pendingEarnings > 0) PendingPayCard( amount: state.summary.pendingEarnings, onCashOut: () { Modular.to.pushNamed('/early-pay'); @@ -166,61 +173,44 @@ class _PaymentsPageState extends State { ), const SizedBox(height: 24), - // Recent Payments - const Text( - "Recent Payments", - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w600, - color: Color(0xFF0F172A), - ), - ), - const SizedBox(height: 12), - Column( - children: state.history.map((StaffPayment payment) { - return Padding( - padding: const EdgeInsets.only(bottom: 8), - child: PaymentHistoryItem( - amount: payment.amount, - title: 'Assignment ${payment.assignmentId}', - location: 'Location', // TODO: Fetch from assignment - address: '', - date: payment.paidAt != null ? DateFormat('E, MMM d').format(payment.paidAt!) : 'Pending', - workedTime: '00:00 - 00:00', // TODO: Fetch from assignment - hours: 0, - rate: 0, - status: payment.status.toString().split('.').last, - ), - ); - }).toList(), - ), - const SizedBox(height: 16), - // Export History Button - SizedBox( - width: double.infinity, - height: 48, - child: OutlinedButton.icon( - onPressed: () { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('PDF Exported'), - duration: Duration(seconds: 2), - ), - ); - }, - icon: const Icon(LucideIcons.download, size: 16), - label: const Text("Export History"), - style: OutlinedButton.styleFrom( - foregroundColor: const Color(0xFF0F172A), - side: const BorderSide(color: Color(0xFFE2E8F0)), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), + + // Recent Payments + if (state.history.isNotEmpty) Column( + children: [ + const Text( + "Recent Payments", + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: Color(0xFF0F172A), ), ), - ), + const SizedBox(height: 12), + Column( + children: state.history.map((StaffPayment payment) { + return Padding( + padding: const EdgeInsets.only(bottom: 8), + child: PaymentHistoryItem( + amount: payment.amount, + title: "Shift Payment", + location: "Varies", + address: "Payment ID: ${payment.id}", + date: payment.paidAt != null + ? DateFormat('E, MMM d').format(payment.paidAt!) + : 'Pending', + workedTime: "Completed", + hours: 0, + rate: 0.0, + status: payment.status.name.toUpperCase(), + ), + ); + }).toList(), + ), + ], ), - const SizedBox(height: 32), + + const SizedBox(height: 100), ], ), ), diff --git a/apps/mobile/packages/features/staff/payments/lib/src/presentation/widgets/earnings_graph.dart b/apps/mobile/packages/features/staff/payments/lib/src/presentation/widgets/earnings_graph.dart new file mode 100644 index 00000000..7a87df72 --- /dev/null +++ b/apps/mobile/packages/features/staff/payments/lib/src/presentation/widgets/earnings_graph.dart @@ -0,0 +1,128 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:krow_domain/krow_domain.dart'; + +class EarningsGraph extends StatelessWidget { + final List payments; + final String period; + + const EarningsGraph({ + super.key, + required this.payments, + required this.period, + }); + + @override + Widget build(BuildContext context) { + // Basic data processing for the graph + // We'll aggregate payments by date + final validPayments = payments.where((p) => p.paidAt != null).toList() + ..sort((a, b) => a.paidAt!.compareTo(b.paidAt!)); + + // If no data, show empty state or simple placeholder + if (validPayments.isEmpty) { + return Container( + height: 200, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + ), + child: const Center(child: Text("No sufficient data for graph")), + ); + } + + final spots = _generateSpots(validPayments); + final maxX = spots.isNotEmpty ? spots.last.x : 0.0; + final maxY = spots.isNotEmpty ? spots.map((s) => s.y).reduce((a, b) => a > b ? a : b) : 0.0; + + return Container( + height: 220, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + offset: const Offset(0, 4), + blurRadius: 12, + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "Earnings Trend", + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: Color(0xFF0F172A), + ), + ), + const SizedBox(height: 16), + Expanded( + child: LineChart( + LineChartData( + gridData: const FlGridData(show: false), + titlesData: FlTitlesData( + bottomTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + getTitlesWidget: (value, meta) { + // Simple logic to show a few dates + if (value % 2 != 0) return const SizedBox(); + final index = value.toInt(); + if (index >= 0 && index < validPayments.length) { + return Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text( + DateFormat('d').format(validPayments[index].paidAt!), + style: const TextStyle(fontSize: 10, color: Colors.grey), + ), + ); + } + return const SizedBox(); + }, + ), + ), + leftTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)), + topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)), + rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)), + ), + borderData: FlBorderData(show: false), + lineBarsData: [ + LineChartBarData( + spots: spots, + isCurved: true, + color: const Color(0xFF0032A0), + barWidth: 3, + isStrokeCapRound: true, + dotData: const FlDotData(show: false), + belowBarData: BarAreaData( + show: true, + color: const Color(0xFF0032A0).withOpacity(0.1), + ), + ), + ], + minX: 0, + maxX: (spots.length - 1).toDouble(), + minY: 0, + maxY: maxY * 1.2, + ), + ), + ), + ], + ), + ); + } + + List _generateSpots(List data) { + // Generate spots based on index in the list for simplicity in this demo + // Real implementation would map to actual dates on X-axis + return List.generate(data.length, (index) { + return FlSpot(index.toDouble(), data[index].amount); + }); + } +} diff --git a/apps/mobile/packages/features/staff/payments/lib/src/presentation/widgets/pending_pay_card.dart b/apps/mobile/packages/features/staff/payments/lib/src/presentation/widgets/pending_pay_card.dart index 3ca7c602..833a119e 100644 --- a/apps/mobile/packages/features/staff/payments/lib/src/presentation/widgets/pending_pay_card.dart +++ b/apps/mobile/packages/features/staff/payments/lib/src/presentation/widgets/pending_pay_card.dart @@ -72,6 +72,7 @@ class PendingPayCard extends StatelessWidget { ), ], ), + /* ElevatedButton.icon( onPressed: onCashOut, icon: const Icon(LucideIcons.zap, size: 14), @@ -91,6 +92,7 @@ class PendingPayCard extends StatelessWidget { ), ), ), + */ ], ), ); diff --git a/apps/mobile/packages/features/staff/payments/pubspec.yaml b/apps/mobile/packages/features/staff/payments/pubspec.yaml index 5f3a2c18..22435a8f 100644 --- a/apps/mobile/packages/features/staff/payments/pubspec.yaml +++ b/apps/mobile/packages/features/staff/payments/pubspec.yaml @@ -11,9 +11,11 @@ environment: dependencies: flutter: sdk: flutter + firebase_data_connect: flutter_modular: ^6.3.2 lucide_icons: ^0.257.0 intl: ^0.20.0 + fl_chart: ^0.66.0 # Internal packages design_system: @@ -30,4 +32,5 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^3.0.0 + flutter_lints: ^6.0.0 + \ No newline at end of file diff --git a/apps/mobile/packages/features/staff/profile/lib/src/presentation/pages/staff_profile_page.dart b/apps/mobile/packages/features/staff/profile/lib/src/presentation/pages/staff_profile_page.dart index 4930ee08..15f6d155 100644 --- a/apps/mobile/packages/features/staff/profile/lib/src/presentation/pages/staff_profile_page.dart +++ b/apps/mobile/packages/features/staff/profile/lib/src/presentation/pages/staff_profile_page.dart @@ -124,73 +124,34 @@ class StaffProfilePage extends StatelessWidget { ProfileMenuItem( icon: UiIcons.user, label: i18n.menu_items.personal_info, - completed: profile.phone != null, onTap: () => Modular.to.pushPersonalInfo(), ), ProfileMenuItem( icon: UiIcons.phone, label: i18n.menu_items.emergency_contact, - completed: false, onTap: () => Modular.to.pushEmergencyContact(), ), ProfileMenuItem( icon: UiIcons.briefcase, label: i18n.menu_items.experience, - completed: false, onTap: () => Modular.to.pushExperience(), ), - ProfileMenuItem( - icon: UiIcons.user, - label: i18n.menu_items.attire, - completed: false, - onTap: () => Modular.to.pushAttire(), - ), ], ), const SizedBox(height: UiConstants.space6), - SectionTitle(i18n.sections.compliance), - ProfileMenuGrid( - crossAxisCount: 3, + Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - ProfileMenuItem( - icon: UiIcons.file, - label: i18n.menu_items.documents, - completed: false, - onTap: () => Modular.to.pushDocuments(), - ), - ProfileMenuItem( - icon: UiIcons.shield, - label: i18n.menu_items.certificates, - completed: false, - onTap: () => Modular.to.pushCertificates(), - ), - ProfileMenuItem( - icon: UiIcons.file, - label: i18n.menu_items.tax_forms, - completed: false, - onTap: () => Modular.to.pushTaxForms(), - ), - ], - ), - const SizedBox(height: UiConstants.space6), - SectionTitle(i18n.sections.level_up), - ProfileMenuGrid( - crossAxisCount: 3, - children: [ - ProfileMenuItem( - icon: UiIcons.sparkles, - label: i18n.menu_items.krow_university, - onTap: () => Modular.to.pushKrowUniversity(), - ), - ProfileMenuItem( - icon: UiIcons.briefcase, - label: i18n.menu_items.trainings, - onTap: () => Modular.to.pushTrainings(), - ), - ProfileMenuItem( - icon: UiIcons.target, - label: i18n.menu_items.leaderboard, - onTap: () => Modular.to.pushLeaderboard(), + SectionTitle(i18n.sections.compliance), + ProfileMenuGrid( + crossAxisCount: 3, + children: [ + ProfileMenuItem( + icon: UiIcons.file, + label: i18n.menu_items.tax_forms, + onTap: () => Modular.to.pushTaxForms(), + ), + ], ), ], ), @@ -217,31 +178,10 @@ class StaffProfilePage extends StatelessWidget { ], ), const SizedBox(height: UiConstants.space6), - SectionTitle(i18n.sections.support), - ProfileMenuGrid( - crossAxisCount: 3, - children: [ - ProfileMenuItem( - icon: UiIcons.help, - label: i18n.menu_items.faqs, - onTap: () => Modular.to.pushFaqs(), - ), - ProfileMenuItem( - icon: UiIcons.shield, - label: i18n.menu_items.privacy_security, - onTap: () => Modular.to.pushPrivacy(), - ), - ProfileMenuItem( - icon: UiIcons.messageCircle, - label: i18n.menu_items.messages, - onTap: () => Modular.to.pushMessages(), - ), - ], - ), - const SizedBox(height: UiConstants.space6), LogoutButton( onTap: () => _onSignOut(cubit, state), ), + const SizedBox(height: UiConstants.space12), ], ), ), diff --git a/apps/mobile/packages/features/staff/profile/lib/src/presentation/widgets/profile_menu_grid.dart b/apps/mobile/packages/features/staff/profile/lib/src/presentation/widgets/profile_menu_grid.dart index 960eec89..ad00b1eb 100644 --- a/apps/mobile/packages/features/staff/profile/lib/src/presentation/widgets/profile_menu_grid.dart +++ b/apps/mobile/packages/features/staff/profile/lib/src/presentation/widgets/profile_menu_grid.dart @@ -16,7 +16,7 @@ class ProfileMenuGrid extends StatelessWidget { @override Widget build(BuildContext context) { // Spacing between items - final double spacing = UiConstants.space3; + const double spacing = UiConstants.space3; return LayoutBuilder( builder: (context, constraints) { @@ -27,6 +27,8 @@ class ProfileMenuGrid extends StatelessWidget { return Wrap( spacing: spacing, runSpacing: spacing, + alignment: WrapAlignment.start, + crossAxisAlignment: WrapCrossAlignment.start, children: children.map((child) { return SizedBox( width: itemWidth, diff --git a/apps/mobile/packages/features/staff/profile/lib/src/presentation/widgets/profile_menu_item.dart b/apps/mobile/packages/features/staff/profile/lib/src/presentation/widgets/profile_menu_item.dart index 31e064b3..6fafeaa9 100644 --- a/apps/mobile/packages/features/staff/profile/lib/src/presentation/widgets/profile_menu_item.dart +++ b/apps/mobile/packages/features/staff/profile/lib/src/presentation/widgets/profile_menu_item.dart @@ -39,16 +39,16 @@ class ProfileMenuItem extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, children: [ Container( - width: 48, - height: 48, + width: 36, + height: 36, decoration: BoxDecoration( color: UiColors.primary.withOpacity(0.08), borderRadius: BorderRadius.circular(UiConstants.radiusBase), ), alignment: Alignment.center, - child: Icon(icon, color: UiColors.primary, size: 24), + child: Icon(icon, color: UiColors.primary, size: 20), ), - SizedBox(height: UiConstants.space2), + SizedBox(height: UiConstants.space1), Padding( padding: EdgeInsets.symmetric(horizontal: UiConstants.space1), child: Text( diff --git a/apps/mobile/packages/features/staff/profile_sections/compliance/certificates/lib/src/data/repositories_impl/certificates_repository_impl.dart b/apps/mobile/packages/features/staff/profile_sections/compliance/certificates/lib/src/data/repositories_impl/certificates_repository_impl.dart index 18ce89df..90c40c0e 100644 --- a/apps/mobile/packages/features/staff/profile_sections/compliance/certificates/lib/src/data/repositories_impl/certificates_repository_impl.dart +++ b/apps/mobile/packages/features/staff/profile_sections/compliance/certificates/lib/src/data/repositories_impl/certificates_repository_impl.dart @@ -2,6 +2,7 @@ import 'package:firebase_auth/firebase_auth.dart'; import 'package:firebase_data_connect/firebase_data_connect.dart'; import 'package:krow_data_connect/krow_data_connect.dart'; import 'package:krow_domain/krow_domain.dart' as domain; +import 'package:krow_core/core.dart'; import '../../domain/repositories/certificates_repository.dart'; @@ -63,7 +64,9 @@ class CertificatesRepositoryImpl implements CertificatesRepository { description: null, // Description not available in this query response status: _mapStatus(doc.status), documentUrl: doc.documentUrl, - expiryDate: doc.expiryDate?.toDateTime(), + expiryDate: doc.expiryDate == null + ? null + : DateTimeUtils.toDeviceTime(doc.expiryDate!.toDateTime()), ); } diff --git a/apps/mobile/packages/features/staff/profile_sections/compliance/certificates/pubspec.lock b/apps/mobile/packages/features/staff/profile_sections/compliance/certificates/pubspec.lock deleted file mode 100644 index 7f71a701..00000000 --- a/apps/mobile/packages/features/staff/profile_sections/compliance/certificates/pubspec.lock +++ /dev/null @@ -1,1002 +0,0 @@ -# Generated by pub -# See https://dart.dev/tools/pub/glossary#lockfile -packages: - _fe_analyzer_shared: - dependency: transitive - description: - name: _fe_analyzer_shared - sha256: c209688d9f5a5f26b2fb47a188131a6fb9e876ae9e47af3737c0b4f58a93470d - url: "https://pub.dev" - source: hosted - version: "91.0.0" - _flutterfire_internals: - dependency: transitive - description: - name: _flutterfire_internals - sha256: ff0a84a2734d9e1089f8aedd5c0af0061b82fb94e95260d943404e0ef2134b11 - url: "https://pub.dev" - source: hosted - version: "1.3.59" - analyzer: - dependency: transitive - description: - name: analyzer - sha256: f51c8499b35f9b26820cfe914828a6a98a94efd5cc78b37bb7d03debae3a1d08 - url: "https://pub.dev" - source: hosted - version: "8.4.1" - archive: - dependency: transitive - description: - name: archive - sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d - url: "https://pub.dev" - source: hosted - version: "3.6.1" - args: - dependency: transitive - description: - name: args - sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 - url: "https://pub.dev" - source: hosted - version: "2.7.0" - async: - dependency: transitive - description: - name: async - sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" - url: "https://pub.dev" - source: hosted - version: "2.13.0" - auto_injector: - dependency: transitive - description: - name: auto_injector - sha256: "1fc2624898e92485122eb2b1698dd42511d7ff6574f84a3a8606fc4549a1e8f8" - url: "https://pub.dev" - source: hosted - version: "2.1.1" - bloc: - dependency: transitive - description: - name: bloc - sha256: "106842ad6569f0b60297619e9e0b1885c2fb9bf84812935490e6c5275777804e" - url: "https://pub.dev" - source: hosted - version: "8.1.4" - bloc_test: - dependency: "direct dev" - description: - name: bloc_test - sha256: "165a6ec950d9252ebe36dc5335f2e6eb13055f33d56db0eeb7642768849b43d2" - url: "https://pub.dev" - source: hosted - version: "9.1.7" - boolean_selector: - dependency: transitive - description: - name: boolean_selector - sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" - url: "https://pub.dev" - source: hosted - version: "2.1.2" - characters: - dependency: transitive - description: - name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 - url: "https://pub.dev" - source: hosted - version: "1.4.0" - cli_config: - dependency: transitive - description: - name: cli_config - sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec - url: "https://pub.dev" - source: hosted - version: "0.2.0" - clock: - dependency: transitive - description: - name: clock - sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b - url: "https://pub.dev" - source: hosted - version: "1.1.2" - code_assets: - dependency: transitive - description: - name: code_assets - sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687" - url: "https://pub.dev" - source: hosted - version: "1.0.0" - collection: - dependency: transitive - description: - name: collection - sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" - url: "https://pub.dev" - source: hosted - version: "1.19.1" - convert: - dependency: transitive - description: - name: convert - sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 - url: "https://pub.dev" - source: hosted - version: "3.1.2" - core_localization: - dependency: "direct main" - description: - path: "../../../../../core_localization" - relative: true - source: path - version: "0.0.1" - coverage: - dependency: transitive - description: - name: coverage - sha256: "5da775aa218eaf2151c721b16c01c7676fbfdd99cebba2bf64e8b807a28ff94d" - url: "https://pub.dev" - source: hosted - version: "1.15.0" - crypto: - dependency: transitive - description: - name: crypto - sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf - url: "https://pub.dev" - source: hosted - version: "3.0.7" - csv: - dependency: transitive - description: - name: csv - sha256: c6aa2679b2a18cb57652920f674488d89712efaf4d3fdf2e537215b35fc19d6c - url: "https://pub.dev" - source: hosted - version: "6.0.0" - design_system: - dependency: "direct main" - description: - path: "../../../../../design_system" - relative: true - source: path - version: "0.0.1" - diff_match_patch: - dependency: transitive - description: - name: diff_match_patch - sha256: "2efc9e6e8f449d0abe15be240e2c2a3bcd977c8d126cfd70598aee60af35c0a4" - url: "https://pub.dev" - source: hosted - version: "0.4.1" - equatable: - dependency: "direct main" - description: - name: equatable - sha256: "3e0141505477fd8ad55d6eb4e7776d3fe8430be8e497ccb1521370c3f21a3e2b" - url: "https://pub.dev" - source: hosted - version: "2.0.8" - fake_async: - dependency: transitive - description: - name: fake_async - sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" - url: "https://pub.dev" - source: hosted - version: "1.3.3" - ffi: - dependency: transitive - description: - name: ffi - sha256: d07d37192dbf97461359c1518788f203b0c9102cfd2c35a716b823741219542c - url: "https://pub.dev" - source: hosted - version: "2.1.5" - file: - dependency: transitive - description: - name: file - sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 - url: "https://pub.dev" - source: hosted - version: "7.0.1" - firebase_app_check: - dependency: transitive - description: - name: firebase_app_check - sha256: c4124632094a4062d7a1ff0a9f9c657ff54bece5d8393af4626cb191351a2aac - url: "https://pub.dev" - source: hosted - version: "0.3.2+10" - firebase_app_check_platform_interface: - dependency: transitive - description: - name: firebase_app_check_platform_interface - sha256: "4ca80bcc6c5c55289514d85e7c8ba8bc354342d23ab807b01c3f82e2fc7158e4" - url: "https://pub.dev" - source: hosted - version: "0.1.1+10" - firebase_app_check_web: - dependency: transitive - description: - name: firebase_app_check_web - sha256: b3150a78fe18c27525af05b149724ee33bd8592a5959e484fdfa5c98e25edb5f - url: "https://pub.dev" - source: hosted - version: "0.2.0+14" - firebase_auth: - dependency: "direct main" - description: - name: firebase_auth - sha256: "0fed2133bee1369ee1118c1fef27b2ce0d84c54b7819a2b17dada5cfec3b03ff" - url: "https://pub.dev" - source: hosted - version: "5.7.0" - firebase_auth_platform_interface: - dependency: transitive - description: - name: firebase_auth_platform_interface - sha256: "871c9df4ec9a754d1a793f7eb42fa3b94249d464cfb19152ba93e14a5966b386" - url: "https://pub.dev" - source: hosted - version: "7.7.3" - firebase_auth_web: - dependency: transitive - description: - name: firebase_auth_web - sha256: d9ada769c43261fd1b18decf113186e915c921a811bd5014f5ea08f4cf4bc57e - url: "https://pub.dev" - source: hosted - version: "5.15.3" - firebase_core: - dependency: transitive - description: - name: firebase_core - sha256: "7be63a3f841fc9663342f7f3a011a42aef6a61066943c90b1c434d79d5c995c5" - url: "https://pub.dev" - source: hosted - version: "3.15.2" - firebase_core_platform_interface: - dependency: transitive - description: - name: firebase_core_platform_interface - sha256: cccb4f572325dc14904c02fcc7db6323ad62ba02536833dddb5c02cac7341c64 - url: "https://pub.dev" - source: hosted - version: "6.0.2" - firebase_core_web: - dependency: transitive - description: - name: firebase_core_web - sha256: "0ed0dc292e8f9ac50992e2394e9d336a0275b6ae400d64163fdf0a8a8b556c37" - url: "https://pub.dev" - source: hosted - version: "2.24.1" - firebase_data_connect: - dependency: "direct main" - description: - name: firebase_data_connect - sha256: decc24f2ce21a305aa38f4840302aa893ad5cafd7ee4e05c2eb8a2808cb21c97 - url: "https://pub.dev" - source: hosted - version: "0.1.5+4" - fixnum: - dependency: transitive - description: - name: fixnum - sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be - url: "https://pub.dev" - source: hosted - version: "1.1.1" - flutter: - dependency: "direct main" - description: flutter - source: sdk - version: "0.0.0" - flutter_bloc: - dependency: "direct main" - description: - name: flutter_bloc - sha256: b594505eac31a0518bdcb4b5b79573b8d9117b193cc80cc12e17d639b10aa27a - url: "https://pub.dev" - source: hosted - version: "8.1.6" - flutter_localizations: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" - flutter_modular: - dependency: "direct main" - description: - name: flutter_modular - sha256: "33a63d9fe61429d12b3dfa04795ed890f17d179d3d38e988ba7969651fcd5586" - url: "https://pub.dev" - source: hosted - version: "6.4.1" - flutter_test: - dependency: "direct dev" - description: flutter - source: sdk - version: "0.0.0" - flutter_web_plugins: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" - font_awesome_flutter: - dependency: transitive - description: - name: font_awesome_flutter - sha256: b9011df3a1fa02993630b8fb83526368cf2206a711259830325bab2f1d2a4eb0 - url: "https://pub.dev" - source: hosted - version: "10.12.0" - frontend_server_client: - dependency: transitive - description: - name: frontend_server_client - sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 - url: "https://pub.dev" - source: hosted - version: "4.0.0" - get_it: - dependency: "direct main" - description: - name: get_it - sha256: d85128a5dae4ea777324730dc65edd9c9f43155c109d5cc0a69cab74139fbac1 - url: "https://pub.dev" - source: hosted - version: "7.7.0" - glob: - dependency: transitive - description: - name: glob - sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de - url: "https://pub.dev" - source: hosted - version: "2.1.3" - google_fonts: - dependency: transitive - description: - name: google_fonts - sha256: "6996212014b996eaa17074e02b1b925b212f5e053832d9048970dc27255a8fb3" - url: "https://pub.dev" - source: hosted - version: "7.1.0" - google_identity_services_web: - dependency: transitive - description: - name: google_identity_services_web - sha256: "5d187c46dc59e02646e10fe82665fc3884a9b71bc1c90c2b8b749316d33ee454" - url: "https://pub.dev" - source: hosted - version: "0.3.3+1" - googleapis_auth: - dependency: transitive - description: - name: googleapis_auth - sha256: befd71383a955535060acde8792e7efc11d2fccd03dd1d3ec434e85b68775938 - url: "https://pub.dev" - source: hosted - version: "1.6.0" - grpc: - dependency: transitive - description: - name: grpc - sha256: e93ee3bce45c134bf44e9728119102358c7cd69de7832d9a874e2e74eb8cab40 - url: "https://pub.dev" - source: hosted - version: "3.2.4" - hooks: - dependency: transitive - description: - name: hooks - sha256: "5d309c86e7ce34cd8e37aa71cb30cb652d3829b900ab145e4d9da564b31d59f7" - url: "https://pub.dev" - source: hosted - version: "1.0.0" - http: - dependency: transitive - description: - name: http - sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" - url: "https://pub.dev" - source: hosted - version: "1.6.0" - http2: - dependency: transitive - description: - name: http2 - sha256: "382d3aefc5bd6dc68c6b892d7664f29b5beb3251611ae946a98d35158a82bbfa" - url: "https://pub.dev" - source: hosted - version: "2.3.1" - http_multi_server: - dependency: transitive - description: - name: http_multi_server - sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 - url: "https://pub.dev" - source: hosted - version: "3.2.2" - http_parser: - dependency: transitive - description: - name: http_parser - sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" - url: "https://pub.dev" - source: hosted - version: "4.1.2" - intl: - dependency: transitive - description: - name: intl - sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" - url: "https://pub.dev" - source: hosted - version: "0.20.2" - io: - dependency: transitive - description: - name: io - sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b - url: "https://pub.dev" - source: hosted - version: "1.0.5" - js: - dependency: transitive - description: - name: js - sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc" - url: "https://pub.dev" - source: hosted - version: "0.7.2" - krow_core: - dependency: "direct main" - description: - path: "../../../../../core" - relative: true - source: path - version: "0.0.1" - krow_data_connect: - dependency: "direct main" - description: - path: "../../../../../data_connect" - relative: true - source: path - version: "0.0.1" - krow_domain: - dependency: "direct main" - description: - path: "../../../../../domain" - relative: true - source: path - version: "0.0.1" - leak_tracker: - dependency: transitive - description: - name: leak_tracker - sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" - url: "https://pub.dev" - source: hosted - version: "11.0.2" - leak_tracker_flutter_testing: - dependency: transitive - description: - name: leak_tracker_flutter_testing - sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" - url: "https://pub.dev" - source: hosted - version: "3.0.10" - leak_tracker_testing: - dependency: transitive - description: - name: leak_tracker_testing - sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" - url: "https://pub.dev" - source: hosted - version: "3.0.2" - logging: - dependency: transitive - description: - name: logging - sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 - url: "https://pub.dev" - source: hosted - version: "1.3.0" - lucide_icons: - dependency: transitive - description: - name: lucide_icons - sha256: ad24d0fd65707e48add30bebada7d90bff2a1bba0a72d6e9b19d44246b0e83c4 - url: "https://pub.dev" - source: hosted - version: "0.257.0" - matcher: - dependency: transitive - description: - name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 - url: "https://pub.dev" - source: hosted - version: "0.12.17" - material_color_utilities: - dependency: transitive - description: - name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec - url: "https://pub.dev" - source: hosted - version: "0.11.1" - meta: - dependency: transitive - description: - name: meta - sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" - url: "https://pub.dev" - source: hosted - version: "1.17.0" - mime: - dependency: transitive - description: - name: mime - sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" - url: "https://pub.dev" - source: hosted - version: "2.0.0" - mocktail: - dependency: "direct dev" - description: - name: mocktail - sha256: "890df3f9688106f25755f26b1c60589a92b3ab91a22b8b224947ad041bf172d8" - url: "https://pub.dev" - source: hosted - version: "1.0.4" - modular_core: - dependency: transitive - description: - name: modular_core - sha256: "1db0420a0dfb8a2c6dca846e7cbaa4ffeb778e247916dbcb27fb25aa566e5436" - url: "https://pub.dev" - source: hosted - version: "3.4.1" - native_toolchain_c: - dependency: transitive - description: - name: native_toolchain_c - sha256: "89e83885ba09da5fdf2cdacc8002a712ca238c28b7f717910b34bcd27b0d03ac" - url: "https://pub.dev" - source: hosted - version: "0.17.4" - nested: - dependency: transitive - description: - name: nested - sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" - url: "https://pub.dev" - source: hosted - version: "1.0.0" - node_preamble: - dependency: transitive - description: - name: node_preamble - sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" - url: "https://pub.dev" - source: hosted - version: "2.0.2" - objective_c: - dependency: transitive - description: - name: objective_c - sha256: "7fd0c4d8ac8980011753b9bdaed2bf15111365924cdeeeaeb596214ea2b03537" - url: "https://pub.dev" - source: hosted - version: "9.2.4" - package_config: - dependency: transitive - description: - name: package_config - sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc - url: "https://pub.dev" - source: hosted - version: "2.2.0" - path: - dependency: transitive - description: - name: path - sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" - url: "https://pub.dev" - source: hosted - version: "1.9.1" - path_provider: - dependency: transitive - description: - name: path_provider - sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" - url: "https://pub.dev" - source: hosted - version: "2.1.5" - path_provider_android: - dependency: transitive - description: - name: path_provider_android - sha256: f2c65e21139ce2c3dad46922be8272bb5963516045659e71bb16e151c93b580e - url: "https://pub.dev" - source: hosted - version: "2.2.22" - path_provider_foundation: - dependency: transitive - description: - name: path_provider_foundation - sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699" - url: "https://pub.dev" - source: hosted - version: "2.6.0" - path_provider_linux: - dependency: transitive - description: - name: path_provider_linux - sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 - url: "https://pub.dev" - source: hosted - version: "2.2.1" - path_provider_platform_interface: - dependency: transitive - description: - name: path_provider_platform_interface - sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" - url: "https://pub.dev" - source: hosted - version: "2.1.2" - path_provider_windows: - dependency: transitive - description: - name: path_provider_windows - sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 - url: "https://pub.dev" - source: hosted - version: "2.3.0" - platform: - dependency: transitive - description: - name: platform - sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" - url: "https://pub.dev" - source: hosted - version: "3.1.6" - plugin_platform_interface: - dependency: transitive - description: - name: plugin_platform_interface - sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" - url: "https://pub.dev" - source: hosted - version: "2.1.8" - pool: - dependency: transitive - description: - name: pool - sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d" - url: "https://pub.dev" - source: hosted - version: "1.5.2" - protobuf: - dependency: transitive - description: - name: protobuf - sha256: "68645b24e0716782e58948f8467fd42a880f255096a821f9e7d0ec625b00c84d" - url: "https://pub.dev" - source: hosted - version: "3.1.0" - provider: - dependency: transitive - description: - name: provider - sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272" - url: "https://pub.dev" - source: hosted - version: "6.1.5+1" - pub_semver: - dependency: transitive - description: - name: pub_semver - sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" - url: "https://pub.dev" - source: hosted - version: "2.2.0" - result_dart: - dependency: transitive - description: - name: result_dart - sha256: "0666b21fbdf697b3bdd9986348a380aa204b3ebe7c146d8e4cdaa7ce735e6054" - url: "https://pub.dev" - source: hosted - version: "2.1.1" - shared_preferences: - dependency: transitive - description: - name: shared_preferences - sha256: "2939ae520c9024cb197fc20dee269cd8cdbf564c8b5746374ec6cacdc5169e64" - url: "https://pub.dev" - source: hosted - version: "2.5.4" - shared_preferences_android: - dependency: transitive - description: - name: shared_preferences_android - sha256: "83af5c682796c0f7719c2bbf74792d113e40ae97981b8f266fa84574573556bc" - url: "https://pub.dev" - source: hosted - version: "2.4.18" - shared_preferences_foundation: - dependency: transitive - description: - name: shared_preferences_foundation - sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f" - url: "https://pub.dev" - source: hosted - version: "2.5.6" - shared_preferences_linux: - dependency: transitive - description: - name: shared_preferences_linux - sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" - url: "https://pub.dev" - source: hosted - version: "2.4.1" - shared_preferences_platform_interface: - dependency: transitive - description: - name: shared_preferences_platform_interface - sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" - url: "https://pub.dev" - source: hosted - version: "2.4.1" - shared_preferences_web: - dependency: transitive - description: - name: shared_preferences_web - sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 - url: "https://pub.dev" - source: hosted - version: "2.4.3" - shared_preferences_windows: - dependency: transitive - description: - name: shared_preferences_windows - sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" - url: "https://pub.dev" - source: hosted - version: "2.4.1" - shelf: - dependency: transitive - description: - name: shelf - sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 - url: "https://pub.dev" - source: hosted - version: "1.4.2" - shelf_packages_handler: - dependency: transitive - description: - name: shelf_packages_handler - sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" - url: "https://pub.dev" - source: hosted - version: "3.0.2" - shelf_static: - dependency: transitive - description: - name: shelf_static - sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3 - url: "https://pub.dev" - source: hosted - version: "1.1.3" - shelf_web_socket: - dependency: transitive - description: - name: shelf_web_socket - sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" - url: "https://pub.dev" - source: hosted - version: "3.0.0" - sky_engine: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" - slang: - dependency: transitive - description: - name: slang - sha256: "13e3b6f07adc51ab751e7889647774d294cbce7a3382f81d9e5029acfe9c37b2" - url: "https://pub.dev" - source: hosted - version: "4.12.0" - slang_flutter: - dependency: transitive - description: - name: slang_flutter - sha256: "0a4545cca5404d6b7487cf61cf1fe56c52daeb08de56a7574ee8381fbad035a0" - url: "https://pub.dev" - source: hosted - version: "4.12.0" - source_map_stack_trace: - dependency: transitive - description: - name: source_map_stack_trace - sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b - url: "https://pub.dev" - source: hosted - version: "2.1.2" - source_maps: - dependency: transitive - description: - name: source_maps - sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812" - url: "https://pub.dev" - source: hosted - version: "0.10.13" - source_span: - dependency: transitive - description: - name: source_span - sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" - url: "https://pub.dev" - source: hosted - version: "1.10.1" - stack_trace: - dependency: transitive - description: - name: stack_trace - sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" - url: "https://pub.dev" - source: hosted - version: "1.12.1" - stream_channel: - dependency: transitive - description: - name: stream_channel - sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" - url: "https://pub.dev" - source: hosted - version: "2.1.4" - string_scanner: - dependency: transitive - description: - name: string_scanner - sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" - url: "https://pub.dev" - source: hosted - version: "1.4.1" - term_glyph: - dependency: transitive - description: - name: term_glyph - sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" - url: "https://pub.dev" - source: hosted - version: "1.2.2" - test: - dependency: transitive - description: - name: test - sha256: "75906bf273541b676716d1ca7627a17e4c4070a3a16272b7a3dc7da3b9f3f6b7" - url: "https://pub.dev" - source: hosted - version: "1.26.3" - test_api: - dependency: transitive - description: - name: test_api - sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 - url: "https://pub.dev" - source: hosted - version: "0.7.7" - test_core: - dependency: transitive - description: - name: test_core - sha256: "0cc24b5ff94b38d2ae73e1eb43cc302b77964fbf67abad1e296025b78deb53d0" - url: "https://pub.dev" - source: hosted - version: "0.6.12" - typed_data: - dependency: transitive - description: - name: typed_data - sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 - url: "https://pub.dev" - source: hosted - version: "1.4.0" - uuid: - dependency: transitive - description: - name: uuid - sha256: a11b666489b1954e01d992f3d601b1804a33937b5a8fe677bd26b8a9f96f96e8 - url: "https://pub.dev" - source: hosted - version: "4.5.2" - vector_math: - dependency: transitive - description: - name: vector_math - sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b - url: "https://pub.dev" - source: hosted - version: "2.2.0" - vm_service: - dependency: transitive - description: - name: vm_service - sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" - url: "https://pub.dev" - source: hosted - version: "15.0.2" - watcher: - dependency: transitive - description: - name: watcher - sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635" - url: "https://pub.dev" - source: hosted - version: "1.2.1" - web: - dependency: transitive - description: - name: web - sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" - url: "https://pub.dev" - source: hosted - version: "1.1.1" - web_socket: - dependency: transitive - description: - name: web_socket - sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" - url: "https://pub.dev" - source: hosted - version: "1.0.1" - web_socket_channel: - dependency: transitive - description: - name: web_socket_channel - sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 - url: "https://pub.dev" - source: hosted - version: "3.0.3" - webkit_inspection_protocol: - dependency: transitive - description: - name: webkit_inspection_protocol - sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" - url: "https://pub.dev" - source: hosted - version: "1.2.1" - xdg_directories: - dependency: transitive - description: - name: xdg_directories - sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" - url: "https://pub.dev" - source: hosted - version: "1.1.0" - yaml: - dependency: transitive - description: - name: yaml - sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce - url: "https://pub.dev" - source: hosted - version: "3.1.3" -sdks: - dart: ">=3.10.7 <4.0.0" - flutter: ">=3.38.4" diff --git a/apps/mobile/packages/features/staff/profile_sections/compliance/certificates/pubspec.yaml b/apps/mobile/packages/features/staff/profile_sections/compliance/certificates/pubspec.yaml index b5cb42c3..e98a60a7 100644 --- a/apps/mobile/packages/features/staff/profile_sections/compliance/certificates/pubspec.yaml +++ b/apps/mobile/packages/features/staff/profile_sections/compliance/certificates/pubspec.yaml @@ -2,9 +2,10 @@ name: staff_certificates description: Staff certificates feature version: 0.0.1 publish_to: none +resolution: workspace environment: - sdk: '>=3.0.0 <4.0.0' + sdk: '>=3.10.0 <4.0.0' dependencies: flutter: diff --git a/apps/mobile/packages/features/staff/profile_sections/compliance/documents/lib/src/data/repositories_impl/documents_repository_impl.dart b/apps/mobile/packages/features/staff/profile_sections/compliance/documents/lib/src/data/repositories_impl/documents_repository_impl.dart index e6b06150..b30df9ad 100644 --- a/apps/mobile/packages/features/staff/profile_sections/compliance/documents/lib/src/data/repositories_impl/documents_repository_impl.dart +++ b/apps/mobile/packages/features/staff/profile_sections/compliance/documents/lib/src/data/repositories_impl/documents_repository_impl.dart @@ -2,6 +2,7 @@ import 'package:firebase_auth/firebase_auth.dart'; import 'package:firebase_data_connect/firebase_data_connect.dart'; import 'package:krow_data_connect/krow_data_connect.dart'; import 'package:krow_domain/krow_domain.dart' as domain; +import 'package:krow_core/core.dart'; import '../../domain/repositories/documents_repository.dart'; @@ -75,7 +76,9 @@ class DocumentsRepositoryImpl implements DocumentsRepository { description: null, // Description not available in data source status: _mapStatus(doc.status), documentUrl: doc.documentUrl, - expiryDate: doc.expiryDate?.toDateTime(), + expiryDate: doc.expiryDate == null + ? null + : DateTimeUtils.toDeviceTime(doc.expiryDate!.toDateTime()), ); } diff --git a/apps/mobile/packages/features/staff/profile_sections/compliance/documents/pubspec.lock b/apps/mobile/packages/features/staff/profile_sections/compliance/documents/pubspec.lock deleted file mode 100644 index 9b3416a2..00000000 --- a/apps/mobile/packages/features/staff/profile_sections/compliance/documents/pubspec.lock +++ /dev/null @@ -1,778 +0,0 @@ -# Generated by pub -# See https://dart.dev/tools/pub/glossary#lockfile -packages: - _flutterfire_internals: - dependency: transitive - description: - name: _flutterfire_internals - sha256: cd83f7d6bd4e4c0b0b4fef802e8796784032e1cc23d7b0e982cf5d05d9bbe182 - url: "https://pub.dev" - source: hosted - version: "1.3.66" - archive: - dependency: transitive - description: - name: archive - sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d - url: "https://pub.dev" - source: hosted - version: "3.6.1" - args: - dependency: transitive - description: - name: args - sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 - url: "https://pub.dev" - source: hosted - version: "2.7.0" - async: - dependency: transitive - description: - name: async - sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" - url: "https://pub.dev" - source: hosted - version: "2.13.0" - auto_injector: - dependency: transitive - description: - name: auto_injector - sha256: "1fc2624898e92485122eb2b1698dd42511d7ff6574f84a3a8606fc4549a1e8f8" - url: "https://pub.dev" - source: hosted - version: "2.1.1" - bloc: - dependency: "direct main" - description: - name: bloc - sha256: "106842ad6569f0b60297619e9e0b1885c2fb9bf84812935490e6c5275777804e" - url: "https://pub.dev" - source: hosted - version: "8.1.4" - boolean_selector: - dependency: transitive - description: - name: boolean_selector - sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" - url: "https://pub.dev" - source: hosted - version: "2.1.2" - characters: - dependency: transitive - description: - name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 - url: "https://pub.dev" - source: hosted - version: "1.4.0" - clock: - dependency: transitive - description: - name: clock - sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b - url: "https://pub.dev" - source: hosted - version: "1.1.2" - code_assets: - dependency: transitive - description: - name: code_assets - sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687" - url: "https://pub.dev" - source: hosted - version: "1.0.0" - collection: - dependency: transitive - description: - name: collection - sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" - url: "https://pub.dev" - source: hosted - version: "1.19.1" - core_localization: - dependency: "direct main" - description: - path: "../../../../../core_localization" - relative: true - source: path - version: "0.0.1" - crypto: - dependency: transitive - description: - name: crypto - sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf - url: "https://pub.dev" - source: hosted - version: "3.0.7" - csv: - dependency: transitive - description: - name: csv - sha256: c6aa2679b2a18cb57652920f674488d89712efaf4d3fdf2e537215b35fc19d6c - url: "https://pub.dev" - source: hosted - version: "6.0.0" - design_system: - dependency: "direct main" - description: - path: "../../../../../design_system" - relative: true - source: path - version: "0.0.1" - equatable: - dependency: "direct main" - description: - name: equatable - sha256: "3e0141505477fd8ad55d6eb4e7776d3fe8430be8e497ccb1521370c3f21a3e2b" - url: "https://pub.dev" - source: hosted - version: "2.0.8" - fake_async: - dependency: transitive - description: - name: fake_async - sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" - url: "https://pub.dev" - source: hosted - version: "1.3.3" - ffi: - dependency: transitive - description: - name: ffi - sha256: d07d37192dbf97461359c1518788f203b0c9102cfd2c35a716b823741219542c - url: "https://pub.dev" - source: hosted - version: "2.1.5" - file: - dependency: transitive - description: - name: file - sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 - url: "https://pub.dev" - source: hosted - version: "7.0.1" - firebase_app_check: - dependency: transitive - description: - name: firebase_app_check - sha256: "45f0d279ea7ae4eac1867a4c85aa225761e3ac0ccf646386a860b2bc16581f76" - url: "https://pub.dev" - source: hosted - version: "0.4.1+4" - firebase_app_check_platform_interface: - dependency: transitive - description: - name: firebase_app_check_platform_interface - sha256: e32b4e6adeaac207a6f7afe0906d97c0811de42fb200d9b6317a09155de65e2b - url: "https://pub.dev" - source: hosted - version: "0.2.1+4" - firebase_app_check_web: - dependency: transitive - description: - name: firebase_app_check_web - sha256: "2cbc8a18a34813a7e31d7b30f989973087421cd5d0e397b4dd88a90289aa2bed" - url: "https://pub.dev" - source: hosted - version: "0.2.2+2" - firebase_auth: - dependency: "direct main" - description: - name: firebase_auth - sha256: b20d1540460814c5984474c1e9dd833bdbcff6ecd8d6ad86cc9da8cfd581c172 - url: "https://pub.dev" - source: hosted - version: "6.1.4" - firebase_auth_platform_interface: - dependency: transitive - description: - name: firebase_auth_platform_interface - sha256: fd0225320b6bbc92460c86352d16b60aea15f9ef88292774cca97b0522ea9f72 - url: "https://pub.dev" - source: hosted - version: "8.1.6" - firebase_auth_web: - dependency: transitive - description: - name: firebase_auth_web - sha256: be7dccb263b89fbda2a564de9d8193118196e8481ffb937222a025cdfdf82c40 - url: "https://pub.dev" - source: hosted - version: "6.1.2" - firebase_core: - dependency: transitive - description: - name: firebase_core - sha256: "923085c881663ef685269b013e241b428e1fb03cdd0ebde265d9b40ff18abf80" - url: "https://pub.dev" - source: hosted - version: "4.4.0" - firebase_core_platform_interface: - dependency: transitive - description: - name: firebase_core_platform_interface - sha256: cccb4f572325dc14904c02fcc7db6323ad62ba02536833dddb5c02cac7341c64 - url: "https://pub.dev" - source: hosted - version: "6.0.2" - firebase_core_web: - dependency: transitive - description: - name: firebase_core_web - sha256: "83e7356c704131ca4d8d8dd57e360d8acecbca38b1a3705c7ae46cc34c708084" - url: "https://pub.dev" - source: hosted - version: "3.4.0" - firebase_data_connect: - dependency: "direct main" - description: - name: firebase_data_connect - sha256: "01d0f8e33c520a6e6f59cf5ac6ff281d1927f7837f094fa8eb5fdb0b1b328ad8" - url: "https://pub.dev" - source: hosted - version: "0.2.2+2" - fixnum: - dependency: transitive - description: - name: fixnum - sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be - url: "https://pub.dev" - source: hosted - version: "1.1.1" - flutter: - dependency: "direct main" - description: flutter - source: sdk - version: "0.0.0" - flutter_bloc: - dependency: "direct main" - description: - name: flutter_bloc - sha256: b594505eac31a0518bdcb4b5b79573b8d9117b193cc80cc12e17d639b10aa27a - url: "https://pub.dev" - source: hosted - version: "8.1.6" - flutter_localizations: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" - flutter_modular: - dependency: "direct main" - description: - name: flutter_modular - sha256: "33a63d9fe61429d12b3dfa04795ed890f17d179d3d38e988ba7969651fcd5586" - url: "https://pub.dev" - source: hosted - version: "6.4.1" - flutter_test: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" - flutter_web_plugins: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" - font_awesome_flutter: - dependency: transitive - description: - name: font_awesome_flutter - sha256: b9011df3a1fa02993630b8fb83526368cf2206a711259830325bab2f1d2a4eb0 - url: "https://pub.dev" - source: hosted - version: "10.12.0" - glob: - dependency: transitive - description: - name: glob - sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de - url: "https://pub.dev" - source: hosted - version: "2.1.3" - google_fonts: - dependency: transitive - description: - name: google_fonts - sha256: "6996212014b996eaa17074e02b1b925b212f5e053832d9048970dc27255a8fb3" - url: "https://pub.dev" - source: hosted - version: "7.1.0" - google_identity_services_web: - dependency: transitive - description: - name: google_identity_services_web - sha256: "5d187c46dc59e02646e10fe82665fc3884a9b71bc1c90c2b8b749316d33ee454" - url: "https://pub.dev" - source: hosted - version: "0.3.3+1" - googleapis_auth: - dependency: transitive - description: - name: googleapis_auth - sha256: befd71383a955535060acde8792e7efc11d2fccd03dd1d3ec434e85b68775938 - url: "https://pub.dev" - source: hosted - version: "1.6.0" - grpc: - dependency: transitive - description: - name: grpc - sha256: e93ee3bce45c134bf44e9728119102358c7cd69de7832d9a874e2e74eb8cab40 - url: "https://pub.dev" - source: hosted - version: "3.2.4" - hooks: - dependency: transitive - description: - name: hooks - sha256: "5d309c86e7ce34cd8e37aa71cb30cb652d3829b900ab145e4d9da564b31d59f7" - url: "https://pub.dev" - source: hosted - version: "1.0.0" - http: - dependency: transitive - description: - name: http - sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" - url: "https://pub.dev" - source: hosted - version: "1.6.0" - http2: - dependency: transitive - description: - name: http2 - sha256: "382d3aefc5bd6dc68c6b892d7664f29b5beb3251611ae946a98d35158a82bbfa" - url: "https://pub.dev" - source: hosted - version: "2.3.1" - http_parser: - dependency: transitive - description: - name: http_parser - sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" - url: "https://pub.dev" - source: hosted - version: "4.1.2" - intl: - dependency: transitive - description: - name: intl - sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" - url: "https://pub.dev" - source: hosted - version: "0.20.2" - krow_core: - dependency: "direct main" - description: - path: "../../../../../core" - relative: true - source: path - version: "0.0.1" - krow_data_connect: - dependency: "direct main" - description: - path: "../../../../../data_connect" - relative: true - source: path - version: "0.0.1" - krow_domain: - dependency: "direct main" - description: - path: "../../../../../domain" - relative: true - source: path - version: "0.0.1" - leak_tracker: - dependency: transitive - description: - name: leak_tracker - sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" - url: "https://pub.dev" - source: hosted - version: "11.0.2" - leak_tracker_flutter_testing: - dependency: transitive - description: - name: leak_tracker_flutter_testing - sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" - url: "https://pub.dev" - source: hosted - version: "3.0.10" - leak_tracker_testing: - dependency: transitive - description: - name: leak_tracker_testing - sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" - url: "https://pub.dev" - source: hosted - version: "3.0.2" - logging: - dependency: transitive - description: - name: logging - sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 - url: "https://pub.dev" - source: hosted - version: "1.3.0" - lucide_icons: - dependency: "direct main" - description: - name: lucide_icons - sha256: ad24d0fd65707e48add30bebada7d90bff2a1bba0a72d6e9b19d44246b0e83c4 - url: "https://pub.dev" - source: hosted - version: "0.257.0" - matcher: - dependency: transitive - description: - name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 - url: "https://pub.dev" - source: hosted - version: "0.12.17" - material_color_utilities: - dependency: transitive - description: - name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec - url: "https://pub.dev" - source: hosted - version: "0.11.1" - meta: - dependency: transitive - description: - name: meta - sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" - url: "https://pub.dev" - source: hosted - version: "1.17.0" - modular_core: - dependency: transitive - description: - name: modular_core - sha256: "1db0420a0dfb8a2c6dca846e7cbaa4ffeb778e247916dbcb27fb25aa566e5436" - url: "https://pub.dev" - source: hosted - version: "3.4.1" - native_toolchain_c: - dependency: transitive - description: - name: native_toolchain_c - sha256: "89e83885ba09da5fdf2cdacc8002a712ca238c28b7f717910b34bcd27b0d03ac" - url: "https://pub.dev" - source: hosted - version: "0.17.4" - nested: - dependency: transitive - description: - name: nested - sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" - url: "https://pub.dev" - source: hosted - version: "1.0.0" - objective_c: - dependency: transitive - description: - name: objective_c - sha256: "7fd0c4d8ac8980011753b9bdaed2bf15111365924cdeeeaeb596214ea2b03537" - url: "https://pub.dev" - source: hosted - version: "9.2.4" - path: - dependency: transitive - description: - name: path - sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" - url: "https://pub.dev" - source: hosted - version: "1.9.1" - path_provider: - dependency: transitive - description: - name: path_provider - sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" - url: "https://pub.dev" - source: hosted - version: "2.1.5" - path_provider_android: - dependency: transitive - description: - name: path_provider_android - sha256: f2c65e21139ce2c3dad46922be8272bb5963516045659e71bb16e151c93b580e - url: "https://pub.dev" - source: hosted - version: "2.2.22" - path_provider_foundation: - dependency: transitive - description: - name: path_provider_foundation - sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699" - url: "https://pub.dev" - source: hosted - version: "2.6.0" - path_provider_linux: - dependency: transitive - description: - name: path_provider_linux - sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 - url: "https://pub.dev" - source: hosted - version: "2.2.1" - path_provider_platform_interface: - dependency: transitive - description: - name: path_provider_platform_interface - sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" - url: "https://pub.dev" - source: hosted - version: "2.1.2" - path_provider_windows: - dependency: transitive - description: - name: path_provider_windows - sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 - url: "https://pub.dev" - source: hosted - version: "2.3.0" - platform: - dependency: transitive - description: - name: platform - sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" - url: "https://pub.dev" - source: hosted - version: "3.1.6" - plugin_platform_interface: - dependency: transitive - description: - name: plugin_platform_interface - sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" - url: "https://pub.dev" - source: hosted - version: "2.1.8" - protobuf: - dependency: transitive - description: - name: protobuf - sha256: "68645b24e0716782e58948f8467fd42a880f255096a821f9e7d0ec625b00c84d" - url: "https://pub.dev" - source: hosted - version: "3.1.0" - provider: - dependency: transitive - description: - name: provider - sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272" - url: "https://pub.dev" - source: hosted - version: "6.1.5+1" - pub_semver: - dependency: transitive - description: - name: pub_semver - sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" - url: "https://pub.dev" - source: hosted - version: "2.2.0" - result_dart: - dependency: transitive - description: - name: result_dart - sha256: "0666b21fbdf697b3bdd9986348a380aa204b3ebe7c146d8e4cdaa7ce735e6054" - url: "https://pub.dev" - source: hosted - version: "2.1.1" - shared_preferences: - dependency: transitive - description: - name: shared_preferences - sha256: "2939ae520c9024cb197fc20dee269cd8cdbf564c8b5746374ec6cacdc5169e64" - url: "https://pub.dev" - source: hosted - version: "2.5.4" - shared_preferences_android: - dependency: transitive - description: - name: shared_preferences_android - sha256: "83af5c682796c0f7719c2bbf74792d113e40ae97981b8f266fa84574573556bc" - url: "https://pub.dev" - source: hosted - version: "2.4.18" - shared_preferences_foundation: - dependency: transitive - description: - name: shared_preferences_foundation - sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f" - url: "https://pub.dev" - source: hosted - version: "2.5.6" - shared_preferences_linux: - dependency: transitive - description: - name: shared_preferences_linux - sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" - url: "https://pub.dev" - source: hosted - version: "2.4.1" - shared_preferences_platform_interface: - dependency: transitive - description: - name: shared_preferences_platform_interface - sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" - url: "https://pub.dev" - source: hosted - version: "2.4.1" - shared_preferences_web: - dependency: transitive - description: - name: shared_preferences_web - sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 - url: "https://pub.dev" - source: hosted - version: "2.4.3" - shared_preferences_windows: - dependency: transitive - description: - name: shared_preferences_windows - sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" - url: "https://pub.dev" - source: hosted - version: "2.4.1" - sky_engine: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" - slang: - dependency: transitive - description: - name: slang - sha256: "13e3b6f07adc51ab751e7889647774d294cbce7a3382f81d9e5029acfe9c37b2" - url: "https://pub.dev" - source: hosted - version: "4.12.0" - slang_flutter: - dependency: transitive - description: - name: slang_flutter - sha256: "0a4545cca5404d6b7487cf61cf1fe56c52daeb08de56a7574ee8381fbad035a0" - url: "https://pub.dev" - source: hosted - version: "4.12.0" - source_span: - dependency: transitive - description: - name: source_span - sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" - url: "https://pub.dev" - source: hosted - version: "1.10.1" - stack_trace: - dependency: transitive - description: - name: stack_trace - sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" - url: "https://pub.dev" - source: hosted - version: "1.12.1" - stream_channel: - dependency: transitive - description: - name: stream_channel - sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" - url: "https://pub.dev" - source: hosted - version: "2.1.4" - string_scanner: - dependency: transitive - description: - name: string_scanner - sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" - url: "https://pub.dev" - source: hosted - version: "1.4.1" - term_glyph: - dependency: transitive - description: - name: term_glyph - sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" - url: "https://pub.dev" - source: hosted - version: "1.2.2" - test_api: - dependency: transitive - description: - name: test_api - sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 - url: "https://pub.dev" - source: hosted - version: "0.7.7" - typed_data: - dependency: transitive - description: - name: typed_data - sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 - url: "https://pub.dev" - source: hosted - version: "1.4.0" - uuid: - dependency: transitive - description: - name: uuid - sha256: a11b666489b1954e01d992f3d601b1804a33937b5a8fe677bd26b8a9f96f96e8 - url: "https://pub.dev" - source: hosted - version: "4.5.2" - vector_math: - dependency: transitive - description: - name: vector_math - sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b - url: "https://pub.dev" - source: hosted - version: "2.2.0" - vm_service: - dependency: transitive - description: - name: vm_service - sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" - url: "https://pub.dev" - source: hosted - version: "15.0.2" - watcher: - dependency: transitive - description: - name: watcher - sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635" - url: "https://pub.dev" - source: hosted - version: "1.2.1" - web: - dependency: transitive - description: - name: web - sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" - url: "https://pub.dev" - source: hosted - version: "1.1.1" - xdg_directories: - dependency: transitive - description: - name: xdg_directories - sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" - url: "https://pub.dev" - source: hosted - version: "1.1.0" - yaml: - dependency: transitive - description: - name: yaml - sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce - url: "https://pub.dev" - source: hosted - version: "3.1.3" -sdks: - dart: ">=3.10.7 <4.0.0" - flutter: ">=3.38.4" diff --git a/apps/mobile/packages/features/staff/profile_sections/compliance/documents/pubspec.yaml b/apps/mobile/packages/features/staff/profile_sections/compliance/documents/pubspec.yaml index 8db95533..e6d64e0c 100644 --- a/apps/mobile/packages/features/staff/profile_sections/compliance/documents/pubspec.yaml +++ b/apps/mobile/packages/features/staff/profile_sections/compliance/documents/pubspec.yaml @@ -2,6 +2,7 @@ name: staff_documents description: Staff Documents feature. version: 0.0.1 publish_to: none +resolution: workspace environment: sdk: '>=3.10.0 <4.0.0' diff --git a/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/data/mappers/tax_form_mapper.dart b/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/data/mappers/tax_form_mapper.dart new file mode 100644 index 00000000..973cb983 --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/data/mappers/tax_form_mapper.dart @@ -0,0 +1,80 @@ +import 'package:firebase_data_connect/firebase_data_connect.dart'; +import 'package:krow_core/core.dart'; +import 'package:krow_data_connect/krow_data_connect.dart' as dc; +import 'package:krow_domain/krow_domain.dart'; + +class TaxFormMapper { + static TaxForm fromDataConnect(dc.GetTaxFormsByStaffIdTaxForms form) { + // Construct the legacy map for the entity + final Map formData = { + 'firstName': form.firstName, + 'lastName': form.lastName, + 'middleInitial': form.mInitial, + 'otherLastNames': form.oLastName, + 'dob': _formatDate(form.dob), + 'ssn': form.socialSN.toString(), + 'email': form.email, + 'phone': form.phone, + 'address': form.address, + 'aptNumber': form.apt, + 'city': form.city, + 'state': form.state, + 'zipCode': form.zipCode, + + // I-9 Fields + 'citizenshipStatus': form.citizen?.stringValue, + 'uscisNumber': form.uscis, + 'passportNumber': form.passportNumber, + 'countryIssuance': form.countryIssue, + 'preparerUsed': form.prepartorOrTranslator, + + // W-4 Fields + 'filingStatus': form.marital?.stringValue, + 'multipleJobs': form.multipleJob, + 'qualifyingChildren': form.childrens, + 'otherDependents': form.otherDeps, + 'otherIncome': form.otherInconme?.toString(), + 'deductions': form.deductions?.toString(), + 'extraWithholding': form.extraWithholding?.toString(), + + 'signature': form.signature, + }; + + String title = ''; + String subtitle = ''; + String description = ''; + + if (form.formType == dc.TaxFormType.I9) { + title = 'Form I-9'; + subtitle = 'Employment Eligibility Verification'; + description = 'Required for all new hires to verify identity.'; + } else { + title = 'Form W-4'; + subtitle = 'Employee\'s Withholding Certificate'; + description = 'Determines federal income tax withholding.'; + } + + return TaxFormAdapter.fromPrimitives( + id: form.id, + type: form.formType.stringValue, + title: title, + subtitle: subtitle, + description: description, + status: form.status.stringValue, + staffId: form.staffId, + formData: formData, + updatedAt: form.updatedAt == null + ? null + : DateTimeUtils.toDeviceTime(form.updatedAt!.toDateTime()), + ); + } + + static String? _formatDate(Timestamp? timestamp) { + if (timestamp == null) return null; + + final DateTime date = + DateTimeUtils.toDeviceTime(timestamp.toDateTime()); + + return '${date.month.toString().padLeft(2, '0')}/${date.day.toString().padLeft(2, '0')}/${date.year}'; + } +} diff --git a/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/data/repositories/tax_forms_repository_impl.dart b/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/data/repositories/tax_forms_repository_impl.dart index 5f8368a4..7307c194 100644 --- a/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/data/repositories/tax_forms_repository_impl.dart +++ b/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/data/repositories/tax_forms_repository_impl.dart @@ -6,6 +6,7 @@ import 'package:krow_data_connect/krow_data_connect.dart' as dc; import 'package:krow_domain/krow_domain.dart'; import '../../domain/repositories/tax_forms_repository.dart'; +import '../mappers/tax_form_mapper.dart'; class TaxFormsRepositoryImpl implements TaxFormsRepository { TaxFormsRepositoryImpl({ @@ -33,11 +34,11 @@ class TaxFormsRepositoryImpl implements TaxFormsRepository { @override Future> getTaxForms() async { final String staffId = _getStaffId(); - final QueryResult + final QueryResult result = - await dataConnect.getTaxFormsBystaffId(staffId: staffId).execute(); + await dataConnect.getTaxFormsByStaffId(staffId: staffId).execute(); - final List forms = result.data.taxForms.map((dc.GetTaxFormsBystaffIdTaxForms e) => _mapToEntity(e)).toList(); + final List forms = result.data.taxForms.map(TaxFormMapper.fromDataConnect).toList(); // Check if required forms exist, create if not. final Set typesPresent = forms.map((TaxForm f) => f.type).toSet(); @@ -53,98 +54,161 @@ class TaxFormsRepositoryImpl implements TaxFormsRepository { } if (createdNew) { - final QueryResult + final QueryResult result2 = - await dataConnect.getTaxFormsBystaffId(staffId: staffId).execute(); - return result2.data.taxForms.map((dc.GetTaxFormsBystaffIdTaxForms e) => _mapToEntity(e)).toList(); + await dataConnect.getTaxFormsByStaffId(staffId: staffId).execute(); + return result2.data.taxForms.map(TaxFormMapper.fromDataConnect).toList(); } return forms; } Future _createInitialForm(String staffId, TaxFormType type) async { - String title = ''; - String subtitle = ''; - String description = ''; - - if (type == TaxFormType.i9) { - title = 'Form I-9'; - subtitle = 'Employment Eligibility Verification'; - description = 'Required for all new hires to verify identity.'; - } else { - title = 'Form W-4'; - subtitle = 'Employee\'s Withholding Certificate'; - description = 'Determines federal income tax withholding.'; - } - await dataConnect .createTaxForm( staffId: staffId, - formType: dc.TaxFormType.values.byName(TaxFormAdapter.typeToString(type)), - title: title, + formType: + dc.TaxFormType.values.byName(TaxFormAdapter.typeToString(type)), + firstName: '', + lastName: '', + socialSN: 0, + address: '', + status: dc.TaxFormStatus.NOT_STARTED, ) - .subtitle(subtitle) - .description(description) - .status(dc.TaxFormStatus.NOT_STARTED) .execute(); } @override - Future submitForm(TaxFormType type, Map data) async { - final String staffId = _getStaffId(); - final QueryResult - result = - await dataConnect.getTaxFormsBystaffId(staffId: staffId).execute(); - final String targetTypeString = TaxFormAdapter.typeToString(type); - - final dc.GetTaxFormsBystaffIdTaxForms form = result.data.taxForms.firstWhere( - (dc.GetTaxFormsBystaffIdTaxForms e) => e.formType.stringValue == targetTypeString, - orElse: () => throw Exception('Form not found for submission'), - ); - - // AnyValue expects a scalar, list, or map. - await dataConnect - .updateTaxForm( - id: form.id, - ) - .formData(AnyValue.fromJson(data)) - .status(dc.TaxFormStatus.SUBMITTED) - .execute(); + Future updateI9Form(I9TaxForm form) async { + final Map data = form.formData; + final dc.UpdateTaxFormVariablesBuilder builder = dataConnect.updateTaxForm(id: form.id); + _mapCommonFields(builder, data); + _mapI9Fields(builder, data); + await builder.execute(); } @override - Future updateFormStatus(TaxFormType type, TaxFormStatus status) async { - final String staffId = _getStaffId(); - final QueryResult - result = - await dataConnect.getTaxFormsBystaffId(staffId: staffId).execute(); - final String targetTypeString = TaxFormAdapter.typeToString(type); - - final dc.GetTaxFormsBystaffIdTaxForms form = result.data.taxForms.firstWhere( - (dc.GetTaxFormsBystaffIdTaxForms e) => e.formType.stringValue == targetTypeString, - orElse: () => throw Exception('Form not found for update'), - ); - - await dataConnect - .updateTaxForm( - id: form.id, - ) - .status(dc.TaxFormStatus.values.byName(TaxFormAdapter.statusToString(status))) - .execute(); + Future submitI9Form(I9TaxForm form) async { + final Map data = form.formData; + final dc.UpdateTaxFormVariablesBuilder builder = dataConnect.updateTaxForm(id: form.id); + _mapCommonFields(builder, data); + _mapI9Fields(builder, data); + await builder.status(dc.TaxFormStatus.SUBMITTED).execute(); } - TaxForm _mapToEntity(dc.GetTaxFormsBystaffIdTaxForms form) { - return TaxFormAdapter.fromPrimitives( - id: form.id, - type: form.formType.stringValue, - title: form.title, - subtitle: form.subtitle, - description: form.description, - status: form.status.stringValue, - staffId: form.staffId, - formData: form.formData, // Adapter expects dynamic - updatedAt: form.updatedAt?.toDateTime(), - ); + @override + Future updateW4Form(W4TaxForm form) async { + final Map data = form.formData; + final dc.UpdateTaxFormVariablesBuilder builder = dataConnect.updateTaxForm(id: form.id); + _mapCommonFields(builder, data); + _mapW4Fields(builder, data); + await builder.execute(); + } + + @override + Future submitW4Form(W4TaxForm form) async { + final Map data = form.formData; + final dc.UpdateTaxFormVariablesBuilder builder = dataConnect.updateTaxForm(id: form.id); + _mapCommonFields(builder, data); + _mapW4Fields(builder, data); + await builder.status(dc.TaxFormStatus.SUBMITTED).execute(); + } + + void _mapCommonFields(dc.UpdateTaxFormVariablesBuilder builder, Map data) { + if (data.containsKey('firstName')) builder.firstName(data['firstName'] as String?); + if (data.containsKey('lastName')) builder.lastName(data['lastName'] as String?); + if (data.containsKey('middleInitial')) builder.mInitial(data['middleInitial'] as String?); + if (data.containsKey('otherLastNames')) builder.oLastName(data['otherLastNames'] as String?); + if (data.containsKey('dob')) { + final String dob = data['dob'] as String; + // Handle both ISO string and MM/dd/yyyy manual entry + DateTime? date; + try { + date = DateTime.parse(dob); + } catch (_) { + try { + // Fallback minimal parse for mm/dd/yyyy + final List parts = dob.split('/'); + if (parts.length == 3) { + date = DateTime( + int.parse(parts[2]), + int.parse(parts[0]), + int.parse(parts[1]), + ); + } + } catch (_) {} + } + if (date != null) { + final int ms = date.millisecondsSinceEpoch; + final int seconds = (ms / 1000).floor(); + builder.dob(Timestamp(0, seconds)); + } + } + if (data.containsKey('ssn') && data['ssn']?.toString().isNotEmpty == true) { + builder.socialSN(int.tryParse(data['ssn'].toString().replaceAll(RegExp(r'\D'), '')) ?? 0); + } + if (data.containsKey('email')) builder.email(data['email'] as String?); + if (data.containsKey('phone')) builder.phone(data['phone'] as String?); + if (data.containsKey('address')) builder.address(data['address'] as String?); + if (data.containsKey('aptNumber')) builder.apt(data['aptNumber'] as String?); + if (data.containsKey('city')) builder.city(data['city'] as String?); + if (data.containsKey('state')) builder.state(data['state'] as String?); + if (data.containsKey('zipCode')) builder.zipCode(data['zipCode'] as String?); + } + + void _mapI9Fields(dc.UpdateTaxFormVariablesBuilder builder, Map data) { + if (data.containsKey('citizenshipStatus')) { + final String status = data['citizenshipStatus'] as String; + // Map string to enum if possible, or handle otherwise. + // Generated enum: CITIZEN, NONCITIZEN_NATIONAL, PERMANENT_RESIDENT, ALIEN_AUTHORIZED + try { + builder.citizen(dc.CitizenshipStatus.values.byName(status.toUpperCase())); + } catch (_) {} + } + if (data.containsKey('uscisNumber')) builder.uscis(data['uscisNumber'] as String?); + if (data.containsKey('passportNumber')) builder.passportNumber(data['passportNumber'] as String?); + if (data.containsKey('countryIssuance')) builder.countryIssue(data['countryIssuance'] as String?); + if (data.containsKey('preparerUsed')) builder.prepartorOrTranslator(data['preparerUsed'] as bool?); + if (data.containsKey('signature')) builder.signature(data['signature'] as String?); + // Note: admissionNumber not in builder based on file read + } + + void _mapW4Fields(dc.UpdateTaxFormVariablesBuilder builder, Map data) { + if (data.containsKey('cityStateZip')) { + final String csz = data['cityStateZip'] as String; + // Extremely basic split: City, State Zip + final List parts = csz.split(','); + if (parts.length >= 2) { + builder.city(parts[0].trim()); + final String stateZip = parts[1].trim(); + final List szParts = stateZip.split(' '); + if (szParts.isNotEmpty) builder.state(szParts[0]); + if (szParts.length > 1) builder.zipCode(szParts.last); + } + } + if (data.containsKey('filingStatus')) { + // MARITIAL_STATUS_SINGLE, MARITIAL_STATUS_MARRIED, MARITIAL_STATUS_HEAD + try { + final String status = data['filingStatus'] as String; + // Simple mapping assumptions: + if (status.contains('single')) builder.marital(dc.MaritalStatus.SINGLE); + else if (status.contains('married')) builder.marital(dc.MaritalStatus.MARRIED); + else if (status.contains('head')) builder.marital(dc.MaritalStatus.HEAD); + } catch (_) {} + } + if (data.containsKey('multipleJobs')) builder.multipleJob(data['multipleJobs'] as bool?); + if (data.containsKey('qualifyingChildren')) builder.childrens(data['qualifyingChildren'] as int?); + if (data.containsKey('otherDependents')) builder.otherDeps(data['otherDependents'] as int?); + if (data.containsKey('otherIncome')) { + builder.otherInconme(double.tryParse(data['otherIncome'].toString())); + } + if (data.containsKey('deductions')) { + builder.deductions(double.tryParse(data['deductions'].toString())); + } + if (data.containsKey('extraWithholding')) { + builder.extraWithholding(double.tryParse(data['extraWithholding'].toString())); + } + if (data.containsKey('signature')) builder.signature(data['signature'] as String?); } } diff --git a/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/domain/entities/tax_form_entity.dart b/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/domain/entities/tax_form_entity.dart deleted file mode 100644 index c2ee5088..00000000 --- a/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/domain/entities/tax_form_entity.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'package:equatable/equatable.dart'; - -enum TaxFormType { i9, w4 } - -enum TaxFormStatus { notStarted, inProgress, submitted, approved, rejected } - -class TaxFormEntity extends Equatable { - final TaxFormType type; - final String title; - final String subtitle; - final String description; - final TaxFormStatus status; - final DateTime? lastUpdated; - - const TaxFormEntity({ - required this.type, - required this.title, - required this.subtitle, - required this.description, - this.status = TaxFormStatus.notStarted, - this.lastUpdated, - }); - - @override - List get props => [type, title, subtitle, description, status, lastUpdated]; -} diff --git a/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/domain/repositories/tax_forms_repository.dart b/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/domain/repositories/tax_forms_repository.dart index de7095f5..26f5b061 100644 --- a/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/domain/repositories/tax_forms_repository.dart +++ b/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/domain/repositories/tax_forms_repository.dart @@ -2,6 +2,8 @@ import 'package:krow_domain/krow_domain.dart'; abstract class TaxFormsRepository { Future> getTaxForms(); - Future submitForm(TaxFormType type, Map data); - Future updateFormStatus(TaxFormType type, TaxFormStatus status); + Future updateI9Form(I9TaxForm form); + Future submitI9Form(I9TaxForm form); + Future updateW4Form(W4TaxForm form); + Future submitW4Form(W4TaxForm form); } diff --git a/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/domain/usecases/save_i9_form_usecase.dart b/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/domain/usecases/save_i9_form_usecase.dart new file mode 100644 index 00000000..09c52e27 --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/domain/usecases/save_i9_form_usecase.dart @@ -0,0 +1,12 @@ +import 'package:krow_domain/krow_domain.dart'; +import '../repositories/tax_forms_repository.dart'; + +class SaveI9FormUseCase { + final TaxFormsRepository _repository; + + SaveI9FormUseCase(this._repository); + + Future call(I9TaxForm form) async { + return _repository.updateI9Form(form); + } +} diff --git a/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/domain/usecases/save_w4_form_usecase.dart b/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/domain/usecases/save_w4_form_usecase.dart new file mode 100644 index 00000000..995e090a --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/domain/usecases/save_w4_form_usecase.dart @@ -0,0 +1,12 @@ +import 'package:krow_domain/krow_domain.dart'; +import '../repositories/tax_forms_repository.dart'; + +class SaveW4FormUseCase { + final TaxFormsRepository _repository; + + SaveW4FormUseCase(this._repository); + + Future call(W4TaxForm form) async { + return _repository.updateW4Form(form); + } +} diff --git a/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/domain/usecases/submit_i9_form_usecase.dart b/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/domain/usecases/submit_i9_form_usecase.dart new file mode 100644 index 00000000..b57370c7 --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/domain/usecases/submit_i9_form_usecase.dart @@ -0,0 +1,12 @@ +import 'package:krow_domain/krow_domain.dart'; +import '../repositories/tax_forms_repository.dart'; + +class SubmitI9FormUseCase { + final TaxFormsRepository _repository; + + SubmitI9FormUseCase(this._repository); + + Future call(I9TaxForm form) async { + return _repository.submitI9Form(form); + } +} diff --git a/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/domain/usecases/submit_tax_form_usecase.dart b/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/domain/usecases/submit_tax_form_usecase.dart deleted file mode 100644 index c6a7f143..00000000 --- a/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/domain/usecases/submit_tax_form_usecase.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'package:krow_domain/krow_domain.dart'; -import '../repositories/tax_forms_repository.dart'; - -class SubmitTaxFormUseCase { - final TaxFormsRepository _repository; - - SubmitTaxFormUseCase(this._repository); - - Future call(TaxFormType type, Map data) async { - return _repository.submitForm(type, data); - } -} diff --git a/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/domain/usecases/submit_w4_form_usecase.dart b/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/domain/usecases/submit_w4_form_usecase.dart new file mode 100644 index 00000000..d4170855 --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/domain/usecases/submit_w4_form_usecase.dart @@ -0,0 +1,12 @@ +import 'package:krow_domain/krow_domain.dart'; +import '../repositories/tax_forms_repository.dart'; + +class SubmitW4FormUseCase { + final TaxFormsRepository _repository; + + SubmitW4FormUseCase(this._repository); + + Future call(W4TaxForm form) async { + return _repository.submitW4Form(form); + } +} diff --git a/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/presentation/blocs/i9/form_i9_cubit.dart b/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/presentation/blocs/i9/form_i9_cubit.dart index d4f1972a..4c7a6430 100644 --- a/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/presentation/blocs/i9/form_i9_cubit.dart +++ b/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/presentation/blocs/i9/form_i9_cubit.dart @@ -1,13 +1,47 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:krow_domain/krow_domain.dart'; +import 'package:uuid/uuid.dart'; -import '../../../domain/usecases/submit_tax_form_usecase.dart'; +import '../../../domain/usecases/submit_i9_form_usecase.dart'; import 'form_i9_state.dart'; class FormI9Cubit extends Cubit { - final SubmitTaxFormUseCase _submitTaxFormUseCase; + final SubmitI9FormUseCase _submitI9FormUseCase; + String _formId = ''; - FormI9Cubit(this._submitTaxFormUseCase) : super(const FormI9State()); + FormI9Cubit(this._submitI9FormUseCase) : super(const FormI9State()); + + void initialize(TaxForm? form) { + if (form == null || form.formData.isEmpty) { + emit(const FormI9State()); // Reset to empty if no form + return; + } + + final Map data = form.formData; + _formId = form.id; + emit(FormI9State( + firstName: data['firstName'] as String? ?? '', + lastName: data['lastName'] as String? ?? '', + middleInitial: data['middleInitial'] as String? ?? '', + otherLastNames: data['otherLastNames'] as String? ?? '', + dob: data['dob'] as String? ?? '', + ssn: data['ssn'] as String? ?? '', + email: data['email'] as String? ?? '', + phone: data['phone'] as String? ?? '', + address: data['address'] as String? ?? '', + aptNumber: data['aptNumber'] as String? ?? '', + city: data['city'] as String? ?? '', + state: data['state'] as String? ?? '', + zipCode: data['zipCode'] as String? ?? '', + citizenshipStatus: data['citizenshipStatus'] as String? ?? '', + uscisNumber: data['uscisNumber'] as String? ?? '', + admissionNumber: data['admissionNumber'] as String? ?? '', + passportNumber: data['passportNumber'] as String? ?? '', + countryIssuance: data['countryIssuance'] as String? ?? '', + preparerUsed: data['preparerUsed'] as bool? ?? false, + signature: data['signature'] as String? ?? '', + )); + } void nextStep(int totalSteps) { if (state.currentStep < totalSteps - 1) { @@ -52,18 +86,36 @@ class FormI9Cubit extends Cubit { Future submit() async { emit(state.copyWith(status: FormI9Status.submitting)); try { - await _submitTaxFormUseCase( - TaxFormType.i9, - { - 'firstName': state.firstName, - 'lastName': state.lastName, - 'middleInitial': state.middleInitial, - 'citizenshipStatus': state.citizenshipStatus, - 'ssn': state.ssn, - 'signature': state.signature, - // ... add other fields as needed for backend - }, + final Map formData = { + 'firstName': state.firstName, + 'lastName': state.lastName, + 'middleInitial': state.middleInitial, + 'otherLastNames': state.otherLastNames, + 'dob': state.dob, + 'ssn': state.ssn, + 'email': state.email, + 'phone': state.phone, + 'address': state.address, + 'aptNumber': state.aptNumber, + 'city': state.city, + 'state': state.state, + 'zipCode': state.zipCode, + 'citizenshipStatus': state.citizenshipStatus, + 'uscisNumber': state.uscisNumber, + 'admissionNumber': state.admissionNumber, + 'passportNumber': state.passportNumber, + 'countryIssuance': state.countryIssuance, + 'preparerUsed': state.preparerUsed, + 'signature': state.signature, + }; + + final I9TaxForm form = I9TaxForm( + id: _formId.isNotEmpty ? _formId : const Uuid().v4(), + title: 'Form I-9', + formData: formData, ); + + await _submitI9FormUseCase(form); emit(state.copyWith(status: FormI9Status.success)); } catch (e) { emit(state.copyWith( diff --git a/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/presentation/blocs/w4/form_w4_cubit.dart b/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/presentation/blocs/w4/form_w4_cubit.dart index 536a51fd..440ef51c 100644 --- a/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/presentation/blocs/w4/form_w4_cubit.dart +++ b/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/presentation/blocs/w4/form_w4_cubit.dart @@ -1,13 +1,47 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:krow_domain/krow_domain.dart'; +import 'package:uuid/uuid.dart'; -import '../../../domain/usecases/submit_tax_form_usecase.dart'; +import '../../../domain/usecases/submit_w4_form_usecase.dart'; import 'form_w4_state.dart'; class FormW4Cubit extends Cubit { - final SubmitTaxFormUseCase _submitTaxFormUseCase; + final SubmitW4FormUseCase _submitW4FormUseCase; + String _formId = ''; - FormW4Cubit(this._submitTaxFormUseCase) : super(const FormW4State()); + FormW4Cubit(this._submitW4FormUseCase) : super(const FormW4State()); + + void initialize(TaxForm? form) { + if (form == null || form.formData.isEmpty) { + emit(const FormW4State()); // Reset + return; + } + + final Map data = form.formData; + _formId = form.id; + + // Combine address parts if needed, or take existing + final String city = data['city'] as String? ?? ''; + final String stateVal = data['state'] as String? ?? ''; + final String zip = data['zipCode'] as String? ?? ''; + final String cityStateZip = '$city, $stateVal $zip'.trim(); + + emit(FormW4State( + firstName: data['firstName'] as String? ?? '', + lastName: data['lastName'] as String? ?? '', + ssn: data['ssn'] as String? ?? '', + address: data['address'] as String? ?? '', + cityStateZip: cityStateZip.contains(',') ? cityStateZip : '', + filingStatus: data['filingStatus'] as String? ?? '', + multipleJobs: data['multipleJobs'] as bool? ?? false, + qualifyingChildren: data['qualifyingChildren'] as int? ?? 0, + otherDependents: data['otherDependents'] as int? ?? 0, + otherIncome: data['otherIncome'] as String? ?? '', + deductions: data['deductions'] as String? ?? '', + extraWithholding: data['extraWithholding'] as String? ?? '', + signature: data['signature'] as String? ?? '', + )); + } void nextStep(int totalSteps) { if (state.currentStep < totalSteps - 1) { @@ -45,18 +79,29 @@ class FormW4Cubit extends Cubit { Future submit() async { emit(state.copyWith(status: FormW4Status.submitting)); try { - await _submitTaxFormUseCase( - TaxFormType.w4, - { - 'firstName': state.firstName, - 'lastName': state.lastName, - 'ssn': state.ssn, - 'filingStatus': state.filingStatus, - 'multipleJobs': state.multipleJobs, - 'signature': state.signature, - // ... add other fields as needed - }, + final Map formData = { + 'firstName': state.firstName, + 'lastName': state.lastName, + 'ssn': state.ssn, + 'address': state.address, + 'cityStateZip': state.cityStateZip, // Note: Repository should split this if needed. + 'filingStatus': state.filingStatus, + 'multipleJobs': state.multipleJobs, + 'qualifyingChildren': state.qualifyingChildren, + 'otherDependents': state.otherDependents, + 'otherIncome': state.otherIncome, + 'deductions': state.deductions, + 'extraWithholding': state.extraWithholding, + 'signature': state.signature, + }; + + final W4TaxForm form = W4TaxForm( + id: _formId.isNotEmpty ? _formId : const Uuid().v4(), + title: 'Form W-4', + formData: formData, ); + + await _submitW4FormUseCase(form); emit(state.copyWith(status: FormW4Status.success)); } catch (e) { emit(state.copyWith( diff --git a/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/presentation/pages/form_i9_page.dart b/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/presentation/pages/form_i9_page.dart index b62dd855..fc513b7a 100644 --- a/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/presentation/pages/form_i9_page.dart +++ b/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/presentation/pages/form_i9_page.dart @@ -2,12 +2,14 @@ import 'package:design_system/design_system.dart'; import 'package:flutter/material.dart'; import 'package:flutter_modular/flutter_modular.dart' hide ModularWatchExtension; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:krow_domain/krow_domain.dart'; import '../blocs/i9/form_i9_cubit.dart'; import '../blocs/i9/form_i9_state.dart'; class FormI9Page extends StatefulWidget { - const FormI9Page({super.key}); + final TaxForm? form; + const FormI9Page({super.key, this.form}); @override State createState() => _FormI9PageState(); @@ -22,6 +24,16 @@ class _FormI9PageState extends State { 'SD', 'TN', 'TX', 'UT', 'VT', 'VA', 'WA', 'WV', 'WI', 'WY' ]; + @override + void initState() { + super.initState(); + if (widget.form != null) { + // Use post-frame callback or simple direct call since we are using Modular.get in build + // But better helper: + Modular.get().initialize(widget.form); + } + } + final List> _steps = >[ {'title': 'Personal Information', 'subtitle': 'Name and contact details'}, {'title': 'Address', 'subtitle': 'Your current address'}, @@ -147,7 +159,7 @@ class _FormI9PageState extends State { SizedBox( width: double.infinity, child: ElevatedButton( - onPressed: () => Modular.to.pop(), + onPressed: () => Modular.to.pop(true), style: ElevatedButton.styleFrom( backgroundColor: UiColors.primary, foregroundColor: UiColors.bgPopup, @@ -501,23 +513,23 @@ class _FormI9PageState extends State { _buildRadioOption( context, state, - 'citizen', + 'CITIZEN', '1. A citizen of the United States', ), const SizedBox(height: 12), _buildRadioOption( context, state, - 'noncitizen_national', + 'NONCITIZEN', '2. A noncitizen national of the United States', ), const SizedBox(height: 12), _buildRadioOption( context, state, - 'permanent_resident', + 'PERMANENT_RESIDENT', '3. A lawful permanent resident', - child: state.citizenshipStatus == 'permanent_resident' + child: state.citizenshipStatus == 'PERMANENT_RESIDENT' ? Padding( padding: const EdgeInsets.only(top: 12), child: _buildTextField( @@ -533,9 +545,9 @@ class _FormI9PageState extends State { _buildRadioOption( context, state, - 'alien_authorized', + 'ALIEN', '4. An alien authorized to work', - child: state.citizenshipStatus == 'alien_authorized' + child: state.citizenshipStatus == 'ALIEN' ? Padding( padding: const EdgeInsets.only(top: 12), child: Column( @@ -753,13 +765,13 @@ class _FormI9PageState extends State { String _getReadableCitizenship(String status) { switch (status) { - case 'citizen': + case 'CITIZEN': return 'US Citizen'; - case 'noncitizen_national': + case 'NONCITIZEN': return 'Noncitizen National'; - case 'permanent_resident': + case 'PERMANENT_RESIDENT': return 'Permanent Resident'; - case 'alien_authorized': + case 'ALIEN': return 'Alien Authorized to Work'; default: return 'Unknown'; diff --git a/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/presentation/pages/form_w4_page.dart b/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/presentation/pages/form_w4_page.dart index d7eec588..7d147b91 100644 --- a/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/presentation/pages/form_w4_page.dart +++ b/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/presentation/pages/form_w4_page.dart @@ -2,18 +2,81 @@ import 'package:design_system/design_system.dart'; import 'package:flutter/material.dart'; import 'package:flutter_modular/flutter_modular.dart' hide ModularWatchExtension; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:krow_domain/krow_domain.dart'; import '../blocs/w4/form_w4_cubit.dart'; import '../blocs/w4/form_w4_state.dart'; class FormW4Page extends StatefulWidget { - const FormW4Page({super.key}); + final TaxForm? form; + const FormW4Page({super.key, this.form}); @override State createState() => _FormW4PageState(); } class _FormW4PageState extends State { + @override + void initState() { + super.initState(); + if (widget.form != null) { + Modular.get().initialize(widget.form); + } + } + + final List _usStates = [ + 'Alabama', + 'Alaska', + 'Arizona', + 'Arkansas', + 'California', + 'Colorado', + 'Connecticut', + 'Delaware', + 'Florida', + 'Georgia', + 'Hawaii', + 'Idaho', + 'Illinois', + 'Indiana', + 'Iowa', + 'Kansas', + 'Kentucky', + 'Louisiana', + 'Maine', + 'Maryland', + 'Massachusetts', + 'Michigan', + 'Minnesota', + 'Mississippi', + 'Missouri', + 'Montana', + 'Nebraska', + 'Nevada', + 'New Hampshire', + 'New Jersey', + 'New Mexico', + 'New York', + 'North Carolina', + 'North Dakota', + 'Ohio', + 'Oklahoma', + 'Oregon', + 'Pennsylvania', + 'Rhode Island', + 'South Carolina', + 'South Dakota', + 'Tennessee', + 'Texas', + 'Utah', + 'Vermont', + 'Virginia', + 'Washington', + 'West Virginia', + 'Wisconsin', + 'Wyoming', + ]; + final List> _steps = >[ {'title': 'Personal Information', 'subtitle': 'Step 1'}, {'title': 'Filing Status', 'subtitle': 'Step 1c'}, @@ -142,7 +205,7 @@ class _FormW4PageState extends State { SizedBox( width: double.infinity, child: ElevatedButton( - onPressed: () => Modular.to.pop(), + onPressed: () => Modular.to.pop(true), style: ElevatedButton.styleFrom( backgroundColor: UiColors.primary, foregroundColor: UiColors.bgPopup, @@ -397,7 +460,7 @@ class _FormW4PageState extends State { _buildRadioOption( context, state, - 'single', + 'SINGLE', 'Single or Married filing separately', null, ), @@ -405,7 +468,7 @@ class _FormW4PageState extends State { _buildRadioOption( context, state, - 'married', + 'MARRIED', 'Married filing jointly or Qualifying surviving spouse', null, ), @@ -413,7 +476,7 @@ class _FormW4PageState extends State { _buildRadioOption( context, state, - 'head_of_household', + 'HEAD', 'Head of household', 'Check only if you\'re unmarried and pay more than half the costs of keeping up a home', ), diff --git a/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/presentation/pages/tax_forms_page.dart b/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/presentation/pages/tax_forms_page.dart index c0abdb8b..f214a99b 100644 --- a/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/presentation/pages/tax_forms_page.dart +++ b/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/presentation/pages/tax_forms_page.dart @@ -75,7 +75,7 @@ class TaxFormsPage extends StatelessWidget { spacing: UiConstants.space6, children: [ _buildProgressOverview(state.forms), - ...state.forms.map(_buildFormCard), + ...state.forms.map((TaxForm form) => _buildFormCard(context, form)), _buildInfoCard(), ], ), @@ -138,16 +138,22 @@ class TaxFormsPage extends StatelessWidget { ); } - Widget _buildFormCard(TaxForm form) { + Widget _buildFormCard(BuildContext context, TaxForm form) { // Helper to get icon based on type (could be in entity or a mapper) - final String icon = form.type == TaxFormType.i9 ? '🛂' : '📋'; + final String icon = form is I9TaxForm ? '🛂' : '📋'; return GestureDetector( - onTap: () { - if (form.type == TaxFormType.i9) { - Modular.to.pushNamed('i9'); - } else if (form.type == TaxFormType.w4) { - Modular.to.pushNamed('w4'); + onTap: () async { + if (form is I9TaxForm) { + final result = await Modular.to.pushNamed('i9', arguments: form); + if (result == true && context.mounted) { + BlocProvider.of(context).loadTaxForms(); + } + } else if (form is W4TaxForm) { + final result = await Modular.to.pushNamed('w4', arguments: form); + if (result == true && context.mounted) { + BlocProvider.of(context).loadTaxForms(); + } } }, child: Container( diff --git a/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/staff_tax_forms_module.dart b/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/staff_tax_forms_module.dart index 18f67e4b..7b390a17 100644 --- a/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/staff_tax_forms_module.dart +++ b/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/staff_tax_forms_module.dart @@ -1,10 +1,12 @@ import 'package:firebase_auth/firebase_auth.dart'; import 'package:flutter_modular/flutter_modular.dart'; import 'package:krow_data_connect/krow_data_connect.dart'; +import 'package:krow_domain/krow_domain.dart'; import 'data/repositories/tax_forms_repository_impl.dart'; import 'domain/repositories/tax_forms_repository.dart'; import 'domain/usecases/get_tax_forms_usecase.dart'; -import 'domain/usecases/submit_tax_form_usecase.dart'; +import 'domain/usecases/submit_i9_form_usecase.dart'; +import 'domain/usecases/submit_w4_form_usecase.dart'; import 'presentation/blocs/i9/form_i9_cubit.dart'; import 'presentation/blocs/tax_forms/tax_forms_cubit.dart'; import 'presentation/blocs/w4/form_w4_cubit.dart'; @@ -24,7 +26,8 @@ class StaffTaxFormsModule extends Module { // Use Cases i.addLazySingleton(GetTaxFormsUseCase.new); - i.addLazySingleton(SubmitTaxFormUseCase.new); + i.addLazySingleton(SubmitI9FormUseCase.new); + i.addLazySingleton(SubmitW4FormUseCase.new); // Blocs i.addLazySingleton(TaxFormsCubit.new); @@ -35,7 +38,13 @@ class StaffTaxFormsModule extends Module { @override void routes(RouteManager r) { r.child('/', child: (_) => const TaxFormsPage()); - r.child('/i9', child: (_) => const FormI9Page()); - r.child('/w4', child: (_) => const FormW4Page()); + r.child( + '/i9', + child: (_) => FormI9Page(form: r.args.data as TaxForm?), + ); + r.child( + '/w4', + child: (_) => FormW4Page(form: r.args.data as TaxForm?), + ); } } diff --git a/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/pubspec.lock b/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/pubspec.lock deleted file mode 100644 index 9b3416a2..00000000 --- a/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/pubspec.lock +++ /dev/null @@ -1,778 +0,0 @@ -# Generated by pub -# See https://dart.dev/tools/pub/glossary#lockfile -packages: - _flutterfire_internals: - dependency: transitive - description: - name: _flutterfire_internals - sha256: cd83f7d6bd4e4c0b0b4fef802e8796784032e1cc23d7b0e982cf5d05d9bbe182 - url: "https://pub.dev" - source: hosted - version: "1.3.66" - archive: - dependency: transitive - description: - name: archive - sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d - url: "https://pub.dev" - source: hosted - version: "3.6.1" - args: - dependency: transitive - description: - name: args - sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 - url: "https://pub.dev" - source: hosted - version: "2.7.0" - async: - dependency: transitive - description: - name: async - sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" - url: "https://pub.dev" - source: hosted - version: "2.13.0" - auto_injector: - dependency: transitive - description: - name: auto_injector - sha256: "1fc2624898e92485122eb2b1698dd42511d7ff6574f84a3a8606fc4549a1e8f8" - url: "https://pub.dev" - source: hosted - version: "2.1.1" - bloc: - dependency: "direct main" - description: - name: bloc - sha256: "106842ad6569f0b60297619e9e0b1885c2fb9bf84812935490e6c5275777804e" - url: "https://pub.dev" - source: hosted - version: "8.1.4" - boolean_selector: - dependency: transitive - description: - name: boolean_selector - sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" - url: "https://pub.dev" - source: hosted - version: "2.1.2" - characters: - dependency: transitive - description: - name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 - url: "https://pub.dev" - source: hosted - version: "1.4.0" - clock: - dependency: transitive - description: - name: clock - sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b - url: "https://pub.dev" - source: hosted - version: "1.1.2" - code_assets: - dependency: transitive - description: - name: code_assets - sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687" - url: "https://pub.dev" - source: hosted - version: "1.0.0" - collection: - dependency: transitive - description: - name: collection - sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" - url: "https://pub.dev" - source: hosted - version: "1.19.1" - core_localization: - dependency: "direct main" - description: - path: "../../../../../core_localization" - relative: true - source: path - version: "0.0.1" - crypto: - dependency: transitive - description: - name: crypto - sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf - url: "https://pub.dev" - source: hosted - version: "3.0.7" - csv: - dependency: transitive - description: - name: csv - sha256: c6aa2679b2a18cb57652920f674488d89712efaf4d3fdf2e537215b35fc19d6c - url: "https://pub.dev" - source: hosted - version: "6.0.0" - design_system: - dependency: "direct main" - description: - path: "../../../../../design_system" - relative: true - source: path - version: "0.0.1" - equatable: - dependency: "direct main" - description: - name: equatable - sha256: "3e0141505477fd8ad55d6eb4e7776d3fe8430be8e497ccb1521370c3f21a3e2b" - url: "https://pub.dev" - source: hosted - version: "2.0.8" - fake_async: - dependency: transitive - description: - name: fake_async - sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" - url: "https://pub.dev" - source: hosted - version: "1.3.3" - ffi: - dependency: transitive - description: - name: ffi - sha256: d07d37192dbf97461359c1518788f203b0c9102cfd2c35a716b823741219542c - url: "https://pub.dev" - source: hosted - version: "2.1.5" - file: - dependency: transitive - description: - name: file - sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 - url: "https://pub.dev" - source: hosted - version: "7.0.1" - firebase_app_check: - dependency: transitive - description: - name: firebase_app_check - sha256: "45f0d279ea7ae4eac1867a4c85aa225761e3ac0ccf646386a860b2bc16581f76" - url: "https://pub.dev" - source: hosted - version: "0.4.1+4" - firebase_app_check_platform_interface: - dependency: transitive - description: - name: firebase_app_check_platform_interface - sha256: e32b4e6adeaac207a6f7afe0906d97c0811de42fb200d9b6317a09155de65e2b - url: "https://pub.dev" - source: hosted - version: "0.2.1+4" - firebase_app_check_web: - dependency: transitive - description: - name: firebase_app_check_web - sha256: "2cbc8a18a34813a7e31d7b30f989973087421cd5d0e397b4dd88a90289aa2bed" - url: "https://pub.dev" - source: hosted - version: "0.2.2+2" - firebase_auth: - dependency: "direct main" - description: - name: firebase_auth - sha256: b20d1540460814c5984474c1e9dd833bdbcff6ecd8d6ad86cc9da8cfd581c172 - url: "https://pub.dev" - source: hosted - version: "6.1.4" - firebase_auth_platform_interface: - dependency: transitive - description: - name: firebase_auth_platform_interface - sha256: fd0225320b6bbc92460c86352d16b60aea15f9ef88292774cca97b0522ea9f72 - url: "https://pub.dev" - source: hosted - version: "8.1.6" - firebase_auth_web: - dependency: transitive - description: - name: firebase_auth_web - sha256: be7dccb263b89fbda2a564de9d8193118196e8481ffb937222a025cdfdf82c40 - url: "https://pub.dev" - source: hosted - version: "6.1.2" - firebase_core: - dependency: transitive - description: - name: firebase_core - sha256: "923085c881663ef685269b013e241b428e1fb03cdd0ebde265d9b40ff18abf80" - url: "https://pub.dev" - source: hosted - version: "4.4.0" - firebase_core_platform_interface: - dependency: transitive - description: - name: firebase_core_platform_interface - sha256: cccb4f572325dc14904c02fcc7db6323ad62ba02536833dddb5c02cac7341c64 - url: "https://pub.dev" - source: hosted - version: "6.0.2" - firebase_core_web: - dependency: transitive - description: - name: firebase_core_web - sha256: "83e7356c704131ca4d8d8dd57e360d8acecbca38b1a3705c7ae46cc34c708084" - url: "https://pub.dev" - source: hosted - version: "3.4.0" - firebase_data_connect: - dependency: "direct main" - description: - name: firebase_data_connect - sha256: "01d0f8e33c520a6e6f59cf5ac6ff281d1927f7837f094fa8eb5fdb0b1b328ad8" - url: "https://pub.dev" - source: hosted - version: "0.2.2+2" - fixnum: - dependency: transitive - description: - name: fixnum - sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be - url: "https://pub.dev" - source: hosted - version: "1.1.1" - flutter: - dependency: "direct main" - description: flutter - source: sdk - version: "0.0.0" - flutter_bloc: - dependency: "direct main" - description: - name: flutter_bloc - sha256: b594505eac31a0518bdcb4b5b79573b8d9117b193cc80cc12e17d639b10aa27a - url: "https://pub.dev" - source: hosted - version: "8.1.6" - flutter_localizations: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" - flutter_modular: - dependency: "direct main" - description: - name: flutter_modular - sha256: "33a63d9fe61429d12b3dfa04795ed890f17d179d3d38e988ba7969651fcd5586" - url: "https://pub.dev" - source: hosted - version: "6.4.1" - flutter_test: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" - flutter_web_plugins: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" - font_awesome_flutter: - dependency: transitive - description: - name: font_awesome_flutter - sha256: b9011df3a1fa02993630b8fb83526368cf2206a711259830325bab2f1d2a4eb0 - url: "https://pub.dev" - source: hosted - version: "10.12.0" - glob: - dependency: transitive - description: - name: glob - sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de - url: "https://pub.dev" - source: hosted - version: "2.1.3" - google_fonts: - dependency: transitive - description: - name: google_fonts - sha256: "6996212014b996eaa17074e02b1b925b212f5e053832d9048970dc27255a8fb3" - url: "https://pub.dev" - source: hosted - version: "7.1.0" - google_identity_services_web: - dependency: transitive - description: - name: google_identity_services_web - sha256: "5d187c46dc59e02646e10fe82665fc3884a9b71bc1c90c2b8b749316d33ee454" - url: "https://pub.dev" - source: hosted - version: "0.3.3+1" - googleapis_auth: - dependency: transitive - description: - name: googleapis_auth - sha256: befd71383a955535060acde8792e7efc11d2fccd03dd1d3ec434e85b68775938 - url: "https://pub.dev" - source: hosted - version: "1.6.0" - grpc: - dependency: transitive - description: - name: grpc - sha256: e93ee3bce45c134bf44e9728119102358c7cd69de7832d9a874e2e74eb8cab40 - url: "https://pub.dev" - source: hosted - version: "3.2.4" - hooks: - dependency: transitive - description: - name: hooks - sha256: "5d309c86e7ce34cd8e37aa71cb30cb652d3829b900ab145e4d9da564b31d59f7" - url: "https://pub.dev" - source: hosted - version: "1.0.0" - http: - dependency: transitive - description: - name: http - sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" - url: "https://pub.dev" - source: hosted - version: "1.6.0" - http2: - dependency: transitive - description: - name: http2 - sha256: "382d3aefc5bd6dc68c6b892d7664f29b5beb3251611ae946a98d35158a82bbfa" - url: "https://pub.dev" - source: hosted - version: "2.3.1" - http_parser: - dependency: transitive - description: - name: http_parser - sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" - url: "https://pub.dev" - source: hosted - version: "4.1.2" - intl: - dependency: transitive - description: - name: intl - sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" - url: "https://pub.dev" - source: hosted - version: "0.20.2" - krow_core: - dependency: "direct main" - description: - path: "../../../../../core" - relative: true - source: path - version: "0.0.1" - krow_data_connect: - dependency: "direct main" - description: - path: "../../../../../data_connect" - relative: true - source: path - version: "0.0.1" - krow_domain: - dependency: "direct main" - description: - path: "../../../../../domain" - relative: true - source: path - version: "0.0.1" - leak_tracker: - dependency: transitive - description: - name: leak_tracker - sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" - url: "https://pub.dev" - source: hosted - version: "11.0.2" - leak_tracker_flutter_testing: - dependency: transitive - description: - name: leak_tracker_flutter_testing - sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" - url: "https://pub.dev" - source: hosted - version: "3.0.10" - leak_tracker_testing: - dependency: transitive - description: - name: leak_tracker_testing - sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" - url: "https://pub.dev" - source: hosted - version: "3.0.2" - logging: - dependency: transitive - description: - name: logging - sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 - url: "https://pub.dev" - source: hosted - version: "1.3.0" - lucide_icons: - dependency: "direct main" - description: - name: lucide_icons - sha256: ad24d0fd65707e48add30bebada7d90bff2a1bba0a72d6e9b19d44246b0e83c4 - url: "https://pub.dev" - source: hosted - version: "0.257.0" - matcher: - dependency: transitive - description: - name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 - url: "https://pub.dev" - source: hosted - version: "0.12.17" - material_color_utilities: - dependency: transitive - description: - name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec - url: "https://pub.dev" - source: hosted - version: "0.11.1" - meta: - dependency: transitive - description: - name: meta - sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" - url: "https://pub.dev" - source: hosted - version: "1.17.0" - modular_core: - dependency: transitive - description: - name: modular_core - sha256: "1db0420a0dfb8a2c6dca846e7cbaa4ffeb778e247916dbcb27fb25aa566e5436" - url: "https://pub.dev" - source: hosted - version: "3.4.1" - native_toolchain_c: - dependency: transitive - description: - name: native_toolchain_c - sha256: "89e83885ba09da5fdf2cdacc8002a712ca238c28b7f717910b34bcd27b0d03ac" - url: "https://pub.dev" - source: hosted - version: "0.17.4" - nested: - dependency: transitive - description: - name: nested - sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" - url: "https://pub.dev" - source: hosted - version: "1.0.0" - objective_c: - dependency: transitive - description: - name: objective_c - sha256: "7fd0c4d8ac8980011753b9bdaed2bf15111365924cdeeeaeb596214ea2b03537" - url: "https://pub.dev" - source: hosted - version: "9.2.4" - path: - dependency: transitive - description: - name: path - sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" - url: "https://pub.dev" - source: hosted - version: "1.9.1" - path_provider: - dependency: transitive - description: - name: path_provider - sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" - url: "https://pub.dev" - source: hosted - version: "2.1.5" - path_provider_android: - dependency: transitive - description: - name: path_provider_android - sha256: f2c65e21139ce2c3dad46922be8272bb5963516045659e71bb16e151c93b580e - url: "https://pub.dev" - source: hosted - version: "2.2.22" - path_provider_foundation: - dependency: transitive - description: - name: path_provider_foundation - sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699" - url: "https://pub.dev" - source: hosted - version: "2.6.0" - path_provider_linux: - dependency: transitive - description: - name: path_provider_linux - sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 - url: "https://pub.dev" - source: hosted - version: "2.2.1" - path_provider_platform_interface: - dependency: transitive - description: - name: path_provider_platform_interface - sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" - url: "https://pub.dev" - source: hosted - version: "2.1.2" - path_provider_windows: - dependency: transitive - description: - name: path_provider_windows - sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 - url: "https://pub.dev" - source: hosted - version: "2.3.0" - platform: - dependency: transitive - description: - name: platform - sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" - url: "https://pub.dev" - source: hosted - version: "3.1.6" - plugin_platform_interface: - dependency: transitive - description: - name: plugin_platform_interface - sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" - url: "https://pub.dev" - source: hosted - version: "2.1.8" - protobuf: - dependency: transitive - description: - name: protobuf - sha256: "68645b24e0716782e58948f8467fd42a880f255096a821f9e7d0ec625b00c84d" - url: "https://pub.dev" - source: hosted - version: "3.1.0" - provider: - dependency: transitive - description: - name: provider - sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272" - url: "https://pub.dev" - source: hosted - version: "6.1.5+1" - pub_semver: - dependency: transitive - description: - name: pub_semver - sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" - url: "https://pub.dev" - source: hosted - version: "2.2.0" - result_dart: - dependency: transitive - description: - name: result_dart - sha256: "0666b21fbdf697b3bdd9986348a380aa204b3ebe7c146d8e4cdaa7ce735e6054" - url: "https://pub.dev" - source: hosted - version: "2.1.1" - shared_preferences: - dependency: transitive - description: - name: shared_preferences - sha256: "2939ae520c9024cb197fc20dee269cd8cdbf564c8b5746374ec6cacdc5169e64" - url: "https://pub.dev" - source: hosted - version: "2.5.4" - shared_preferences_android: - dependency: transitive - description: - name: shared_preferences_android - sha256: "83af5c682796c0f7719c2bbf74792d113e40ae97981b8f266fa84574573556bc" - url: "https://pub.dev" - source: hosted - version: "2.4.18" - shared_preferences_foundation: - dependency: transitive - description: - name: shared_preferences_foundation - sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f" - url: "https://pub.dev" - source: hosted - version: "2.5.6" - shared_preferences_linux: - dependency: transitive - description: - name: shared_preferences_linux - sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" - url: "https://pub.dev" - source: hosted - version: "2.4.1" - shared_preferences_platform_interface: - dependency: transitive - description: - name: shared_preferences_platform_interface - sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" - url: "https://pub.dev" - source: hosted - version: "2.4.1" - shared_preferences_web: - dependency: transitive - description: - name: shared_preferences_web - sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 - url: "https://pub.dev" - source: hosted - version: "2.4.3" - shared_preferences_windows: - dependency: transitive - description: - name: shared_preferences_windows - sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" - url: "https://pub.dev" - source: hosted - version: "2.4.1" - sky_engine: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" - slang: - dependency: transitive - description: - name: slang - sha256: "13e3b6f07adc51ab751e7889647774d294cbce7a3382f81d9e5029acfe9c37b2" - url: "https://pub.dev" - source: hosted - version: "4.12.0" - slang_flutter: - dependency: transitive - description: - name: slang_flutter - sha256: "0a4545cca5404d6b7487cf61cf1fe56c52daeb08de56a7574ee8381fbad035a0" - url: "https://pub.dev" - source: hosted - version: "4.12.0" - source_span: - dependency: transitive - description: - name: source_span - sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" - url: "https://pub.dev" - source: hosted - version: "1.10.1" - stack_trace: - dependency: transitive - description: - name: stack_trace - sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" - url: "https://pub.dev" - source: hosted - version: "1.12.1" - stream_channel: - dependency: transitive - description: - name: stream_channel - sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" - url: "https://pub.dev" - source: hosted - version: "2.1.4" - string_scanner: - dependency: transitive - description: - name: string_scanner - sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" - url: "https://pub.dev" - source: hosted - version: "1.4.1" - term_glyph: - dependency: transitive - description: - name: term_glyph - sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" - url: "https://pub.dev" - source: hosted - version: "1.2.2" - test_api: - dependency: transitive - description: - name: test_api - sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 - url: "https://pub.dev" - source: hosted - version: "0.7.7" - typed_data: - dependency: transitive - description: - name: typed_data - sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 - url: "https://pub.dev" - source: hosted - version: "1.4.0" - uuid: - dependency: transitive - description: - name: uuid - sha256: a11b666489b1954e01d992f3d601b1804a33937b5a8fe677bd26b8a9f96f96e8 - url: "https://pub.dev" - source: hosted - version: "4.5.2" - vector_math: - dependency: transitive - description: - name: vector_math - sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b - url: "https://pub.dev" - source: hosted - version: "2.2.0" - vm_service: - dependency: transitive - description: - name: vm_service - sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" - url: "https://pub.dev" - source: hosted - version: "15.0.2" - watcher: - dependency: transitive - description: - name: watcher - sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635" - url: "https://pub.dev" - source: hosted - version: "1.2.1" - web: - dependency: transitive - description: - name: web - sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" - url: "https://pub.dev" - source: hosted - version: "1.1.1" - xdg_directories: - dependency: transitive - description: - name: xdg_directories - sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" - url: "https://pub.dev" - source: hosted - version: "1.1.0" - yaml: - dependency: transitive - description: - name: yaml - sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce - url: "https://pub.dev" - source: hosted - version: "3.1.3" -sdks: - dart: ">=3.10.7 <4.0.0" - flutter: ">=3.38.4" diff --git a/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/pubspec.yaml b/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/pubspec.yaml index d0ae944d..3991f442 100644 --- a/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/pubspec.yaml +++ b/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/pubspec.yaml @@ -2,6 +2,7 @@ name: staff_tax_forms description: Staff Tax Forms feature. version: 0.0.1 publish_to: none +resolution: workspace environment: sdk: '>=3.10.0 <4.0.0' diff --git a/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/presentation/blocs/bank_account_cubit.dart b/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/presentation/blocs/bank_account_cubit.dart index 5baac87a..4f948740 100644 --- a/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/presentation/blocs/bank_account_cubit.dart +++ b/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/presentation/blocs/bank_account_cubit.dart @@ -37,6 +37,7 @@ class BankAccountCubit extends Cubit { } Future addAccount({ + required String bankName, required String routingNumber, required String accountNumber, required String type, @@ -47,7 +48,7 @@ class BankAccountCubit extends Cubit { final BankAccount newAccount = BankAccount( id: '', // Generated by server usually userId: '', // Handled by Repo/Auth - bankName: 'New Bank', // Mock + bankName: bankName, accountNumber: accountNumber, accountName: '', sortCode: routingNumber, @@ -63,6 +64,7 @@ class BankAccountCubit extends Cubit { await loadAccounts(); emit(state.copyWith( + status: BankAccountStatus.accountAdded, showForm: false, // Close form on success )); } catch (e) { diff --git a/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/presentation/blocs/bank_account_state.dart b/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/presentation/blocs/bank_account_state.dart index 30a5e8c0..09038616 100644 --- a/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/presentation/blocs/bank_account_state.dart +++ b/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/presentation/blocs/bank_account_state.dart @@ -1,7 +1,7 @@ import 'package:equatable/equatable.dart'; import 'package:krow_domain/krow_domain.dart'; -enum BankAccountStatus { initial, loading, loaded, error } +enum BankAccountStatus { initial, loading, loaded, error, accountAdded } class BankAccountState extends Equatable { final BankAccountStatus status; diff --git a/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/presentation/pages/bank_account_page.dart b/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/presentation/pages/bank_account_page.dart index f5672232..54a06471 100644 --- a/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/presentation/pages/bank_account_page.dart +++ b/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/presentation/pages/bank_account_page.dart @@ -44,8 +44,23 @@ class BankAccountPage extends StatelessWidget { child: Container(color: UiColors.border, height: 1.0), ), ), - body: BlocBuilder( + body: BlocConsumer( bloc: cubit, + listener: (BuildContext context, BankAccountState state) { + if (state.status == BankAccountStatus.accountAdded) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + strings.account_added_success, + style: UiTypography.body2r.textPrimary, + ), + backgroundColor: UiColors.tagSuccess, + behavior: SnackBarBehavior.floating, + duration: const Duration(seconds: 3), + ), + ); + } + }, builder: (BuildContext context, BankAccountState state) { if (state.status == BankAccountStatus.loading && state.accounts.isEmpty) { return const Center(child: CircularProgressIndicator()); @@ -96,8 +111,9 @@ class BankAccountPage extends StatelessWidget { backgroundColor: Colors.transparent, child: AddAccountForm( strings: strings, - onSubmit: (String routing, String account, String type) { + onSubmit: (String bankName, String routing, String account, String type) { cubit.addAccount( + bankName: bankName, routingNumber: routing, accountNumber: account, type: type, diff --git a/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/presentation/widgets/add_account_form.dart b/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/presentation/widgets/add_account_form.dart index 6b07b661..a7ad00c9 100644 --- a/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/presentation/widgets/add_account_form.dart +++ b/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/presentation/widgets/add_account_form.dart @@ -5,7 +5,7 @@ import '../blocs/bank_account_cubit.dart'; class AddAccountForm extends StatefulWidget { final dynamic strings; - final Function(String routing, String account, String type) onSubmit; + final Function(String bankName, String routing, String account, String type) onSubmit; final VoidCallback onCancel; const AddAccountForm({super.key, required this.strings, required this.onSubmit, required this.onCancel}); @@ -15,12 +15,14 @@ class AddAccountForm extends StatefulWidget { } class _AddAccountFormState extends State { + final TextEditingController _bankNameController = TextEditingController(); final TextEditingController _routingController = TextEditingController(); final TextEditingController _accountController = TextEditingController(); String _selectedType = 'CHECKING'; @override void dispose() { + _bankNameController.dispose(); _routingController.dispose(); _accountController.dispose(); super.dispose(); @@ -44,6 +46,13 @@ class _AddAccountFormState extends State { style: UiTypography.headline4m.copyWith(color: UiColors.textPrimary), // Was header4 ), const SizedBox(height: UiConstants.space4), + UiTextField( + label: widget.strings.bank_name, + hintText: widget.strings.bank_hint, + controller: _bankNameController, + keyboardType: TextInputType.text, + ), + const SizedBox(height: UiConstants.space4), UiTextField( label: widget.strings.routing_number, hintText: widget.strings.routing_hint, @@ -90,6 +99,7 @@ class _AddAccountFormState extends State { text: widget.strings.save, onPressed: () { widget.onSubmit( + _bankNameController.text, _routingController.text, _accountController.text, _selectedType, diff --git a/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/pubspec.yaml b/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/pubspec.yaml index 4d6785ee..a8605f2a 100644 --- a/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/pubspec.yaml +++ b/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/pubspec.yaml @@ -34,4 +34,4 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^2.0.0 + flutter_lints: ^6.0.0 diff --git a/apps/mobile/packages/features/staff/profile_sections/finances/time_card/lib/src/data/repositories_impl/time_card_repository_impl.dart b/apps/mobile/packages/features/staff/profile_sections/finances/time_card/lib/src/data/repositories_impl/time_card_repository_impl.dart index f1f7d3f4..4a6aba1c 100644 --- a/apps/mobile/packages/features/staff/profile_sections/finances/time_card/lib/src/data/repositories_impl/time_card_repository_impl.dart +++ b/apps/mobile/packages/features/staff/profile_sections/finances/time_card/lib/src/data/repositories_impl/time_card_repository_impl.dart @@ -1,64 +1,83 @@ -import 'package:krow_data_connect/krow_data_connect.dart'; +import 'package:firebase_auth/firebase_auth.dart' as firebase; +import 'package:firebase_data_connect/firebase_data_connect.dart' as fdc; +import 'package:intl/intl.dart'; +import 'package:krow_data_connect/krow_data_connect.dart' as dc; import 'package:krow_domain/krow_domain.dart'; -import '../../domain/entities/time_card.dart'; +// ignore: implementation_imports +import 'package:krow_domain/src/adapters/financial/time_card_adapter.dart'; +import 'package:krow_core/core.dart'; import '../../domain/repositories/time_card_repository.dart'; +/// Implementation of [TimeCardRepository] using Firebase Data Connect. class TimeCardRepositoryImpl implements TimeCardRepository { - final ShiftsRepositoryMock shiftsRepository; + final dc.ExampleConnector _dataConnect; + final firebase.FirebaseAuth _firebaseAuth; - TimeCardRepositoryImpl({required this.shiftsRepository}); + /// Creates a [TimeCardRepositoryImpl]. + TimeCardRepositoryImpl({ + required dc.ExampleConnector dataConnect, + required firebase.FirebaseAuth firebaseAuth, + }) : _dataConnect = dataConnect, + _firebaseAuth = firebaseAuth; + + Future _getStaffId() async { + final firebase.User? user = _firebaseAuth.currentUser; + if (user == null) throw Exception('User not authenticated'); + + final fdc.QueryResult result = + await _dataConnect.getStaffByUserId(userId: user.uid).execute(); + if (result.data.staffs.isEmpty) { + throw Exception('Staff profile not found'); + } + return result.data.staffs.first.id; + } @override Future> getTimeCards(DateTime month) async { - // We use ShiftsRepositoryMock as it contains shift details (title, client, etc). - // In a real app, we might query 'TimeCards' directly or join Shift+Payment. - // For now, we simulate TimeCards from Shifts. - final List shifts = await shiftsRepository.getMyShifts(); + final String staffId = await _getStaffId(); + // Fetch applications. Limit can be adjusted, assuming 100 is safe for now. + final fdc.QueryResult result = + await _dataConnect.getApplicationsByStaffId(staffId: staffId).limit(100).execute(); - // Map to TimeCard and filter by the requested month. - return shifts - .map((Shift shift) { - double hours = 8.0; - // Simple parse for mock - try { - // Assuming HH:mm - final int start = int.parse(shift.startTime.split(':')[0]); - final int end = int.parse(shift.endTime.split(':')[0]); - hours = (end - start).abs().toDouble(); - if (hours == 0) hours = 8.0; - } catch (_) {} + return result.data.applications + .where((dc.GetApplicationsByStaffIdApplications app) { + final DateTime? shiftDate = app.shift.date == null + ? null + : DateTimeUtils.toDeviceTime(app.shift.date!.toDateTime()); + if (shiftDate == null) return false; + return shiftDate.year == month.year && shiftDate.month == month.month; + }) + .map((dc.GetApplicationsByStaffIdApplications app) { + final DateTime shiftDate = + DateTimeUtils.toDeviceTime(app.shift.date!.toDateTime()); + final String startTime = _formatTime(app.checkInTime) ?? _formatTime(app.shift.startTime) ?? ''; + final String endTime = _formatTime(app.checkOutTime) ?? _formatTime(app.shift.endTime) ?? ''; - return TimeCard( - id: shift.id, - shiftTitle: shift.title, - clientName: shift.clientName, - date: DateTime.tryParse(shift.date) ?? DateTime.now(), - startTime: shift.startTime, - endTime: shift.endTime, + // Prefer shiftRole values for pay/hours + final double hours = app.shiftRole.hours ?? 0.0; + final double rate = app.shiftRole.role.costPerHour; + final double pay = app.shiftRole.totalValue ?? 0.0; + + return TimeCardAdapter.fromPrimitives( + id: app.id, + shiftTitle: app.shift.title, + clientName: app.shift.order.business.businessName, + date: shiftDate, + startTime: startTime, + endTime: endTime, totalHours: hours, - hourlyRate: shift.hourlyRate, - totalPay: hours * shift.hourlyRate, - status: _mapStatus(shift.status), - location: shift.location, + hourlyRate: rate, + totalPay: pay, + status: app.status.stringValue, + location: app.shift.location, ); }) - .where((TimeCard tc) => - tc.date.year == month.year && tc.date.month == month.month) .toList(); } - TimeCardStatus _mapStatus(String? shiftStatus) { - if (shiftStatus == null) return TimeCardStatus.pending; - // Map shift status to TimeCardStatus - switch (shiftStatus.toLowerCase()) { - case 'confirmed': - return TimeCardStatus.pending; - case 'completed': - return TimeCardStatus.approved; - case 'paid': - return TimeCardStatus.paid; - default: - return TimeCardStatus.pending; - } + String? _formatTime(fdc.Timestamp? timestamp) { + if (timestamp == null) return null; + return DateFormat('HH:mm') + .format(DateTimeUtils.toDeviceTime(timestamp.toDateTime())); } } diff --git a/apps/mobile/packages/features/staff/profile_sections/finances/time_card/lib/src/domain/repositories/time_card_repository.dart b/apps/mobile/packages/features/staff/profile_sections/finances/time_card/lib/src/domain/repositories/time_card_repository.dart index c5147009..c44f86e4 100644 --- a/apps/mobile/packages/features/staff/profile_sections/finances/time_card/lib/src/domain/repositories/time_card_repository.dart +++ b/apps/mobile/packages/features/staff/profile_sections/finances/time_card/lib/src/domain/repositories/time_card_repository.dart @@ -1,4 +1,4 @@ -import '../entities/time_card.dart'; +import 'package:krow_domain/krow_domain.dart'; /// Repository interface for accessing time card data. /// diff --git a/apps/mobile/packages/features/staff/profile_sections/finances/time_card/lib/src/domain/usecases/get_time_cards_usecase.dart b/apps/mobile/packages/features/staff/profile_sections/finances/time_card/lib/src/domain/usecases/get_time_cards_usecase.dart index 00f207dd..1ee76890 100644 --- a/apps/mobile/packages/features/staff/profile_sections/finances/time_card/lib/src/domain/usecases/get_time_cards_usecase.dart +++ b/apps/mobile/packages/features/staff/profile_sections/finances/time_card/lib/src/domain/usecases/get_time_cards_usecase.dart @@ -1,5 +1,5 @@ import 'package:krow_core/core.dart'; -import '../../domain/entities/time_card.dart'; +import 'package:krow_domain/krow_domain.dart'; import '../arguments/get_time_cards_arguments.dart'; import '../repositories/time_card_repository.dart'; diff --git a/apps/mobile/packages/features/staff/profile_sections/finances/time_card/lib/src/presentation/blocs/time_card_bloc.dart b/apps/mobile/packages/features/staff/profile_sections/finances/time_card/lib/src/presentation/blocs/time_card_bloc.dart index 75d5adcf..e655755e 100644 --- a/apps/mobile/packages/features/staff/profile_sections/finances/time_card/lib/src/presentation/blocs/time_card_bloc.dart +++ b/apps/mobile/packages/features/staff/profile_sections/finances/time_card/lib/src/presentation/blocs/time_card_bloc.dart @@ -1,6 +1,6 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:equatable/equatable.dart'; -import '../../domain/entities/time_card.dart'; +import 'package:krow_domain/krow_domain.dart'; import '../../domain/arguments/get_time_cards_arguments.dart'; import '../../domain/usecases/get_time_cards_usecase.dart'; diff --git a/apps/mobile/packages/features/staff/profile_sections/finances/time_card/lib/src/presentation/widgets/shift_history_list.dart b/apps/mobile/packages/features/staff/profile_sections/finances/time_card/lib/src/presentation/widgets/shift_history_list.dart index 827e5273..0135e0cb 100644 --- a/apps/mobile/packages/features/staff/profile_sections/finances/time_card/lib/src/presentation/widgets/shift_history_list.dart +++ b/apps/mobile/packages/features/staff/profile_sections/finances/time_card/lib/src/presentation/widgets/shift_history_list.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:design_system/design_system.dart'; import 'package:core_localization/core_localization.dart'; -import '../../domain/entities/time_card.dart'; +import 'package:krow_domain/krow_domain.dart'; import 'timesheet_card.dart'; /// Displays the list of shift history or an empty state. diff --git a/apps/mobile/packages/features/staff/profile_sections/finances/time_card/lib/src/presentation/widgets/timesheet_card.dart b/apps/mobile/packages/features/staff/profile_sections/finances/time_card/lib/src/presentation/widgets/timesheet_card.dart index 960c3619..4e8d7351 100644 --- a/apps/mobile/packages/features/staff/profile_sections/finances/time_card/lib/src/presentation/widgets/timesheet_card.dart +++ b/apps/mobile/packages/features/staff/profile_sections/finances/time_card/lib/src/presentation/widgets/timesheet_card.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:design_system/design_system.dart'; import 'package:core_localization/core_localization.dart'; -import '../../domain/entities/time_card.dart'; +import 'package:krow_domain/krow_domain.dart'; /// A card widget displaying details of a single shift/timecard. class TimesheetCard extends StatelessWidget { diff --git a/apps/mobile/packages/features/staff/profile_sections/finances/time_card/lib/src/staff_time_card_module.dart b/apps/mobile/packages/features/staff/profile_sections/finances/time_card/lib/src/staff_time_card_module.dart index 2819cdb9..4f7e7856 100644 --- a/apps/mobile/packages/features/staff/profile_sections/finances/time_card/lib/src/staff_time_card_module.dart +++ b/apps/mobile/packages/features/staff/profile_sections/finances/time_card/lib/src/staff_time_card_module.dart @@ -1,5 +1,6 @@ library staff_time_card; +import 'package:firebase_auth/firebase_auth.dart'; import 'package:flutter_modular/flutter_modular.dart'; import 'package:krow_data_connect/krow_data_connect.dart'; @@ -11,15 +12,23 @@ import 'presentation/pages/time_card_page.dart'; export 'presentation/pages/time_card_page.dart'; +/// Module for the Staff Time Card feature. +/// +/// This module configures dependency injection for accessing time card data, +/// including the repositories, use cases, and BLoCs. class StaffTimeCardModule extends Module { + @override + List get imports => [DataConnectModule()]; + @override void binds(Injector i) { // Repositories - // In a real app, ShiftsRepository might be provided by a Core Data Module. - // For this self-contained feature/mock, we instantiate it here if not available globally. - // Assuming we need a local instance for the mock to work or it's stateless. - i.add(ShiftsRepositoryMock.new); - i.add(TimeCardRepositoryImpl.new); + i.add( + () => TimeCardRepositoryImpl( + dataConnect: ExampleConnector.instance, + firebaseAuth: FirebaseAuth.instance, + ), + ); // UseCases i.add(GetTimeCardsUseCase.new); diff --git a/apps/mobile/packages/features/staff/profile_sections/finances/time_card/pubspec.yaml b/apps/mobile/packages/features/staff/profile_sections/finances/time_card/pubspec.yaml index d74dc6b2..311e8ca8 100644 --- a/apps/mobile/packages/features/staff/profile_sections/finances/time_card/pubspec.yaml +++ b/apps/mobile/packages/features/staff/profile_sections/finances/time_card/pubspec.yaml @@ -2,9 +2,10 @@ name: staff_time_card description: Staff Time Card Feature version: 0.0.1 publish_to: none +resolution: workspace environment: - sdk: '>=3.0.0 <4.0.0' + sdk: '>=3.10.0 <4.0.0' flutter: ">=3.0.0" dependencies: diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/pubspec.yaml b/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/pubspec.yaml index b87789a7..07a124c8 100644 --- a/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/pubspec.yaml +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/pubspec.yaml @@ -31,4 +31,4 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^2.0.0 + flutter_lints: ^6.0.0 diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/blocs/personal_info_bloc.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/blocs/personal_info_bloc.dart index 29f5e700..47ba08f7 100644 --- a/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/blocs/personal_info_bloc.dart +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/blocs/personal_info_bloc.dart @@ -25,11 +25,13 @@ class PersonalInfoBloc extends Bloc required UpdatePersonalInfoUseCase updatePersonalInfoUseCase, }) : _getPersonalInfoUseCase = getPersonalInfoUseCase, _updatePersonalInfoUseCase = updatePersonalInfoUseCase, - super(const PersonalInfoState()) { + super(const PersonalInfoState.initial()) { on(_onLoadRequested); - on(_onFieldUpdated); - on(_onSaveRequested); - on(_onPhotoUploadRequested); + on(_onFieldChanged); + on(_onAddressSelected); + on(_onSubmitted); + + add(const PersonalInfoLoadRequested()); } /// Handles loading staff profile information. @@ -67,8 +69,8 @@ class PersonalInfoBloc extends Bloc } /// Handles updating a field value in the current staff profile. - void _onFieldUpdated( - PersonalInfoFieldUpdated event, + void _onFieldChanged( + PersonalInfoFieldChanged event, Emitter emit, ) { final Map updatedValues = Map.from(state.formValues); @@ -77,8 +79,8 @@ class PersonalInfoBloc extends Bloc } /// Handles saving staff profile information. - Future _onSaveRequested( - PersonalInfoSaveRequested event, + Future _onSubmitted( + PersonalInfoFormSubmitted event, Emitter emit, ) async { if (state.staff == null) return; @@ -116,33 +118,16 @@ class PersonalInfoBloc extends Bloc } } - /// Handles uploading a profile photo. - Future _onPhotoUploadRequested( - PersonalInfoPhotoUploadRequested event, + void _onAddressSelected( + PersonalInfoAddressSelected event, Emitter emit, - ) async { - if (state.staff == null) return; - - emit(state.copyWith(status: PersonalInfoStatus.uploadingPhoto)); - try { - // TODO: Implement photo upload when repository method is available - // final photoUrl = await _repository.uploadProfilePhoto(event.filePath); - // final updatedStaff = Staff(...); - // emit(state.copyWith( - // status: PersonalInfoStatus.loaded, - // staff: updatedStaff, - // )); - - // For now, just return to loaded state - emit(state.copyWith(status: PersonalInfoStatus.loaded)); - } catch (e) { - emit(state.copyWith( - status: PersonalInfoStatus.error, - errorMessage: e.toString(), - )); - } + ) { + // TODO: Implement Google Places logic if needed } + /// With _onPhotoUploadRequested and _onSaveRequested removed or renamed, + /// there are no errors pointing to them here. + @override void dispose() { close(); diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/blocs/personal_info_event.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/blocs/personal_info_event.dart index f50adf60..b09d4860 100644 --- a/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/blocs/personal_info_event.dart +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/blocs/personal_info_event.dart @@ -14,11 +14,11 @@ class PersonalInfoLoadRequested extends PersonalInfoEvent { } /// Event to update a field value. -class PersonalInfoFieldUpdated extends PersonalInfoEvent { +class PersonalInfoFieldChanged extends PersonalInfoEvent { final String field; final dynamic value; - const PersonalInfoFieldUpdated({ + const PersonalInfoFieldChanged({ required this.field, required this.value, }); @@ -27,17 +27,16 @@ class PersonalInfoFieldUpdated extends PersonalInfoEvent { List get props => [field, value]; } -/// Event to save personal information. -class PersonalInfoSaveRequested extends PersonalInfoEvent { - const PersonalInfoSaveRequested(); +/// Event to submit the form. +class PersonalInfoFormSubmitted extends PersonalInfoEvent { + const PersonalInfoFormSubmitted(); } -/// Event to upload a profile photo. -class PersonalInfoPhotoUploadRequested extends PersonalInfoEvent { - final String filePath; - - const PersonalInfoPhotoUploadRequested({required this.filePath}); - +/// Event when an address is selected from autocomplete. +class PersonalInfoAddressSelected extends PersonalInfoEvent { + final String address; + const PersonalInfoAddressSelected(this.address); + @override - List get props => [filePath]; + List get props => [address]; } diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/blocs/personal_info_state.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/blocs/personal_info_state.dart index c97a0931..cd0eabf8 100644 --- a/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/blocs/personal_info_state.dart +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/blocs/personal_info_state.dart @@ -49,6 +49,13 @@ class PersonalInfoState extends Equatable { this.errorMessage, }); + /// Initial state. + const PersonalInfoState.initial() + : status = PersonalInfoStatus.initial, + staff = null, + formValues = const {}, + errorMessage = null; + /// Creates a copy of this state with the given fields replaced. PersonalInfoState copyWith({ PersonalInfoStatus? status, diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/pages/personal_info_page.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/pages/personal_info_page.dart index 01971c2a..50fae0a9 100644 --- a/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/pages/personal_info_page.dart +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/pages/personal_info_page.dart @@ -1,12 +1,11 @@ +import 'package:core_localization/core_localization.dart'; +import 'package:design_system/design_system.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_modular/flutter_modular.dart'; -import 'package:core_localization/core_localization.dart'; -import 'package:design_system/design_system.dart'; import '../blocs/personal_info_bloc.dart'; -import '../blocs/personal_info_event.dart'; import '../blocs/personal_info_state.dart'; import '../widgets/personal_info_content.dart'; @@ -26,8 +25,7 @@ class PersonalInfoPage extends StatelessWidget { Widget build(BuildContext context) { final TranslationsStaffOnboardingPersonalInfoEn i18n = t.staff.onboarding.personal_info; return BlocProvider( - create: (BuildContext context) => Modular.get() - ..add(const PersonalInfoLoadRequested()), + create: (BuildContext context) => Modular.get(), child: BlocListener( listener: (BuildContext context, PersonalInfoState state) { if (state.status == PersonalInfoStatus.saved) { diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/personal_info_content.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/personal_info_content.dart index 41b89e6b..ba71594e 100644 --- a/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/personal_info_content.dart +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/personal_info_content.dart @@ -32,31 +32,44 @@ class PersonalInfoContent extends StatefulWidget { } class _PersonalInfoContentState extends State { + late final TextEditingController _emailController; late final TextEditingController _phoneController; late final TextEditingController _locationsController; @override void initState() { super.initState(); + _emailController = TextEditingController(text: widget.staff.email); _phoneController = TextEditingController(text: widget.staff.phone ?? ''); _locationsController = TextEditingController(text: widget.staff.preferredLocations?.join(', ')?? ''); // Listen to changes and update BLoC + _emailController.addListener(_onEmailChanged); _phoneController.addListener(_onPhoneChanged); _locationsController.addListener(_onAddressChanged); } @override void dispose() { + _emailController.dispose(); _phoneController.dispose(); _locationsController.dispose(); super.dispose(); } + void _onEmailChanged() { + context.read().add( + PersonalInfoFieldChanged( + field: 'email', + value: _emailController.text, + ), + ); + } + void _onPhoneChanged() { context.read().add( - PersonalInfoFieldUpdated( + PersonalInfoFieldChanged( field: 'phone', value: _phoneController.text, ), @@ -73,7 +86,7 @@ class _PersonalInfoContentState extends State { .toList(); context.read().add( - PersonalInfoFieldUpdated( + PersonalInfoFieldChanged( field: 'preferredLocations', value: locations, ), @@ -81,7 +94,7 @@ class _PersonalInfoContentState extends State { } void _handleSave() { - context.read().add(const PersonalInfoSaveRequested()); + context.read().add(const PersonalInfoFormSubmitted()); } void _handlePhotoTap() { @@ -114,6 +127,7 @@ class _PersonalInfoContentState extends State { PersonalInfoForm( fullName: widget.staff.name, email: widget.staff.email, + emailController: _emailController, phoneController: _phoneController, locationsController: _locationsController, enabled: !isSaving, diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/personal_info_form.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/personal_info_form.dart index 2897c37b..3f7a0af7 100644 --- a/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/personal_info_form.dart +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/personal_info_form.dart @@ -15,6 +15,9 @@ class PersonalInfoForm extends StatelessWidget { /// The staff member's email (read-only). final String email; + /// Controller for the email field. + final TextEditingController emailController; + /// Controller for the phone number field. final TextEditingController phoneController; @@ -29,6 +32,7 @@ class PersonalInfoForm extends StatelessWidget { super.key, required this.fullName, required this.email, + required this.emailController, required this.phoneController, required this.locationsController, this.enabled = true, @@ -48,7 +52,13 @@ class PersonalInfoForm extends StatelessWidget { _FieldLabel(text: i18n.email_label), const SizedBox(height: UiConstants.space2), - _ReadOnlyField(value: email), + _EditableField( + controller: emailController, + hint: i18n.email_label, + enabled: enabled, + keyboardType: TextInputType.emailAddress, + autofillHints: const [AutofillHints.email], + ), const SizedBox(height: UiConstants.space4), _FieldLabel(text: i18n.phone_label), @@ -122,11 +132,15 @@ class _EditableField extends StatelessWidget { final TextEditingController controller; final String hint; final bool enabled; + final TextInputType? keyboardType; + final Iterable? autofillHints; const _EditableField({ required this.controller, required this.hint, this.enabled = true, + this.keyboardType, + this.autofillHints, }); @override @@ -134,6 +148,8 @@ class _EditableField extends StatelessWidget { return TextField( controller: controller, enabled: enabled, + keyboardType: keyboardType, + autofillHints: autofillHints, style: UiTypography.body2r.copyWith(color: UiColors.textPrimary), decoration: InputDecoration( hintText: hint, 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 1c54242b..2d769b91 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 @@ -1,70 +1,617 @@ -import 'package:krow_data_connect/krow_data_connect.dart'; +import 'package:krow_data_connect/krow_data_connect.dart' as dc; +import 'package:krow_data_connect/src/session/staff_session_store.dart'; import 'package:krow_domain/krow_domain.dart'; +import 'package:intl/intl.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:firebase_data_connect/firebase_data_connect.dart'; +import 'package:krow_core/core.dart'; import '../../domain/repositories/shifts_repository_interface.dart'; -/// Implementation of [ShiftsRepositoryInterface] that delegates to [ShiftsRepositoryMock]. -/// -/// This class resides in the data layer and handles the communication with -/// the external data sources (currently mocks). class ShiftsRepositoryImpl implements ShiftsRepositoryInterface { - final ShiftsRepositoryMock _mock; + final dc.ExampleConnector _dataConnect; + final FirebaseAuth _auth = FirebaseAuth.instance; - ShiftsRepositoryImpl({ShiftsRepositoryMock? mock}) : _mock = mock ?? ShiftsRepositoryMock(); + ShiftsRepositoryImpl() : _dataConnect = dc.ExampleConnector.instance; - @override - Future> getMyShifts() async { - return _mock.getMyShifts(); + // Cache: ShiftID -> ApplicationID (For Accept/Decline) + final Map _shiftToAppIdMap = {}; + // Cache: ApplicationID -> RoleID (For Accept/Decline w/ Update mutation) + final Map _appToRoleIdMap = {}; + + String? _cachedStaffId; + + Future _getStaffId() async { + // 1. Check Session Store + final StaffSession? session = StaffSessionStore.instance.session; + if (session?.staff?.id != null) { + return session!.staff!.id; + } + + // 2. Check Cache + if (_cachedStaffId != null) return _cachedStaffId!; + + // 3. Fetch from Data Connect using Firebase UID + final user = _auth.currentUser; + if (user == null) { + throw Exception('User is not authenticated'); + } + + try { + final response = await _dataConnect + .getStaffByUserId(userId: user.uid) + .execute(); + if (response.data.staffs.isNotEmpty) { + _cachedStaffId = response.data.staffs.first.id; + return _cachedStaffId!; + } + } catch (e) { + // Log or handle error + } + + // 4. Fallback (should ideally not happen if DB is seeded) + return user.uid; + } + + DateTime? _toDateTime(dynamic t, {String? debugKey}) { + if (t == null) return null; + DateTime? dt; + if (t is Timestamp) { + dt = t.toDateTime(); + } else if (t is String) { + dt = DateTime.tryParse(t); + } else { + try { + dt = DateTime.tryParse(t.toJson() as String); + } catch (_) { + try { + dt = DateTime.tryParse(t.toString()); + } catch (e) { + dt = null; + } + } + } + + if (dt != null) { + final local = DateTimeUtils.toDeviceTime(dt); + if (debugKey != null && debugKey.isNotEmpty) { + print( + 'ShiftDate convert: key=$debugKey raw=$t parsed=${dt.toIso8601String()} local=${local.toIso8601String()}', + ); + } + return local; + } + return null; } @override - Future> getAvailableShifts(String query, String type) async { - // Delegates to mock. Logic kept here temporarily as per architecture constraints - // on data_connect modifications, mimicking a query capable datasource. - var shifts = await _mock.getAvailableShifts(); - - // Simple in-memory filtering for mock adapter - if (query.isNotEmpty) { - shifts = shifts.where((s) => - s.title.toLowerCase().contains(query.toLowerCase()) || - s.clientName.toLowerCase().contains(query.toLowerCase()) - ).toList(); - } - - if (type != 'all') { - if (type == 'one-day') { - shifts = shifts.where((s) => !s.title.contains('Multi-Day') && !s.title.contains('Long Term')).toList(); - } else if (type == 'multi-day') { - shifts = shifts.where((s) => s.title.contains('Multi-Day')).toList(); - } else if (type == 'long-term') { - shifts = shifts.where((s) => s.title.contains('Long Term')).toList(); - } - } - - return shifts; + Future> getMyShifts({ + required DateTime start, + required DateTime end, + }) async { + return _fetchApplications(start: start, end: end); } @override Future> getPendingAssignments() async { - return _mock.getPendingAssignments(); + return []; } @override - Future getShiftDetails(String shiftId) async { - return _mock.getShiftDetails(shiftId); + Future> getCancelledShifts() async { + return []; } @override - Future applyForShift(String shiftId) async { - await Future.delayed(const Duration(milliseconds: 500)); + Future> getHistoryShifts() async { + try { + final staffId = await _getStaffId(); + final response = await _dataConnect + .listCompletedApplicationsByStaffId(staffId: staffId) + .execute(); + final List shifts = []; + + for (final app in response.data.applications) { + _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, + ), + ); + } + return shifts; + } catch (e) { + return []; + } } - + + Future> _fetchApplications({ + DateTime? start, + DateTime? end, + }) async { + try { + final staffId = await _getStaffId(); + var query = _dataConnect.getApplicationsByStaffId(staffId: staffId); + if (start != null && end != null) { + query = query + .dayStart(_toTimestamp(start)) + .dayEnd(_toTimestamp(end)); + } + final response = await query.execute(); + + final apps = response.data.applications; + final List shifts = []; + + for (final app in apps) { + _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) + final bool hasCheckIn = app.checkInTime != null; + final bool hasCheckOut = app.checkOutTime != null; + dc.ApplicationStatus? appStatus; + if (app.status is dc.Known) { + appStatus = (app.status as dc.Known).value; + } + final String mappedStatus = hasCheckOut + ? 'completed' + : hasCheckIn + ? 'checked_in' + : _mapStatus(appStatus ?? dc.ApplicationStatus.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: mappedStatus, + description: app.shift.description, + durationDays: app.shift.durationDays, + requiredSlots: app.shiftRole.count, + filledSlots: app.shiftRole.assigned ?? 0, + hasApplied: true, + ), + ); + } + 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); + } + + String _mapStatus(dc.ApplicationStatus status) { + switch (status) { + case dc.ApplicationStatus.ACCEPTED: + case dc.ApplicationStatus.CONFIRMED: + return 'confirmed'; + case dc.ApplicationStatus.PENDING: + return 'pending'; + case dc.ApplicationStatus.CHECKED_OUT: + return 'completed'; + case dc.ApplicationStatus.REJECTED: + return 'cancelled'; + default: + return 'open'; + } + } + + @override + Future> getAvailableShifts(String query, String type) async { + try { + final String? vendorId = dc.StaffSessionStore.instance.session?.ownerId; + if (vendorId == null || vendorId.isEmpty) { + return []; + } + + final result = await _dataConnect + .listShiftRolesByVendorId(vendorId: vendorId) + .execute(); + final allShiftRoles = result.data.shiftRoles; + + final List mappedShifts = []; + for (final sr in allShiftRoles) { + print( + 'FindShifts raw: shiftId=${sr.shiftId} roleId=${sr.roleId} ' + 'start=${sr.startTime?.toJson()} end=${sr.endTime?.toJson()} ' + 'shiftDate=${sr.shift.date?.toJson()}', + ); + final DateTime? shiftDate = _toDateTime(sr.shift.date); + final startDt = _toDateTime(sr.startTime); + final endDt = _toDateTime(sr.endTime); + final createdDt = _toDateTime(sr.createdAt); + print( + 'FindShifts mapped: shiftId=${sr.shiftId} ' + 'origStart=${sr.startTime?.toJson()} ' + 'origEnd=${sr.endTime?.toJson()} ' + 'mappedStart=${startDt != null ? DateFormat('HH:mm').format(startDt) : ''} ' + 'mappedEnd=${endDt != null ? DateFormat('HH:mm').format(endDt) : ''}', + ); + mappedShifts.add( + Shift( + id: sr.shiftId, + roleId: sr.roleId, + title: sr.role.name, + clientName: sr.shift.order.business.businessName, + logoUrl: null, + hourlyRate: sr.role.costPerHour, + location: sr.shift.location ?? '', + locationAddress: sr.shift.locationAddress ?? '', + date: shiftDate?.toIso8601String() ?? '', + startTime: startDt != null + ? DateFormat('HH:mm').format(startDt) + : '', + endTime: endDt != null ? DateFormat('HH:mm').format(endDt) : '', + createdDate: createdDt?.toIso8601String() ?? '', + status: sr.shift.status?.stringValue.toLowerCase() ?? 'open', + description: sr.shift.description, + durationDays: sr.shift.durationDays, + requiredSlots: sr.count, + filledSlots: sr.assigned ?? 0, + ), + ); + } + + if (query.isNotEmpty) { + return mappedShifts + .where( + (s) => + s.title.toLowerCase().contains(query.toLowerCase()) || + s.clientName.toLowerCase().contains(query.toLowerCase()), + ) + .toList(); + } + + return mappedShifts; + } catch (e) { + return []; + } + } + + @override + Future getShiftDetails(String shiftId, {String? roleId}) async { + return _getShiftDetails(shiftId, roleId: roleId); + } + + Future _getShiftDetails(String shiftId, {String? roleId}) async { + try { + if (roleId != null && roleId.isNotEmpty) { + final roleResult = await _dataConnect + .getShiftRoleById(shiftId: shiftId, roleId: roleId) + .execute(); + final sr = roleResult.data.shiftRole; + if (sr == null) return null; + + final DateTime? startDt = _toDateTime(sr.startTime); + final DateTime? endDt = _toDateTime(sr.endTime); + final DateTime? createdDt = _toDateTime(sr.createdAt); + + final String? staffId = await _getStaffId(); + bool hasApplied = false; + String status = 'open'; + if (staffId != null) { + final apps = await _dataConnect + .getApplicationsByStaffId(staffId: staffId) + .execute(); + final app = apps.data.applications + .where( + (a) => a.shiftId == shiftId && a.shiftRole.roleId == roleId, + ) + .firstOrNull; + if (app != null) { + hasApplied = true; + if (app.status is dc.Known) { + final dc.ApplicationStatus s = + (app.status as dc.Known).value; + status = _mapStatus(s); + } + } + } + + return Shift( + id: sr.shiftId, + roleId: sr.roleId, + title: sr.shift.order.business.businessName, + clientName: sr.shift.order.business.businessName, + logoUrl: sr.shift.order.business.companyLogoUrl, + hourlyRate: sr.role.costPerHour, + location: sr.shift.location ?? sr.shift.order.teamHub.hubName, + locationAddress: sr.shift.locationAddress ?? '', + date: startDt?.toIso8601String() ?? '', + startTime: startDt != null ? DateFormat('HH:mm').format(startDt) : '', + endTime: endDt != null ? DateFormat('HH:mm').format(endDt) : '', + createdDate: createdDt?.toIso8601String() ?? '', + status: status, + description: sr.shift.description, + durationDays: null, + requiredSlots: sr.count, + filledSlots: sr.assigned ?? 0, + hasApplied: hasApplied, + totalValue: sr.totalValue, + ); + } + + final result = await _dataConnect.getShiftById(id: shiftId).execute(); + final s = result.data.shift; + if (s == null) return null; + + int? required; + int? filled; + try { + final rolesRes = await _dataConnect + .listShiftRolesByShiftId(shiftId: shiftId) + .execute(); + if (rolesRes.data.shiftRoles.isNotEmpty) { + required = 0; + filled = 0; + for (var r in rolesRes.data.shiftRoles) { + required = (required ?? 0) + r.count; + filled = (filled ?? 0) + (r.assigned ?? 0); + } + } + } catch (_) {} + + final startDt = _toDateTime(s.startTime); + final endDt = _toDateTime(s.endTime); + final createdDt = _toDateTime(s.createdAt); + + return Shift( + id: s.id, + title: s.title, + clientName: s.order.business.businessName, + logoUrl: null, + hourlyRate: s.cost ?? 0.0, + location: s.location ?? '', + locationAddress: s.locationAddress ?? '', + date: startDt?.toIso8601String() ?? '', + startTime: startDt != null ? DateFormat('HH:mm').format(startDt) : '', + endTime: endDt != null ? DateFormat('HH:mm').format(endDt) : '', + createdDate: createdDt?.toIso8601String() ?? '', + status: s.status?.stringValue ?? 'OPEN', + description: s.description, + durationDays: s.durationDays, + requiredSlots: required, + filledSlots: filled, + ); + } catch (e) { + return null; + } + } + + @override + Future applyForShift( + String shiftId, { + bool isInstantBook = false, + String? roleId, + }) async { + final staffId = await _getStaffId(); + + String targetRoleId = roleId ?? ''; + if (targetRoleId.isEmpty) { + throw Exception('Missing role id.'); + } + + final roleResult = await _dataConnect + .getShiftRoleById(shiftId: shiftId, roleId: targetRoleId) + .execute(); + final role = roleResult.data.shiftRole; + if (role == null) { + throw Exception('Shift role not found'); + } + final shiftResult = await _dataConnect.getShiftById(id: shiftId).execute(); + final shift = shiftResult.data.shift; + if (shift == null) { + throw Exception('Shift not found'); + } + final DateTime? shiftDate = _toDateTime(shift.date); + if (shiftDate != null) { + final DateTime dayStartUtc = DateTime.utc( + shiftDate.year, + shiftDate.month, + shiftDate.day, + ); + final DateTime dayEndUtc = DateTime.utc( + shiftDate.year, + shiftDate.month, + shiftDate.day, + 23, + 59, + 59, + 999, + 999, + ); + print( + 'Staff applyForShift: dayStartUtc=${_toTimestamp(dayStartUtc).toJson()} ' + 'dayEndUtc=${_toTimestamp(dayEndUtc).toJson()}', + ); + final dayApplications = await _dataConnect + .vaidateDayStaffApplication(staffId: staffId) + .dayStart(_toTimestamp(dayStartUtc)) + .dayEnd(_toTimestamp(dayEndUtc)) + .execute(); + if (dayApplications.data.applications.isNotEmpty) { + throw Exception('The user already has a shift that day.'); + } + } + final existingApplicationResult = await _dataConnect + .getApplicationByStaffShiftAndRole( + staffId: staffId, + shiftId: shiftId, + roleId: targetRoleId, + ) + .execute(); + if (existingApplicationResult.data.applications.isNotEmpty) { + throw Exception('Application already exists.'); + } + final int assigned = role.assigned ?? 0; + if (assigned >= role.count) { + throw Exception('This shift is full.'); + } + + final int filled = shift.filled ?? 0; + + String? appId; + bool updatedRole = false; + bool updatedShift = false; + try { + final appResult = await _dataConnect + .createApplication( + shiftId: shiftId, + staffId: staffId, + roleId: targetRoleId, + status: dc.ApplicationStatus.ACCEPTED, + origin: dc.ApplicationOrigin.STAFF, + ) + // TODO: this should be PENDING so a vendor can accept it. + .execute(); + appId = appResult.data.application_insert.id; + + await _dataConnect + .updateShiftRole(shiftId: shiftId, roleId: targetRoleId) + .assigned(assigned + 1) + .execute(); + updatedRole = true; + + await _dataConnect.updateShift(id: shiftId).filled(filled + 1).execute(); + updatedShift = true; + } catch (e) { + if (updatedShift) { + await _dataConnect.updateShift(id: shiftId).filled(filled).execute(); + } + if (updatedRole) { + await _dataConnect + .updateShiftRole(shiftId: shiftId, roleId: targetRoleId) + .assigned(assigned) + .execute(); + } + if (appId != null) { + await _dataConnect.deleteApplication(id: appId).execute(); + } + rethrow; + } + } + @override Future acceptShift(String shiftId) async { - await Future.delayed(const Duration(milliseconds: 500)); + await _updateApplicationStatus(shiftId, dc.ApplicationStatus.ACCEPTED); } - + @override Future declineShift(String shiftId) async { - await Future.delayed(const Duration(milliseconds: 500)); + await _updateApplicationStatus(shiftId, dc.ApplicationStatus.REJECTED); + } + + Future _updateApplicationStatus( + String shiftId, + dc.ApplicationStatus newStatus, + ) async { + String? appId = _shiftToAppIdMap[shiftId]; + String? roleId; + + if (appId == null) { + // Try to find it in pending + await getPendingAssignments(); + } + // Re-check map + appId = _shiftToAppIdMap[shiftId]; + if (appId != null) { + roleId = _appToRoleIdMap[appId]; + } else { + // Fallback fetch + final staffId = await _getStaffId(); + final apps = await _dataConnect + .getApplicationsByStaffId(staffId: staffId) + .execute(); + final app = apps.data.applications + .where((a) => a.shiftId == shiftId) + .firstOrNull; + if (app != null) { + appId = app.id; + roleId = app.shiftRole.id; + } + } + + if (appId == null || roleId == null) { + // If we are rejecting and can't find an application, create one as rejected (declining an available shift) + if (newStatus == dc.ApplicationStatus.REJECTED) { + final rolesResult = await _dataConnect + .listShiftRolesByShiftId(shiftId: shiftId) + .execute(); + if (rolesResult.data.shiftRoles.isNotEmpty) { + final role = rolesResult.data.shiftRoles.first; + final staffId = await _getStaffId(); + await _dataConnect + .createApplication( + shiftId: shiftId, + staffId: staffId, + roleId: role.id, + status: dc.ApplicationStatus.REJECTED, + origin: dc.ApplicationOrigin.STAFF, + ) + .execute(); + return; + } + } + throw Exception("Application not found for shift $shiftId"); + } + + await _dataConnect + .updateApplicationStatus(id: appId) + .status(newStatus) + .execute(); } } diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/domain/arguments/get_my_shifts_arguments.dart b/apps/mobile/packages/features/staff/shifts/lib/src/domain/arguments/get_my_shifts_arguments.dart new file mode 100644 index 00000000..572cd3df --- /dev/null +++ b/apps/mobile/packages/features/staff/shifts/lib/src/domain/arguments/get_my_shifts_arguments.dart @@ -0,0 +1,11 @@ +import 'package:krow_core/core.dart'; + +class GetMyShiftsArguments extends UseCaseArgument { + final DateTime start; + final DateTime end; + + const GetMyShiftsArguments({ + required this.start, + required this.end, + }); +} diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/domain/arguments/get_shift_details_arguments.dart b/apps/mobile/packages/features/staff/shifts/lib/src/domain/arguments/get_shift_details_arguments.dart new file mode 100644 index 00000000..f742108d --- /dev/null +++ b/apps/mobile/packages/features/staff/shifts/lib/src/domain/arguments/get_shift_details_arguments.dart @@ -0,0 +1,14 @@ +import 'package:equatable/equatable.dart'; + +class GetShiftDetailsArguments extends Equatable { + final String shiftId; + final String? roleId; + + const GetShiftDetailsArguments({ + required this.shiftId, + this.roleId, + }); + + @override + List get props => [shiftId, roleId]; +} diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/domain/repositories/shifts_repository_interface.dart b/apps/mobile/packages/features/staff/shifts/lib/src/domain/repositories/shifts_repository_interface.dart index e0c36133..87d363c2 100644 --- a/apps/mobile/packages/features/staff/shifts/lib/src/domain/repositories/shifts_repository_interface.dart +++ b/apps/mobile/packages/features/staff/shifts/lib/src/domain/repositories/shifts_repository_interface.dart @@ -6,7 +6,10 @@ import 'package:krow_domain/krow_domain.dart'; /// Implementations of this interface should reside in the data layer. abstract interface class ShiftsRepositoryInterface { /// Retrieves the list of shifts assigned to the current user. - Future> getMyShifts(); + Future> getMyShifts({ + required DateTime start, + required DateTime end, + }); /// Retrieves available shifts matching the given [query] and [type]. Future> getAvailableShifts(String query, String type); @@ -15,14 +18,26 @@ abstract interface class ShiftsRepositoryInterface { Future> getPendingAssignments(); /// Retrieves detailed information for a specific shift by [shiftId]. - Future getShiftDetails(String shiftId); + Future getShiftDetails(String shiftId, {String? roleId}); /// Applies for a specific open shift. - Future applyForShift(String shiftId); + /// + /// [isInstantBook] determines if the application should be immediately accepted. + Future applyForShift( + String shiftId, { + bool isInstantBook = false, + String? roleId, + }); /// Accepts a pending shift assignment. Future acceptShift(String shiftId); /// Declines a pending shift assignment. Future declineShift(String shiftId); + + /// Retrieves shifts that were cancelled for the current user. + Future> getCancelledShifts(); + + /// Retrieves completed shifts for the current user. + Future> getHistoryShifts(); } diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/domain/usecases/accept_shift_usecase.dart b/apps/mobile/packages/features/staff/shifts/lib/src/domain/usecases/accept_shift_usecase.dart new file mode 100644 index 00000000..d3056716 --- /dev/null +++ b/apps/mobile/packages/features/staff/shifts/lib/src/domain/usecases/accept_shift_usecase.dart @@ -0,0 +1,12 @@ +import 'package:krow_domain/krow_domain.dart'; +import '../repositories/shifts_repository_interface.dart'; + +class AcceptShiftUseCase { + final ShiftsRepositoryInterface repository; + + AcceptShiftUseCase(this.repository); + + Future call(String shiftId) async { + return repository.acceptShift(shiftId); + } +} diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/domain/usecases/apply_for_shift_usecase.dart b/apps/mobile/packages/features/staff/shifts/lib/src/domain/usecases/apply_for_shift_usecase.dart new file mode 100644 index 00000000..6f2f3c7e --- /dev/null +++ b/apps/mobile/packages/features/staff/shifts/lib/src/domain/usecases/apply_for_shift_usecase.dart @@ -0,0 +1,19 @@ +import '../repositories/shifts_repository_interface.dart'; + +class ApplyForShiftUseCase { + final ShiftsRepositoryInterface repository; + + ApplyForShiftUseCase(this.repository); + + Future call( + String shiftId, { + bool isInstantBook = false, + String? roleId, + }) async { + return repository.applyForShift( + shiftId, + isInstantBook: isInstantBook, + roleId: roleId, + ); + } +} diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/domain/usecases/decline_shift_usecase.dart b/apps/mobile/packages/features/staff/shifts/lib/src/domain/usecases/decline_shift_usecase.dart new file mode 100644 index 00000000..7dbbee45 --- /dev/null +++ b/apps/mobile/packages/features/staff/shifts/lib/src/domain/usecases/decline_shift_usecase.dart @@ -0,0 +1,12 @@ +import 'package:krow_domain/krow_domain.dart'; +import '../repositories/shifts_repository_interface.dart'; + +class DeclineShiftUseCase { + final ShiftsRepositoryInterface repository; + + DeclineShiftUseCase(this.repository); + + Future call(String shiftId) async { + return repository.declineShift(shiftId); + } +} diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/domain/usecases/get_cancelled_shifts_usecase.dart b/apps/mobile/packages/features/staff/shifts/lib/src/domain/usecases/get_cancelled_shifts_usecase.dart new file mode 100644 index 00000000..47b82182 --- /dev/null +++ b/apps/mobile/packages/features/staff/shifts/lib/src/domain/usecases/get_cancelled_shifts_usecase.dart @@ -0,0 +1,12 @@ +import 'package:krow_domain/krow_domain.dart'; +import '../repositories/shifts_repository_interface.dart'; + +class GetCancelledShiftsUseCase { + final ShiftsRepositoryInterface repository; + + GetCancelledShiftsUseCase(this.repository); + + Future> call() async { + return repository.getCancelledShifts(); + } +} diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/domain/usecases/get_history_shifts_usecase.dart b/apps/mobile/packages/features/staff/shifts/lib/src/domain/usecases/get_history_shifts_usecase.dart new file mode 100644 index 00000000..7cb4066d --- /dev/null +++ b/apps/mobile/packages/features/staff/shifts/lib/src/domain/usecases/get_history_shifts_usecase.dart @@ -0,0 +1,12 @@ +import 'package:krow_domain/krow_domain.dart'; +import '../repositories/shifts_repository_interface.dart'; + +class GetHistoryShiftsUseCase { + final ShiftsRepositoryInterface repository; + + GetHistoryShiftsUseCase(this.repository); + + Future> call() async { + return repository.getHistoryShifts(); + } +} diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/domain/usecases/get_my_shifts_usecase.dart b/apps/mobile/packages/features/staff/shifts/lib/src/domain/usecases/get_my_shifts_usecase.dart index 5b9f172d..bcfea64c 100644 --- a/apps/mobile/packages/features/staff/shifts/lib/src/domain/usecases/get_my_shifts_usecase.dart +++ b/apps/mobile/packages/features/staff/shifts/lib/src/domain/usecases/get_my_shifts_usecase.dart @@ -1,18 +1,21 @@ import 'package:krow_core/core.dart'; import 'package:krow_domain/krow_domain.dart'; +import '../arguments/get_my_shifts_arguments.dart'; import '../repositories/shifts_repository_interface.dart'; /// Use case for retrieving the user's assigned shifts. /// /// This use case delegates to [ShiftsRepositoryInterface]. -class GetMyShiftsUseCase extends NoInputUseCase> { +class GetMyShiftsUseCase extends UseCase> { final ShiftsRepositoryInterface repository; GetMyShiftsUseCase(this.repository); @override - Future> call() async { - return repository.getMyShifts(); + Future> call(GetMyShiftsArguments arguments) async { + return repository.getMyShifts( + start: arguments.start, + end: arguments.end, + ); } } - diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/domain/usecases/get_shift_details_usecase.dart b/apps/mobile/packages/features/staff/shifts/lib/src/domain/usecases/get_shift_details_usecase.dart new file mode 100644 index 00000000..c7b38473 --- /dev/null +++ b/apps/mobile/packages/features/staff/shifts/lib/src/domain/usecases/get_shift_details_usecase.dart @@ -0,0 +1,18 @@ +import 'package:krow_core/core.dart'; +import 'package:krow_domain/krow_domain.dart'; +import '../arguments/get_shift_details_arguments.dart'; +import '../repositories/shifts_repository_interface.dart'; + +class GetShiftDetailsUseCase extends UseCase { + final ShiftsRepositoryInterface repository; + + GetShiftDetailsUseCase(this.repository); + + @override + Future call(GetShiftDetailsArguments params) { + return repository.getShiftDetails( + params.shiftId, + roleId: params.roleId, + ); + } +} diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/blocs/shift_details/shift_details_bloc.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/blocs/shift_details/shift_details_bloc.dart new file mode 100644 index 00000000..c16263fc --- /dev/null +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/blocs/shift_details/shift_details_bloc.dart @@ -0,0 +1,72 @@ +import 'package:bloc/bloc.dart'; +import '../../../domain/usecases/apply_for_shift_usecase.dart'; +import '../../../domain/usecases/decline_shift_usecase.dart'; +import '../../../domain/usecases/get_shift_details_usecase.dart'; +import '../../../domain/arguments/get_shift_details_arguments.dart'; +import 'shift_details_event.dart'; +import 'shift_details_state.dart'; + +class ShiftDetailsBloc extends Bloc { + final GetShiftDetailsUseCase getShiftDetails; + final ApplyForShiftUseCase applyForShift; + final DeclineShiftUseCase declineShift; + + ShiftDetailsBloc({ + required this.getShiftDetails, + required this.applyForShift, + required this.declineShift, + }) : super(ShiftDetailsInitial()) { + on(_onLoadDetails); + on(_onBookShift); + on(_onDeclineShift); + } + + Future _onLoadDetails( + LoadShiftDetailsEvent event, + Emitter emit, + ) async { + emit(ShiftDetailsLoading()); + try { + final shift = await getShiftDetails( + GetShiftDetailsArguments(shiftId: event.shiftId, roleId: event.roleId), + ); + if (shift != null) { + emit(ShiftDetailsLoaded(shift)); + } else { + emit(const ShiftDetailsError("Shift not found")); + } + } catch (e) { + emit(ShiftDetailsError(e.toString())); + } + } + + Future _onBookShift( + BookShiftDetailsEvent event, + Emitter emit, + ) async { + try { + await applyForShift( + event.shiftId, + isInstantBook: true, + roleId: event.roleId, + ); + emit( + ShiftActionSuccess("Shift successfully booked!", shiftDate: event.date), + ); + } catch (e) { + emit(ShiftDetailsError(e.toString())); + } + } + + Future _onDeclineShift( + DeclineShiftDetailsEvent event, + Emitter emit, + ) async { + try { + await declineShift(event.shiftId); + emit(const ShiftActionSuccess("Shift declined")); + } catch (e) { + emit(ShiftDetailsError(e.toString())); + } + } +} diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/blocs/shift_details/shift_details_event.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/blocs/shift_details/shift_details_event.dart new file mode 100644 index 00000000..48599313 --- /dev/null +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/blocs/shift_details/shift_details_event.dart @@ -0,0 +1,35 @@ +import 'package:equatable/equatable.dart'; + +abstract class ShiftDetailsEvent extends Equatable { + const ShiftDetailsEvent(); + + @override + List get props => []; +} + +class LoadShiftDetailsEvent extends ShiftDetailsEvent { + final String shiftId; + final String? roleId; + const LoadShiftDetailsEvent(this.shiftId, {this.roleId}); + + @override + List get props => [shiftId, roleId]; +} + +class BookShiftDetailsEvent extends ShiftDetailsEvent { + final String shiftId; + final String? roleId; + final DateTime? date; + const BookShiftDetailsEvent(this.shiftId, {this.roleId, this.date}); + + @override + List get props => [shiftId, roleId, date]; +} + +class DeclineShiftDetailsEvent extends ShiftDetailsEvent { + final String shiftId; + const DeclineShiftDetailsEvent(this.shiftId); + + @override + List get props => [shiftId]; +} diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/blocs/shift_details/shift_details_state.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/blocs/shift_details/shift_details_state.dart new file mode 100644 index 00000000..cf6cda49 --- /dev/null +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/blocs/shift_details/shift_details_state.dart @@ -0,0 +1,38 @@ +import 'package:equatable/equatable.dart'; +import 'package:krow_domain/krow_domain.dart'; + +abstract class ShiftDetailsState extends Equatable { + const ShiftDetailsState(); + + @override + List get props => []; +} + +class ShiftDetailsInitial extends ShiftDetailsState {} + +class ShiftDetailsLoading extends ShiftDetailsState {} + +class ShiftDetailsLoaded extends ShiftDetailsState { + final Shift shift; + const ShiftDetailsLoaded(this.shift); + + @override + List get props => [shift]; +} + +class ShiftDetailsError extends ShiftDetailsState { + final String message; + const ShiftDetailsError(this.message); + + @override + List get props => [message]; +} + +class ShiftActionSuccess extends ShiftDetailsState { + final String message; + final DateTime? shiftDate; + const ShiftActionSuccess(this.message, {this.shiftDate}); + + @override + List get props => [message, shiftDate]; +} diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/blocs/shifts/shifts_bloc.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/blocs/shifts/shifts_bloc.dart index 9b33b7c4..ff8dd4fd 100644 --- a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/blocs/shifts/shifts_bloc.dart +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/blocs/shifts/shifts_bloc.dart @@ -3,8 +3,11 @@ import 'package:equatable/equatable.dart'; import 'package:krow_domain/krow_domain.dart'; import 'package:meta/meta.dart'; -import '../../../domain/usecases/get_available_shifts_usecase.dart'; import '../../../domain/arguments/get_available_shifts_arguments.dart'; +import '../../../domain/arguments/get_my_shifts_arguments.dart'; +import '../../../domain/usecases/get_available_shifts_usecase.dart'; +import '../../../domain/usecases/get_cancelled_shifts_usecase.dart'; +import '../../../domain/usecases/get_history_shifts_usecase.dart'; import '../../../domain/usecases/get_my_shifts_usecase.dart'; import '../../../domain/usecases/get_pending_assignments_usecase.dart'; @@ -15,13 +18,21 @@ class ShiftsBloc extends Bloc { final GetMyShiftsUseCase getMyShifts; final GetAvailableShiftsUseCase getAvailableShifts; final GetPendingAssignmentsUseCase getPendingAssignments; + final GetCancelledShiftsUseCase getCancelledShifts; + final GetHistoryShiftsUseCase getHistoryShifts; ShiftsBloc({ required this.getMyShifts, required this.getAvailableShifts, required this.getPendingAssignments, + required this.getCancelledShifts, + required this.getHistoryShifts, }) : super(ShiftsInitial()) { on(_onLoadShifts); + on(_onLoadHistoryShifts); + on(_onLoadAvailableShifts); + on(_onLoadFindFirst); + on(_onLoadShiftsForRange); on(_onFilterAvailableShifts); } @@ -37,16 +48,164 @@ class ShiftsBloc extends Bloc { // Or load all for simplicity as per prototype logic which had them all in memory. try { - final myShiftsResult = await getMyShifts(); - final pendingResult = await getPendingAssignments(); + final List days = _getCalendarDaysForOffset(0); + final myShiftsResult = await getMyShifts( + GetMyShiftsArguments(start: days.first, end: days.last), + ); - // Initial available with defaults - final availableResult = await getAvailableShifts(const GetAvailableShiftsArguments()); + emit(ShiftsLoaded( + myShifts: myShiftsResult, + pendingShifts: const [], + cancelledShifts: const [], + availableShifts: const [], + historyShifts: const [], + availableLoading: false, + availableLoaded: false, + historyLoading: false, + historyLoaded: false, + myShiftsLoaded: true, + searchQuery: '', + jobType: 'all', + )); + } catch (_) { + emit(const ShiftsError('Failed to load shifts')); + } + } + + Future _onLoadHistoryShifts( + LoadHistoryShiftsEvent event, + Emitter emit, + ) async { + final currentState = state; + if (currentState is! ShiftsLoaded) return; + if (currentState.historyLoading || currentState.historyLoaded) return; + + emit(currentState.copyWith(historyLoading: true)); + try { + final historyResult = await getHistoryShifts(); + emit(currentState.copyWith( + myShiftsLoaded: true, + historyShifts: historyResult, + historyLoading: false, + historyLoaded: true, + )); + } catch (_) { + emit(currentState.copyWith(historyLoading: false)); + } + } + + Future _onLoadAvailableShifts( + LoadAvailableShiftsEvent event, + Emitter emit, + ) async { + final currentState = state; + if (currentState is! ShiftsLoaded) return; + if (currentState.availableLoading || currentState.availableLoaded) return; + + emit(currentState.copyWith(availableLoading: true)); + try { + final availableResult = + await getAvailableShifts(const GetAvailableShiftsArguments()); + emit(currentState.copyWith( + availableShifts: _filterPastShifts(availableResult), + availableLoading: false, + availableLoaded: true, + )); + } catch (_) { + emit(currentState.copyWith(availableLoading: false)); + } + } + + Future _onLoadFindFirst( + LoadFindFirstEvent event, + Emitter emit, + ) async { + if (state is! ShiftsLoaded) { + emit(const ShiftsLoaded( + myShifts: [], + pendingShifts: [], + cancelledShifts: [], + availableShifts: [], + historyShifts: [], + availableLoading: false, + availableLoaded: false, + historyLoading: false, + historyLoaded: false, + myShiftsLoaded: false, + searchQuery: '', + jobType: 'all', + )); + } + + final currentState = + state is ShiftsLoaded ? state as ShiftsLoaded : null; + if (currentState != null && currentState.availableLoaded) return; + + if (currentState != null) { + emit(currentState.copyWith(availableLoading: true)); + } + + try { + final availableResult = + await getAvailableShifts(const GetAvailableShiftsArguments()); + final loadedState = state is ShiftsLoaded + ? state as ShiftsLoaded + : const ShiftsLoaded( + myShifts: [], + pendingShifts: [], + cancelledShifts: [], + availableShifts: [], + historyShifts: [], + availableLoading: true, + availableLoaded: false, + historyLoading: false, + historyLoaded: false, + myShiftsLoaded: false, + searchQuery: '', + jobType: 'all', + ); + emit(loadedState.copyWith( + availableShifts: _filterPastShifts(availableResult), + availableLoading: false, + availableLoaded: true, + )); + } catch (_) { + if (state is ShiftsLoaded) { + final current = state as ShiftsLoaded; + emit(current.copyWith(availableLoading: false)); + } + } + } + + Future _onLoadShiftsForRange( + LoadShiftsForRangeEvent event, + Emitter emit, + ) async { + try { + final myShiftsResult = await getMyShifts( + GetMyShiftsArguments(start: event.start, end: event.end), + ); + + if (state is ShiftsLoaded) { + final currentState = state as ShiftsLoaded; + emit(currentState.copyWith( + myShifts: myShiftsResult, + myShiftsLoaded: true, + )); + return; + } emit(ShiftsLoaded( myShifts: myShiftsResult, - pendingShifts: pendingResult, - availableShifts: availableResult, + pendingShifts: const [], + cancelledShifts: const [], + availableShifts: const [], + historyShifts: const [], + availableLoading: false, + availableLoaded: false, + historyLoading: false, + historyLoaded: false, + myShiftsLoaded: true, searchQuery: '', jobType: 'all', )); @@ -61,6 +220,10 @@ class ShiftsBloc extends Bloc { ) async { final currentState = state; if (currentState is ShiftsLoaded) { + if (!currentState.availableLoaded && !currentState.availableLoading) { + add(LoadAvailableShiftsEvent()); + return; + } // Optimistic update or loading indicator? // Since it's filtering, we can just reload available. @@ -71,7 +234,7 @@ class ShiftsBloc extends Bloc { )); emit(currentState.copyWith( - availableShifts: result, + availableShifts: _filterPastShifts(result), searchQuery: event.query ?? currentState.searchQuery, jobType: event.jobType ?? currentState.jobType, )); @@ -80,4 +243,28 @@ class ShiftsBloc extends Bloc { } } } + + List _getCalendarDaysForOffset(int weekOffset) { + final now = DateTime.now(); + final int reactDayIndex = now.weekday == 7 ? 0 : now.weekday; + final int daysSinceFriday = (reactDayIndex + 2) % 7; + final start = now + .subtract(Duration(days: daysSinceFriday)) + .add(Duration(days: weekOffset * 7)); + final startDate = DateTime(start.year, start.month, start.day); + return List.generate(7, (index) => startDate.add(Duration(days: index))); + } + + List _filterPastShifts(List shifts) { + final now = DateTime.now(); + return shifts.where((shift) { + if (shift.date.isEmpty) return false; + try { + final shiftDate = DateTime.parse(shift.date); + return shiftDate.isAfter(now); + } catch (_) { + return false; + } + }).toList(); + } } diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/blocs/shifts/shifts_event.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/blocs/shifts/shifts_event.dart index 41e01253..d25866e0 100644 --- a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/blocs/shifts/shifts_event.dart +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/blocs/shifts/shifts_event.dart @@ -10,6 +10,25 @@ sealed class ShiftsEvent extends Equatable { class LoadShiftsEvent extends ShiftsEvent {} +class LoadHistoryShiftsEvent extends ShiftsEvent {} + +class LoadAvailableShiftsEvent extends ShiftsEvent {} + +class LoadFindFirstEvent extends ShiftsEvent {} + +class LoadShiftsForRangeEvent extends ShiftsEvent { + final DateTime start; + final DateTime end; + + const LoadShiftsForRangeEvent({ + required this.start, + required this.end, + }); + + @override + List get props => [start, end]; +} + class FilterAvailableShiftsEvent extends ShiftsEvent { final String? query; final String? jobType; @@ -19,3 +38,19 @@ class FilterAvailableShiftsEvent extends ShiftsEvent { @override List get props => [query, jobType]; } + +class AcceptShiftEvent extends ShiftsEvent { + final String shiftId; + const AcceptShiftEvent(this.shiftId); + + @override + List get props => [shiftId]; +} + +class DeclineShiftEvent extends ShiftsEvent { + final String shiftId; + const DeclineShiftEvent(this.shiftId); + + @override + List get props => [shiftId]; +} diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/blocs/shifts/shifts_state.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/blocs/shifts/shifts_state.dart index c6051cea..d32e3fba 100644 --- a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/blocs/shifts/shifts_state.dart +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/blocs/shifts/shifts_state.dart @@ -15,14 +15,28 @@ class ShiftsLoading extends ShiftsState {} class ShiftsLoaded extends ShiftsState { final List myShifts; final List pendingShifts; + final List cancelledShifts; final List availableShifts; + final List historyShifts; + final bool availableLoading; + final bool availableLoaded; + final bool historyLoading; + final bool historyLoaded; + final bool myShiftsLoaded; final String searchQuery; final String jobType; const ShiftsLoaded({ required this.myShifts, required this.pendingShifts, + required this.cancelledShifts, required this.availableShifts, + required this.historyShifts, + required this.availableLoading, + required this.availableLoaded, + required this.historyLoading, + required this.historyLoaded, + required this.myShiftsLoaded, required this.searchQuery, required this.jobType, }); @@ -30,21 +44,48 @@ class ShiftsLoaded extends ShiftsState { ShiftsLoaded copyWith({ List? myShifts, List? pendingShifts, + List? cancelledShifts, List? availableShifts, + List? historyShifts, + bool? availableLoading, + bool? availableLoaded, + bool? historyLoading, + bool? historyLoaded, + bool? myShiftsLoaded, String? searchQuery, String? jobType, }) { return ShiftsLoaded( myShifts: myShifts ?? this.myShifts, pendingShifts: pendingShifts ?? this.pendingShifts, + cancelledShifts: cancelledShifts ?? this.cancelledShifts, availableShifts: availableShifts ?? this.availableShifts, + historyShifts: historyShifts ?? this.historyShifts, + availableLoading: availableLoading ?? this.availableLoading, + availableLoaded: availableLoaded ?? this.availableLoaded, + historyLoading: historyLoading ?? this.historyLoading, + historyLoaded: historyLoaded ?? this.historyLoaded, + myShiftsLoaded: myShiftsLoaded ?? this.myShiftsLoaded, searchQuery: searchQuery ?? this.searchQuery, jobType: jobType ?? this.jobType, ); } @override - List get props => [myShifts, pendingShifts, availableShifts, searchQuery, jobType]; + List get props => [ + myShifts, + pendingShifts, + cancelledShifts, + availableShifts, + historyShifts, + availableLoading, + availableLoaded, + historyLoading, + historyLoaded, + myShiftsLoaded, + searchQuery, + jobType, + ]; } class ShiftsError extends ShiftsState { diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/navigation/shifts_navigator.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/navigation/shifts_navigator.dart index 4832055b..4724218e 100644 --- a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/navigation/shifts_navigator.dart +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/navigation/shifts_navigator.dart @@ -2,9 +2,11 @@ import 'package:flutter_modular/flutter_modular.dart'; import 'package:krow_domain/krow_domain.dart'; extension ShiftsNavigator on IModularNavigator { - void pushShiftDetails(Shift shift) { - pushNamed('/shifts/details/${shift.id}', arguments: shift); + void navigateToShiftsHome({DateTime? selectedDate}) { + navigate('/worker-main/shifts/', arguments: {'selectedDate': selectedDate}); + } + + void pushShiftDetails(Shift shift) { + navigate('/worker-main/shift-details/${shift.id}', arguments: shift); } - - // Example for going back or internal navigation if needed } diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/pages/shift_details_page.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/pages/shift_details_page.dart index fdb92535..d384df20 100644 --- a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/pages/shift_details_page.dart +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/pages/shift_details_page.dart @@ -1,20 +1,13 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_modular/flutter_modular.dart'; -import 'package:lucide_icons/lucide_icons.dart'; -import 'package:intl/intl.dart'; -import 'package:design_system/design_system.dart'; import 'package:krow_domain/krow_domain.dart'; - -// Shim to match POC styles locally -class AppColors { - static const Color krowBlue = UiColors.primary; - static const Color krowYellow = Color(0xFFFFED4A); - static const Color krowCharcoal = UiColors.textPrimary; // 121826 - static const Color krowMuted = UiColors.textSecondary; // 6A7382 - static const Color krowBorder = UiColors.border; // E3E6E9 - static const Color krowBackground = UiColors.background; // FAFBFC - static const Color white = Colors.white; -} +import 'package:design_system/design_system.dart'; // Re-added for UiIcons/Colors as they are used in expanded logic +import 'package:intl/intl.dart'; +import 'package:staff_shifts/staff_shifts.dart'; +import '../blocs/shift_details/shift_details_bloc.dart'; +import '../blocs/shift_details/shift_details_event.dart'; +import '../blocs/shift_details/shift_details_state.dart'; class ShiftDetailsPage extends StatefulWidget { final String shiftId; @@ -27,53 +20,9 @@ class ShiftDetailsPage extends StatefulWidget { } class _ShiftDetailsPageState extends State { - late Shift _shift; - bool _isLoading = true; - bool _showDetails = true; + bool _actionDialogOpen = false; bool _isApplying = false; - // Mock Managers - final List> _managers = [ - {'name': 'John Smith', 'phone': '+1 123 456 7890'}, - {'name': 'Jane Doe', 'phone': '+1 123 456 7890'}, - ]; - - @override - void initState() { - super.initState(); - _loadShift(); - } - - void _loadShift() async { - if (widget.shift != null) { - _shift = widget.shift!; - setState(() => _isLoading = false); - } else { - await Future.delayed(const Duration(milliseconds: 500)); - if (mounted) { - // Fallback mock shift - setState(() { - _shift = Shift( - id: widget.shiftId, - title: 'Event Server', - clientName: 'Grand Hotel', - logoUrl: null, - hourlyRate: 25.0, - date: DateFormat('yyyy-MM-dd').format(DateTime.now()), - startTime: '16:00', - endTime: '22:00', - location: 'Downtown', - locationAddress: '123 Main St, New York, NY', - status: 'open', - createdDate: DateTime.now().toIso8601String(), - description: 'Provide exceptional customer service. Respond to guest requests or concerns promptly and professionally.', - ); - _isLoading = false; - }); - } - } - } - String _formatTime(String time) { if (time.isEmpty) return ''; try { @@ -81,7 +30,7 @@ class _ShiftDetailsPageState extends State { final hour = int.parse(parts[0]); final minute = int.parse(parts[1]); final dt = DateTime(2022, 1, 1, hour, minute); - return DateFormat('h:mma').format(dt).toLowerCase(); + return DateFormat('h:mm a').format(dt); } catch (e) { return time; } @@ -91,724 +40,610 @@ class _ShiftDetailsPageState extends State { if (dateStr.isEmpty) return ''; try { final date = DateTime.parse(dateStr); - return DateFormat('MMMM d').format(date); + return DateFormat('EEEE, MMMM d, y').format(date); } catch (e) { return dateStr; } } - double _calculateHours(String start, String end) { + double _calculateDuration(Shift shift) { + if (shift.startTime.isEmpty || shift.endTime.isEmpty) { + return 0; + } try { - final startParts = start.split(':').map(int.parse).toList(); - final endParts = end.split(':').map(int.parse).toList(); - double h = (endParts[0] - startParts[0]) + (endParts[1] - startParts[1]) / 60; - if (h < 0) h += 24; - return h; - } catch (e) { + final s = shift.startTime.split(':').map(int.parse).toList(); + final e = shift.endTime.split(':').map(int.parse).toList(); + double hours = ((e[0] * 60 + e[1]) - (s[0] * 60 + s[1])) / 60; + if (hours < 0) hours += 24; + return hours.roundToDouble(); + } catch (_) { return 0; } } + Widget _buildStatCard(IconData icon, String value, String label) { + return Container( + padding: const EdgeInsets.symmetric(vertical: 16), + decoration: BoxDecoration( + color: const Color(0xFFF8FAFC), + borderRadius: BorderRadius.circular(16), + border: Border.all(color: UiColors.border), + ), + child: Column( + children: [ + Container( + width: 40, + height: 40, + decoration: const BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + ), + child: Icon(icon, size: 20, color: UiColors.iconSecondary), + ), + const SizedBox(height: 8), + Text( + value, + style: UiTypography.title1m.copyWith(color: UiColors.textPrimary), + ), + Text( + label, + style: UiTypography.footnote2r.copyWith( + color: UiColors.textSecondary, + ), + ), + ], + ), + ); + } + + Widget _buildTimeBox(String label, String time) { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: const Color(0xFFF8FAFC), + borderRadius: BorderRadius.circular(16), + ), + child: Column( + children: [ + Text( + label, + style: const TextStyle( + fontSize: 10, + fontWeight: FontWeight.bold, + color: UiColors.textSecondary, + letterSpacing: 0.5, + ), + ), + const SizedBox(height: 4), + Text( + _formatTime(time), + style: UiTypography.display2m.copyWith( + fontSize: 20, + color: UiColors.textPrimary, + ), + ), + ], + ), + ); + } + @override Widget build(BuildContext context) { - if (_isLoading) { - return const Scaffold( - backgroundColor: AppColors.krowBackground, - body: Center(child: CircularProgressIndicator()), - ); - } - - final hours = _calculateHours(_shift.startTime, _shift.endTime); - final totalPay = _shift.hourlyRate * hours; - - return Scaffold( - backgroundColor: AppColors.krowBackground, - appBar: AppBar( - backgroundColor: Colors.white, - elevation: 0, - leading: IconButton( - icon: const Icon(LucideIcons.chevronLeft, color: AppColors.krowMuted), - onPressed: () => Modular.to.pop(), - ), - bottom: PreferredSize( - preferredSize: const Size.fromHeight(1.0), - child: Container(color: AppColors.krowBorder, height: 1.0), - ), - ), - body: Stack( - children: [ - SingleChildScrollView( - padding: const EdgeInsets.fromLTRB(20, 20, 20, 120), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Pending Badge - Align( - alignment: Alignment.centerRight, - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 4, - ), - decoration: BoxDecoration( - color: AppColors.krowYellow.withOpacity(0.3), - borderRadius: BorderRadius.circular(20), - ), - child: const Text( - 'Pending 6h ago', - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w500, - color: AppColors.krowCharcoal, - ), - ), - ), + return BlocProvider( + create: (_) => + Modular.get() + ..add( + LoadShiftDetailsEvent( + widget.shiftId, + roleId: widget.shift?.roleId, + ), + ), + child: BlocListener( + listener: (context, state) { + if (state is ShiftActionSuccess || state is ShiftDetailsError) { + _closeActionDialog(context); + } + if (state is ShiftActionSuccess) { + _isApplying = false; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(state.message), + backgroundColor: const Color(0xFF10B981), + ), + ); + Modular.to.navigateToShiftsHome(selectedDate: state.shiftDate); + } else if (state is ShiftDetailsError) { + if (_isApplying || widget.shift == null) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(state.message), + backgroundColor: const Color(0xFFEF4444), ), - const SizedBox(height: 16), + ); + } + _isApplying = false; + } + }, + child: BlocBuilder( + builder: (context, state) { + if (state is ShiftDetailsLoading) { + return const Scaffold( + body: Center(child: CircularProgressIndicator()), + ); + } - // Header - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - width: 56, - height: 56, - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - border: Border.all(color: AppColors.krowBorder), - ), - child: _shift.logoUrl != null - ? ClipRRect( - borderRadius: BorderRadius.circular(12), - child: Image.network( - _shift.logoUrl!, - fit: BoxFit.contain, - ), - ) - : Center( - child: Text( - _shift.clientName.isNotEmpty ? _shift.clientName[0] : 'K', - style: const TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, - color: AppColors.krowBlue, - ), - ), - ), - ), - const SizedBox(width: 16), - Expanded( + Shift? displayShift; + if (state is ShiftDetailsLoaded) { + displayShift = state.shift; + } else { + displayShift = widget.shift; + } + + if (displayShift == null) { + return const Scaffold( + body: Center(child: Text("Shift not found")), + ); + } + + final duration = _calculateDuration(displayShift); + final estimatedTotal = + displayShift.totalValue ?? (displayShift.hourlyRate * duration); + final openSlots = + (displayShift.requiredSlots ?? 0) - + (displayShift.filledSlots ?? 0); + + return Scaffold( + appBar: UiAppBar( + title: displayShift.title, + centerTitle: false, + onLeadingPressed: () => Modular.to.navigateToShiftsHome(), + ), + body: Column( + children: [ + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.all(20.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + // Vendor Section + Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Expanded( - child: Text( - _shift.title, - style: const TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - color: AppColors.krowCharcoal, - ), + const Text( + "VENDOR", + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.bold, + color: UiColors.textSecondary, + letterSpacing: 0.5, ), ), - Column( - crossAxisAlignment: CrossAxisAlignment.end, + const SizedBox(height: 8), + Row( children: [ - Text( - '\$${_shift.hourlyRate.toStringAsFixed(0)}/h', - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: AppColors.krowCharcoal, - ), + Container( + width: 24, + height: 24, + child: displayShift.logoUrl != null + ? ClipRRect( + borderRadius: BorderRadius.circular( + 6, + ), + child: Image.network( + displayShift.logoUrl!, + fit: BoxFit.cover, + ), + ) + : const Center( + child: Icon( + UiIcons.briefcase, + color: UiColors.primary, + size: 20, + ), + ), ), + const SizedBox(width: 8), Text( - '(exp.total \$${totalPay.toStringAsFixed(0)})', - style: const TextStyle( - fontSize: 12, - color: AppColors.krowMuted, + displayShift.clientName, + style: UiTypography.headline5m.copyWith( + color: UiColors.textPrimary, ), ), ], ), ], ), - Text( - _shift.clientName, - style: const TextStyle(color: AppColors.krowMuted), - ), - ], - ), - ), - ], - ), - const SizedBox(height: 16), + const SizedBox(height: 24), - // Tags - Row( - children: [ - _buildTag( - LucideIcons.zap, - 'Immediate start', - AppColors.krowBlue.withOpacity(0.1), - AppColors.krowBlue, - ), - const SizedBox(width: 8), - _buildTag( - LucideIcons.star, - 'No experience', - AppColors.krowYellow.withOpacity(0.3), - AppColors.krowCharcoal, - ), - ], - ), - const SizedBox(height: 24), - - // Additional Details Collapsible - Container( - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - border: Border.all(color: AppColors.krowBorder), - ), - child: Column( - children: [ - InkWell( - onTap: () => - setState(() => _showDetails = !_showDetails), - child: Padding( - padding: const EdgeInsets.all(16), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + // Date Section + Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( - 'ADDITIONAL DETAILS', + "SHIFT DATE", style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w600, + fontSize: 10, + fontWeight: FontWeight.bold, + color: UiColors.textSecondary, letterSpacing: 0.5, - color: AppColors.krowMuted, ), ), - Icon( - _showDetails - ? LucideIcons.chevronUp - : LucideIcons.chevronDown, - color: AppColors.krowMuted, - size: 20, + const SizedBox(height: 8), + Row( + children: [ + const Icon( + UiIcons.calendar, + size: 20, + color: UiColors.primary, + ), + const SizedBox(width: 8), + Text( + _formatDate(displayShift.date), + style: UiTypography.headline5m.copyWith( + color: UiColors.textPrimary, + ), + ), + ], ), ], ), - ), - ), - if (_showDetails) - Padding( - padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), - child: Column( + const SizedBox(height: 24), + + // Worker Capacity / Open Slots + if ((displayShift.requiredSlots ?? 0) > 0) + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: const Color(0xFFF0FDF4), // green-50 + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: const Color(0xFFBBF7D0), + ), // green-200 + ), + child: Row( + children: [ + const Icon( + Icons.people_alt_outlined, + size: 20, + color: Color(0xFF15803D), + ), // green-700, using Material Icon as generic fallback + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "$openSlots spots remaining", + style: UiTypography.body2b.copyWith( + color: const Color(0xFF15803D), + ), + ), + Text( + "${displayShift.filledSlots ?? 0} filled out of ${displayShift.requiredSlots}", + style: UiTypography.body3r.copyWith( + color: const Color(0xFF166534), + ), + ), + ], + ), + ), + SizedBox( + width: 60, + child: LinearProgressIndicator( + value: (displayShift.requiredSlots! > 0) + ? (displayShift.filledSlots ?? 0) / + displayShift.requiredSlots! + : 0, + backgroundColor: Colors.white, + color: const Color(0xFF15803D), + minHeight: 6, + borderRadius: BorderRadius.circular(3), + ), + ), + ], + ), + ), + const SizedBox(height: 24), + + // Stats Grid + GridView.count( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + crossAxisCount: 3, + crossAxisSpacing: 12, + childAspectRatio: 0.85, children: [ - _buildDetailRow('Tips', 'Yes', true), - _buildDetailRow('Travel Time', 'Yes', true), - _buildDetailRow('Meal Provided', 'No', false), - _buildDetailRow('Parking Available', 'Yes', true), - _buildDetailRow('Gas Compensation', 'No', false), + _buildStatCard( + UiIcons.dollar, + "\$${estimatedTotal.toStringAsFixed(0)}", + "Total Pay", + ), + _buildStatCard( + UiIcons.dollar, + "\$${displayShift.hourlyRate.toInt()}", + "Per Hour", + ), + _buildStatCard( + UiIcons.clock, + "${duration.toInt()}h", + "Duration", + ), ], ), - ), - ], - ), - ), - const SizedBox(height: 16), + const SizedBox(height: 24), - // Date & Duration Grid - Row( - children: [ - Expanded( - child: Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - border: Border.all(color: AppColors.krowBorder), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'START', - style: TextStyle( - fontSize: 10, - fontWeight: FontWeight.bold, - color: AppColors.krowMuted, + // Shift Timing + Row( + children: [ + Expanded( + child: _buildTimeBox( + "START TIME", + displayShift.startTime, + ), ), - ), - const SizedBox(height: 8), - Text( - _formatDate(_shift.date), - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: AppColors.krowCharcoal, + const SizedBox(width: 12), + Expanded( + child: _buildTimeBox( + "END TIME", + displayShift.endTime, + ), ), - ), - const Text( - 'Date', - style: TextStyle( - fontSize: 12, - color: AppColors.krowMuted, + ], + ), + const SizedBox(height: 24), + + // Location + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "LOCATION", + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.bold, + color: UiColors.textSecondary, + letterSpacing: 0.5, + ), ), - ), - const SizedBox(height: 12), - Text( - _formatTime(_shift.startTime), - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: AppColors.krowCharcoal, + const SizedBox(height: 8), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + displayShift.location.isEmpty + ? "TBD" + : displayShift.location, + style: UiTypography.title1m.copyWith( + color: UiColors.textPrimary, + ), + ), + Text( + displayShift.location.isEmpty + ? "TBD" + : displayShift.locationAddress, + style: UiTypography.title1m.copyWith( + color: UiColors.textPrimary, + ), + ), + ], ), - ), - const Text( - 'Time', - style: TextStyle( - fontSize: 12, - color: AppColors.krowMuted, + ], + ), + const SizedBox(height: 24), + + // Additional Info + if (displayShift.description != null) ...[ + SizedBox( + width: double.infinity, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "ADDITIONAL INFO", + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.bold, + color: UiColors.textSecondary, + letterSpacing: 0.5, + ), + ), + const SizedBox(height: 8), + Text( + displayShift.description!, + style: UiTypography.body2m.copyWith( + color: UiColors.textPrimary, + ), + ), + ], ), ), ], - ), - ), - ), - const SizedBox(width: 16), - Expanded( - child: Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - border: Border.all(color: AppColors.krowBorder), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'DURATION', - style: TextStyle( - fontSize: 10, - fontWeight: FontWeight.bold, - color: AppColors.krowMuted, - ), - ), - const SizedBox(height: 8), - Text( - '${hours.toStringAsFixed(0)} hours', - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: AppColors.krowCharcoal, - ), - ), - const Text( - 'Shift duration', - style: TextStyle( - fontSize: 12, - color: AppColors.krowMuted, - ), - ), - const SizedBox(height: 12), - const Text( - '1 hour', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: AppColors.krowCharcoal, - ), - ), - const Text( - 'Break duration', - style: TextStyle( - fontSize: 12, - color: AppColors.krowMuted, - ), - ), - ], - ), - ), - ), - ], - ), - const SizedBox(height: 16), - - // Location - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - border: Border.all(color: AppColors.krowBorder), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'LOCATION', - style: TextStyle( - fontSize: 10, - fontWeight: FontWeight.bold, - color: AppColors.krowMuted, - ), - ), - const SizedBox(height: 12), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + const SizedBox(height: 20), + if (displayShift!.status != 'confirmed' && + displayShift!.hasApplied != true && + (displayShift!.requiredSlots == null || + displayShift!.filledSlots == null || + displayShift!.filledSlots! < + displayShift!.requiredSlots!)) + Row( children: [ - Text( - _shift.location, - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: AppColors.krowCharcoal, + Expanded( + child: OutlinedButton( + onPressed: () => _declineShift( + context, + displayShift!.id, + ), + style: OutlinedButton.styleFrom( + foregroundColor: const Color(0xFFEF4444), + side: const BorderSide( + color: Color(0xFFEF4444), + ), + padding: const EdgeInsets.symmetric( + vertical: 16, + ), + ), + child: const Text("Decline"), ), ), - Text( - _shift.locationAddress, - style: const TextStyle( - fontSize: 14, - color: AppColors.krowMuted, + const SizedBox(width: 16), + Expanded( + child: ElevatedButton( + onPressed: () => _bookShift( + context, + displayShift!, + ), + style: ElevatedButton.styleFrom( + backgroundColor: const Color( + 0xFF10B981, + ), + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric( + vertical: 16, + ), + ), + child: const Text("Book Shift"), ), ), ], ), - ), - OutlinedButton.icon( - onPressed: () { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - _shift.locationAddress, - ), - duration: const Duration(seconds: 3), - ), - ); - }, - icon: const Icon(LucideIcons.navigation, size: 14), - label: const Text('Get direction'), - style: OutlinedButton.styleFrom( - foregroundColor: AppColors.krowCharcoal, - side: const BorderSide( - color: AppColors.krowBorder, - ), - textStyle: const TextStyle(fontSize: 12), - ), + SizedBox( + height: MediaQuery.of(context).padding.bottom + 10, ), ], ), - const SizedBox(height: 16), - Container( - height: 160, - width: double.infinity, - decoration: BoxDecoration( - color: const Color(0xFFF1F3F5), - borderRadius: BorderRadius.circular(12), - ), - child: const Center( - child: Icon( - LucideIcons.map, - color: AppColors.krowMuted, - size: 48, - ), - ), - ), - ], + ), ), - ), - const SizedBox(height: 16), + ], + ), + ); + }, + ), + ), + ); + } - // Manager Contact - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - border: Border.all(color: AppColors.krowBorder), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'MANAGER CONTACT DETAILS', - style: TextStyle( - fontSize: 10, - fontWeight: FontWeight.bold, - color: AppColors.krowMuted, - ), - ), - const SizedBox(height: 16), - ..._managers - .map( - (manager) => Padding( - padding: const EdgeInsets.only(bottom: 16), - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - Container( - width: 40, - height: 40, - decoration: BoxDecoration( - gradient: const LinearGradient( - colors: [ - AppColors.krowBlue, - Color(0xFF0830B8), - ], - ), - borderRadius: BorderRadius.circular( - 8, - ), - ), - child: const Center( - child: Icon( - LucideIcons.user, - color: Colors.white, - size: 20, - ), - ), - ), - const SizedBox(width: 12), - Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - manager['name']!, - style: const TextStyle( - fontWeight: FontWeight.w600, - color: AppColors.krowCharcoal, - ), - ), - Text( - manager['phone']!, - style: const TextStyle( - fontSize: 12, - color: AppColors.krowMuted, - ), - ), - ], - ), - ], - ), - OutlinedButton.icon( - onPressed: () { - ScaffoldMessenger.of( - context, - ).showSnackBar( - SnackBar( - content: Text(manager['phone']!), - duration: const Duration(seconds: 3), - ), - ); - }, - icon: const Icon( - LucideIcons.phone, - size: 14, - color: Color(0xFF059669), - ), - label: const Text( - 'Call', - style: TextStyle( - color: Color(0xFF059669), - ), - ), - style: OutlinedButton.styleFrom( - side: const BorderSide( - color: Color(0xFFA7F3D0), - ), - backgroundColor: const Color(0xFFECFDF5), - textStyle: const TextStyle(fontSize: 12), - ), - ), - ], - ), - ), - ) - .toList(), - ], - ), - ), - const SizedBox(height: 16), - - // Additional Info - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - border: Border.all(color: AppColors.krowBorder), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'ADDITIONAL INFO', - style: TextStyle( - fontSize: 10, - fontWeight: FontWeight.bold, - color: AppColors.krowMuted, - ), - ), - const SizedBox(height: 12), - Text( - _shift.description ?? - 'Providing Exceptional Customer Service.', - style: const TextStyle( - fontSize: 14, - color: AppColors.krowMuted, - height: 1.5, - ), - ), - ], - ), - ), - ], - ), + void _bookShift( + BuildContext context, + Shift shift, + ) { + showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: const Text('Book Shift'), + content: const Text('Do you want to instantly book this shift?'), + actions: [ + TextButton( + onPressed: () => Modular.to.pop(), + child: const Text('Cancel'), ), - - // Bottom Actions - Positioned( - bottom: 0, - left: 0, - right: 0, - child: Container( - padding: const EdgeInsets.all(20), - decoration: const BoxDecoration( - color: Colors.white, - border: Border(top: BorderSide(color: AppColors.krowBorder)), - ), - child: SafeArea( - top: false, - child: Column( - children: [ - SizedBox( - width: double.infinity, - height: 56, - child: ElevatedButton( - onPressed: () async { - setState(() => _isApplying = true); - await Future.delayed(const Duration(seconds: 1)); - if (mounted) { - setState(() => _isApplying = false); - Modular.to.pop(); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Shift Accepted!'), - backgroundColor: Color(0xFF10B981), - ), - ); - } - }, - style: ElevatedButton.styleFrom( - backgroundColor: AppColors.krowBlue, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - elevation: 0, - ), - child: _isApplying - ? const SizedBox( - width: 24, - height: 24, - child: CircularProgressIndicator( - color: Colors.white, - ), - ) - : const Text( - 'Accept shift', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - color: Colors.white, - ), - ), - ), - ), - const SizedBox(height: 12), - SizedBox( - width: double.infinity, - height: 48, - child: TextButton( - onPressed: () => Modular.to.pop(), - child: const Text( - 'Decline shift', - style: TextStyle( - color: Color(0xFFEF4444), - fontSize: 16, - fontWeight: FontWeight.w500, - ), - ), - ), - ), - ], + TextButton( + onPressed: () { + Modular.to.pop(); + _showApplyingDialog(context, shift); + BlocProvider.of(context).add( + BookShiftDetailsEvent( + shift.id, + roleId: shift.roleId, + date: DateTime.tryParse(shift.date), ), - ), + ); + }, + style: TextButton.styleFrom( + foregroundColor: const Color(0xFF10B981), ), + child: const Text('Book'), ), ], ), ); } - Widget _buildTag(IconData icon, String label, Color bg, Color text) { - return Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), - decoration: BoxDecoration( - color: bg, - borderRadius: BorderRadius.circular(20), - ), - child: Row( - children: [ - Icon(icon, size: 14, color: text), - const SizedBox(width: 4), - Text( - label, - style: TextStyle( - color: text, - fontSize: 12, - fontWeight: FontWeight.w600, + void _declineShift(BuildContext context, String id) { + showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: const Text('Decline Shift'), + content: const Text( + 'Are you sure you want to decline this shift? It will be hidden from your available jobs.', + ), + actions: [ + TextButton( + onPressed: () => Modular.to.pop(), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () { + BlocProvider.of( + context, + ).add(DeclineShiftDetailsEvent(id)); + }, + style: TextButton.styleFrom( + foregroundColor: const Color(0xFFEF4444), ), + child: const Text('Decline'), ), ], ), ); } - Widget _buildDetailRow(String label, String value, bool isPositive) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 6), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - label, - style: const TextStyle(fontSize: 14, color: AppColors.krowMuted), - ), - Text( - value, - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - color: isPositive ? const Color(0xFF059669) : AppColors.krowMuted, + void _showApplyingDialog(BuildContext context, Shift shift) { + if (_actionDialogOpen) return; + _actionDialogOpen = true; + _isApplying = true; + showDialog( + context: context, + useRootNavigator: true, + barrierDismissible: false, + builder: (ctx) => AlertDialog( + title: const Text('Applying'), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox( + height: 36, + width: 36, + child: CircularProgressIndicator(), ), - ), - ], + const SizedBox(height: 16), + Text( + shift.title, + style: UiTypography.body2b.copyWith( + color: UiColors.textPrimary, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 6), + Text( + '${_formatDate(shift.date)} • ${_formatTime(shift.startTime)} - ${_formatTime(shift.endTime)}', + style: UiTypography.body3r.copyWith( + color: UiColors.textSecondary, + ), + textAlign: TextAlign.center, + ), + if (shift.clientName.isNotEmpty) ...[ + const SizedBox(height: 6), + Text( + shift.clientName, + style: UiTypography.body3r.copyWith( + color: UiColors.textSecondary, + ), + textAlign: TextAlign.center, + ), + ], + ], + ), ), - ); + ).then((_) { + _actionDialogOpen = false; + }); + } + + void _closeActionDialog(BuildContext context) { + if (!_actionDialogOpen) return; + Navigator.of(context, rootNavigator: true).pop(); + _actionDialogOpen = false; } } diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/pages/shifts_page.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/pages/shifts_page.dart index e89ded58..3d86039d 100644 --- a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/pages/shifts_page.dart +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/pages/shifts_page.dart @@ -1,29 +1,18 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_modular/flutter_modular.dart'; -import 'package:lucide_icons/lucide_icons.dart'; -import 'package:intl/intl.dart'; import 'package:design_system/design_system.dart'; import 'package:krow_domain/krow_domain.dart'; import '../blocs/shifts/shifts_bloc.dart'; -import '../widgets/my_shift_card.dart'; -import '../widgets/shift_assignment_card.dart'; - -// Shim to match POC styles locally -class AppColors { - static const Color krowBlue = UiColors.primary; - static const Color krowYellow = Color(0xFFFFED4A); - static const Color krowCharcoal = UiColors.textPrimary; - static const Color krowMuted = UiColors.textSecondary; - static const Color krowBorder = UiColors.border; - static const Color krowBackground = UiColors.background; - static const Color white = Colors.white; - static const Color black = Colors.black; -} +import '../widgets/tabs/my_shifts_tab.dart'; +import '../widgets/tabs/find_shifts_tab.dart'; +import '../widgets/tabs/history_shifts_tab.dart'; +import '../styles/shifts_styles.dart'; class ShiftsPage extends StatefulWidget { final String? initialTab; - const ShiftsPage({super.key, this.initialTab}); + final DateTime? selectedDate; + const ShiftsPage({super.key, this.initialTab, this.selectedDate}); @override State createState() => _ShiftsPageState(); @@ -31,22 +20,32 @@ class ShiftsPage extends StatefulWidget { class _ShiftsPageState extends State { late String _activeTab; - String _searchQuery = ''; - // ignore: unused_field - String? _cancelledShiftDemo; // 'lastMinute' or 'advance' - String _jobType = 'all'; // all, one-day, multi-day, long-term - - // Calendar State - DateTime _selectedDate = DateTime.now(); - int _weekOffset = 0; - + DateTime? _selectedDate; + bool _prioritizeFind = false; final ShiftsBloc _bloc = Modular.get(); @override void initState() { super.initState(); _activeTab = widget.initialTab ?? 'myshifts'; - _bloc.add(LoadShiftsEvent()); + _selectedDate = widget.selectedDate; + print('ShiftsPage init: initialTab=$_activeTab'); + _prioritizeFind = widget.initialTab == 'find'; + if (_prioritizeFind) { + _bloc.add(LoadFindFirstEvent()); + } else { + _bloc.add(LoadShiftsEvent()); + } + if (_activeTab == 'history') { + print('ShiftsPage init: loading history tab'); + _bloc.add(LoadHistoryShiftsEvent()); + } + if (_activeTab == 'find') { + print('ShiftsPage init: entering find tab (not loaded yet)'); + if (!_prioritizeFind) { + _bloc.add(LoadAvailableShiftsEvent()); + } + } } @override @@ -55,37 +54,14 @@ class _ShiftsPageState extends State { if (widget.initialTab != null && widget.initialTab != _activeTab) { setState(() { _activeTab = widget.initialTab!; + _prioritizeFind = widget.initialTab == 'find'; + }); + } + if (widget.selectedDate != null && widget.selectedDate != _selectedDate) { + setState(() { + _selectedDate = widget.selectedDate; }); } - } - - List _getCalendarDays() { - final now = DateTime.now(); - int reactDayIndex = now.weekday == 7 ? 0 : now.weekday; - int daysSinceFriday = (reactDayIndex + 2) % 7; - final start = now - .subtract(Duration(days: daysSinceFriday)) - .add(Duration(days: _weekOffset * 7)); - final startDate = DateTime(start.year, start.month, start.day); - return List.generate(7, (index) => startDate.add(Duration(days: index))); - } - - bool _isSameDay(DateTime a, DateTime b) { - return a.year == b.year && a.month == b.month && a.day == b.day; - } - - void _confirmShift(String id) { - // TODO: Implement Bloc event - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Shift confirmed! (Placeholder)')), - ); - } - - void _declineShift(String id) { - // TODO: Implement Bloc event - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Shift declined. (Placeholder)')), - ); } @override @@ -94,42 +70,41 @@ class _ShiftsPageState extends State { value: _bloc, child: BlocBuilder( builder: (context, state) { - final List myShifts = (state is ShiftsLoaded) ? state.myShifts : []; - final List availableJobs = (state is ShiftsLoaded) ? state.availableShifts : []; - final List pendingAssignments = (state is ShiftsLoaded) ? state.pendingShifts : []; - final List historyShifts = []; // Not in state yet, placeholder + final bool baseLoaded = state is ShiftsLoaded; + final List myShifts = (state is ShiftsLoaded) + ? state.myShifts + : []; + final List availableJobs = (state is ShiftsLoaded) + ? state.availableShifts + : []; + final bool availableLoading = (state is ShiftsLoaded) + ? state.availableLoading + : false; + final bool availableLoaded = (state is ShiftsLoaded) + ? state.availableLoaded + : false; + final List pendingAssignments = (state is ShiftsLoaded) + ? state.pendingShifts + : []; + final List cancelledShifts = (state is ShiftsLoaded) + ? state.cancelledShifts + : []; + final List historyShifts = (state is ShiftsLoaded) + ? state.historyShifts + : []; + final bool historyLoading = (state is ShiftsLoaded) + ? state.historyLoading + : false; + final bool historyLoaded = (state is ShiftsLoaded) + ? state.historyLoaded + : false; + final bool myShiftsLoaded = (state is ShiftsLoaded) + ? state.myShiftsLoaded + : false; + final bool blockTabsForFind = _prioritizeFind && !availableLoaded; - // Filter logic from POC - final filteredJobs = availableJobs.where((s) { - final matchesSearch = - s.title.toLowerCase().contains(_searchQuery.toLowerCase()) || - s.location.toLowerCase().contains(_searchQuery.toLowerCase()) || - s.clientName.toLowerCase().contains(_searchQuery.toLowerCase()); - - if (!matchesSearch) return false; - - if (_jobType == 'all') return true; - if (_jobType == 'one-day') { - return !s.title.contains('Long Term') && !s.title.contains('Multi-Day'); - } - if (_jobType == 'multi-day') return s.title.contains('Multi-Day'); - if (_jobType == 'long-term') return s.title.contains('Long Term'); - return true; - }).toList(); - - final calendarDays = _getCalendarDays(); - final weekStartDate = calendarDays.first; - final weekEndDate = calendarDays.last; - - final visibleMyShifts = myShifts.where((s) { - // Primitive check if shift date string compare - // In real app use DateTime logic - final sDateStr = s.date; - final wStartStr = DateFormat('yyyy-MM-dd').format(weekStartDate); - final wEndStr = DateFormat('yyyy-MM-dd').format(weekEndDate); - return sDateStr.compareTo(wStartStr) >= 0 && - sDateStr.compareTo(wEndStr) <= 0; - }).toList(); + // Note: "filteredJobs" logic moved to FindShiftsTab + // Note: Calendar logic moved to MyShiftsTab return Scaffold( backgroundColor: AppColors.krowBackground, @@ -140,293 +115,72 @@ class _ShiftsPageState extends State { color: AppColors.krowBlue, padding: EdgeInsets.fromLTRB( 20, - MediaQuery.of(context).padding.top + 20, + MediaQuery.of(context).padding.top + 10, + 20, 20, - 24, ), child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 16, children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text( - "Shifts", - style: TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, - color: Colors.white, - ), - ), - Row( - children: [ - _buildDemoButton("Demo: Cancel <4hr", const Color(0xFFEF4444), () { - setState(() => _cancelledShiftDemo = 'lastMinute'); - _showCancelledModal('lastMinute'); - }), - const SizedBox(width: 8), - _buildDemoButton("Demo: Cancel >4hr", const Color(0xFFF59E0B), () { - setState(() => _cancelledShiftDemo = 'advance'); - _showCancelledModal('advance'); - }), - ], - ), - ], + const Text( + "Shifts", + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: Colors.white, + ), ), - const SizedBox(height: 16), + // Tabs Row( children: [ - _buildTab("myshifts", "My Shifts", LucideIcons.calendar, myShifts.length), + _buildTab( + "myshifts", + "My Shifts", + UiIcons.calendar, + myShifts.length, + showCount: myShiftsLoaded, + enabled: !blockTabsForFind, + ), const SizedBox(width: 8), - _buildTab("find", "Find Shifts", LucideIcons.search, filteredJobs.length), + _buildTab( + "find", + "Find Shifts", + UiIcons.search, + availableJobs + .length, // Passed unfiltered count as badge? Or logic inside? Pass availableJobs. + showCount: availableLoaded, + enabled: baseLoaded, + ), const SizedBox(width: 8), - _buildTab("history", "History", LucideIcons.clock, historyShifts.length), + _buildTab( + "history", + "History", + UiIcons.clock, + historyShifts.length, + showCount: historyLoaded, + enabled: !blockTabsForFind && baseLoaded, + ), ], ), ], ), ), - // Calendar Selector - if (_activeTab == 'myshifts') - Container( - color: Colors.white, - padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 20), - child: Column( - children: [ - Padding( - padding: const EdgeInsets.only(bottom: 16), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - InkWell( - onTap: () => setState(() => _weekOffset--), - borderRadius: BorderRadius.circular(20), - child: const Padding( - padding: EdgeInsets.all(8.0), - child: Icon(LucideIcons.chevronLeft, size: 20, color: AppColors.krowCharcoal), - ), - ), - Text( - DateFormat('MMMM yyyy').format(weekStartDate), - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - color: AppColors.krowCharcoal, - ), - ), - InkWell( - onTap: () => setState(() => _weekOffset++), - borderRadius: BorderRadius.circular(20), - child: const Padding( - padding: EdgeInsets.all(8.0), - child: Icon(LucideIcons.chevronRight, size: 20, color: AppColors.krowCharcoal), - ), - ), - ], - ), - ), - // Days Grid - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: calendarDays.map((date) { - final isSelected = _isSameDay(date, _selectedDate); - final dateStr = DateFormat('yyyy-MM-dd').format(date); - final hasShifts = myShifts.any((s) => s.date == dateStr); - - return GestureDetector( - onTap: () => setState(() => _selectedDate = date), - child: Container( - width: 44, - padding: const EdgeInsets.symmetric(vertical: 12), - decoration: BoxDecoration( - color: isSelected ? AppColors.krowBlue : Colors.white, - borderRadius: BorderRadius.circular(999), - border: Border.all( - color: isSelected ? AppColors.krowBlue : AppColors.krowBorder, - width: 1, - ), - ), - child: Column( - children: [ - Text( - date.day.toString().padLeft(2, '0'), - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - color: isSelected ? Colors.white : AppColors.krowCharcoal, - ), - ), - const SizedBox(height: 2), - Text( - DateFormat('E').format(date), - style: TextStyle( - fontSize: 10, - fontWeight: FontWeight.w500, - color: isSelected ? Colors.white.withOpacity(0.8) : AppColors.krowMuted, - ), - ), - if (hasShifts) - Container( - margin: const EdgeInsets.only(top: 4), - width: 6, - height: 6, - decoration: BoxDecoration( - color: isSelected ? Colors.white : AppColors.krowBlue, - shape: BoxShape.circle, - ), - ), - ], - ), - ), - ); - }).toList(), - ), - ], - ), - ), - - if (_activeTab == 'myshifts') - const Divider(height: 1, color: AppColors.krowBorder), - // Body Content Expanded( - child: SingleChildScrollView( - padding: const EdgeInsets.all(20), - child: Column( - children: [ - if (_activeTab == 'find') ...[ - // Search & Filter - Padding( - padding: const EdgeInsets.only(bottom: 12), - child: Container( - height: 48, - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(8), - border: Border.all(color: AppColors.krowBorder), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.05), - blurRadius: 4, - offset: const Offset(0, 2), - ), - ], - ), - child: TextField( - onChanged: (val) => setState(() => _searchQuery = val), // Local filter for now - decoration: const InputDecoration( - prefixIcon: Icon(LucideIcons.search, size: 20, color: AppColors.krowMuted), - border: InputBorder.none, - hintText: "Search jobs...", - hintStyle: TextStyle(color: AppColors.krowMuted, fontSize: 14), - contentPadding: EdgeInsets.symmetric(vertical: 12), - ), - ), - ), - ), - Container( - margin: const EdgeInsets.only(bottom: 16), - padding: const EdgeInsets.all(4), - decoration: BoxDecoration( - color: const Color(0xFFF1F3F5), - borderRadius: BorderRadius.circular(999), - ), - child: Row( - children: [ - _buildFilterTab('all', 'All Jobs'), - _buildFilterTab('one-day', 'One Day'), - _buildFilterTab('multi-day', 'Multi-Day'), - _buildFilterTab('long-term', 'Long Term'), - ], - ), - ), - ], - - if (_activeTab == 'myshifts') ...[ - if (pendingAssignments.isNotEmpty) ...[ - Align( - alignment: Alignment.centerLeft, - child: Padding( - padding: const EdgeInsets.only(bottom: 12), - child: Row( - children: [ - Container(width: 8, height: 8, decoration: const BoxDecoration(color: Color(0xFFF59E0B), shape: BoxShape.circle)), - const SizedBox(width: 8), - const Text("Awaiting Confirmation", style: TextStyle( - fontSize: 14, fontWeight: FontWeight.w600, color: Color(0xFFD97706) - )), - ], - ), - ), - ), - ...pendingAssignments.map((shift) => Padding( - padding: const EdgeInsets.only(bottom: 16), - child: ShiftAssignmentCard( - shift: shift, - onConfirm: () => _confirmShift(shift.id), - onDecline: () => _declineShift(shift.id), - ), - )), - ], - - // Cancelled Shifts Demo (Visual only as per POC) - Align( - alignment: Alignment.centerLeft, - child: Padding( - padding: const EdgeInsets.only(bottom: 12), - child: const Text("Cancelled Shifts", style: TextStyle( - fontSize: 14, fontWeight: FontWeight.w600, color: AppColors.krowMuted - )), - ), - ), - _buildCancelledCard( - title: "Annual Tech Conference", client: "TechCorp Inc.", pay: "\$200", rate: "\$25/hr · 8h", - date: "Today", time: "10:00 AM - 6:00 PM", address: "123 Convention Center Dr", isLastMinute: true, - onTap: () => setState(() => _cancelledShiftDemo = 'lastMinute') - ), - const SizedBox(height: 12), - _buildCancelledCard( - title: "Morning Catering Setup", client: "EventPro Services", pay: "\$120", rate: "\$20/hr · 6h", - date: "Tomorrow", time: "8:00 AM - 2:00 PM", address: "456 Grand Ballroom Ave", isLastMinute: false, - onTap: () => setState(() => _cancelledShiftDemo = 'advance') - ), - const SizedBox(height: 24), - - // Confirmed Shifts - if (visibleMyShifts.isNotEmpty) ...[ - Align( - alignment: Alignment.centerLeft, - child: Padding( - padding: const EdgeInsets.only(bottom: 12), - child: const Text("Confirmed Shifts", style: TextStyle( - fontSize: 14, fontWeight: FontWeight.w600, color: AppColors.krowMuted - )), - ), - ), - ...visibleMyShifts.map((shift) => Padding( - padding: const EdgeInsets.only(bottom: 12), - child: MyShiftCard(shift: shift), - )), - ], - ], - - if (_activeTab == 'find') ...[ - if (filteredJobs.isEmpty) - _buildEmptyState(LucideIcons.search, "No jobs available", "Check back later", null, null) - else - ...filteredJobs.map((shift) => MyShiftCard( - shift: shift, - onAccept: () {}, - onDecline: () {}, - )), - ], - - if (_activeTab == 'history') - _buildEmptyState(LucideIcons.clock, "No shift history", "Completed shifts appear here", null, null), - ], - ), - ), + child: state is ShiftsLoading + ? const Center(child: CircularProgressIndicator()) + : _buildTabContent( + myShifts, + pendingAssignments, + cancelledShifts, + availableJobs, + historyShifts, + availableLoading, + historyLoading, + ), ), ], ), @@ -436,181 +190,127 @@ class _ShiftsPageState extends State { ); } - Widget _buildFilterTab(String id, String label) { - final isSelected = _jobType == id; - return Expanded( - child: GestureDetector( - onTap: () => setState(() => _jobType = id), - child: Container( - padding: const EdgeInsets.symmetric(vertical: 8), - decoration: BoxDecoration( - color: isSelected ? AppColors.krowBlue : Colors.transparent, - borderRadius: BorderRadius.circular(999), - boxShadow: isSelected ? [BoxShadow(color: AppColors.krowBlue.withOpacity(0.2), blurRadius: 4, offset: const Offset(0, 2))] : null, - ), - child: Text(label, textAlign: TextAlign.center, style: TextStyle( - fontSize: 11, fontWeight: FontWeight.w600, color: isSelected ? Colors.white : AppColors.krowMuted - )), - ), - ), - ); + Widget _buildTabContent( + List myShifts, + List pendingAssignments, + List cancelledShifts, + List availableJobs, + List historyShifts, + bool availableLoading, + bool historyLoading, + ) { + switch (_activeTab) { + case 'myshifts': + return MyShiftsTab( + myShifts: myShifts, + pendingAssignments: pendingAssignments, + cancelledShifts: cancelledShifts, + initialDate: _selectedDate, + ); + case 'find': + if (availableLoading) { + return const Center(child: CircularProgressIndicator()); + } + return FindShiftsTab(availableJobs: availableJobs); + case 'history': + if (historyLoading) { + return const Center(child: CircularProgressIndicator()); + } + return HistoryShiftsTab(historyShifts: historyShifts); + default: + return const SizedBox.shrink(); + } } - Widget _buildTab(String id, String label, IconData icon, int count) { + Widget _buildTab( + String id, + String label, + IconData icon, + int count, { + bool showCount = true, + bool enabled = true, + }) { final isActive = _activeTab == id; return Expanded( child: GestureDetector( - onTap: () => setState(() => _activeTab = id), + onTap: !enabled + ? null + : () { + setState(() => _activeTab = id); + if (id == 'history') { + _bloc.add(LoadHistoryShiftsEvent()); + } + if (id == 'find') { + _bloc.add(LoadAvailableShiftsEvent()); + } + }, child: Container( padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 8), decoration: BoxDecoration( - color: isActive ? Colors.white : Colors.white.withAlpha((0.2 * 255).round()), - borderRadius: BorderRadius.circular(8), + color: isActive + ? Colors.white + : Colors.white.withAlpha((0.2 * 255).round()), + borderRadius: BorderRadius.circular(8), ), child: Row( mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [ - Icon(icon, size: 14, color: isActive ? AppColors.krowBlue : Colors.white), - const SizedBox(width: 6), - Flexible(child: Text(label, style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: isActive ? AppColors.krowBlue : Colors.white), overflow: TextOverflow.ellipsis)), - const SizedBox(width: 4), - Container( - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + Icon( + icon, + size: 14, + color: !enabled + ? Colors.white.withAlpha((0.5 * 255).round()) + : isActive + ? AppColors.krowBlue + : Colors.white, + ), + const SizedBox(width: 6), + Flexible( + child: Text( + label, + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w500, + color: !enabled + ? Colors.white.withAlpha((0.5 * 255).round()) + : isActive + ? AppColors.krowBlue + : Colors.white, + ), + overflow: TextOverflow.ellipsis, + ), + ), + if (showCount) ...[ + const SizedBox(width: 4), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 2, + ), constraints: const BoxConstraints(minWidth: 18), decoration: BoxDecoration( - color: isActive ? AppColors.krowBlue.withAlpha((0.1 * 255).round()) : Colors.white.withAlpha((0.2 * 255).round()), - borderRadius: BorderRadius.circular(999), + color: isActive + ? AppColors.krowBlue.withAlpha((0.1 * 255).round()) + : Colors.white.withAlpha((0.2 * 255).round()), + borderRadius: BorderRadius.circular(999), ), - child: Center(child: Text("$count", style: TextStyle(fontSize: 10, fontWeight: FontWeight.bold, color: isActive ? AppColors.krowBlue : Colors.white))), - ), + child: Center( + child: Text( + "$count", + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.bold, + color: isActive ? AppColors.krowBlue : Colors.white, + ), + ), + ), + ), + ], ], ), ), ), ); } - - Widget _buildDemoButton(String label, Color color, VoidCallback onTap) { - return GestureDetector( - onTap: onTap, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - decoration: BoxDecoration(color: color, borderRadius: BorderRadius.circular(4)), - child: Text(label, style: const TextStyle(fontSize: 10, fontWeight: FontWeight.bold, color: Colors.white)), - ), - ); - } - - Widget _buildEmptyState(IconData icon, String title, String subtitle, String? actionLabel, VoidCallback? onAction) { - return Center(child: Padding(padding: const EdgeInsets.symmetric(vertical: 64), child: Column(children: [ - Container(width: 64, height: 64, decoration: BoxDecoration(color: const Color(0xFFF1F3F5), borderRadius: BorderRadius.circular(12)), child: Icon(icon, size: 32, color: AppColors.krowMuted)), - const SizedBox(height: 16), - Text(title, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500, color: AppColors.krowCharcoal)), - const SizedBox(height: 4), - Text(subtitle, style: const TextStyle(fontSize: 14, color: AppColors.krowMuted)), - if (actionLabel != null && onAction != null) ...[ - const SizedBox(height: 16), - ElevatedButton(onPressed: onAction, style: ElevatedButton.styleFrom(backgroundColor: AppColors.krowBlue, foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))), child: Text(actionLabel)), - ] - ]))); - } - - Widget _buildCancelledCard({required String title, required String client, required String pay, required String rate, required String date, required String time, required String address, required bool isLastMinute, required VoidCallback onTap}) { - return GestureDetector( - onTap: onTap, - child: Container(padding: const EdgeInsets.all(16), decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(16), border: Border.all(color: AppColors.krowBorder)), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row(children: [Container(width: 6, height: 6, decoration: const BoxDecoration(color: Color(0xFFEF4444), shape: BoxShape.circle)), const SizedBox(width: 6), const Text("CANCELLED", style: TextStyle(fontSize: 10, fontWeight: FontWeight.bold, color: Color(0xFFEF4444))), if (isLastMinute) ...[const SizedBox(width: 4), const Text("• 4hr compensation", style: TextStyle(fontSize: 10, fontWeight: FontWeight.w500, color: Color(0xFF10B981)))]]), - const SizedBox(height: 12), - Row(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Container(width: 44, height: 44, decoration: BoxDecoration(gradient: LinearGradient(begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [AppColors.krowBlue.withAlpha((0.15 * 255).round()), AppColors.krowBlue.withAlpha((0.08 * 255).round())]), borderRadius: BorderRadius.circular(12), border: Border.all(color: AppColors.krowBlue.withAlpha((0.15 * 255).round()))), child: const Center(child: Icon(LucideIcons.briefcase, color: AppColors.krowBlue, size: 20))), - const SizedBox(width: 12), - Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [Text(title, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: AppColors.krowCharcoal)), Text(client, style: const TextStyle(fontSize: 12, color: AppColors.krowMuted))])), Column(crossAxisAlignment: CrossAxisAlignment.end, children: [Text(pay, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: AppColors.krowCharcoal)), Text(rate, style: const TextStyle(fontSize: 10, color: AppColors.krowMuted))])]), - const SizedBox(height: 8), - Row(children: [const Icon(LucideIcons.calendar, size: 12, color: AppColors.krowMuted), const SizedBox(width: 4), Text(date, style: const TextStyle(fontSize: 12, color: AppColors.krowMuted)), const SizedBox(width: 12), const Icon(LucideIcons.clock, size: 12, color: AppColors.krowMuted), const SizedBox(width: 4), Text(time, style: const TextStyle(fontSize: 12, color: AppColors.krowMuted))]), - const SizedBox(height: 4), - Row(children: [const Icon(LucideIcons.mapPin, size: 12, color: AppColors.krowMuted), const SizedBox(width: 4), Expanded(child: Text(address, style: const TextStyle(fontSize: 12, color: AppColors.krowMuted), overflow: TextOverflow.ellipsis))]), - ])), - ]), - ])), - ); - } - - void _showCancelledModal(String type) { - final isLastMinute = type == 'lastMinute'; - showDialog( - context: context, - builder: (context) => AlertDialog( - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), - title: Row( - children: [ - const Icon(LucideIcons.xCircle, color: Color(0xFFEF4444)), - const SizedBox(width: 8), - const Text("Shift Cancelled"), - ], - ), - content: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - "We're sorry, but the following shift has been cancelled by the client:", - style: TextStyle(fontSize: 14), - ), - const SizedBox(height: 12), - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Colors.grey.shade50, - borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.grey.shade200), - ), - child: const Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text("Annual Tech Conference", style: TextStyle(fontWeight: FontWeight.bold)), - Text("Today, 10:00 AM - 6:00 PM"), - ], - ), - ), - const SizedBox(height: 16), - if (isLastMinute) - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: const Color(0xFFECFDF5), - borderRadius: BorderRadius.circular(8), - border: Border.all(color: const Color(0xFF10B981)), - ), - child: const Row( - children: [ - Icon(LucideIcons.checkCircle, color: Color(0xFF10B981), size: 16), - SizedBox(width: 8), - Expanded( - child: Text( - "You are eligible for 4hr cancellation compensation.", - style: TextStyle( - fontSize: 12, color: Color(0xFF065F46), fontWeight: FontWeight.w500), - ), - ), - ], - ), - ) - else - const Text( - "Reduced schedule at the venue. No compensation is due as this was cancelled more than 4 hours in advance.", - style: TextStyle(fontSize: 12, color: AppColors.krowMuted), - ), - ], - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text("Close"), - ), - ], - ), - ); - } } diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/styles/shifts_styles.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/styles/shifts_styles.dart new file mode 100644 index 00000000..7f98111b --- /dev/null +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/styles/shifts_styles.dart @@ -0,0 +1,13 @@ +import 'package:flutter/material.dart'; +import 'package:design_system/design_system.dart'; + +class AppColors { + static const Color krowBlue = UiColors.primary; + static const Color krowYellow = Color(0xFFFFED4A); + static const Color krowCharcoal = UiColors.textPrimary; + static const Color krowMuted = UiColors.textSecondary; + static const Color krowBorder = UiColors.border; + static const Color krowBackground = UiColors.background; + static const Color white = Colors.white; + static const Color black = Colors.black; +} diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/my_shift_card.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/my_shift_card.dart index e9e99b35..e1bba099 100644 --- a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/my_shift_card.dart +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/my_shift_card.dart @@ -1,25 +1,17 @@ import 'package:flutter/material.dart'; +import 'package:flutter_modular/flutter_modular.dart'; import 'package:intl/intl.dart'; import 'package:krow_domain/krow_domain.dart'; import 'package:design_system/design_system.dart'; import 'package:core_localization/core_localization.dart'; +import 'package:staff_shifts/src/presentation/navigation/shifts_navigator.dart'; class MyShiftCard extends StatefulWidget { final Shift shift; - final bool historyMode; - final VoidCallback? onAccept; - final VoidCallback? onDecline; - final VoidCallback? onRequestSwap; - final int index; const MyShiftCard({ super.key, required this.shift, - this.historyMode = false, - this.onAccept, - this.onDecline, - this.onRequestSwap, - this.index = 0, }); @override @@ -27,8 +19,6 @@ class MyShiftCard extends StatefulWidget { } class _MyShiftCardState extends State { - bool _isExpanded = false; - String _formatTime(String time) { if (time.isEmpty) return ''; try { @@ -75,16 +65,19 @@ class _MyShiftCardState extends State { } String _getShiftType() { - // Check title for type indicators (for mock data) - if (widget.shift.title.contains('Long Term')) return t.staff_shifts.filter.long_term; - if (widget.shift.title.contains('Multi-Day')) return t.staff_shifts.filter.multi_day; + if (widget.shift.durationDays != null && widget.shift.durationDays! > 30) { + return t.staff_shifts.filter.long_term; + } + if (widget.shift.durationDays != null && widget.shift.durationDays! > 1) { + return t.staff_shifts.filter.multi_day; + } return t.staff_shifts.filter.one_day; } @override Widget build(BuildContext context) { - // ignore: unused_local_variable final duration = _calculateDuration(); + final estimatedTotal = (widget.shift.hourlyRate) * duration; // Status Logic String? status = widget.shift.status; @@ -97,6 +90,10 @@ class _MyShiftCardState extends State { statusText = t.staff_shifts.status.confirmed; statusColor = UiColors.textLink; statusBg = UiColors.primary; + } else if (status == 'checked_in') { + statusText = 'Checked in'; + statusColor = UiColors.textSuccess; + statusBg = UiColors.iconSuccess; } else if (status == 'pending' || status == 'open') { statusText = t.staff_shifts.status.act_now; statusColor = UiColors.destructive; @@ -117,9 +114,10 @@ class _MyShiftCardState extends State { } return GestureDetector( - onTap: () => setState(() => _isExpanded = !_isExpanded), - child: AnimatedContainer( - duration: const Duration(milliseconds: 300), + onTap: () { + Modular.to.pushShiftDetails(widget.shift); + }, + child: Container( margin: const EdgeInsets.only(bottom: 12), decoration: BoxDecoration( color: Colors.white, @@ -168,7 +166,7 @@ class _MyShiftCardState extends State { ), Text( statusText, - style: UiTypography.display3r.copyWith( + style: UiTypography.footnote2b.copyWith( color: statusColor, letterSpacing: 0.5, ), @@ -187,9 +185,8 @@ class _MyShiftCardState extends State { ), child: Text( _getShiftType(), - style: UiTypography.display3r.copyWith( + style: UiTypography.footnote2m.copyWith( color: UiColors.primary, - fontWeight: FontWeight.w500, ), ), ), @@ -197,59 +194,167 @@ class _MyShiftCardState extends State { ], ), ), - - // Main Content + Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Date/Time Column + // Logo + Container( + width: 44, + height: 44, + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + UiColors.primary.withOpacity(0.09), + UiColors.primary.withOpacity(0.03), + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: UiColors.primary.withOpacity(0.09), + ), + ), + child: widget.shift.logoUrl != null + ? ClipRRect( + borderRadius: BorderRadius.circular(12), + child: Image.network( + widget.shift.logoUrl!, + fit: BoxFit.contain, + ), + ) + : const Center( + child: Icon( + UiIcons.briefcase, + color: UiColors.primary, + size: 20, + ), + ), + ), + const SizedBox(width: 12), + + // Details Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - _formatDate(widget.shift.date), - style: UiTypography.display2m.copyWith( - color: UiColors.textPrimary, + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + widget.shift.title, + style: UiTypography.body2m.copyWith( + color: UiColors.textPrimary, + ), + overflow: TextOverflow.ellipsis, + ), + Text( + widget.shift.clientName, + style: UiTypography.body3r.copyWith( + color: UiColors.textSecondary, + ), + overflow: TextOverflow.ellipsis, + ), + ], ), ), - if (widget.shift.durationDays != null) ...[ - const SizedBox(width: 8), - Container( - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), - decoration: BoxDecoration( - color: UiColors.primary.withOpacity(0.1), - borderRadius: BorderRadius.circular(4), - ), - child: Text( - t.staff_shifts.details.days(days: widget.shift.durationDays!), - style: UiTypography.display3r.copyWith( - color: UiColors.primary, - fontWeight: FontWeight.w600, + const SizedBox(width: 8), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + "\$${estimatedTotal.toStringAsFixed(0)}", + style: UiTypography.title1m.copyWith( + color: UiColors.textPrimary, ), ), + Text( + "\$${widget.shift.hourlyRate.toInt()}/hr · ${duration.toInt()}h", + style: UiTypography.footnote2r.copyWith( + color: UiColors.textSecondary, + ), + ), + ], + ), + ], + ), + const SizedBox(height: 8), + + // Date & Time - Multi-Day or Single Day + if (widget.shift.durationDays != null && + widget.shift.durationDays! > 1) ...[ + // Multi-Day Schedule Display + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Icon( + UiIcons.clock, + size: 12, + color: UiColors.primary, + ), + const SizedBox(width: 4), + Text( + t.staff_shifts.details.days( + days: widget.shift.durationDays!, + ), + style: UiTypography.footnote2m.copyWith( + color: UiColors.primary, + ), + ), + ], + ), + const SizedBox(height: 4), + Text( + "Showing first schedule...", + style: UiTypography.footnote2r.copyWith( + color: UiColors.primary, + ), ), ], - ], - ), + ), + ] else ...[ + // Single Day Display + Row( + children: [ + const Icon( + UiIcons.calendar, + size: 12, + color: UiColors.iconSecondary, + ), + const SizedBox(width: 4), + Text( + _formatDate(widget.shift.date), + style: UiTypography.footnote1r.copyWith( + color: UiColors.textSecondary, + ), + ), + const SizedBox(width: 12), + const Icon( + UiIcons.clock, + size: 12, + color: UiColors.iconSecondary, + ), + const SizedBox(width: 4), + Text( + "${_formatTime(widget.shift.startTime)} - ${_formatTime(widget.shift.endTime)}", + style: UiTypography.footnote1r.copyWith( + color: UiColors.textSecondary, + ), + ), + ], + ), + ], const SizedBox(height: 4), - Text( - '${_formatTime(widget.shift.startTime)} - ${_formatTime(widget.shift.endTime)}', - style: UiTypography.body2r.copyWith( - color: UiColors.textSecondary, - ), - ), - const SizedBox(height: 12), - Text( - widget.shift.title, - style: UiTypography.body2m.copyWith( - color: UiColors.textPrimary, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), + + // Location Row( children: [ const Icon( @@ -258,10 +363,15 @@ class _MyShiftCardState extends State { color: UiColors.iconSecondary, ), const SizedBox(width: 4), - Text( - widget.shift.clientName, - style: UiTypography.display3r.copyWith( - color: UiColors.textSecondary, + Expanded( + child: Text( + widget.shift.locationAddress.isNotEmpty + ? widget.shift.locationAddress + : widget.shift.location, + style: UiTypography.footnote1r.copyWith( + color: UiColors.textSecondary, + ), + overflow: TextOverflow.ellipsis, ), ), ], @@ -269,141 +379,11 @@ class _MyShiftCardState extends State { ], ), ), - - // Logo Box - Container( - width: 48, - height: 48, - decoration: BoxDecoration( - color: UiColors.background, - borderRadius: BorderRadius.circular(8), - border: Border.all(color: UiColors.border), - ), - child: widget.shift.logoUrl != null - ? ClipRRect( - borderRadius: BorderRadius.circular(8), - child: Image.network( - widget.shift.logoUrl!, - fit: BoxFit.cover, - ), - ) - : Center( - child: Text( - widget.shift.clientName.isNotEmpty - ? widget.shift.clientName[0] - : 'K', - style: UiTypography.title1m.textLink, - ), - ), - ), ], ), ], ), ), - - // Expanded Actions - AnimatedCrossFade( - firstChild: const SizedBox(height: 0), - secondChild: Container( - decoration: const BoxDecoration( - border: Border( - top: BorderSide(color: UiColors.border), - ), - ), - child: Column( - children: [ - // Warning for Pending - if (status == 'pending' || status == 'open') - Container( - width: double.infinity, - padding: const EdgeInsets.symmetric( - vertical: 8, - horizontal: 16, - ), - color: UiColors.accent.withOpacity(0.1), - child: Row( - children: [ - const Icon( - UiIcons.warning, - size: 14, - color: UiColors.textWarning, - ), - const SizedBox(width: 8), - Text( - t.staff_shifts.status.pending_warning, - style: UiTypography.display3r.copyWith( - color: UiColors.textWarning, - fontWeight: FontWeight.w500, - ), - ), - ], - ), - ), - - Padding( - padding: const EdgeInsets.all(12), - child: Row( - children: [ - if (status == 'pending' || status == 'open') ...[ - Expanded( - child: OutlinedButton( - onPressed: widget.onDecline, - style: OutlinedButton.styleFrom( - foregroundColor: UiColors.destructive, - side: const BorderSide(color: UiColors.border), - padding: const EdgeInsets.symmetric(vertical: 12), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - child: Text(t.staff_shifts.action.decline), - ), - ), - const SizedBox(width: 12), - Expanded( - child: ElevatedButton( - onPressed: widget.onAccept, - style: ElevatedButton.styleFrom( - backgroundColor: UiColors.primary, - foregroundColor: Colors.white, - elevation: 0, - padding: const EdgeInsets.symmetric(vertical: 12), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - child: Text(t.staff_shifts.action.confirm), - ), - ), - ] else if (status == 'confirmed') ...[ - Expanded( - child: OutlinedButton.icon( - onPressed: widget.onRequestSwap, - icon: const Icon(UiIcons.swap, size: 16), - label: Text(t.staff_shifts.action.request_swap), - style: OutlinedButton.styleFrom( - foregroundColor: UiColors.textPrimary, - side: const BorderSide(color: UiColors.border), - padding: const EdgeInsets.symmetric(vertical: 12), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - ), - ), - ], - ], - ), - ), - ], - ), - ), - crossFadeState: _isExpanded - ? CrossFadeState.showSecond - : CrossFadeState.showFirst, - duration: const Duration(milliseconds: 200), - ), ], ), ), diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shared/empty_state_view.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shared/empty_state_view.dart new file mode 100644 index 00000000..32bfdcd4 --- /dev/null +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shared/empty_state_view.dart @@ -0,0 +1,69 @@ +import 'package:flutter/material.dart'; +import '../../styles/shifts_styles.dart'; + +class EmptyStateView extends StatelessWidget { + final IconData icon; + final String title; + final String subtitle; + final String? actionLabel; + final VoidCallback? onAction; + + const EmptyStateView({ + super.key, + required this.icon, + required this.title, + required this.subtitle, + this.actionLabel, + this.onAction, + }); + + @override + Widget build(BuildContext context) { + return Center( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 64), + child: Column( + children: [ + Container( + width: 64, + height: 64, + decoration: BoxDecoration( + color: const Color(0xFFF1F3F5), + borderRadius: BorderRadius.circular(12), + ), + child: Icon(icon, size: 32, color: AppColors.krowMuted), + ), + const SizedBox(height: 16), + Text( + title, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: AppColors.krowCharcoal, + ), + ), + const SizedBox(height: 4), + Text( + subtitle, + style: const TextStyle(fontSize: 14, color: AppColors.krowMuted), + ), + if (actionLabel != null && onAction != null) ...[ + const SizedBox(height: 16), + ElevatedButton( + onPressed: onAction, + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.krowBlue, + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: Text(actionLabel!), + ), + ], + ], + ), + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_assignment_card.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_assignment_card.dart index d3c813ff..d46eb14a 100644 --- a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_assignment_card.dart +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_assignment_card.dart @@ -67,7 +67,7 @@ class ShiftAssignmentCard extends StatelessWidget { return Container( decoration: BoxDecoration( color: Colors.white, - borderRadius: BorderRadius.circular(12), + borderRadius: UiConstants.radiusLg, border: Border.all(color: UiColors.border), boxShadow: [ BoxShadow( @@ -81,64 +81,157 @@ class ShiftAssignmentCard extends StatelessWidget { children: [ // Header Padding( - padding: const EdgeInsets.fromLTRB(16, 16, 16, 12), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + padding: const EdgeInsets.all(16), + child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + // Card content starts directly as per prototype + Row( + crossAxisAlignment: CrossAxisAlignment.start, children: [ + // Logo Container( - width: 36, - height: 36, + width: 44, + height: 44, decoration: BoxDecoration( - color: UiColors.secondary, - borderRadius: BorderRadius.circular(8), - ), - child: Center( - child: Text( - shift.clientName.isNotEmpty - ? shift.clientName[0] - : 'K', - style: UiTypography.body2b.copyWith( - color: UiColors.textSecondary, - ), + gradient: LinearGradient( + colors: [ + UiColors.primary.withOpacity(0.09), + UiColors.primary.withOpacity(0.03), + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: UiColors.primary.withOpacity(0.09), ), ), + child: shift.logoUrl != null + ? ClipRRect( + borderRadius: BorderRadius.circular(12), + child: Image.network( + shift.logoUrl!, + fit: BoxFit.contain, + ), + ) + : const Center( + child: Icon( + UiIcons.briefcase, + color: UiColors.primary, + size: 20, + ), + ), ), const SizedBox(width: 12), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - shift.title, - style: UiTypography.body2b.copyWith( - color: UiColors.textPrimary, + + // Details + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + shift.title, + style: UiTypography.body2m.copyWith( + color: UiColors.textPrimary, + ), + overflow: TextOverflow.ellipsis, + ), + Text( + shift.clientName, + style: UiTypography.body3r.copyWith( + color: UiColors.textSecondary, + ), + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + const SizedBox(width: 8), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + "\$${totalPay.toStringAsFixed(0)}", + style: UiTypography.title1m.copyWith( + color: UiColors.textPrimary, + ), + ), + Text( + "\$${shift.hourlyRate.toInt()}/hr · ${hours.toInt()}h", + style: UiTypography.footnote2r.copyWith( + color: UiColors.textSecondary, + ), + ), + ], + ), + ], ), - ), - Text( - shift.clientName, - style: UiTypography.display3r.copyWith( - color: UiColors.textSecondary, + const SizedBox(height: 12), + + // Date & Time + Row( + children: [ + const Icon( + UiIcons.calendar, + size: 12, + color: UiColors.iconSecondary, + ), + const SizedBox(width: 4), + Text( + _formatDate(shift.date), + style: UiTypography.footnote1r.copyWith( + color: UiColors.textSecondary, + ), + ), + const SizedBox(width: 12), + const Icon( + UiIcons.clock, + size: 12, + color: UiColors.iconSecondary, + ), + const SizedBox(width: 4), + Text( + "${_formatTime(shift.startTime)} - ${_formatTime(shift.endTime)}", + style: UiTypography.footnote1r.copyWith( + color: UiColors.textSecondary, + ), + ), + ], ), - ), - ], - ), - ], - ), - Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Text( - "\$${totalPay.toStringAsFixed(0)}", - style: UiTypography.display2m.copyWith( - color: UiColors.textPrimary, - ), - ), - Text( - "\$${shift.hourlyRate}/hr · ${hours}h", - style: UiTypography.display3r.copyWith( - color: UiColors.textSecondary, + const SizedBox(height: 4), + + // Location + Row( + children: [ + const Icon( + UiIcons.mapPin, + size: 12, + color: UiColors.iconSecondary, + ), + const SizedBox(width: 4), + Expanded( + child: Text( + shift.locationAddress.isNotEmpty + ? shift.locationAddress + : shift.location, + style: UiTypography.footnote1r.copyWith( + color: UiColors.textSecondary, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ], ), ), ], @@ -147,94 +240,43 @@ class ShiftAssignmentCard extends StatelessWidget { ), ), - // Details Padding( - padding: const EdgeInsets.fromLTRB(16, 0, 16, 12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - const Icon( - UiIcons.calendar, - size: 14, - color: UiColors.iconSecondary, - ), - const SizedBox(width: 6), - Text( - _formatDate(shift.date), - style: UiTypography.display3r.copyWith( - color: UiColors.textSecondary, - ), - ), - const SizedBox(width: 16), - const Icon( - UiIcons.clock, - size: 14, - color: UiColors.iconSecondary, - ), - const SizedBox(width: 6), - Text( - "${_formatTime(shift.startTime)} - ${_formatTime(shift.endTime)}", - style: UiTypography.display3r.copyWith( - color: UiColors.textSecondary, - ), - ), - ], - ), - const SizedBox(height: 8), - Row( - children: [ - const Icon( - UiIcons.mapPin, - size: 14, - color: UiColors.iconSecondary, - ), - const SizedBox(width: 6), - Expanded( - child: Text( - shift.location, - style: UiTypography.display3r.copyWith( - color: UiColors.textSecondary, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - ], - ), - ), - - if (isConfirming) ...[ - const Divider(height: 1), - Row( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), + child: Row( children: [ Expanded( - child: TextButton( + child: OutlinedButton( onPressed: onDecline, - style: TextButton.styleFrom( - foregroundColor: UiColors.destructive, - padding: const EdgeInsets.symmetric(vertical: 16), + style: OutlinedButton.styleFrom( + foregroundColor: UiColors.iconSecondary, + side: const BorderSide(color: UiColors.border), + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), ), child: Text(t.staff_shifts.action.decline), ), ), - Container(width: 1, height: 48, color: UiColors.border), + const SizedBox(width: 12), Expanded( - child: TextButton( + child: ElevatedButton( onPressed: onConfirm, - style: TextButton.styleFrom( - foregroundColor: UiColors.primary, - padding: const EdgeInsets.symmetric(vertical: 16), + style: ElevatedButton.styleFrom( + backgroundColor: UiColors.primary, + foregroundColor: Colors.white, + elevation: 0, + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), ), child: Text(t.staff_shifts.action.confirm), ), ), ], ), - ], + ), ], ), ); diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/tabs/find_shifts_tab.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/tabs/find_shifts_tab.dart new file mode 100644 index 00000000..4e914b84 --- /dev/null +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/tabs/find_shifts_tab.dart @@ -0,0 +1,205 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:krow_domain/krow_domain.dart'; + +import '../../styles/shifts_styles.dart'; +import '../my_shift_card.dart'; +import '../shared/empty_state_view.dart'; + +class FindShiftsTab extends StatefulWidget { + final List availableJobs; + + const FindShiftsTab({ + super.key, + required this.availableJobs, + }); + + @override + State createState() => _FindShiftsTabState(); +} + +class _FindShiftsTabState extends State { + String _searchQuery = ''; + String _jobType = 'all'; + + @override + void initState() { + super.initState(); + print('FindShiftsTab init: tab entered, data pending'); + } + + Widget _buildFilterTab(String id, String label) { + final isSelected = _jobType == id; + return GestureDetector( + onTap: () => setState(() => _jobType = id), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + decoration: BoxDecoration( + color: isSelected ? AppColors.krowBlue : Colors.white, + borderRadius: BorderRadius.circular(999), + border: Border.all( + color: isSelected ? AppColors.krowBlue : const Color(0xFFE2E8F0), + ), + ), + child: Text( + label, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: isSelected ? Colors.white : const Color(0xFF64748B), + ), + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + // Filter logic + final filteredJobs = widget.availableJobs.where((s) { + final matchesSearch = + s.title.toLowerCase().contains(_searchQuery.toLowerCase()) || + s.location.toLowerCase().contains(_searchQuery.toLowerCase()) || + s.clientName.toLowerCase().contains(_searchQuery.toLowerCase()); + + if (!matchesSearch) return false; + + if (_jobType == 'all') return true; + if (_jobType == 'one-day') { + return s.durationDays == null || s.durationDays! <= 1; + } + if (_jobType == 'multi-day') + return s.durationDays != null && s.durationDays! > 1; + return true; + }).toList(); + + return Column( + children: [ + // Search and Filters + Container( + color: Colors.white, + padding: const EdgeInsets.fromLTRB(20, 16, 20, 16), + child: Column( + children: [ + // Search Bar + Row( + children: [ + Expanded( + child: Container( + height: 48, + padding: const EdgeInsets.symmetric(horizontal: 12), + decoration: BoxDecoration( + color: const Color(0xFFF8FAFC), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: const Color(0xFFE2E8F0), + ), + ), + child: Row( + children: [ + const Icon( + UiIcons.search, + size: 20, + color: Color(0xFF94A3B8), + ), + const SizedBox(width: 10), + Expanded( + child: TextField( + onChanged: (v) => + setState(() => _searchQuery = v), + decoration: const InputDecoration( + border: InputBorder.none, + hintText: "Search jobs, location...", + hintStyle: TextStyle( + color: Color(0xFF94A3B8), + fontSize: 14, + ), + ), + ), + ), + ], + ), + ), + ), + const SizedBox(width: 8), + Container( + height: 48, + width: 48, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: const Color(0xFFE2E8F0), + ), + ), + child: const Icon( + UiIcons.filter, + size: 18, + color: Color(0xFF64748B), + ), + ), + ], + ), + const SizedBox(height: 16), + // Filter Tabs + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + _buildFilterTab('all', 'All Jobs'), + const SizedBox(width: 8), + _buildFilterTab('one-day', 'One Day'), + const SizedBox(width: 8), + _buildFilterTab('multi-day', 'Multi-Day'), + const SizedBox(width: 8), + _buildFilterTab('long-term', 'Long Term'), + ], + ), + ), + ], + ), + ), + + Expanded( + child: filteredJobs.isEmpty + ? EmptyStateView( + icon: UiIcons.search, + title: "No jobs available", + subtitle: "Check back later", + ) + : SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Column( + children: [ + const SizedBox(height: 20), + ...filteredJobs.map( + (shift) => Padding( + padding: const EdgeInsets.only(bottom: 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Debug shiftId: ${shift.id}', + style: const TextStyle( + fontSize: 10, + color: Color(0xFF94A3B8), + ), + ), + const SizedBox(height: 4), + MyShiftCard( + shift: shift, + ), + ], + ), + ), + ), + const SizedBox(height: UiConstants.space32), + ], + ), + ), + ), + ], + ); + } +} diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/tabs/history_shifts_tab.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/tabs/history_shifts_tab.dart new file mode 100644 index 00000000..951d8fb8 --- /dev/null +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/tabs/history_shifts_tab.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:design_system/design_system.dart'; +import 'package:krow_domain/krow_domain.dart'; +import 'package:flutter_modular/flutter_modular.dart'; +import '../../navigation/shifts_navigator.dart'; +import '../my_shift_card.dart'; +import '../shared/empty_state_view.dart'; + +class HistoryShiftsTab extends StatelessWidget { + final List historyShifts; + + const HistoryShiftsTab({ + super.key, + required this.historyShifts, + }); + + @override + Widget build(BuildContext context) { + if (historyShifts.isEmpty) { + return EmptyStateView( + icon: UiIcons.clock, + title: "No shift history", + subtitle: "Completed shifts appear here", + ); + } + + return SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Column( + children: [ + const SizedBox(height: 20), + ...historyShifts.map( + (shift) => Padding( + padding: const EdgeInsets.only(bottom: 12), + child: GestureDetector( + onTap: () => Modular.to.pushShiftDetails(shift), + child: MyShiftCard( + shift: shift, + ), + ), + ), + ), + const SizedBox(height: UiConstants.space32), + ], + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/tabs/my_shifts_tab.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/tabs/my_shifts_tab.dart new file mode 100644 index 00000000..d2aecd1f --- /dev/null +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/tabs/my_shifts_tab.dart @@ -0,0 +1,633 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:intl/intl.dart'; +import 'package:lucide_icons/lucide_icons.dart'; +import 'package:design_system/design_system.dart'; +import 'package:krow_domain/krow_domain.dart'; +import '../../blocs/shifts/shifts_bloc.dart'; +import '../my_shift_card.dart'; +import '../shift_assignment_card.dart'; +import '../shared/empty_state_view.dart'; +import '../../styles/shifts_styles.dart'; + +class MyShiftsTab extends StatefulWidget { + final List myShifts; + final List pendingAssignments; + final List cancelledShifts; + final DateTime? initialDate; + + const MyShiftsTab({ + super.key, + required this.myShifts, + required this.pendingAssignments, + required this.cancelledShifts, + this.initialDate, + }); + + @override + State createState() => _MyShiftsTabState(); +} + +class _MyShiftsTabState extends State { + DateTime _selectedDate = DateTime.now(); + int _weekOffset = 0; + + @override + void initState() { + super.initState(); + if (widget.initialDate != null) { + _applyInitialDate(widget.initialDate!); + } + WidgetsBinding.instance.addPostFrameCallback((_) { + _loadShiftsForCurrentWeek(); + }); + } + + @override + void didUpdateWidget(MyShiftsTab oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.initialDate != null && + widget.initialDate != oldWidget.initialDate) { + _applyInitialDate(widget.initialDate!); + } + } + + void _applyInitialDate(DateTime date) { + _selectedDate = date; + + final now = DateTime.now(); + int reactDayIndex = now.weekday == 7 ? 0 : now.weekday; + int daysSinceFriday = (reactDayIndex + 2) % 7; + + // Base Friday + final baseStart = DateTime( + now.year, + now.month, + now.day, + ).subtract(Duration(days: daysSinceFriday)); + + final target = DateTime(date.year, date.month, date.day); + final diff = target.difference(baseStart).inDays; + + setState(() { + _weekOffset = (diff / 7).floor(); + }); + WidgetsBinding.instance.addPostFrameCallback((_) { + _loadShiftsForCurrentWeek(); + }); + } + + List _getCalendarDays() { + final now = DateTime.now(); + int reactDayIndex = now.weekday == 7 ? 0 : now.weekday; + int daysSinceFriday = (reactDayIndex + 2) % 7; + final start = now + .subtract(Duration(days: daysSinceFriday)) + .add(Duration(days: _weekOffset * 7)); + final startDate = DateTime(start.year, start.month, start.day); + return List.generate(7, (index) => startDate.add(Duration(days: index))); + } + + void _loadShiftsForCurrentWeek() { + final List calendarDays = _getCalendarDays(); + context.read().add( + LoadShiftsForRangeEvent( + start: calendarDays.first, + end: calendarDays.last, + ), + ); + } + + bool _isSameDay(DateTime a, DateTime b) { + return a.year == b.year && a.month == b.month && a.day == b.day; + } + + void _confirmShift(String id) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Accept Shift'), + content: const Text('Are you sure you want to accept this shift?'), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () { + Navigator.of(context).pop(); + context.read().add(AcceptShiftEvent(id)); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Shift confirmed!'), + backgroundColor: Color(0xFF10B981), + ), + ); + }, + style: TextButton.styleFrom( + foregroundColor: const Color(0xFF10B981), + ), + child: const Text('Accept'), + ), + ], + ), + ); + } + + void _declineShift(String id) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Decline Shift'), + content: const Text( + 'Are you sure you want to decline this shift? This action cannot be undone.', + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () { + Navigator.of(context).pop(); + context.read().add(DeclineShiftEvent(id)); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Shift declined.'), + backgroundColor: Color(0xFFEF4444), + ), + ); + }, + style: TextButton.styleFrom( + foregroundColor: const Color(0xFFEF4444), + ), + child: const Text('Decline'), + ), + ], + ), + ); + } + + String _formatDateStr(String dateStr) { + try { + final date = DateTime.parse(dateStr); + final now = DateTime.now(); + if (_isSameDay(date, now)) return "Today"; + final tomorrow = now.add(const Duration(days: 1)); + if (_isSameDay(date, tomorrow)) return "Tomorrow"; + return DateFormat('EEE, MMM d').format(date); + } catch (_) { + return dateStr; + } + } + + @override + Widget build(BuildContext context) { + final calendarDays = _getCalendarDays(); + final weekStartDate = calendarDays.first; + final weekEndDate = calendarDays.last; + + final visibleMyShifts = widget.myShifts.where((s) { + try { + final date = DateTime.parse(s.date); + return _isSameDay(date, _selectedDate); + } catch (_) { + return false; + } + }).toList(); + + final visibleCancelledShifts = widget.cancelledShifts.where((s) { + try { + final date = DateTime.parse(s.date); + return date.isAfter( + weekStartDate.subtract(const Duration(seconds: 1)), + ) && + date.isBefore(weekEndDate.add(const Duration(days: 1))); + } catch (_) { + return false; + } + }).toList(); + + return Column( + children: [ + // Calendar Selector + Container( + color: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + IconButton( + icon: const Icon( + UiIcons.chevronLeft, + size: 20, + color: AppColors.krowCharcoal, + ), + onPressed: () => setState(() { + _weekOffset--; + _selectedDate = _getCalendarDays().first; + _loadShiftsForCurrentWeek(); + }), + constraints: const BoxConstraints(), + padding: EdgeInsets.zero, + ), + Text( + DateFormat('MMMM yyyy').format(weekStartDate), + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: AppColors.krowCharcoal, + ), + ), + IconButton( + icon: const Icon( + UiIcons.chevronRight, + size: 20, + color: AppColors.krowCharcoal, + ), + onPressed: () => setState(() { + _weekOffset++; + _selectedDate = _getCalendarDays().first; + _loadShiftsForCurrentWeek(); + }), + constraints: const BoxConstraints(), + padding: EdgeInsets.zero, + ), + ], + ), + ), + // Days Grid + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: calendarDays.map((date) { + final isSelected = _isSameDay(date, _selectedDate); + // ignore: unused_local_variable + final dateStr = DateFormat('yyyy-MM-dd').format(date); + final hasShifts = widget.myShifts.any((s) { + try { + return _isSameDay(DateTime.parse(s.date), date); + } catch (_) { + return false; + } + }); + + return GestureDetector( + onTap: () => setState(() => _selectedDate = date), + child: Column( + children: [ + Container( + width: 44, + height: 60, + decoration: BoxDecoration( + color: isSelected + ? AppColors.krowBlue + : Colors.white, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: isSelected + ? AppColors.krowBlue + : AppColors.krowBorder, + width: 1, + ), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + date.day.toString().padLeft(2, '0'), + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: isSelected + ? Colors.white + : AppColors.krowCharcoal, + ), + ), + Text( + DateFormat('E').format(date), + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.w500, + color: isSelected + ? Colors.white.withOpacity(0.8) + : AppColors.krowMuted, + ), + ), + if (hasShifts && !isSelected) + Container( + margin: const EdgeInsets.only(top: 4), + width: 4, + height: 4, + decoration: const BoxDecoration( + color: AppColors.krowBlue, + shape: BoxShape.circle, + ), + ), + ], + ), + ), + ], + ), + ); + }).toList(), + ), + ], + ), + ), + const Divider(height: 1, color: AppColors.krowBorder), + + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Column( + children: [ + const SizedBox(height: 20), + if (widget.pendingAssignments.isNotEmpty) ...[ + _buildSectionHeader( + "Awaiting Confirmation", + const Color(0xFFF59E0B), + ), + ...widget.pendingAssignments.map( + (shift) => Padding( + padding: const EdgeInsets.only(bottom: 16), + child: ShiftAssignmentCard( + shift: shift, + onConfirm: () => _confirmShift(shift.id), + onDecline: () => _declineShift(shift.id), + isConfirming: true, + ), + ), + ), + const SizedBox(height: 12), + ], + + if (visibleCancelledShifts.isNotEmpty) ...[ + _buildSectionHeader("Cancelled Shifts", AppColors.krowMuted), + ...visibleCancelledShifts.map( + (shift) => Padding( + padding: const EdgeInsets.only(bottom: 16), + child: _buildCancelledCard( + title: shift.title, + client: shift.clientName, + pay: "\$${(shift.hourlyRate * 8).toStringAsFixed(0)}", + rate: "\$${shift.hourlyRate}/hr · 8h", + date: _formatDateStr(shift.date), + time: "${shift.startTime} - ${shift.endTime}", + address: shift.locationAddress, + isLastMinute: true, + onTap: () {}, + ), + ), + ), + const SizedBox(height: 12), + ], + + // Confirmed Shifts + if (visibleMyShifts.isNotEmpty) ...[ + _buildSectionHeader("Confirmed Shifts", AppColors.krowMuted), + ...visibleMyShifts.map( + (shift) => Padding( + padding: const EdgeInsets.only(bottom: 12), + child: MyShiftCard(shift: shift), + ), + ), + ], + + if (visibleMyShifts.isEmpty && + widget.pendingAssignments.isEmpty && + widget.cancelledShifts.isEmpty) + const EmptyStateView( + icon: UiIcons.calendar, + title: "No shifts this week", + subtitle: "Try finding new jobs in the Find tab", + ), + + const SizedBox(height: UiConstants.space32), + ], + ), + ), + ), + ], + ); + } + + Widget _buildSectionHeader(String title, Color dotColor) { + return Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Row( + children: [ + Container( + width: 8, + height: 8, + decoration: BoxDecoration(color: dotColor, shape: BoxShape.circle), + ), + const SizedBox(width: 8), + Text( + title, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: dotColor == AppColors.krowMuted + ? AppColors.krowMuted + : dotColor, + ), + ), + ], + ), + ); + } + + Widget _buildCancelledCard({ + required String title, + required String client, + required String pay, + required String rate, + required String date, + required String time, + required String address, + required bool isLastMinute, + required VoidCallback onTap, + }) { + return GestureDetector( + onTap: onTap, + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: AppColors.krowBorder), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + width: 6, + height: 6, + decoration: const BoxDecoration( + color: Color(0xFFEF4444), + shape: BoxShape.circle, + ), + ), + const SizedBox(width: 6), + const Text( + "CANCELLED", + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.bold, + color: Color(0xFFEF4444), + ), + ), + if (isLastMinute) ...[ + const SizedBox(width: 4), + const Text( + "• 4hr compensation", + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.w500, + color: Color(0xFF10B981), + ), + ), + ], + ], + ), + const SizedBox(height: 12), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 44, + height: 44, + decoration: BoxDecoration( + color: AppColors.krowBlue.withOpacity(0.05), + borderRadius: BorderRadius.circular(12), + ), + child: const Center( + child: Icon( + LucideIcons.briefcase, + color: AppColors.krowBlue, + size: 20, + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: AppColors.krowCharcoal, + ), + ), + Text( + client, + style: const TextStyle( + fontSize: 12, + color: AppColors.krowMuted, + ), + ), + ], + ), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + pay, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: AppColors.krowCharcoal, + ), + ), + Text( + rate, + style: const TextStyle( + fontSize: 10, + color: AppColors.krowMuted, + ), + ), + ], + ), + ], + ), + const SizedBox(height: 8), + Row( + children: [ + const Icon( + LucideIcons.calendar, + size: 12, + color: AppColors.krowMuted, + ), + const SizedBox(width: 4), + Text( + date, + style: const TextStyle( + fontSize: 12, + color: AppColors.krowMuted, + ), + ), + const SizedBox(width: 12), + const Icon( + LucideIcons.clock, + size: 12, + color: AppColors.krowMuted, + ), + const SizedBox(width: 4), + Text( + time, + style: const TextStyle( + fontSize: 12, + color: AppColors.krowMuted, + ), + ), + ], + ), + const SizedBox(height: 4), + Row( + children: [ + const Icon( + LucideIcons.mapPin, + size: 12, + color: AppColors.krowMuted, + ), + const SizedBox(width: 4), + Expanded( + child: Text( + address, + style: const TextStyle( + fontSize: 12, + color: AppColors.krowMuted, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ], + ), + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/shift_details_module.dart b/apps/mobile/packages/features/staff/shifts/lib/src/shift_details_module.dart new file mode 100644 index 00000000..78fddf80 --- /dev/null +++ b/apps/mobile/packages/features/staff/shifts/lib/src/shift_details_module.dart @@ -0,0 +1,31 @@ +import 'package:flutter_modular/flutter_modular.dart'; +import 'domain/repositories/shifts_repository_interface.dart'; +import 'data/repositories_impl/shifts_repository_impl.dart'; +import 'domain/usecases/get_shift_details_usecase.dart'; +import 'domain/usecases/accept_shift_usecase.dart'; +import 'domain/usecases/decline_shift_usecase.dart'; +import 'domain/usecases/apply_for_shift_usecase.dart'; +import 'presentation/blocs/shift_details/shift_details_bloc.dart'; +import 'presentation/pages/shift_details_page.dart'; + +class ShiftDetailsModule extends Module { + @override + void binds(Injector i) { + // Repository + i.add(ShiftsRepositoryImpl.new); + + // UseCases + i.add(GetShiftDetailsUseCase.new); + i.add(AcceptShiftUseCase.new); + i.add(DeclineShiftUseCase.new); + i.add(ApplyForShiftUseCase.new); + + // Bloc + i.add(ShiftDetailsBloc.new); + } + + @override + void routes(RouteManager r) { + r.child('/:id', child: (_) => ShiftDetailsPage(shiftId: r.args.params['id'], shift: r.args.data)); + } +} diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/staff_shifts_module.dart b/apps/mobile/packages/features/staff/shifts/lib/src/staff_shifts_module.dart index 95c428fc..47fc79f0 100644 --- a/apps/mobile/packages/features/staff/shifts/lib/src/staff_shifts_module.dart +++ b/apps/mobile/packages/features/staff/shifts/lib/src/staff_shifts_module.dart @@ -4,7 +4,14 @@ import 'data/repositories_impl/shifts_repository_impl.dart'; import 'domain/usecases/get_my_shifts_usecase.dart'; import 'domain/usecases/get_available_shifts_usecase.dart'; import 'domain/usecases/get_pending_assignments_usecase.dart'; +import 'domain/usecases/get_cancelled_shifts_usecase.dart'; +import 'domain/usecases/get_history_shifts_usecase.dart'; +import 'domain/usecases/accept_shift_usecase.dart'; +import 'domain/usecases/decline_shift_usecase.dart'; +import 'domain/usecases/apply_for_shift_usecase.dart'; +import 'domain/usecases/get_shift_details_usecase.dart'; import 'presentation/blocs/shifts/shifts_bloc.dart'; +import 'presentation/blocs/shift_details/shift_details_bloc.dart'; import 'presentation/pages/shifts_page.dart'; import 'presentation/pages/shift_details_page.dart'; @@ -18,14 +25,30 @@ class StaffShiftsModule extends Module { i.add(GetMyShiftsUseCase.new); i.add(GetAvailableShiftsUseCase.new); i.add(GetPendingAssignmentsUseCase.new); + i.add(GetCancelledShiftsUseCase.new); + i.add(GetHistoryShiftsUseCase.new); + i.add(AcceptShiftUseCase.new); + i.add(DeclineShiftUseCase.new); + i.add(ApplyForShiftUseCase.new); + i.add(GetShiftDetailsUseCase.new); // Bloc i.add(ShiftsBloc.new); + i.add(ShiftDetailsBloc.new); } @override void routes(RouteManager r) { - r.child('/', child: (_) => const ShiftsPage()); - r.child('/details/:id', child: (_) => ShiftDetailsPage(shiftId: r.args.params['id'], shift: r.args.data)); + r.child( + '/', + child: (_) { + final args = r.args.data as Map?; + final queryParams = r.args.queryParams; + return ShiftsPage( + initialTab: queryParams['tab'] ?? args?['initialTab'], + selectedDate: args?['selectedDate'], + ); + }, + ); } } diff --git a/apps/mobile/packages/features/staff/shifts/lib/staff_shifts.dart b/apps/mobile/packages/features/staff/shifts/lib/staff_shifts.dart index 28ae0ac4..7d0a0518 100644 --- a/apps/mobile/packages/features/staff/shifts/lib/staff_shifts.dart +++ b/apps/mobile/packages/features/staff/shifts/lib/staff_shifts.dart @@ -1,4 +1,6 @@ library staff_shifts; export 'src/staff_shifts_module.dart'; +export 'src/shift_details_module.dart'; +export 'src/presentation/navigation/shifts_navigator.dart'; diff --git a/apps/mobile/packages/features/staff/shifts/pubspec.lock b/apps/mobile/packages/features/staff/shifts/pubspec.lock deleted file mode 100644 index a2cdf2f8..00000000 --- a/apps/mobile/packages/features/staff/shifts/pubspec.lock +++ /dev/null @@ -1,650 +0,0 @@ -# Generated by pub -# See https://dart.dev/tools/pub/glossary#lockfile -packages: - async: - dependency: transitive - description: - name: async - sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" - url: "https://pub.dev" - source: hosted - version: "2.13.0" - auto_injector: - dependency: transitive - description: - name: auto_injector - sha256: "1fc2624898e92485122eb2b1698dd42511d7ff6574f84a3a8606fc4549a1e8f8" - url: "https://pub.dev" - source: hosted - version: "2.1.1" - bloc: - dependency: transitive - description: - name: bloc - sha256: "106842ad6569f0b60297619e9e0b1885c2fb9bf84812935490e6c5275777804e" - url: "https://pub.dev" - source: hosted - version: "8.1.4" - boolean_selector: - dependency: transitive - description: - name: boolean_selector - sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" - url: "https://pub.dev" - source: hosted - version: "2.1.2" - characters: - dependency: transitive - description: - name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 - url: "https://pub.dev" - source: hosted - version: "1.4.0" - clock: - dependency: transitive - description: - name: clock - sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b - url: "https://pub.dev" - source: hosted - version: "1.1.2" - code_assets: - dependency: transitive - description: - name: code_assets - sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687" - url: "https://pub.dev" - source: hosted - version: "1.0.0" - collection: - dependency: transitive - description: - name: collection - sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" - url: "https://pub.dev" - source: hosted - version: "1.19.1" - core_localization: - dependency: "direct main" - description: - path: "../../../core_localization" - relative: true - source: path - version: "0.0.1" - crypto: - dependency: transitive - description: - name: crypto - sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf - url: "https://pub.dev" - source: hosted - version: "3.0.7" - csv: - dependency: transitive - description: - name: csv - sha256: c6aa2679b2a18cb57652920f674488d89712efaf4d3fdf2e537215b35fc19d6c - url: "https://pub.dev" - source: hosted - version: "6.0.0" - design_system: - dependency: "direct main" - description: - path: "../../../design_system" - relative: true - source: path - version: "0.0.1" - equatable: - dependency: "direct main" - description: - name: equatable - sha256: "3e0141505477fd8ad55d6eb4e7776d3fe8430be8e497ccb1521370c3f21a3e2b" - url: "https://pub.dev" - source: hosted - version: "2.0.8" - fake_async: - dependency: transitive - description: - name: fake_async - sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" - url: "https://pub.dev" - source: hosted - version: "1.3.3" - ffi: - dependency: transitive - description: - name: ffi - sha256: d07d37192dbf97461359c1518788f203b0c9102cfd2c35a716b823741219542c - url: "https://pub.dev" - source: hosted - version: "2.1.5" - file: - dependency: transitive - description: - name: file - sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 - url: "https://pub.dev" - source: hosted - version: "7.0.1" - fixnum: - dependency: transitive - description: - name: fixnum - sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be - url: "https://pub.dev" - source: hosted - version: "1.1.1" - flutter: - dependency: "direct main" - description: flutter - source: sdk - version: "0.0.0" - flutter_bloc: - dependency: "direct main" - description: - name: flutter_bloc - sha256: b594505eac31a0518bdcb4b5b79573b8d9117b193cc80cc12e17d639b10aa27a - url: "https://pub.dev" - source: hosted - version: "8.1.6" - flutter_lints: - dependency: "direct dev" - description: - name: flutter_lints - sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1" - url: "https://pub.dev" - source: hosted - version: "3.0.2" - flutter_localizations: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" - flutter_modular: - dependency: "direct main" - description: - name: flutter_modular - sha256: "33a63d9fe61429d12b3dfa04795ed890f17d179d3d38e988ba7969651fcd5586" - url: "https://pub.dev" - source: hosted - version: "6.4.1" - flutter_test: - dependency: "direct dev" - description: flutter - source: sdk - version: "0.0.0" - flutter_web_plugins: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" - font_awesome_flutter: - dependency: transitive - description: - name: font_awesome_flutter - sha256: b9011df3a1fa02993630b8fb83526368cf2206a711259830325bab2f1d2a4eb0 - url: "https://pub.dev" - source: hosted - version: "10.12.0" - glob: - dependency: transitive - description: - name: glob - sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de - url: "https://pub.dev" - source: hosted - version: "2.1.3" - google_fonts: - dependency: transitive - description: - name: google_fonts - sha256: "6996212014b996eaa17074e02b1b925b212f5e053832d9048970dc27255a8fb3" - url: "https://pub.dev" - source: hosted - version: "7.1.0" - hooks: - dependency: transitive - description: - name: hooks - sha256: "5d309c86e7ce34cd8e37aa71cb30cb652d3829b900ab145e4d9da564b31d59f7" - url: "https://pub.dev" - source: hosted - version: "1.0.0" - http: - dependency: transitive - description: - name: http - sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" - url: "https://pub.dev" - source: hosted - version: "1.6.0" - http_parser: - dependency: transitive - description: - name: http_parser - sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" - url: "https://pub.dev" - source: hosted - version: "4.1.2" - intl: - dependency: "direct main" - description: - name: intl - sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" - url: "https://pub.dev" - source: hosted - version: "0.20.2" - krow_core: - dependency: "direct main" - description: - path: "../../../core" - relative: true - source: path - version: "0.0.1" - krow_data_connect: - dependency: "direct main" - description: - path: "../../../data_connect" - relative: true - source: path - version: "0.0.1" - krow_domain: - dependency: "direct main" - description: - path: "../../../domain" - relative: true - source: path - version: "0.0.1" - leak_tracker: - dependency: transitive - description: - name: leak_tracker - sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" - url: "https://pub.dev" - source: hosted - version: "11.0.2" - leak_tracker_flutter_testing: - dependency: transitive - description: - name: leak_tracker_flutter_testing - sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" - url: "https://pub.dev" - source: hosted - version: "3.0.10" - leak_tracker_testing: - dependency: transitive - description: - name: leak_tracker_testing - sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" - url: "https://pub.dev" - source: hosted - version: "3.0.2" - lints: - dependency: transitive - description: - name: lints - sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290 - url: "https://pub.dev" - source: hosted - version: "3.0.0" - logging: - dependency: transitive - description: - name: logging - sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 - url: "https://pub.dev" - source: hosted - version: "1.3.0" - lucide_icons: - dependency: transitive - description: - name: lucide_icons - sha256: ad24d0fd65707e48add30bebada7d90bff2a1bba0a72d6e9b19d44246b0e83c4 - url: "https://pub.dev" - source: hosted - version: "0.257.0" - matcher: - dependency: transitive - description: - name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 - url: "https://pub.dev" - source: hosted - version: "0.12.17" - material_color_utilities: - dependency: transitive - description: - name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec - url: "https://pub.dev" - source: hosted - version: "0.11.1" - meta: - dependency: transitive - description: - name: meta - sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" - url: "https://pub.dev" - source: hosted - version: "1.17.0" - modular_core: - dependency: transitive - description: - name: modular_core - sha256: "1db0420a0dfb8a2c6dca846e7cbaa4ffeb778e247916dbcb27fb25aa566e5436" - url: "https://pub.dev" - source: hosted - version: "3.4.1" - native_toolchain_c: - dependency: transitive - description: - name: native_toolchain_c - sha256: "89e83885ba09da5fdf2cdacc8002a712ca238c28b7f717910b34bcd27b0d03ac" - url: "https://pub.dev" - source: hosted - version: "0.17.4" - nested: - dependency: transitive - description: - name: nested - sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" - url: "https://pub.dev" - source: hosted - version: "1.0.0" - objective_c: - dependency: transitive - description: - name: objective_c - sha256: "7fd0c4d8ac8980011753b9bdaed2bf15111365924cdeeeaeb596214ea2b03537" - url: "https://pub.dev" - source: hosted - version: "9.2.4" - path: - dependency: transitive - description: - name: path - sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" - url: "https://pub.dev" - source: hosted - version: "1.9.1" - path_provider: - dependency: transitive - description: - name: path_provider - sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" - url: "https://pub.dev" - source: hosted - version: "2.1.5" - path_provider_android: - dependency: transitive - description: - name: path_provider_android - sha256: f2c65e21139ce2c3dad46922be8272bb5963516045659e71bb16e151c93b580e - url: "https://pub.dev" - source: hosted - version: "2.2.22" - path_provider_foundation: - dependency: transitive - description: - name: path_provider_foundation - sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699" - url: "https://pub.dev" - source: hosted - version: "2.6.0" - path_provider_linux: - dependency: transitive - description: - name: path_provider_linux - sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 - url: "https://pub.dev" - source: hosted - version: "2.2.1" - path_provider_platform_interface: - dependency: transitive - description: - name: path_provider_platform_interface - sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" - url: "https://pub.dev" - source: hosted - version: "2.1.2" - path_provider_windows: - dependency: transitive - description: - name: path_provider_windows - sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 - url: "https://pub.dev" - source: hosted - version: "2.3.0" - platform: - dependency: transitive - description: - name: platform - sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" - url: "https://pub.dev" - source: hosted - version: "3.1.6" - plugin_platform_interface: - dependency: transitive - description: - name: plugin_platform_interface - sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" - url: "https://pub.dev" - source: hosted - version: "2.1.8" - provider: - dependency: transitive - description: - name: provider - sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272" - url: "https://pub.dev" - source: hosted - version: "6.1.5+1" - pub_semver: - dependency: transitive - description: - name: pub_semver - sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" - url: "https://pub.dev" - source: hosted - version: "2.2.0" - result_dart: - dependency: transitive - description: - name: result_dart - sha256: "0666b21fbdf697b3bdd9986348a380aa204b3ebe7c146d8e4cdaa7ce735e6054" - url: "https://pub.dev" - source: hosted - version: "2.1.1" - shared_preferences: - dependency: transitive - description: - name: shared_preferences - sha256: "2939ae520c9024cb197fc20dee269cd8cdbf564c8b5746374ec6cacdc5169e64" - url: "https://pub.dev" - source: hosted - version: "2.5.4" - shared_preferences_android: - dependency: transitive - description: - name: shared_preferences_android - sha256: "83af5c682796c0f7719c2bbf74792d113e40ae97981b8f266fa84574573556bc" - url: "https://pub.dev" - source: hosted - version: "2.4.18" - shared_preferences_foundation: - dependency: transitive - description: - name: shared_preferences_foundation - sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f" - url: "https://pub.dev" - source: hosted - version: "2.5.6" - shared_preferences_linux: - dependency: transitive - description: - name: shared_preferences_linux - sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" - url: "https://pub.dev" - source: hosted - version: "2.4.1" - shared_preferences_platform_interface: - dependency: transitive - description: - name: shared_preferences_platform_interface - sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" - url: "https://pub.dev" - source: hosted - version: "2.4.1" - shared_preferences_web: - dependency: transitive - description: - name: shared_preferences_web - sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 - url: "https://pub.dev" - source: hosted - version: "2.4.3" - shared_preferences_windows: - dependency: transitive - description: - name: shared_preferences_windows - sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" - url: "https://pub.dev" - source: hosted - version: "2.4.1" - sky_engine: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" - slang: - dependency: transitive - description: - name: slang - sha256: "13e3b6f07adc51ab751e7889647774d294cbce7a3382f81d9e5029acfe9c37b2" - url: "https://pub.dev" - source: hosted - version: "4.12.0" - slang_flutter: - dependency: transitive - description: - name: slang_flutter - sha256: "0a4545cca5404d6b7487cf61cf1fe56c52daeb08de56a7574ee8381fbad035a0" - url: "https://pub.dev" - source: hosted - version: "4.12.0" - source_span: - dependency: transitive - description: - name: source_span - sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" - url: "https://pub.dev" - source: hosted - version: "1.10.1" - stack_trace: - dependency: transitive - description: - name: stack_trace - sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" - url: "https://pub.dev" - source: hosted - version: "1.12.1" - stream_channel: - dependency: transitive - description: - name: stream_channel - sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" - url: "https://pub.dev" - source: hosted - version: "2.1.4" - string_scanner: - dependency: transitive - description: - name: string_scanner - sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" - url: "https://pub.dev" - source: hosted - version: "1.4.1" - term_glyph: - dependency: transitive - description: - name: term_glyph - sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" - url: "https://pub.dev" - source: hosted - version: "1.2.2" - test_api: - dependency: transitive - description: - name: test_api - sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 - url: "https://pub.dev" - source: hosted - version: "0.7.7" - typed_data: - dependency: transitive - description: - name: typed_data - sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 - url: "https://pub.dev" - source: hosted - version: "1.4.0" - uuid: - dependency: transitive - description: - name: uuid - sha256: a11b666489b1954e01d992f3d601b1804a33937b5a8fe677bd26b8a9f96f96e8 - url: "https://pub.dev" - source: hosted - version: "4.5.2" - vector_math: - dependency: transitive - description: - name: vector_math - sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b - url: "https://pub.dev" - source: hosted - version: "2.2.0" - vm_service: - dependency: transitive - description: - name: vm_service - sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" - url: "https://pub.dev" - source: hosted - version: "15.0.2" - watcher: - dependency: transitive - description: - name: watcher - sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635" - url: "https://pub.dev" - source: hosted - version: "1.2.1" - web: - dependency: transitive - description: - name: web - sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" - url: "https://pub.dev" - source: hosted - version: "1.1.1" - xdg_directories: - dependency: transitive - description: - name: xdg_directories - sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" - url: "https://pub.dev" - source: hosted - version: "1.1.0" - yaml: - dependency: transitive - description: - name: yaml - sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce - url: "https://pub.dev" - source: hosted - version: "3.1.3" -sdks: - dart: ">=3.10.7 <4.0.0" - flutter: ">=3.38.4" diff --git a/apps/mobile/packages/features/staff/shifts/pubspec.yaml b/apps/mobile/packages/features/staff/shifts/pubspec.yaml index 64467b03..99360d7a 100644 --- a/apps/mobile/packages/features/staff/shifts/pubspec.yaml +++ b/apps/mobile/packages/features/staff/shifts/pubspec.yaml @@ -2,9 +2,10 @@ name: staff_shifts description: A new Flutter package project. version: 0.0.1 publish_to: 'none' +resolution: workspace environment: - sdk: '>=3.0.0 <4.0.0' + sdk: '>=3.10.0 <4.0.0' flutter: ">=3.0.0" dependencies: @@ -30,4 +31,4 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^3.0.0 + flutter_lints: ^6.0.0 diff --git a/apps/mobile/packages/features/staff/staff_main/lib/src/presentation/blocs/staff_main_cubit.dart b/apps/mobile/packages/features/staff/staff_main/lib/src/presentation/blocs/staff_main_cubit.dart index 62953e03..2ea79cbb 100644 --- a/apps/mobile/packages/features/staff/staff_main/lib/src/presentation/blocs/staff_main_cubit.dart +++ b/apps/mobile/packages/features/staff/staff_main/lib/src/presentation/blocs/staff_main_cubit.dart @@ -16,16 +16,15 @@ class StaffMainCubit extends Cubit implements Disposable { int newIndex = state.currentIndex; // Detect which tab is active based on the route path - // Using contains() to handle child routes and trailing slashes - if (path.contains(StaffMainRoutes.shiftsFull)) { - newIndex = 0; - } else if (path.contains(StaffMainRoutes.paymentsFull)) { - newIndex = 1; - } else if (path.contains(StaffMainRoutes.homeFull)) { - newIndex = 2; - } else if (path.contains(StaffMainRoutes.clockInFull)) { + if (path.contains('/clock-in')) { newIndex = 3; - } else if (path.contains(StaffMainRoutes.profileFull)) { + } else if (path.contains('/payments')) { + newIndex = 1; + } else if (path.contains('/home')) { + newIndex = 2; + } else if (path.contains('/shifts')) { + newIndex = 0; + } else if (path.contains('/profile')) { newIndex = 4; } @@ -37,6 +36,9 @@ class StaffMainCubit extends Cubit implements Disposable { void navigateToTab(int index) { if (index == state.currentIndex) return; + // Optimistically update the tab index for instant feedback + emit(state.copyWith(currentIndex: index)); + switch (index) { case 0: Modular.to.navigateToShifts(); @@ -54,7 +56,6 @@ class StaffMainCubit extends Cubit implements Disposable { Modular.to.navigateToProfile(); break; } - // State update will happen via _onRouteChanged } @override diff --git a/apps/mobile/packages/features/staff/staff_main/lib/src/presentation/pages/staff_main_page.dart b/apps/mobile/packages/features/staff/staff_main/lib/src/presentation/pages/staff_main_page.dart index 53cad7c8..10ae9f8f 100644 --- a/apps/mobile/packages/features/staff/staff_main/lib/src/presentation/pages/staff_main_page.dart +++ b/apps/mobile/packages/features/staff/staff_main/lib/src/presentation/pages/staff_main_page.dart @@ -17,8 +17,8 @@ class StaffMainPage extends StatelessWidget { @override Widget build(BuildContext context) { - return BlocProvider( - create: (BuildContext context) => Modular.get(), + return BlocProvider.value( + value: Modular.get(), child: Scaffold( extendBody: true, body: const RouterOutlet(), diff --git a/apps/mobile/packages/features/staff/staff_main/lib/src/staff_main_module.dart b/apps/mobile/packages/features/staff/staff_main/lib/src/staff_main_module.dart index 1e154963..661aa05d 100644 --- a/apps/mobile/packages/features/staff/staff_main/lib/src/staff_main_module.dart +++ b/apps/mobile/packages/features/staff/staff_main/lib/src/staff_main_module.dart @@ -73,10 +73,14 @@ class StaffMainModule extends Module { '/time-card', module: StaffTimeCardModule(), ); - r.module('/availability', module: StaffAvailabilityModule()); r.module( - '/clock-in', - module: StaffClockInModule(), + '/availability', + module: StaffAvailabilityModule(), ); + r.module( + '/shift-details', + module: ShiftDetailsModule(), + ); + } } diff --git a/apps/mobile/packages/features/staff/staff_main/pubspec.yaml b/apps/mobile/packages/features/staff/staff_main/pubspec.yaml index 441aea74..5140d163 100644 --- a/apps/mobile/packages/features/staff/staff_main/pubspec.yaml +++ b/apps/mobile/packages/features/staff/staff_main/pubspec.yaml @@ -53,7 +53,7 @@ dependencies: path: ../availability staff_clock_in: path: ../clock_in - + dev_dependencies: flutter_test: sdk: flutter diff --git a/apps/mobile/pubspec.lock b/apps/mobile/pubspec.lock index 594c00bd..344d70d0 100644 --- a/apps/mobile/pubspec.lock +++ b/apps/mobile/pubspec.lock @@ -65,13 +65,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" - billing: - dependency: transitive - description: - path: "packages/features/client/billing" - relative: true - source: path - version: "1.0.0+1" bloc: dependency: transitive description: @@ -192,13 +185,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.4.2" - client_coverage: - dependency: transitive - description: - path: "packages/features/client/client_coverage" - relative: true - source: path - version: "1.0.0" clock: dependency: transitive description: @@ -295,6 +281,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.4.1" + dio: + dependency: transitive + description: + name: dio + sha256: b9d46faecab38fc8cc286f80bc4d61a3bb5d4ac49e51ed877b4d6706efe57b25 + url: "https://pub.dev" + source: hosted + version: "5.9.1" + dio_web_adapter: + dependency: transitive + description: + name: dio_web_adapter + sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78" + url: "https://pub.dev" + source: hosted + version: "2.1.1" equatable: dependency: transitive description: @@ -415,6 +417,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" + fl_chart: + dependency: transitive + description: + name: fl_chart + sha256: "00b74ae680df6b1135bdbea00a7d1fc072a9180b7c3f3702e4b19a9943f5ed7d" + url: "https://pub.dev" + source: hosted + version: "0.66.2" flutter: dependency: transitive description: flutter @@ -483,6 +493,54 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.0" + geolocator: + dependency: transitive + description: + name: geolocator + sha256: f4efb8d3c4cdcad2e226af9661eb1a0dd38c71a9494b22526f9da80ab79520e5 + url: "https://pub.dev" + source: hosted + version: "10.1.1" + geolocator_android: + dependency: transitive + description: + name: geolocator_android + sha256: fcb1760a50d7500deca37c9a666785c047139b5f9ee15aa5469fae7dbbe3170d + url: "https://pub.dev" + source: hosted + version: "4.6.2" + geolocator_apple: + dependency: transitive + description: + name: geolocator_apple + sha256: dbdd8789d5aaf14cf69f74d4925ad1336b4433a6efdf2fce91e8955dc921bf22 + url: "https://pub.dev" + source: hosted + version: "2.3.13" + geolocator_platform_interface: + dependency: transitive + description: + name: geolocator_platform_interface + sha256: "30cb64f0b9adcc0fb36f628b4ebf4f731a2961a0ebd849f4b56200205056fe67" + url: "https://pub.dev" + source: hosted + version: "4.2.6" + geolocator_web: + dependency: transitive + description: + name: geolocator_web + sha256: "102e7da05b48ca6bf0a5bda0010f886b171d1a08059f01bfe02addd0175ebece" + url: "https://pub.dev" + source: hosted + version: "2.2.1" + geolocator_windows: + dependency: transitive + description: + name: geolocator_windows + sha256: "175435404d20278ffd220de83c2ca293b73db95eafbdc8131fe8609be1421eb6" + url: "https://pub.dev" + source: hosted + version: "0.2.5" get_it: dependency: transitive description: @@ -515,6 +573,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.3.3+1" + google_places_flutter: + dependency: transitive + description: + name: google_places_flutter + sha256: "37bd64221cf4a5aa97eb3a33dc2d40f6326aa5ae4e2f2a9a7116bdc1a14f5194" + url: "https://pub.dev" + source: hosted + version: "2.1.1" googleapis_auth: dependency: transitive description: @@ -835,6 +901,54 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.0" + permission_handler: + dependency: transitive + description: + name: permission_handler + sha256: "59adad729136f01ea9e35a48f5d1395e25cba6cea552249ddbe9cf950f5d7849" + url: "https://pub.dev" + source: hosted + version: "11.4.0" + permission_handler_android: + dependency: transitive + description: + name: permission_handler_android + sha256: d3971dcdd76182a0c198c096b5db2f0884b0d4196723d21a866fc4cdea057ebc + url: "https://pub.dev" + source: hosted + version: "12.1.0" + permission_handler_apple: + dependency: transitive + description: + name: permission_handler_apple + sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023 + url: "https://pub.dev" + source: hosted + version: "9.4.7" + permission_handler_html: + dependency: transitive + description: + name: permission_handler_html + sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24" + url: "https://pub.dev" + source: hosted + version: "0.1.3+5" + permission_handler_platform_interface: + dependency: transitive + description: + name: permission_handler_platform_interface + sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878 + url: "https://pub.dev" + source: hosted + version: "4.3.0" + permission_handler_windows: + dependency: transitive + description: + name: permission_handler_windows + sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e" + url: "https://pub.dev" + source: hosted + version: "0.2.1" petitparser: dependency: transitive description: @@ -939,6 +1053,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" + url: "https://pub.dev" + source: hosted + version: "0.28.0" shared_preferences: dependency: transitive description: @@ -1088,76 +1210,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.12.1" - staff_attire: - dependency: transitive - description: - path: "packages/features/staff/profile_sections/onboarding/attire" - relative: true - source: path - version: "0.0.1" - staff_availability: - dependency: transitive - description: - path: "packages/features/staff/availability" - relative: true - source: path - version: "0.0.1" - staff_bank_account: - dependency: transitive - description: - path: "packages/features/staff/profile_sections/finances/staff_bank_account" - relative: true - source: path - version: "0.0.1" - staff_certificates: - dependency: transitive - description: - path: "packages/features/staff/profile_sections/compliance/certificates" - relative: true - source: path - version: "0.0.1" - staff_clock_in: - dependency: transitive - description: - path: "packages/features/staff/clock_in" - relative: true - source: path - version: "0.0.1" - staff_documents: - dependency: transitive - description: - path: "packages/features/staff/profile_sections/compliance/documents" - relative: true - source: path - version: "0.0.1" - staff_payments: - dependency: transitive - description: - path: "packages/features/staff/payments" - relative: true - source: path - version: "0.0.1" - staff_shifts: - dependency: transitive - description: - path: "packages/features/staff/shifts" - relative: true - source: path - version: "0.0.1" - staff_tax_forms: - dependency: transitive - description: - path: "packages/features/staff/profile_sections/compliance/tax_forms" - relative: true - source: path - version: "0.0.1" - staff_time_card: - dependency: transitive - description: - path: "packages/features/staff/profile_sections/finances/time_card" - relative: true - source: path - version: "0.0.1" stream_channel: dependency: transitive description: @@ -1383,5 +1435,5 @@ packages: source: hosted version: "2.2.3" sdks: - dart: ">=3.10.7 <4.0.0" + dart: ">=3.10.3 <4.0.0" flutter: ">=3.38.4" diff --git a/apps/mobile/pubspec.yaml b/apps/mobile/pubspec.yaml index 0d3eba1a..83f170e7 100644 --- a/apps/mobile/pubspec.yaml +++ b/apps/mobile/pubspec.yaml @@ -12,16 +12,28 @@ workspace: - packages/features/staff/authentication - packages/features/staff/home - packages/features/staff/staff_main + - packages/features/staff/payments - packages/features/staff/profile + - packages/features/staff/availability + - packages/features/staff/clock_in - packages/features/staff/profile_sections/onboarding/emergency_contact - packages/features/staff/profile_sections/onboarding/experience - packages/features/staff/profile_sections/onboarding/profile_info + - packages/features/staff/profile_sections/onboarding/attire + - packages/features/staff/profile_sections/finances/staff_bank_account + - packages/features/staff/profile_sections/finances/time_card + - packages/features/staff/profile_sections/compliance/certificates + - packages/features/staff/profile_sections/compliance/documents + - packages/features/staff/profile_sections/compliance/tax_forms + - packages/features/staff/shifts - packages/features/client/authentication + - packages/features/client/billing - packages/features/client/home - packages/features/client/settings - packages/features/client/hubs - packages/features/client/create_order - packages/features/client/view_orders + - packages/features/client/client_coverage - packages/features/client/client_main - apps/staff - apps/client diff --git a/backend/dataconnect/connector/application/mutations.gql b/backend/dataconnect/connector/application/mutations.gql index c978383d..3f4755cb 100644 --- a/backend/dataconnect/connector/application/mutations.gql +++ b/backend/dataconnect/connector/application/mutations.gql @@ -27,7 +27,7 @@ mutation updateApplicationStatus( $status: ApplicationStatus $checkInTime: Timestamp $checkOutTime: Timestamp - $roleId: UUID! + $roleId: UUID ) @auth(level: USER) { application_update( id: $id diff --git a/backend/dataconnect/connector/application/queries.gql b/backend/dataconnect/connector/application/queries.gql index d16e0f80..b7719115 100644 --- a/backend/dataconnect/connector/application/queries.gql +++ b/backend/dataconnect/connector/application/queries.gql @@ -700,4 +700,4 @@ query listCompletedApplicationsByStaffId( } } } -} \ No newline at end of file +} diff --git a/backend/dataconnect/connector/invoice/queries.gql b/backend/dataconnect/connector/invoice/queries.gql index f685fe34..394c70fd 100644 --- a/backend/dataconnect/connector/invoice/queries.gql +++ b/backend/dataconnect/connector/invoice/queries.gql @@ -51,6 +51,13 @@ query listInvoices( eventName deparment poReference + + teamHub { + address + placeId + hubName + } + } } } @@ -104,6 +111,13 @@ query getInvoiceById($id: UUID!) @auth(level: USER) { eventName deparment poReference + + teamHub { + address + placeId + hubName + } + } } } @@ -166,6 +180,13 @@ query listInvoicesByVendorId( eventName deparment poReference + + teamHub { + address + placeId + hubName + } + } } } @@ -228,6 +249,13 @@ query listInvoicesByBusinessId( eventName deparment poReference + + teamHub { + address + placeId + hubName + } + } } } @@ -290,6 +318,13 @@ query listInvoicesByOrderId( eventName deparment poReference + + teamHub { + address + placeId + hubName + } + } } } @@ -352,6 +387,13 @@ query listInvoicesByStatus( eventName deparment poReference + + teamHub { + address + placeId + hubName + } + } } } @@ -433,6 +475,13 @@ query filterInvoices( eventName deparment poReference + + teamHub { + address + placeId + hubName + } + } } } @@ -499,6 +548,13 @@ query listOverdueInvoices( eventName deparment poReference + + teamHub { + address + placeId + hubName + } + } } } diff --git a/backend/dataconnect/connector/order/queries.gql b/backend/dataconnect/connector/order/queries.gql index a2700936..334c60b6 100644 --- a/backend/dataconnect/connector/order/queries.gql +++ b/backend/dataconnect/connector/order/queries.gql @@ -393,6 +393,47 @@ query getRapidOrders( } } +#to validate if an hub has orders before delete +query listOrdersByBusinessAndTeamHub( + $businessId: UUID! + $teamHubId: UUID! + $offset: Int + $limit: Int +) @auth(level: USER) { + orders( + where: { + businessId: { eq: $businessId } + teamHubId: { eq: $teamHubId } + #status: {in: [ DRAFT POSTED FILLED PENDING FULLY_STAFFED PARTIAL_STAFFED ] } + } + offset: $offset + limit: $limit + orderBy: { createdAt: DESC } + ) { + id + eventName + orderType + status + duration + + businessId + vendorId + teamHubId + + date + startDate + endDate + + requested + total + notes + + createdAt + updatedAt + createdBy + } +} + #to validate if an hub has orders before delete query listOrdersByBusinessAndTeamHub( $businessId: UUID! diff --git a/backend/dataconnect/connector/shift/mutations.gql b/backend/dataconnect/connector/shift/mutations.gql index 3d275639..0a81f9bc 100644 --- a/backend/dataconnect/connector/shift/mutations.gql +++ b/backend/dataconnect/connector/shift/mutations.gql @@ -13,6 +13,11 @@ mutation createShift( $locationAddress: String $latitude: Float $longitude: Float + $placeId: String + $city: String + $state: String + $street: String + $country: String $description: String $status: ShiftStatus @@ -40,6 +45,11 @@ mutation createShift( locationAddress: $locationAddress latitude: $latitude longitude: $longitude + placeId: $placeId + city: $city + state: $state + street: $street + country: $country description: $description status: $status @@ -68,6 +78,11 @@ mutation updateShift( $locationAddress: String $latitude: Float $longitude: Float + $placeId: String + $city: String + $state: String + $street: String + $country: String $description: String $status: ShiftStatus @@ -95,6 +110,11 @@ mutation updateShift( locationAddress: $locationAddress latitude: $latitude longitude: $longitude + placeId: $placeId + city: $city + state: $state + street: $street + country: $country description: $description status: $status diff --git a/backend/dataconnect/connector/shift/queries.gql b/backend/dataconnect/connector/shift/queries.gql index 43c461df..96fdfbe8 100644 --- a/backend/dataconnect/connector/shift/queries.gql +++ b/backend/dataconnect/connector/shift/queries.gql @@ -22,6 +22,11 @@ query listShifts( locationAddress latitude longitude + placeId + city + state + street + country description status @@ -69,6 +74,11 @@ query getShiftById($id: UUID!) @auth(level: USER) { locationAddress latitude longitude + placeId + city + state + street + country description status @@ -134,6 +144,11 @@ query filterShifts( locationAddress latitude longitude + placeId + city + state + street + country description status @@ -194,6 +209,11 @@ query getShiftsByBusinessId( locationAddress latitude longitude + placeId + city + state + street + country description status @@ -254,6 +274,11 @@ query getShiftsByVendorId( locationAddress latitude longitude + placeId + city + state + street + country description status diff --git a/backend/dataconnect/connector/taxForm/mutations.gql b/backend/dataconnect/connector/taxForm/mutations.gql index 28a77120..868ed420 100644 --- a/backend/dataconnect/connector/taxForm/mutations.gql +++ b/backend/dataconnect/connector/taxForm/mutations.gql @@ -166,4 +166,4 @@ mutation updateTaxForm( mutation deleteTaxForm($id: UUID!) @auth(level: USER) { taxForm_delete(id: $id) -} \ No newline at end of file +} diff --git a/backend/dataconnect/connector/teamHub/mutations.gql b/backend/dataconnect/connector/teamHub/mutations.gql index 38542c35..adf57c42 100644 --- a/backend/dataconnect/connector/teamHub/mutations.gql +++ b/backend/dataconnect/connector/teamHub/mutations.gql @@ -2,9 +2,17 @@ mutation createTeamHub( $teamId: UUID! $hubName: String! $address: String! + + $placeId: String + $latitude: Float + $longitude: Float + $city: String $state: String + $street: String + $country: String $zipCode: String + $managerName: String $isActive: Boolean $departments: Any @@ -14,42 +22,72 @@ mutation createTeamHub( teamId: $teamId hubName: $hubName address: $address + + placeId: $placeId + latitude: $latitude + longitude: $longitude + city: $city state: $state + street: $street + country: $country zipCode: $zipCode + managerName: $managerName isActive: $isActive departments: $departments + } - ) + ) } mutation updateTeamHub( $id: UUID! + + $teamId: UUID $hubName: String $address: String + + $placeId: String + $latitude: Float + $longitude: Float + $city: String $state: String + $street: String + $country: String $zipCode: String + $managerName: String $isActive: Boolean $departments: Any + ) @auth(level: USER) { teamHub_update( id: $id data: { + teamId: $teamId hubName: $hubName address: $address + + placeId: $placeId + latitude: $latitude + longitude: $longitude + city: $city state: $state + street: $street + country: $country zipCode: $zipCode + managerName: $managerName isActive: $isActive departments: $departments + } - ) + ) } mutation deleteTeamHub($id: UUID!) @auth(level: USER) { - teamHub_delete(id: $id) -} + teamHub_delete(id: $id) +} \ No newline at end of file diff --git a/backend/dataconnect/connector/teamHub/queries.gql b/backend/dataconnect/connector/teamHub/queries.gql index 697b50c3..19619802 100644 --- a/backend/dataconnect/connector/teamHub/queries.gql +++ b/backend/dataconnect/connector/teamHub/queries.gql @@ -1,18 +1,30 @@ -query listTeamHubs @auth(level: USER) { - teamHubs { + +# ========================================================== +# TEAM HUB - QUERIES (USE where, NOT filter) +# Include ALL fields in TeamHub +# ========================================================== + +query listTeamHubs($offset: Int, $limit: Int) @auth(level: USER) { + teamHubs(offset: $offset, limit: $limit, orderBy: { createdAt: DESC }) { id teamId hubName + address + placeId + latitude + longitude + city state + street + country zipCode + managerName isActive departments - createdAt - updatedAt - createdBy + } } @@ -21,34 +33,55 @@ query getTeamHubById($id: UUID!) @auth(level: USER) { id teamId hubName + address + placeId + latitude + longitude + city state + street + country zipCode + managerName isActive departments - createdAt - updatedAt - createdBy + } } -query getTeamHubsByTeamId($teamId: UUID!) @auth(level: USER) { - teamHubs(where: { teamId: { eq: $teamId } }) { +query getTeamHubsByTeamId( + $teamId: UUID! + $offset: Int + $limit: Int +) @auth(level: USER) { + teamHubs( + where: { teamId: { eq: $teamId } } + offset: $offset + limit: $limit + orderBy: { createdAt: DESC } + ) { id teamId hubName + address + placeId + latitude + longitude + city state + street + country zipCode + managerName isActive departments - createdAt - updatedAt - createdBy + } } @@ -57,6 +90,8 @@ query getTeamHubsByTeamId($teamId: UUID!) @auth(level: USER) { # ------------------------------------------------------------ query listTeamHubsByOwnerId( $ownerId: UUID! + $offset: Int + $limit: Int ) @auth(level: USER) { teamHubs( where: { @@ -64,17 +99,28 @@ query listTeamHubsByOwnerId( ownerId: { eq: $ownerId } } } + offset: $offset + limit: $limit + orderBy: { createdAt: DESC } ) { id teamId hubName + address + placeId + latitude + longitude + city state + street + country zipCode + managerName isActive departments - createdAt + } -} \ No newline at end of file +} diff --git a/backend/dataconnect/functions/clean.gql b/backend/dataconnect/functions/clean.gql new file mode 100644 index 00000000..df8e84d5 --- /dev/null +++ b/backend/dataconnect/functions/clean.gql @@ -0,0 +1,67 @@ +mutation unseedAll @auth(level: USER) { + # ---------------------------------- + # Leaf/Join tables + # ---------------------------------- + taskComment_deleteMany(all: true) + memberTask_deleteMany(all: true) + assignment_deleteMany(all: true) + application_deleteMany(all: true) + message_deleteMany(all: true) + userConversation_deleteMany(all: true) + conversation_deleteMany(all: true) + activityLog_deleteMany(all: true) + clientFeedback_deleteMany(all: true) + recentPayment_deleteMany(all: true) + invoice_deleteMany(all: true) + invoiceTemplate_deleteMany(all: true) + customRateCard_deleteMany(all: true) + vendorRate_deleteMany(all: true) + vendorBenefitPlan_deleteMany(all: true) + staffCourse_deleteMany(all: true) + staffDocument_deleteMany(all: true) + staffRole_deleteMany(all: true) + staffAvailability_deleteMany(all: true) + staffAvailabilityStats_deleteMany(all: true) + emergencyContact_deleteMany(all: true) + taxForm_deleteMany(all: true) + certificate_deleteMany(all: true) + + # ---------------------------------- + # Tasks / Shifts / Orders + # ---------------------------------- + task_deleteMany(all: true) + shiftRole_deleteMany(all: true) + shift_deleteMany(all: true) + order_deleteMany(all: true) + + # ---------------------------------- + # Teams / Hubs / Org + # ---------------------------------- + teamMember_deleteMany(all: true) + teamHudDepartment_deleteMany(all: true) + team_deleteMany(all: true) + teamHub_deleteMany(all: true) + hub_deleteMany(all: true) + + # ---------------------------------- + # Catalog / Config + # ---------------------------------- + roleCategory_deleteMany(all: true) + role_deleteMany(all: true) + category_deleteMany(all: true) + level_deleteMany(all: true) + course_deleteMany(all: true) + faqData_deleteMany(all: true) + benefitsData_deleteMany(all: true) + attireOption_deleteMany(all: true) + document_deleteMany(all: true) + + # ---------------------------------- + # Core entities + # ---------------------------------- + staff_deleteMany(all: true) + vendor_deleteMany(all: true) + business_deleteMany(all: true) + account_deleteMany(all: true) + user_deleteMany(all: true) +} diff --git a/backend/dataconnect/functions/seed.gql b/backend/dataconnect/functions/seed.gql new file mode 100644 index 00000000..073861ae --- /dev/null +++ b/backend/dataconnect/functions/seed.gql @@ -0,0 +1,1726 @@ +mutation seedAll @transaction { + # Users + user_1: user_insert( + + data: { + id: "dvpWnaBjT6UksS5lo04hfMTyq1q1" + email: "legendary@krowd.com" + fullName: "Krow Payements" + role: USER + userRole: "BUSINESS" + } + ) + user_2: user_insert( + data: { + id: "hWjFHY11K3X1MChMseVVaCDfAl32" + email: "mariana.torres@gmail.com" + fullName: "Mariana" + role: USER + userRole: "STAFF" + } + ) + + # Business + business_1: business_insert( + data: { + id: "ef69e942-d6e5-48e5-a8bc-69d3faa63b2f" + businessName: "Krow" + userId: "dvpWnaBjT6UksS5lo04hfMTyq1q1" + contactName: "Krow Payements" + email: "legendary@krowd.com" + phone: "+1-818-555-0148" + address: "5000 San Jose Street, Granada Hills, CA, USA" + city: "Los Angeles" + area: SOUTHERN_CALIFORNIA + sector: OTHER + rateGroup: PREMIUM + status: ACTIVE + } + ) + + # Team + team_1: team_insert( + data: { + id: "9508c187-7612-4084-90de-4ece4a63773f" + teamName: "Krow" + ownerId: "ef69e942-d6e5-48e5-a8bc-69d3faa63b2f" + ownerName: "Krow" + ownerRole: "ADMIN" + totalHubs: 3 + } + ) + + # Team Hubs + team_hub_1: teamHub_insert( + data: { + id: "22a0b119-e6dc-4011-9043-d857cd4c12f3" + teamId: "9508c187-7612-4084-90de-4ece4a63773f" + hubName: "City Operations Center" + address: "5000 San Jose Street, Granada Hills, CA, USA" + city: "Los Angeles" + state: "CA" + street: "San Jose Street" + country: "US" + placeId: "Eiw1MDAwIFNhbiBKb3NlIFN0cmVldCwgR3JhbmFkYSBIaWxscywgQ0EsIFVTQSIuKiwKFAoSCYNJZBTdmsKAEddGOfBj8LvTEhQKEglnNXI0zZrCgBEjR6om62lcVw" + latitude: 34.2611486 + longitude: -118.5010287 + isActive: true + } + ) + team_hub_2: teamHub_insert( + data: { + id: "9c8eb9c6-c186-4d55-877e-35be852c3e86" + teamId: "9508c187-7612-4084-90de-4ece4a63773f" + hubName: "Central Operations Hub" + address: "4000 San Jose Street, Granada Hills, CA, USA" + city: "Los Angeles" + state: "CA" + street: "San Jose Street" + country: "US" + placeId: "Eiw0MDAwIFNhbiBKb3NlIFN0cmVldCwgR3JhbmFkYSBIaWxscywgQ0EsIFVTQSIuKiwKFAoSCYNJZBTdmsKAEddGOfBj8LvTEhQKEglnNXI0zZrCgBEjR6om62lcVw" + latitude: 34.2611486 + longitude: -118.5010287 + isActive: true + } + ) + team_hub_3: teamHub_insert( + data: { + id: "75c70d83-1680-4b28-ab61-2fe64a74fc5f" + teamId: "9508c187-7612-4084-90de-4ece4a63773f" + hubName: "Downtown Operations Hub" + address: "6800 San Jose Street, Granada Hills, CA, USA" + city: "Los Angeles" + state: "CA" + street: "San Jose Street" + country: "US" + placeId: "Eiw2ODAwIFNhbiBKb3NlIFN0cmVldCwgR3JhbmFkYSBIaWxscywgQ0EsIFVTQSIuKiwKFAoSCYNJZBTdmsKAEddGOfBj8LvTEhQKEglnNXI0zZrCgBEjR6om62lcVw" + latitude: 34.2611486 + longitude: -118.5010287 + isActive: true + } + ) + + # Vendor + vendor_1: vendor_insert( + data: { + id: "c3b25c47-0ebd-4402-a9b1-b8a875a7f71a" + userId: "xP7mQ2rL8vK5tR1nC3yH6uJ9wA0" + companyName: "Golden Gate Event Services" + legalName: "Golden Gate Event Services LLC" + doingBusinessAs: "GGE Services" + email: "hello@ggevents.com" + phone: "+1-415-555-0136" + address: "2100 Sunset Blvd, Los Angeles, CA 90026" + billingAddress: "2100 Sunset Blvd, Los Angeles, CA 90026" + city: "Los Angeles" + state: "CA" + region: "Southern California" + timezone: "America/Los_Angeles" + serviceSpecialty: "Event staffing and concessions" + approvalStatus: APPROVED + isActive: true + markup: 0.25 + fee: 2.5 + csat: 4.7 + tier: PREFERRED + } + ) + + # Role Categories + role_category_1: roleCategory_insert( + data: { id: "a8716f27-9e4c-4141-9ae2-6c9b91083b94", roleName: "Kitchen & Culinary", category: KITCHEN_AND_CULINARY } + ) + role_category_2: roleCategory_insert( + data: { id: "cb256793-50a5-4e0f-8464-e4092b25b6ab", roleName: "Concessions", category: CONCESSIONS } + ) + role_category_3: roleCategory_insert( + data: { id: "19e5e945-658f-4889-89b2-9fb14082650b", roleName: "Facilities", category: FACILITIES } + ) + role_category_4: roleCategory_insert( + data: { id: "291dd656-e801-4c69-aac1-90e4c22480d6", roleName: "Bartending", category: BARTENDING } + ) + role_category_5: roleCategory_insert( + data: { id: "4b4622c9-cc55-4b1a-970f-a01643fdb01c", roleName: "Security", category: SECURITY } + ) + role_category_6: roleCategory_insert( + data: { id: "2f8bf4ab-854b-4094-ac1c-cfd08fc79d9b", roleName: "Event Staff", category: EVENT_STAFF } + ) + role_category_7: roleCategory_insert( + data: { id: "143dee86-d7d4-476d-a5b0-e9c6fff0b64a", roleName: "Management", category: MANAGEMENT } + ) + role_category_8: roleCategory_insert( + data: { id: "2042d478-695d-4577-9781-47215188572a", roleName: "Technical", category: TECHNICAL } + ) + role_category_9: roleCategory_insert( + data: { id: "2951c364-202e-4a62-adf9-2270842150ab", roleName: "Other", category: OTHER } + ) + + # Roles + role_1: role_insert( + data: { + id: "e51f3553-f2ee-400b-91e6-92b534239697" + name: "Cook" + vendorId: "c3b25c47-0ebd-4402-a9b1-b8a875a7f71a" + roleCategoryId: "a8716f27-9e4c-4141-9ae2-6c9b91083b94" + costPerHour: 24 + } + ) + role_2: role_insert( + data: { + id: "7de956ce-743b-4271-b826-73313a5f07f5" + name: "Bartender" + vendorId: "c3b25c47-0ebd-4402-a9b1-b8a875a7f71a" + roleCategoryId: "291dd656-e801-4c69-aac1-90e4c22480d6" + costPerHour: 26 + } + ) + role_3: role_insert( + data: { + id: "73fdb09b-ecbd-402e-8eb4-e7d79237d017" + name: "Event Staff" + vendorId: "c3b25c47-0ebd-4402-a9b1-b8a875a7f71a" + roleCategoryId: "2f8bf4ab-854b-4094-ac1c-cfd08fc79d9b" + costPerHour: 20 + } + ) + role_4: role_insert( + data: { + id: "67ab1dcb-5b54-4dd9-aeb5-9cc58bdda0ed" + name: "Security Guard" + vendorId: "c3b25c47-0ebd-4402-a9b1-b8a875a7f71a" + roleCategoryId: "4b4622c9-cc55-4b1a-970f-a01643fdb01c" + costPerHour: 28 + } + ) + + # Staff (6 total) + staff_1: staff_insert( + data: { + id: "633df3ce-b92c-473f-90d8-38dd027fdf57" + userId: "hWjFHY11K3X1MChMseVVaCDfAl32" + fullName: "Mariana Torres" + email: "mariana.torres@gmail.com" + phone: "+1-818-555-0101" + ownerId: "c3b25c47-0ebd-4402-a9b1-b8a875a7f71a" + city: "Los Angeles" + addres: "North Hollywood, Los Angeles, CA" + englishRequired: true + isRecommended: true + totalShifts: 4 + averageRating: 4.5 + onTimeRate: 100 + noShowCount: 0 + cancellationCount: 1 + reliabilityScore: 95 + } + ) + staff_2: staff_insert( + data: { + id: "9631581a-1601-4e06-8e5e-600e9f305bcf" + userId: "V7mQ2pL8sKx5tR1nC3yH6uJ9wA0" + fullName: "Ethan Walker" + email: "ethan.walker@gmail.com" + phone: "+1-818-555-0102" + ownerId: "c3b25c47-0ebd-4402-a9b1-b8a875a7f71a" + city: "Los Angeles" + addres: "Burbank, CA" + englishRequired: true + } + ) + staff_3: staff_insert( + data: { + id: "2b678a6d-b8cd-4d5e-95ae-f35e4569f92c" + userId: "aB3cD5eF7gH9iJ2kL4mN6pQ8rS1" + fullName: "Sofia Ramirez" + email: "sofia.ramirez@gmail.com" + phone: "+1-818-555-0103" + ownerId: "c3b25c47-0ebd-4402-a9b1-b8a875a7f71a" + city: "Los Angeles" + addres: "Glendale, CA" + englishRequired: true + } + ) + staff_4: staff_insert( + data: { + id: "d62605f9-366d-42c5-8f3b-f276c0d27ea3" + userId: "Z9yX7wV5uT3sR1qP8nM6lK4jH2" + fullName: "Lucas Chen" + email: "lucas.chen@gmail.com" + phone: "+1-818-555-0104" + ownerId: "c3b25c47-0ebd-4402-a9b1-b8a875a7f71a" + city: "Los Angeles" + addres: "Pasadena, CA" + englishRequired: true + } + ) + staff_5: staff_insert( + data: { + id: "c6428f90-9c29-4e5c-b362-dc67a9a8cbba" + userId: "mN2bV5cX7zL9kJ4hG6fD1sA3qW8" + fullName: "Priya Patel" + email: "priya.patel@gmail.com" + phone: "+1-818-555-0105" + ownerId: "c3b25c47-0ebd-4402-a9b1-b8a875a7f71a" + city: "Los Angeles" + addres: "Studio City, CA" + englishRequired: true + } + ) + staff_6: staff_insert( + data: { + id: "56d7178c-f4ab-4c50-9b1f-d6efe25ba50b" + userId: "tR8yU6iO4pL2kJ9hG7fD5sA3qW1" + fullName: "Miguel Alvarez" + email: "miguel.alvarez@gmail.com" + phone: "+1-818-555-0106" + ownerId: "c3b25c47-0ebd-4402-a9b1-b8a875a7f71a" + city: "Los Angeles" + addres: "Granada Hills, CA" + englishRequired: true + } + ) + + # Orders (20 total) + order_01: order_insert( + data: { + id: "0e3b8fbb-ffd7-496d-a20a-2375b9205f54" + vendorId: "c3b25c47-0ebd-4402-a9b1-b8a875a7f71a" + businessId: "ef69e942-d6e5-48e5-a8bc-69d3faa63b2f" + orderType: ONE_TIME + status: COMPLETED + eventName: "Krow Opening Night" + teamHubId: "22a0b119-e6dc-4011-9043-d857cd4c12f3" + date: "2026-01-26T05:00:00Z" + requested: 1 + total: 192 + } + ) + order_02: order_insert( + data: { + id: "8927e7c7-7e99-400b-ba26-3e94d7039605" + vendorId: "c3b25c47-0ebd-4402-a9b1-b8a875a7f71a" + businessId: "ef69e942-d6e5-48e5-a8bc-69d3faa63b2f" + orderType: ONE_TIME + status: COMPLETED + eventName: "Downtown Launch Mixer" + teamHubId: "9c8eb9c6-c186-4d55-877e-35be852c3e86" + date: "2026-01-26T05:00:00Z" + requested: 1 + total: 208 + } + ) + order_03: order_insert( + data: { + id: "8bb46c96-74cd-48d6-bbb1-287823376e30" + vendorId: "c3b25c47-0ebd-4402-a9b1-b8a875a7f71a" + businessId: "ef69e942-d6e5-48e5-a8bc-69d3faa63b2f" + orderType: ONE_TIME + status: COMPLETED + eventName: "Community Night Market" + teamHubId: "75c70d83-1680-4b28-ab61-2fe64a74fc5f" + date: "2026-01-27T05:00:00Z" + requested: 1 + total: 160 + } + ) + order_04: order_insert( + data: { + id: "83b7dd83-2223-4585-a75f-b247368ebfcb" + vendorId: "c3b25c47-0ebd-4402-a9b1-b8a875a7f71a" + businessId: "ef69e942-d6e5-48e5-a8bc-69d3faa63b2f" + orderType: ONE_TIME + status: COMPLETED + eventName: "Krow Partner Showcase" + teamHubId: "22a0b119-e6dc-4011-9043-d857cd4c12f3" + date: "2026-01-28T05:00:00Z" + requested: 1 + total: 224 + } + ) + order_05: order_insert( + data: { + id: "1f7589f3-5bac-4174-82ed-844995ffb36e" + vendorId: "c3b25c47-0ebd-4402-a9b1-b8a875a7f71a" + businessId: "ef69e942-d6e5-48e5-a8bc-69d3faa63b2f" + orderType: ONE_TIME + status: COMPLETED + eventName: "Vendor Appreciation Lunch" + teamHubId: "9c8eb9c6-c186-4d55-877e-35be852c3e86" + date: "2026-01-28T05:00:00Z" + requested: 1 + total: 192 + } + ) + order_06: order_insert( + data: { + id: "df585e06-05f9-4859-865f-de23d8fa29fe" + vendorId: "c3b25c47-0ebd-4402-a9b1-b8a875a7f71a" + businessId: "ef69e942-d6e5-48e5-a8bc-69d3faa63b2f" + orderType: ONE_TIME + status: COMPLETED + eventName: "Operations Wrap-Up" + teamHubId: "75c70d83-1680-4b28-ab61-2fe64a74fc5f" + date: "2026-01-29T05:00:00Z" + requested: 1 + total: 208 + } + ) + order_07: order_insert( + data: { + id: "c3c5dca6-c8f9-4948-bb8c-10d8129914b3" + vendorId: "c3b25c47-0ebd-4402-a9b1-b8a875a7f71a" + businessId: "ef69e942-d6e5-48e5-a8bc-69d3faa63b2f" + orderType: ONE_TIME + status: COMPLETED + eventName: "Krow Friday Preview" + teamHubId: "22a0b119-e6dc-4011-9043-d857cd4c12f3" + date: "2026-01-30T05:00:00Z" + requested: 2 + total: 320 + } + ) + order_08: order_insert( + data: { + id: "a1fe2d34-cd5e-4372-bd73-e220a1840e1d" + vendorId: "c3b25c47-0ebd-4402-a9b1-b8a875a7f71a" + businessId: "ef69e942-d6e5-48e5-a8bc-69d3faa63b2f" + orderType: ONE_TIME + status: COMPLETED + eventName: "Saturday Security Detail" + teamHubId: "9c8eb9c6-c186-4d55-877e-35be852c3e86" + date: "2026-01-30T05:00:00Z" + requested: 1 + total: 224 + } + ) + order_09: order_insert( + data: { + id: "858753bc-dfa3-46fd-b383-ecd38de40b05" + vendorId: "c3b25c47-0ebd-4402-a9b1-b8a875a7f71a" + businessId: "ef69e942-d6e5-48e5-a8bc-69d3faa63b2f" + orderType: ONE_TIME + status: COMPLETED + eventName: "Weekend Brunch" + teamHubId: "75c70d83-1680-4b28-ab61-2fe64a74fc5f" + date: "2026-01-31T05:00:00Z" + requested: 1 + total: 192 + } + ) + order_10: order_insert( + data: { + id: "634386c5-45f3-46a0-a267-9971f0c19728" + vendorId: "c3b25c47-0ebd-4402-a9b1-b8a875a7f71a" + businessId: "ef69e942-d6e5-48e5-a8bc-69d3faa63b2f" + orderType: ONE_TIME + status: COMPLETED + eventName: "Sunday Service" + teamHubId: "22a0b119-e6dc-4011-9043-d857cd4c12f3" + date: "2026-02-01T05:00:00Z" + requested: 2 + total: 416 + } + ) + order_11: order_insert( + data: { + id: "43d593ed-0c58-4675-ae12-34aa27cb0d0c" + vendorId: "c3b25c47-0ebd-4402-a9b1-b8a875a7f71a" + businessId: "ef69e942-d6e5-48e5-a8bc-69d3faa63b2f" + orderType: ONE_TIME + status: COMPLETED + eventName: "Monday Concessions" + teamHubId: "9c8eb9c6-c186-4d55-877e-35be852c3e86" + date: "2026-02-02T05:00:00Z" + requested: 2 + total: 320 + } + ) + order_12: order_insert( + data: { + id: "7abf0183-a989-4c2a-b420-e959663da61b" + vendorId: "c3b25c47-0ebd-4402-a9b1-b8a875a7f71a" + businessId: "ef69e942-d6e5-48e5-a8bc-69d3faa63b2f" + orderType: ONE_TIME + status: COMPLETED + eventName: "Night Security Coverage" + teamHubId: "75c70d83-1680-4b28-ab61-2fe64a74fc5f" + date: "2026-02-02T05:00:00Z" + requested: 1 + total: 224 + } + ) + order_13: order_insert( + data: { + id: "2d2d1d8a-1771-4499-831c-2146207105c2" + vendorId: "c3b25c47-0ebd-4402-a9b1-b8a875a7f71a" + businessId: "ef69e942-d6e5-48e5-a8bc-69d3faa63b2f" + orderType: ONE_TIME + status: POSTED + eventName: "Tuesday Kitchen Prep" + teamHubId: "22a0b119-e6dc-4011-9043-d857cd4c12f3" + date: "2026-02-03T05:00:00Z" + requested: 1 + total: 192 + } + ) + order_14: order_insert( + data: { + id: "fb29987a-945d-434c-84e4-9870d04146e7" + vendorId: "c3b25c47-0ebd-4402-a9b1-b8a875a7f71a" + businessId: "ef69e942-d6e5-48e5-a8bc-69d3faa63b2f" + orderType: ONE_TIME + status: PARTIAL_STAFFED + eventName: "Midweek Bar Service" + teamHubId: "9c8eb9c6-c186-4d55-877e-35be852c3e86" + date: "2026-02-04T05:00:00Z" + requested: 2 + total: 416 + } + ) + order_15: order_insert( + data: { + id: "baee688f-6eb9-41cf-a88c-b5c4826767a5" + vendorId: "c3b25c47-0ebd-4402-a9b1-b8a875a7f71a" + businessId: "ef69e942-d6e5-48e5-a8bc-69d3faa63b2f" + orderType: ONE_TIME + status: PARTIAL_STAFFED + eventName: "Community Volunteer Night" + teamHubId: "75c70d83-1680-4b28-ab61-2fe64a74fc5f" + date: "2026-02-04T05:00:00Z" + requested: 2 + total: 320 + } + ) + order_16: order_insert( + data: { + id: "724eb236-aee2-4529-b702-65c8dfc7dcc0" + vendorId: "c3b25c47-0ebd-4402-a9b1-b8a875a7f71a" + businessId: "ef69e942-d6e5-48e5-a8bc-69d3faa63b2f" + orderType: ONE_TIME + status: POSTED + eventName: "Thursday Security Watch" + teamHubId: "22a0b119-e6dc-4011-9043-d857cd4c12f3" + date: "2026-02-05T05:00:00Z" + requested: 1 + total: 224 + } + ) + order_17: order_insert( + data: { + id: "ed2f36a7-1198-4515-8a24-f2495cf95dda" + vendorId: "c3b25c47-0ebd-4402-a9b1-b8a875a7f71a" + businessId: "ef69e942-d6e5-48e5-a8bc-69d3faa63b2f" + orderType: ONE_TIME + status: PARTIAL_STAFFED + eventName: "Friday Kitchen Support" + teamHubId: "9c8eb9c6-c186-4d55-877e-35be852c3e86" + date: "2026-02-06T05:00:00Z" + requested: 2 + total: 384 + } + ) + order_18: order_insert( + data: { + id: "5cf4ca96-fdf4-4d08-bcee-79fae59812b6" + vendorId: "c3b25c47-0ebd-4402-a9b1-b8a875a7f71a" + businessId: "ef69e942-d6e5-48e5-a8bc-69d3faa63b2f" + orderType: ONE_TIME + status: POSTED + eventName: "Friday Bar Coverage" + teamHubId: "75c70d83-1680-4b28-ab61-2fe64a74fc5f" + date: "2026-02-06T05:00:00Z" + requested: 1 + total: 208 + } + ) + order_19: order_insert( + data: { + id: "60307e4b-d9d8-4cd1-9516-8c52227072da" + vendorId: "c3b25c47-0ebd-4402-a9b1-b8a875a7f71a" + businessId: "ef69e942-d6e5-48e5-a8bc-69d3faa63b2f" + orderType: ONE_TIME + status: PARTIAL_STAFFED + eventName: "Saturday Event Support" + teamHubId: "22a0b119-e6dc-4011-9043-d857cd4c12f3" + date: "2026-02-07T05:00:00Z" + requested: 2 + total: 320 + } + ) + order_20: order_insert( + data: { + id: "700d75e6-4ad8-4ed2-8c52-4f23e0a1bd4c" + vendorId: "c3b25c47-0ebd-4402-a9b1-b8a875a7f71a" + businessId: "ef69e942-d6e5-48e5-a8bc-69d3faa63b2f" + orderType: ONE_TIME + status: POSTED + eventName: "Sunday Security Patrol" + teamHubId: "9c8eb9c6-c186-4d55-877e-35be852c3e86" + date: "2026-02-08T05:00:00Z" + requested: 1 + total: 224 + } + ) + + # Shifts (1 per order) + shift_01: shift_insert( + data: { + id: "97475714-44d9-4a52-8486-672977689bc0" + title: "Krow Opening Night Shift" + orderId: "0e3b8fbb-ffd7-496d-a20a-2375b9205f54" + date: "2026-01-26T05:00:00Z" + startTime: "2026-01-26T14:00:00Z" + endTime: "2026-01-26T22:00:00Z" + hours: 8 + cost: 192 + locationAddress: "5000 San Jose Street, Granada Hills, CA, USA" + city: "Los Angeles" + state: "CA" + street: "San Jose Street" + country: "US" + placeId: "Eiw1MDAwIFNhbiBKb3NlIFN0cmVldCwgR3JhbmFkYSBIaWxscywgQ0EsIFVTQSIuKiwKFAoSCYNJZBTdmsKAEddGOfBj8LvTEhQKEglnNXI0zZrCgBEjR6om62lcVw" + latitude: 34.2611486 + longitude: -118.5010287 + status: COMPLETED + workersNeeded: 1 + filled: 1 + } + ) + shift_02: shift_insert( + data: { + id: "50493c45-ad80-4a7b-993b-cd54a9bb1cbf" + title: "Downtown Launch Mixer Shift" + orderId: "8927e7c7-7e99-400b-ba26-3e94d7039605" + date: "2026-01-26T05:00:00Z" + startTime: "2026-01-26T14:00:00Z" + endTime: "2026-01-26T22:00:00Z" + hours: 8 + cost: 208 + locationAddress: "4000 San Jose Street, Granada Hills, CA, USA" + city: "Los Angeles" + state: "CA" + street: "San Jose Street" + country: "US" + placeId: "Eiw0MDAwIFNhbiBKb3NlIFN0cmVldCwgR3JhbmFkYSBIaWxscywgQ0EsIFVTQSIuKiwKFAoSCYNJZBTdmsKAEddGOfBj8LvTEhQKEglnNXI0zZrCgBEjR6om62lcVw" + latitude: 34.2611486 + longitude: -118.5010287 + status: COMPLETED + workersNeeded: 1 + filled: 1 + } + ) + shift_03: shift_insert( + data: { + id: "05101aa0-48b5-4f6e-8327-3c3679fd59dd" + title: "Community Night Market Shift" + orderId: "8bb46c96-74cd-48d6-bbb1-287823376e30" + date: "2026-01-27T05:00:00Z" + startTime: "2026-01-27T14:00:00Z" + endTime: "2026-01-27T22:00:00Z" + hours: 8 + cost: 160 + locationAddress: "6800 San Jose Street, Granada Hills, CA, USA" + city: "Los Angeles" + state: "CA" + street: "San Jose Street" + country: "US" + placeId: "Eiw2ODAwIFNhbiBKb3NlIFN0cmVldCwgR3JhbmFkYSBIaWxscywgQ0EsIFVTQSIuKiwKFAoSCYNJZBTdmsKAEddGOfBj8LvTEhQKEglnNXI0zZrCgBEjR6om62lcVw" + latitude: 34.2611486 + longitude: -118.5010287 + status: COMPLETED + workersNeeded: 1 + filled: 1 + } + ) + shift_04: shift_insert( + data: { + id: "dafa7ede-5245-436c-af4a-1d1f20d68ab5" + title: "Krow Partner Showcase Shift" + orderId: "83b7dd83-2223-4585-a75f-b247368ebfcb" + date: "2026-01-28T05:00:00Z" + startTime: "2026-01-28T14:00:00Z" + endTime: "2026-01-28T22:00:00Z" + hours: 8 + cost: 224 + locationAddress: "5000 San Jose Street, Granada Hills, CA, USA" + city: "Los Angeles" + state: "CA" + street: "San Jose Street" + country: "US" + placeId: "Eiw1MDAwIFNhbiBKb3NlIFN0cmVldCwgR3JhbmFkYSBIaWxscywgQ0EsIFVTQSIuKiwKFAoSCYNJZBTdmsKAEddGOfBj8LvTEhQKEglnNXI0zZrCgBEjR6om62lcVw" + latitude: 34.2611486 + longitude: -118.5010287 + status: COMPLETED + workersNeeded: 1 + filled: 1 + } + ) + shift_05: shift_insert( + data: { + id: "ab51c851-8d93-4a7c-907a-d768d6908b7f" + title: "Vendor Appreciation Lunch Shift" + orderId: "1f7589f3-5bac-4174-82ed-844995ffb36e" + date: "2026-01-28T05:00:00Z" + startTime: "2026-01-28T14:00:00Z" + endTime: "2026-01-28T22:00:00Z" + hours: 8 + cost: 192 + locationAddress: "4000 San Jose Street, Granada Hills, CA, USA" + city: "Los Angeles" + state: "CA" + street: "San Jose Street" + country: "US" + placeId: "Eiw0MDAwIFNhbiBKb3NlIFN0cmVldCwgR3JhbmFkYSBIaWxscywgQ0EsIFVTQSIuKiwKFAoSCYNJZBTdmsKAEddGOfBj8LvTEhQKEglnNXI0zZrCgBEjR6om62lcVw" + latitude: 34.2611486 + longitude: -118.5010287 + status: COMPLETED + workersNeeded: 1 + filled: 1 + } + ) + shift_06: shift_insert( + data: { + id: "dbb94e32-7f51-4fd4-bfc9-148a90867437" + title: "Operations Wrap-Up Shift" + orderId: "df585e06-05f9-4859-865f-de23d8fa29fe" + date: "2026-01-29T05:00:00Z" + startTime: "2026-01-29T14:00:00Z" + endTime: "2026-01-29T22:00:00Z" + hours: 8 + cost: 208 + locationAddress: "6800 San Jose Street, Granada Hills, CA, USA" + city: "Los Angeles" + state: "CA" + street: "San Jose Street" + country: "US" + placeId: "Eiw2ODAwIFNhbiBKb3NlIFN0cmVldCwgR3JhbmFkYSBIaWxscywgQ0EsIFVTQSIuKiwKFAoSCYNJZBTdmsKAEddGOfBj8LvTEhQKEglnNXI0zZrCgBEjR6om62lcVw" + latitude: 34.2611486 + longitude: -118.5010287 + status: COMPLETED + workersNeeded: 1 + filled: 1 + } + ) + shift_07: shift_insert( + data: { + id: "7dc230cb-5680-4799-b45a-8a8269675a42" + title: "Krow Friday Preview Shift" + orderId: "c3c5dca6-c8f9-4948-bb8c-10d8129914b3" + date: "2026-01-30T05:00:00Z" + startTime: "2026-01-30T14:00:00Z" + endTime: "2026-01-30T22:00:00Z" + hours: 8 + cost: 320 + locationAddress: "5000 San Jose Street, Granada Hills, CA, USA" + city: "Los Angeles" + state: "CA" + street: "San Jose Street" + country: "US" + placeId: "Eiw1MDAwIFNhbiBKb3NlIFN0cmVldCwgR3JhbmFkYSBIaWxscywgQ0EsIFVTQSIuKiwKFAoSCYNJZBTdmsKAEddGOfBj8LvTEhQKEglnNXI0zZrCgBEjR6om62lcVw" + latitude: 34.2611486 + longitude: -118.5010287 + status: COMPLETED + workersNeeded: 2 + filled: 2 + } + ) + shift_08: shift_insert( + data: { + id: "5e4cc4e0-51a4-406b-82cd-39bfa3a0970a" + title: "Saturday Security Detail Shift" + orderId: "a1fe2d34-cd5e-4372-bd73-e220a1840e1d" + date: "2026-01-30T05:00:00Z" + startTime: "2026-01-30T14:00:00Z" + endTime: "2026-01-30T22:00:00Z" + hours: 8 + cost: 224 + locationAddress: "4000 San Jose Street, Granada Hills, CA, USA" + city: "Los Angeles" + state: "CA" + street: "San Jose Street" + country: "US" + placeId: "Eiw0MDAwIFNhbiBKb3NlIFN0cmVldCwgR3JhbmFkYSBIaWxscywgQ0EsIFVTQSIuKiwKFAoSCYNJZBTdmsKAEddGOfBj8LvTEhQKEglnNXI0zZrCgBEjR6om62lcVw" + latitude: 34.2611486 + longitude: -118.5010287 + status: COMPLETED + workersNeeded: 1 + filled: 1 + } + ) + shift_09: shift_insert( + data: { + id: "07be57d0-a580-46b7-b98e-1e29249cff63" + title: "Weekend Brunch Shift" + orderId: "858753bc-dfa3-46fd-b383-ecd38de40b05" + date: "2026-01-31T05:00:00Z" + startTime: "2026-01-31T14:00:00Z" + endTime: "2026-01-31T22:00:00Z" + hours: 8 + cost: 192 + locationAddress: "6800 San Jose Street, Granada Hills, CA, USA" + city: "Los Angeles" + state: "CA" + street: "San Jose Street" + country: "US" + placeId: "Eiw2ODAwIFNhbiBKb3NlIFN0cmVldCwgR3JhbmFkYSBIaWxscywgQ0EsIFVTQSIuKiwKFAoSCYNJZBTdmsKAEddGOfBj8LvTEhQKEglnNXI0zZrCgBEjR6om62lcVw" + latitude: 34.2611486 + longitude: -118.5010287 + status: COMPLETED + workersNeeded: 1 + filled: 1 + } + ) + shift_10: shift_insert( + data: { + id: "845b366b-3ec6-4322-b3cf-b386e6ad8ba5" + title: "Sunday Service Shift" + orderId: "634386c5-45f3-46a0-a267-9971f0c19728" + date: "2026-02-01T05:00:00Z" + startTime: "2026-02-01T14:00:00Z" + endTime: "2026-02-01T22:00:00Z" + hours: 8 + cost: 416 + locationAddress: "5000 San Jose Street, Granada Hills, CA, USA" + city: "Los Angeles" + state: "CA" + street: "San Jose Street" + country: "US" + placeId: "Eiw1MDAwIFNhbiBKb3NlIFN0cmVldCwgR3JhbmFkYSBIaWxscywgQ0EsIFVTQSIuKiwKFAoSCYNJZBTdmsKAEddGOfBj8LvTEhQKEglnNXI0zZrCgBEjR6om62lcVw" + latitude: 34.2611486 + longitude: -118.5010287 + status: COMPLETED + workersNeeded: 2 + filled: 2 + } + ) + shift_11: shift_insert( + data: { + id: "940ae08f-a9f4-4fe4-ab8b-a219c53f3bfa" + title: "Monday Concessions Shift" + orderId: "43d593ed-0c58-4675-ae12-34aa27cb0d0c" + date: "2026-02-02T05:00:00Z" + startTime: "2026-02-02T14:00:00Z" + endTime: "2026-02-02T22:00:00Z" + hours: 8 + cost: 320 + locationAddress: "4000 San Jose Street, Granada Hills, CA, USA" + city: "Los Angeles" + state: "CA" + street: "San Jose Street" + country: "US" + placeId: "Eiw0MDAwIFNhbiBKb3NlIFN0cmVldCwgR3JhbmFkYSBIaWxscywgQ0EsIFVTQSIuKiwKFAoSCYNJZBTdmsKAEddGOfBj8LvTEhQKEglnNXI0zZrCgBEjR6om62lcVw" + latitude: 34.2611486 + longitude: -118.5010287 + status: COMPLETED + workersNeeded: 2 + filled: 2 + } + ) + shift_12: shift_insert( + data: { + id: "738cd678-9179-4360-bf24-426700651a37" + title: "Night Security Coverage Shift" + orderId: "7abf0183-a989-4c2a-b420-e959663da61b" + date: "2026-02-02T05:00:00Z" + startTime: "2026-02-02T14:00:00Z" + endTime: "2026-02-02T22:00:00Z" + hours: 8 + cost: 224 + locationAddress: "6800 San Jose Street, Granada Hills, CA, USA" + city: "Los Angeles" + state: "CA" + street: "San Jose Street" + country: "US" + placeId: "Eiw2ODAwIFNhbiBKb3NlIFN0cmVldCwgR3JhbmFkYSBIaWxscywgQ0EsIFVTQSIuKiwKFAoSCYNJZBTdmsKAEddGOfBj8LvTEhQKEglnNXI0zZrCgBEjR6om62lcVw" + latitude: 34.2611486 + longitude: -118.5010287 + status: COMPLETED + workersNeeded: 1 + filled: 1 + } + ) + shift_13: shift_insert( + data: { + id: "c08dd45c-ce93-4f98-948a-5ba6a8f15296" + title: "Tuesday Kitchen Prep Shift" + orderId: "2d2d1d8a-1771-4499-831c-2146207105c2" + date: "2026-02-03T05:00:00Z" + startTime: "2026-02-03T14:00:00Z" + endTime: "2026-02-03T22:00:00Z" + hours: 8 + cost: 192 + locationAddress: "5000 San Jose Street, Granada Hills, CA, USA" + city: "Los Angeles" + state: "CA" + street: "San Jose Street" + country: "US" + placeId: "Eiw1MDAwIFNhbiBKb3NlIFN0cmVldCwgR3JhbmFkYSBIaWxscywgQ0EsIFVTQSIuKiwKFAoSCYNJZBTdmsKAEddGOfBj8LvTEhQKEglnNXI0zZrCgBEjR6om62lcVw" + latitude: 34.2611486 + longitude: -118.5010287 + status: OPEN + workersNeeded: 1 + filled: 0 + } + ) + shift_14: shift_insert( + data: { + id: "38b194b2-55f4-4af7-991d-38d46c95916c" + title: "Midweek Bar Service Shift" + orderId: "fb29987a-945d-434c-84e4-9870d04146e7" + date: "2026-02-04T05:00:00Z" + startTime: "2026-02-04T14:00:00Z" + endTime: "2026-02-04T22:00:00Z" + hours: 8 + cost: 416 + locationAddress: "4000 San Jose Street, Granada Hills, CA, USA" + city: "Los Angeles" + state: "CA" + street: "San Jose Street" + country: "US" + placeId: "Eiw0MDAwIFNhbiBKb3NlIFN0cmVldCwgR3JhbmFkYSBIaWxscywgQ0EsIFVTQSIuKiwKFAoSCYNJZBTdmsKAEddGOfBj8LvTEhQKEglnNXI0zZrCgBEjR6om62lcVw" + latitude: 34.2611486 + longitude: -118.5010287 + status: ASSIGNED + workersNeeded: 2 + filled: 1 + } + ) + shift_15: shift_insert( + data: { + id: "9cdd54c7-7e48-4149-bb79-0cd142550328" + title: "Community Volunteer Night Shift" + orderId: "baee688f-6eb9-41cf-a88c-b5c4826767a5" + date: "2026-02-04T05:00:00Z" + startTime: "2026-02-04T14:00:00Z" + endTime: "2026-02-04T22:00:00Z" + hours: 8 + cost: 320 + locationAddress: "6800 San Jose Street, Granada Hills, CA, USA" + city: "Los Angeles" + state: "CA" + street: "San Jose Street" + country: "US" + placeId: "Eiw2ODAwIFNhbiBKb3NlIFN0cmVldCwgR3JhbmFkYSBIaWxscywgQ0EsIFVTQSIuKiwKFAoSCYNJZBTdmsKAEddGOfBj8LvTEhQKEglnNXI0zZrCgBEjR6om62lcVw" + latitude: 34.2611486 + longitude: -118.5010287 + status: ASSIGNED + workersNeeded: 2 + filled: 1 + } + ) + shift_16: shift_insert( + data: { + id: "96896f2c-525f-4a71-980a-843007b6115b" + title: "Thursday Security Watch Shift" + orderId: "724eb236-aee2-4529-b702-65c8dfc7dcc0" + date: "2026-02-05T05:00:00Z" + startTime: "2026-02-05T14:00:00Z" + endTime: "2026-02-05T22:00:00Z" + hours: 8 + cost: 224 + locationAddress: "5000 San Jose Street, Granada Hills, CA, USA" + city: "Los Angeles" + state: "CA" + street: "San Jose Street" + country: "US" + placeId: "Eiw1MDAwIFNhbiBKb3NlIFN0cmVldCwgR3JhbmFkYSBIaWxscywgQ0EsIFVTQSIuKiwKFAoSCYNJZBTdmsKAEddGOfBj8LvTEhQKEglnNXI0zZrCgBEjR6om62lcVw" + latitude: 34.2611486 + longitude: -118.5010287 + status: OPEN + workersNeeded: 1 + filled: 0 + } + ) + shift_17: shift_insert( + data: { + id: "1cd2e3d1-42d5-4c04-8778-171d599fe157" + title: "Friday Kitchen Support Shift" + orderId: "ed2f36a7-1198-4515-8a24-f2495cf95dda" + date: "2026-02-06T05:00:00Z" + startTime: "2026-02-06T14:00:00Z" + endTime: "2026-02-06T22:00:00Z" + hours: 8 + cost: 384 + locationAddress: "4000 San Jose Street, Granada Hills, CA, USA" + city: "Los Angeles" + state: "CA" + street: "San Jose Street" + country: "US" + placeId: "Eiw0MDAwIFNhbiBKb3NlIFN0cmVldCwgR3JhbmFkYSBIaWxscywgQ0EsIFVTQSIuKiwKFAoSCYNJZBTdmsKAEddGOfBj8LvTEhQKEglnNXI0zZrCgBEjR6om62lcVw" + latitude: 34.2611486 + longitude: -118.5010287 + status: ASSIGNED + workersNeeded: 2 + filled: 1 + } + ) + shift_18: shift_insert( + data: { + id: "fa6e2567-bbcc-4eee-a4ac-16cca06283ad" + title: "Friday Bar Coverage Shift" + orderId: "5cf4ca96-fdf4-4d08-bcee-79fae59812b6" + date: "2026-02-06T05:00:00Z" + startTime: "2026-02-06T14:00:00Z" + endTime: "2026-02-06T22:00:00Z" + hours: 8 + cost: 208 + locationAddress: "6800 San Jose Street, Granada Hills, CA, USA" + city: "Los Angeles" + state: "CA" + street: "San Jose Street" + country: "US" + placeId: "Eiw2ODAwIFNhbiBKb3NlIFN0cmVldCwgR3JhbmFkYSBIaWxscywgQ0EsIFVTQSIuKiwKFAoSCYNJZBTdmsKAEddGOfBj8LvTEhQKEglnNXI0zZrCgBEjR6om62lcVw" + latitude: 34.2611486 + longitude: -118.5010287 + status: OPEN + workersNeeded: 1 + filled: 0 + } + ) + shift_19: shift_insert( + data: { + id: "0f451a6b-610f-4b50-8617-d8b668227ec7" + title: "Saturday Event Support Shift" + orderId: "60307e4b-d9d8-4cd1-9516-8c52227072da" + date: "2026-02-07T05:00:00Z" + startTime: "2026-02-07T14:00:00Z" + endTime: "2026-02-07T22:00:00Z" + hours: 8 + cost: 320 + locationAddress: "5000 San Jose Street, Granada Hills, CA, USA" + city: "Los Angeles" + state: "CA" + street: "San Jose Street" + country: "US" + placeId: "Eiw1MDAwIFNhbiBKb3NlIFN0cmVldCwgR3JhbmFkYSBIaWxscywgQ0EsIFVTQSIuKiwKFAoSCYNJZBTdmsKAEddGOfBj8LvTEhQKEglnNXI0zZrCgBEjR6om62lcVw" + latitude: 34.2611486 + longitude: -118.5010287 + status: ASSIGNED + workersNeeded: 2 + filled: 1 + } + ) + shift_20: shift_insert( + data: { + id: "5f70a60f-283d-4cb6-a666-ae2691f46ddc" + title: "Sunday Security Patrol Shift" + orderId: "700d75e6-4ad8-4ed2-8c52-4f23e0a1bd4c" + date: "2026-02-08T05:00:00Z" + startTime: "2026-02-08T14:00:00Z" + endTime: "2026-02-08T22:00:00Z" + hours: 8 + cost: 224 + locationAddress: "4000 San Jose Street, Granada Hills, CA, USA" + city: "Los Angeles" + state: "CA" + street: "San Jose Street" + country: "US" + placeId: "Eiw0MDAwIFNhbiBKb3NlIFN0cmVldCwgR3JhbmFkYSBIaWxscywgQ0EsIFVTQSIuKiwKFAoSCYNJZBTdmsKAEddGOfBj8LvTEhQKEglnNXI0zZrCgBEjR6om62lcVw" + latitude: 34.2611486 + longitude: -118.5010287 + status: OPEN + workersNeeded: 1 + filled: 0 + } + ) + + # Shift Roles (1 per shift) + shift_role_01: shiftRole_insert( + data: { + id: "29b997e3-8d76-4031-ac0b-c6cb85c9dda0" + shiftId: "97475714-44d9-4a52-8486-672977689bc0" + roleId: "e51f3553-f2ee-400b-91e6-92b534239697" + count: 1 + assigned: 1 + startTime: "2026-01-26T14:00:00Z" + endTime: "2026-01-26T22:00:00Z" + hours: 8 + breakType: MIN_30 + totalValue: 192 + } + ) + shift_role_02: shiftRole_insert( + data: { + id: "6c72edc0-2bb5-45e2-b38a-f17685b243ad" + shiftId: "50493c45-ad80-4a7b-993b-cd54a9bb1cbf" + roleId: "7de956ce-743b-4271-b826-73313a5f07f5" + count: 1 + assigned: 1 + startTime: "2026-01-26T14:00:00Z" + endTime: "2026-01-26T22:00:00Z" + hours: 8 + breakType: MIN_30 + totalValue: 208 + } + ) + shift_role_03: shiftRole_insert( + data: { + id: "74567266-347d-476a-83f4-e95b4f7cd25c" + shiftId: "05101aa0-48b5-4f6e-8327-3c3679fd59dd" + roleId: "73fdb09b-ecbd-402e-8eb4-e7d79237d017" + count: 1 + assigned: 1 + startTime: "2026-01-27T14:00:00Z" + endTime: "2026-01-27T22:00:00Z" + hours: 8 + breakType: MIN_30 + totalValue: 160 + } + ) + shift_role_04: shiftRole_insert( + data: { + id: "6b07d4e3-e9f2-4d6c-8aef-31668d834ff0" + shiftId: "dafa7ede-5245-436c-af4a-1d1f20d68ab5" + roleId: "67ab1dcb-5b54-4dd9-aeb5-9cc58bdda0ed" + count: 1 + assigned: 1 + startTime: "2026-01-28T14:00:00Z" + endTime: "2026-01-28T22:00:00Z" + hours: 8 + breakType: MIN_30 + totalValue: 224 + } + ) + shift_role_05: shiftRole_insert( + data: { + id: "0e081523-a8a3-497d-8221-26ddad17c75a" + shiftId: "ab51c851-8d93-4a7c-907a-d768d6908b7f" + roleId: "e51f3553-f2ee-400b-91e6-92b534239697" + count: 1 + assigned: 1 + startTime: "2026-01-28T14:00:00Z" + endTime: "2026-01-28T22:00:00Z" + hours: 8 + breakType: MIN_30 + totalValue: 192 + } + ) + shift_role_06: shiftRole_insert( + data: { + id: "cfa2d60e-f96c-49e9-bd4d-a112ff01485c" + shiftId: "dbb94e32-7f51-4fd4-bfc9-148a90867437" + roleId: "7de956ce-743b-4271-b826-73313a5f07f5" + count: 1 + assigned: 1 + startTime: "2026-01-29T14:00:00Z" + endTime: "2026-01-29T22:00:00Z" + hours: 8 + breakType: MIN_30 + totalValue: 208 + } + ) + shift_role_07: shiftRole_insert( + data: { + id: "27481670-6f28-4d37-8b2d-8768f650c561" + shiftId: "7dc230cb-5680-4799-b45a-8a8269675a42" + roleId: "73fdb09b-ecbd-402e-8eb4-e7d79237d017" + count: 2 + assigned: 2 + startTime: "2026-01-30T14:00:00Z" + endTime: "2026-01-30T22:00:00Z" + hours: 8 + breakType: MIN_30 + totalValue: 320 + } + ) + shift_role_08: shiftRole_insert( + data: { + id: "2ddb7112-b9de-41b6-9637-48f12c7cf63e" + shiftId: "5e4cc4e0-51a4-406b-82cd-39bfa3a0970a" + roleId: "67ab1dcb-5b54-4dd9-aeb5-9cc58bdda0ed" + count: 1 + assigned: 1 + startTime: "2026-01-30T14:00:00Z" + endTime: "2026-01-30T22:00:00Z" + hours: 8 + breakType: MIN_30 + totalValue: 224 + } + ) + shift_role_09: shiftRole_insert( + data: { + id: "25718f64-ae53-4c28-813a-26d6af1bb533" + shiftId: "07be57d0-a580-46b7-b98e-1e29249cff63" + roleId: "e51f3553-f2ee-400b-91e6-92b534239697" + count: 1 + assigned: 1 + startTime: "2026-01-31T14:00:00Z" + endTime: "2026-01-31T22:00:00Z" + hours: 8 + breakType: MIN_30 + totalValue: 192 + } + ) + shift_role_10: shiftRole_insert( + data: { + id: "944bc40d-bdab-44e7-8ca9-c4ec23f235cb" + shiftId: "845b366b-3ec6-4322-b3cf-b386e6ad8ba5" + roleId: "7de956ce-743b-4271-b826-73313a5f07f5" + count: 2 + assigned: 2 + startTime: "2026-02-01T14:00:00Z" + endTime: "2026-02-01T22:00:00Z" + hours: 8 + breakType: MIN_30 + totalValue: 416 + } + ) + shift_role_11: shiftRole_insert( + data: { + id: "443052d5-d0c7-4948-8607-e42520a6d069" + shiftId: "940ae08f-a9f4-4fe4-ab8b-a219c53f3bfa" + roleId: "73fdb09b-ecbd-402e-8eb4-e7d79237d017" + count: 2 + assigned: 2 + startTime: "2026-02-02T14:00:00Z" + endTime: "2026-02-02T22:00:00Z" + hours: 8 + breakType: MIN_30 + totalValue: 320 + } + ) + shift_role_12: shiftRole_insert( + data: { + id: "bdd79b68-f4ab-4039-b7b0-c89e3a29bb9a" + shiftId: "738cd678-9179-4360-bf24-426700651a37" + roleId: "67ab1dcb-5b54-4dd9-aeb5-9cc58bdda0ed" + count: 1 + assigned: 1 + startTime: "2026-02-02T14:00:00Z" + endTime: "2026-02-02T22:00:00Z" + hours: 8 + breakType: MIN_30 + totalValue: 224 + } + ) + shift_role_13: shiftRole_insert( + data: { + id: "59ce3054-ac51-44bd-9b67-1fb9ffc01c79" + shiftId: "c08dd45c-ce93-4f98-948a-5ba6a8f15296" + roleId: "e51f3553-f2ee-400b-91e6-92b534239697" + count: 1 + assigned: 0 + startTime: "2026-02-03T14:00:00Z" + endTime: "2026-02-03T22:00:00Z" + hours: 8 + breakType: MIN_30 + totalValue: 192 + } + ) + shift_role_14: shiftRole_insert( + data: { + id: "7731be5a-780f-4fed-8bc4-963d84a8f14f" + shiftId: "38b194b2-55f4-4af7-991d-38d46c95916c" + roleId: "7de956ce-743b-4271-b826-73313a5f07f5" + count: 2 + assigned: 1 + startTime: "2026-02-04T14:00:00Z" + endTime: "2026-02-04T22:00:00Z" + hours: 8 + breakType: MIN_30 + totalValue: 416 + } + ) + shift_role_15: shiftRole_insert( + data: { + id: "8a9ca09f-fe02-4a31-aba3-8920da941bcc" + shiftId: "9cdd54c7-7e48-4149-bb79-0cd142550328" + roleId: "73fdb09b-ecbd-402e-8eb4-e7d79237d017" + count: 2 + assigned: 1 + startTime: "2026-02-04T14:00:00Z" + endTime: "2026-02-04T22:00:00Z" + hours: 8 + breakType: MIN_30 + totalValue: 320 + } + ) + shift_role_16: shiftRole_insert( + data: { + id: "184be03d-257f-4e6b-b796-a9d0da89b2cc" + shiftId: "96896f2c-525f-4a71-980a-843007b6115b" + roleId: "67ab1dcb-5b54-4dd9-aeb5-9cc58bdda0ed" + count: 1 + assigned: 0 + startTime: "2026-02-05T14:00:00Z" + endTime: "2026-02-05T22:00:00Z" + hours: 8 + breakType: MIN_30 + totalValue: 224 + } + ) + shift_role_17: shiftRole_insert( + data: { + id: "0ae7fa52-ffea-43b7-a2a5-03c5a7cc0c4f" + shiftId: "1cd2e3d1-42d5-4c04-8778-171d599fe157" + roleId: "e51f3553-f2ee-400b-91e6-92b534239697" + count: 2 + assigned: 1 + startTime: "2026-02-06T14:00:00Z" + endTime: "2026-02-06T22:00:00Z" + hours: 8 + breakType: MIN_30 + totalValue: 384 + } + ) + shift_role_18: shiftRole_insert( + data: { + id: "812b9b83-2913-4d59-92d9-e110b4f4c0ad" + shiftId: "fa6e2567-bbcc-4eee-a4ac-16cca06283ad" + roleId: "7de956ce-743b-4271-b826-73313a5f07f5" + count: 1 + assigned: 0 + startTime: "2026-02-06T14:00:00Z" + endTime: "2026-02-06T22:00:00Z" + hours: 8 + breakType: MIN_30 + totalValue: 208 + } + ) + shift_role_19: shiftRole_insert( + data: { + id: "fb27127e-7162-43ec-a98d-220517f5c326" + shiftId: "0f451a6b-610f-4b50-8617-d8b668227ec7" + roleId: "73fdb09b-ecbd-402e-8eb4-e7d79237d017" + count: 2 + assigned: 1 + startTime: "2026-02-07T14:00:00Z" + endTime: "2026-02-07T22:00:00Z" + hours: 8 + breakType: MIN_30 + totalValue: 320 + } + ) + shift_role_20: shiftRole_insert( + data: { + id: "360616bf-8083-4dff-8d22-82380304d838" + shiftId: "5f70a60f-283d-4cb6-a666-ae2691f46ddc" + roleId: "67ab1dcb-5b54-4dd9-aeb5-9cc58bdda0ed" + count: 1 + assigned: 0 + startTime: "2026-02-08T14:00:00Z" + endTime: "2026-02-08T22:00:00Z" + hours: 8 + breakType: MIN_30 + totalValue: 224 + } + ) + + # Applications + application_01: application_insert( + data: { + id: "89f99e27-999b-41e4-a8d8-c918759a5638" + shiftId: "97475714-44d9-4a52-8486-672977689bc0" + staffId: "633df3ce-b92c-473f-90d8-38dd027fdf57" + roleId: "e51f3553-f2ee-400b-91e6-92b534239697" + status: COMPLETED + origin: STAFF + } + ) + application_02: application_insert( + data: { + id: "fc772ef9-eb2c-4f03-a594-7e439b6ca74e" + shiftId: "50493c45-ad80-4a7b-993b-cd54a9bb1cbf" + staffId: "9631581a-1601-4e06-8e5e-600e9f305bcf" + roleId: "7de956ce-743b-4271-b826-73313a5f07f5" + status: COMPLETED + origin: STAFF + } + ) + application_03: application_insert( + data: { + id: "a8090a7c-56ca-4164-9f1f-1c3ed9aa80de" + shiftId: "05101aa0-48b5-4f6e-8327-3c3679fd59dd" + staffId: "2b678a6d-b8cd-4d5e-95ae-f35e4569f92c" + roleId: "73fdb09b-ecbd-402e-8eb4-e7d79237d017" + status: COMPLETED + origin: STAFF + } + ) + application_04: application_insert( + data: { + id: "245c496f-19f7-4a6a-a913-2b741f998c14" + shiftId: "dafa7ede-5245-436c-af4a-1d1f20d68ab5" + staffId: "d62605f9-366d-42c5-8f3b-f276c0d27ea3" + roleId: "67ab1dcb-5b54-4dd9-aeb5-9cc58bdda0ed" + status: COMPLETED + origin: STAFF + } + ) + application_05: application_insert( + data: { + id: "b28c4cd4-372a-43b2-9b27-13afec1be3a0" + shiftId: "ab51c851-8d93-4a7c-907a-d768d6908b7f" + staffId: "c6428f90-9c29-4e5c-b362-dc67a9a8cbba" + roleId: "e51f3553-f2ee-400b-91e6-92b534239697" + status: COMPLETED + origin: STAFF + } + ) + application_06: application_insert( + data: { + id: "0ec8cf17-d56b-4d19-bda5-3e5e1aa86c3f" + shiftId: "dbb94e32-7f51-4fd4-bfc9-148a90867437" + staffId: "56d7178c-f4ab-4c50-9b1f-d6efe25ba50b" + roleId: "7de956ce-743b-4271-b826-73313a5f07f5" + status: COMPLETED + origin: STAFF + } + ) + application_07: application_insert( + data: { + id: "e59efae5-5fda-4a45-b26a-608ccd014c8f" + shiftId: "7dc230cb-5680-4799-b45a-8a8269675a42" + staffId: "633df3ce-b92c-473f-90d8-38dd027fdf57" + roleId: "73fdb09b-ecbd-402e-8eb4-e7d79237d017" + status: COMPLETED + origin: STAFF + } + ) + application_08: application_insert( + data: { + id: "37259af7-27b9-48d5-b762-3ce8abf61316" + shiftId: "845b366b-3ec6-4322-b3cf-b386e6ad8ba5" + staffId: "9631581a-1601-4e06-8e5e-600e9f305bcf" + roleId: "7de956ce-743b-4271-b826-73313a5f07f5" + status: COMPLETED + origin: STAFF + } + ) + application_09: application_insert( + data: { + id: "7bc24537-2a03-4ac2-a6d8-2f3441c479af" + shiftId: "940ae08f-a9f4-4fe4-ab8b-a219c53f3bfa" + staffId: "2b678a6d-b8cd-4d5e-95ae-f35e4569f92c" + roleId: "73fdb09b-ecbd-402e-8eb4-e7d79237d017" + status: COMPLETED + origin: STAFF + } + ) + application_10: application_insert( + data: { + id: "a6d76379-7634-4bee-a3c2-9e8b81fae6ac" + shiftId: "38b194b2-55f4-4af7-991d-38d46c95916c" + staffId: "d62605f9-366d-42c5-8f3b-f276c0d27ea3" + roleId: "7de956ce-743b-4271-b826-73313a5f07f5" + status: CONFIRMED + origin: STAFF + } + ) + application_11: application_insert( + data: { + id: "8ece3010-2da7-4bda-a97d-fa4bd5113760" + shiftId: "9cdd54c7-7e48-4149-bb79-0cd142550328" + staffId: "c6428f90-9c29-4e5c-b362-dc67a9a8cbba" + roleId: "73fdb09b-ecbd-402e-8eb4-e7d79237d017" + status: CONFIRMED + origin: STAFF + } + ) + application_12: application_insert( + data: { + id: "da453bf7-a25d-462b-930c-f0a490e29890" + shiftId: "1cd2e3d1-42d5-4c04-8778-171d599fe157" + staffId: "56d7178c-f4ab-4c50-9b1f-d6efe25ba50b" + roleId: "e51f3553-f2ee-400b-91e6-92b534239697" + status: CONFIRMED + origin: STAFF + } + ) + application_13: application_insert( + data: { + id: "661e1078-aa64-4188-b438-5088b3dfb75a" + shiftId: "0f451a6b-610f-4b50-8617-d8b668227ec7" + staffId: "633df3ce-b92c-473f-90d8-38dd027fdf57" + roleId: "73fdb09b-ecbd-402e-8eb4-e7d79237d017" + status: CONFIRMED + origin: STAFF + } + ) + application_14: application_insert( + data: { + id: "f5a68adc-6bd3-4fe2-b156-09375c5761e5" + shiftId: "7dc230cb-5680-4799-b45a-8a8269675a42" + staffId: "d62605f9-366d-42c5-8f3b-f276c0d27ea3" + roleId: "73fdb09b-ecbd-402e-8eb4-e7d79237d017" + status: COMPLETED + origin: STAFF + } + ) + application_15: application_insert( + data: { + id: "89a62213-06b3-49fd-8ed6-54baa595862f" + shiftId: "5e4cc4e0-51a4-406b-82cd-39bfa3a0970a" + staffId: "9631581a-1601-4e06-8e5e-600e9f305bcf" + roleId: "67ab1dcb-5b54-4dd9-aeb5-9cc58bdda0ed" + status: COMPLETED + origin: STAFF + } + ) + application_16: application_insert( + data: { + id: "beb9770e-2e1c-41d7-80bf-4a2f6acb33d3" + shiftId: "07be57d0-a580-46b7-b98e-1e29249cff63" + staffId: "2b678a6d-b8cd-4d5e-95ae-f35e4569f92c" + roleId: "e51f3553-f2ee-400b-91e6-92b534239697" + status: COMPLETED + origin: STAFF + } + ) + application_17: application_insert( + data: { + id: "94578e49-9ecb-475c-825b-6bf5a4642f13" + shiftId: "845b366b-3ec6-4322-b3cf-b386e6ad8ba5" + staffId: "c6428f90-9c29-4e5c-b362-dc67a9a8cbba" + roleId: "7de956ce-743b-4271-b826-73313a5f07f5" + status: COMPLETED + origin: STAFF + } + ) + application_18: application_insert( + data: { + id: "22b93790-36a6-405c-b0c7-546d2cfd4411" + shiftId: "940ae08f-a9f4-4fe4-ab8b-a219c53f3bfa" + staffId: "56d7178c-f4ab-4c50-9b1f-d6efe25ba50b" + roleId: "73fdb09b-ecbd-402e-8eb4-e7d79237d017" + status: COMPLETED + origin: STAFF + } + ) + application_19: application_insert( + data: { + id: "b8c4b723-346d-4bcd-9667-35944ba5dbbd" + shiftId: "738cd678-9179-4360-bf24-426700651a37" + staffId: "633df3ce-b92c-473f-90d8-38dd027fdf57" + roleId: "67ab1dcb-5b54-4dd9-aeb5-9cc58bdda0ed" + status: COMPLETED + origin: STAFF + } + ) + + # Invoices (for completed orders) + invoice_01: invoice_insert( + data: { + id: "16e27caa-9d1e-44de-afed-e7bd4546e35e" + status: PAID + vendorId: "c3b25c47-0ebd-4402-a9b1-b8a875a7f71a" + businessId: "ef69e942-d6e5-48e5-a8bc-69d3faa63b2f" + orderId: "0e3b8fbb-ffd7-496d-a20a-2375b9205f54" + paymentTerms: NET_30 + invoiceNumber: "INV-2026-0001" + issueDate: "2026-01-26T05:00:00Z" + dueDate: "2026-02-25T05:00:00Z" + amount: 192 + staffCount: 1 + chargesCount: 1 + } + ) + invoice_02: invoice_insert( + data: { + id: "fde8af05-374c-44ea-a5ed-75bc8088bd5f" + status: PAID + vendorId: "c3b25c47-0ebd-4402-a9b1-b8a875a7f71a" + businessId: "ef69e942-d6e5-48e5-a8bc-69d3faa63b2f" + orderId: "8927e7c7-7e99-400b-ba26-3e94d7039605" + paymentTerms: NET_30 + invoiceNumber: "INV-2026-0002" + issueDate: "2026-01-26T05:00:00Z" + dueDate: "2026-02-25T05:00:00Z" + amount: 208 + staffCount: 1 + chargesCount: 1 + } + ) + invoice_03: invoice_insert( + data: { + id: "ba0529be-7906-417f-8ec7-c866d0633fee" + status: PAID + vendorId: "c3b25c47-0ebd-4402-a9b1-b8a875a7f71a" + businessId: "ef69e942-d6e5-48e5-a8bc-69d3faa63b2f" + orderId: "8bb46c96-74cd-48d6-bbb1-287823376e30" + paymentTerms: NET_30 + invoiceNumber: "INV-2026-0003" + issueDate: "2026-01-27T05:00:00Z" + dueDate: "2026-02-26T05:00:00Z" + amount: 160 + staffCount: 1 + chargesCount: 1 + } + ) + invoice_04: invoice_insert( + data: { + id: "8cfdce8b-f794-454a-8c05-aa1b3af5dbc6" + status: APPROVED + vendorId: "c3b25c47-0ebd-4402-a9b1-b8a875a7f71a" + businessId: "ef69e942-d6e5-48e5-a8bc-69d3faa63b2f" + orderId: "83b7dd83-2223-4585-a75f-b247368ebfcb" + paymentTerms: NET_30 + invoiceNumber: "INV-2026-0004" + issueDate: "2026-01-28T05:00:00Z" + dueDate: "2026-02-27T05:00:00Z" + amount: 224 + staffCount: 1 + chargesCount: 1 + } + ) + invoice_05: invoice_insert( + data: { + id: "c473807f-f77c-4ea4-8ee0-dbd7430704b2" + status: APPROVED + vendorId: "c3b25c47-0ebd-4402-a9b1-b8a875a7f71a" + businessId: "ef69e942-d6e5-48e5-a8bc-69d3faa63b2f" + orderId: "1f7589f3-5bac-4174-82ed-844995ffb36e" + paymentTerms: NET_30 + invoiceNumber: "INV-2026-0005" + issueDate: "2026-01-28T05:00:00Z" + dueDate: "2026-02-27T05:00:00Z" + amount: 192 + staffCount: 1 + chargesCount: 1 + } + ) + invoice_06: invoice_insert( + data: { + id: "24826ae1-d18f-4b7b-9a1f-3a73aff11412" + status: APPROVED + vendorId: "c3b25c47-0ebd-4402-a9b1-b8a875a7f71a" + businessId: "ef69e942-d6e5-48e5-a8bc-69d3faa63b2f" + orderId: "df585e06-05f9-4859-865f-de23d8fa29fe" + paymentTerms: NET_30 + invoiceNumber: "INV-2026-0006" + issueDate: "2026-01-29T05:00:00Z" + dueDate: "2026-02-28T05:00:00Z" + amount: 208 + staffCount: 1 + chargesCount: 1 + } + ) + invoice_07: invoice_insert( + data: { + id: "2d7fd51e-b9ca-439a-abbd-c3bd382232eb" + status: APPROVED + vendorId: "c3b25c47-0ebd-4402-a9b1-b8a875a7f71a" + businessId: "ef69e942-d6e5-48e5-a8bc-69d3faa63b2f" + orderId: "c3c5dca6-c8f9-4948-bb8c-10d8129914b3" + paymentTerms: NET_30 + invoiceNumber: "INV-2026-0007" + issueDate: "2026-01-30T05:00:00Z" + dueDate: "2026-03-01T05:00:00Z" + amount: 320 + staffCount: 2 + chargesCount: 1 + } + ) + invoice_08: invoice_insert( + data: { + id: "dfc9ea8e-17fc-474e-9948-df14ed24cd79" + status: APPROVED + vendorId: "c3b25c47-0ebd-4402-a9b1-b8a875a7f71a" + businessId: "ef69e942-d6e5-48e5-a8bc-69d3faa63b2f" + orderId: "a1fe2d34-cd5e-4372-bd73-e220a1840e1d" + paymentTerms: NET_30 + invoiceNumber: "INV-2026-0008" + issueDate: "2026-01-30T05:00:00Z" + dueDate: "2026-03-01T05:00:00Z" + amount: 224 + staffCount: 1 + chargesCount: 1 + } + ) + invoice_09: invoice_insert( + data: { + id: "10a71d9a-4d35-476c-9f6a-d491e699b657" + status: APPROVED + vendorId: "c3b25c47-0ebd-4402-a9b1-b8a875a7f71a" + businessId: "ef69e942-d6e5-48e5-a8bc-69d3faa63b2f" + orderId: "858753bc-dfa3-46fd-b383-ecd38de40b05" + paymentTerms: NET_30 + invoiceNumber: "INV-2026-0009" + issueDate: "2026-01-31T05:00:00Z" + dueDate: "2026-03-02T05:00:00Z" + amount: 192 + staffCount: 1 + chargesCount: 1 + } + ) + invoice_10: invoice_insert( + data: { + id: "76d7647f-eb9d-4b3d-adb2-637be41123d2" + status: APPROVED + vendorId: "c3b25c47-0ebd-4402-a9b1-b8a875a7f71a" + businessId: "ef69e942-d6e5-48e5-a8bc-69d3faa63b2f" + orderId: "634386c5-45f3-46a0-a267-9971f0c19728" + paymentTerms: NET_30 + invoiceNumber: "INV-2026-0010" + issueDate: "2026-02-01T05:00:00Z" + dueDate: "2026-03-03T05:00:00Z" + amount: 416 + staffCount: 2 + chargesCount: 1 + } + ) + invoice_11: invoice_insert( + data: { + id: "43b63f62-105b-4de3-b59e-bd8c9f334417" + status: APPROVED + vendorId: "c3b25c47-0ebd-4402-a9b1-b8a875a7f71a" + businessId: "ef69e942-d6e5-48e5-a8bc-69d3faa63b2f" + orderId: "43d593ed-0c58-4675-ae12-34aa27cb0d0c" + paymentTerms: NET_30 + invoiceNumber: "INV-2026-0011" + issueDate: "2026-02-02T05:00:00Z" + dueDate: "2026-03-04T05:00:00Z" + amount: 320 + staffCount: 2 + chargesCount: 1 + } + ) + invoice_12: invoice_insert( + data: { + id: "c23f3ed2-7fa1-43f5-88e9-4227e34cb5eb" + status: APPROVED + vendorId: "c3b25c47-0ebd-4402-a9b1-b8a875a7f71a" + businessId: "ef69e942-d6e5-48e5-a8bc-69d3faa63b2f" + orderId: "7abf0183-a989-4c2a-b420-e959663da61b" + paymentTerms: NET_30 + invoiceNumber: "INV-2026-0012" + issueDate: "2026-02-02T05:00:00Z" + dueDate: "2026-03-04T05:00:00Z" + amount: 224 + staffCount: 1 + chargesCount: 1 + } + ) + + # Recent Payments (only for PAID invoices) + recent_payment_01: recentPayment_insert( + data: { + id: "2297f0e5-a99b-476c-9c65-69743ec7788f" + workedTime: "8h" + status: PAID + staffId: "633df3ce-b92c-473f-90d8-38dd027fdf57" + applicationId: "89f99e27-999b-41e4-a8d8-c918759a5638" + invoiceId: "16e27caa-9d1e-44de-afed-e7bd4546e35e" + } + ) + recent_payment_02: recentPayment_insert( + data: { + id: "949fbd9e-041b-405a-bba1-04216fa778b8" + workedTime: "8h" + status: PAID + staffId: "9631581a-1601-4e06-8e5e-600e9f305bcf" + applicationId: "fc772ef9-eb2c-4f03-a594-7e439b6ca74e" + invoiceId: "fde8af05-374c-44ea-a5ed-75bc8088bd5f" + } + ) + recent_payment_03: recentPayment_insert( + data: { + id: "4d45192e-34fe-4e07-a4f9-708e7591a9a5" + workedTime: "8h" + status: PAID + staffId: "2b678a6d-b8cd-4d5e-95ae-f35e4569f92c" + applicationId: "a8090a7c-56ca-4164-9f1f-1c3ed9aa80de" + invoiceId: "ba0529be-7906-417f-8ec7-c866d0633fee" + } + ) +} diff --git a/backend/dataconnect/seed_last.gql b/backend/dataconnect/seed_last.gql new file mode 100644 index 00000000..dbf5af3b --- /dev/null +++ b/backend/dataconnect/seed_last.gql @@ -0,0 +1,1897 @@ +mutation @transaction { + user_insert_1: user_insert( + data: { + id: "rU9P2kG7mQ4sV1xT6bN8hC3jZ0L5" + email: "vendor1@example.com" + fullName: "Seed Vendor 1" + role: USER + userRole: "vendor" + photoUrl: "https://i.pravatar.cc/150?u=rU9P2kG7mQ4sV1xT6bN8hC3jZ0L5" + } + ) + user_insert_2: user_insert( + data: { + id: "Y3kL9vT2sR8pQ6mN1wE4xC7zA0B5" + email: "staff2@example.com" + fullName: "Seed Staff 2" + role: USER + userRole: "staff" + photoUrl: "https://i.pravatar.cc/150?u=Y3kL9vT2sR8pQ6mN1wE4xC7zA0B5" + } + ) + user_insert_3: user_insert( + data: { + id: "kP7vR2mX9tQ4sL1nB6cE8zA3fD0H" + email: "staff3@example.com" + fullName: "Seed Staff 3" + role: USER + userRole: "staff" + photoUrl: "https://i.pravatar.cc/150?u=kP7vR2mX9tQ4sL1nB6cE8zA3fD0H" + } + ) + user_insert_4: user_insert( + data: { + id: "mT4sQ8nR1vP6kX2bL7cE9aD3zF0H" + email: "staff4@example.com" + fullName: "Seed Staff 4" + role: USER + userRole: "staff" + photoUrl: "https://i.pravatar.cc/150?u=mT4sQ8nR1vP6kX2bL7cE9aD3zF0H" + } + ) + user_insert_5: user_insert( + data: { + id: "sN2pQ6vT9kR4mX1bL8cE3zA7dF0H" + email: "staff5@example.com" + fullName: "Seed Staff 5" + role: USER + userRole: "staff" + photoUrl: "https://i.pravatar.cc/150?u=sN2pQ6vT9kR4mX1bL8cE3zA7dF0H" + } + ) + user_insert_6: user_insert( + data: { + id: "vR6mQ1tP8kN3xL2bE7cA9zD4fH0" + email: "staff6@example.com" + fullName: "Seed Staff 6" + role: USER + userRole: "staff" + photoUrl: "https://i.pravatar.cc/150?u=vR6mQ1tP8kN3xL2bE7cA9zD4fH0" + } + ) + user_insert_7: user_insert( + data: { + id: "Q8kR2mT5vP1nX9bL6cE3zA7dF0H" + email: "staff7@example.com" + fullName: "Seed Staff 7" + role: USER + userRole: "staff" + photoUrl: "https://i.pravatar.cc/150?u=Q8kR2mT5vP1nX9bL6cE3zA7dF0H" + } + ) + user_insert_8: user_insert( + data: { + id: "nP5vR9kT2mQ1xL8bE6cA3zD7fH0" + email: "staff8@example.com" + fullName: "Seed Staff 8" + role: USER + userRole: "staff" + photoUrl: "https://i.pravatar.cc/150?u=nP5vR9kT2mQ1xL8bE6cA3zD7fH0" + } + ) + user_insert_9: user_insert( + data: { + id: "tR1mQ6vP9kN2xL8bE3cA7zD4fH0" + email: "staff9@example.com" + fullName: "Seed Staff 9" + role: USER + userRole: "staff" + photoUrl: "https://i.pravatar.cc/150?u=tR1mQ6vP9kN2xL8bE3cA7zD4fH0" + } + ) + user_insert_10: user_insert( + data: { + id: "pQ2vR7kT1mN9xL6bE3cA8zD4fH0" + email: "staff10@example.com" + fullName: "Seed Staff 10" + role: USER + userRole: "staff" + photoUrl: "https://i.pravatar.cc/150?u=pQ2vR7kT1mN9xL6bE3cA8zD4fH0" + } + ) + roleCategory_insert_1: roleCategory_insert(data: { id: "a9ca81d8-dd76-4576-943a-af7576968764", roleName: "Kitchen", category: KITCHEN_AND_CULINARY }) + roleCategory_insert_2: roleCategory_insert(data: { id: "e131e670-bedf-4c20-8ec6-cb80821b3b15", roleName: "Security", category: SECURITY }) + vendor_insert_new: vendor_insert( + data: { + id: "32adf31b-8a8f-49be-ace4-50402daa8046" + userId: "rU9P2kG7mQ4sV1xT6bN8hC3jZ0L5" + companyName: "Seed Vendor C" + email: "vendorc@example.com" + phone: "555-0101" + approvalStatus: APPROVED + isActive: true + tier: STANDARD + region: "CA" + city: "Los Angeles" + } + ) + role_insert_c_1: role_insert( + data: { + id: "a6cee403-e3fa-4295-a6d1-623e7e2e18f7" + name: "Line Cook" + costPerHour: 21.0 + vendorId: "32adf31b-8a8f-49be-ace4-50402daa8046" + roleCategoryId: "a9ca81d8-dd76-4576-943a-af7576968764" + } + ) + role_insert_c_2: role_insert( + data: { + id: "c1f6b67f-f9ef-45b5-8dcb-bd32b4295fcb" + name: "Security" + costPerHour: 24.0 + vendorId: "32adf31b-8a8f-49be-ace4-50402daa8046" + roleCategoryId: "e131e670-bedf-4c20-8ec6-cb80821b3b15" + } + ) + staff_insert_1: staff_insert( + data: { + id: "1e45b730-9c45-43a2-bda3-9b2e9d22b019" + userId: "Y3kL9vT2sR8pQ6mN1wE4xC7zA0B5" + fullName: "Ava Morales" + ownerId: "afa9ef70-fd90-493b-95b0-bc05791c514f" + totalShifts: 0 + averageRating: 4.5 + onTimeRate: 90 + noShowCount: 0 + cancellationCount: 0 + reliabilityScore: 90 + englishRequired: false + backgroundCheckStatus: CLEARED + employmentType: PART_TIME + english: FLUENT + isRecommended: false + } + ) + staff_insert_2: staff_insert( + data: { + id: "15ca4017-4759-4db2-b68b-0f0aba860807" + userId: "kP7vR2mX9tQ4sL1nB6cE8zA3fD0H" + fullName: "Luis Herrera" + ownerId: "afa9ef70-fd90-493b-95b0-bc05791c514f" + totalShifts: 0 + averageRating: 4.5 + onTimeRate: 90 + noShowCount: 0 + cancellationCount: 0 + reliabilityScore: 90 + englishRequired: false + backgroundCheckStatus: CLEARED + employmentType: PART_TIME + english: FLUENT + isRecommended: false + } + ) + staff_insert_3: staff_insert( + data: { + id: "273c47c5-ae9e-46e3-885c-553a1600263b" + userId: "mT4sQ8nR1vP6kX2bL7cE9aD3zF0H" + fullName: "Sofia Vega" + ownerId: "afa9ef70-fd90-493b-95b0-bc05791c514f" + totalShifts: 0 + averageRating: 4.5 + onTimeRate: 90 + noShowCount: 0 + cancellationCount: 0 + reliabilityScore: 90 + englishRequired: false + backgroundCheckStatus: CLEARED + employmentType: PART_TIME + english: FLUENT + isRecommended: false + } + ) + staff_insert_4: staff_insert( + data: { + id: "bacfd25e-b5b0-463b-bce8-fad1aff2d9bb" + userId: "sN2pQ6vT9kR4mX1bL8cE3zA7dF0H" + fullName: "Noah King" + ownerId: "b959b8d6-9133-4025-b3d2-a4c1564f9a25" + totalShifts: 0 + averageRating: 4.5 + onTimeRate: 90 + noShowCount: 0 + cancellationCount: 0 + reliabilityScore: 90 + englishRequired: false + backgroundCheckStatus: CLEARED + employmentType: PART_TIME + english: FLUENT + isRecommended: false + } + ) + staff_insert_5: staff_insert( + data: { + id: "44c79ae8-d759-41e0-94d7-ae60b504d65c" + userId: "vR6mQ1tP8kN3xL2bE7cA9zD4fH0" + fullName: "Emma Reed" + ownerId: "b959b8d6-9133-4025-b3d2-a4c1564f9a25" + totalShifts: 0 + averageRating: 4.5 + onTimeRate: 90 + noShowCount: 0 + cancellationCount: 0 + reliabilityScore: 90 + englishRequired: false + backgroundCheckStatus: CLEARED + employmentType: PART_TIME + english: FLUENT + isRecommended: false + } + ) + staff_insert_6: staff_insert( + data: { + id: "e168945c-0ede-4e72-bd47-683e02b55188" + userId: "Q8kR2mT5vP1nX9bL6cE3zA7dF0H" + fullName: "Diego Cruz" + ownerId: "b959b8d6-9133-4025-b3d2-a4c1564f9a25" + totalShifts: 0 + averageRating: 4.5 + onTimeRate: 90 + noShowCount: 0 + cancellationCount: 0 + reliabilityScore: 90 + englishRequired: false + backgroundCheckStatus: CLEARED + employmentType: PART_TIME + english: FLUENT + isRecommended: false + } + ) + staff_insert_7: staff_insert( + data: { + id: "c5801579-2472-4ddb-b81b-1af656c85e73" + userId: "nP5vR9kT2mQ1xL8bE6cA3zD7fH0" + fullName: "Maya Patel" + ownerId: "32adf31b-8a8f-49be-ace4-50402daa8046" + totalShifts: 0 + averageRating: 4.5 + onTimeRate: 90 + noShowCount: 0 + cancellationCount: 0 + reliabilityScore: 90 + englishRequired: false + backgroundCheckStatus: CLEARED + employmentType: PART_TIME + english: FLUENT + isRecommended: false + } + ) + staff_insert_8: staff_insert( + data: { + id: "24421973-9a75-48dd-97d5-a931851abc97" + userId: "tR1mQ6vP9kN2xL8bE3cA7zD4fH0" + fullName: "Ethan Brooks" + ownerId: "32adf31b-8a8f-49be-ace4-50402daa8046" + totalShifts: 0 + averageRating: 4.5 + onTimeRate: 90 + noShowCount: 0 + cancellationCount: 0 + reliabilityScore: 90 + englishRequired: false + backgroundCheckStatus: CLEARED + employmentType: PART_TIME + english: FLUENT + isRecommended: false + } + ) + staff_insert_9: staff_insert( + data: { + id: "6fe2f40c-717d-4fbe-954d-4eb1e8dfc129" + userId: "pQ2vR7kT1mN9xL6bE3cA8zD4fH0" + fullName: "Lina Chen" + ownerId: "32adf31b-8a8f-49be-ace4-50402daa8046" + totalShifts: 0 + averageRating: 4.5 + onTimeRate: 90 + noShowCount: 0 + cancellationCount: 0 + reliabilityScore: 90 + englishRequired: false + backgroundCheckStatus: CLEARED + employmentType: PART_TIME + english: FLUENT + isRecommended: false + } + ) + order_insert_1: order_insert( + data: { + id: "dccde341-3d23-4587-8037-188963f5ae26" + vendorId: "afa9ef70-fd90-493b-95b0-bc05791c514f" + businessId: "c5d0f18b-ba95-479b-8915-1bd498fd6860" + orderType: ONE_TIME + status: COMPLETED + date: "2026-01-26T05:00:00Z" + eventName: "Event 1" + requested: 2 + teamHubId: "9c8eb9c6-c186-4d55-877e-35be852c3e86" + total: 400.0 + } + ) + order_insert_2: order_insert( + data: { + id: "e2825341-d6dd-4dff-9ee0-12332549c3d6" + vendorId: "b959b8d6-9133-4025-b3d2-a4c1564f9a25" + businessId: "c5d0f18b-ba95-479b-8915-1bd498fd6860" + orderType: ONE_TIME + status: COMPLETED + date: "2026-01-26T05:00:00Z" + eventName: "Event 2" + requested: 1 + teamHubId: "75c70d83-1680-4b28-ab61-2fe64a74fc5f" + total: 200.0 + } + ) + order_insert_3: order_insert( + data: { + id: "19798ac3-adb6-4c77-9fd1-026d3af8138d" + vendorId: "32adf31b-8a8f-49be-ace4-50402daa8046" + businessId: "c5d0f18b-ba95-479b-8915-1bd498fd6860" + orderType: ONE_TIME + status: COMPLETED + date: "2026-01-27T05:00:00Z" + eventName: "Event 3" + requested: 2 + teamHubId: "22a0b119-e6dc-4011-9043-d857cd4c12f3" + total: 400.0 + } + ) + order_insert_4: order_insert( + data: { + id: "4ca2f85b-63ef-453f-b9e0-bc0c1a51817a" + vendorId: "afa9ef70-fd90-493b-95b0-bc05791c514f" + businessId: "c5d0f18b-ba95-479b-8915-1bd498fd6860" + orderType: ONE_TIME + status: COMPLETED + date: "2026-01-27T05:00:00Z" + eventName: "Event 4" + requested: 2 + teamHubId: "9c8eb9c6-c186-4d55-877e-35be852c3e86" + total: 400.0 + } + ) + order_insert_5: order_insert( + data: { + id: "62eed7f5-d8de-4146-bf4c-d4b04965b97d" + vendorId: "b959b8d6-9133-4025-b3d2-a4c1564f9a25" + businessId: "c5d0f18b-ba95-479b-8915-1bd498fd6860" + orderType: ONE_TIME + status: COMPLETED + date: "2026-01-28T05:00:00Z" + eventName: "Event 5" + requested: 1 + teamHubId: "75c70d83-1680-4b28-ab61-2fe64a74fc5f" + total: 200.0 + } + ) + order_insert_6: order_insert( + data: { + id: "170c1ff2-67ce-4527-83fd-c4722fc755b0" + vendorId: "32adf31b-8a8f-49be-ace4-50402daa8046" + businessId: "c5d0f18b-ba95-479b-8915-1bd498fd6860" + orderType: ONE_TIME + status: COMPLETED + date: "2026-01-29T05:00:00Z" + eventName: "Event 6" + requested: 2 + teamHubId: "22a0b119-e6dc-4011-9043-d857cd4c12f3" + total: 400.0 + } + ) + order_insert_7: order_insert( + data: { + id: "b507de75-e9ff-4372-9552-1f34a2161502" + vendorId: "afa9ef70-fd90-493b-95b0-bc05791c514f" + businessId: "c5d0f18b-ba95-479b-8915-1bd498fd6860" + orderType: ONE_TIME + status: PARTIAL_STAFFED + date: "2026-01-30T05:00:00Z" + eventName: "Event 7" + requested: 2 + teamHubId: "9c8eb9c6-c186-4d55-877e-35be852c3e86" + total: 400.0 + } + ) + order_insert_8: order_insert( + data: { + id: "69c2992a-773a-4987-9c6a-c44fbcde3142" + vendorId: "b959b8d6-9133-4025-b3d2-a4c1564f9a25" + businessId: "c5d0f18b-ba95-479b-8915-1bd498fd6860" + orderType: ONE_TIME + status: PARTIAL_STAFFED + date: "2026-01-30T05:00:00Z" + eventName: "Event 8" + requested: 2 + teamHubId: "75c70d83-1680-4b28-ab61-2fe64a74fc5f" + total: 400.0 + } + ) + order_insert_9: order_insert( + data: { + id: "64899806-9d73-43bc-b3ee-59016cb581ae" + vendorId: "32adf31b-8a8f-49be-ace4-50402daa8046" + businessId: "c5d0f18b-ba95-479b-8915-1bd498fd6860" + orderType: ONE_TIME + status: POSTED + date: "2026-01-31T05:00:00Z" + eventName: "Event 9" + requested: 2 + teamHubId: "22a0b119-e6dc-4011-9043-d857cd4c12f3" + total: 400.0 + } + ) + order_insert_10: order_insert( + data: { + id: "00717d39-aadc-4e66-aad6-00580176ca1f" + vendorId: "afa9ef70-fd90-493b-95b0-bc05791c514f" + businessId: "c5d0f18b-ba95-479b-8915-1bd498fd6860" + orderType: ONE_TIME + status: POSTED + date: "2026-02-01T05:00:00Z" + eventName: "Event 10" + requested: 2 + teamHubId: "9c8eb9c6-c186-4d55-877e-35be852c3e86" + total: 400.0 + } + ) + order_insert_11: order_insert( + data: { + id: "e7633cc7-8c0e-4eac-b02a-975db83f23ee" + vendorId: "b959b8d6-9133-4025-b3d2-a4c1564f9a25" + businessId: "c5d0f18b-ba95-479b-8915-1bd498fd6860" + orderType: ONE_TIME + status: PARTIAL_STAFFED + date: "2026-02-02T05:00:00Z" + eventName: "Event 11" + requested: 3 + teamHubId: "75c70d83-1680-4b28-ab61-2fe64a74fc5f" + total: 600.0 + } + ) + order_insert_12: order_insert( + data: { + id: "370c65c4-d470-4d06-b9e6-35d2010ea6d8" + vendorId: "32adf31b-8a8f-49be-ace4-50402daa8046" + businessId: "c5d0f18b-ba95-479b-8915-1bd498fd6860" + orderType: ONE_TIME + status: PARTIAL_STAFFED + date: "2026-02-02T05:00:00Z" + eventName: "Event 12" + requested: 3 + teamHubId: "22a0b119-e6dc-4011-9043-d857cd4c12f3" + total: 600.0 + } + ) + order_insert_13: order_insert( + data: { + id: "cf41794b-fe23-46f6-a201-4ccb8a98c859" + vendorId: "afa9ef70-fd90-493b-95b0-bc05791c514f" + businessId: "c5d0f18b-ba95-479b-8915-1bd498fd6860" + orderType: ONE_TIME + status: POSTED + date: "2026-02-03T05:00:00Z" + eventName: "Event 13" + requested: 2 + teamHubId: "9c8eb9c6-c186-4d55-877e-35be852c3e86" + total: 400.0 + } + ) + order_insert_14: order_insert( + data: { + id: "92667bc1-2b5f-425d-9447-24b86b8526d3" + vendorId: "b959b8d6-9133-4025-b3d2-a4c1564f9a25" + businessId: "c5d0f18b-ba95-479b-8915-1bd498fd6860" + orderType: ONE_TIME + status: PARTIAL_STAFFED + date: "2026-02-04T05:00:00Z" + eventName: "Event 14" + requested: 3 + teamHubId: "75c70d83-1680-4b28-ab61-2fe64a74fc5f" + total: 600.0 + } + ) + order_insert_15: order_insert( + data: { + id: "666f9195-0e39-47c9-9b47-953eac28b30d" + vendorId: "32adf31b-8a8f-49be-ace4-50402daa8046" + businessId: "c5d0f18b-ba95-479b-8915-1bd498fd6860" + orderType: ONE_TIME + status: POSTED + date: "2026-02-05T05:00:00Z" + eventName: "Event 15" + requested: 2 + teamHubId: "22a0b119-e6dc-4011-9043-d857cd4c12f3" + total: 400.0 + } + ) + order_insert_16: order_insert( + data: { + id: "13ec91cd-fbb5-4a39-a993-1c238c00761f" + vendorId: "afa9ef70-fd90-493b-95b0-bc05791c514f" + businessId: "c5d0f18b-ba95-479b-8915-1bd498fd6860" + orderType: ONE_TIME + status: POSTED + date: "2026-02-06T05:00:00Z" + eventName: "Event 16" + requested: 2 + teamHubId: "9c8eb9c6-c186-4d55-877e-35be852c3e86" + total: 400.0 + } + ) + order_insert_17: order_insert( + data: { + id: "32ec8f3e-3e07-43c6-8775-4afba7d7f09b" + vendorId: "b959b8d6-9133-4025-b3d2-a4c1564f9a25" + businessId: "c5d0f18b-ba95-479b-8915-1bd498fd6860" + orderType: ONE_TIME + status: POSTED + date: "2026-02-06T05:00:00Z" + eventName: "Event 17" + requested: 2 + teamHubId: "75c70d83-1680-4b28-ab61-2fe64a74fc5f" + total: 400.0 + } + ) + order_insert_18: order_insert( + data: { + id: "0d0e06a5-a03b-4a8f-b0f5-c44d8f41ccfc" + vendorId: "32adf31b-8a8f-49be-ace4-50402daa8046" + businessId: "c5d0f18b-ba95-479b-8915-1bd498fd6860" + orderType: ONE_TIME + status: POSTED + date: "2026-02-07T05:00:00Z" + eventName: "Event 18" + requested: 2 + teamHubId: "22a0b119-e6dc-4011-9043-d857cd4c12f3" + total: 400.0 + } + ) + order_insert_19: order_insert( + data: { + id: "8a90077e-e45b-42dd-aec0-fe8289f43708" + vendorId: "afa9ef70-fd90-493b-95b0-bc05791c514f" + businessId: "c5d0f18b-ba95-479b-8915-1bd498fd6860" + orderType: ONE_TIME + status: POSTED + date: "2026-02-07T05:00:00Z" + eventName: "Event 19" + requested: 2 + teamHubId: "9c8eb9c6-c186-4d55-877e-35be852c3e86" + total: 400.0 + } + ) + order_insert_20: order_insert( + data: { + id: "4bc87380-8388-4f27-bbe0-aa7f3b24620d" + vendorId: "b959b8d6-9133-4025-b3d2-a4c1564f9a25" + businessId: "c5d0f18b-ba95-479b-8915-1bd498fd6860" + orderType: ONE_TIME + status: PARTIAL_STAFFED + date: "2026-02-08T05:00:00Z" + eventName: "Event 20" + requested: 3 + teamHubId: "75c70d83-1680-4b28-ab61-2fe64a74fc5f" + total: 600.0 + } + ) + shift_insert_1: shift_insert( + data: { + id: "6ed269b0-770b-4d01-b694-ed298d6ea441" + title: "Shift 1" + orderId: "dccde341-3d23-4587-8037-188963f5ae26" + date: "2026-01-26T05:00:00Z" + startTime: "2026-01-26T14:00:00Z" + endTime: "2026-01-26T22:00:00Z" + hours: 8.0 + cost: 376.0 + status: COMPLETED + workersNeeded: 2 + filled: 2 + location: "Los Angeles" + locationAddress: "San Jose Street" + city: "Los Angeles" + state: "CA" + street: "San Jose Street" + country: "US" + } + ) + shift_insert_2: shift_insert( + data: { + id: "ba1343dc-56be-4887-88cf-9cd626fcfcb5" + title: "Shift 2" + orderId: "e2825341-d6dd-4dff-9ee0-12332549c3d6" + date: "2026-01-26T05:00:00Z" + startTime: "2026-01-26T14:00:00Z" + endTime: "2026-01-26T22:00:00Z" + hours: 8.0 + cost: 216.0 + status: COMPLETED + workersNeeded: 1 + filled: 1 + location: "Los Angeles" + locationAddress: "San Jose Street" + city: "Los Angeles" + state: "CA" + street: "San Jose Street" + country: "US" + } + ) + shift_insert_3: shift_insert( + data: { + id: "e3d006a1-c0b6-4cef-82ca-df157ee5462f" + title: "Shift 3" + orderId: "19798ac3-adb6-4c77-9fd1-026d3af8138d" + date: "2026-01-27T05:00:00Z" + startTime: "2026-01-27T14:00:00Z" + endTime: "2026-01-27T22:00:00Z" + hours: 8.0 + cost: 360.0 + status: COMPLETED + workersNeeded: 2 + filled: 2 + location: "Los Angeles" + locationAddress: "San Jose Street" + city: "Los Angeles" + state: "CA" + street: "San Jose Street" + country: "US" + } + ) + shift_insert_4: shift_insert( + data: { + id: "5991da4f-86cb-4a38-b654-01de5fc79616" + title: "Shift 4" + orderId: "4ca2f85b-63ef-453f-b9e0-bc0c1a51817a" + date: "2026-01-27T05:00:00Z" + startTime: "2026-01-27T14:00:00Z" + endTime: "2026-01-27T22:00:00Z" + hours: 8.0 + cost: 376.0 + status: COMPLETED + workersNeeded: 2 + filled: 2 + location: "Los Angeles" + locationAddress: "San Jose Street" + city: "Los Angeles" + state: "CA" + street: "San Jose Street" + country: "US" + } + ) + shift_insert_5: shift_insert( + data: { + id: "c1831485-fffa-494b-aaf0-1d83f0ccbd30" + title: "Shift 5" + orderId: "62eed7f5-d8de-4146-bf4c-d4b04965b97d" + date: "2026-01-28T05:00:00Z" + startTime: "2026-01-28T14:00:00Z" + endTime: "2026-01-28T22:00:00Z" + hours: 8.0 + cost: 216.0 + status: COMPLETED + workersNeeded: 1 + filled: 1 + location: "Los Angeles" + locationAddress: "San Jose Street" + city: "Los Angeles" + state: "CA" + street: "San Jose Street" + country: "US" + } + ) + shift_insert_6: shift_insert( + data: { + id: "a7fda158-4fef-4e96-8b26-f3cf95fa0a50" + title: "Shift 6" + orderId: "170c1ff2-67ce-4527-83fd-c4722fc755b0" + date: "2026-01-29T05:00:00Z" + startTime: "2026-01-29T14:00:00Z" + endTime: "2026-01-29T22:00:00Z" + hours: 8.0 + cost: 360.0 + status: COMPLETED + workersNeeded: 2 + filled: 2 + location: "Los Angeles" + locationAddress: "San Jose Street" + city: "Los Angeles" + state: "CA" + street: "San Jose Street" + country: "US" + } + ) + shift_insert_7: shift_insert( + data: { + id: "efaf6e1c-27e3-414f-93f0-cd65031dee08" + title: "Shift 7" + orderId: "b507de75-e9ff-4372-9552-1f34a2161502" + date: "2026-01-30T05:00:00Z" + startTime: "2026-01-30T14:00:00Z" + endTime: "2026-01-30T22:00:00Z" + hours: 8.0 + cost: 376.0 + status: IN_PROGRESS + workersNeeded: 2 + filled: 2 + location: "Los Angeles" + locationAddress: "San Jose Street" + city: "Los Angeles" + state: "CA" + street: "San Jose Street" + country: "US" + } + ) + shift_insert_8: shift_insert( + data: { + id: "65414c88-9655-40ab-9f6c-23e7e443ad8c" + title: "Shift 8" + orderId: "69c2992a-773a-4987-9c6a-c44fbcde3142" + date: "2026-01-30T05:00:00Z" + startTime: "2026-01-30T14:00:00Z" + endTime: "2026-01-30T22:00:00Z" + hours: 8.0 + cost: 472.0 + status: IN_PROGRESS + workersNeeded: 2 + filled: 2 + location: "Los Angeles" + locationAddress: "San Jose Street" + city: "Los Angeles" + state: "CA" + street: "San Jose Street" + country: "US" + } + ) + shift_insert_9: shift_insert( + data: { + id: "e92e0b29-80b1-48b9-a704-a72aae95180f" + title: "Shift 9" + orderId: "64899806-9d73-43bc-b3ee-59016cb581ae" + date: "2026-01-31T05:00:00Z" + startTime: "2026-01-31T14:00:00Z" + endTime: "2026-01-31T22:00:00Z" + hours: 8.0 + cost: 360.0 + status: OPEN + workersNeeded: 2 + filled: 0 + location: "Los Angeles" + locationAddress: "San Jose Street" + city: "Los Angeles" + state: "CA" + street: "San Jose Street" + country: "US" + } + ) + shift_insert_10: shift_insert( + data: { + id: "685cf551-2164-458a-8e87-fefa82dd3e90" + title: "Shift 10" + orderId: "00717d39-aadc-4e66-aad6-00580176ca1f" + date: "2026-02-01T05:00:00Z" + startTime: "2026-02-01T14:00:00Z" + endTime: "2026-02-01T22:00:00Z" + hours: 8.0 + cost: 376.0 + status: OPEN + workersNeeded: 2 + filled: 0 + location: "Los Angeles" + locationAddress: "San Jose Street" + city: "Los Angeles" + state: "CA" + street: "San Jose Street" + country: "US" + } + ) + shift_insert_11: shift_insert( + data: { + id: "b51ca5e8-3f83-4937-bbee-8fae228d18cf" + title: "Shift 11" + orderId: "e7633cc7-8c0e-4eac-b02a-975db83f23ee" + date: "2026-02-02T05:00:00Z" + startTime: "2026-02-02T14:00:00Z" + endTime: "2026-02-02T22:00:00Z" + hours: 8.0 + cost: 688.0 + status: OPEN + workersNeeded: 3 + filled: 2 + location: "Los Angeles" + locationAddress: "San Jose Street" + city: "Los Angeles" + state: "CA" + street: "San Jose Street" + country: "US" + } + ) + shift_insert_12: shift_insert( + data: { + id: "d3260cd9-1b15-4c0e-9c7f-b1b0fc5da2ff" + title: "Shift 12" + orderId: "370c65c4-d470-4d06-b9e6-35d2010ea6d8" + date: "2026-02-02T05:00:00Z" + startTime: "2026-02-02T14:00:00Z" + endTime: "2026-02-02T22:00:00Z" + hours: 8.0 + cost: 528.0 + status: OPEN + workersNeeded: 3 + filled: 2 + location: "Los Angeles" + locationAddress: "San Jose Street" + city: "Los Angeles" + state: "CA" + street: "San Jose Street" + country: "US" + } + ) + shift_insert_13: shift_insert( + data: { + id: "fe82461f-8e87-4ac4-a40c-d34d9ddf34d2" + title: "Shift 13" + orderId: "cf41794b-fe23-46f6-a201-4ccb8a98c859" + date: "2026-02-03T05:00:00Z" + startTime: "2026-02-03T14:00:00Z" + endTime: "2026-02-03T22:00:00Z" + hours: 8.0 + cost: 376.0 + status: OPEN + workersNeeded: 2 + filled: 0 + location: "Los Angeles" + locationAddress: "San Jose Street" + city: "Los Angeles" + state: "CA" + street: "San Jose Street" + country: "US" + } + ) + shift_insert_14: shift_insert( + data: { + id: "001be679-3e96-464a-a1a1-696ac9677fda" + title: "Shift 14" + orderId: "92667bc1-2b5f-425d-9447-24b86b8526d3" + date: "2026-02-04T05:00:00Z" + startTime: "2026-02-04T14:00:00Z" + endTime: "2026-02-04T22:00:00Z" + hours: 8.0 + cost: 688.0 + status: OPEN + workersNeeded: 3 + filled: 2 + location: "Los Angeles" + locationAddress: "San Jose Street" + city: "Los Angeles" + state: "CA" + street: "San Jose Street" + country: "US" + } + ) + shift_insert_15: shift_insert( + data: { + id: "f52a4c9b-c75d-42e2-9310-7c51a0224bff" + title: "Shift 15" + orderId: "666f9195-0e39-47c9-9b47-953eac28b30d" + date: "2026-02-05T05:00:00Z" + startTime: "2026-02-05T14:00:00Z" + endTime: "2026-02-05T22:00:00Z" + hours: 8.0 + cost: 360.0 + status: OPEN + workersNeeded: 2 + filled: 0 + location: "Los Angeles" + locationAddress: "San Jose Street" + city: "Los Angeles" + state: "CA" + street: "San Jose Street" + country: "US" + } + ) + shift_insert_16: shift_insert( + data: { + id: "081eaceb-938a-4ccf-affa-7c1c8d7c6204" + title: "Shift 16" + orderId: "13ec91cd-fbb5-4a39-a993-1c238c00761f" + date: "2026-02-06T05:00:00Z" + startTime: "2026-02-06T14:00:00Z" + endTime: "2026-02-06T22:00:00Z" + hours: 8.0 + cost: 376.0 + status: OPEN + workersNeeded: 2 + filled: 0 + location: "Los Angeles" + locationAddress: "San Jose Street" + city: "Los Angeles" + state: "CA" + street: "San Jose Street" + country: "US" + } + ) + shift_insert_17: shift_insert( + data: { + id: "e34fda56-0a92-4976-b4b9-41e72233eea1" + title: "Shift 17" + orderId: "32ec8f3e-3e07-43c6-8775-4afba7d7f09b" + date: "2026-02-06T05:00:00Z" + startTime: "2026-02-06T14:00:00Z" + endTime: "2026-02-06T22:00:00Z" + hours: 8.0 + cost: 472.0 + status: OPEN + workersNeeded: 2 + filled: 0 + location: "Los Angeles" + locationAddress: "San Jose Street" + city: "Los Angeles" + state: "CA" + street: "San Jose Street" + country: "US" + } + ) + shift_insert_18: shift_insert( + data: { + id: "ba9f6b73-ab07-4264-a027-5ccb2271e18e" + title: "Shift 18" + orderId: "0d0e06a5-a03b-4a8f-b0f5-c44d8f41ccfc" + date: "2026-02-07T05:00:00Z" + startTime: "2026-02-07T14:00:00Z" + endTime: "2026-02-07T22:00:00Z" + hours: 8.0 + cost: 360.0 + status: OPEN + workersNeeded: 2 + filled: 0 + location: "Los Angeles" + locationAddress: "San Jose Street" + city: "Los Angeles" + state: "CA" + street: "San Jose Street" + country: "US" + } + ) + shift_insert_19: shift_insert( + data: { + id: "cb494616-35eb-4d3c-8c48-a131e6e03eff" + title: "Shift 19" + orderId: "8a90077e-e45b-42dd-aec0-fe8289f43708" + date: "2026-02-07T05:00:00Z" + startTime: "2026-02-07T14:00:00Z" + endTime: "2026-02-07T22:00:00Z" + hours: 8.0 + cost: 376.0 + status: OPEN + workersNeeded: 2 + filled: 0 + location: "Los Angeles" + locationAddress: "San Jose Street" + city: "Los Angeles" + state: "CA" + street: "San Jose Street" + country: "US" + } + ) + shift_insert_20: shift_insert( + data: { + id: "d394a15c-1a42-41fa-b4cd-cc3e33929c05" + title: "Shift 20" + orderId: "4bc87380-8388-4f27-bbe0-aa7f3b24620d" + date: "2026-02-08T05:00:00Z" + startTime: "2026-02-08T14:00:00Z" + endTime: "2026-02-08T22:00:00Z" + hours: 8.0 + cost: 688.0 + status: OPEN + workersNeeded: 3 + filled: 2 + location: "Los Angeles" + locationAddress: "San Jose Street" + city: "Los Angeles" + state: "CA" + street: "San Jose Street" + country: "US" + } + ) + shiftRole_insert_1: shiftRole_insert( + data: { + shiftId: "6ed269b0-770b-4d01-b694-ed298d6ea441" + roleId: "cd5a0e59-1f1a-42ff-897c-36ae9349859a" + count: 1 + assigned: 1 + startTime: "2026-01-26T14:00:00Z" + endTime: "2026-01-26T22:00:00Z" + hours: 8.0 + breakType: MIN_30 + totalValue: 176.0 + } + ) + shiftRole_insert_2: shiftRole_insert( + data: { + shiftId: "6ed269b0-770b-4d01-b694-ed298d6ea441" + roleId: "25c3e5d7-6ee1-4672-8ed8-a9241587233e" + count: 1 + assigned: 1 + startTime: "2026-01-26T14:00:00Z" + endTime: "2026-01-26T22:00:00Z" + hours: 8.0 + breakType: MIN_30 + totalValue: 200.0 + } + ) + shiftRole_insert_3: shiftRole_insert( + data: { + shiftId: "ba1343dc-56be-4887-88cf-9cd626fcfcb5" + roleId: "e39f7067-8742-43b0-bfff-626094a8cb55" + count: 1 + assigned: 1 + startTime: "2026-01-26T14:00:00Z" + endTime: "2026-01-26T22:00:00Z" + hours: 8.0 + breakType: MIN_30 + totalValue: 216.0 + } + ) + shiftRole_insert_4: shiftRole_insert( + data: { + shiftId: "e3d006a1-c0b6-4cef-82ca-df157ee5462f" + roleId: "a6cee403-e3fa-4295-a6d1-623e7e2e18f7" + count: 1 + assigned: 1 + startTime: "2026-01-27T14:00:00Z" + endTime: "2026-01-27T22:00:00Z" + hours: 8.0 + breakType: MIN_30 + totalValue: 168.0 + } + ) + shiftRole_insert_5: shiftRole_insert( + data: { + shiftId: "e3d006a1-c0b6-4cef-82ca-df157ee5462f" + roleId: "c1f6b67f-f9ef-45b5-8dcb-bd32b4295fcb" + count: 1 + assigned: 1 + startTime: "2026-01-27T14:00:00Z" + endTime: "2026-01-27T22:00:00Z" + hours: 8.0 + breakType: MIN_30 + totalValue: 192.0 + } + ) + shiftRole_insert_6: shiftRole_insert( + data: { + shiftId: "5991da4f-86cb-4a38-b654-01de5fc79616" + roleId: "cd5a0e59-1f1a-42ff-897c-36ae9349859a" + count: 1 + assigned: 1 + startTime: "2026-01-27T14:00:00Z" + endTime: "2026-01-27T22:00:00Z" + hours: 8.0 + breakType: MIN_30 + totalValue: 176.0 + } + ) + shiftRole_insert_7: shiftRole_insert( + data: { + shiftId: "5991da4f-86cb-4a38-b654-01de5fc79616" + roleId: "25c3e5d7-6ee1-4672-8ed8-a9241587233e" + count: 1 + assigned: 1 + startTime: "2026-01-27T14:00:00Z" + endTime: "2026-01-27T22:00:00Z" + hours: 8.0 + breakType: MIN_30 + totalValue: 200.0 + } + ) + shiftRole_insert_8: shiftRole_insert( + data: { + shiftId: "c1831485-fffa-494b-aaf0-1d83f0ccbd30" + roleId: "e39f7067-8742-43b0-bfff-626094a8cb55" + count: 1 + assigned: 1 + startTime: "2026-01-28T14:00:00Z" + endTime: "2026-01-28T22:00:00Z" + hours: 8.0 + breakType: MIN_30 + totalValue: 216.0 + } + ) + shiftRole_insert_9: shiftRole_insert( + data: { + shiftId: "a7fda158-4fef-4e96-8b26-f3cf95fa0a50" + roleId: "a6cee403-e3fa-4295-a6d1-623e7e2e18f7" + count: 1 + assigned: 1 + startTime: "2026-01-29T14:00:00Z" + endTime: "2026-01-29T22:00:00Z" + hours: 8.0 + breakType: MIN_30 + totalValue: 168.0 + } + ) + shiftRole_insert_10: shiftRole_insert( + data: { + shiftId: "a7fda158-4fef-4e96-8b26-f3cf95fa0a50" + roleId: "c1f6b67f-f9ef-45b5-8dcb-bd32b4295fcb" + count: 1 + assigned: 1 + startTime: "2026-01-29T14:00:00Z" + endTime: "2026-01-29T22:00:00Z" + hours: 8.0 + breakType: MIN_30 + totalValue: 192.0 + } + ) + shiftRole_insert_11: shiftRole_insert( + data: { + shiftId: "efaf6e1c-27e3-414f-93f0-cd65031dee08" + roleId: "cd5a0e59-1f1a-42ff-897c-36ae9349859a" + count: 1 + assigned: 1 + startTime: "2026-01-30T14:00:00Z" + endTime: "2026-01-30T22:00:00Z" + hours: 8.0 + breakType: MIN_30 + totalValue: 176.0 + } + ) + shiftRole_insert_12: shiftRole_insert( + data: { + shiftId: "efaf6e1c-27e3-414f-93f0-cd65031dee08" + roleId: "25c3e5d7-6ee1-4672-8ed8-a9241587233e" + count: 1 + assigned: 1 + startTime: "2026-01-30T14:00:00Z" + endTime: "2026-01-30T22:00:00Z" + hours: 8.0 + breakType: MIN_30 + totalValue: 200.0 + } + ) + shiftRole_insert_13: shiftRole_insert( + data: { + shiftId: "65414c88-9655-40ab-9f6c-23e7e443ad8c" + roleId: "e39f7067-8742-43b0-bfff-626094a8cb55" + count: 1 + assigned: 1 + startTime: "2026-01-30T14:00:00Z" + endTime: "2026-01-30T22:00:00Z" + hours: 8.0 + breakType: MIN_30 + totalValue: 216.0 + } + ) + shiftRole_insert_14: shiftRole_insert( + data: { + shiftId: "65414c88-9655-40ab-9f6c-23e7e443ad8c" + roleId: "29ddea5b-9040-4c46-96d9-3eba480b56b7" + count: 1 + assigned: 1 + startTime: "2026-01-30T14:00:00Z" + endTime: "2026-01-30T22:00:00Z" + hours: 8.0 + breakType: MIN_30 + totalValue: 256.0 + } + ) + shiftRole_insert_15: shiftRole_insert( + data: { + shiftId: "e92e0b29-80b1-48b9-a704-a72aae95180f" + roleId: "a6cee403-e3fa-4295-a6d1-623e7e2e18f7" + count: 1 + assigned: 0 + startTime: "2026-01-31T14:00:00Z" + endTime: "2026-01-31T22:00:00Z" + hours: 8.0 + breakType: MIN_30 + totalValue: 168.0 + } + ) + shiftRole_insert_16: shiftRole_insert( + data: { + shiftId: "e92e0b29-80b1-48b9-a704-a72aae95180f" + roleId: "c1f6b67f-f9ef-45b5-8dcb-bd32b4295fcb" + count: 1 + assigned: 0 + startTime: "2026-01-31T14:00:00Z" + endTime: "2026-01-31T22:00:00Z" + hours: 8.0 + breakType: MIN_30 + totalValue: 192.0 + } + ) + shiftRole_insert_17: shiftRole_insert( + data: { + shiftId: "685cf551-2164-458a-8e87-fefa82dd3e90" + roleId: "cd5a0e59-1f1a-42ff-897c-36ae9349859a" + count: 1 + assigned: 0 + startTime: "2026-02-01T14:00:00Z" + endTime: "2026-02-01T22:00:00Z" + hours: 8.0 + breakType: MIN_30 + totalValue: 176.0 + } + ) + shiftRole_insert_18: shiftRole_insert( + data: { + shiftId: "685cf551-2164-458a-8e87-fefa82dd3e90" + roleId: "25c3e5d7-6ee1-4672-8ed8-a9241587233e" + count: 1 + assigned: 0 + startTime: "2026-02-01T14:00:00Z" + endTime: "2026-02-01T22:00:00Z" + hours: 8.0 + breakType: MIN_30 + totalValue: 200.0 + } + ) + shiftRole_insert_19: shiftRole_insert( + data: { + shiftId: "b51ca5e8-3f83-4937-bbee-8fae228d18cf" + roleId: "e39f7067-8742-43b0-bfff-626094a8cb55" + count: 2 + assigned: 1 + startTime: "2026-02-02T14:00:00Z" + endTime: "2026-02-02T22:00:00Z" + hours: 8.0 + breakType: MIN_30 + totalValue: 432.0 + } + ) + shiftRole_insert_20: shiftRole_insert( + data: { + shiftId: "b51ca5e8-3f83-4937-bbee-8fae228d18cf" + roleId: "29ddea5b-9040-4c46-96d9-3eba480b56b7" + count: 1 + assigned: 1 + startTime: "2026-02-02T14:00:00Z" + endTime: "2026-02-02T22:00:00Z" + hours: 8.0 + breakType: MIN_30 + totalValue: 256.0 + } + ) + shiftRole_insert_21: shiftRole_insert( + data: { + shiftId: "d3260cd9-1b15-4c0e-9c7f-b1b0fc5da2ff" + roleId: "a6cee403-e3fa-4295-a6d1-623e7e2e18f7" + count: 2 + assigned: 1 + startTime: "2026-02-02T14:00:00Z" + endTime: "2026-02-02T22:00:00Z" + hours: 8.0 + breakType: MIN_30 + totalValue: 336.0 + } + ) + shiftRole_insert_22: shiftRole_insert( + data: { + shiftId: "d3260cd9-1b15-4c0e-9c7f-b1b0fc5da2ff" + roleId: "c1f6b67f-f9ef-45b5-8dcb-bd32b4295fcb" + count: 1 + assigned: 1 + startTime: "2026-02-02T14:00:00Z" + endTime: "2026-02-02T22:00:00Z" + hours: 8.0 + breakType: MIN_30 + totalValue: 192.0 + } + ) + shiftRole_insert_23: shiftRole_insert( + data: { + shiftId: "fe82461f-8e87-4ac4-a40c-d34d9ddf34d2" + roleId: "cd5a0e59-1f1a-42ff-897c-36ae9349859a" + count: 1 + assigned: 0 + startTime: "2026-02-03T14:00:00Z" + endTime: "2026-02-03T22:00:00Z" + hours: 8.0 + breakType: MIN_30 + totalValue: 176.0 + } + ) + shiftRole_insert_24: shiftRole_insert( + data: { + shiftId: "fe82461f-8e87-4ac4-a40c-d34d9ddf34d2" + roleId: "25c3e5d7-6ee1-4672-8ed8-a9241587233e" + count: 1 + assigned: 0 + startTime: "2026-02-03T14:00:00Z" + endTime: "2026-02-03T22:00:00Z" + hours: 8.0 + breakType: MIN_30 + totalValue: 200.0 + } + ) + shiftRole_insert_25: shiftRole_insert( + data: { + shiftId: "001be679-3e96-464a-a1a1-696ac9677fda" + roleId: "e39f7067-8742-43b0-bfff-626094a8cb55" + count: 2 + assigned: 1 + startTime: "2026-02-04T14:00:00Z" + endTime: "2026-02-04T22:00:00Z" + hours: 8.0 + breakType: MIN_30 + totalValue: 432.0 + } + ) + shiftRole_insert_26: shiftRole_insert( + data: { + shiftId: "001be679-3e96-464a-a1a1-696ac9677fda" + roleId: "29ddea5b-9040-4c46-96d9-3eba480b56b7" + count: 1 + assigned: 1 + startTime: "2026-02-04T14:00:00Z" + endTime: "2026-02-04T22:00:00Z" + hours: 8.0 + breakType: MIN_30 + totalValue: 256.0 + } + ) + shiftRole_insert_27: shiftRole_insert( + data: { + shiftId: "f52a4c9b-c75d-42e2-9310-7c51a0224bff" + roleId: "a6cee403-e3fa-4295-a6d1-623e7e2e18f7" + count: 1 + assigned: 0 + startTime: "2026-02-05T14:00:00Z" + endTime: "2026-02-05T22:00:00Z" + hours: 8.0 + breakType: MIN_30 + totalValue: 168.0 + } + ) + shiftRole_insert_28: shiftRole_insert( + data: { + shiftId: "f52a4c9b-c75d-42e2-9310-7c51a0224bff" + roleId: "c1f6b67f-f9ef-45b5-8dcb-bd32b4295fcb" + count: 1 + assigned: 0 + startTime: "2026-02-05T14:00:00Z" + endTime: "2026-02-05T22:00:00Z" + hours: 8.0 + breakType: MIN_30 + totalValue: 192.0 + } + ) + shiftRole_insert_29: shiftRole_insert( + data: { + shiftId: "081eaceb-938a-4ccf-affa-7c1c8d7c6204" + roleId: "cd5a0e59-1f1a-42ff-897c-36ae9349859a" + count: 1 + assigned: 0 + startTime: "2026-02-06T14:00:00Z" + endTime: "2026-02-06T22:00:00Z" + hours: 8.0 + breakType: MIN_30 + totalValue: 176.0 + } + ) + shiftRole_insert_30: shiftRole_insert( + data: { + shiftId: "081eaceb-938a-4ccf-affa-7c1c8d7c6204" + roleId: "25c3e5d7-6ee1-4672-8ed8-a9241587233e" + count: 1 + assigned: 0 + startTime: "2026-02-06T14:00:00Z" + endTime: "2026-02-06T22:00:00Z" + hours: 8.0 + breakType: MIN_30 + totalValue: 200.0 + } + ) + shiftRole_insert_31: shiftRole_insert( + data: { + shiftId: "e34fda56-0a92-4976-b4b9-41e72233eea1" + roleId: "e39f7067-8742-43b0-bfff-626094a8cb55" + count: 1 + assigned: 0 + startTime: "2026-02-06T14:00:00Z" + endTime: "2026-02-06T22:00:00Z" + hours: 8.0 + breakType: MIN_30 + totalValue: 216.0 + } + ) + shiftRole_insert_32: shiftRole_insert( + data: { + shiftId: "e34fda56-0a92-4976-b4b9-41e72233eea1" + roleId: "29ddea5b-9040-4c46-96d9-3eba480b56b7" + count: 1 + assigned: 0 + startTime: "2026-02-06T14:00:00Z" + endTime: "2026-02-06T22:00:00Z" + hours: 8.0 + breakType: MIN_30 + totalValue: 256.0 + } + ) + shiftRole_insert_33: shiftRole_insert( + data: { + shiftId: "ba9f6b73-ab07-4264-a027-5ccb2271e18e" + roleId: "a6cee403-e3fa-4295-a6d1-623e7e2e18f7" + count: 1 + assigned: 0 + startTime: "2026-02-07T14:00:00Z" + endTime: "2026-02-07T22:00:00Z" + hours: 8.0 + breakType: MIN_30 + totalValue: 168.0 + } + ) + shiftRole_insert_34: shiftRole_insert( + data: { + shiftId: "ba9f6b73-ab07-4264-a027-5ccb2271e18e" + roleId: "c1f6b67f-f9ef-45b5-8dcb-bd32b4295fcb" + count: 1 + assigned: 0 + startTime: "2026-02-07T14:00:00Z" + endTime: "2026-02-07T22:00:00Z" + hours: 8.0 + breakType: MIN_30 + totalValue: 192.0 + } + ) + shiftRole_insert_35: shiftRole_insert( + data: { + shiftId: "cb494616-35eb-4d3c-8c48-a131e6e03eff" + roleId: "cd5a0e59-1f1a-42ff-897c-36ae9349859a" + count: 1 + assigned: 0 + startTime: "2026-02-07T14:00:00Z" + endTime: "2026-02-07T22:00:00Z" + hours: 8.0 + breakType: MIN_30 + totalValue: 176.0 + } + ) + shiftRole_insert_36: shiftRole_insert( + data: { + shiftId: "cb494616-35eb-4d3c-8c48-a131e6e03eff" + roleId: "25c3e5d7-6ee1-4672-8ed8-a9241587233e" + count: 1 + assigned: 0 + startTime: "2026-02-07T14:00:00Z" + endTime: "2026-02-07T22:00:00Z" + hours: 8.0 + breakType: MIN_30 + totalValue: 200.0 + } + ) + shiftRole_insert_37: shiftRole_insert( + data: { + shiftId: "d394a15c-1a42-41fa-b4cd-cc3e33929c05" + roleId: "e39f7067-8742-43b0-bfff-626094a8cb55" + count: 2 + assigned: 1 + startTime: "2026-02-08T14:00:00Z" + endTime: "2026-02-08T22:00:00Z" + hours: 8.0 + breakType: MIN_30 + totalValue: 432.0 + } + ) + shiftRole_insert_38: shiftRole_insert( + data: { + shiftId: "d394a15c-1a42-41fa-b4cd-cc3e33929c05" + roleId: "29ddea5b-9040-4c46-96d9-3eba480b56b7" + count: 1 + assigned: 1 + startTime: "2026-02-08T14:00:00Z" + endTime: "2026-02-08T22:00:00Z" + hours: 8.0 + breakType: MIN_30 + totalValue: 256.0 + } + ) + application_insert_1: application_insert( + data: { + id: "759cc343-e6a4-45ad-92f0-8c6ddf498af8" + shiftId: "6ed269b0-770b-4d01-b694-ed298d6ea441" + staffId: "584756aa-0294-4caa-8256-bcc9c18995eb" + roleId: "cd5a0e59-1f1a-42ff-897c-36ae9349859a" + status: COMPLETED + origin: STAFF + checkInTime: "2026-01-26T14:00:00Z" + checkOutTime: "2026-01-26T22:00:00Z" + } + ) + application_insert_2: application_insert( + data: { + id: "6f61a6e0-b4d4-48e2-bbeb-8ec059a4d260" + shiftId: "6ed269b0-770b-4d01-b694-ed298d6ea441" + staffId: "1e45b730-9c45-43a2-bda3-9b2e9d22b019" + roleId: "25c3e5d7-6ee1-4672-8ed8-a9241587233e" + status: COMPLETED + origin: STAFF + checkInTime: "2026-01-26T14:00:00Z" + checkOutTime: "2026-01-26T22:00:00Z" + } + ) + application_insert_3: application_insert( + data: { + id: "94b0aef9-1d1b-490b-b68e-0a495083ad9c" + shiftId: "ba1343dc-56be-4887-88cf-9cd626fcfcb5" + staffId: "bacfd25e-b5b0-463b-bce8-fad1aff2d9bb" + roleId: "e39f7067-8742-43b0-bfff-626094a8cb55" + status: COMPLETED + origin: STAFF + checkInTime: "2026-01-26T14:00:00Z" + checkOutTime: "2026-01-26T22:00:00Z" + } + ) + application_insert_4: application_insert( + data: { + id: "5997f3dc-9d67-4af5-beb2-22fb99ec2e3d" + shiftId: "e3d006a1-c0b6-4cef-82ca-df157ee5462f" + staffId: "c5801579-2472-4ddb-b81b-1af656c85e73" + roleId: "a6cee403-e3fa-4295-a6d1-623e7e2e18f7" + status: COMPLETED + origin: STAFF + checkInTime: "2026-01-27T14:00:00Z" + checkOutTime: "2026-01-27T22:00:00Z" + } + ) + application_insert_5: application_insert( + data: { + id: "5e2f6f5e-a2ac-4592-81a0-73ab883c7cc2" + shiftId: "e3d006a1-c0b6-4cef-82ca-df157ee5462f" + staffId: "24421973-9a75-48dd-97d5-a931851abc97" + roleId: "c1f6b67f-f9ef-45b5-8dcb-bd32b4295fcb" + status: COMPLETED + origin: STAFF + checkInTime: "2026-01-27T14:00:00Z" + checkOutTime: "2026-01-27T22:00:00Z" + } + ) + application_insert_6: application_insert( + data: { + id: "b71ad968-be58-4e73-a916-f2da800285b1" + shiftId: "5991da4f-86cb-4a38-b654-01de5fc79616" + staffId: "584756aa-0294-4caa-8256-bcc9c18995eb" + roleId: "cd5a0e59-1f1a-42ff-897c-36ae9349859a" + status: COMPLETED + origin: STAFF + checkInTime: "2026-01-27T14:00:00Z" + checkOutTime: "2026-01-27T22:00:00Z" + } + ) + application_insert_7: application_insert( + data: { + id: "8116a76a-97b8-4606-971a-a18cbf1bb65f" + shiftId: "5991da4f-86cb-4a38-b654-01de5fc79616" + staffId: "1e45b730-9c45-43a2-bda3-9b2e9d22b019" + roleId: "25c3e5d7-6ee1-4672-8ed8-a9241587233e" + status: COMPLETED + origin: STAFF + checkInTime: "2026-01-27T14:00:00Z" + checkOutTime: "2026-01-27T22:00:00Z" + } + ) + application_insert_8: application_insert( + data: { + id: "dd7167dc-4218-46ca-af43-0d87601fb964" + shiftId: "c1831485-fffa-494b-aaf0-1d83f0ccbd30" + staffId: "bacfd25e-b5b0-463b-bce8-fad1aff2d9bb" + roleId: "e39f7067-8742-43b0-bfff-626094a8cb55" + status: COMPLETED + origin: STAFF + checkInTime: "2026-01-28T14:00:00Z" + checkOutTime: "2026-01-28T22:00:00Z" + } + ) + application_insert_9: application_insert( + data: { + id: "412e0b38-00fb-4d6b-877e-62394673c2ea" + shiftId: "a7fda158-4fef-4e96-8b26-f3cf95fa0a50" + staffId: "c5801579-2472-4ddb-b81b-1af656c85e73" + roleId: "a6cee403-e3fa-4295-a6d1-623e7e2e18f7" + status: COMPLETED + origin: STAFF + checkInTime: "2026-01-29T14:00:00Z" + checkOutTime: "2026-01-29T22:00:00Z" + } + ) + application_insert_10: application_insert( + data: { + id: "27f8c3d4-9341-429d-9e14-4efba72ab41f" + shiftId: "a7fda158-4fef-4e96-8b26-f3cf95fa0a50" + staffId: "24421973-9a75-48dd-97d5-a931851abc97" + roleId: "c1f6b67f-f9ef-45b5-8dcb-bd32b4295fcb" + status: COMPLETED + origin: STAFF + checkInTime: "2026-01-29T14:00:00Z" + checkOutTime: "2026-01-29T22:00:00Z" + } + ) + application_insert_11: application_insert( + data: { + id: "4c05f90d-5ca5-4ea2-9c67-5f2a86f66d94" + shiftId: "efaf6e1c-27e3-414f-93f0-cd65031dee08" + staffId: "584756aa-0294-4caa-8256-bcc9c18995eb" + roleId: "cd5a0e59-1f1a-42ff-897c-36ae9349859a" + status: CONFIRMED + origin: STAFF + checkInTime: "2026-01-30T14:00:00Z" + } + ) + application_insert_12: application_insert( + data: { + id: "3692d105-a9c4-4443-b4b2-8973e31be92a" + shiftId: "efaf6e1c-27e3-414f-93f0-cd65031dee08" + staffId: "1e45b730-9c45-43a2-bda3-9b2e9d22b019" + roleId: "25c3e5d7-6ee1-4672-8ed8-a9241587233e" + status: CHECKED_IN + origin: STAFF + checkInTime: "2026-01-30T14:00:00Z" + } + ) + application_insert_13: application_insert( + data: { + id: "61a88f86-17d0-4bd2-80fe-2358eb9f1ab3" + shiftId: "65414c88-9655-40ab-9f6c-23e7e443ad8c" + staffId: "bacfd25e-b5b0-463b-bce8-fad1aff2d9bb" + roleId: "e39f7067-8742-43b0-bfff-626094a8cb55" + status: CONFIRMED + origin: STAFF + checkInTime: "2026-01-30T14:00:00Z" + } + ) + application_insert_14: application_insert( + data: { + id: "3002d811-fcf2-41e0-81ad-87e772f9d632" + shiftId: "65414c88-9655-40ab-9f6c-23e7e443ad8c" + staffId: "44c79ae8-d759-41e0-94d7-ae60b504d65c" + roleId: "29ddea5b-9040-4c46-96d9-3eba480b56b7" + status: CHECKED_IN + origin: STAFF + checkInTime: "2026-01-30T14:00:00Z" + } + ) + application_insert_15: application_insert( + data: { + id: "48ddc0cb-9764-4113-b2c6-dfee2970b56f" + shiftId: "b51ca5e8-3f83-4937-bbee-8fae228d18cf" + staffId: "bacfd25e-b5b0-463b-bce8-fad1aff2d9bb" + roleId: "e39f7067-8742-43b0-bfff-626094a8cb55" + status: ACCEPTED + origin: STAFF + } + ) + application_insert_16: application_insert( + data: { + id: "3e701714-52e3-452d-b55c-8e669de41555" + shiftId: "b51ca5e8-3f83-4937-bbee-8fae228d18cf" + staffId: "44c79ae8-d759-41e0-94d7-ae60b504d65c" + roleId: "29ddea5b-9040-4c46-96d9-3eba480b56b7" + status: ACCEPTED + origin: STAFF + } + ) + application_insert_17: application_insert( + data: { + id: "b408ba8f-9c87-415d-a226-1c91b56b1ca3" + shiftId: "d3260cd9-1b15-4c0e-9c7f-b1b0fc5da2ff" + staffId: "c5801579-2472-4ddb-b81b-1af656c85e73" + roleId: "a6cee403-e3fa-4295-a6d1-623e7e2e18f7" + status: ACCEPTED + origin: STAFF + } + ) + application_insert_18: application_insert( + data: { + id: "ee99cca8-6412-404f-ad4d-e688ac36f3d6" + shiftId: "d3260cd9-1b15-4c0e-9c7f-b1b0fc5da2ff" + staffId: "24421973-9a75-48dd-97d5-a931851abc97" + roleId: "c1f6b67f-f9ef-45b5-8dcb-bd32b4295fcb" + status: ACCEPTED + origin: STAFF + } + ) + application_insert_19: application_insert( + data: { + id: "de01d803-429d-4549-9a91-e3a5a3750911" + shiftId: "001be679-3e96-464a-a1a1-696ac9677fda" + staffId: "bacfd25e-b5b0-463b-bce8-fad1aff2d9bb" + roleId: "e39f7067-8742-43b0-bfff-626094a8cb55" + status: ACCEPTED + origin: STAFF + } + ) + application_insert_20: application_insert( + data: { + id: "d071f83a-5071-4061-b843-c69df4d58d08" + shiftId: "001be679-3e96-464a-a1a1-696ac9677fda" + staffId: "44c79ae8-d759-41e0-94d7-ae60b504d65c" + roleId: "29ddea5b-9040-4c46-96d9-3eba480b56b7" + status: ACCEPTED + origin: STAFF + } + ) + application_insert_21: application_insert( + data: { + id: "5268e4b8-fa6c-4594-bcb6-4bace5e1be7c" + shiftId: "d394a15c-1a42-41fa-b4cd-cc3e33929c05" + staffId: "bacfd25e-b5b0-463b-bce8-fad1aff2d9bb" + roleId: "e39f7067-8742-43b0-bfff-626094a8cb55" + status: ACCEPTED + origin: STAFF + } + ) + application_insert_22: application_insert( + data: { + id: "eeac50a3-4ff2-4212-a0ec-009a99ab717f" + shiftId: "d394a15c-1a42-41fa-b4cd-cc3e33929c05" + staffId: "44c79ae8-d759-41e0-94d7-ae60b504d65c" + roleId: "29ddea5b-9040-4c46-96d9-3eba480b56b7" + status: ACCEPTED + origin: STAFF + } + ) + invoice_insert_1: invoice_insert( + data: { + id: "f91168e1-2dc9-4544-86b9-71145e987d83" + status: PAID + vendorId: "afa9ef70-fd90-493b-95b0-bc05791c514f" + businessId: "c5d0f18b-ba95-479b-8915-1bd498fd6860" + orderId: "dccde341-3d23-4587-8037-188963f5ae26" + paymentTerms: NET_30 + invoiceNumber: "INV-2026-001" + issueDate: "2026-01-26T17:00:00Z" + dueDate: "2026-02-09T17:00:00Z" + hub: "Los Angeles" + amount: 400.0 + staffCount: 2 + chargesCount: 2 + } + ) + invoice_insert_2: invoice_insert( + data: { + id: "edf18bc2-1317-487e-89cc-0d8fe95b592a" + status: PAID + vendorId: "b959b8d6-9133-4025-b3d2-a4c1564f9a25" + businessId: "c5d0f18b-ba95-479b-8915-1bd498fd6860" + orderId: "e2825341-d6dd-4dff-9ee0-12332549c3d6" + paymentTerms: NET_30 + invoiceNumber: "INV-2026-002" + issueDate: "2026-01-26T17:00:00Z" + dueDate: "2026-02-09T17:00:00Z" + hub: "Los Angeles" + amount: 200.0 + staffCount: 1 + chargesCount: 1 + } + ) + invoice_insert_3: invoice_insert( + data: { + id: "1747bac6-588d-4fd9-8bad-0067f602d14c" + status: PAID + vendorId: "32adf31b-8a8f-49be-ace4-50402daa8046" + businessId: "c5d0f18b-ba95-479b-8915-1bd498fd6860" + orderId: "19798ac3-adb6-4c77-9fd1-026d3af8138d" + paymentTerms: NET_30 + invoiceNumber: "INV-2026-003" + issueDate: "2026-01-27T17:00:00Z" + dueDate: "2026-02-10T17:00:00Z" + hub: "Los Angeles" + amount: 400.0 + staffCount: 2 + chargesCount: 2 + } + ) + invoice_insert_4: invoice_insert( + data: { + id: "67d95e3e-151f-447e-82c8-c9cfad58b83a" + status: APPROVED + vendorId: "afa9ef70-fd90-493b-95b0-bc05791c514f" + businessId: "c5d0f18b-ba95-479b-8915-1bd498fd6860" + orderId: "4ca2f85b-63ef-453f-b9e0-bc0c1a51817a" + paymentTerms: NET_30 + invoiceNumber: "INV-2026-004" + issueDate: "2026-01-27T17:00:00Z" + dueDate: "2026-02-10T17:00:00Z" + hub: "Los Angeles" + amount: 400.0 + staffCount: 2 + chargesCount: 2 + } + ) + invoice_insert_5: invoice_insert( + data: { + id: "335d6d39-51ec-4dba-8065-d1909a28dbfd" + status: APPROVED + vendorId: "b959b8d6-9133-4025-b3d2-a4c1564f9a25" + businessId: "c5d0f18b-ba95-479b-8915-1bd498fd6860" + orderId: "62eed7f5-d8de-4146-bf4c-d4b04965b97d" + paymentTerms: NET_30 + invoiceNumber: "INV-2026-005" + issueDate: "2026-01-28T17:00:00Z" + dueDate: "2026-02-11T17:00:00Z" + hub: "Los Angeles" + amount: 200.0 + staffCount: 1 + chargesCount: 1 + } + ) + invoice_insert_6: invoice_insert( + data: { + id: "3396091d-b921-4316-9956-8de8e7a0c0e9" + status: APPROVED + vendorId: "32adf31b-8a8f-49be-ace4-50402daa8046" + businessId: "c5d0f18b-ba95-479b-8915-1bd498fd6860" + orderId: "170c1ff2-67ce-4527-83fd-c4722fc755b0" + paymentTerms: NET_30 + invoiceNumber: "INV-2026-006" + issueDate: "2026-01-29T17:00:00Z" + dueDate: "2026-02-12T17:00:00Z" + hub: "Los Angeles" + amount: 400.0 + staffCount: 2 + chargesCount: 2 + } + ) + recentPayment_insert_1: recentPayment_insert( + data: { + id: "fa09b5ec-535c-462d-8660-c034472f0f64" + workedTime: "8.0h" + status: PAID + staffId: "584756aa-0294-4caa-8256-bcc9c18995eb" + applicationId: "759cc343-e6a4-45ad-92f0-8c6ddf498af8" + invoiceId: "f91168e1-2dc9-4544-86b9-71145e987d83" + } + ) + recentPayment_insert_2: recentPayment_insert( + data: { + id: "ae9f05c3-9597-4f66-9ee2-f1b388b42273" + workedTime: "8.0h" + status: PAID + staffId: "1e45b730-9c45-43a2-bda3-9b2e9d22b019" + applicationId: "6f61a6e0-b4d4-48e2-bbeb-8ec059a4d260" + invoiceId: "f91168e1-2dc9-4544-86b9-71145e987d83" + } + ) + recentPayment_insert_3: recentPayment_insert( + data: { + id: "98031410-6035-406c-8842-9bc3f960fced" + workedTime: "8.0h" + status: PAID + staffId: "bacfd25e-b5b0-463b-bce8-fad1aff2d9bb" + applicationId: "94b0aef9-1d1b-490b-b68e-0a495083ad9c" + invoiceId: "edf18bc2-1317-487e-89cc-0d8fe95b592a" + } + ) + recentPayment_insert_4: recentPayment_insert( + data: { + id: "77d8b914-242a-46a1-b467-9ecd8dd5c1fa" + workedTime: "8.0h" + status: PAID + staffId: "c5801579-2472-4ddb-b81b-1af656c85e73" + applicationId: "5997f3dc-9d67-4af5-beb2-22fb99ec2e3d" + invoiceId: "1747bac6-588d-4fd9-8bad-0067f602d14c" + } + ) + recentPayment_insert_5: recentPayment_insert( + data: { + id: "4f5c1961-5b1e-4bf5-8415-5b812bbf0d0b" + workedTime: "8.0h" + status: PAID + staffId: "24421973-9a75-48dd-97d5-a931851abc97" + applicationId: "5e2f6f5e-a2ac-4592-81a0-73ab883c7cc2" + invoiceId: "1747bac6-588d-4fd9-8bad-0067f602d14c" + } + ) +} diff --git a/bugs/1.png b/bugs/1.png new file mode 100644 index 00000000..2a3962da Binary files /dev/null and b/bugs/1.png differ diff --git a/bugs/10.png b/bugs/10.png new file mode 100644 index 00000000..c2127bb0 Binary files /dev/null and b/bugs/10.png differ diff --git a/bugs/2.png b/bugs/2.png new file mode 100644 index 00000000..9621a5f7 Binary files /dev/null and b/bugs/2.png differ diff --git a/bugs/3.png b/bugs/3.png new file mode 100644 index 00000000..d3242b8b Binary files /dev/null and b/bugs/3.png differ diff --git a/bugs/4.png b/bugs/4.png new file mode 100644 index 00000000..8bf45daf Binary files /dev/null and b/bugs/4.png differ diff --git a/bugs/5.png b/bugs/5.png new file mode 100644 index 00000000..4165b41d Binary files /dev/null and b/bugs/5.png differ diff --git a/bugs/6.png b/bugs/6.png new file mode 100644 index 00000000..02ece018 Binary files /dev/null and b/bugs/6.png differ diff --git a/bugs/7.png b/bugs/7.png new file mode 100644 index 00000000..3852ec11 Binary files /dev/null and b/bugs/7.png differ diff --git a/bugs/8.png b/bugs/8.png new file mode 100644 index 00000000..f199c9f0 Binary files /dev/null and b/bugs/8.png differ diff --git a/bugs/9.png b/bugs/9.png new file mode 100644 index 00000000..595a01d2 Binary files /dev/null and b/bugs/9.png differ diff --git a/bugs/BUG-REPORT-2026-01-31.md b/bugs/BUG-REPORT-2026-01-31.md new file mode 100644 index 00000000..596927c9 --- /dev/null +++ b/bugs/BUG-REPORT-2026-01-31.md @@ -0,0 +1,932 @@ +# Bug Report & Technical Debt Analysis + +**Branch:** `fix/check-boris` +**Apps Tested:** Client Mobile (Android) + +--- + +## Housekeeping: Git Hygiene Issue + +### Problem + +Several Flutter-generated ephemeral files were incorrectly committed to the repository. These files are platform-specific symlinks and generated configs that should be ignored. + +### Affected Files + +``` +apps/mobile/apps/*/linux/flutter/ephemeral/.plugin_symlinks/* +apps/mobile/apps/*/windows/flutter/ephemeral/.plugin_symlinks/* +apps/mobile/apps/*/macos/Flutter/ephemeral/* +apps/mobile/apps/*/ios/Flutter/ephemeral/* +``` + +### Fix Applied + +1. Updated `.gitignore` to include: +```gitignore +# Ephemeral files (generated by Flutter for desktop platforms) +**/linux/flutter/ephemeral/ +**/windows/flutter/ephemeral/ +**/macos/Flutter/ephemeral/ +**/ios/Flutter/ephemeral/ +``` + +2. Run these commands to remove from tracking (files stay on disk): +```bash +git rm -r --cached apps/mobile/apps/client/linux/flutter/ephemeral/ +git rm -r --cached apps/mobile/apps/client/windows/flutter/ephemeral/ +git rm -r --cached apps/mobile/apps/client/macos/Flutter/ephemeral/ +git rm -r --cached apps/mobile/apps/client/ios/Flutter/ephemeral/ +git rm -r --cached apps/mobile/apps/staff/linux/flutter/ephemeral/ +git rm -r --cached apps/mobile/apps/staff/windows/flutter/ephemeral/ +git rm -r --cached apps/mobile/apps/staff/ios/Flutter/ephemeral/ +git rm -r --cached apps/mobile/apps/design_system_viewer/ios/Flutter/ephemeral/ +git rm -r --cached apps/mobile/apps/design_system_viewer/macos/Flutter/ephemeral/ +``` + +### Note on `strings.g.dart` + +The file `apps/mobile/packages/core_localization/lib/src/l10n/strings.g.dart` is auto-generated by Slang. It is listed in `apps/mobile/.gitignore` as `*.g.dart` but was committed before the gitignore rule existed. + +**Recommendation:** Remove from tracking with `git rm --cached` and regenerate via `melos run gen:all` after each pull. + +--- + +## Executive Summary + +This report documents critical bugs discovered during manual testing of the Client mobile application, along with architectural issues requiring immediate attention. The analysis covers authentication flow, data model design, UI state management, and error handling practices. + +| Priority | Issue | Severity | Effort | +|----------|-------|----------|--------| +| P0 | Auth/User Sync Issue | Critical | Medium | +| P1 | Error Handling Architecture | High | High | +| P1 | Order Display Logic | Medium | Low | +| P2 | Hub Delete UI Freeze | Medium | Low | +| P2 | Hub Name vs Address Display | Low | Low | + +--- + +## Bug 1: Authentication & User Sync Issue + +### Status: CRITICAL + +### Description + +Users who attempt to create an account may end up in an inconsistent state where: +- Firebase Authentication has their account (email/password stored) +- PostgreSQL database does NOT have their user profile + +This results in: +- "Account already exists" when trying to register again +- "Incorrect email or password" when trying to log in (even with correct credentials) + +### Screenshots + +- `1.png` - Registration attempt with boris@bwnyasse.net showing "Account already exists" +- `2.png` - Login attempt showing "Incorrect email or password" +- `3.png`, `4.png` - Additional registration failures + +### Root Cause + +**File:** `apps/mobile/packages/features/client/authentication/lib/src/data/repositories_impl/auth_repository_impl.dart` + +The registration flow (lines 57-120) performs three sequential operations: + +```dart +// Step 1: Create Firebase Auth user +final credential = await _firebaseAuth.createUserWithEmailAndPassword(...); + +// Step 2: Create Business in PostgreSQL +final createBusinessResponse = await _dataConnect.createBusiness(...); + +// Step 3: Create User in PostgreSQL +final createUserResponse = await _dataConnect.createUser(...); +``` + +**Problem:** If Step 2 or Step 3 fails AFTER Step 1 succeeds: +- The rollback `firebaseUser.delete()` may fail silently +- User exists in Firebase Auth but NOT in PostgreSQL +- Login fails because `_getUserProfile()` cannot find the user (line 142-145) + +**Additionally, line 100 has an incomplete TODO:** +```dart +// TO-DO: Also delete the created Business if this fails +``` + +If `createUser` fails, the orphaned Business record remains in PostgreSQL. + +### Technical Flow + +``` +Registration Attempt: +├─ Firebase Auth: createUserWithEmailAndPassword() ✓ (User created) +├─ Data Connect: createBusiness() ✓ (Business created) +└─ Data Connect: createUser() ✗ (FAILS - network error, constraint violation, etc.) + └─ Rollback: firebaseUser.delete() ✗ (May fail silently) + +Result: +- Firebase Auth: User EXISTS +- PostgreSQL users table: User MISSING +- PostgreSQL businesses table: Orphaned Business record + +Subsequent Login: +├─ Firebase Auth: signInWithEmailAndPassword() ✓ (Credentials valid) +└─ _getUserProfile(): getUserById() returns NULL + └─ Throws: "Authenticated user profile not found in database." + └─ But error shown to user: "Incorrect email or password." (misleading!) +``` + +### Recommended Fix + +1. **Implement transactional rollback:** +```dart +Future signUpWithEmail({...}) async { + firebase.User? firebaseUser; + String? businessId; + + try { + // Step 1 + final credential = await _firebaseAuth.createUserWithEmailAndPassword(...); + firebaseUser = credential.user; + + // Step 2 + final businessResult = await _dataConnect.createBusiness(...); + businessId = businessResult.data?.business_insert.id; + + // Step 3 + final userResult = await _dataConnect.createUser(...); + if (userResult.data?.user_insert == null) { + throw Exception('User creation failed'); + } + + return _getUserProfile(...); + + } catch (e) { + // Full rollback + if (businessId != null) { + await _dataConnect.deleteBusiness(id: businessId).execute(); + } + if (firebaseUser != null) { + await firebaseUser.delete(); + } + rethrow; + } +} +``` + +2. **Add admin tool to reconcile orphaned accounts** +3. **Add retry mechanism with idempotency checks** + +--- + +## Bug 2: Hub Name vs Address Display + +### Status: LOW PRIORITY + +### Description + +Order cards display the hub's street address instead of the hub name, creating inconsistency with the hub management screen. + +### Screenshots + +- `5.png` - Order showing "6800 San Jose Street, Granada Hills, CA, USA" +- `6.png` - Order showing "San Jose Street" +- Compare with `7.png` showing hub names: "Downtown Operations Hub", "Central Operations Hub" + +### Root Cause + +**File:** `apps/mobile/packages/features/client/view_orders/lib/src/presentation/widgets/view_order_card.dart` + +Lines 213-219 display `order.locationAddress` instead of `order.location` (hub name): + +```dart +Expanded( + child: Text( + order.locationAddress, // Shows address, not hub name + style: UiTypography.footnote2r.textSecondary, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), +), +``` + +The data is correctly stored during order creation in `client_create_order_repository_impl.dart`: +- Line 104: `.location(hub.name)` - Hub name +- Line 105: `.locationAddress(hub.address)` - Address + +### Recommended Fix + +Update `view_order_card.dart` to show hub name as primary, address as secondary: + +```dart +Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + order.location, // Hub name + style: UiTypography.body2m.textPrimary, + ), + Text( + order.locationAddress, // Address as subtitle + style: UiTypography.footnote2r.textSecondary, + ), + ], +), +``` + +--- + +## Bug 3: Hub Delete UI Freeze + +### Status: MEDIUM PRIORITY + +### Description + +After attempting to delete a hub that has orders (which correctly shows an error), subsequent delete attempts on ANY hub cause the UI to freeze with a loading overlay that never disappears. + +### Screenshot + +- `7.png` - Error message "Sorry this hub has orders, it can't be deleted." + +### Root Cause + +**File:** `apps/mobile/packages/features/client/hubs/lib/src/presentation/blocs/client_hubs_state.dart` + +The `copyWith` method cannot reset `errorMessage` to `null` due to Dart's null-coalescing behavior: + +```dart +ClientHubsState copyWith({ + String? errorMessage, + String? successMessage, + // ... +}) { + return ClientHubsState( + // BUG: null ?? this.errorMessage keeps the OLD value! + errorMessage: errorMessage ?? this.errorMessage, + successMessage: successMessage ?? this.successMessage, + // ... + ); +} +``` + +**Flow of the bug:** + +1. Delete Hub A → Error "Sorry this hub has orders..." +2. `_onMessageCleared` calls `copyWith(errorMessage: null)` +3. `null ?? this.errorMessage` = OLD ERROR MESSAGE (not cleared!) +4. Delete Hub B → Same error message +5. `listenWhen` checks: `previous.errorMessage != current.errorMessage` +6. Both are "Sorry this hub has orders..." → FALSE, listener not called +7. `MessageCleared` never sent → Status never resets → Overlay stays forever + +**Proof the team knows this pattern:** They correctly implemented `clearHubToIdentify` flag (lines 45, 53-54) but forgot to do the same for messages. + +### Recommended Fix + +**File:** `client_hubs_state.dart` + +```dart +ClientHubsState copyWith({ + ClientHubsStatus? status, + List? hubs, + String? errorMessage, + String? successMessage, + bool? showAddHubDialog, + Hub? hubToIdentify, + bool clearHubToIdentify = false, + bool clearErrorMessage = false, // ADD THIS + bool clearSuccessMessage = false, // ADD THIS +}) { + return ClientHubsState( + status: status ?? this.status, + hubs: hubs ?? this.hubs, + errorMessage: clearErrorMessage + ? null + : (errorMessage ?? this.errorMessage), + successMessage: clearSuccessMessage + ? null + : (successMessage ?? this.successMessage), + showAddHubDialog: showAddHubDialog ?? this.showAddHubDialog, + hubToIdentify: clearHubToIdentify + ? null + : (hubToIdentify ?? this.hubToIdentify), + ); +} +``` + +**File:** `client_hubs_bloc.dart` - Update `_onMessageCleared`: + +```dart +void _onMessageCleared( + ClientHubsMessageCleared event, + Emitter emit, +) { + emit( + state.copyWith( + clearErrorMessage: true, // USE FLAG + clearSuccessMessage: true, // USE FLAG + status: state.status == ClientHubsStatus.actionSuccess || + state.status == ClientHubsStatus.actionFailure + ? ClientHubsStatus.success + : state.status, + ), + ); +} +``` + +--- + +## Bug 4: Order Display Shows Positions as Separate Orders + +### Status: MEDIUM PRIORITY - DESIGN DECISION NEEDED + +### Description + +When creating an order with multiple positions (e.g., 1 Cook + 1 Bartender), the Orders list shows them as separate cards instead of a single order with multiple positions. + +### Screenshot + +- `8.png` - Shows "Cook - Boris Test Order 1" and "Bartender - Boris Test Order 1" as separate cards + +### Analysis + +**This is NOT a data bug.** The data model is correct: +- 1 Order → 1 Shift → N ShiftRoles (positions) + +The issue is in the **display logic**. + +**File:** `apps/mobile/packages/features/client/view_orders/lib/src/data/repositories/view_orders_repository_impl.dart` + +Lines 32-89 query `ShiftRoles` and create one `OrderItem` per role: + +```dart +// Query returns ShiftRoles, not Orders +final result = await _dataConnect + .listShiftRolesByBusinessAndDateRange(...) + .execute(); + +// Each ShiftRole becomes a card +return result.data.shiftRoles.map((shiftRole) { + return domain.OrderItem( + title: '${shiftRole.role.name} - $eventName', // "Cook - Boris Test Order 1" + // ... + ); +}).toList(); +``` + +### Options + +**Option A: Keep current behavior (per-position cards)** +- Pros: Granular view of each position's status +- Cons: Confusing for users who think they created one order + +**Option B: Group by Order (recommended)** +- Pros: Matches user mental model +- Cons: Requires refactoring view layer + +### Recommended Fix (Option B) + +```dart +Future> getOrdersForRange({...}) async { + final result = await _dataConnect + .listShiftRolesByBusinessAndDateRange(...) + .execute(); + + // Group ShiftRoles by Order ID + final Map> orderGroups = {}; + for (final shiftRole in result.data.shiftRoles) { + final orderId = shiftRole.shift.order.id; + orderGroups.putIfAbsent(orderId, () => []); + orderGroups[orderId]!.add(shiftRole); + } + + // Create one OrderItem per Order with positions summary + return orderGroups.entries.map((entry) { + final roles = entry.value; + final firstRole = roles.first; + final positionsSummary = roles.map((r) => r.role.name).join(', '); + final totalWorkers = roles.fold(0, (sum, r) => sum + r.count); + + return domain.OrderItem( + id: entry.key, + orderId: entry.key, + title: firstRole.shift.order.eventName ?? 'Order', + subtitle: positionsSummary, // "Cook, Bartender" + workersNeeded: totalWorkers, + // ... aggregate other fields + ); + }).toList(); +} +``` + +--- + +## Bug 5: Mock Data in Production Views + +### Status: INFORMATIONAL + +### Description + +The Coverage screen shows "Jose Salazar - Checked in at 9:00 AM" which appears to be test/mock data. + +### Screenshot + +- `9.png` - Daily Coverage showing mock worker data + +### Recommendation + +Ensure mock data is clearly separated and not visible in builds distributed for testing. Consider adding a visual indicator (e.g., "TEST DATA" banner) when using mock repositories. + +--- + +## Architectural Issue: Error Handling + +### Status: HIGH PRIORITY - TECHNICAL DEBT + +### Description + +The application exposes raw technical error messages to end users. This is unprofessional and potentially a security concern. + +### Evidence + +Found **60+ instances** of `throw Exception('technical message')` across the codebase: + +```dart +// Examples of problematic error messages shown to users: +throw Exception('Authenticated user profile not found in database.'); +throw Exception('Business creation failed after Firebase user registration.'); +throw Exception('Staff profile not found for User ID: ${user.uid}'); +throw Exception('Failed to fetch certificates: $e'); // Exposes stack trace! +throw Exception('Error signing out: ${e.toString()}'); +``` + +### Current State + +- No centralized error handling system +- No custom exception classes +- Technical messages shown directly to users +- No i18n support for error messages +- No error codes for logging/tracking + +### Recommended Architecture + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ DOMAIN LAYER │ +│ │ +│ packages/domain/lib/src/exceptions/app_exception.dart │ +│ │ +│ sealed class AppException implements Exception { │ +│ const AppException({required this.code, this.technical}); │ +│ final String code; // For logging: "AUTH_001" │ +│ final String? technical; // For devs only │ +│ String get messageKey; // For i18n: "errors.auth.x" │ +│ } │ +│ │ +│ class InvalidCredentialsException extends AuthException { │ +│ String get messageKey => 'errors.auth.invalid_credentials'; │ +│ } │ +│ │ +│ class HubHasOrdersException extends HubException { │ +│ String get messageKey => 'errors.hub.has_orders'; │ +│ } │ +└─────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ PRESENTATION LAYER │ +│ │ +│ } on AppException catch (e) { │ +│ log('Error ${e.code}: ${e.technical}'); // Dev logging │ +│ emit(state.copyWith(errorKey: e.messageKey)); │ +│ } catch (e) { │ +│ log('Unexpected: $e'); │ +│ emit(state.copyWith(errorKey: 'errors.generic.unknown')); │ +│ } │ +└─────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ LOCALIZATION (i18n) │ +│ │ +│ errors: │ +│ auth: │ +│ invalid_credentials: "Email or password is incorrect" │ +│ account_exists: "An account with this email exists" │ +│ session_expired: "Please sign in again" │ +│ hub: │ +│ has_orders: "This hub has active orders" │ +│ order: │ +│ missing_hub: "Please select a location" │ +│ generic: │ +│ unknown: "Something went wrong. Please try again." │ +│ no_connection: "No internet connection" │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### Implementation Plan + +#### Phase 1: Create Exception Classes (1 hour) + +**Create:** `packages/domain/lib/src/exceptions/app_exception.dart` + +```dart +/// Base sealed class for all application exceptions. +sealed class AppException implements Exception { + const AppException({ + required this.code, + this.technicalMessage, + }); + + /// Unique error code for logging/tracking (e.g., "AUTH_001") + final String code; + + /// Technical details for developers (never shown to users) + final String? technicalMessage; + + /// Returns the localization key for user-friendly message + String get messageKey; + + @override + String toString() => 'AppException($code): $technicalMessage'; +} + +// ============================================================ +// AUTH EXCEPTIONS +// ============================================================ + +sealed class AuthException extends AppException { + const AuthException({required super.code, super.technicalMessage}); +} + +class InvalidCredentialsException extends AuthException { + const InvalidCredentialsException({String? technicalMessage}) + : super(code: 'AUTH_001', technicalMessage: technicalMessage); + + @override + String get messageKey => 'errors.auth.invalid_credentials'; +} + +class AccountExistsException extends AuthException { + const AccountExistsException({String? technicalMessage}) + : super(code: 'AUTH_002', technicalMessage: technicalMessage); + + @override + String get messageKey => 'errors.auth.account_exists'; +} + +class SessionExpiredException extends AuthException { + const SessionExpiredException({String? technicalMessage}) + : super(code: 'AUTH_003', technicalMessage: technicalMessage); + + @override + String get messageKey => 'errors.auth.session_expired'; +} + +class UserNotFoundException extends AuthException { + const UserNotFoundException({String? technicalMessage}) + : super(code: 'AUTH_004', technicalMessage: technicalMessage); + + @override + String get messageKey => 'errors.auth.user_not_found'; +} + +class UnauthorizedAppException extends AuthException { + const UnauthorizedAppException({String? technicalMessage}) + : super(code: 'AUTH_005', technicalMessage: technicalMessage); + + @override + String get messageKey => 'errors.auth.unauthorized_app'; +} + +class WeakPasswordException extends AuthException { + const WeakPasswordException({String? technicalMessage}) + : super(code: 'AUTH_006', technicalMessage: technicalMessage); + + @override + String get messageKey => 'errors.auth.weak_password'; +} + +// ============================================================ +// HUB EXCEPTIONS +// ============================================================ + +sealed class HubException extends AppException { + const HubException({required super.code, super.technicalMessage}); +} + +class HubHasOrdersException extends HubException { + const HubHasOrdersException({String? technicalMessage}) + : super(code: 'HUB_001', technicalMessage: technicalMessage); + + @override + String get messageKey => 'errors.hub.has_orders'; +} + +class HubNotFoundException extends HubException { + const HubNotFoundException({String? technicalMessage}) + : super(code: 'HUB_002', technicalMessage: technicalMessage); + + @override + String get messageKey => 'errors.hub.not_found'; +} + +class HubCreationFailedException extends HubException { + const HubCreationFailedException({String? technicalMessage}) + : super(code: 'HUB_003', technicalMessage: technicalMessage); + + @override + String get messageKey => 'errors.hub.creation_failed'; +} + +// ============================================================ +// ORDER EXCEPTIONS +// ============================================================ + +sealed class OrderException extends AppException { + const OrderException({required super.code, super.technicalMessage}); +} + +class OrderMissingHubException extends OrderException { + const OrderMissingHubException({String? technicalMessage}) + : super(code: 'ORDER_001', technicalMessage: technicalMessage); + + @override + String get messageKey => 'errors.order.missing_hub'; +} + +class OrderMissingVendorException extends OrderException { + const OrderMissingVendorException({String? technicalMessage}) + : super(code: 'ORDER_002', technicalMessage: technicalMessage); + + @override + String get messageKey => 'errors.order.missing_vendor'; +} + +class OrderCreationFailedException extends OrderException { + const OrderCreationFailedException({String? technicalMessage}) + : super(code: 'ORDER_003', technicalMessage: technicalMessage); + + @override + String get messageKey => 'errors.order.creation_failed'; +} + +class ShiftCreationFailedException extends OrderException { + const ShiftCreationFailedException({String? technicalMessage}) + : super(code: 'ORDER_004', technicalMessage: technicalMessage); + + @override + String get messageKey => 'errors.order.shift_creation_failed'; +} + +// ============================================================ +// PROFILE EXCEPTIONS +// ============================================================ + +sealed class ProfileException extends AppException { + const ProfileException({required super.code, super.technicalMessage}); +} + +class StaffProfileNotFoundException extends ProfileException { + const StaffProfileNotFoundException({String? technicalMessage}) + : super(code: 'PROFILE_001', technicalMessage: technicalMessage); + + @override + String get messageKey => 'errors.profile.staff_not_found'; +} + +class BusinessNotFoundException extends ProfileException { + const BusinessNotFoundException({String? technicalMessage}) + : super(code: 'PROFILE_002', technicalMessage: technicalMessage); + + @override + String get messageKey => 'errors.profile.business_not_found'; +} + +// ============================================================ +// NETWORK/GENERIC EXCEPTIONS +// ============================================================ + +class NetworkException extends AppException { + const NetworkException({String? technicalMessage}) + : super(code: 'NET_001', technicalMessage: technicalMessage); + + @override + String get messageKey => 'errors.generic.no_connection'; +} + +class UnknownException extends AppException { + const UnknownException({String? technicalMessage}) + : super(code: 'UNKNOWN', technicalMessage: technicalMessage); + + @override + String get messageKey => 'errors.generic.unknown'; +} +``` + +#### Phase 2: Add Localization Keys (30 minutes) + +**Update:** `packages/core_localization/lib/src/l10n/strings.i18n.yaml` + +```yaml +errors: + auth: + invalid_credentials: "The email or password you entered is incorrect." + account_exists: "An account with this email already exists. Try signing in instead." + session_expired: "Your session has expired. Please sign in again." + user_not_found: "We couldn't find your account. Please check your email and try again." + unauthorized_app: "This account is not authorized for this app." + weak_password: "Please choose a stronger password with at least 8 characters." + hub: + has_orders: "This hub has active orders and cannot be deleted." + not_found: "The hub you're looking for doesn't exist." + creation_failed: "We couldn't create the hub. Please try again." + order: + missing_hub: "Please select a location for your order." + missing_vendor: "Please select a vendor for your order." + creation_failed: "We couldn't create your order. Please try again." + shift_creation_failed: "We couldn't schedule the shift. Please try again." + profile: + staff_not_found: "Your profile couldn't be loaded. Please sign in again." + business_not_found: "Your business profile couldn't be loaded. Please sign in again." + generic: + unknown: "Something went wrong. Please try again." + no_connection: "No internet connection. Please check your network and try again." +``` + +#### Phase 3: Migrate Repositories (4-6 hours) + +**Example migration for auth_repository_impl.dart:** + +```dart +// BEFORE +if (e.code == 'invalid-credential' || e.code == 'wrong-password') { + throw Exception('Incorrect email or password.'); +} + +// AFTER +if (e.code == 'invalid-credential' || e.code == 'wrong-password') { + throw InvalidCredentialsException( + technicalMessage: 'Firebase error: ${e.code}', + ); +} +``` + +```dart +// BEFORE +if (e.code == 'email-already-in-use') { + throw Exception('An account already exists for that email address.'); +} + +// AFTER +if (e.code == 'email-already-in-use') { + throw AccountExistsException( + technicalMessage: 'Email: $email', + ); +} +``` + +```dart +// BEFORE +if (user == null) { + throw Exception('Authenticated user profile not found in database.'); +} + +// AFTER +if (user == null) { + throw UserNotFoundException( + technicalMessage: 'Firebase UID: $firebaseUserId not found in users table', + ); +} +``` + +#### Phase 4: Update BLoCs (2-3 hours) + +**Example for client_hubs_bloc.dart:** + +```dart +// BEFORE +Future _onDeleteRequested(...) async { + emit(state.copyWith(status: ClientHubsStatus.actionInProgress)); + try { + await _deleteHubUseCase(DeleteHubArguments(hubId: event.hubId)); + final List hubs = await _getHubsUseCase(); + emit(state.copyWith( + status: ClientHubsStatus.actionSuccess, + hubs: hubs, + successMessage: 'Hub deleted successfully', + )); + } catch (e) { + emit(state.copyWith( + status: ClientHubsStatus.actionFailure, + errorMessage: e.toString(), // EXPOSES TECHNICAL ERROR! + )); + } +} + +// AFTER +Future _onDeleteRequested(...) async { + emit(state.copyWith(status: ClientHubsStatus.actionInProgress)); + try { + await _deleteHubUseCase(DeleteHubArguments(hubId: event.hubId)); + final List hubs = await _getHubsUseCase(); + emit(state.copyWith( + status: ClientHubsStatus.actionSuccess, + hubs: hubs, + successMessageKey: 'success.hub.deleted', + )); + } on AppException catch (e) { + // Log technical details for debugging + debugPrint('Error ${e.code}: ${e.technicalMessage}'); + emit(state.copyWith( + status: ClientHubsStatus.actionFailure, + errorMessageKey: e.messageKey, + )); + } catch (e, stackTrace) { + // Unexpected error - log full details + debugPrint('Unexpected error: $e\n$stackTrace'); + emit(state.copyWith( + status: ClientHubsStatus.actionFailure, + errorMessageKey: 'errors.generic.unknown', + )); + } +} +``` + +#### Phase 5: Update UI Layer (1-2 hours) + +**Example for client_hubs_page.dart:** + +```dart +// BEFORE +listener: (context, state) { + if (state.errorMessage != null && state.errorMessage!.isNotEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(state.errorMessage!)), + ); + // ... + } +} + +// AFTER +listener: (context, state) { + if (state.errorMessageKey != null) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(t[state.errorMessageKey!])), + ); + // ... + } +} +``` + +--- + +## Summary of Required Changes + +### Immediate Fixes (This Sprint) + +| File | Change | Priority | +|------|--------|----------| +| `client_hubs_state.dart` | Add `clearErrorMessage`/`clearSuccessMessage` flags | P2 | +| `client_hubs_bloc.dart` | Use clear flags in `_onMessageCleared` | P2 | +| `view_order_card.dart` | Display `location` instead of `locationAddress` | P2 | + +### Short-term (Next Sprint) + +| Task | Effort | +|------|--------| +| Create `AppException` sealed class in domain | 1h | +| Add error localization keys | 30min | +| Migrate auth repositories | 2h | +| Migrate hub repositories | 1h | + +### Medium-term (Next 2-3 Sprints) + +| Task | Effort | +|------|--------| +| Migrate all repositories to AppException | 4-6h | +| Update all BLoCs for proper error handling | 2-3h | +| Update all UI components for localized errors | 1-2h | +| Add admin tool to reconcile orphaned Firebase accounts | 4h | + +--- + +## Appendix: Files Requiring Changes + +### Repositories with `throw Exception()` to migrate: + +1. `packages/features/client/authentication/lib/src/data/repositories_impl/auth_repository_impl.dart` (13 instances) +2. `packages/features/client/hubs/lib/src/data/repositories_impl/hub_repository_impl.dart` (6 instances) +3. `packages/features/client/create_order/lib/src/data/repositories_impl/client_create_order_repository_impl.dart` (6 instances) +4. `packages/features/client/view_orders/lib/src/data/repositories/view_orders_repository_impl.dart` (2 instances) +5. `packages/features/client/settings/lib/src/data/repositories_impl/settings_repository_impl.dart` (1 instance) +6. `packages/features/staff/authentication/lib/src/data/repositories_impl/auth_repository_impl.dart` (7 instances) +7. `packages/features/staff/shifts/lib/src/data/repositories_impl/shifts_repository_impl.dart` (2 instances) +8. `packages/features/staff/profile/lib/src/data/repositories/profile_repository_impl.dart` (3 instances) +9. `packages/features/staff/availability/lib/src/data/repositories_impl/availability_repository_impl.dart` (2 instances) +10. `packages/features/staff/clock_in/lib/src/data/repositories_impl/clock_in_repository_impl.dart` (3 instances) +11. And 10+ more files in staff profile sections... + +--- + +**Report prepared for development team review.** diff --git a/codemagic.yaml b/codemagic.yaml index 0ddeaa08..caee57e3 100644 --- a/codemagic.yaml +++ b/codemagic.yaml @@ -208,6 +208,9 @@ workflows: <<: *staff-app-base name: 🚛🤖👨‍🍳 Staff App Dev (Android App Distribution) environment: + flutter: stable + xcode: latest + cocoapods: default groups: - staff_app_dev_credentials vars: @@ -220,6 +223,9 @@ workflows: <<: *staff-app-base name: 🚛🤖👨‍🍳 Staff App Staging (Android App Distribution) environment: + flutter: stable + xcode: latest + cocoapods: default groups: - staff_app_staging_credentials vars: @@ -232,6 +238,9 @@ workflows: <<: *staff-app-base name: 🚛🤖👨‍🍳 Staff App Prod (Android App Distribution) environment: + flutter: stable + xcode: latest + cocoapods: default groups: - staff_app_prod_credentials vars: diff --git a/demos/m3/m3-notes.md b/demos/m3/m3-notes.md new file mode 100644 index 00000000..e4111452 --- /dev/null +++ b/demos/m3/m3-notes.md @@ -0,0 +1,63 @@ +# KROW M3 Demo — Test Feedback + +**Date:** February 3, 2026 + +--- + +## Demo 1: Register Business & Show Empty States (Client App) + +- **Flickering company name:** Every time I navigate to the home screen, I see "your company" for a moment before it changes to the real name. +- Creating a One-Time Order shows "No Vendors Available" — this is expected, OK. + +**Suggestions: Achintha:** +- We need to have a shimmer loading state while fetching data, to avoid flickering and empty states. + +--- + +## Demo 2: Register Staff & Show Empty States (Staff App) + +**Onboarding — Add preferred work locations:** +- Suggestion: Use Google Maps to suggest only city names. Currently users can type anything, which will cause misspellings and inconsistent data. Important for the max distance feature. + +**Home page:** +- Same flickering issue — shows "Krower" briefly before displaying the real name. + +**Profile page:** +- Phone number should be read-only, or require re-verification if changed. +- Emergency contact: "Save & Continue" works, but shouldn't we navigate to the profile page after? Other flows do this. +- Tax Documents: Would be great to add a file uploader where our AI could identify documents and prefill fields. +- Bank Account: Need to plan real bank verification (KYC)? Ensure the account is real and belongs to the user. Also, I can list banks but I don't see how to change/switch bank. + +**Home (empty state):** +- Clicking "Find shifts →" does nothing. But "Find Shifts" with the search icon works. + +**My Availability:** +- Working. Some latency, but OK for now. + +--- + +## Demo 5: Client Creates a New Hub + +- Hub editing feature seems missing — we'll need this for NFC configuration later. +- No confirmation before deleting a hub. + +--- + +## Demo 6: Client Creates New Order + +- "Up Next (x)" counter is confusing. I created 2 orders but it shows "Up Next (1)". Sometimes shows 0 when navigating, then back to 1. + +--- + +## Demo 8: Staff Logs In with Existing Account + +- If you accidentally click "Sign Up" with an existing phone number, you get stuck: + 1. OTP screen shows error: "This user already has a staff profile. Please log in" + 2. Clicking back → login → same OTP error loop + 3. Only fix: kill and restart the app + +--- + +## Demo 10: Staff Browses Available Shifts + +- **Blocker:** I don't see the shift I created as the Client. \ No newline at end of file diff --git a/demos/m3/m3.md b/demos/m3/m3.md new file mode 100644 index 00000000..31b86d29 --- /dev/null +++ b/demos/m3/m3.md @@ -0,0 +1,348 @@ +# KROW Workforce Platform — Feature Demo Plan for Milestone 3 + +**Version:** Milestone 3 (v3.0) +**Date:** February 3, 2026 +**Audience:** Business Stakeholders, Customer Engineers, Sales Teams +**Duration:** 25-30 minutes + +--- + +## 1️⃣ Demo Overview + +### Purpose + +This demo showcases the progress of the milestone 3. + +- **For Businesses (Client App):** One-time shift creation, worker management, real-time coverage tracking +- **For Workers (Staff App):** Easy access to available shifts, clock-in and profile management +- **Complete Workflow:** From shift posting and worker check-in and completion + +### Estimated Demo Duration + +**25-30 minutes** + +--- + +## 2️⃣ Demo Environment Setup + +### Required Test Accounts + +**Client Account (Business User):** +- Email: `legendary@krowd.com` +- Password: `Demo2026!` +- Client Name: "Krow" + +**Staff Account (Worker):** +- Phone: `+15557654321` +- OTP Code: `123456` (demo mode) +- Name: "Mariana Torres" + +### Prerequisites +1. ✅ Both apps installed on demo devices (or simulators) +2. ✅ Network connection stable +3. ✅ Seed data is ready to be populated (the database should be empty at start) + +### Make Commands Reference + +| Command | Purpose | +|---------|---------| +| `make dataconnect-clean` | Clean the database before seeding | +| `make dataconnect-seed` | Populate the database with seed data for demo | + +### Recent Fixes Applied +- ✅ Fixed 2 bugs on TaxForm: marital status and Citizenship Status now properly saved +- ✅ Fixed update screen after create or update TaxForm +- ✅ Created seed data script +- ✅ Created make commands to create and delete information in DataConnect + +--- + +## 3️⃣ Demo Flows + +### Demo 0: Show Empty Database +**Purpose:** Demonstrate the starting point before any data exists + +**Steps:** +1. Run `make dataconnect-clean` to ensure database is empty +2. Show the empty database in Firebase console + +--- + +### Demo 1: Register Business & Show Empty States (Client App) +**Purpose:** Show the client onboarding experience and empty states + +**Steps:** +1. Open Client App → Tap "Create Account" +2. Enter business email, and password +3. Navigate to home page +4. **Point out:** Empty dashboard, no orders, no workers, clean slate + +--- + +### Demo 2: Register Staff & Show Empty States (Staff App) +**Purpose:** Show the worker onboarding experience and empty states + +**Steps:** +1. Open Staff App → Tap "Sign Up" +2. Enter phone number and verify with OTP code +3. Follow the onboarding process +4. Navigate to home page +5. **Point out:** Empty shifts list, no available work yet + +--- + +### 🔄 PAUSE: Populate Database + +Run the seeding command: +```bash +make dataconnect-seed +``` + +--- + +### Demo 3: Client Logs In with Existing Account +**Purpose:** Show the sign-in experience for returning users + +**Screen:** Get Started → Sign In + +**Steps:** +1. Restart Client App +2. Tap "Sign In" button +3. Enter credentials: + - Email: `legendary@krowd.com` + - Password: `Demo2026!` +4. Tap "Sign In" + +--- + +### Demo 4: Client Views Populated Dashboard +**Purpose:** Show how the client app displays active operations + +**Steps:** +1. After signing in, observe the home screen +2. Navigate through populated sections: + - Home: Coverage stats, upcoming shifts + - Orders: Posted shifts with workers assigned + - Coverage: Real-time worker status + +**What to Notice:** +- Coverage percentage for today's shifts +- Workers checked in vs. needed +- Late workers alerts +- Today's estimated labor cost + +--- + +### Demo 5: Client Creates a New Hub +**Screen:** Hubs Tab → "Add Hub" button + +**Steps:** +1. Navigate to Hubs tab in bottom navigation +2. Tap the "+" or "Add Hub" button +3. Fill in hub details: + - Hub name: "Downtown Convention Center" + - Address: Start typing and select from Google Places autocomplete +4. Tap "Create Hub" +5. See the new hub appear in the hubs list + +--- + +### 📋 Main Demo Flow Explanation + +``` +Client Posts Shift [O1] + ↓ +*Vendor Accepts the Shift (Missing for now) / Vendor is selected by client* [O2] + ↓ +Worker Searches for a Shift [O3] + ↓ +Worker Applies [O4] + ↓ +Confirmation (Missing for now, auto-confirmed)* [O5] + ↓ +Worker Checks In [O6] + ↓ +Shift Completed [O7] +``` + +--- + +### Demo 6: Client Creates New Order - [O1] +**Purpose:** Walk through the shift creation process + +**Screen:** Orders Tab → "Post" button + +**What to Fill:** +- Order name: "Spring Gala 2026" +- Date: [Select upcoming date] +- Location: [Select existing hub] +- Add position: Server, Count: 3, Hours: 5PM-9PM + +--- + +### Demo 7: Client Views Order Details +**Purpose:** Show detailed shift information and worker assignments (second part is missing for now) + +**Screen:** Orders Tab → Tap on any order card + +**What to Notice:** +- Event name and location +- Roles needed (e.g., "2 Servers") +- Clock in/out times +- Estimated cost +- Coverage percentage bar + +--- + +### Demo 8: Staff Logs In with Existing Account +**Purpose:** Show the worker sign-in experience + +**Screen:** Get Started → Sign In with Phone + +**Steps:** +1. Restart the staff app +2. Enter phone number: `5557654321` +3. Tap "Send Code" +4. Enter OTP: `123456` + +--- + +### Demo 9: Staff Views Home Dashboard +**Purpose:** Show worker's personalized dashboard + +**What to Notice:** +- Today's Shifts section (confirmed shifts for today) +- Tomorrow's Shifts section + +--- + +### Demo 10: Staff Browses Available Shifts - [O3] +**Purpose:** Show how workers discover and view available work + +**Screen:** Shifts → "Find Work" + +**What to Notice:** +- List of shifts matching worker skills +- Hourly rate prominently displayed +- Role requirements (e.g., "Bartender - Spring Gala") +- Date, time, and duration + +--- + +### Demo 11: Staff Applies for Shift - [O4] +**Purpose:** Show the application process from worker side + +**Screen:** Shift Details → "Book" Shift button + +**Steps:** +1. Tap on an available shift to view details +2. Review business name, location, pay, requirements +3. Tap "Book" Shift button +4. See confirmation + +--- + +### Demo 12: Staff Views Confirmed Shifts - [O5] +**Purpose:** Show worker's shift management interface + +**Screen:** Shifts Tab → "My Shifts" + +**What to Notice:** +- Week-by-week calendar navigation +- Color-coded status (Confirmed, Pending, Completed) +- Quick access to shift details and directions + +--- + +### Demo 13: Client Monitors Coverage Dashboard - [O5] +**Purpose:** Show real-time worker tracking capabilities + +**Screen:** Client App → Coverage Tab + +**What to Notice:** +- Live worker status (Checked In, En Route, Late, Not Arrived) +- Color-coded status badges (green, yellow, red) +- Worker information + +--- + +### Demo 14: Staff Clock-In to Shift (Day of Event) - [O6] +**Purpose:** Demonstrate the clock-in process +**Screen:** Clockin page → "Clock In" slider + +**What to Notice:** +- Timestamp automatically recorded +- Status changes to "Checked In" with green indicator + +--- + +### Demo 15: Client Sees Clock-In Update - [O6] +**Purpose:** Show cross-app interaction and real-time updates + +**Screen:** Client App → Coverage Tab + +**Action:** Press the update button on the top right to refresh worker statuses + +**What to Notice:** +- Status update +- User status changes to "Checked In" +- Check-in time displayed + +--- + +### Demo 16: Staff Clocks-Out of Shift - [O7] +**Purpose:** Demonstrate the clocks-out process and shift completion +**Screen:** Clockin page -> Clock-out slider + +**What to Notice:** +- Clock-out timestamp automatically recorded +- Status changes to "Completed" +- Total hours worked calculated automatically + +--- + +### Demo 17: Client Views Completed Shift in Coverage - [O7] +**Purpose:** Show how completed shifts appear in the client app + +**Screen:** Client App → Coverage Tab + +**Action:** Press the refresh button to update worker statuses + +**What to Notice:** +- Worker status changes to "Completed" +- Check-out time displayed alongside check-in time +- Total hours worked visible +- Shift marked as complete in orders list +- Cost finalized based on actual hours + +--- + +### Demo 18: Staff Profile Management +**Purpose:** Demonstrate worker profile features and compliance management + +**Screen:** Staff App → Profile Tab + +**Steps:** +1. Navigate to Profile tab in bottom navigation +2. Review profile sections: + - **Profile Info:** + - **Emergency Contact:** Name, relationship, phone number + - **Bank Account:** Linked payment account for direct deposit + - **Tax Forms:** W-9, I-9 compliance documents *(bugs fixed: marital status and Citizenship Status now work properly)* + - **Time Card:** Historical shift records with hours and earnings + +--- + +## 4️⃣ Customer Handover Checklist + +### Deliverables + +- [ ] Android apps (Client and Staff) +- [ ] Demo account credentials (see below) + +### Demo Accounts + +| Account | Credentials | +|---------|-------------| +| **Client** | Email: `legendary@krowd.com` / Password: `Demo2026!` | +| **Staff** | Phone: `+15557654321` / OTP: `123456` (demo mode) | diff --git a/docs/QA_TESTING_CHECKLIST.md b/docs/QA_TESTING_CHECKLIST.md new file mode 100644 index 00000000..49fef7e4 --- /dev/null +++ b/docs/QA_TESTING_CHECKLIST.md @@ -0,0 +1,908 @@ +# 🧪 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 + +**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 + +**Loading & Empty States:** +- [ ] Loading spinner displays during authentication +- [ ] OAuth redirect shows appropriate loading state + +--- + +#### 📱 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 + +**Validation & Error States:** +- [ ] Empty state shows "No data available" when no orders exist + +**Loading & Empty States:** +- [ ] Empty coverage shows "No shifts today" +- [ ] Empty reorders shows "No recent orders" + +--- + +#### 📱 CLIENT-003: Create Order + +**Applications:** Client +**Entry Points:** +- Home → Create Order button +- Orders tab → + FAB button +- Order type → One-Time + +**Happy Path Test Cases:** +- [ ] Order type selection displays. +- [ ] Hub selection shows list of business hubs +- [ ] 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 +- [ ] 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 + +--- + +#### 📱 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 + +- [ ] Missing staff data shows placeholder + +**Loading & Empty States:** +- [ ] Empty date shows "No orders for this date" +- [ ] Empty accepted applications shows "No confirmed staff" + +--- + +#### 📱 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 + +**Loading & Empty States:** + +- [ ] 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 + +**Loading & Empty States:** +- [ ] Empty pending invoices shows "No pending invoices" +- [ ] Empty history shows "No invoice history" +- [ ] Empty spending breakdown shows "No spending data" + + +--- + +#### 📱 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) + +**Loading & Empty States:** +- [ ] Empty hubs list shows "No hubs configured" +- [ ] Hub creation shows loading spinner + +--- + +#### 📱 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 + +--- + +#### 📱 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 + +--- + +### 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 + +**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 + +--- + +#### 📱 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 + +**Loading & Empty States:** +- [ ] Empty today's shifts shows "No shifts today" +- [ ] Empty recommended shows "No available shifts" + +--- + +#### 📱 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 + +--- + +#### 📱 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 +- [ ] Already applied shift prevents duplicate application +- [ ] Past shifts cannot be applied to +- [ ] Cancelled shifts show cancellation reason + +--- + +#### 📱 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:** + +- [ ] Save failure shows error message + +**Loading & Empty States:** +- [ ] Loading spinner displays while fetching availability +- [ ] Default state shows all unavailable + +--- + +#### 📱 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 + +- [ ] Clock in outside shift time shows warning + +**Loading & Empty States:** +- [ ] Loading spinner displays while fetching shift +- [ ] Empty state shows "No shifts scheduled" + +--- + +#### 📱 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 + +**Loading & Empty States:** +- [ ] Empty history shows "No payment history" + +--- + +#### 📱 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 + +- [ ] Photo upload failure shows error + +**Loading & Empty States:** +- [ ] Form loads with skeleton placeholders +- [ ] Photo upload shows progress indicator +- [ ] Save button shows loading spinner + +--- + +#### 📱 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) + +**Loading & Empty States:** +- [ ] Loading spinner displays while fetching contacts +- [ ] Empty state shows "No emergency contacts" +- [ ] Save button shows loading spinner + +--- + +#### 📱 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) + + +**Loading & Empty States:** +- [ ] Loading spinner displays while fetching data +- [ ] Save button shows loading spinner + +--- + +#### 📱 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 + + +**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" + +**Loading & Empty States:** +- [ ] Empty state shows "No time card history" + +--- + +#### 📱 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 + +**Loading & Empty States:** +- [ ] Loading spinner displays while fetching forms +- [ ] Form editor loads with skeleton placeholders +- [ ] Save button shows loading spinner + +--- + +#### 📱 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 + +--- diff --git a/internal/launchpad/allowed-hashes.json b/internal/launchpad/allowed-hashes.json index 55e478d5..4340efd4 100644 --- a/internal/launchpad/allowed-hashes.json +++ b/internal/launchpad/allowed-hashes.json @@ -4,6 +4,11 @@ "8205f6c72d8e97358fe7638b02503c17b4bfd4a397c143ea27151f3f106720d4", "994b31c1aef3d59fe59bc3b8e1dec860a6fb3c73cbf41bdf45028e2c1ecbcf7a", "69d44565e675b319114ac2fa7f9708d102a37a75ab3d7628b305c37d185dd2ed", + "b511f4e5680cacb047007752a83a032eb421f76c8dd2bd9ecfac2fa10288a781", + "7535ebe4fafb400148f9d8649fead0a2a39887856baa937dd54f7f3fe7df9651", + "4e2a3b5d11b760489426bb7a4acd7f4d4b7b79b8810ee8af0c6d2433f089cc5b", + "5e8b584b58479970ff6b8bde5375ef2384d2d751513482c5df142ebf58b398ca", + "3416519f8ac6069082697cce71adc1923e5889b93c59038f51e1604878534972", "99144d7e873dd0ab56c185d5f42fee2048b1be73513cf36dc1df3eae1e524489", "088aa009feb3ac78d37242c9abc339aacf42665dccdb626415590148855d0623" ] \ No newline at end of file diff --git a/internal/launchpad/iap-users.txt b/internal/launchpad/iap-users.txt index 7cad216a..a4f95cb0 100644 --- a/internal/launchpad/iap-users.txt +++ b/internal/launchpad/iap-users.txt @@ -15,6 +15,13 @@ user:achintha.isuru@oloodi.com # External users - Legendary employees user:iortega@legendaryeventstaff.com +# External users - TenExt +user:gokulraj@tenext.in +user:suriya@tenext.in +user:dhinesh@tenext.in +user:ilahi@tenext.in +user:abhishek@tenext.in + # External users - gmail.com user:fazulilahi@gmail.com user:zouantchaw74@gmail.com \ No newline at end of file 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 = ` diff --git a/krow-workforce-web.code-workspace b/krow-workforce-web.code-workspace new file mode 100644 index 00000000..876a1499 --- /dev/null +++ b/krow-workforce-web.code-workspace @@ -0,0 +1,8 @@ +{ + "folders": [ + { + "path": "." + } + ], + "settings": {} +} \ No newline at end of file diff --git a/makefiles/dataconnect.mk b/makefiles/dataconnect.mk index d1be5e32..ba7298ef 100644 --- a/makefiles/dataconnect.mk +++ b/makefiles/dataconnect.mk @@ -1,6 +1,6 @@ # --- Data Connect / Backend --- -.PHONY: dataconnect-enable-apis dataconnect-init dataconnect-deploy dataconnect-sql-migrate dataconnect-generate-sdk dataconnect-sync dataconnect-bootstrap-db check-gcloud-beta +.PHONY: dataconnect-enable-apis dataconnect-init dataconnect-deploy dataconnect-sql-migrate dataconnect-generate-sdk dataconnect-sync dataconnect-bootstrap-db check-gcloud-beta dataconnect-clean # Enable all required APIs for Firebase Data Connect + Cloud SQL dataconnect-enable-apis: @@ -50,9 +50,16 @@ dataconnect-sync: # Execute seed in Firebase Data Connect dataconnect-seed: @echo "--> Exec seed in Firebase Data Connect..." - @firebase dataconnect:execute seeds/seed_min.graphql --project=$(FIREBASE_ALIAS) + @firebase dataconnect:execute backend/dataconnect/functions/seed.gql --project=$(FIREBASE_ALIAS) @echo "✅ Seed executed successfully." + +# Execute clean, to delete all the data in Firebase Data Connect +dataconnect-clean: + @echo "--> Exec clean all the data in Firebase Data Connect..." + @firebase dataconnect:execute backend/dataconnect/functions/clean.gql --project=$(FIREBASE_ALIAS) + @echo "✅ Clean information executed successfully." + # Run tests for Data Connect deployment and migrations dataconnect-test: @echo "--> Running Data Connect tests..." diff --git a/makefiles/launchpad.mk b/makefiles/launchpad.mk index 8dbe860f..87050e19 100644 --- a/makefiles/launchpad.mk +++ b/makefiles/launchpad.mk @@ -2,13 +2,13 @@ .PHONY: launchpad-dev deploy-launchpad-hosting -launchpad-dev: +launchpad-dev: sync-prototypes @echo "--> Starting local Launchpad server using Firebase Hosting emulator..." @echo " - Generating secure email hashes..." @node scripts/generate-allowed-hashes.js @firebase serve --only hosting:launchpad --project=$(FIREBASE_ALIAS) -deploy-launchpad-hosting: +deploy-launchpad-hosting: sync-prototypes @echo "--> Deploying Internal Launchpad to Firebase Hosting..." @echo " - Generating secure email hashes..." @node scripts/generate-allowed-hashes.js diff --git a/makefiles/mobile.mk b/makefiles/mobile.mk index b3bb2a84..fce6c43c 100644 --- a/makefiles/mobile.mk +++ b/makefiles/mobile.mk @@ -1,11 +1,15 @@ # --- Mobile App Development --- -.PHONY: mobile-install mobile-info mobile-client-dev-android mobile-staff-dev-android mobile-client-build mobile-staff-build +.PHONY: mobile-install mobile-info mobile-client-dev-android mobile-staff-dev-android mobile-client-build mobile-staff-build mobile-hot-reload mobile-hot-restart MOBILE_DIR := apps/mobile +# Device ID for running mobile apps (override with DEVICE=) +# Find your device ID with: flutter devices +DEVICE ?= android + # --- General --- -mobile-install: install-melos +mobile-install: install-melos dataconnect-generate-sdk @echo "--> Bootstrapping mobile workspace (Melos)..." @cd $(MOBILE_DIR) && melos bootstrap @echo "--> Generating localization files..." @@ -15,12 +19,27 @@ mobile-info: @echo "--> Fetching mobile command info..." @cd $(MOBILE_DIR) && melos run info -# --- Client App --- -mobile-client-dev-android: - @echo "--> Running client app on Android..." - @cd $(MOBILE_DIR) && melos run start:client -- -d android +# --- Hot Reload & Restart --- +mobile-hot-reload: + @echo "--> Triggering hot reload for running Flutter app..." + @cd $(MOBILE_DIR) && echo "r" | nc localhost 54321 2>/dev/null || \ + (cd apps/client && flutter attach --pid-file /tmp/flutter_client.pid && echo "r") || \ + (cd apps/staff && flutter attach --pid-file /tmp/flutter_staff.pid && echo "r") || \ + echo "❌ No running Flutter app found. Start an app first with mobile-client-dev-android or mobile-staff-dev-android" -mobile-client-build: +mobile-hot-restart: + @echo "--> Triggering hot restart for running Flutter app..." + @cd $(MOBILE_DIR) && echo "R" | nc localhost 54321 2>/dev/null || \ + (cd apps/client && flutter attach --pid-file /tmp/flutter_client.pid && echo "R") || \ + (cd apps/staff && flutter attach --pid-file /tmp/flutter_staff.pid && echo "R") || \ + echo "❌ No running Flutter app found. Start an app first with mobile-client-dev-android or mobile-staff-dev-android" + +# --- Client App --- +mobile-client-dev-android: dataconnect-generate-sdk + @echo "--> Running client app on Android (device: $(DEVICE))..." + @cd $(MOBILE_DIR) && melos run start:client -- -d $(DEVICE) --dart-define-from-file=../../config.dev.json + +mobile-client-build: dataconnect-generate-sdk @if [ -z "$(PLATFORM)" ]; then \ echo "ERROR: PLATFORM is required (e.g. make mobile-client-build PLATFORM=apk)"; exit 1; \ fi @@ -29,14 +48,14 @@ mobile-client-build: @cd $(MOBILE_DIR) && \ melos exec --scope="core_localization" -- "dart run slang" && \ melos exec --scope="core_localization" -- "dart run build_runner build --delete-conflicting-outputs" && \ - melos exec --scope="krowwithus_client" -- "flutter build $(PLATFORM) --$(MODE)" + melos exec --scope="krowwithus_client" -- "flutter build $(PLATFORM) --$(MODE) --dart-define-from-file=../../config.dev.json" # --- Staff App --- -mobile-staff-dev-android: - @echo "--> Running staff app on Android..." - @cd $(MOBILE_DIR) && melos run start:staff -- -d android +mobile-staff-dev-android: dataconnect-generate-sdk + @echo "--> Running staff app on Android (device: $(DEVICE))..." + @cd $(MOBILE_DIR) && melos run start:staff -- -d $(DEVICE) -mobile-staff-build: +mobile-staff-build: dataconnect-generate-sdk @if [ -z "$(PLATFORM)" ]; then \ echo "ERROR: PLATFORM is required (e.g. make mobile-staff-build PLATFORM=apk)"; exit 1; \ fi