seed data
This commit is contained in:
64
.github/workflows/backend-foundation.yml
vendored
Normal file
64
.github/workflows/backend-foundation.yml
vendored
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
name: Backend Foundation
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- dev
|
||||||
|
- main
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- dev
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
backend-foundation-makefile:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Validate backend make targets
|
||||||
|
run: |
|
||||||
|
make backend-help
|
||||||
|
make help | grep "backend-"
|
||||||
|
|
||||||
|
- name: Dry-run backend automation targets
|
||||||
|
run: |
|
||||||
|
make -n backend-enable-apis ENV=dev
|
||||||
|
make -n backend-bootstrap-dev ENV=dev
|
||||||
|
make -n backend-deploy-core ENV=dev
|
||||||
|
make -n backend-deploy-commands ENV=dev
|
||||||
|
make -n backend-deploy-workers ENV=dev
|
||||||
|
make -n backend-smoke-core ENV=dev
|
||||||
|
make -n backend-smoke-commands ENV=dev
|
||||||
|
make -n backend-logs-core ENV=dev
|
||||||
|
|
||||||
|
backend-services-tests:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
service:
|
||||||
|
- backend/core-api
|
||||||
|
- backend/command-api
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: ${{ matrix.service }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
cache: npm
|
||||||
|
cache-dependency-path: ${{ matrix.service }}/package-lock.json
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
env:
|
||||||
|
AUTH_BYPASS: "true"
|
||||||
|
LLM_MOCK: "true"
|
||||||
|
run: npm test
|
||||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -43,6 +43,7 @@ lerna-debug.log*
|
|||||||
*.temp
|
*.temp
|
||||||
tmp/
|
tmp/
|
||||||
temp/
|
temp/
|
||||||
|
scripts/issues-to-create.md
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# SECURITY (CRITICAL)
|
# SECURITY (CRITICAL)
|
||||||
@@ -186,3 +187,9 @@ krow-workforce-export-latest/
|
|||||||
# Data Connect Generated SDKs (Explicit)
|
# Data Connect Generated SDKs (Explicit)
|
||||||
apps/mobile/packages/data_connect/lib/src/dataconnect_generated/
|
apps/mobile/packages/data_connect/lib/src/dataconnect_generated/
|
||||||
apps/web/src/dataconnect-generated/
|
apps/web/src/dataconnect-generated/
|
||||||
|
|
||||||
|
|
||||||
|
AGENTS.md
|
||||||
|
CLAUDE.md
|
||||||
|
GEMINI.md
|
||||||
|
TASKS.md
|
||||||
|
|||||||
30
CHANGELOG.md
Normal file
30
CHANGELOG.md
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# KROW Workforce Change Log
|
||||||
|
|
||||||
|
| Date | Version | Change |
|
||||||
|
|---|---|---|
|
||||||
|
| 2026-02-24 | 0.1.0 | Confirmed dev owner access and current runtime baseline in `krow-workforce-dev`. |
|
||||||
|
| 2026-02-24 | 0.1.1 | Added backend foundation implementation plan document. |
|
||||||
|
| 2026-02-24 | 0.1.2 | Added API implementation contract and transition route aliases. |
|
||||||
|
| 2026-02-24 | 0.1.3 | Added auth-first security policy with deferred role-map integration hooks. |
|
||||||
|
| 2026-02-24 | 0.1.4 | Locked defaults for idempotency, validation, bucket split, model provider, and p95 objectives. |
|
||||||
|
| 2026-02-24 | 0.1.5 | Added backend makefile module and CI workflow for backend target validation. |
|
||||||
|
| 2026-02-24 | 0.1.6 | Added Cloud SQL-backed idempotency storage, migration script, and command API test coverage. |
|
||||||
|
| 2026-02-24 | 0.1.7 | Added `/health` endpoints and switched smoke checks to `/health` for Cloud Run compatibility. |
|
||||||
|
| 2026-02-24 | 0.1.8 | Enabled dev frontend reachability and made deploy auth mode environment-aware (`dev` public, `staging` private). |
|
||||||
|
| 2026-02-24 | 0.1.9 | Switched core API from mock behavior to real GCS upload/signed URLs and real Vertex model calls in dev deployment. |
|
||||||
|
| 2026-02-24 | 0.1.10 | Hardened core APIs with signed URL ownership/expiry checks, object existence checks, and per-user LLM rate limiting. |
|
||||||
|
| 2026-02-24 | 0.1.11 | Added frontend-ready core API guide and linked M4 API catalog to it as source of truth for consumption. |
|
||||||
|
| 2026-02-24 | 0.1.12 | Reduced M4 API docs to core-only scope and removed command-route references until command implementation is complete. |
|
||||||
|
| 2026-02-24 | 0.1.13 | Added verification architecture contract with endpoint design and workflow split for attire, government ID, and certification. |
|
||||||
|
| 2026-02-24 | 0.1.14 | Implemented core verification endpoints in dev and updated frontend/API docs with live verification route contracts. |
|
||||||
|
| 2026-02-24 | 0.1.15 | Added live Vertex Flash Lite attire verification path and third-party adapter scaffolding for government ID and certification checks. |
|
||||||
|
| 2026-02-24 | 0.1.16 | Added M4 target schema blueprint doc with first-principles modular model, constraints, and migration phases. |
|
||||||
|
| 2026-02-24 | 0.1.17 | Added full current-schema mermaid model relationship map to the M4 target schema blueprint. |
|
||||||
|
| 2026-02-24 | 0.1.18 | Updated schema blueprint with explicit multi-tenant stakeholder model and phased RBAC rollout with shadow mode before enforcement. |
|
||||||
|
| 2026-02-24 | 0.1.19 | Added customer stakeholder-wheel mapping and future stakeholder extension model to the M4 schema blueprint. |
|
||||||
|
| 2026-02-25 | 0.1.20 | Added roadmap CSV schema-reconciliation document with stakeholder capability matrix and concrete schema gap analysis. |
|
||||||
|
| 2026-02-25 | 0.1.21 | Updated target schema blueprint with roadmap-evidence section plus attendance/offense, stakeholder-network, and settlement-table coverage. |
|
||||||
|
| 2026-02-25 | 0.1.22 | Updated core actor scenarios with explicit business and vendor user partitioning via membership tables. |
|
||||||
|
| 2026-02-25 | 0.1.23 | Updated schema blueprint and reconciliation docs to add `business_memberships` and `vendor_memberships` as first-class data actors. |
|
||||||
|
| 2026-02-25 | 0.1.24 | Removed stale `m4-discrepencies.md` document from M4 planning docs cleanup. |
|
||||||
|
| 2026-02-25 | 0.1.25 | Added target schema model catalog with keys and domain relationship diagrams for slide/workshop use. |
|
||||||
129
CLAUDE.md
129
CLAUDE.md
@@ -1,129 +0,0 @@
|
|||||||
# 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=<device_id>
|
|
||||||
|
|
||||||
# Run staff app
|
|
||||||
make mobile-staff-dev-android DEVICE=<device_id>
|
|
||||||
|
|
||||||
# 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=<your_device_id>`
|
|
||||||
|
|
||||||
## 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
|
|
||||||
```
|
|
||||||
138
GEMINI.md
138
GEMINI.md
@@ -1,138 +0,0 @@
|
|||||||
# 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=<device_id>
|
|
||||||
|
|
||||||
# Run staff app
|
|
||||||
make mobile-staff-dev-android DEVICE=<device_id>
|
|
||||||
|
|
||||||
# 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=<your_device_id>`
|
|
||||||
|
|
||||||
## 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)
|
|
||||||
17
Makefile
17
Makefile
@@ -11,6 +11,7 @@ include makefiles/web.mk
|
|||||||
include makefiles/launchpad.mk
|
include makefiles/launchpad.mk
|
||||||
include makefiles/mobile.mk
|
include makefiles/mobile.mk
|
||||||
include makefiles/dataconnect.mk
|
include makefiles/dataconnect.mk
|
||||||
|
include makefiles/backend.mk
|
||||||
include makefiles/tools.mk
|
include makefiles/tools.mk
|
||||||
|
|
||||||
# --- Main Help Command ---
|
# --- Main Help Command ---
|
||||||
@@ -55,6 +56,9 @@ help:
|
|||||||
@echo " make mobile-test Run flutter test for client+staff"
|
@echo " make mobile-test Run flutter test for client+staff"
|
||||||
@echo " make mobile-hot-reload Hot reload running Flutter app"
|
@echo " make mobile-hot-reload Hot reload running Flutter app"
|
||||||
@echo " make mobile-hot-restart Hot restart running Flutter app"
|
@echo " make mobile-hot-restart Hot restart running Flutter app"
|
||||||
|
@echo " make test-e2e Run full Maestro E2E suite (Client + Staff auth)"
|
||||||
|
@echo " make test-e2e-client Run Client Maestro E2E only"
|
||||||
|
@echo " make test-e2e-staff Run Staff Maestro E2E only"
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo " 🗄️ DATA CONNECT & BACKEND (backend/dataconnect)"
|
@echo " 🗄️ DATA CONNECT & BACKEND (backend/dataconnect)"
|
||||||
@echo " ────────────────────────────────────────────────────────────────────"
|
@echo " ────────────────────────────────────────────────────────────────────"
|
||||||
@@ -71,6 +75,19 @@ help:
|
|||||||
@echo " make dataconnect-bootstrap-validation-database ONE-TIME: Setup validation database"
|
@echo " make dataconnect-bootstrap-validation-database ONE-TIME: Setup validation database"
|
||||||
@echo " make dataconnect-backup-dev-to-validation Backup dev database to validation"
|
@echo " make dataconnect-backup-dev-to-validation Backup dev database to validation"
|
||||||
@echo ""
|
@echo ""
|
||||||
|
@echo " ☁️ BACKEND FOUNDATION (Cloud Run + Workers)"
|
||||||
|
@echo " ────────────────────────────────────────────────────────────────────"
|
||||||
|
@echo " make backend-help Show backend foundation commands"
|
||||||
|
@echo " make backend-enable-apis [ENV=dev] Enable backend GCP APIs"
|
||||||
|
@echo " make backend-bootstrap-dev Bootstrap backend foundation resources (dev)"
|
||||||
|
@echo " make backend-migrate-idempotency Create/upgrade command idempotency table"
|
||||||
|
@echo " make backend-deploy-core [ENV=dev] Build and deploy core API service"
|
||||||
|
@echo " make backend-deploy-commands [ENV=dev] Build and deploy command API service"
|
||||||
|
@echo " make backend-deploy-workers [ENV=dev] Deploy async worker functions scaffold"
|
||||||
|
@echo " make backend-smoke-core [ENV=dev] Run health smoke test for core service (/health)"
|
||||||
|
@echo " make backend-smoke-commands [ENV=dev] Run health smoke test for command service (/health)"
|
||||||
|
@echo " make backend-logs-core [ENV=dev] Tail/read logs for core service"
|
||||||
|
@echo ""
|
||||||
@echo " 🛠️ DEVELOPMENT TOOLS"
|
@echo " 🛠️ DEVELOPMENT TOOLS"
|
||||||
@echo " ────────────────────────────────────────────────────────────────────"
|
@echo " ────────────────────────────────────────────────────────────────────"
|
||||||
@echo " make install-melos Install Melos globally (for mobile dev)"
|
@echo " make install-melos Install Melos globally (for mobile dev)"
|
||||||
|
|||||||
@@ -15,6 +15,11 @@ import io.flutter.embedding.engine.FlutterEngine;
|
|||||||
public final class GeneratedPluginRegistrant {
|
public final class GeneratedPluginRegistrant {
|
||||||
private static final String TAG = "GeneratedPluginRegistrant";
|
private static final String TAG = "GeneratedPluginRegistrant";
|
||||||
public static void registerWith(@NonNull FlutterEngine flutterEngine) {
|
public static void registerWith(@NonNull FlutterEngine flutterEngine) {
|
||||||
|
try {
|
||||||
|
flutterEngine.getPlugins().add(new com.mr.flutter.plugin.filepicker.FilePickerPlugin());
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error registering plugin file_picker, com.mr.flutter.plugin.filepicker.FilePickerPlugin", e);
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
flutterEngine.getPlugins().add(new io.flutter.plugins.firebase.appcheck.FlutterFirebaseAppCheckPlugin());
|
flutterEngine.getPlugins().add(new io.flutter.plugins.firebase.appcheck.FlutterFirebaseAppCheckPlugin());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@@ -30,6 +35,16 @@ public final class GeneratedPluginRegistrant {
|
|||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Log.e(TAG, "Error registering plugin firebase_core, io.flutter.plugins.firebase.core.FlutterFirebaseCorePlugin", e);
|
Log.e(TAG, "Error registering plugin firebase_core, io.flutter.plugins.firebase.core.FlutterFirebaseCorePlugin", e);
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
flutterEngine.getPlugins().add(new io.flutter.plugins.flutter_plugin_android_lifecycle.FlutterAndroidLifecyclePlugin());
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error registering plugin flutter_plugin_android_lifecycle, io.flutter.plugins.flutter_plugin_android_lifecycle.FlutterAndroidLifecyclePlugin", e);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
flutterEngine.getPlugins().add(new io.flutter.plugins.imagepicker.ImagePickerPlugin());
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error registering plugin image_picker_android, io.flutter.plugins.imagepicker.ImagePickerPlugin", e);
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
flutterEngine.getPlugins().add(new io.flutter.plugins.pathprovider.PathProviderPlugin());
|
flutterEngine.getPlugins().add(new io.flutter.plugins.pathprovider.PathProviderPlugin());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
|||||||
@@ -6,6 +6,12 @@
|
|||||||
|
|
||||||
#import "GeneratedPluginRegistrant.h"
|
#import "GeneratedPluginRegistrant.h"
|
||||||
|
|
||||||
|
#if __has_include(<file_picker/FilePickerPlugin.h>)
|
||||||
|
#import <file_picker/FilePickerPlugin.h>
|
||||||
|
#else
|
||||||
|
@import file_picker;
|
||||||
|
#endif
|
||||||
|
|
||||||
#if __has_include(<firebase_app_check/FLTFirebaseAppCheckPlugin.h>)
|
#if __has_include(<firebase_app_check/FLTFirebaseAppCheckPlugin.h>)
|
||||||
#import <firebase_app_check/FLTFirebaseAppCheckPlugin.h>
|
#import <firebase_app_check/FLTFirebaseAppCheckPlugin.h>
|
||||||
#else
|
#else
|
||||||
@@ -24,6 +30,12 @@
|
|||||||
@import firebase_core;
|
@import firebase_core;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#if __has_include(<image_picker_ios/FLTImagePickerPlugin.h>)
|
||||||
|
#import <image_picker_ios/FLTImagePickerPlugin.h>
|
||||||
|
#else
|
||||||
|
@import image_picker_ios;
|
||||||
|
#endif
|
||||||
|
|
||||||
#if __has_include(<shared_preferences_foundation/SharedPreferencesPlugin.h>)
|
#if __has_include(<shared_preferences_foundation/SharedPreferencesPlugin.h>)
|
||||||
#import <shared_preferences_foundation/SharedPreferencesPlugin.h>
|
#import <shared_preferences_foundation/SharedPreferencesPlugin.h>
|
||||||
#else
|
#else
|
||||||
@@ -39,9 +51,11 @@
|
|||||||
@implementation GeneratedPluginRegistrant
|
@implementation GeneratedPluginRegistrant
|
||||||
|
|
||||||
+ (void)registerWithRegistry:(NSObject<FlutterPluginRegistry>*)registry {
|
+ (void)registerWithRegistry:(NSObject<FlutterPluginRegistry>*)registry {
|
||||||
|
[FilePickerPlugin registerWithRegistrar:[registry registrarForPlugin:@"FilePickerPlugin"]];
|
||||||
[FLTFirebaseAppCheckPlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTFirebaseAppCheckPlugin"]];
|
[FLTFirebaseAppCheckPlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTFirebaseAppCheckPlugin"]];
|
||||||
[FLTFirebaseAuthPlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTFirebaseAuthPlugin"]];
|
[FLTFirebaseAuthPlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTFirebaseAuthPlugin"]];
|
||||||
[FLTFirebaseCorePlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTFirebaseCorePlugin"]];
|
[FLTFirebaseCorePlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTFirebaseCorePlugin"]];
|
||||||
|
[FLTImagePickerPlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTImagePickerPlugin"]];
|
||||||
[SharedPreferencesPlugin registerWithRegistrar:[registry registrarForPlugin:@"SharedPreferencesPlugin"]];
|
[SharedPreferencesPlugin registerWithRegistrar:[registry registrarForPlugin:@"SharedPreferencesPlugin"]];
|
||||||
[URLLauncherPlugin registerWithRegistrar:[registry registrarForPlugin:@"URLLauncherPlugin"]];
|
[URLLauncherPlugin registerWithRegistrar:[registry registrarForPlugin:@"URLLauncherPlugin"]];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,13 @@
|
|||||||
|
|
||||||
#include "generated_plugin_registrant.h"
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
|
#include <file_selector_linux/file_selector_plugin.h>
|
||||||
#include <url_launcher_linux/url_launcher_plugin.h>
|
#include <url_launcher_linux/url_launcher_plugin.h>
|
||||||
|
|
||||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||||
|
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
|
||||||
|
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
|
||||||
|
file_selector_plugin_register_with_registrar(file_selector_linux_registrar);
|
||||||
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
|
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
|
||||||
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
|
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
|
file_selector_linux
|
||||||
url_launcher_linux
|
url_launcher_linux
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
import FlutterMacOS
|
import FlutterMacOS
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
import file_picker
|
||||||
|
import file_selector_macos
|
||||||
import firebase_app_check
|
import firebase_app_check
|
||||||
import firebase_auth
|
import firebase_auth
|
||||||
import firebase_core
|
import firebase_core
|
||||||
@@ -12,6 +14,8 @@ import shared_preferences_foundation
|
|||||||
import url_launcher_macos
|
import url_launcher_macos
|
||||||
|
|
||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
|
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
|
||||||
|
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
||||||
FLTFirebaseAppCheckPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAppCheckPlugin"))
|
FLTFirebaseAppCheckPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAppCheckPlugin"))
|
||||||
FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin"))
|
FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin"))
|
||||||
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
|
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
|
||||||
|
|||||||
33
apps/mobile/apps/client/maestro/README.md
Normal file
33
apps/mobile/apps/client/maestro/README.md
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# Maestro Integration Tests — Client App
|
||||||
|
|
||||||
|
Auth flows for the KROW Client app.
|
||||||
|
See [docs/research/flutter-testing-tools.md](/docs/research/flutter-testing-tools.md) and [maestro-test-run-instructions.md](/docs/research/maestro-test-run-instructions.md).
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
maestro/
|
||||||
|
auth/
|
||||||
|
sign_in.yaml
|
||||||
|
sign_up.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
## Credentials (env, never hardcoded)
|
||||||
|
|
||||||
|
| Flow | Env variables |
|
||||||
|
|------|---------------|
|
||||||
|
| sign_in | `TEST_CLIENT_EMAIL`, `TEST_CLIENT_PASSWORD` |
|
||||||
|
| sign_up | `TEST_CLIENT_EMAIL`, `TEST_CLIENT_PASSWORD`, `TEST_CLIENT_COMPANY` |
|
||||||
|
|
||||||
|
## Run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Via Makefile (export vars first)
|
||||||
|
make test-e2e-client
|
||||||
|
|
||||||
|
# Direct
|
||||||
|
maestro test apps/mobile/apps/client/maestro/auth/sign_in.yaml \
|
||||||
|
-e TEST_CLIENT_EMAIL=... -e TEST_CLIENT_PASSWORD=...
|
||||||
|
maestro test apps/mobile/apps/client/maestro/auth/sign_up.yaml \
|
||||||
|
-e TEST_CLIENT_EMAIL=... -e TEST_CLIENT_PASSWORD=... -e TEST_CLIENT_COMPANY=...
|
||||||
|
```
|
||||||
22
apps/mobile/apps/client/maestro/auth/sign_in.yaml
Normal file
22
apps/mobile/apps/client/maestro/auth/sign_in.yaml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# Client App — Sign In flow
|
||||||
|
# Credentials via env: TEST_CLIENT_EMAIL, TEST_CLIENT_PASSWORD
|
||||||
|
# Run: maestro test apps/mobile/apps/client/maestro/auth/sign_in.yaml -e TEST_CLIENT_EMAIL=... -e TEST_CLIENT_PASSWORD=...
|
||||||
|
# Or: export MAESTRO_TEST_CLIENT_EMAIL / MAESTRO_TEST_CLIENT_PASSWORD (Maestro auto-reads MAESTRO_*)
|
||||||
|
|
||||||
|
appId: com.krowwithus.client
|
||||||
|
env:
|
||||||
|
EMAIL: ${TEST_CLIENT_EMAIL}
|
||||||
|
PASSWORD: ${TEST_CLIENT_PASSWORD}
|
||||||
|
---
|
||||||
|
- launchApp
|
||||||
|
- assertVisible: "Sign In"
|
||||||
|
- tapOn: "Sign In"
|
||||||
|
- assertVisible: "Email"
|
||||||
|
- tapOn:
|
||||||
|
id: sign_in_email
|
||||||
|
- inputText: ${EMAIL}
|
||||||
|
- tapOn:
|
||||||
|
id: sign_in_password
|
||||||
|
- inputText: ${PASSWORD}
|
||||||
|
- tapOn: "Sign In"
|
||||||
|
- assertVisible: "Home"
|
||||||
28
apps/mobile/apps/client/maestro/auth/sign_up.yaml
Normal file
28
apps/mobile/apps/client/maestro/auth/sign_up.yaml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# Client App — Sign Up flow
|
||||||
|
# Credentials via env: TEST_CLIENT_EMAIL, TEST_CLIENT_PASSWORD, TEST_CLIENT_COMPANY
|
||||||
|
# Run: maestro test apps/mobile/apps/client/maestro/auth/sign_up.yaml -e TEST_CLIENT_EMAIL=... -e TEST_CLIENT_PASSWORD=... -e TEST_CLIENT_COMPANY=...
|
||||||
|
|
||||||
|
appId: com.krowwithus.client
|
||||||
|
env:
|
||||||
|
EMAIL: ${TEST_CLIENT_EMAIL}
|
||||||
|
PASSWORD: ${TEST_CLIENT_PASSWORD}
|
||||||
|
COMPANY: ${TEST_CLIENT_COMPANY}
|
||||||
|
---
|
||||||
|
- launchApp
|
||||||
|
- assertVisible: "Create Account"
|
||||||
|
- tapOn: "Create Account"
|
||||||
|
- assertVisible: "Company"
|
||||||
|
- tapOn:
|
||||||
|
id: sign_up_company
|
||||||
|
- inputText: ${COMPANY}
|
||||||
|
- tapOn:
|
||||||
|
id: sign_up_email
|
||||||
|
- inputText: ${EMAIL}
|
||||||
|
- tapOn:
|
||||||
|
id: sign_up_password
|
||||||
|
- inputText: ${PASSWORD}
|
||||||
|
- tapOn:
|
||||||
|
id: sign_up_confirm_password
|
||||||
|
- inputText: ${PASSWORD}
|
||||||
|
- tapOn: "Create Account"
|
||||||
|
- assertVisible: "Home"
|
||||||
@@ -6,11 +6,14 @@
|
|||||||
|
|
||||||
#include "generated_plugin_registrant.h"
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
|
#include <file_selector_windows/file_selector_windows.h>
|
||||||
#include <firebase_auth/firebase_auth_plugin_c_api.h>
|
#include <firebase_auth/firebase_auth_plugin_c_api.h>
|
||||||
#include <firebase_core/firebase_core_plugin_c_api.h>
|
#include <firebase_core/firebase_core_plugin_c_api.h>
|
||||||
#include <url_launcher_windows/url_launcher_windows.h>
|
#include <url_launcher_windows/url_launcher_windows.h>
|
||||||
|
|
||||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||||
|
FileSelectorWindowsRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("FileSelectorWindows"));
|
||||||
FirebaseAuthPluginCApiRegisterWithRegistrar(
|
FirebaseAuthPluginCApiRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("FirebaseAuthPluginCApi"));
|
registry->GetRegistrarForPlugin("FirebaseAuthPluginCApi"));
|
||||||
FirebaseCorePluginCApiRegisterWithRegistrar(
|
FirebaseCorePluginCApiRegisterWithRegistrar(
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
|
file_selector_windows
|
||||||
firebase_auth
|
firebase_auth
|
||||||
firebase_core
|
firebase_core
|
||||||
url_launcher_windows
|
url_launcher_windows
|
||||||
|
|||||||
@@ -15,6 +15,11 @@ import io.flutter.embedding.engine.FlutterEngine;
|
|||||||
public final class GeneratedPluginRegistrant {
|
public final class GeneratedPluginRegistrant {
|
||||||
private static final String TAG = "GeneratedPluginRegistrant";
|
private static final String TAG = "GeneratedPluginRegistrant";
|
||||||
public static void registerWith(@NonNull FlutterEngine flutterEngine) {
|
public static void registerWith(@NonNull FlutterEngine flutterEngine) {
|
||||||
|
try {
|
||||||
|
flutterEngine.getPlugins().add(new com.mr.flutter.plugin.filepicker.FilePickerPlugin());
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error registering plugin file_picker, com.mr.flutter.plugin.filepicker.FilePickerPlugin", e);
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
flutterEngine.getPlugins().add(new io.flutter.plugins.firebase.appcheck.FlutterFirebaseAppCheckPlugin());
|
flutterEngine.getPlugins().add(new io.flutter.plugins.firebase.appcheck.FlutterFirebaseAppCheckPlugin());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@@ -45,6 +50,11 @@ public final class GeneratedPluginRegistrant {
|
|||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Log.e(TAG, "Error registering plugin google_maps_flutter_android, io.flutter.plugins.googlemaps.GoogleMapsPlugin", e);
|
Log.e(TAG, "Error registering plugin google_maps_flutter_android, io.flutter.plugins.googlemaps.GoogleMapsPlugin", e);
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
flutterEngine.getPlugins().add(new io.flutter.plugins.imagepicker.ImagePickerPlugin());
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error registering plugin image_picker_android, io.flutter.plugins.imagepicker.ImagePickerPlugin", e);
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
flutterEngine.getPlugins().add(new io.flutter.plugins.pathprovider.PathProviderPlugin());
|
flutterEngine.getPlugins().add(new io.flutter.plugins.pathprovider.PathProviderPlugin());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
|||||||
@@ -6,6 +6,12 @@
|
|||||||
|
|
||||||
#import "GeneratedPluginRegistrant.h"
|
#import "GeneratedPluginRegistrant.h"
|
||||||
|
|
||||||
|
#if __has_include(<file_picker/FilePickerPlugin.h>)
|
||||||
|
#import <file_picker/FilePickerPlugin.h>
|
||||||
|
#else
|
||||||
|
@import file_picker;
|
||||||
|
#endif
|
||||||
|
|
||||||
#if __has_include(<firebase_app_check/FLTFirebaseAppCheckPlugin.h>)
|
#if __has_include(<firebase_app_check/FLTFirebaseAppCheckPlugin.h>)
|
||||||
#import <firebase_app_check/FLTFirebaseAppCheckPlugin.h>
|
#import <firebase_app_check/FLTFirebaseAppCheckPlugin.h>
|
||||||
#else
|
#else
|
||||||
@@ -36,6 +42,12 @@
|
|||||||
@import google_maps_flutter_ios;
|
@import google_maps_flutter_ios;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#if __has_include(<image_picker_ios/FLTImagePickerPlugin.h>)
|
||||||
|
#import <image_picker_ios/FLTImagePickerPlugin.h>
|
||||||
|
#else
|
||||||
|
@import image_picker_ios;
|
||||||
|
#endif
|
||||||
|
|
||||||
#if __has_include(<permission_handler_apple/PermissionHandlerPlugin.h>)
|
#if __has_include(<permission_handler_apple/PermissionHandlerPlugin.h>)
|
||||||
#import <permission_handler_apple/PermissionHandlerPlugin.h>
|
#import <permission_handler_apple/PermissionHandlerPlugin.h>
|
||||||
#else
|
#else
|
||||||
@@ -57,11 +69,13 @@
|
|||||||
@implementation GeneratedPluginRegistrant
|
@implementation GeneratedPluginRegistrant
|
||||||
|
|
||||||
+ (void)registerWithRegistry:(NSObject<FlutterPluginRegistry>*)registry {
|
+ (void)registerWithRegistry:(NSObject<FlutterPluginRegistry>*)registry {
|
||||||
|
[FilePickerPlugin registerWithRegistrar:[registry registrarForPlugin:@"FilePickerPlugin"]];
|
||||||
[FLTFirebaseAppCheckPlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTFirebaseAppCheckPlugin"]];
|
[FLTFirebaseAppCheckPlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTFirebaseAppCheckPlugin"]];
|
||||||
[FLTFirebaseAuthPlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTFirebaseAuthPlugin"]];
|
[FLTFirebaseAuthPlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTFirebaseAuthPlugin"]];
|
||||||
[FLTFirebaseCorePlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTFirebaseCorePlugin"]];
|
[FLTFirebaseCorePlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTFirebaseCorePlugin"]];
|
||||||
[GeolocatorPlugin registerWithRegistrar:[registry registrarForPlugin:@"GeolocatorPlugin"]];
|
[GeolocatorPlugin registerWithRegistrar:[registry registrarForPlugin:@"GeolocatorPlugin"]];
|
||||||
[FLTGoogleMapsPlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTGoogleMapsPlugin"]];
|
[FLTGoogleMapsPlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTGoogleMapsPlugin"]];
|
||||||
|
[FLTImagePickerPlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTImagePickerPlugin"]];
|
||||||
[PermissionHandlerPlugin registerWithRegistrar:[registry registrarForPlugin:@"PermissionHandlerPlugin"]];
|
[PermissionHandlerPlugin registerWithRegistrar:[registry registrarForPlugin:@"PermissionHandlerPlugin"]];
|
||||||
[SharedPreferencesPlugin registerWithRegistrar:[registry registrarForPlugin:@"SharedPreferencesPlugin"]];
|
[SharedPreferencesPlugin registerWithRegistrar:[registry registrarForPlugin:@"SharedPreferencesPlugin"]];
|
||||||
[URLLauncherPlugin registerWithRegistrar:[registry registrarForPlugin:@"URLLauncherPlugin"]];
|
[URLLauncherPlugin registerWithRegistrar:[registry registrarForPlugin:@"URLLauncherPlugin"]];
|
||||||
|
|||||||
@@ -5,12 +5,12 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
import 'package:flutter_modular/flutter_modular.dart';
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
|
import 'package:krow_core/core.dart';
|
||||||
import 'package:krow_data_connect/krow_data_connect.dart';
|
import 'package:krow_data_connect/krow_data_connect.dart';
|
||||||
import 'package:krowwithus_staff/firebase_options.dart';
|
import 'package:krowwithus_staff/firebase_options.dart';
|
||||||
import 'package:staff_authentication/staff_authentication.dart'
|
import 'package:staff_authentication/staff_authentication.dart'
|
||||||
as staff_authentication;
|
as staff_authentication;
|
||||||
import 'package:staff_main/staff_main.dart' as staff_main;
|
import 'package:staff_main/staff_main.dart' as staff_main;
|
||||||
import 'package:krow_core/core.dart';
|
|
||||||
|
|
||||||
import 'src/widgets/session_listener.dart';
|
import 'src/widgets/session_listener.dart';
|
||||||
|
|
||||||
@@ -26,7 +26,10 @@ void main() async {
|
|||||||
|
|
||||||
// Initialize session listener for Firebase Auth state changes
|
// Initialize session listener for Firebase Auth state changes
|
||||||
DataConnectService.instance.initializeAuthListener(
|
DataConnectService.instance.initializeAuthListener(
|
||||||
allowedRoles: <String>['STAFF', 'BOTH'], // Only allow users with STAFF or BOTH roles
|
allowedRoles: <String>[
|
||||||
|
'STAFF',
|
||||||
|
'BOTH',
|
||||||
|
], // Only allow users with STAFF or BOTH roles
|
||||||
);
|
);
|
||||||
|
|
||||||
runApp(
|
runApp(
|
||||||
@@ -40,11 +43,11 @@ void main() async {
|
|||||||
/// The main application module.
|
/// The main application module.
|
||||||
class AppModule extends Module {
|
class AppModule extends Module {
|
||||||
@override
|
@override
|
||||||
List<Module> get imports =>
|
List<Module> get imports => <Module>[
|
||||||
<Module>[
|
CoreModule(),
|
||||||
core_localization.LocalizationModule(),
|
core_localization.LocalizationModule(),
|
||||||
staff_authentication.StaffAuthenticationModule(),
|
staff_authentication.StaffAuthenticationModule(),
|
||||||
];
|
];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void routes(RouteManager r) {
|
void routes(RouteManager r) {
|
||||||
|
|||||||
@@ -6,9 +6,13 @@
|
|||||||
|
|
||||||
#include "generated_plugin_registrant.h"
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
|
#include <file_selector_linux/file_selector_plugin.h>
|
||||||
#include <url_launcher_linux/url_launcher_plugin.h>
|
#include <url_launcher_linux/url_launcher_plugin.h>
|
||||||
|
|
||||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||||
|
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
|
||||||
|
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
|
||||||
|
file_selector_plugin_register_with_registrar(file_selector_linux_registrar);
|
||||||
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
|
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
|
||||||
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
|
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
|
file_selector_linux
|
||||||
url_launcher_linux
|
url_launcher_linux
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
import FlutterMacOS
|
import FlutterMacOS
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
import file_picker
|
||||||
|
import file_selector_macos
|
||||||
import firebase_app_check
|
import firebase_app_check
|
||||||
import firebase_auth
|
import firebase_auth
|
||||||
import firebase_core
|
import firebase_core
|
||||||
@@ -13,6 +15,8 @@ import shared_preferences_foundation
|
|||||||
import url_launcher_macos
|
import url_launcher_macos
|
||||||
|
|
||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
|
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
|
||||||
|
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
||||||
FLTFirebaseAppCheckPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAppCheckPlugin"))
|
FLTFirebaseAppCheckPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAppCheckPlugin"))
|
||||||
FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin"))
|
FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin"))
|
||||||
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
|
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
|
||||||
|
|||||||
38
apps/mobile/apps/staff/maestro/README.md
Normal file
38
apps/mobile/apps/staff/maestro/README.md
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# Maestro Integration Tests — Staff App
|
||||||
|
|
||||||
|
Auth flows for the KROW Staff app.
|
||||||
|
See [docs/research/flutter-testing-tools.md](/docs/research/flutter-testing-tools.md) and [maestro-test-run-instructions.md](/docs/research/maestro-test-run-instructions.md).
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
maestro/
|
||||||
|
auth/
|
||||||
|
sign_in.yaml
|
||||||
|
sign_up.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Firebase test phone in Auth > Phone (e.g. +1 555-765-4321 / OTP 123456)
|
||||||
|
- For sign_up: use a different test number (not yet registered)
|
||||||
|
|
||||||
|
## Credentials (env, never hardcoded)
|
||||||
|
|
||||||
|
| Flow | Env variables |
|
||||||
|
|------|---------------|
|
||||||
|
| sign_in | `TEST_STAFF_PHONE`, `TEST_STAFF_OTP` |
|
||||||
|
| sign_up | `TEST_STAFF_SIGNUP_PHONE`, `TEST_STAFF_OTP` |
|
||||||
|
|
||||||
|
## Run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Via Makefile (export vars first)
|
||||||
|
make test-e2e-staff
|
||||||
|
|
||||||
|
# Direct
|
||||||
|
maestro test apps/mobile/apps/staff/maestro/auth/sign_in.yaml \
|
||||||
|
-e TEST_STAFF_PHONE=5557654321 -e TEST_STAFF_OTP=123456
|
||||||
|
maestro test apps/mobile/apps/staff/maestro/auth/sign_up.yaml \
|
||||||
|
-e TEST_STAFF_SIGNUP_PHONE=... -e TEST_STAFF_OTP=123456
|
||||||
|
```
|
||||||
24
apps/mobile/apps/staff/maestro/auth/sign_in.yaml
Normal file
24
apps/mobile/apps/staff/maestro/auth/sign_in.yaml
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Staff App — Sign In flow (Phone + OTP)
|
||||||
|
# Credentials via env: TEST_STAFF_PHONE, TEST_STAFF_OTP
|
||||||
|
# Firebase: add test phone in Auth > Phone (e.g. +1 555-765-4321 / OTP 123456)
|
||||||
|
# Run: maestro test apps/mobile/apps/staff/maestro/auth/sign_in.yaml -e TEST_STAFF_PHONE=5557654321 -e TEST_STAFF_OTP=123456
|
||||||
|
|
||||||
|
appId: com.krowwithus.staff
|
||||||
|
env:
|
||||||
|
PHONE: ${TEST_STAFF_PHONE}
|
||||||
|
OTP: ${TEST_STAFF_OTP}
|
||||||
|
---
|
||||||
|
- launchApp
|
||||||
|
- assertVisible: "Log In"
|
||||||
|
- tapOn: "Log In"
|
||||||
|
- assertVisible: "Send Code"
|
||||||
|
- tapOn:
|
||||||
|
id: staff_phone_input
|
||||||
|
- inputText: ${PHONE}
|
||||||
|
- tapOn: "Send Code"
|
||||||
|
# OTP screen: Continue button visible until we finish typing
|
||||||
|
- assertVisible: "Continue"
|
||||||
|
- tapOn:
|
||||||
|
id: staff_otp_input
|
||||||
|
- inputText: ${OTP}
|
||||||
|
# OTP auto-submits when 6th digit is entered; app navigates to staff main
|
||||||
23
apps/mobile/apps/staff/maestro/auth/sign_up.yaml
Normal file
23
apps/mobile/apps/staff/maestro/auth/sign_up.yaml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# Staff App — Sign Up flow (Phone + OTP)
|
||||||
|
# Credentials via env: TEST_STAFF_SIGNUP_PHONE, TEST_STAFF_OTP
|
||||||
|
# Use a NEW Firebase test phone (not yet registered)
|
||||||
|
# Run: maestro test apps/mobile/apps/staff/maestro/auth/sign_up.yaml -e TEST_STAFF_SIGNUP_PHONE=... -e TEST_STAFF_OTP=123456
|
||||||
|
|
||||||
|
appId: com.krowwithus.staff
|
||||||
|
env:
|
||||||
|
PHONE: ${TEST_STAFF_SIGNUP_PHONE}
|
||||||
|
OTP: ${TEST_STAFF_OTP}
|
||||||
|
---
|
||||||
|
- launchApp
|
||||||
|
- assertVisible: "Sign Up"
|
||||||
|
- tapOn: "Sign Up"
|
||||||
|
- assertVisible: "Send Code"
|
||||||
|
- tapOn:
|
||||||
|
id: staff_phone_input
|
||||||
|
- inputText: ${PHONE}
|
||||||
|
- tapOn: "Send Code"
|
||||||
|
# OTP auto-submits when 6th digit entered
|
||||||
|
- assertVisible: "Continue"
|
||||||
|
- tapOn:
|
||||||
|
id: staff_otp_input
|
||||||
|
- inputText: ${OTP}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
#include "generated_plugin_registrant.h"
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
|
#include <file_selector_windows/file_selector_windows.h>
|
||||||
#include <firebase_auth/firebase_auth_plugin_c_api.h>
|
#include <firebase_auth/firebase_auth_plugin_c_api.h>
|
||||||
#include <firebase_core/firebase_core_plugin_c_api.h>
|
#include <firebase_core/firebase_core_plugin_c_api.h>
|
||||||
#include <geolocator_windows/geolocator_windows.h>
|
#include <geolocator_windows/geolocator_windows.h>
|
||||||
@@ -13,6 +14,8 @@
|
|||||||
#include <url_launcher_windows/url_launcher_windows.h>
|
#include <url_launcher_windows/url_launcher_windows.h>
|
||||||
|
|
||||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||||
|
FileSelectorWindowsRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("FileSelectorWindows"));
|
||||||
FirebaseAuthPluginCApiRegisterWithRegistrar(
|
FirebaseAuthPluginCApiRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("FirebaseAuthPluginCApi"));
|
registry->GetRegistrarForPlugin("FirebaseAuthPluginCApi"));
|
||||||
FirebaseCorePluginCApiRegisterWithRegistrar(
|
FirebaseCorePluginCApiRegisterWithRegistrar(
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
|
file_selector_windows
|
||||||
firebase_auth
|
firebase_auth
|
||||||
firebase_core
|
firebase_core
|
||||||
geolocator_windows
|
geolocator_windows
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
{
|
{
|
||||||
"GOOGLE_MAPS_API_KEY": "AIzaSyAyRS9I4xxoVPAX91RJvWJHszB3ZY3-IC0"
|
"GOOGLE_MAPS_API_KEY": "AIzaSyAyRS9I4xxoVPAX91RJvWJHszB3ZY3-IC0",
|
||||||
|
"CORE_API_BASE_URL": "https://krow-core-api-e3g6witsvq-uc.a.run.app"
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
library;
|
library;
|
||||||
|
|
||||||
|
export 'src/core_module.dart';
|
||||||
|
|
||||||
export 'src/domain/arguments/usecase_argument.dart';
|
export 'src/domain/arguments/usecase_argument.dart';
|
||||||
export 'src/domain/usecases/usecase.dart';
|
export 'src/domain/usecases/usecase.dart';
|
||||||
export 'src/utils/date_time_utils.dart';
|
export 'src/utils/date_time_utils.dart';
|
||||||
@@ -8,3 +10,22 @@ export 'src/presentation/mixins/bloc_error_handler.dart';
|
|||||||
export 'src/presentation/observers/core_bloc_observer.dart';
|
export 'src/presentation/observers/core_bloc_observer.dart';
|
||||||
export 'src/config/app_config.dart';
|
export 'src/config/app_config.dart';
|
||||||
export 'src/routing/routing.dart';
|
export 'src/routing/routing.dart';
|
||||||
|
export 'src/services/api_service/api_service.dart';
|
||||||
|
export 'src/services/api_service/dio_client.dart';
|
||||||
|
|
||||||
|
// Core API Services
|
||||||
|
export 'src/services/api_service/core_api_services/core_api_endpoints.dart';
|
||||||
|
export 'src/services/api_service/core_api_services/file_upload/file_upload_service.dart';
|
||||||
|
export 'src/services/api_service/core_api_services/file_upload/file_upload_response.dart';
|
||||||
|
export 'src/services/api_service/core_api_services/signed_url/signed_url_service.dart';
|
||||||
|
export 'src/services/api_service/core_api_services/signed_url/signed_url_response.dart';
|
||||||
|
export 'src/services/api_service/core_api_services/llm/llm_service.dart';
|
||||||
|
export 'src/services/api_service/core_api_services/llm/llm_response.dart';
|
||||||
|
export 'src/services/api_service/core_api_services/verification/verification_service.dart';
|
||||||
|
export 'src/services/api_service/core_api_services/verification/verification_response.dart';
|
||||||
|
|
||||||
|
// Device Services
|
||||||
|
export 'src/services/device/camera/camera_service.dart';
|
||||||
|
export 'src/services/device/gallery/gallery_service.dart';
|
||||||
|
export 'src/services/device/file/file_picker_service.dart';
|
||||||
|
export 'src/services/device/file_upload/device_file_upload_service.dart';
|
||||||
|
|||||||
@@ -5,5 +5,12 @@ class AppConfig {
|
|||||||
AppConfig._();
|
AppConfig._();
|
||||||
|
|
||||||
/// The Google Maps API key.
|
/// The Google Maps API key.
|
||||||
static const String googleMapsApiKey = String.fromEnvironment('GOOGLE_MAPS_API_KEY');
|
static const String googleMapsApiKey = String.fromEnvironment(
|
||||||
|
'GOOGLE_MAPS_API_KEY',
|
||||||
|
);
|
||||||
|
|
||||||
|
/// The base URL for the Core API.
|
||||||
|
static const String coreApiBaseUrl = String.fromEnvironment(
|
||||||
|
'CORE_API_BASE_URL',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
48
apps/mobile/packages/core/lib/src/core_module.dart
Normal file
48
apps/mobile/packages/core/lib/src/core_module.dart
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
|
import 'package:image_picker/image_picker.dart';
|
||||||
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
|
||||||
|
import '../core.dart';
|
||||||
|
|
||||||
|
/// A module that provides core services and shared dependencies.
|
||||||
|
///
|
||||||
|
/// This module should be imported by the root [AppModule] to make
|
||||||
|
/// core services available globally as singletons.
|
||||||
|
class CoreModule extends Module {
|
||||||
|
@override
|
||||||
|
void exportedBinds(Injector i) {
|
||||||
|
// 1. Register the base HTTP client
|
||||||
|
i.addSingleton<Dio>(() => DioClient());
|
||||||
|
|
||||||
|
// 2. Register the base API service
|
||||||
|
i.addSingleton<BaseApiService>(() => ApiService(i.get<Dio>()));
|
||||||
|
|
||||||
|
// 3. Register Core API Services (Orchestrators)
|
||||||
|
i.addSingleton<FileUploadService>(
|
||||||
|
() => FileUploadService(i.get<BaseApiService>()),
|
||||||
|
);
|
||||||
|
i.addSingleton<SignedUrlService>(
|
||||||
|
() => SignedUrlService(i.get<BaseApiService>()),
|
||||||
|
);
|
||||||
|
i.addSingleton<VerificationService>(
|
||||||
|
() => VerificationService(i.get<BaseApiService>()),
|
||||||
|
);
|
||||||
|
i.addSingleton<LlmService>(() => LlmService(i.get<BaseApiService>()));
|
||||||
|
|
||||||
|
// 4. Register Device dependency
|
||||||
|
i.addSingleton<ImagePicker>(() => ImagePicker());
|
||||||
|
|
||||||
|
// 5. Register Device Services
|
||||||
|
i.addSingleton<CameraService>(() => CameraService(i.get<ImagePicker>()));
|
||||||
|
i.addSingleton<GalleryService>(() => GalleryService(i.get<ImagePicker>()));
|
||||||
|
i.addSingleton<FilePickerService>(FilePickerService.new);
|
||||||
|
i.addSingleton<DeviceFileUploadService>(
|
||||||
|
() => DeviceFileUploadService(
|
||||||
|
cameraService: i.get<CameraService>(),
|
||||||
|
galleryService: i.get<GalleryService>(),
|
||||||
|
apiUploadService: i.get<FileUploadService>(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:flutter_modular/flutter_modular.dart';
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
|
||||||
import 'route_paths.dart';
|
import 'route_paths.dart';
|
||||||
|
|
||||||
@@ -134,6 +135,11 @@ extension ClientNavigator on IModularNavigator {
|
|||||||
pushNamed(ClientPaths.settings);
|
pushNamed(ClientPaths.settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Pushes the edit profile page.
|
||||||
|
void toClientEditProfile() {
|
||||||
|
pushNamed('${ClientPaths.settings}/edit-profile');
|
||||||
|
}
|
||||||
|
|
||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
// HUBS MANAGEMENT
|
// HUBS MANAGEMENT
|
||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
@@ -145,6 +151,25 @@ extension ClientNavigator on IModularNavigator {
|
|||||||
await pushNamed(ClientPaths.hubs);
|
await pushNamed(ClientPaths.hubs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Navigates to the details of a specific hub.
|
||||||
|
Future<bool?> toHubDetails(Hub hub) {
|
||||||
|
return pushNamed<bool?>(
|
||||||
|
ClientPaths.hubDetails,
|
||||||
|
arguments: <String, dynamic>{'hub': hub},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Navigates to the page to add a new hub or edit an existing one.
|
||||||
|
Future<bool?> toEditHub({Hub? hub}) async {
|
||||||
|
return pushNamed<bool?>(
|
||||||
|
ClientPaths.editHub,
|
||||||
|
arguments: <String, dynamic>{'hub': hub},
|
||||||
|
// Some versions of Modular allow passing opaque here, but if not
|
||||||
|
// we'll handle transparency in the page itself which we already do.
|
||||||
|
// To ensure it's not opaque, we'll use push with a PageRouteBuilder if needed.
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
// ORDER CREATION
|
// ORDER CREATION
|
||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
|
|||||||
@@ -16,14 +16,14 @@ class ClientPaths {
|
|||||||
/// Generate child route based on the given route and parent route
|
/// Generate child route based on the given route and parent route
|
||||||
///
|
///
|
||||||
/// This is useful for creating nested routes within modules.
|
/// This is useful for creating nested routes within modules.
|
||||||
static String childRoute(String parent, String child) {
|
static String childRoute(String parent, String child) {
|
||||||
final String childPath = child.replaceFirst(parent, '');
|
final String childPath = child.replaceFirst(parent, '');
|
||||||
|
|
||||||
// check if the child path is empty
|
// check if the child path is empty
|
||||||
if (childPath.isEmpty) {
|
if (childPath.isEmpty) {
|
||||||
return '/';
|
return '/';
|
||||||
}
|
}
|
||||||
|
|
||||||
// ensure the child path starts with a '/'
|
// ensure the child path starts with a '/'
|
||||||
if (!childPath.startsWith('/')) {
|
if (!childPath.startsWith('/')) {
|
||||||
return '/$childPath';
|
return '/$childPath';
|
||||||
@@ -82,10 +82,12 @@ class ClientPaths {
|
|||||||
static const String billing = '/client-main/billing';
|
static const String billing = '/client-main/billing';
|
||||||
|
|
||||||
/// Completion review page - review shift completion records.
|
/// Completion review page - review shift completion records.
|
||||||
static const String completionReview = '/client-main/billing/completion-review';
|
static const String completionReview =
|
||||||
|
'/client-main/billing/completion-review';
|
||||||
|
|
||||||
/// Full list of invoices awaiting approval.
|
/// Full list of invoices awaiting approval.
|
||||||
static const String awaitingApproval = '/client-main/billing/awaiting-approval';
|
static const String awaitingApproval =
|
||||||
|
'/client-main/billing/awaiting-approval';
|
||||||
|
|
||||||
/// Invoice ready page - view status of approved invoices.
|
/// Invoice ready page - view status of approved invoices.
|
||||||
static const String invoiceReady = '/client-main/billing/invoice-ready';
|
static const String invoiceReady = '/client-main/billing/invoice-ready';
|
||||||
@@ -118,6 +120,12 @@ class ClientPaths {
|
|||||||
/// View and manage physical locations/hubs where staff are deployed.
|
/// View and manage physical locations/hubs where staff are deployed.
|
||||||
static const String hubs = '/client-hubs';
|
static const String hubs = '/client-hubs';
|
||||||
|
|
||||||
|
/// Specific hub details.
|
||||||
|
static const String hubDetails = '/client-hubs/details';
|
||||||
|
|
||||||
|
/// Page for adding or editing a hub.
|
||||||
|
static const String editHub = '/client-hubs/edit';
|
||||||
|
|
||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
// ORDER CREATION & MANAGEMENT
|
// ORDER CREATION & MANAGEMENT
|
||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
|
|||||||
@@ -196,7 +196,22 @@ extension StaffNavigator on IModularNavigator {
|
|||||||
///
|
///
|
||||||
/// Record sizing and appearance information for uniform allocation.
|
/// Record sizing and appearance information for uniform allocation.
|
||||||
void toAttire() {
|
void toAttire() {
|
||||||
pushNamed(StaffPaths.attire);
|
navigate(StaffPaths.attire);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pushes the attire capture page.
|
||||||
|
///
|
||||||
|
/// Parameters:
|
||||||
|
/// * [item] - The attire item to capture
|
||||||
|
/// * [initialPhotoUrl] - Optional initial photo URL
|
||||||
|
void toAttireCapture({required AttireItem item, String? initialPhotoUrl}) {
|
||||||
|
navigate(
|
||||||
|
StaffPaths.attireCapture,
|
||||||
|
arguments: <String, dynamic>{
|
||||||
|
'item': item,
|
||||||
|
'initialPhotoUrl': initialPhotoUrl,
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
|
|||||||
@@ -152,6 +152,9 @@ class StaffPaths {
|
|||||||
/// Record sizing and appearance information for uniform allocation.
|
/// Record sizing and appearance information for uniform allocation.
|
||||||
static const String attire = '/worker-main/attire/';
|
static const String attire = '/worker-main/attire/';
|
||||||
|
|
||||||
|
/// Attire capture page.
|
||||||
|
static const String attireCapture = '/worker-main/attire/capture/';
|
||||||
|
|
||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
// COMPLIANCE & DOCUMENTS
|
// COMPLIANCE & DOCUMENTS
|
||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
|
|||||||
@@ -0,0 +1,127 @@
|
|||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
|
||||||
|
/// A service that handles HTTP communication using the [Dio] client.
|
||||||
|
///
|
||||||
|
/// This class provides a wrapper around [Dio]'s methods to handle
|
||||||
|
/// response parsing and error handling in a consistent way.
|
||||||
|
class ApiService implements BaseApiService {
|
||||||
|
/// Creates an [ApiService] with the given [Dio] instance.
|
||||||
|
ApiService(this._dio);
|
||||||
|
|
||||||
|
/// The underlying [Dio] client used for network requests.
|
||||||
|
final Dio _dio;
|
||||||
|
|
||||||
|
/// Performs a GET request to the specified [endpoint].
|
||||||
|
@override
|
||||||
|
Future<ApiResponse> get(
|
||||||
|
String endpoint, {
|
||||||
|
Map<String, dynamic>? params,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
final Response<dynamic> response = await _dio.get<dynamic>(
|
||||||
|
endpoint,
|
||||||
|
queryParameters: params,
|
||||||
|
);
|
||||||
|
return _handleResponse(response);
|
||||||
|
} on DioException catch (e) {
|
||||||
|
return _handleError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Performs a POST request to the specified [endpoint].
|
||||||
|
@override
|
||||||
|
Future<ApiResponse> post(
|
||||||
|
String endpoint, {
|
||||||
|
dynamic data,
|
||||||
|
Map<String, dynamic>? params,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
final Response<dynamic> response = await _dio.post<dynamic>(
|
||||||
|
endpoint,
|
||||||
|
data: data,
|
||||||
|
queryParameters: params,
|
||||||
|
);
|
||||||
|
return _handleResponse(response);
|
||||||
|
} on DioException catch (e) {
|
||||||
|
return _handleError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Performs a PUT request to the specified [endpoint].
|
||||||
|
@override
|
||||||
|
Future<ApiResponse> put(
|
||||||
|
String endpoint, {
|
||||||
|
dynamic data,
|
||||||
|
Map<String, dynamic>? params,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
final Response<dynamic> response = await _dio.put<dynamic>(
|
||||||
|
endpoint,
|
||||||
|
data: data,
|
||||||
|
queryParameters: params,
|
||||||
|
);
|
||||||
|
return _handleResponse(response);
|
||||||
|
} on DioException catch (e) {
|
||||||
|
return _handleError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Performs a PATCH request to the specified [endpoint].
|
||||||
|
@override
|
||||||
|
Future<ApiResponse> patch(
|
||||||
|
String endpoint, {
|
||||||
|
dynamic data,
|
||||||
|
Map<String, dynamic>? params,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
final Response<dynamic> response = await _dio.patch<dynamic>(
|
||||||
|
endpoint,
|
||||||
|
data: data,
|
||||||
|
queryParameters: params,
|
||||||
|
);
|
||||||
|
return _handleResponse(response);
|
||||||
|
} on DioException catch (e) {
|
||||||
|
return _handleError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extracts [ApiResponse] from a successful [Response].
|
||||||
|
ApiResponse _handleResponse(Response<dynamic> response) {
|
||||||
|
return ApiResponse(
|
||||||
|
code: response.statusCode?.toString() ?? '200',
|
||||||
|
message: response.data['message']?.toString() ?? 'Success',
|
||||||
|
data: response.data,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extracts [ApiResponse] from a [DioException].
|
||||||
|
ApiResponse _handleError(DioException e) {
|
||||||
|
if (e.response?.data is Map<String, dynamic>) {
|
||||||
|
final Map<String, dynamic> body =
|
||||||
|
e.response!.data as Map<String, dynamic>;
|
||||||
|
return ApiResponse(
|
||||||
|
code:
|
||||||
|
body['code']?.toString() ??
|
||||||
|
e.response?.statusCode?.toString() ??
|
||||||
|
'error',
|
||||||
|
message: body['message']?.toString() ?? e.message ?? 'Error occurred',
|
||||||
|
data: body['data'],
|
||||||
|
errors: _parseErrors(body['errors']),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return ApiResponse(
|
||||||
|
code: e.response?.statusCode?.toString() ?? 'error',
|
||||||
|
message: e.message ?? 'Unknown error',
|
||||||
|
errors: <String, dynamic>{'exception': e.type.toString()},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper to parse the errors map from various possible formats.
|
||||||
|
Map<String, dynamic> _parseErrors(dynamic errors) {
|
||||||
|
if (errors is Map) {
|
||||||
|
return Map<String, dynamic>.from(errors);
|
||||||
|
}
|
||||||
|
return const <String, dynamic>{};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import '../../../config/app_config.dart';
|
||||||
|
|
||||||
|
/// Constants for Core API endpoints.
|
||||||
|
class CoreApiEndpoints {
|
||||||
|
CoreApiEndpoints._();
|
||||||
|
|
||||||
|
/// The base URL for the Core API.
|
||||||
|
static const String baseUrl = AppConfig.coreApiBaseUrl;
|
||||||
|
|
||||||
|
/// Upload a file.
|
||||||
|
static const String uploadFile = '$baseUrl/core/upload-file';
|
||||||
|
|
||||||
|
/// Create a signed URL for a file.
|
||||||
|
static const String createSignedUrl = '$baseUrl/core/create-signed-url';
|
||||||
|
|
||||||
|
/// Invoke a Large Language Model.
|
||||||
|
static const String invokeLlm = '$baseUrl/core/invoke-llm';
|
||||||
|
|
||||||
|
/// Root for verification operations.
|
||||||
|
static const String verifications = '$baseUrl/core/verifications';
|
||||||
|
|
||||||
|
/// Get status of a verification job.
|
||||||
|
static String verificationStatus(String id) =>
|
||||||
|
'$baseUrl/core/verifications/$id';
|
||||||
|
|
||||||
|
/// Review a verification decision.
|
||||||
|
static String verificationReview(String id) =>
|
||||||
|
'$baseUrl/core/verifications/$id/review';
|
||||||
|
|
||||||
|
/// Retry a verification job.
|
||||||
|
static String verificationRetry(String id) =>
|
||||||
|
'$baseUrl/core/verifications/$id/retry';
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
/// Response model for file upload operation.
|
||||||
|
class FileUploadResponse {
|
||||||
|
/// Creates a [FileUploadResponse].
|
||||||
|
const FileUploadResponse({
|
||||||
|
required this.fileUri,
|
||||||
|
required this.contentType,
|
||||||
|
required this.size,
|
||||||
|
required this.bucket,
|
||||||
|
required this.path,
|
||||||
|
this.requestId,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Factory to create [FileUploadResponse] from JSON.
|
||||||
|
factory FileUploadResponse.fromJson(Map<String, dynamic> json) {
|
||||||
|
return FileUploadResponse(
|
||||||
|
fileUri: json['fileUri'] as String,
|
||||||
|
contentType: json['contentType'] as String,
|
||||||
|
size: json['size'] as int,
|
||||||
|
bucket: json['bucket'] as String,
|
||||||
|
path: json['path'] as String,
|
||||||
|
requestId: json['requestId'] as String?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The Cloud Storage URI of the uploaded file.
|
||||||
|
final String fileUri;
|
||||||
|
|
||||||
|
/// The MIME type of the file.
|
||||||
|
final String contentType;
|
||||||
|
|
||||||
|
/// The size of the file in bytes.
|
||||||
|
final int size;
|
||||||
|
|
||||||
|
/// The bucket where the file was uploaded.
|
||||||
|
final String bucket;
|
||||||
|
|
||||||
|
/// The path within the bucket.
|
||||||
|
final String path;
|
||||||
|
|
||||||
|
/// The unique request ID from the server.
|
||||||
|
final String? requestId;
|
||||||
|
|
||||||
|
/// Converts the response to a JSON map.
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return <String, dynamic>{
|
||||||
|
'fileUri': fileUri,
|
||||||
|
'contentType': contentType,
|
||||||
|
'size': size,
|
||||||
|
'bucket': bucket,
|
||||||
|
'path': path,
|
||||||
|
'requestId': requestId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
import '../core_api_endpoints.dart';
|
||||||
|
import 'file_upload_response.dart';
|
||||||
|
|
||||||
|
/// Service for uploading files to the Core API.
|
||||||
|
class FileUploadService extends BaseCoreService {
|
||||||
|
/// Creates a [FileUploadService].
|
||||||
|
FileUploadService(super.api);
|
||||||
|
|
||||||
|
/// Uploads a file with optional visibility and category.
|
||||||
|
///
|
||||||
|
/// [filePath] is the local path to the file.
|
||||||
|
/// [visibility] can be [FileVisibility.public] or [FileVisibility.private].
|
||||||
|
/// [category] is an optional metadata field.
|
||||||
|
Future<FileUploadResponse> uploadFile({
|
||||||
|
required String filePath,
|
||||||
|
required String fileName,
|
||||||
|
FileVisibility visibility = FileVisibility.private,
|
||||||
|
String? category,
|
||||||
|
}) async {
|
||||||
|
final ApiResponse res = await action(() async {
|
||||||
|
final FormData formData = FormData.fromMap(<String, dynamic>{
|
||||||
|
'file': await MultipartFile.fromFile(filePath, filename: fileName),
|
||||||
|
'visibility': visibility.value,
|
||||||
|
if (category != null) 'category': category,
|
||||||
|
});
|
||||||
|
|
||||||
|
return api.post(CoreApiEndpoints.uploadFile, data: formData);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.code.startsWith('2')) {
|
||||||
|
return FileUploadResponse.fromJson(res.data as Map<String, dynamic>);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw Exception(res.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
/// Response model for LLM invocation.
|
||||||
|
class LlmResponse {
|
||||||
|
/// Creates an [LlmResponse].
|
||||||
|
const LlmResponse({
|
||||||
|
required this.result,
|
||||||
|
required this.model,
|
||||||
|
required this.latencyMs,
|
||||||
|
this.requestId,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Factory to create [LlmResponse] from JSON.
|
||||||
|
factory LlmResponse.fromJson(Map<String, dynamic> json) {
|
||||||
|
return LlmResponse(
|
||||||
|
result: json['result'] as Map<String, dynamic>,
|
||||||
|
model: json['model'] as String,
|
||||||
|
latencyMs: json['latencyMs'] as int,
|
||||||
|
requestId: json['requestId'] as String?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The JSON result returned by the model.
|
||||||
|
final Map<String, dynamic> result;
|
||||||
|
|
||||||
|
/// The model name used for invocation.
|
||||||
|
final String model;
|
||||||
|
|
||||||
|
/// Time taken for the request in milliseconds.
|
||||||
|
final int latencyMs;
|
||||||
|
|
||||||
|
/// The unique request ID from the server.
|
||||||
|
final String? requestId;
|
||||||
|
|
||||||
|
/// Converts the response to a JSON map.
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return <String, dynamic>{
|
||||||
|
'result': result,
|
||||||
|
'model': model,
|
||||||
|
'latencyMs': latencyMs,
|
||||||
|
'requestId': requestId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
import '../core_api_endpoints.dart';
|
||||||
|
import 'llm_response.dart';
|
||||||
|
|
||||||
|
/// Service for invoking Large Language Models (LLM).
|
||||||
|
class LlmService extends BaseCoreService {
|
||||||
|
/// Creates an [LlmService].
|
||||||
|
LlmService(super.api);
|
||||||
|
|
||||||
|
/// Invokes the LLM with a [prompt] and optional [schema].
|
||||||
|
///
|
||||||
|
/// [prompt] is the text instruction for the model.
|
||||||
|
/// [responseJsonSchema] is an optional JSON schema to enforce structure.
|
||||||
|
/// [fileUrls] are optional URLs of files (images/PDFs) to include in context.
|
||||||
|
Future<LlmResponse> invokeLlm({
|
||||||
|
required String prompt,
|
||||||
|
Map<String, dynamic>? responseJsonSchema,
|
||||||
|
List<String>? fileUrls,
|
||||||
|
}) async {
|
||||||
|
final ApiResponse res = await action(() async {
|
||||||
|
return api.post(
|
||||||
|
CoreApiEndpoints.invokeLlm,
|
||||||
|
data: <String, dynamic>{
|
||||||
|
'prompt': prompt,
|
||||||
|
if (responseJsonSchema != null)
|
||||||
|
'responseJsonSchema': responseJsonSchema,
|
||||||
|
if (fileUrls != null) 'fileUrls': fileUrls,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.code.startsWith('2')) {
|
||||||
|
return LlmResponse.fromJson(res.data as Map<String, dynamic>);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw Exception(res.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
/// Response model for creating a signed URL.
|
||||||
|
class SignedUrlResponse {
|
||||||
|
/// Creates a [SignedUrlResponse].
|
||||||
|
const SignedUrlResponse({
|
||||||
|
required this.signedUrl,
|
||||||
|
required this.expiresAt,
|
||||||
|
this.requestId,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Factory to create [SignedUrlResponse] from JSON.
|
||||||
|
factory SignedUrlResponse.fromJson(Map<String, dynamic> json) {
|
||||||
|
return SignedUrlResponse(
|
||||||
|
signedUrl: json['signedUrl'] as String,
|
||||||
|
expiresAt: DateTime.parse(json['expiresAt'] as String),
|
||||||
|
requestId: json['requestId'] as String?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The generated signed URL.
|
||||||
|
final String signedUrl;
|
||||||
|
|
||||||
|
/// The timestamp when the URL expires.
|
||||||
|
final DateTime expiresAt;
|
||||||
|
|
||||||
|
/// The unique request ID from the server.
|
||||||
|
final String? requestId;
|
||||||
|
|
||||||
|
/// Converts the response to a JSON map.
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return <String, dynamic>{
|
||||||
|
'signedUrl': signedUrl,
|
||||||
|
'expiresAt': expiresAt.toIso8601String(),
|
||||||
|
'requestId': requestId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
import '../core_api_endpoints.dart';
|
||||||
|
import 'signed_url_response.dart';
|
||||||
|
|
||||||
|
/// Service for creating signed URLs for Cloud Storage objects.
|
||||||
|
class SignedUrlService extends BaseCoreService {
|
||||||
|
/// Creates a [SignedUrlService].
|
||||||
|
SignedUrlService(super.api);
|
||||||
|
|
||||||
|
/// Creates a signed URL for a specific [fileUri].
|
||||||
|
///
|
||||||
|
/// [fileUri] should be in gs:// format.
|
||||||
|
/// [expiresInSeconds] must be <= 900.
|
||||||
|
Future<SignedUrlResponse> createSignedUrl({
|
||||||
|
required String fileUri,
|
||||||
|
int expiresInSeconds = 300,
|
||||||
|
}) async {
|
||||||
|
final ApiResponse res = await action(() async {
|
||||||
|
return api.post(
|
||||||
|
CoreApiEndpoints.createSignedUrl,
|
||||||
|
data: <String, dynamic>{
|
||||||
|
'fileUri': fileUri,
|
||||||
|
'expiresInSeconds': expiresInSeconds,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.code.startsWith('2')) {
|
||||||
|
return SignedUrlResponse.fromJson(res.data as Map<String, dynamic>);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw Exception(res.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
/// Represents the possible statuses of a verification job.
|
||||||
|
enum VerificationStatus {
|
||||||
|
/// Job is created and waiting to be processed.
|
||||||
|
pending('PENDING'),
|
||||||
|
|
||||||
|
/// Job is currently being processed by machine or human.
|
||||||
|
processing('PROCESSING'),
|
||||||
|
|
||||||
|
/// Machine verification passed automatically.
|
||||||
|
autoPass('AUTO_PASS'),
|
||||||
|
|
||||||
|
/// Machine verification failed automatically.
|
||||||
|
autoFail('AUTO_FAIL'),
|
||||||
|
|
||||||
|
/// Machine results are inconclusive and require human review.
|
||||||
|
needsReview('NEEDS_REVIEW'),
|
||||||
|
|
||||||
|
/// Human reviewer approved the verification.
|
||||||
|
approved('APPROVED'),
|
||||||
|
|
||||||
|
/// Human reviewer rejected the verification.
|
||||||
|
rejected('REJECTED'),
|
||||||
|
|
||||||
|
/// An error occurred during processing.
|
||||||
|
error('ERROR');
|
||||||
|
|
||||||
|
const VerificationStatus(this.value);
|
||||||
|
|
||||||
|
/// The string value expected by the Core API.
|
||||||
|
final String value;
|
||||||
|
|
||||||
|
/// Creates a [VerificationStatus] from a string.
|
||||||
|
static VerificationStatus fromString(String value) {
|
||||||
|
return VerificationStatus.values.firstWhere(
|
||||||
|
(VerificationStatus e) => e.value == value,
|
||||||
|
orElse: () => VerificationStatus.error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Response model for verification operations.
|
||||||
|
class VerificationResponse {
|
||||||
|
/// Creates a [VerificationResponse].
|
||||||
|
const VerificationResponse({
|
||||||
|
required this.verificationId,
|
||||||
|
required this.status,
|
||||||
|
this.type,
|
||||||
|
this.review,
|
||||||
|
this.requestId,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Factory to create [VerificationResponse] from JSON.
|
||||||
|
factory VerificationResponse.fromJson(Map<String, dynamic> json) {
|
||||||
|
return VerificationResponse(
|
||||||
|
verificationId: json['verificationId'] as String,
|
||||||
|
status: VerificationStatus.fromString(json['status'] as String),
|
||||||
|
type: json['type'] as String?,
|
||||||
|
review: json['review'] != null
|
||||||
|
? json['review'] as Map<String, dynamic>
|
||||||
|
: null,
|
||||||
|
requestId: json['requestId'] as String?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The unique ID of the verification job.
|
||||||
|
final String verificationId;
|
||||||
|
|
||||||
|
/// Current status of the verification.
|
||||||
|
final VerificationStatus status;
|
||||||
|
|
||||||
|
/// The type of verification (e.g., attire, government_id).
|
||||||
|
final String? type;
|
||||||
|
|
||||||
|
/// Optional human review details.
|
||||||
|
final Map<String, dynamic>? review;
|
||||||
|
|
||||||
|
/// The unique request ID from the server.
|
||||||
|
final String? requestId;
|
||||||
|
|
||||||
|
/// Converts the response to a JSON map.
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return <String, dynamic>{
|
||||||
|
'verificationId': verificationId,
|
||||||
|
'status': status.value,
|
||||||
|
'type': type,
|
||||||
|
'review': review,
|
||||||
|
'requestId': requestId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
import '../core_api_endpoints.dart';
|
||||||
|
import 'verification_response.dart';
|
||||||
|
|
||||||
|
/// Service for handling async verification jobs.
|
||||||
|
class VerificationService extends BaseCoreService {
|
||||||
|
/// Creates a [VerificationService].
|
||||||
|
VerificationService(super.api);
|
||||||
|
|
||||||
|
/// Enqueues a new verification job.
|
||||||
|
///
|
||||||
|
/// [type] can be 'attire', 'government_id', etc.
|
||||||
|
/// [subjectType] is usually 'worker'.
|
||||||
|
/// [fileUri] is the gs:// path of the uploaded file.
|
||||||
|
Future<VerificationResponse> createVerification({
|
||||||
|
required String type,
|
||||||
|
required String subjectType,
|
||||||
|
required String subjectId,
|
||||||
|
required String fileUri,
|
||||||
|
Map<String, dynamic>? rules,
|
||||||
|
}) async {
|
||||||
|
final ApiResponse res = await action(() async {
|
||||||
|
return api.post(
|
||||||
|
CoreApiEndpoints.verifications,
|
||||||
|
data: <String, dynamic>{
|
||||||
|
'type': type,
|
||||||
|
'subjectType': subjectType,
|
||||||
|
'subjectId': subjectId,
|
||||||
|
'fileUri': fileUri,
|
||||||
|
if (rules != null) 'rules': rules,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.code.startsWith('2')) {
|
||||||
|
return VerificationResponse.fromJson(res.data as Map<String, dynamic>);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw Exception(res.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Polls the status of a specific verification.
|
||||||
|
Future<VerificationResponse> getStatus(String verificationId) async {
|
||||||
|
final ApiResponse res = await action(() async {
|
||||||
|
return api.get(CoreApiEndpoints.verificationStatus(verificationId));
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.code.startsWith('2')) {
|
||||||
|
return VerificationResponse.fromJson(res.data as Map<String, dynamic>);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw Exception(res.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Submits a manual review decision.
|
||||||
|
///
|
||||||
|
/// [decision] should be 'APPROVED' or 'REJECTED'.
|
||||||
|
Future<VerificationResponse> reviewVerification({
|
||||||
|
required String verificationId,
|
||||||
|
required String decision,
|
||||||
|
String? note,
|
||||||
|
String? reasonCode,
|
||||||
|
}) async {
|
||||||
|
final ApiResponse res = await action(() async {
|
||||||
|
return api.post(
|
||||||
|
CoreApiEndpoints.verificationReview(verificationId),
|
||||||
|
data: <String, dynamic>{
|
||||||
|
'decision': decision,
|
||||||
|
if (note != null) 'note': note,
|
||||||
|
if (reasonCode != null) 'reasonCode': reasonCode,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.code.startsWith('2')) {
|
||||||
|
return VerificationResponse.fromJson(res.data as Map<String, dynamic>);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw Exception(res.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retries a verification job that failed or needs re-processing.
|
||||||
|
Future<VerificationResponse> retryVerification(String verificationId) async {
|
||||||
|
final ApiResponse res = await action(() async {
|
||||||
|
return api.post(CoreApiEndpoints.verificationRetry(verificationId));
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.code.startsWith('2')) {
|
||||||
|
return VerificationResponse.fromJson(res.data as Map<String, dynamic>);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw Exception(res.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:krow_core/src/services/api_service/inspectors/auth_interceptor.dart';
|
||||||
|
|
||||||
|
/// A custom Dio client for the Krow project that includes basic configuration
|
||||||
|
/// and an [AuthInterceptor].
|
||||||
|
class DioClient extends DioMixin implements Dio {
|
||||||
|
DioClient([BaseOptions? baseOptions]) {
|
||||||
|
options =
|
||||||
|
baseOptions ??
|
||||||
|
BaseOptions(
|
||||||
|
connectTimeout: const Duration(seconds: 10),
|
||||||
|
receiveTimeout: const Duration(seconds: 10),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Use the default adapter
|
||||||
|
httpClientAdapter = HttpClientAdapter();
|
||||||
|
|
||||||
|
// Add interceptors
|
||||||
|
interceptors.addAll(<Interceptor>[
|
||||||
|
AuthInterceptor(),
|
||||||
|
LogInterceptor(
|
||||||
|
requestBody: true,
|
||||||
|
responseBody: true,
|
||||||
|
), // Added for better debugging
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:firebase_auth/firebase_auth.dart';
|
||||||
|
|
||||||
|
/// An interceptor that adds the Firebase Auth ID token to the Authorization header.
|
||||||
|
class AuthInterceptor extends Interceptor {
|
||||||
|
@override
|
||||||
|
Future<void> onRequest(
|
||||||
|
RequestOptions options,
|
||||||
|
RequestInterceptorHandler handler,
|
||||||
|
) async {
|
||||||
|
final User? user = FirebaseAuth.instance.currentUser;
|
||||||
|
if (user != null) {
|
||||||
|
try {
|
||||||
|
final String? token = await user.getIdToken();
|
||||||
|
if (token != null) {
|
||||||
|
options.headers['Authorization'] = 'Bearer $token';
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return handler.next(options);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import 'package:image_picker/image_picker.dart';
|
||||||
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
|
||||||
|
/// Service for capturing photos and videos using the device camera.
|
||||||
|
class CameraService extends BaseDeviceService {
|
||||||
|
/// Creates a [CameraService].
|
||||||
|
CameraService(ImagePicker picker) : _picker = picker;
|
||||||
|
|
||||||
|
final ImagePicker _picker;
|
||||||
|
|
||||||
|
/// Captures a photo using the camera.
|
||||||
|
///
|
||||||
|
/// Returns the path to the captured image, or null if cancelled.
|
||||||
|
Future<String?> takePhoto() async {
|
||||||
|
return action(() async {
|
||||||
|
final XFile? file = await _picker.pickImage(
|
||||||
|
source: ImageSource.camera,
|
||||||
|
imageQuality: 80,
|
||||||
|
);
|
||||||
|
return file?.path;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import 'package:file_picker/file_picker.dart';
|
||||||
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
|
||||||
|
/// Service for picking files from the device filesystem.
|
||||||
|
class FilePickerService extends BaseDeviceService {
|
||||||
|
/// Creates a [FilePickerService].
|
||||||
|
const FilePickerService();
|
||||||
|
|
||||||
|
/// Picks a single file from the device.
|
||||||
|
///
|
||||||
|
/// Returns the path to the selected file, or null if cancelled.
|
||||||
|
Future<String?> pickFile({List<String>? allowedExtensions}) async {
|
||||||
|
return action(() async {
|
||||||
|
final FilePickerResult? result = await FilePicker.platform.pickFiles(
|
||||||
|
type: allowedExtensions != null ? FileType.custom : FileType.any,
|
||||||
|
allowedExtensions: allowedExtensions,
|
||||||
|
);
|
||||||
|
|
||||||
|
return result?.files.single.path;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
import '../camera/camera_service.dart';
|
||||||
|
import '../gallery/gallery_service.dart';
|
||||||
|
import '../../api_service/core_api_services/file_upload/file_upload_service.dart';
|
||||||
|
import '../../api_service/core_api_services/file_upload/file_upload_response.dart';
|
||||||
|
|
||||||
|
/// Orchestrator service that combines device picking and network uploading.
|
||||||
|
///
|
||||||
|
/// This provides a simplified entry point for features to "pick and upload"
|
||||||
|
/// in a single call.
|
||||||
|
class DeviceFileUploadService extends BaseDeviceService {
|
||||||
|
/// Creates a [DeviceFileUploadService].
|
||||||
|
DeviceFileUploadService({
|
||||||
|
required this.cameraService,
|
||||||
|
required this.galleryService,
|
||||||
|
required this.apiUploadService,
|
||||||
|
});
|
||||||
|
|
||||||
|
final CameraService cameraService;
|
||||||
|
final GalleryService galleryService;
|
||||||
|
final FileUploadService apiUploadService;
|
||||||
|
|
||||||
|
/// Captures a photo from the camera and uploads it immediately.
|
||||||
|
Future<FileUploadResponse?> uploadFromCamera({
|
||||||
|
required String fileName,
|
||||||
|
FileVisibility visibility = FileVisibility.private,
|
||||||
|
String? category,
|
||||||
|
}) async {
|
||||||
|
return action(() async {
|
||||||
|
final String? path = await cameraService.takePhoto();
|
||||||
|
if (path == null) return null;
|
||||||
|
|
||||||
|
return apiUploadService.uploadFile(
|
||||||
|
filePath: path,
|
||||||
|
fileName: fileName,
|
||||||
|
visibility: visibility,
|
||||||
|
category: category,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Picks an image from the gallery and uploads it immediately.
|
||||||
|
Future<FileUploadResponse?> uploadFromGallery({
|
||||||
|
required String fileName,
|
||||||
|
FileVisibility visibility = FileVisibility.private,
|
||||||
|
String? category,
|
||||||
|
}) async {
|
||||||
|
return action(() async {
|
||||||
|
final String? path = await galleryService.pickImage();
|
||||||
|
if (path == null) return null;
|
||||||
|
|
||||||
|
return apiUploadService.uploadFile(
|
||||||
|
filePath: path,
|
||||||
|
fileName: fileName,
|
||||||
|
visibility: visibility,
|
||||||
|
category: category,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import 'package:image_picker/image_picker.dart';
|
||||||
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
|
||||||
|
/// Service for picking media from the device gallery.
|
||||||
|
class GalleryService extends BaseDeviceService {
|
||||||
|
/// Creates a [GalleryService].
|
||||||
|
GalleryService(this._picker);
|
||||||
|
|
||||||
|
final ImagePicker _picker;
|
||||||
|
|
||||||
|
/// Picks an image from the gallery.
|
||||||
|
///
|
||||||
|
/// Returns the path to the selected image, or null if cancelled.
|
||||||
|
Future<String?> pickImage() async {
|
||||||
|
return action(() async {
|
||||||
|
final XFile? file = await _picker.pickImage(
|
||||||
|
source: ImageSource.gallery,
|
||||||
|
imageQuality: 80,
|
||||||
|
);
|
||||||
|
return file?.path;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,10 +11,18 @@ environment:
|
|||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
flutter_bloc: ^8.1.0
|
|
||||||
design_system:
|
# internal packages
|
||||||
path: ../design_system
|
|
||||||
equatable: ^2.0.8
|
|
||||||
flutter_modular: ^6.4.1
|
|
||||||
krow_domain:
|
krow_domain:
|
||||||
path: ../domain
|
path: ../domain
|
||||||
|
design_system:
|
||||||
|
path: ../design_system
|
||||||
|
|
||||||
|
flutter_bloc: ^8.1.0
|
||||||
|
equatable: ^2.0.8
|
||||||
|
flutter_modular: ^6.4.1
|
||||||
|
dio: ^5.9.1
|
||||||
|
image_picker: ^1.1.2
|
||||||
|
path_provider: ^2.1.3
|
||||||
|
file_picker: ^8.1.7
|
||||||
|
firebase_auth: ^6.1.4
|
||||||
|
|||||||
@@ -208,6 +208,7 @@
|
|||||||
"edit_profile": "Edit Profile",
|
"edit_profile": "Edit Profile",
|
||||||
"hubs": "Hubs",
|
"hubs": "Hubs",
|
||||||
"log_out": "Log Out",
|
"log_out": "Log Out",
|
||||||
|
"log_out_confirmation": "Are you sure you want to log out?",
|
||||||
"quick_links": "Quick Links",
|
"quick_links": "Quick Links",
|
||||||
"clock_in_hubs": "Clock-In Hubs",
|
"clock_in_hubs": "Clock-In Hubs",
|
||||||
"billing_payments": "Billing & Payments"
|
"billing_payments": "Billing & Payments"
|
||||||
@@ -252,6 +253,11 @@
|
|||||||
"location_hint": "e.g., Downtown Restaurant",
|
"location_hint": "e.g., Downtown Restaurant",
|
||||||
"address_label": "Address",
|
"address_label": "Address",
|
||||||
"address_hint": "Full address",
|
"address_hint": "Full address",
|
||||||
|
"cost_center_label": "Cost Center",
|
||||||
|
"cost_center_hint": "eg: 1001, 1002",
|
||||||
|
"cost_centers_empty": "No cost centers available",
|
||||||
|
"name_required": "Name is required",
|
||||||
|
"address_required": "Address is required",
|
||||||
"create_button": "Create Hub"
|
"create_button": "Create Hub"
|
||||||
},
|
},
|
||||||
"edit_hub": {
|
"edit_hub": {
|
||||||
@@ -261,8 +267,14 @@
|
|||||||
"name_hint": "e.g., Main Kitchen, Front Desk",
|
"name_hint": "e.g., Main Kitchen, Front Desk",
|
||||||
"address_label": "Address",
|
"address_label": "Address",
|
||||||
"address_hint": "Full address",
|
"address_hint": "Full address",
|
||||||
|
"cost_center_label": "Cost Center",
|
||||||
|
"cost_center_hint": "eg: 1001, 1002",
|
||||||
|
"cost_centers_empty": "No cost centers available",
|
||||||
|
"name_required": "Name is required",
|
||||||
"save_button": "Save Changes",
|
"save_button": "Save Changes",
|
||||||
"success": "Hub updated successfully!"
|
"success": "Hub updated successfully!",
|
||||||
|
"created_success": "Hub created successfully",
|
||||||
|
"updated_success": "Hub updated successfully"
|
||||||
},
|
},
|
||||||
"hub_details": {
|
"hub_details": {
|
||||||
"title": "Hub Details",
|
"title": "Hub Details",
|
||||||
@@ -270,7 +282,10 @@
|
|||||||
"address_label": "Address",
|
"address_label": "Address",
|
||||||
"nfc_label": "NFC Tag",
|
"nfc_label": "NFC Tag",
|
||||||
"nfc_not_assigned": "Not Assigned",
|
"nfc_not_assigned": "Not Assigned",
|
||||||
"edit_button": "Edit Hub"
|
"cost_center_label": "Cost Center",
|
||||||
|
"cost_center_none": "Not Assigned",
|
||||||
|
"edit_button": "Edit Hub",
|
||||||
|
"deleted_success": "Hub deleted successfully"
|
||||||
},
|
},
|
||||||
"nfc_dialog": {
|
"nfc_dialog": {
|
||||||
"title": "Identify NFC Tag",
|
"title": "Identify NFC Tag",
|
||||||
@@ -326,6 +341,11 @@
|
|||||||
"date_hint": "Select date",
|
"date_hint": "Select date",
|
||||||
"location_label": "Location",
|
"location_label": "Location",
|
||||||
"location_hint": "Enter address",
|
"location_hint": "Enter address",
|
||||||
|
"hub_manager_label": "Shift Contact",
|
||||||
|
"hub_manager_desc": "On-site manager or supervisor for this shift",
|
||||||
|
"hub_manager_hint": "Select Contact",
|
||||||
|
"hub_manager_empty": "No hub managers available",
|
||||||
|
"hub_manager_none": "None",
|
||||||
"positions_title": "Positions",
|
"positions_title": "Positions",
|
||||||
"add_position": "Add Position",
|
"add_position": "Add Position",
|
||||||
"position_number": "Position $number",
|
"position_number": "Position $number",
|
||||||
@@ -377,6 +397,41 @@
|
|||||||
"active": "Active",
|
"active": "Active",
|
||||||
"completed": "Completed"
|
"completed": "Completed"
|
||||||
},
|
},
|
||||||
|
"order_edit_sheet": {
|
||||||
|
"title": "Edit Your Order",
|
||||||
|
"vendor_section": "VENDOR",
|
||||||
|
"location_section": "LOCATION",
|
||||||
|
"shift_contact_section": "SHIFT CONTACT",
|
||||||
|
"shift_contact_desc": "On-site manager or supervisor for this shift",
|
||||||
|
"select_contact": "Select Contact",
|
||||||
|
"no_hub_managers": "No hub managers available",
|
||||||
|
"none": "None",
|
||||||
|
"positions_section": "POSITIONS",
|
||||||
|
"add_position": "Add Position",
|
||||||
|
"review_positions": "Review $count Positions",
|
||||||
|
"order_name_hint": "Order name",
|
||||||
|
"remove": "Remove",
|
||||||
|
"select_role_hint": "Select role",
|
||||||
|
"start_label": "Start",
|
||||||
|
"end_label": "End",
|
||||||
|
"workers_label": "Workers",
|
||||||
|
"different_location": "Use different location for this position",
|
||||||
|
"different_location_title": "Different Location",
|
||||||
|
"enter_address_hint": "Enter different address",
|
||||||
|
"no_break": "No Break",
|
||||||
|
"positions": "Positions",
|
||||||
|
"workers": "Workers",
|
||||||
|
"est_cost": "Est. Cost",
|
||||||
|
"positions_breakdown": "Positions Breakdown",
|
||||||
|
"edit_button": "Edit",
|
||||||
|
"confirm_save": "Confirm & Save",
|
||||||
|
"position_singular": "Position",
|
||||||
|
"order_updated_title": "Order Updated!",
|
||||||
|
"order_updated_message": "Your shift has been updated successfully.",
|
||||||
|
"back_to_orders": "Back to Orders",
|
||||||
|
"one_time_order_title": "One-Time Order",
|
||||||
|
"refine_subtitle": "Refine your staffing needs"
|
||||||
|
},
|
||||||
"card": {
|
"card": {
|
||||||
"open": "OPEN",
|
"open": "OPEN",
|
||||||
"filled": "FILLED",
|
"filled": "FILLED",
|
||||||
@@ -1039,7 +1094,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"staff_profile_attire": {
|
"staff_profile_attire": {
|
||||||
"title": "Attire",
|
"title": "Verify Attire",
|
||||||
"info_card": {
|
"info_card": {
|
||||||
"title": "Your Wardrobe",
|
"title": "Your Wardrobe",
|
||||||
"description": "Select the attire items you own. This helps us match you with shifts that fit your wardrobe."
|
"description": "Select the attire items you own. This helps us match you with shifts that fit your wardrobe."
|
||||||
|
|||||||
@@ -208,6 +208,7 @@
|
|||||||
"edit_profile": "Editar Perfil",
|
"edit_profile": "Editar Perfil",
|
||||||
"hubs": "Hubs",
|
"hubs": "Hubs",
|
||||||
"log_out": "Cerrar sesi\u00f3n",
|
"log_out": "Cerrar sesi\u00f3n",
|
||||||
|
"log_out_confirmation": "\u00bfEst\u00e1 seguro de que desea cerrar sesi\u00f3n?",
|
||||||
"quick_links": "Enlaces r\u00e1pidos",
|
"quick_links": "Enlaces r\u00e1pidos",
|
||||||
"clock_in_hubs": "Hubs de Marcaje",
|
"clock_in_hubs": "Hubs de Marcaje",
|
||||||
"billing_payments": "Facturaci\u00f3n y Pagos"
|
"billing_payments": "Facturaci\u00f3n y Pagos"
|
||||||
@@ -252,6 +253,11 @@
|
|||||||
"location_hint": "ej., Restaurante Centro",
|
"location_hint": "ej., Restaurante Centro",
|
||||||
"address_label": "Direcci\u00f3n",
|
"address_label": "Direcci\u00f3n",
|
||||||
"address_hint": "Direcci\u00f3n completa",
|
"address_hint": "Direcci\u00f3n completa",
|
||||||
|
"cost_center_label": "Centro de Costos",
|
||||||
|
"cost_center_hint": "ej: 1001, 1002",
|
||||||
|
"cost_centers_empty": "No hay centros de costos disponibles",
|
||||||
|
"name_required": "Nombre es obligatorio",
|
||||||
|
"address_required": "La direcci\u00f3n es obligatoria",
|
||||||
"create_button": "Crear Hub"
|
"create_button": "Crear Hub"
|
||||||
},
|
},
|
||||||
"nfc_dialog": {
|
"nfc_dialog": {
|
||||||
@@ -276,8 +282,14 @@
|
|||||||
"name_hint": "Ingresar nombre del hub",
|
"name_hint": "Ingresar nombre del hub",
|
||||||
"address_label": "Direcci\u00f3n",
|
"address_label": "Direcci\u00f3n",
|
||||||
"address_hint": "Ingresar direcci\u00f3n",
|
"address_hint": "Ingresar direcci\u00f3n",
|
||||||
|
"cost_center_label": "Centro de Costos",
|
||||||
|
"cost_center_hint": "ej: 1001, 1002",
|
||||||
|
"cost_centers_empty": "No hay centros de costos disponibles",
|
||||||
|
"name_required": "El nombre es obligatorio",
|
||||||
"save_button": "Guardar Cambios",
|
"save_button": "Guardar Cambios",
|
||||||
"success": "\u00a1Hub actualizado exitosamente!"
|
"success": "\u00a1Hub actualizado exitosamente!",
|
||||||
|
"created_success": "Hub creado exitosamente",
|
||||||
|
"updated_success": "Hub actualizado exitosamente"
|
||||||
},
|
},
|
||||||
"hub_details": {
|
"hub_details": {
|
||||||
"title": "Detalles del Hub",
|
"title": "Detalles del Hub",
|
||||||
@@ -285,7 +297,10 @@
|
|||||||
"name_label": "Nombre del Hub",
|
"name_label": "Nombre del Hub",
|
||||||
"address_label": "Direcci\u00f3n",
|
"address_label": "Direcci\u00f3n",
|
||||||
"nfc_label": "Etiqueta NFC",
|
"nfc_label": "Etiqueta NFC",
|
||||||
"nfc_not_assigned": "No asignada"
|
"nfc_not_assigned": "No asignada",
|
||||||
|
"cost_center_label": "Centro de Costos",
|
||||||
|
"cost_center_none": "No asignado",
|
||||||
|
"deleted_success": "Hub eliminado exitosamente"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"client_create_order": {
|
"client_create_order": {
|
||||||
@@ -326,6 +341,11 @@
|
|||||||
"date_hint": "Seleccionar fecha",
|
"date_hint": "Seleccionar fecha",
|
||||||
"location_label": "Ubicaci\u00f3n",
|
"location_label": "Ubicaci\u00f3n",
|
||||||
"location_hint": "Ingresar direcci\u00f3n",
|
"location_hint": "Ingresar direcci\u00f3n",
|
||||||
|
"hub_manager_label": "Contacto del Turno",
|
||||||
|
"hub_manager_desc": "Gerente o supervisor en el sitio para este turno",
|
||||||
|
"hub_manager_hint": "Seleccionar Contacto",
|
||||||
|
"hub_manager_empty": "No hay contactos de turno disponibles",
|
||||||
|
"hub_manager_none": "Ninguno",
|
||||||
"positions_title": "Posiciones",
|
"positions_title": "Posiciones",
|
||||||
"add_position": "A\u00f1adir Posici\u00f3n",
|
"add_position": "A\u00f1adir Posici\u00f3n",
|
||||||
"position_number": "Posici\u00f3n $number",
|
"position_number": "Posici\u00f3n $number",
|
||||||
@@ -377,6 +397,41 @@
|
|||||||
"active": "Activos",
|
"active": "Activos",
|
||||||
"completed": "Completados"
|
"completed": "Completados"
|
||||||
},
|
},
|
||||||
|
"order_edit_sheet": {
|
||||||
|
"title": "Editar Tu Orden",
|
||||||
|
"vendor_section": "PROVEEDOR",
|
||||||
|
"location_section": "UBICACI\u00d3N",
|
||||||
|
"shift_contact_section": "CONTACTO DEL TURNO",
|
||||||
|
"shift_contact_desc": "Gerente o supervisor en el sitio para este turno",
|
||||||
|
"select_contact": "Seleccionar Contacto",
|
||||||
|
"no_hub_managers": "No hay contactos de turno disponibles",
|
||||||
|
"none": "Ninguno",
|
||||||
|
"positions_section": "POSICIONES",
|
||||||
|
"add_position": "A\u00f1adir Posici\u00f3n",
|
||||||
|
"review_positions": "Revisar $count Posiciones",
|
||||||
|
"order_name_hint": "Nombre de la orden",
|
||||||
|
"remove": "Eliminar",
|
||||||
|
"select_role_hint": "Seleccionar rol",
|
||||||
|
"start_label": "Inicio",
|
||||||
|
"end_label": "Fin",
|
||||||
|
"workers_label": "Trabajadores",
|
||||||
|
"different_location": "Usar ubicaci\u00f3n diferente para esta posici\u00f3n",
|
||||||
|
"different_location_title": "Ubicaci\u00f3n Diferente",
|
||||||
|
"enter_address_hint": "Ingresar direcci\u00f3n diferente",
|
||||||
|
"no_break": "Sin Descanso",
|
||||||
|
"positions": "Posiciones",
|
||||||
|
"workers": "Trabajadores",
|
||||||
|
"est_cost": "Costo Est.",
|
||||||
|
"positions_breakdown": "Desglose de Posiciones",
|
||||||
|
"edit_button": "Editar",
|
||||||
|
"confirm_save": "Confirmar y Guardar",
|
||||||
|
"position_singular": "Posici\u00f3n",
|
||||||
|
"order_updated_title": "\u00a1Orden Actualizada!",
|
||||||
|
"order_updated_message": "Tu turno ha sido actualizado exitosamente.",
|
||||||
|
"back_to_orders": "Volver a \u00d3rdenes",
|
||||||
|
"one_time_order_title": "Orden \u00danica Vez",
|
||||||
|
"refine_subtitle": "Ajusta tus necesidades de personal"
|
||||||
|
},
|
||||||
"card": {
|
"card": {
|
||||||
"open": "ABIERTO",
|
"open": "ABIERTO",
|
||||||
"filled": "LLENO",
|
"filled": "LLENO",
|
||||||
@@ -1039,7 +1094,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"staff_profile_attire": {
|
"staff_profile_attire": {
|
||||||
"title": "Vestimenta",
|
"title": "Verificar Vestimenta",
|
||||||
"info_card": {
|
"info_card": {
|
||||||
"title": "Tu Vestuario",
|
"title": "Tu Vestuario",
|
||||||
"description": "Selecciona los art\u00edculos de vestimenta que posees. Esto nos ayuda a asignarte turnos que se ajusten a tu vestuario."
|
"description": "Selecciona los art\u00edculos de vestimenta que posees. Esto nos ayuda a asignarte turnos que se ajusten a tu vestuario."
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// ignore_for_file: always_specify_types, depend_on_referenced_packages, dead_code, dead_null_aware_expression, unused_local_variable, unused_import, sort_constructors_first, prefer_final_fields, prefer_const_constructors, deprecated_member_use, implicit_call_tearoffs
|
// ignore_for_file: always_specify_types, depend_on_referenced_packages, dead_code, dead_null_aware_expression, unused_local_variable, unused_import, sort_constructors_first, prefer_final_fields, prefer_const_constructors, deprecated_member_use, implicit_call_tearoffs
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'package:firebase_data_connect/src/core/ref.dart';
|
import 'package:firebase_data_connect/src/core/ref.dart';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
@@ -23,7 +23,25 @@ class HubsConnectorRepositoryImpl implements HubsConnectorRepository {
|
|||||||
.getTeamHubsByTeamId(teamId: teamId)
|
.getTeamHubsByTeamId(teamId: teamId)
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
|
final QueryResult<
|
||||||
|
dc.ListTeamHudDepartmentsData,
|
||||||
|
dc.ListTeamHudDepartmentsVariables
|
||||||
|
>
|
||||||
|
deptsResult = await _service.connector.listTeamHudDepartments().execute();
|
||||||
|
final Map<String, dc.ListTeamHudDepartmentsTeamHudDepartments> hubToDept =
|
||||||
|
<String, dc.ListTeamHudDepartmentsTeamHudDepartments>{};
|
||||||
|
for (final dc.ListTeamHudDepartmentsTeamHudDepartments dep
|
||||||
|
in deptsResult.data.teamHudDepartments) {
|
||||||
|
if (dep.costCenter != null &&
|
||||||
|
dep.costCenter!.isNotEmpty &&
|
||||||
|
!hubToDept.containsKey(dep.teamHubId)) {
|
||||||
|
hubToDept[dep.teamHubId] = dep;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return response.data.teamHubs.map((dc.GetTeamHubsByTeamIdTeamHubs h) {
|
return response.data.teamHubs.map((dc.GetTeamHubsByTeamIdTeamHubs h) {
|
||||||
|
final dc.ListTeamHudDepartmentsTeamHudDepartments? dept =
|
||||||
|
hubToDept[h.id];
|
||||||
return Hub(
|
return Hub(
|
||||||
id: h.id,
|
id: h.id,
|
||||||
businessId: businessId,
|
businessId: businessId,
|
||||||
@@ -31,6 +49,13 @@ class HubsConnectorRepositoryImpl implements HubsConnectorRepository {
|
|||||||
address: h.address,
|
address: h.address,
|
||||||
nfcTagId: null,
|
nfcTagId: null,
|
||||||
status: h.isActive ? HubStatus.active : HubStatus.inactive,
|
status: h.isActive ? HubStatus.active : HubStatus.inactive,
|
||||||
|
costCenter: dept != null
|
||||||
|
? CostCenter(
|
||||||
|
id: dept.id,
|
||||||
|
name: dept.name,
|
||||||
|
code: dept.costCenter ?? dept.name,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
);
|
);
|
||||||
}).toList();
|
}).toList();
|
||||||
});
|
});
|
||||||
@@ -49,6 +74,7 @@ class HubsConnectorRepositoryImpl implements HubsConnectorRepository {
|
|||||||
String? street,
|
String? street,
|
||||||
String? country,
|
String? country,
|
||||||
String? zipCode,
|
String? zipCode,
|
||||||
|
String? costCenterId,
|
||||||
}) async {
|
}) async {
|
||||||
return _service.run(() async {
|
return _service.run(() async {
|
||||||
final String teamId = await _getOrCreateTeamId(businessId);
|
final String teamId = await _getOrCreateTeamId(businessId);
|
||||||
@@ -72,13 +98,27 @@ class HubsConnectorRepositoryImpl implements HubsConnectorRepository {
|
|||||||
.zipCode(zipCode ?? placeAddress?.zipCode)
|
.zipCode(zipCode ?? placeAddress?.zipCode)
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
|
final String hubId = result.data.teamHub_insert.id;
|
||||||
|
CostCenter? costCenter;
|
||||||
|
if (costCenterId != null && costCenterId.isNotEmpty) {
|
||||||
|
await _service.connector
|
||||||
|
.createTeamHudDepartment(
|
||||||
|
name: costCenterId,
|
||||||
|
teamHubId: hubId,
|
||||||
|
)
|
||||||
|
.costCenter(costCenterId)
|
||||||
|
.execute();
|
||||||
|
costCenter = CostCenter(id: costCenterId, name: costCenterId, code: costCenterId);
|
||||||
|
}
|
||||||
|
|
||||||
return Hub(
|
return Hub(
|
||||||
id: result.data.teamHub_insert.id,
|
id: hubId,
|
||||||
businessId: businessId,
|
businessId: businessId,
|
||||||
name: name,
|
name: name,
|
||||||
address: address,
|
address: address,
|
||||||
nfcTagId: null,
|
nfcTagId: null,
|
||||||
status: HubStatus.active,
|
status: HubStatus.active,
|
||||||
|
costCenter: costCenter,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -97,6 +137,7 @@ class HubsConnectorRepositoryImpl implements HubsConnectorRepository {
|
|||||||
String? street,
|
String? street,
|
||||||
String? country,
|
String? country,
|
||||||
String? zipCode,
|
String? zipCode,
|
||||||
|
String? costCenterId,
|
||||||
}) async {
|
}) async {
|
||||||
return _service.run(() async {
|
return _service.run(() async {
|
||||||
final _PlaceAddress? placeAddress = (placeId != null && placeId.isNotEmpty)
|
final _PlaceAddress? placeAddress = (placeId != null && placeId.isNotEmpty)
|
||||||
@@ -128,7 +169,43 @@ class HubsConnectorRepositoryImpl implements HubsConnectorRepository {
|
|||||||
|
|
||||||
await builder.execute();
|
await builder.execute();
|
||||||
|
|
||||||
// Return a basic hub object reflecting changes (or we could re-fetch)
|
CostCenter? costCenter;
|
||||||
|
final QueryResult<
|
||||||
|
dc.ListTeamHudDepartmentsByTeamHubIdData,
|
||||||
|
dc.ListTeamHudDepartmentsByTeamHubIdVariables
|
||||||
|
>
|
||||||
|
deptsResult = await _service.connector
|
||||||
|
.listTeamHudDepartmentsByTeamHubId(teamHubId: id)
|
||||||
|
.execute();
|
||||||
|
final List<dc.ListTeamHudDepartmentsByTeamHubIdTeamHudDepartments> depts =
|
||||||
|
deptsResult.data.teamHudDepartments;
|
||||||
|
|
||||||
|
if (costCenterId == null || costCenterId.isEmpty) {
|
||||||
|
if (depts.isNotEmpty) {
|
||||||
|
await _service.connector
|
||||||
|
.updateTeamHudDepartment(id: depts.first.id)
|
||||||
|
.costCenter(null)
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (depts.isNotEmpty) {
|
||||||
|
await _service.connector
|
||||||
|
.updateTeamHudDepartment(id: depts.first.id)
|
||||||
|
.costCenter(costCenterId)
|
||||||
|
.execute();
|
||||||
|
costCenter = CostCenter(id: costCenterId, name: costCenterId, code: costCenterId);
|
||||||
|
} else {
|
||||||
|
await _service.connector
|
||||||
|
.createTeamHudDepartment(
|
||||||
|
name: costCenterId,
|
||||||
|
teamHubId: id,
|
||||||
|
)
|
||||||
|
.costCenter(costCenterId)
|
||||||
|
.execute();
|
||||||
|
costCenter = CostCenter(id: costCenterId, name: costCenterId, code: costCenterId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return Hub(
|
return Hub(
|
||||||
id: id,
|
id: id,
|
||||||
businessId: businessId,
|
businessId: businessId,
|
||||||
@@ -136,6 +213,7 @@ class HubsConnectorRepositoryImpl implements HubsConnectorRepository {
|
|||||||
address: address ?? '',
|
address: address ?? '',
|
||||||
nfcTagId: null,
|
nfcTagId: null,
|
||||||
status: HubStatus.active,
|
status: HubStatus.active,
|
||||||
|
costCenter: costCenter,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ abstract interface class HubsConnectorRepository {
|
|||||||
String? street,
|
String? street,
|
||||||
String? country,
|
String? country,
|
||||||
String? zipCode,
|
String? zipCode,
|
||||||
|
String? costCenterId,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Updates an existing hub.
|
/// Updates an existing hub.
|
||||||
@@ -36,6 +37,7 @@ abstract interface class HubsConnectorRepository {
|
|||||||
String? street,
|
String? street,
|
||||||
String? country,
|
String? country,
|
||||||
String? zipCode,
|
String? zipCode,
|
||||||
|
String? costCenterId,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Deletes a hub.
|
/// Deletes a hub.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// ignore_for_file: always_specify_types, depend_on_referenced_packages, dead_code, dead_null_aware_expression, unused_local_variable, unused_import, sort_constructors_first, prefer_final_fields, prefer_const_constructors, deprecated_member_use, implicit_call_tearoffs, implementation_imports
|
import 'package:firebase_data_connect/firebase_data_connect.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/krow_data_connect.dart';
|
import 'package:krow_domain/krow_domain.dart' as domain;
|
||||||
import 'package:krow_domain/krow_domain.dart';
|
import '../../domain/repositories/staff_connector_repository.dart';
|
||||||
|
|
||||||
/// Implementation of [StaffConnectorRepository].
|
/// Implementation of [StaffConnectorRepository].
|
||||||
///
|
///
|
||||||
@@ -11,27 +11,28 @@ class StaffConnectorRepositoryImpl implements StaffConnectorRepository {
|
|||||||
/// Creates a new [StaffConnectorRepositoryImpl].
|
/// Creates a new [StaffConnectorRepositoryImpl].
|
||||||
///
|
///
|
||||||
/// Requires a [DataConnectService] instance for backend communication.
|
/// Requires a [DataConnectService] instance for backend communication.
|
||||||
StaffConnectorRepositoryImpl({
|
StaffConnectorRepositoryImpl({dc.DataConnectService? service})
|
||||||
DataConnectService? service,
|
: _service = service ?? dc.DataConnectService.instance;
|
||||||
}) : _service = service ?? DataConnectService.instance;
|
|
||||||
|
|
||||||
final DataConnectService _service;
|
final dc.DataConnectService _service;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<bool> getProfileCompletion() async {
|
Future<bool> getProfileCompletion() async {
|
||||||
return _service.run(() async {
|
return _service.run(() async {
|
||||||
final String staffId = await _service.getStaffId();
|
final String staffId = await _service.getStaffId();
|
||||||
|
|
||||||
final QueryResult<GetStaffProfileCompletionData,
|
final QueryResult<
|
||||||
GetStaffProfileCompletionVariables> response =
|
dc.GetStaffProfileCompletionData,
|
||||||
await _service.connector
|
dc.GetStaffProfileCompletionVariables
|
||||||
.getStaffProfileCompletion(id: staffId)
|
>
|
||||||
.execute();
|
response = await _service.connector
|
||||||
|
.getStaffProfileCompletion(id: staffId)
|
||||||
|
.execute();
|
||||||
|
|
||||||
final GetStaffProfileCompletionStaff? staff = response.data.staff;
|
final dc.GetStaffProfileCompletionStaff? staff = response.data.staff;
|
||||||
final List<GetStaffProfileCompletionEmergencyContacts>
|
final List<dc.GetStaffProfileCompletionEmergencyContacts>
|
||||||
emergencyContacts = response.data.emergencyContacts;
|
emergencyContacts = response.data.emergencyContacts;
|
||||||
final List<GetStaffProfileCompletionTaxForms> taxForms =
|
final List<dc.GetStaffProfileCompletionTaxForms> taxForms =
|
||||||
response.data.taxForms;
|
response.data.taxForms;
|
||||||
|
|
||||||
return _isProfileComplete(staff, emergencyContacts, taxForms);
|
return _isProfileComplete(staff, emergencyContacts, taxForms);
|
||||||
@@ -43,14 +44,15 @@ class StaffConnectorRepositoryImpl implements StaffConnectorRepository {
|
|||||||
return _service.run(() async {
|
return _service.run(() async {
|
||||||
final String staffId = await _service.getStaffId();
|
final String staffId = await _service.getStaffId();
|
||||||
|
|
||||||
final QueryResult<GetStaffPersonalInfoCompletionData,
|
final QueryResult<
|
||||||
GetStaffPersonalInfoCompletionVariables> response =
|
dc.GetStaffPersonalInfoCompletionData,
|
||||||
await _service.connector
|
dc.GetStaffPersonalInfoCompletionVariables
|
||||||
.getStaffPersonalInfoCompletion(id: staffId)
|
>
|
||||||
.execute();
|
response = await _service.connector
|
||||||
|
.getStaffPersonalInfoCompletion(id: staffId)
|
||||||
final GetStaffPersonalInfoCompletionStaff? staff = response.data.staff;
|
.execute();
|
||||||
|
|
||||||
|
final dc.GetStaffPersonalInfoCompletionStaff? staff = response.data.staff;
|
||||||
return _isPersonalInfoComplete(staff);
|
return _isPersonalInfoComplete(staff);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -60,11 +62,13 @@ class StaffConnectorRepositoryImpl implements StaffConnectorRepository {
|
|||||||
return _service.run(() async {
|
return _service.run(() async {
|
||||||
final String staffId = await _service.getStaffId();
|
final String staffId = await _service.getStaffId();
|
||||||
|
|
||||||
final QueryResult<GetStaffEmergencyProfileCompletionData,
|
final QueryResult<
|
||||||
GetStaffEmergencyProfileCompletionVariables> response =
|
dc.GetStaffEmergencyProfileCompletionData,
|
||||||
await _service.connector
|
dc.GetStaffEmergencyProfileCompletionVariables
|
||||||
.getStaffEmergencyProfileCompletion(id: staffId)
|
>
|
||||||
.execute();
|
response = await _service.connector
|
||||||
|
.getStaffEmergencyProfileCompletion(id: staffId)
|
||||||
|
.execute();
|
||||||
|
|
||||||
return response.data.emergencyContacts.isNotEmpty;
|
return response.data.emergencyContacts.isNotEmpty;
|
||||||
});
|
});
|
||||||
@@ -75,15 +79,16 @@ class StaffConnectorRepositoryImpl implements StaffConnectorRepository {
|
|||||||
return _service.run(() async {
|
return _service.run(() async {
|
||||||
final String staffId = await _service.getStaffId();
|
final String staffId = await _service.getStaffId();
|
||||||
|
|
||||||
final QueryResult<GetStaffExperienceProfileCompletionData,
|
final QueryResult<
|
||||||
GetStaffExperienceProfileCompletionVariables> response =
|
dc.GetStaffExperienceProfileCompletionData,
|
||||||
await _service.connector
|
dc.GetStaffExperienceProfileCompletionVariables
|
||||||
.getStaffExperienceProfileCompletion(id: staffId)
|
>
|
||||||
.execute();
|
response = await _service.connector
|
||||||
|
.getStaffExperienceProfileCompletion(id: staffId)
|
||||||
|
.execute();
|
||||||
|
|
||||||
final GetStaffExperienceProfileCompletionStaff? staff =
|
final dc.GetStaffExperienceProfileCompletionStaff? staff =
|
||||||
response.data.staff;
|
response.data.staff;
|
||||||
|
|
||||||
return _hasExperience(staff);
|
return _hasExperience(staff);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -93,113 +98,248 @@ class StaffConnectorRepositoryImpl implements StaffConnectorRepository {
|
|||||||
return _service.run(() async {
|
return _service.run(() async {
|
||||||
final String staffId = await _service.getStaffId();
|
final String staffId = await _service.getStaffId();
|
||||||
|
|
||||||
final QueryResult<GetStaffTaxFormsProfileCompletionData,
|
final QueryResult<
|
||||||
GetStaffTaxFormsProfileCompletionVariables> response =
|
dc.GetStaffTaxFormsProfileCompletionData,
|
||||||
await _service.connector
|
dc.GetStaffTaxFormsProfileCompletionVariables
|
||||||
.getStaffTaxFormsProfileCompletion(id: staffId)
|
>
|
||||||
.execute();
|
response = await _service.connector
|
||||||
|
.getStaffTaxFormsProfileCompletion(id: staffId)
|
||||||
|
.execute();
|
||||||
|
|
||||||
return response.data.taxForms.isNotEmpty;
|
return response.data.taxForms.isNotEmpty;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Checks if personal info is complete.
|
/// Checks if personal info is complete.
|
||||||
bool _isPersonalInfoComplete(GetStaffPersonalInfoCompletionStaff? staff) {
|
bool _isPersonalInfoComplete(dc.GetStaffPersonalInfoCompletionStaff? staff) {
|
||||||
if (staff == null) return false;
|
if (staff == null) return false;
|
||||||
final String fullName = staff.fullName;
|
final String fullName = staff.fullName;
|
||||||
final String? email = staff.email;
|
final String? email = staff.email;
|
||||||
final String? phone = staff.phone;
|
final String? phone = staff.phone;
|
||||||
return (fullName.trim().isNotEmpty ?? false) &&
|
return fullName.trim().isNotEmpty &&
|
||||||
(email?.trim().isNotEmpty ?? false) &&
|
(email?.trim().isNotEmpty ?? false) &&
|
||||||
(phone?.trim().isNotEmpty ?? false);
|
(phone?.trim().isNotEmpty ?? false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Checks if staff has experience data (skills or industries).
|
/// Checks if staff has experience data (skills or industries).
|
||||||
bool _hasExperience(GetStaffExperienceProfileCompletionStaff? staff) {
|
bool _hasExperience(dc.GetStaffExperienceProfileCompletionStaff? staff) {
|
||||||
if (staff == null) return false;
|
if (staff == null) return false;
|
||||||
final dynamic skills = staff.skills;
|
final List<String>? skills = staff.skills;
|
||||||
final dynamic industries = staff.industries;
|
final List<String>? industries = staff.industries;
|
||||||
return (skills is List && skills.isNotEmpty) ||
|
return (skills?.isNotEmpty ?? false) || (industries?.isNotEmpty ?? false);
|
||||||
(industries is List && industries.isNotEmpty);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Determines if the profile is complete based on all sections.
|
/// Determines if the profile is complete based on all sections.
|
||||||
bool _isProfileComplete(
|
bool _isProfileComplete(
|
||||||
GetStaffProfileCompletionStaff? staff,
|
dc.GetStaffProfileCompletionStaff? staff,
|
||||||
List<GetStaffProfileCompletionEmergencyContacts> emergencyContacts,
|
List<dc.GetStaffProfileCompletionEmergencyContacts> emergencyContacts,
|
||||||
List<GetStaffProfileCompletionTaxForms> taxForms,
|
List<dc.GetStaffProfileCompletionTaxForms> taxForms,
|
||||||
) {
|
) {
|
||||||
if (staff == null) return false;
|
if (staff == null) return false;
|
||||||
final dynamic skills = staff.skills;
|
|
||||||
final dynamic industries = staff.industries;
|
final List<String>? skills = staff.skills;
|
||||||
|
final List<String>? industries = staff.industries;
|
||||||
final bool hasExperience =
|
final bool hasExperience =
|
||||||
(skills is List && skills.isNotEmpty) ||
|
(skills?.isNotEmpty ?? false) || (industries?.isNotEmpty ?? false);
|
||||||
(industries is List && industries.isNotEmpty);
|
|
||||||
return emergencyContacts.isNotEmpty &&
|
return (staff.fullName.trim().isNotEmpty) &&
|
||||||
|
(staff.email?.trim().isNotEmpty ?? false) &&
|
||||||
|
emergencyContacts.isNotEmpty &&
|
||||||
taxForms.isNotEmpty &&
|
taxForms.isNotEmpty &&
|
||||||
hasExperience;
|
hasExperience;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Staff> getStaffProfile() async {
|
Future<domain.Staff> getStaffProfile() async {
|
||||||
return _service.run(() async {
|
return _service.run(() async {
|
||||||
final String staffId = await _service.getStaffId();
|
final String staffId = await _service.getStaffId();
|
||||||
|
|
||||||
final QueryResult<GetStaffByIdData, GetStaffByIdVariables> response =
|
final QueryResult<dc.GetStaffByIdData, dc.GetStaffByIdVariables>
|
||||||
await _service.connector
|
response = await _service.connector.getStaffById(id: staffId).execute();
|
||||||
.getStaffById(id: staffId)
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
if (response.data.staff == null) {
|
final dc.GetStaffByIdStaff? staff = response.data.staff;
|
||||||
throw const ServerException(
|
|
||||||
technicalMessage: 'Staff not found',
|
if (staff == null) {
|
||||||
);
|
throw Exception('Staff not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
final GetStaffByIdStaff rawStaff = response.data.staff!;
|
return domain.Staff(
|
||||||
|
id: staff.id,
|
||||||
// Map the raw data connect object to the Domain Entity
|
authProviderId: staff.userId,
|
||||||
return Staff(
|
name: staff.fullName,
|
||||||
id: rawStaff.id,
|
email: staff.email ?? '',
|
||||||
authProviderId: rawStaff.userId,
|
phone: staff.phone,
|
||||||
name: rawStaff.fullName,
|
avatar: staff.photoUrl,
|
||||||
email: rawStaff.email ?? '',
|
status: domain.StaffStatus.active,
|
||||||
phone: rawStaff.phone,
|
address: staff.addres,
|
||||||
avatar: rawStaff.photoUrl,
|
totalShifts: staff.totalShifts,
|
||||||
status: StaffStatus.active,
|
averageRating: staff.averageRating,
|
||||||
address: rawStaff.addres,
|
onTimeRate: staff.onTimeRate,
|
||||||
totalShifts: rawStaff.totalShifts,
|
noShowCount: staff.noShowCount,
|
||||||
averageRating: rawStaff.averageRating,
|
cancellationCount: staff.cancellationCount,
|
||||||
onTimeRate: rawStaff.onTimeRate,
|
reliabilityScore: staff.reliabilityScore,
|
||||||
noShowCount: rawStaff.noShowCount,
|
|
||||||
cancellationCount: rawStaff.cancellationCount,
|
|
||||||
reliabilityScore: rawStaff.reliabilityScore,
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<Benefit>> getBenefits() async {
|
Future<List<domain.Benefit>> getBenefits() async {
|
||||||
return _service.run(() async {
|
return _service.run(() async {
|
||||||
final String staffId = await _service.getStaffId();
|
final String staffId = await _service.getStaffId();
|
||||||
|
|
||||||
final QueryResult<ListBenefitsDataByStaffIdData,
|
final QueryResult<
|
||||||
ListBenefitsDataByStaffIdVariables> response =
|
dc.ListBenefitsDataByStaffIdData,
|
||||||
await _service.connector
|
dc.ListBenefitsDataByStaffIdVariables
|
||||||
.listBenefitsDataByStaffId(staffId: staffId)
|
>
|
||||||
.execute();
|
response = await _service.connector
|
||||||
|
.listBenefitsDataByStaffId(staffId: staffId)
|
||||||
|
.execute();
|
||||||
|
|
||||||
return response.data.benefitsDatas.map((data) {
|
return response.data.benefitsDatas
|
||||||
final plan = data.vendorBenefitPlan;
|
.map(
|
||||||
return Benefit(
|
(dc.ListBenefitsDataByStaffIdBenefitsDatas e) => domain.Benefit(
|
||||||
title: plan.title,
|
title: e.vendorBenefitPlan.title,
|
||||||
entitlementHours: plan.total?.toDouble() ?? 0.0,
|
entitlementHours: e.vendorBenefitPlan.total?.toDouble() ?? 0,
|
||||||
usedHours: data.current.toDouble(),
|
usedHours: e.current.toDouble(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<domain.AttireItem>> getAttireOptions() async {
|
||||||
|
return _service.run(() async {
|
||||||
|
final String staffId = await _service.getStaffId();
|
||||||
|
|
||||||
|
final List<QueryResult<Object, Object?>> results =
|
||||||
|
await Future.wait<QueryResult<Object, Object?>>(
|
||||||
|
<Future<QueryResult<Object, Object?>>>[
|
||||||
|
_service.connector.listAttireOptions().execute(),
|
||||||
|
_service.connector.getStaffAttire(staffId: staffId).execute(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
final QueryResult<dc.ListAttireOptionsData, void> optionsRes =
|
||||||
|
results[0] as QueryResult<dc.ListAttireOptionsData, void>;
|
||||||
|
final QueryResult<dc.GetStaffAttireData, dc.GetStaffAttireVariables>
|
||||||
|
staffAttireRes =
|
||||||
|
results[1]
|
||||||
|
as QueryResult<dc.GetStaffAttireData, dc.GetStaffAttireVariables>;
|
||||||
|
|
||||||
|
final List<dc.GetStaffAttireStaffAttires> staffAttire =
|
||||||
|
staffAttireRes.data.staffAttires;
|
||||||
|
|
||||||
|
return optionsRes.data.attireOptions.map((
|
||||||
|
dc.ListAttireOptionsAttireOptions opt,
|
||||||
|
) {
|
||||||
|
final dc.GetStaffAttireStaffAttires currentAttire = staffAttire
|
||||||
|
.firstWhere(
|
||||||
|
(dc.GetStaffAttireStaffAttires a) => a.attireOptionId == opt.id,
|
||||||
|
orElse: () => dc.GetStaffAttireStaffAttires(
|
||||||
|
attireOptionId: opt.id,
|
||||||
|
verificationPhotoUrl: null,
|
||||||
|
verificationId: null,
|
||||||
|
verificationStatus: null,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return domain.AttireItem(
|
||||||
|
id: opt.id,
|
||||||
|
code: opt.itemId,
|
||||||
|
label: opt.label,
|
||||||
|
description: opt.description,
|
||||||
|
imageUrl: opt.imageUrl,
|
||||||
|
isMandatory: opt.isMandatory ?? false,
|
||||||
|
photoUrl: currentAttire.verificationPhotoUrl,
|
||||||
|
verificationId: currentAttire.verificationId,
|
||||||
|
verificationStatus: currentAttire.verificationStatus != null
|
||||||
|
? _mapFromDCStatus(currentAttire.verificationStatus!)
|
||||||
|
: null,
|
||||||
);
|
);
|
||||||
}).toList();
|
}).toList();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> upsertStaffAttire({
|
||||||
|
required String attireOptionId,
|
||||||
|
required String photoUrl,
|
||||||
|
String? verificationId,
|
||||||
|
domain.AttireVerificationStatus? verificationStatus,
|
||||||
|
}) async {
|
||||||
|
await _service.run(() async {
|
||||||
|
final String staffId = await _service.getStaffId();
|
||||||
|
|
||||||
|
await _service.connector
|
||||||
|
.upsertStaffAttire(staffId: staffId, attireOptionId: attireOptionId)
|
||||||
|
.verificationPhotoUrl(photoUrl)
|
||||||
|
.verificationId(verificationId)
|
||||||
|
.verificationStatus(
|
||||||
|
verificationStatus != null
|
||||||
|
? dc.AttireVerificationStatus.values.firstWhere(
|
||||||
|
(dc.AttireVerificationStatus e) =>
|
||||||
|
e.name == verificationStatus.value.toUpperCase(),
|
||||||
|
orElse: () => dc.AttireVerificationStatus.PENDING,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
)
|
||||||
|
.execute();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
domain.AttireVerificationStatus _mapFromDCStatus(
|
||||||
|
dc.EnumValue<dc.AttireVerificationStatus> status,
|
||||||
|
) {
|
||||||
|
if (status is dc.Unknown) {
|
||||||
|
return domain.AttireVerificationStatus.error;
|
||||||
|
}
|
||||||
|
final String name =
|
||||||
|
(status as dc.Known<dc.AttireVerificationStatus>).value.name;
|
||||||
|
switch (name) {
|
||||||
|
case 'PENDING':
|
||||||
|
return domain.AttireVerificationStatus.pending;
|
||||||
|
case 'PROCESSING':
|
||||||
|
return domain.AttireVerificationStatus.processing;
|
||||||
|
case 'AUTO_PASS':
|
||||||
|
return domain.AttireVerificationStatus.autoPass;
|
||||||
|
case 'AUTO_FAIL':
|
||||||
|
return domain.AttireVerificationStatus.autoFail;
|
||||||
|
case 'NEEDS_REVIEW':
|
||||||
|
return domain.AttireVerificationStatus.needsReview;
|
||||||
|
case 'APPROVED':
|
||||||
|
return domain.AttireVerificationStatus.approved;
|
||||||
|
case 'REJECTED':
|
||||||
|
return domain.AttireVerificationStatus.rejected;
|
||||||
|
case 'ERROR':
|
||||||
|
return domain.AttireVerificationStatus.error;
|
||||||
|
default:
|
||||||
|
return domain.AttireVerificationStatus.error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> saveStaffProfile({
|
||||||
|
String? firstName,
|
||||||
|
String? lastName,
|
||||||
|
String? bio,
|
||||||
|
String? profilePictureUrl,
|
||||||
|
}) async {
|
||||||
|
await _service.run(() async {
|
||||||
|
final String staffId = await _service.getStaffId();
|
||||||
|
final String? fullName = (firstName != null || lastName != null)
|
||||||
|
? '${firstName ?? ''} ${lastName ?? ''}'.trim()
|
||||||
|
: null;
|
||||||
|
|
||||||
|
await _service.connector
|
||||||
|
.updateStaff(id: staffId)
|
||||||
|
.fullName(fullName)
|
||||||
|
.bio(bio)
|
||||||
|
.photoUrl(profilePictureUrl)
|
||||||
|
.execute();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> signOut() async {
|
Future<void> signOut() async {
|
||||||
try {
|
try {
|
||||||
@@ -210,4 +350,3 @@ class StaffConnectorRepositoryImpl implements StaffConnectorRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -45,10 +45,31 @@ abstract interface class StaffConnectorRepository {
|
|||||||
/// Returns a list of [Benefit] entities.
|
/// Returns a list of [Benefit] entities.
|
||||||
Future<List<Benefit>> getBenefits();
|
Future<List<Benefit>> getBenefits();
|
||||||
|
|
||||||
|
/// Fetches the attire options for the current authenticated user.
|
||||||
|
///
|
||||||
|
/// Returns a list of [AttireItem] entities.
|
||||||
|
Future<List<AttireItem>> getAttireOptions();
|
||||||
|
|
||||||
|
/// Upserts staff attire photo information.
|
||||||
|
Future<void> upsertStaffAttire({
|
||||||
|
required String attireOptionId,
|
||||||
|
required String photoUrl,
|
||||||
|
String? verificationId,
|
||||||
|
AttireVerificationStatus? verificationStatus,
|
||||||
|
});
|
||||||
|
|
||||||
/// Signs out the current user.
|
/// Signs out the current user.
|
||||||
///
|
///
|
||||||
/// Clears the user's session and authentication state.
|
/// Clears the user's session and authentication state.
|
||||||
///
|
///
|
||||||
/// Throws an exception if the sign-out fails.
|
/// Throws an exception if the sign-out fails.
|
||||||
Future<void> signOut();
|
Future<void> signOut();
|
||||||
|
|
||||||
|
/// Saves the staff profile information.
|
||||||
|
Future<void> saveStaffProfile({
|
||||||
|
String? firstName,
|
||||||
|
String? lastName,
|
||||||
|
String? bio,
|
||||||
|
String? profilePictureUrl,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -276,4 +276,7 @@ class UiIcons {
|
|||||||
|
|
||||||
/// Help circle icon for FAQs
|
/// Help circle icon for FAQs
|
||||||
static const IconData helpCircle = _IconLib.helpCircle;
|
static const IconData helpCircle = _IconLib.helpCircle;
|
||||||
|
|
||||||
|
/// Gallery icon for gallery
|
||||||
|
static const IconData gallery = _IconLib.galleryVertical;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -374,7 +374,7 @@ class UiTypography {
|
|||||||
/// Body 4 Regular - Font: Instrument Sans, Size: 14, Height: 1.5, Spacing: 0.05 (#121826)
|
/// Body 4 Regular - Font: Instrument Sans, Size: 14, Height: 1.5, Spacing: 0.05 (#121826)
|
||||||
static final TextStyle body4r = _primaryBase.copyWith(
|
static final TextStyle body4r = _primaryBase.copyWith(
|
||||||
fontWeight: FontWeight.w400,
|
fontWeight: FontWeight.w400,
|
||||||
fontSize: 12,
|
fontSize: 10,
|
||||||
height: 1.5,
|
height: 1.5,
|
||||||
letterSpacing: 0.05,
|
letterSpacing: 0.05,
|
||||||
color: UiColors.textPrimary,
|
color: UiColors.textPrimary,
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
import 'package:design_system/design_system.dart';
|
import 'package:design_system/design_system.dart';
|
||||||
import 'package:design_system/src/ui_typography.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import '../ui_icons.dart';
|
|
||||||
import 'ui_icon_button.dart';
|
|
||||||
|
|
||||||
/// A custom AppBar for the Krow UI design system.
|
/// A custom AppBar for the Krow UI design system.
|
||||||
///
|
///
|
||||||
/// This widget provides a consistent look and feel for top app bars across the application.
|
/// This widget provides a consistent look and feel for top app bars across the application.
|
||||||
@@ -12,6 +8,7 @@ class UiAppBar extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
const UiAppBar({
|
const UiAppBar({
|
||||||
super.key,
|
super.key,
|
||||||
this.title,
|
this.title,
|
||||||
|
this.subtitle,
|
||||||
this.titleWidget,
|
this.titleWidget,
|
||||||
this.leading,
|
this.leading,
|
||||||
this.actions,
|
this.actions,
|
||||||
@@ -25,6 +22,9 @@ class UiAppBar extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
/// The title text to display in the app bar.
|
/// The title text to display in the app bar.
|
||||||
final String? title;
|
final String? title;
|
||||||
|
|
||||||
|
/// The subtitle text to display in the app bar.
|
||||||
|
final String? subtitle;
|
||||||
|
|
||||||
/// A widget to display instead of the title text.
|
/// A widget to display instead of the title text.
|
||||||
final Widget? titleWidget;
|
final Widget? titleWidget;
|
||||||
|
|
||||||
@@ -57,7 +57,19 @@ class UiAppBar extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
return AppBar(
|
return AppBar(
|
||||||
title:
|
title:
|
||||||
titleWidget ??
|
titleWidget ??
|
||||||
(title != null ? Text(title!, style: UiTypography.headline4b) : null),
|
(title != null
|
||||||
|
? Column(
|
||||||
|
crossAxisAlignment: centerTitle
|
||||||
|
? CrossAxisAlignment.center
|
||||||
|
: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: <Widget>[
|
||||||
|
Text(title!, style: UiTypography.headline4b),
|
||||||
|
if (subtitle != null)
|
||||||
|
Text(subtitle!, style: UiTypography.body3r.textSecondary),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
: null),
|
||||||
leading:
|
leading:
|
||||||
leading ??
|
leading ??
|
||||||
(showBackButton
|
(showBackButton
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ import '../ui_typography.dart';
|
|||||||
|
|
||||||
/// Sizes for the [UiChip] widget.
|
/// Sizes for the [UiChip] widget.
|
||||||
enum UiChipSize {
|
enum UiChipSize {
|
||||||
|
// X-Small size (e.g. for tags in tight spaces).
|
||||||
|
xSmall,
|
||||||
|
|
||||||
/// Small size (e.g. for tags in tight spaces).
|
/// Small size (e.g. for tags in tight spaces).
|
||||||
small,
|
small,
|
||||||
|
|
||||||
@@ -25,6 +28,9 @@ enum UiChipVariant {
|
|||||||
|
|
||||||
/// Accent style with highlight background.
|
/// Accent style with highlight background.
|
||||||
accent,
|
accent,
|
||||||
|
|
||||||
|
/// Desructive style with red background.
|
||||||
|
destructive,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A custom chip widget with supports for different sizes, themes, and icons.
|
/// A custom chip widget with supports for different sizes, themes, and icons.
|
||||||
@@ -119,6 +125,8 @@ class UiChip extends StatelessWidget {
|
|||||||
return UiColors.tagInProgress;
|
return UiColors.tagInProgress;
|
||||||
case UiChipVariant.accent:
|
case UiChipVariant.accent:
|
||||||
return UiColors.accent;
|
return UiColors.accent;
|
||||||
|
case UiChipVariant.destructive:
|
||||||
|
return UiColors.iconError.withValues(alpha: 0.1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,11 +142,15 @@ class UiChip extends StatelessWidget {
|
|||||||
return UiColors.primary;
|
return UiColors.primary;
|
||||||
case UiChipVariant.accent:
|
case UiChipVariant.accent:
|
||||||
return UiColors.accentForeground;
|
return UiColors.accentForeground;
|
||||||
|
case UiChipVariant.destructive:
|
||||||
|
return UiColors.iconError;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TextStyle _getTextStyle() {
|
TextStyle _getTextStyle() {
|
||||||
switch (size) {
|
switch (size) {
|
||||||
|
case UiChipSize.xSmall:
|
||||||
|
return UiTypography.body4r;
|
||||||
case UiChipSize.small:
|
case UiChipSize.small:
|
||||||
return UiTypography.body3r;
|
return UiTypography.body3r;
|
||||||
case UiChipSize.medium:
|
case UiChipSize.medium:
|
||||||
@@ -150,6 +162,8 @@ class UiChip extends StatelessWidget {
|
|||||||
|
|
||||||
EdgeInsets _getPadding() {
|
EdgeInsets _getPadding() {
|
||||||
switch (size) {
|
switch (size) {
|
||||||
|
case UiChipSize.xSmall:
|
||||||
|
return const EdgeInsets.symmetric(horizontal: 6, vertical: 4);
|
||||||
case UiChipSize.small:
|
case UiChipSize.small:
|
||||||
return const EdgeInsets.symmetric(horizontal: 10, vertical: 6);
|
return const EdgeInsets.symmetric(horizontal: 10, vertical: 6);
|
||||||
case UiChipSize.medium:
|
case UiChipSize.medium:
|
||||||
@@ -161,6 +175,8 @@ class UiChip extends StatelessWidget {
|
|||||||
|
|
||||||
double _getIconSize() {
|
double _getIconSize() {
|
||||||
switch (size) {
|
switch (size) {
|
||||||
|
case UiChipSize.xSmall:
|
||||||
|
return 10;
|
||||||
case UiChipSize.small:
|
case UiChipSize.small:
|
||||||
return 12;
|
return 12;
|
||||||
case UiChipSize.medium:
|
case UiChipSize.medium:
|
||||||
@@ -172,6 +188,8 @@ class UiChip extends StatelessWidget {
|
|||||||
|
|
||||||
double _getGap() {
|
double _getGap() {
|
||||||
switch (size) {
|
switch (size) {
|
||||||
|
case UiChipSize.xSmall:
|
||||||
|
return UiConstants.space1;
|
||||||
case UiChipSize.small:
|
case UiChipSize.small:
|
||||||
return UiConstants.space1;
|
return UiConstants.space1;
|
||||||
case UiChipSize.medium:
|
case UiChipSize.medium:
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ class UiTextField extends StatelessWidget {
|
|||||||
|
|
||||||
const UiTextField({
|
const UiTextField({
|
||||||
super.key,
|
super.key,
|
||||||
|
this.semanticsIdentifier,
|
||||||
this.label,
|
this.label,
|
||||||
this.hintText,
|
this.hintText,
|
||||||
this.onChanged,
|
this.onChanged,
|
||||||
@@ -29,6 +30,8 @@ class UiTextField extends StatelessWidget {
|
|||||||
this.onTap,
|
this.onTap,
|
||||||
this.validator,
|
this.validator,
|
||||||
});
|
});
|
||||||
|
/// Optional semantics identifier for E2E testing (e.g. Maestro).
|
||||||
|
final String? semanticsIdentifier;
|
||||||
/// The label text to display above the text field.
|
/// The label text to display above the text field.
|
||||||
final String? label;
|
final String? label;
|
||||||
|
|
||||||
@@ -90,7 +93,9 @@ class UiTextField extends StatelessWidget {
|
|||||||
Text(label!, style: UiTypography.body4m.textSecondary),
|
Text(label!, style: UiTypography.body4m.textSecondary),
|
||||||
const SizedBox(height: UiConstants.space1),
|
const SizedBox(height: UiConstants.space1),
|
||||||
],
|
],
|
||||||
TextFormField(
|
Builder(
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
final Widget field = TextFormField(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
onChanged: onChanged,
|
onChanged: onChanged,
|
||||||
keyboardType: keyboardType,
|
keyboardType: keyboardType,
|
||||||
@@ -113,6 +118,15 @@ class UiTextField extends StatelessWidget {
|
|||||||
? Icon(suffixIcon, size: 20, color: UiColors.iconSecondary)
|
? Icon(suffixIcon, size: 20, color: UiColors.iconSecondary)
|
||||||
: suffix,
|
: suffix,
|
||||||
),
|
),
|
||||||
|
);
|
||||||
|
if (semanticsIdentifier != null) {
|
||||||
|
return Semantics(
|
||||||
|
identifier: semanticsIdentifier!,
|
||||||
|
child: field,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return field;
|
||||||
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,6 +6,15 @@
|
|||||||
/// Note: Repository Interfaces are now located in their respective Feature packages.
|
/// Note: Repository Interfaces are now located in their respective Feature packages.
|
||||||
library;
|
library;
|
||||||
|
|
||||||
|
// Core
|
||||||
|
export 'src/core/services/api_services/api_response.dart';
|
||||||
|
export 'src/core/services/api_services/base_api_service.dart';
|
||||||
|
export 'src/core/services/api_services/base_core_service.dart';
|
||||||
|
export 'src/core/services/api_services/file_visibility.dart';
|
||||||
|
|
||||||
|
// Device
|
||||||
|
export 'src/core/services/device/base_device_service.dart';
|
||||||
|
|
||||||
// Users & Membership
|
// Users & Membership
|
||||||
export 'src/entities/users/user.dart';
|
export 'src/entities/users/user.dart';
|
||||||
export 'src/entities/users/staff.dart';
|
export 'src/entities/users/staff.dart';
|
||||||
@@ -19,6 +28,7 @@ export 'src/entities/business/business_setting.dart';
|
|||||||
export 'src/entities/business/hub.dart';
|
export 'src/entities/business/hub.dart';
|
||||||
export 'src/entities/business/hub_department.dart';
|
export 'src/entities/business/hub_department.dart';
|
||||||
export 'src/entities/business/vendor.dart';
|
export 'src/entities/business/vendor.dart';
|
||||||
|
export 'src/entities/business/cost_center.dart';
|
||||||
|
|
||||||
// Events & Assignments
|
// Events & Assignments
|
||||||
export 'src/entities/events/event.dart';
|
export 'src/entities/events/event.dart';
|
||||||
@@ -68,6 +78,7 @@ export 'src/adapters/financial/bank_account/bank_account_adapter.dart';
|
|||||||
// Profile
|
// Profile
|
||||||
export 'src/entities/profile/staff_document.dart';
|
export 'src/entities/profile/staff_document.dart';
|
||||||
export 'src/entities/profile/attire_item.dart';
|
export 'src/entities/profile/attire_item.dart';
|
||||||
|
export 'src/entities/profile/attire_verification_status.dart';
|
||||||
export 'src/entities/profile/relationship_type.dart';
|
export 'src/entities/profile/relationship_type.dart';
|
||||||
export 'src/entities/profile/industry.dart';
|
export 'src/entities/profile/industry.dart';
|
||||||
export 'src/entities/profile/tax_form.dart';
|
export 'src/entities/profile/tax_form.dart';
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
/// Represents a standardized response from the API.
|
||||||
|
class ApiResponse {
|
||||||
|
/// Creates an [ApiResponse].
|
||||||
|
const ApiResponse({
|
||||||
|
required this.code,
|
||||||
|
required this.message,
|
||||||
|
this.data,
|
||||||
|
this.errors = const <String, dynamic>{},
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The response code (e.g., '200', '404', or custom error code).
|
||||||
|
final String code;
|
||||||
|
|
||||||
|
/// A descriptive message about the response.
|
||||||
|
final String message;
|
||||||
|
|
||||||
|
/// The payload returned by the API.
|
||||||
|
final dynamic data;
|
||||||
|
|
||||||
|
/// A map of field-specific error messages, if any.
|
||||||
|
final Map<String, dynamic> errors;
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import 'api_response.dart';
|
||||||
|
|
||||||
|
/// Abstract base class for API services.
|
||||||
|
///
|
||||||
|
/// This defines the contract for making HTTP requests.
|
||||||
|
abstract class BaseApiService {
|
||||||
|
/// Performs a GET request to the specified [endpoint].
|
||||||
|
Future<ApiResponse> get(String endpoint, {Map<String, dynamic>? params});
|
||||||
|
|
||||||
|
/// Performs a POST request to the specified [endpoint].
|
||||||
|
Future<ApiResponse> post(
|
||||||
|
String endpoint, {
|
||||||
|
dynamic data,
|
||||||
|
Map<String, dynamic>? params,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Performs a PUT request to the specified [endpoint].
|
||||||
|
Future<ApiResponse> put(
|
||||||
|
String endpoint, {
|
||||||
|
dynamic data,
|
||||||
|
Map<String, dynamic>? params,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Performs a PATCH request to the specified [endpoint].
|
||||||
|
Future<ApiResponse> patch(
|
||||||
|
String endpoint, {
|
||||||
|
dynamic data,
|
||||||
|
Map<String, dynamic>? params,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import 'api_response.dart';
|
||||||
|
import 'base_api_service.dart';
|
||||||
|
|
||||||
|
/// Abstract base class for core business services.
|
||||||
|
///
|
||||||
|
/// This provides a common [action] wrapper for standardized execution
|
||||||
|
/// and error catching across all core service implementations.
|
||||||
|
abstract class BaseCoreService {
|
||||||
|
/// Creates a [BaseCoreService] with the given [api] client.
|
||||||
|
const BaseCoreService(this.api);
|
||||||
|
|
||||||
|
/// The API client used to perform requests.
|
||||||
|
final BaseApiService api;
|
||||||
|
|
||||||
|
/// Standardized wrapper to execute API actions.
|
||||||
|
///
|
||||||
|
/// This handles generic error normalization for unexpected non-HTTP errors.
|
||||||
|
Future<ApiResponse> action(Future<ApiResponse> Function() execution) async {
|
||||||
|
try {
|
||||||
|
return await execution();
|
||||||
|
} catch (e) {
|
||||||
|
return ApiResponse(
|
||||||
|
code: 'CORE_INTERNAL_ERROR',
|
||||||
|
message: e.toString(),
|
||||||
|
errors: <String, dynamic>{'exception': e.runtimeType.toString()},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
/// Represents the accessibility level of an uploaded file.
|
||||||
|
enum FileVisibility {
|
||||||
|
/// File is accessible only to authenticated owners/authorized users.
|
||||||
|
private('private'),
|
||||||
|
|
||||||
|
/// File is accessible publicly via its URL.
|
||||||
|
public('public');
|
||||||
|
|
||||||
|
/// Creates a [FileVisibility].
|
||||||
|
const FileVisibility(this.value);
|
||||||
|
|
||||||
|
/// The string value expected by the backend.
|
||||||
|
final String value;
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
/// Abstract base class for device-related services.
|
||||||
|
///
|
||||||
|
/// Device services handle native hardware/platform interactions
|
||||||
|
/// like Camera, Gallery, Location, or Biometrics.
|
||||||
|
abstract class BaseDeviceService {
|
||||||
|
const BaseDeviceService();
|
||||||
|
|
||||||
|
/// Standardized wrapper to execute device actions.
|
||||||
|
///
|
||||||
|
/// This can be used for common handling like logging device interactions
|
||||||
|
/// or catching native platform exceptions.
|
||||||
|
Future<T> action<T>(Future<T> Function() execution) async {
|
||||||
|
try {
|
||||||
|
return await execution();
|
||||||
|
} catch (e) {
|
||||||
|
// Re-throw or handle based on project preference.
|
||||||
|
// For device services, we might want to throw specific
|
||||||
|
// DeviceExceptions later.
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
|
||||||
|
/// Represents a financial cost center used for billing and tracking.
|
||||||
|
class CostCenter extends Equatable {
|
||||||
|
const CostCenter({
|
||||||
|
required this.id,
|
||||||
|
required this.name,
|
||||||
|
this.code,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Unique identifier.
|
||||||
|
final String id;
|
||||||
|
|
||||||
|
/// Display name of the cost center.
|
||||||
|
final String name;
|
||||||
|
|
||||||
|
/// Optional alphanumeric code associated with this cost center.
|
||||||
|
final String? code;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => <Object?>[id, name, code];
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
|
|
||||||
|
import 'cost_center.dart';
|
||||||
|
|
||||||
/// The status of a [Hub].
|
/// The status of a [Hub].
|
||||||
enum HubStatus {
|
enum HubStatus {
|
||||||
/// Fully operational.
|
/// Fully operational.
|
||||||
@@ -14,7 +16,6 @@ enum HubStatus {
|
|||||||
|
|
||||||
/// Represents a branch location or operational unit within a [Business].
|
/// Represents a branch location or operational unit within a [Business].
|
||||||
class Hub extends Equatable {
|
class Hub extends Equatable {
|
||||||
|
|
||||||
const Hub({
|
const Hub({
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.businessId,
|
required this.businessId,
|
||||||
@@ -22,6 +23,7 @@ class Hub extends Equatable {
|
|||||||
required this.address,
|
required this.address,
|
||||||
this.nfcTagId,
|
this.nfcTagId,
|
||||||
required this.status,
|
required this.status,
|
||||||
|
this.costCenter,
|
||||||
});
|
});
|
||||||
/// Unique identifier.
|
/// Unique identifier.
|
||||||
final String id;
|
final String id;
|
||||||
@@ -41,6 +43,9 @@ class Hub extends Equatable {
|
|||||||
/// Operational status.
|
/// Operational status.
|
||||||
final HubStatus status;
|
final HubStatus status;
|
||||||
|
|
||||||
|
/// Assigned cost center for this hub.
|
||||||
|
final CostCenter? costCenter;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => <Object?>[id, businessId, name, address, nfcTagId, status];
|
List<Object?> get props => <Object?>[id, businessId, name, address, nfcTagId, status, costCenter];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ class OneTimeOrder extends Equatable {
|
|||||||
this.hub,
|
this.hub,
|
||||||
this.eventName,
|
this.eventName,
|
||||||
this.vendorId,
|
this.vendorId,
|
||||||
|
this.hubManagerId,
|
||||||
this.roleRates = const <String, double>{},
|
this.roleRates = const <String, double>{},
|
||||||
});
|
});
|
||||||
/// The specific date for the shift or event.
|
/// The specific date for the shift or event.
|
||||||
@@ -33,6 +34,9 @@ class OneTimeOrder extends Equatable {
|
|||||||
/// Selected vendor id for this order.
|
/// Selected vendor id for this order.
|
||||||
final String? vendorId;
|
final String? vendorId;
|
||||||
|
|
||||||
|
/// Optional hub manager id.
|
||||||
|
final String? hubManagerId;
|
||||||
|
|
||||||
/// Role hourly rates keyed by role id.
|
/// Role hourly rates keyed by role id.
|
||||||
final Map<String, double> roleRates;
|
final Map<String, double> roleRates;
|
||||||
|
|
||||||
@@ -44,6 +48,7 @@ class OneTimeOrder extends Equatable {
|
|||||||
hub,
|
hub,
|
||||||
eventName,
|
eventName,
|
||||||
vendorId,
|
vendorId,
|
||||||
|
hubManagerId,
|
||||||
roleRates,
|
roleRates,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ class OrderItem extends Equatable {
|
|||||||
this.hours = 0,
|
this.hours = 0,
|
||||||
this.totalValue = 0,
|
this.totalValue = 0,
|
||||||
this.confirmedApps = const <Map<String, dynamic>>[],
|
this.confirmedApps = const <Map<String, dynamic>>[],
|
||||||
|
this.hubManagerId,
|
||||||
|
this.hubManagerName,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Unique identifier of the order.
|
/// Unique identifier of the order.
|
||||||
@@ -83,6 +85,12 @@ class OrderItem extends Equatable {
|
|||||||
/// List of confirmed worker applications.
|
/// List of confirmed worker applications.
|
||||||
final List<Map<String, dynamic>> confirmedApps;
|
final List<Map<String, dynamic>> confirmedApps;
|
||||||
|
|
||||||
|
/// Optional ID of the assigned hub manager.
|
||||||
|
final String? hubManagerId;
|
||||||
|
|
||||||
|
/// Optional Name of the assigned hub manager.
|
||||||
|
final String? hubManagerName;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => <Object?>[
|
List<Object?> get props => <Object?>[
|
||||||
id,
|
id,
|
||||||
@@ -103,5 +111,7 @@ class OrderItem extends Equatable {
|
|||||||
totalValue,
|
totalValue,
|
||||||
eventName,
|
eventName,
|
||||||
confirmedApps,
|
confirmedApps,
|
||||||
|
hubManagerId,
|
||||||
|
hubManagerName,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ class PermanentOrder extends Equatable {
|
|||||||
this.hub,
|
this.hub,
|
||||||
this.eventName,
|
this.eventName,
|
||||||
this.vendorId,
|
this.vendorId,
|
||||||
|
this.hubManagerId,
|
||||||
this.roleRates = const <String, double>{},
|
this.roleRates = const <String, double>{},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -23,6 +24,7 @@ class PermanentOrder extends Equatable {
|
|||||||
final OneTimeOrderHubDetails? hub;
|
final OneTimeOrderHubDetails? hub;
|
||||||
final String? eventName;
|
final String? eventName;
|
||||||
final String? vendorId;
|
final String? vendorId;
|
||||||
|
final String? hubManagerId;
|
||||||
final Map<String, double> roleRates;
|
final Map<String, double> roleRates;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -33,6 +35,7 @@ class PermanentOrder extends Equatable {
|
|||||||
hub,
|
hub,
|
||||||
eventName,
|
eventName,
|
||||||
vendorId,
|
vendorId,
|
||||||
|
hubManagerId,
|
||||||
roleRates,
|
roleRates,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ class RecurringOrder extends Equatable {
|
|||||||
this.hub,
|
this.hub,
|
||||||
this.eventName,
|
this.eventName,
|
||||||
this.vendorId,
|
this.vendorId,
|
||||||
|
this.hubManagerId,
|
||||||
this.roleRates = const <String, double>{},
|
this.roleRates = const <String, double>{},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -39,6 +40,9 @@ class RecurringOrder extends Equatable {
|
|||||||
/// Selected vendor id for this order.
|
/// Selected vendor id for this order.
|
||||||
final String? vendorId;
|
final String? vendorId;
|
||||||
|
|
||||||
|
/// Optional hub manager id.
|
||||||
|
final String? hubManagerId;
|
||||||
|
|
||||||
/// Role hourly rates keyed by role id.
|
/// Role hourly rates keyed by role id.
|
||||||
final Map<String, double> roleRates;
|
final Map<String, double> roleRates;
|
||||||
|
|
||||||
@@ -52,6 +56,7 @@ class RecurringOrder extends Equatable {
|
|||||||
hub,
|
hub,
|
||||||
eventName,
|
eventName,
|
||||||
vendorId,
|
vendorId,
|
||||||
|
hubManagerId,
|
||||||
roleRates,
|
roleRates,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,35 @@
|
|||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
|
|
||||||
|
import 'attire_verification_status.dart';
|
||||||
|
|
||||||
/// Represents an attire item that a staff member might need or possess.
|
/// Represents an attire item that a staff member might need or possess.
|
||||||
///
|
///
|
||||||
/// Attire items are specific clothing or equipment required for jobs.
|
/// Attire items are specific clothing or equipment required for jobs.
|
||||||
class AttireItem extends Equatable {
|
class AttireItem extends Equatable {
|
||||||
|
|
||||||
/// Creates an [AttireItem].
|
/// Creates an [AttireItem].
|
||||||
const AttireItem({
|
const AttireItem({
|
||||||
required this.id,
|
required this.id,
|
||||||
|
required this.code,
|
||||||
required this.label,
|
required this.label,
|
||||||
this.iconName,
|
this.description,
|
||||||
this.imageUrl,
|
this.imageUrl,
|
||||||
this.isMandatory = false,
|
this.isMandatory = false,
|
||||||
|
this.verificationStatus,
|
||||||
|
this.photoUrl,
|
||||||
|
this.verificationId,
|
||||||
});
|
});
|
||||||
/// Unique identifier of the attire item.
|
|
||||||
|
/// Unique identifier of the attire item (UUID).
|
||||||
final String id;
|
final String id;
|
||||||
|
|
||||||
|
/// String code for the attire item (e.g. BLACK_TSHIRT).
|
||||||
|
final String code;
|
||||||
|
|
||||||
/// Display name of the item.
|
/// Display name of the item.
|
||||||
final String label;
|
final String label;
|
||||||
|
|
||||||
/// Name of the icon to display (mapped in UI).
|
/// Optional description for the attire item.
|
||||||
final String? iconName;
|
final String? description;
|
||||||
|
|
||||||
/// URL of the reference image.
|
/// URL of the reference image.
|
||||||
final String? imageUrl;
|
final String? imageUrl;
|
||||||
@@ -28,6 +37,50 @@ class AttireItem extends Equatable {
|
|||||||
/// Whether this item is mandatory for onboarding.
|
/// Whether this item is mandatory for onboarding.
|
||||||
final bool isMandatory;
|
final bool isMandatory;
|
||||||
|
|
||||||
|
/// The current verification status of the uploaded photo.
|
||||||
|
final AttireVerificationStatus? verificationStatus;
|
||||||
|
|
||||||
|
/// The URL of the photo uploaded by the staff member.
|
||||||
|
final String? photoUrl;
|
||||||
|
|
||||||
|
/// The ID of the verification record.
|
||||||
|
final String? verificationId;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => <Object?>[id, label, iconName, imageUrl, isMandatory];
|
List<Object?> get props => <Object?>[
|
||||||
|
id,
|
||||||
|
code,
|
||||||
|
label,
|
||||||
|
description,
|
||||||
|
imageUrl,
|
||||||
|
isMandatory,
|
||||||
|
verificationStatus,
|
||||||
|
photoUrl,
|
||||||
|
verificationId,
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Creates a copy of this [AttireItem] with the given fields replaced.
|
||||||
|
AttireItem copyWith({
|
||||||
|
String? id,
|
||||||
|
String? code,
|
||||||
|
String? label,
|
||||||
|
String? description,
|
||||||
|
String? imageUrl,
|
||||||
|
bool? isMandatory,
|
||||||
|
AttireVerificationStatus? verificationStatus,
|
||||||
|
String? photoUrl,
|
||||||
|
String? verificationId,
|
||||||
|
}) {
|
||||||
|
return AttireItem(
|
||||||
|
id: id ?? this.id,
|
||||||
|
code: code ?? this.code,
|
||||||
|
label: label ?? this.label,
|
||||||
|
description: description ?? this.description,
|
||||||
|
imageUrl: imageUrl ?? this.imageUrl,
|
||||||
|
isMandatory: isMandatory ?? this.isMandatory,
|
||||||
|
verificationStatus: verificationStatus ?? this.verificationStatus,
|
||||||
|
photoUrl: photoUrl ?? this.photoUrl,
|
||||||
|
verificationId: verificationId ?? this.verificationId,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
/// Represents the verification status of an attire item photo.
|
||||||
|
enum AttireVerificationStatus {
|
||||||
|
/// Job is created and waiting to be processed.
|
||||||
|
pending('PENDING'),
|
||||||
|
|
||||||
|
/// Job is currently being processed by machine or human.
|
||||||
|
processing('PROCESSING'),
|
||||||
|
|
||||||
|
/// Machine verification passed automatically.
|
||||||
|
autoPass('AUTO_PASS'),
|
||||||
|
|
||||||
|
/// Machine verification failed automatically.
|
||||||
|
autoFail('AUTO_FAIL'),
|
||||||
|
|
||||||
|
/// Machine results are inconclusive and require human review.
|
||||||
|
needsReview('NEEDS_REVIEW'),
|
||||||
|
|
||||||
|
/// Human reviewer approved the verification.
|
||||||
|
approved('APPROVED'),
|
||||||
|
|
||||||
|
/// Human reviewer rejected the verification.
|
||||||
|
rejected('REJECTED'),
|
||||||
|
|
||||||
|
/// An error occurred during processing.
|
||||||
|
error('ERROR');
|
||||||
|
|
||||||
|
const AttireVerificationStatus(this.value);
|
||||||
|
|
||||||
|
/// The string value expected by the Core API.
|
||||||
|
final String value;
|
||||||
|
|
||||||
|
/// Creates a [AttireVerificationStatus] from a string.
|
||||||
|
static AttireVerificationStatus fromString(String value) {
|
||||||
|
return AttireVerificationStatus.values.firstWhere(
|
||||||
|
(AttireVerificationStatus e) => e.value == value,
|
||||||
|
orElse: () => AttireVerificationStatus.error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -52,6 +52,7 @@ class _ClientSignInFormState extends State<ClientSignInForm> {
|
|||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
// Email Field
|
// Email Field
|
||||||
UiTextField(
|
UiTextField(
|
||||||
|
semanticsIdentifier: 'sign_in_email',
|
||||||
label: i18n.email_label,
|
label: i18n.email_label,
|
||||||
hintText: i18n.email_hint,
|
hintText: i18n.email_hint,
|
||||||
controller: _emailController,
|
controller: _emailController,
|
||||||
@@ -61,6 +62,7 @@ class _ClientSignInFormState extends State<ClientSignInForm> {
|
|||||||
|
|
||||||
// Password Field
|
// Password Field
|
||||||
UiTextField(
|
UiTextField(
|
||||||
|
semanticsIdentifier: 'sign_in_password',
|
||||||
label: i18n.password_label,
|
label: i18n.password_label,
|
||||||
hintText: i18n.password_hint,
|
hintText: i18n.password_hint,
|
||||||
controller: _passwordController,
|
controller: _passwordController,
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ class _ClientSignUpFormState extends State<ClientSignUpForm> {
|
|||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
// Company Name Field
|
// Company Name Field
|
||||||
UiTextField(
|
UiTextField(
|
||||||
|
semanticsIdentifier: 'sign_up_company',
|
||||||
label: i18n.company_label,
|
label: i18n.company_label,
|
||||||
hintText: i18n.company_hint,
|
hintText: i18n.company_hint,
|
||||||
controller: _companyController,
|
controller: _companyController,
|
||||||
@@ -79,6 +80,7 @@ class _ClientSignUpFormState extends State<ClientSignUpForm> {
|
|||||||
|
|
||||||
// Email Field
|
// Email Field
|
||||||
UiTextField(
|
UiTextField(
|
||||||
|
semanticsIdentifier: 'sign_up_email',
|
||||||
label: i18n.email_label,
|
label: i18n.email_label,
|
||||||
hintText: i18n.email_hint,
|
hintText: i18n.email_hint,
|
||||||
controller: _emailController,
|
controller: _emailController,
|
||||||
@@ -89,6 +91,7 @@ class _ClientSignUpFormState extends State<ClientSignUpForm> {
|
|||||||
|
|
||||||
// Password Field
|
// Password Field
|
||||||
UiTextField(
|
UiTextField(
|
||||||
|
semanticsIdentifier: 'sign_up_password',
|
||||||
label: i18n.password_label,
|
label: i18n.password_label,
|
||||||
hintText: i18n.password_hint,
|
hintText: i18n.password_hint,
|
||||||
controller: _passwordController,
|
controller: _passwordController,
|
||||||
@@ -108,6 +111,7 @@ class _ClientSignUpFormState extends State<ClientSignUpForm> {
|
|||||||
|
|
||||||
// Confirm Password Field
|
// Confirm Password Field
|
||||||
UiTextField(
|
UiTextField(
|
||||||
|
semanticsIdentifier: 'sign_up_confirm_password',
|
||||||
label: i18n.confirm_password_label,
|
label: i18n.confirm_password_label,
|
||||||
hintText: i18n.confirm_password_hint,
|
hintText: i18n.confirm_password_hint,
|
||||||
controller: _confirmPasswordController,
|
controller: _confirmPasswordController,
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ class SavingsCard extends StatelessWidget {
|
|||||||
const SizedBox(height: UiConstants.space1),
|
const SizedBox(height: UiConstants.space1),
|
||||||
Text(
|
Text(
|
||||||
// Using a hardcoded 180 here to match prototype mock or derived value
|
// Using a hardcoded 180 here to match prototype mock or derived value
|
||||||
t.client_billing.rate_optimization_body(amount: 180),
|
"180",
|
||||||
style: UiTypography.footnote2r.textSecondary,
|
style: UiTypography.footnote2r.textSecondary,
|
||||||
),
|
),
|
||||||
const SizedBox(height: UiConstants.space2),
|
const SizedBox(height: UiConstants.space2),
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
library;
|
library;
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_modular/flutter_modular.dart';
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
import 'package:krow_core/core.dart';
|
import 'package:krow_core/core.dart';
|
||||||
import 'package:krow_data_connect/krow_data_connect.dart';
|
import 'package:krow_data_connect/krow_data_connect.dart';
|
||||||
@@ -8,10 +9,16 @@ import 'src/domain/repositories/hub_repository_interface.dart';
|
|||||||
import 'src/domain/usecases/assign_nfc_tag_usecase.dart';
|
import 'src/domain/usecases/assign_nfc_tag_usecase.dart';
|
||||||
import 'src/domain/usecases/create_hub_usecase.dart';
|
import 'src/domain/usecases/create_hub_usecase.dart';
|
||||||
import 'src/domain/usecases/delete_hub_usecase.dart';
|
import 'src/domain/usecases/delete_hub_usecase.dart';
|
||||||
|
import 'src/domain/usecases/get_cost_centers_usecase.dart';
|
||||||
import 'src/domain/usecases/get_hubs_usecase.dart';
|
import 'src/domain/usecases/get_hubs_usecase.dart';
|
||||||
import 'src/domain/usecases/update_hub_usecase.dart';
|
import 'src/domain/usecases/update_hub_usecase.dart';
|
||||||
import 'src/presentation/blocs/client_hubs_bloc.dart';
|
import 'src/presentation/blocs/client_hubs_bloc.dart';
|
||||||
|
import 'src/presentation/blocs/edit_hub/edit_hub_bloc.dart';
|
||||||
|
import 'src/presentation/blocs/hub_details/hub_details_bloc.dart';
|
||||||
import 'src/presentation/pages/client_hubs_page.dart';
|
import 'src/presentation/pages/client_hubs_page.dart';
|
||||||
|
import 'src/presentation/pages/edit_hub_page.dart';
|
||||||
|
import 'src/presentation/pages/hub_details_page.dart';
|
||||||
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
|
||||||
export 'src/presentation/pages/client_hubs_page.dart';
|
export 'src/presentation/pages/client_hubs_page.dart';
|
||||||
|
|
||||||
@@ -27,6 +34,7 @@ class ClientHubsModule extends Module {
|
|||||||
|
|
||||||
// UseCases
|
// UseCases
|
||||||
i.addLazySingleton(GetHubsUseCase.new);
|
i.addLazySingleton(GetHubsUseCase.new);
|
||||||
|
i.addLazySingleton(GetCostCentersUseCase.new);
|
||||||
i.addLazySingleton(CreateHubUseCase.new);
|
i.addLazySingleton(CreateHubUseCase.new);
|
||||||
i.addLazySingleton(DeleteHubUseCase.new);
|
i.addLazySingleton(DeleteHubUseCase.new);
|
||||||
i.addLazySingleton(AssignNfcTagUseCase.new);
|
i.addLazySingleton(AssignNfcTagUseCase.new);
|
||||||
@@ -34,10 +42,47 @@ class ClientHubsModule extends Module {
|
|||||||
|
|
||||||
// BLoCs
|
// BLoCs
|
||||||
i.add<ClientHubsBloc>(ClientHubsBloc.new);
|
i.add<ClientHubsBloc>(ClientHubsBloc.new);
|
||||||
|
i.add<EditHubBloc>(EditHubBloc.new);
|
||||||
|
i.add<HubDetailsBloc>(HubDetailsBloc.new);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void routes(RouteManager r) {
|
void routes(RouteManager r) {
|
||||||
r.child(ClientPaths.childRoute(ClientPaths.hubs, ClientPaths.hubs), child: (_) => const ClientHubsPage());
|
r.child(
|
||||||
|
ClientPaths.childRoute(ClientPaths.hubs, ClientPaths.hubs),
|
||||||
|
child: (_) => const ClientHubsPage(),
|
||||||
|
);
|
||||||
|
r.child(
|
||||||
|
ClientPaths.childRoute(ClientPaths.hubs, ClientPaths.hubDetails),
|
||||||
|
child: (_) {
|
||||||
|
final Map<String, dynamic> data = r.args.data as Map<String, dynamic>;
|
||||||
|
return HubDetailsPage(
|
||||||
|
hub: data['hub'] as Hub,
|
||||||
|
bloc: Modular.get<HubDetailsBloc>(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
r.child(
|
||||||
|
ClientPaths.childRoute(ClientPaths.hubs, ClientPaths.editHub),
|
||||||
|
transition: TransitionType.custom,
|
||||||
|
customTransition: CustomTransition(
|
||||||
|
opaque: false,
|
||||||
|
transitionBuilder: (
|
||||||
|
BuildContext context,
|
||||||
|
Animation<double> animation,
|
||||||
|
Animation<double> secondaryAnimation,
|
||||||
|
Widget child,
|
||||||
|
) {
|
||||||
|
return FadeTransition(opacity: animation, child: child);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
child: (_) {
|
||||||
|
final Map<String, dynamic> data = r.args.data as Map<String, dynamic>;
|
||||||
|
return EditHubPage(
|
||||||
|
hub: data['hub'] as Hub?,
|
||||||
|
bloc: Modular.get<EditHubBloc>(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// ignore_for_file: always_specify_types, depend_on_referenced_packages, dead_code, dead_null_aware_expression, unused_local_variable, unused_import, sort_constructors_first, prefer_final_fields, prefer_const_constructors, deprecated_member_use, implicit_call_tearoffs
|
// ignore_for_file: always_specify_types, depend_on_referenced_packages, dead_code, dead_null_aware_expression, unused_local_variable, unused_import, sort_constructors_first, prefer_final_fields, prefer_const_constructors, deprecated_member_use, implicit_call_tearoffs
|
||||||
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
|
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
|
||||||
import 'package:krow_domain/krow_domain.dart';
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
import '../../domain/repositories/hub_repository_interface.dart';
|
import '../../domain/repositories/hub_repository_interface.dart';
|
||||||
@@ -24,6 +24,24 @@ class HubRepositoryImpl implements HubRepositoryInterface {
|
|||||||
return _connectorRepository.getHubs(businessId: businessId);
|
return _connectorRepository.getHubs(businessId: businessId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<CostCenter>> getCostCenters() async {
|
||||||
|
return _service.run(() async {
|
||||||
|
final result = await _service.connector.listTeamHudDepartments().execute();
|
||||||
|
final Set<String> seen = <String>{};
|
||||||
|
final List<CostCenter> costCenters = <CostCenter>[];
|
||||||
|
for (final dc.ListTeamHudDepartmentsTeamHudDepartments dep
|
||||||
|
in result.data.teamHudDepartments) {
|
||||||
|
final String? cc = dep.costCenter;
|
||||||
|
if (cc != null && cc.isNotEmpty && !seen.contains(cc)) {
|
||||||
|
seen.add(cc);
|
||||||
|
costCenters.add(CostCenter(id: cc, name: dep.name, code: cc));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return costCenters;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Hub> createHub({
|
Future<Hub> createHub({
|
||||||
required String name,
|
required String name,
|
||||||
@@ -36,6 +54,7 @@ class HubRepositoryImpl implements HubRepositoryInterface {
|
|||||||
String? street,
|
String? street,
|
||||||
String? country,
|
String? country,
|
||||||
String? zipCode,
|
String? zipCode,
|
||||||
|
String? costCenterId,
|
||||||
}) async {
|
}) async {
|
||||||
final String businessId = await _service.getBusinessId();
|
final String businessId = await _service.getBusinessId();
|
||||||
return _connectorRepository.createHub(
|
return _connectorRepository.createHub(
|
||||||
@@ -50,6 +69,7 @@ class HubRepositoryImpl implements HubRepositoryInterface {
|
|||||||
street: street,
|
street: street,
|
||||||
country: country,
|
country: country,
|
||||||
zipCode: zipCode,
|
zipCode: zipCode,
|
||||||
|
costCenterId: costCenterId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,6 +99,7 @@ class HubRepositoryImpl implements HubRepositoryInterface {
|
|||||||
String? street,
|
String? street,
|
||||||
String? country,
|
String? country,
|
||||||
String? zipCode,
|
String? zipCode,
|
||||||
|
String? costCenterId,
|
||||||
}) async {
|
}) async {
|
||||||
final String businessId = await _service.getBusinessId();
|
final String businessId = await _service.getBusinessId();
|
||||||
return _connectorRepository.updateHub(
|
return _connectorRepository.updateHub(
|
||||||
@@ -94,6 +115,7 @@ class HubRepositoryImpl implements HubRepositoryInterface {
|
|||||||
street: street,
|
street: street,
|
||||||
country: country,
|
country: country,
|
||||||
zipCode: zipCode,
|
zipCode: zipCode,
|
||||||
|
costCenterId: costCenterId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ class CreateHubArguments extends UseCaseArgument {
|
|||||||
this.street,
|
this.street,
|
||||||
this.country,
|
this.country,
|
||||||
this.zipCode,
|
this.zipCode,
|
||||||
|
this.costCenterId,
|
||||||
});
|
});
|
||||||
/// The name of the hub.
|
/// The name of the hub.
|
||||||
final String name;
|
final String name;
|
||||||
@@ -34,6 +35,9 @@ class CreateHubArguments extends UseCaseArgument {
|
|||||||
final String? street;
|
final String? street;
|
||||||
final String? country;
|
final String? country;
|
||||||
final String? zipCode;
|
final String? zipCode;
|
||||||
|
|
||||||
|
/// The cost center of the hub.
|
||||||
|
final String? costCenterId;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => <Object?>[
|
List<Object?> get props => <Object?>[
|
||||||
@@ -47,5 +51,6 @@ class CreateHubArguments extends UseCaseArgument {
|
|||||||
street,
|
street,
|
||||||
country,
|
country,
|
||||||
zipCode,
|
zipCode,
|
||||||
|
costCenterId,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ abstract interface class HubRepositoryInterface {
|
|||||||
/// Returns a list of [Hub] entities.
|
/// Returns a list of [Hub] entities.
|
||||||
Future<List<Hub>> getHubs();
|
Future<List<Hub>> getHubs();
|
||||||
|
|
||||||
|
/// Fetches the list of available cost centers for the current business.
|
||||||
|
Future<List<CostCenter>> getCostCenters();
|
||||||
|
|
||||||
/// Creates a new hub.
|
/// Creates a new hub.
|
||||||
///
|
///
|
||||||
/// Takes the [name] and [address] of the new hub.
|
/// Takes the [name] and [address] of the new hub.
|
||||||
@@ -26,6 +29,7 @@ abstract interface class HubRepositoryInterface {
|
|||||||
String? street,
|
String? street,
|
||||||
String? country,
|
String? country,
|
||||||
String? zipCode,
|
String? zipCode,
|
||||||
|
String? costCenterId,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Deletes a hub by its [id].
|
/// Deletes a hub by its [id].
|
||||||
@@ -51,5 +55,6 @@ abstract interface class HubRepositoryInterface {
|
|||||||
String? street,
|
String? street,
|
||||||
String? country,
|
String? country,
|
||||||
String? zipCode,
|
String? zipCode,
|
||||||
|
String? costCenterId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ class CreateHubUseCase implements UseCase<CreateHubArguments, Hub> {
|
|||||||
street: arguments.street,
|
street: arguments.street,
|
||||||
country: arguments.country,
|
country: arguments.country,
|
||||||
zipCode: arguments.zipCode,
|
zipCode: arguments.zipCode,
|
||||||
|
costCenterId: arguments.costCenterId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
import '../repositories/hub_repository_interface.dart';
|
||||||
|
|
||||||
|
/// Usecase to fetch all available cost centers.
|
||||||
|
class GetCostCentersUseCase {
|
||||||
|
GetCostCentersUseCase({required HubRepositoryInterface repository})
|
||||||
|
: _repository = repository;
|
||||||
|
|
||||||
|
final HubRepositoryInterface _repository;
|
||||||
|
|
||||||
|
Future<List<CostCenter>> call() async {
|
||||||
|
return _repository.getCostCenters();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@ class UpdateHubArguments extends UseCaseArgument {
|
|||||||
this.street,
|
this.street,
|
||||||
this.country,
|
this.country,
|
||||||
this.zipCode,
|
this.zipCode,
|
||||||
|
this.costCenterId,
|
||||||
});
|
});
|
||||||
|
|
||||||
final String id;
|
final String id;
|
||||||
@@ -30,6 +31,7 @@ class UpdateHubArguments extends UseCaseArgument {
|
|||||||
final String? street;
|
final String? street;
|
||||||
final String? country;
|
final String? country;
|
||||||
final String? zipCode;
|
final String? zipCode;
|
||||||
|
final String? costCenterId;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => <Object?>[
|
List<Object?> get props => <Object?>[
|
||||||
@@ -44,6 +46,7 @@ class UpdateHubArguments extends UseCaseArgument {
|
|||||||
street,
|
street,
|
||||||
country,
|
country,
|
||||||
zipCode,
|
zipCode,
|
||||||
|
costCenterId,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,6 +70,7 @@ class UpdateHubUseCase implements UseCase<UpdateHubArguments, Hub> {
|
|||||||
street: params.street,
|
street: params.street,
|
||||||
country: params.country,
|
country: params.country,
|
||||||
zipCode: params.zipCode,
|
zipCode: params.zipCode,
|
||||||
|
costCenterId: params.costCenterId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,80 +2,36 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
|||||||
import 'package:flutter_modular/flutter_modular.dart';
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
import 'package:krow_core/core.dart';
|
import 'package:krow_core/core.dart';
|
||||||
import 'package:krow_domain/krow_domain.dart';
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
import '../../domain/arguments/assign_nfc_tag_arguments.dart';
|
|
||||||
import '../../domain/arguments/create_hub_arguments.dart';
|
|
||||||
import '../../domain/arguments/delete_hub_arguments.dart';
|
|
||||||
import '../../domain/usecases/assign_nfc_tag_usecase.dart';
|
|
||||||
import '../../domain/usecases/create_hub_usecase.dart';
|
|
||||||
import '../../domain/usecases/delete_hub_usecase.dart';
|
|
||||||
import '../../domain/usecases/get_hubs_usecase.dart';
|
import '../../domain/usecases/get_hubs_usecase.dart';
|
||||||
import '../../domain/usecases/update_hub_usecase.dart';
|
|
||||||
import 'client_hubs_event.dart';
|
import 'client_hubs_event.dart';
|
||||||
import 'client_hubs_state.dart';
|
import 'client_hubs_state.dart';
|
||||||
|
|
||||||
/// BLoC responsible for managing the state of the Client Hubs feature.
|
/// BLoC responsible for managing the state of the Client Hubs feature.
|
||||||
///
|
///
|
||||||
/// It orchestrates the flow between the UI and the domain layer by invoking
|
/// It orchestrates the flow between the UI and the domain layer by invoking
|
||||||
/// specific use cases for fetching, creating, deleting, and assigning tags to hubs.
|
/// specific use cases for fetching hubs.
|
||||||
class ClientHubsBloc extends Bloc<ClientHubsEvent, ClientHubsState>
|
class ClientHubsBloc extends Bloc<ClientHubsEvent, ClientHubsState>
|
||||||
with BlocErrorHandler<ClientHubsState>
|
with BlocErrorHandler<ClientHubsState>
|
||||||
implements Disposable {
|
implements Disposable {
|
||||||
|
ClientHubsBloc({required GetHubsUseCase getHubsUseCase})
|
||||||
ClientHubsBloc({
|
: _getHubsUseCase = getHubsUseCase,
|
||||||
required GetHubsUseCase getHubsUseCase,
|
super(const ClientHubsState()) {
|
||||||
required CreateHubUseCase createHubUseCase,
|
|
||||||
required DeleteHubUseCase deleteHubUseCase,
|
|
||||||
required AssignNfcTagUseCase assignNfcTagUseCase,
|
|
||||||
required UpdateHubUseCase updateHubUseCase,
|
|
||||||
}) : _getHubsUseCase = getHubsUseCase,
|
|
||||||
_createHubUseCase = createHubUseCase,
|
|
||||||
_deleteHubUseCase = deleteHubUseCase,
|
|
||||||
_assignNfcTagUseCase = assignNfcTagUseCase,
|
|
||||||
_updateHubUseCase = updateHubUseCase,
|
|
||||||
super(const ClientHubsState()) {
|
|
||||||
on<ClientHubsFetched>(_onFetched);
|
on<ClientHubsFetched>(_onFetched);
|
||||||
on<ClientHubsAddRequested>(_onAddRequested);
|
|
||||||
on<ClientHubsUpdateRequested>(_onUpdateRequested);
|
|
||||||
on<ClientHubsDeleteRequested>(_onDeleteRequested);
|
|
||||||
on<ClientHubsNfcTagAssignRequested>(_onNfcTagAssignRequested);
|
|
||||||
on<ClientHubsMessageCleared>(_onMessageCleared);
|
on<ClientHubsMessageCleared>(_onMessageCleared);
|
||||||
on<ClientHubsAddDialogToggled>(_onAddDialogToggled);
|
|
||||||
on<ClientHubsIdentifyDialogToggled>(_onIdentifyDialogToggled);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final GetHubsUseCase _getHubsUseCase;
|
final GetHubsUseCase _getHubsUseCase;
|
||||||
final CreateHubUseCase _createHubUseCase;
|
|
||||||
final DeleteHubUseCase _deleteHubUseCase;
|
|
||||||
final AssignNfcTagUseCase _assignNfcTagUseCase;
|
|
||||||
final UpdateHubUseCase _updateHubUseCase;
|
|
||||||
|
|
||||||
void _onAddDialogToggled(
|
|
||||||
ClientHubsAddDialogToggled event,
|
|
||||||
Emitter<ClientHubsState> emit,
|
|
||||||
) {
|
|
||||||
emit(state.copyWith(showAddHubDialog: event.visible));
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onIdentifyDialogToggled(
|
|
||||||
ClientHubsIdentifyDialogToggled event,
|
|
||||||
Emitter<ClientHubsState> emit,
|
|
||||||
) {
|
|
||||||
if (event.hub == null) {
|
|
||||||
emit(state.copyWith(clearHubToIdentify: true));
|
|
||||||
} else {
|
|
||||||
emit(state.copyWith(hubToIdentify: event.hub));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _onFetched(
|
Future<void> _onFetched(
|
||||||
ClientHubsFetched event,
|
ClientHubsFetched event,
|
||||||
Emitter<ClientHubsState> emit,
|
Emitter<ClientHubsState> emit,
|
||||||
) async {
|
) async {
|
||||||
emit(state.copyWith(status: ClientHubsStatus.loading));
|
emit(state.copyWith(status: ClientHubsStatus.loading));
|
||||||
|
|
||||||
await handleError(
|
await handleError(
|
||||||
emit: emit.call,
|
emit: emit.call,
|
||||||
action: () async {
|
action: () async {
|
||||||
final List<Hub> hubs = await _getHubsUseCase();
|
final List<Hub> hubs = await _getHubsUseCase.call();
|
||||||
emit(state.copyWith(status: ClientHubsStatus.success, hubs: hubs));
|
emit(state.copyWith(status: ClientHubsStatus.success, hubs: hubs));
|
||||||
},
|
},
|
||||||
onError: (String errorKey) => state.copyWith(
|
onError: (String errorKey) => state.copyWith(
|
||||||
@@ -85,141 +41,6 @@ class ClientHubsBloc extends Bloc<ClientHubsEvent, ClientHubsState>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onAddRequested(
|
|
||||||
ClientHubsAddRequested event,
|
|
||||||
Emitter<ClientHubsState> emit,
|
|
||||||
) async {
|
|
||||||
emit(state.copyWith(status: ClientHubsStatus.actionInProgress));
|
|
||||||
|
|
||||||
await handleError(
|
|
||||||
emit: emit.call,
|
|
||||||
action: () async {
|
|
||||||
await _createHubUseCase(
|
|
||||||
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<Hub> hubs = await _getHubsUseCase();
|
|
||||||
emit(
|
|
||||||
state.copyWith(
|
|
||||||
status: ClientHubsStatus.actionSuccess,
|
|
||||||
hubs: hubs,
|
|
||||||
successMessage: 'Hub created successfully',
|
|
||||||
showAddHubDialog: false,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
onError: (String errorKey) => state.copyWith(
|
|
||||||
status: ClientHubsStatus.actionFailure,
|
|
||||||
errorMessage: errorKey,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _onUpdateRequested(
|
|
||||||
ClientHubsUpdateRequested event,
|
|
||||||
Emitter<ClientHubsState> emit,
|
|
||||||
) async {
|
|
||||||
emit(state.copyWith(status: ClientHubsStatus.actionInProgress));
|
|
||||||
|
|
||||||
await handleError(
|
|
||||||
emit: emit.call,
|
|
||||||
action: () async {
|
|
||||||
await _updateHubUseCase(
|
|
||||||
UpdateHubArguments(
|
|
||||||
id: event.id,
|
|
||||||
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<Hub> hubs = await _getHubsUseCase();
|
|
||||||
emit(
|
|
||||||
state.copyWith(
|
|
||||||
status: ClientHubsStatus.actionSuccess,
|
|
||||||
hubs: hubs,
|
|
||||||
successMessage: 'Hub updated successfully!',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
onError: (String errorKey) => state.copyWith(
|
|
||||||
status: ClientHubsStatus.actionFailure,
|
|
||||||
errorMessage: errorKey,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _onDeleteRequested(
|
|
||||||
ClientHubsDeleteRequested event,
|
|
||||||
Emitter<ClientHubsState> emit,
|
|
||||||
) async {
|
|
||||||
emit(state.copyWith(status: ClientHubsStatus.actionInProgress));
|
|
||||||
|
|
||||||
await handleError(
|
|
||||||
emit: emit.call,
|
|
||||||
action: () async {
|
|
||||||
await _deleteHubUseCase(DeleteHubArguments(hubId: event.hubId));
|
|
||||||
final List<Hub> hubs = await _getHubsUseCase();
|
|
||||||
emit(
|
|
||||||
state.copyWith(
|
|
||||||
status: ClientHubsStatus.actionSuccess,
|
|
||||||
hubs: hubs,
|
|
||||||
successMessage: 'Hub deleted successfully',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
onError: (String errorKey) => state.copyWith(
|
|
||||||
status: ClientHubsStatus.actionFailure,
|
|
||||||
errorMessage: errorKey,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _onNfcTagAssignRequested(
|
|
||||||
ClientHubsNfcTagAssignRequested event,
|
|
||||||
Emitter<ClientHubsState> emit,
|
|
||||||
) async {
|
|
||||||
emit(state.copyWith(status: ClientHubsStatus.actionInProgress));
|
|
||||||
|
|
||||||
await handleError(
|
|
||||||
emit: emit.call,
|
|
||||||
action: () async {
|
|
||||||
await _assignNfcTagUseCase(
|
|
||||||
AssignNfcTagArguments(hubId: event.hubId, nfcTagId: event.nfcTagId),
|
|
||||||
);
|
|
||||||
final List<Hub> hubs = await _getHubsUseCase();
|
|
||||||
emit(
|
|
||||||
state.copyWith(
|
|
||||||
status: ClientHubsStatus.actionSuccess,
|
|
||||||
hubs: hubs,
|
|
||||||
successMessage: 'NFC tag assigned successfully',
|
|
||||||
clearHubToIdentify: true,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
onError: (String errorKey) => state.copyWith(
|
|
||||||
status: ClientHubsStatus.actionFailure,
|
|
||||||
errorMessage: errorKey,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onMessageCleared(
|
void _onMessageCleared(
|
||||||
ClientHubsMessageCleared event,
|
ClientHubsMessageCleared event,
|
||||||
Emitter<ClientHubsState> emit,
|
Emitter<ClientHubsState> emit,
|
||||||
@@ -229,8 +50,8 @@ class ClientHubsBloc extends Bloc<ClientHubsEvent, ClientHubsState>
|
|||||||
clearErrorMessage: true,
|
clearErrorMessage: true,
|
||||||
clearSuccessMessage: true,
|
clearSuccessMessage: true,
|
||||||
status:
|
status:
|
||||||
state.status == ClientHubsStatus.actionSuccess ||
|
state.status == ClientHubsStatus.success ||
|
||||||
state.status == ClientHubsStatus.actionFailure
|
state.status == ClientHubsStatus.failure
|
||||||
? ClientHubsStatus.success
|
? ClientHubsStatus.success
|
||||||
: state.status,
|
: state.status,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:krow_domain/krow_domain.dart';
|
|
||||||
|
|
||||||
/// Base class for all client hubs events.
|
/// Base class for all client hubs events.
|
||||||
abstract class ClientHubsEvent extends Equatable {
|
abstract class ClientHubsEvent extends Equatable {
|
||||||
@@ -14,136 +13,7 @@ class ClientHubsFetched extends ClientHubsEvent {
|
|||||||
const ClientHubsFetched();
|
const ClientHubsFetched();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Event triggered to add a new hub.
|
|
||||||
class ClientHubsAddRequested extends ClientHubsEvent {
|
|
||||||
|
|
||||||
const ClientHubsAddRequested({
|
|
||||||
required this.name,
|
|
||||||
required this.address,
|
|
||||||
this.placeId,
|
|
||||||
this.latitude,
|
|
||||||
this.longitude,
|
|
||||||
this.city,
|
|
||||||
this.state,
|
|
||||||
this.street,
|
|
||||||
this.country,
|
|
||||||
this.zipCode,
|
|
||||||
});
|
|
||||||
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<Object?> get props => <Object?>[
|
|
||||||
name,
|
|
||||||
address,
|
|
||||||
placeId,
|
|
||||||
latitude,
|
|
||||||
longitude,
|
|
||||||
city,
|
|
||||||
state,
|
|
||||||
street,
|
|
||||||
country,
|
|
||||||
zipCode,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Event triggered to update an existing hub.
|
|
||||||
class ClientHubsUpdateRequested extends ClientHubsEvent {
|
|
||||||
const ClientHubsUpdateRequested({
|
|
||||||
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<Object?> get props => <Object?>[
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
address,
|
|
||||||
placeId,
|
|
||||||
latitude,
|
|
||||||
longitude,
|
|
||||||
city,
|
|
||||||
state,
|
|
||||||
street,
|
|
||||||
country,
|
|
||||||
zipCode,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Event triggered to delete a hub.
|
|
||||||
class ClientHubsDeleteRequested extends ClientHubsEvent {
|
|
||||||
|
|
||||||
const ClientHubsDeleteRequested(this.hubId);
|
|
||||||
final String hubId;
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<Object?> get props => <Object?>[hubId];
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Event triggered to assign an NFC tag to a hub.
|
|
||||||
class ClientHubsNfcTagAssignRequested extends ClientHubsEvent {
|
|
||||||
|
|
||||||
const ClientHubsNfcTagAssignRequested({
|
|
||||||
required this.hubId,
|
|
||||||
required this.nfcTagId,
|
|
||||||
});
|
|
||||||
final String hubId;
|
|
||||||
final String nfcTagId;
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<Object?> get props => <Object?>[hubId, nfcTagId];
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Event triggered to clear any error or success messages.
|
/// Event triggered to clear any error or success messages.
|
||||||
class ClientHubsMessageCleared extends ClientHubsEvent {
|
class ClientHubsMessageCleared extends ClientHubsEvent {
|
||||||
const ClientHubsMessageCleared();
|
const ClientHubsMessageCleared();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Event triggered to toggle the visibility of the "Add Hub" dialog.
|
|
||||||
class ClientHubsAddDialogToggled extends ClientHubsEvent {
|
|
||||||
|
|
||||||
const ClientHubsAddDialogToggled({required this.visible});
|
|
||||||
final bool visible;
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<Object?> get props => <Object?>[visible];
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Event triggered to toggle the visibility of the "Identify NFC" dialog.
|
|
||||||
class ClientHubsIdentifyDialogToggled extends ClientHubsEvent {
|
|
||||||
|
|
||||||
const ClientHubsIdentifyDialogToggled({this.hub});
|
|
||||||
final Hub? hub;
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<Object?> get props => <Object?>[hub];
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,47 +2,27 @@ import 'package:equatable/equatable.dart';
|
|||||||
import 'package:krow_domain/krow_domain.dart';
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
|
||||||
/// Enum representing the status of the client hubs state.
|
/// Enum representing the status of the client hubs state.
|
||||||
enum ClientHubsStatus {
|
enum ClientHubsStatus { initial, loading, success, failure }
|
||||||
initial,
|
|
||||||
loading,
|
|
||||||
success,
|
|
||||||
failure,
|
|
||||||
actionInProgress,
|
|
||||||
actionSuccess,
|
|
||||||
actionFailure,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// State class for the ClientHubs BLoC.
|
/// State class for the ClientHubs BLoC.
|
||||||
class ClientHubsState extends Equatable {
|
class ClientHubsState extends Equatable {
|
||||||
|
|
||||||
const ClientHubsState({
|
const ClientHubsState({
|
||||||
this.status = ClientHubsStatus.initial,
|
this.status = ClientHubsStatus.initial,
|
||||||
this.hubs = const <Hub>[],
|
this.hubs = const <Hub>[],
|
||||||
this.errorMessage,
|
this.errorMessage,
|
||||||
this.successMessage,
|
this.successMessage,
|
||||||
this.showAddHubDialog = false,
|
|
||||||
this.hubToIdentify,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
final ClientHubsStatus status;
|
final ClientHubsStatus status;
|
||||||
final List<Hub> hubs;
|
final List<Hub> hubs;
|
||||||
final String? errorMessage;
|
final String? errorMessage;
|
||||||
final String? successMessage;
|
final String? successMessage;
|
||||||
|
|
||||||
/// Whether the "Add Hub" dialog should be visible.
|
|
||||||
final bool showAddHubDialog;
|
|
||||||
|
|
||||||
/// The hub currently being identified/assigned an NFC tag.
|
|
||||||
/// If null, the identification dialog is closed.
|
|
||||||
final Hub? hubToIdentify;
|
|
||||||
|
|
||||||
ClientHubsState copyWith({
|
ClientHubsState copyWith({
|
||||||
ClientHubsStatus? status,
|
ClientHubsStatus? status,
|
||||||
List<Hub>? hubs,
|
List<Hub>? hubs,
|
||||||
String? errorMessage,
|
String? errorMessage,
|
||||||
String? successMessage,
|
String? successMessage,
|
||||||
bool? showAddHubDialog,
|
|
||||||
Hub? hubToIdentify,
|
|
||||||
bool clearHubToIdentify = false,
|
|
||||||
bool clearErrorMessage = false,
|
bool clearErrorMessage = false,
|
||||||
bool clearSuccessMessage = false,
|
bool clearSuccessMessage = false,
|
||||||
}) {
|
}) {
|
||||||
@@ -55,10 +35,6 @@ class ClientHubsState extends Equatable {
|
|||||||
successMessage: clearSuccessMessage
|
successMessage: clearSuccessMessage
|
||||||
? null
|
? null
|
||||||
: (successMessage ?? this.successMessage),
|
: (successMessage ?? this.successMessage),
|
||||||
showAddHubDialog: showAddHubDialog ?? this.showAddHubDialog,
|
|
||||||
hubToIdentify: clearHubToIdentify
|
|
||||||
? null
|
|
||||||
: (hubToIdentify ?? this.hubToIdentify),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,7 +44,5 @@ class ClientHubsState extends Equatable {
|
|||||||
hubs,
|
hubs,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
successMessage,
|
successMessage,
|
||||||
showAddHubDialog,
|
|
||||||
hubToIdentify,
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:krow_core/core.dart';
|
||||||
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
import '../../../domain/arguments/create_hub_arguments.dart';
|
||||||
|
import '../../../domain/usecases/create_hub_usecase.dart';
|
||||||
|
import '../../../domain/usecases/update_hub_usecase.dart';
|
||||||
|
import '../../../domain/usecases/get_cost_centers_usecase.dart';
|
||||||
|
import 'edit_hub_event.dart';
|
||||||
|
import 'edit_hub_state.dart';
|
||||||
|
|
||||||
|
/// Bloc for creating and updating hubs.
|
||||||
|
class EditHubBloc extends Bloc<EditHubEvent, EditHubState>
|
||||||
|
with BlocErrorHandler<EditHubState> {
|
||||||
|
EditHubBloc({
|
||||||
|
required CreateHubUseCase createHubUseCase,
|
||||||
|
required UpdateHubUseCase updateHubUseCase,
|
||||||
|
required GetCostCentersUseCase getCostCentersUseCase,
|
||||||
|
}) : _createHubUseCase = createHubUseCase,
|
||||||
|
_updateHubUseCase = updateHubUseCase,
|
||||||
|
_getCostCentersUseCase = getCostCentersUseCase,
|
||||||
|
super(const EditHubState()) {
|
||||||
|
on<EditHubCostCentersLoadRequested>(_onCostCentersLoadRequested);
|
||||||
|
on<EditHubAddRequested>(_onAddRequested);
|
||||||
|
on<EditHubUpdateRequested>(_onUpdateRequested);
|
||||||
|
}
|
||||||
|
|
||||||
|
final CreateHubUseCase _createHubUseCase;
|
||||||
|
final UpdateHubUseCase _updateHubUseCase;
|
||||||
|
final GetCostCentersUseCase _getCostCentersUseCase;
|
||||||
|
|
||||||
|
Future<void> _onCostCentersLoadRequested(
|
||||||
|
EditHubCostCentersLoadRequested event,
|
||||||
|
Emitter<EditHubState> emit,
|
||||||
|
) async {
|
||||||
|
await handleError(
|
||||||
|
emit: emit.call,
|
||||||
|
action: () async {
|
||||||
|
final List<CostCenter> costCenters = await _getCostCentersUseCase.call();
|
||||||
|
emit(state.copyWith(costCenters: costCenters));
|
||||||
|
},
|
||||||
|
onError: (String errorKey) => state.copyWith(
|
||||||
|
status: EditHubStatus.failure,
|
||||||
|
errorMessage: errorKey,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onAddRequested(
|
||||||
|
EditHubAddRequested event,
|
||||||
|
Emitter<EditHubState> emit,
|
||||||
|
) async {
|
||||||
|
emit(state.copyWith(status: EditHubStatus.loading));
|
||||||
|
|
||||||
|
await handleError(
|
||||||
|
emit: emit.call,
|
||||||
|
action: () async {
|
||||||
|
await _createHubUseCase.call(
|
||||||
|
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,
|
||||||
|
costCenterId: event.costCenterId,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
status: EditHubStatus.success,
|
||||||
|
successKey: 'created',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onError: (String errorKey) =>
|
||||||
|
state.copyWith(status: EditHubStatus.failure, errorMessage: errorKey),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onUpdateRequested(
|
||||||
|
EditHubUpdateRequested event,
|
||||||
|
Emitter<EditHubState> emit,
|
||||||
|
) async {
|
||||||
|
emit(state.copyWith(status: EditHubStatus.loading));
|
||||||
|
|
||||||
|
await handleError(
|
||||||
|
emit: emit.call,
|
||||||
|
action: () async {
|
||||||
|
await _updateHubUseCase.call(
|
||||||
|
UpdateHubArguments(
|
||||||
|
id: event.id,
|
||||||
|
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,
|
||||||
|
costCenterId: event.costCenterId,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
status: EditHubStatus.success,
|
||||||
|
successKey: 'updated',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onError: (String errorKey) =>
|
||||||
|
state.copyWith(status: EditHubStatus.failure, errorMessage: errorKey),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
|
||||||
|
/// Base class for all edit hub events.
|
||||||
|
abstract class EditHubEvent extends Equatable {
|
||||||
|
const EditHubEvent();
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => <Object?>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Event triggered to load all available cost centers.
|
||||||
|
class EditHubCostCentersLoadRequested extends EditHubEvent {
|
||||||
|
const EditHubCostCentersLoadRequested();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Event triggered to add a new hub.
|
||||||
|
class EditHubAddRequested extends EditHubEvent {
|
||||||
|
const EditHubAddRequested({
|
||||||
|
required this.name,
|
||||||
|
required this.address,
|
||||||
|
this.placeId,
|
||||||
|
this.latitude,
|
||||||
|
this.longitude,
|
||||||
|
this.city,
|
||||||
|
this.state,
|
||||||
|
this.street,
|
||||||
|
this.country,
|
||||||
|
this.zipCode,
|
||||||
|
this.costCenterId,
|
||||||
|
});
|
||||||
|
|
||||||
|
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;
|
||||||
|
final String? costCenterId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => <Object?>[
|
||||||
|
name,
|
||||||
|
address,
|
||||||
|
placeId,
|
||||||
|
latitude,
|
||||||
|
longitude,
|
||||||
|
city,
|
||||||
|
state,
|
||||||
|
street,
|
||||||
|
country,
|
||||||
|
zipCode,
|
||||||
|
costCenterId,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Event triggered to update an existing hub.
|
||||||
|
class EditHubUpdateRequested extends EditHubEvent {
|
||||||
|
const EditHubUpdateRequested({
|
||||||
|
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,
|
||||||
|
this.costCenterId,
|
||||||
|
});
|
||||||
|
|
||||||
|
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;
|
||||||
|
final String? costCenterId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => <Object?>[
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
address,
|
||||||
|
placeId,
|
||||||
|
latitude,
|
||||||
|
longitude,
|
||||||
|
city,
|
||||||
|
state,
|
||||||
|
street,
|
||||||
|
country,
|
||||||
|
zipCode,
|
||||||
|
costCenterId,
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
|
||||||
|
/// Status of the edit hub operation.
|
||||||
|
enum EditHubStatus {
|
||||||
|
/// Initial state.
|
||||||
|
initial,
|
||||||
|
|
||||||
|
/// Operation in progress.
|
||||||
|
loading,
|
||||||
|
|
||||||
|
/// Operation succeeded.
|
||||||
|
success,
|
||||||
|
|
||||||
|
/// Operation failed.
|
||||||
|
failure,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// State for the edit hub operation.
|
||||||
|
class EditHubState extends Equatable {
|
||||||
|
const EditHubState({
|
||||||
|
this.status = EditHubStatus.initial,
|
||||||
|
this.errorMessage,
|
||||||
|
this.successMessage,
|
||||||
|
this.successKey,
|
||||||
|
this.costCenters = const <CostCenter>[],
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The status of the operation.
|
||||||
|
final EditHubStatus status;
|
||||||
|
|
||||||
|
/// The error message if the operation failed.
|
||||||
|
final String? errorMessage;
|
||||||
|
|
||||||
|
/// The success message if the operation succeeded.
|
||||||
|
final String? successMessage;
|
||||||
|
|
||||||
|
/// Localization key for success message: 'created' | 'updated'.
|
||||||
|
final String? successKey;
|
||||||
|
|
||||||
|
/// Available cost centers for selection.
|
||||||
|
final List<CostCenter> costCenters;
|
||||||
|
|
||||||
|
/// Create a copy of this state with the given fields replaced.
|
||||||
|
EditHubState copyWith({
|
||||||
|
EditHubStatus? status,
|
||||||
|
String? errorMessage,
|
||||||
|
String? successMessage,
|
||||||
|
String? successKey,
|
||||||
|
List<CostCenter>? costCenters,
|
||||||
|
}) {
|
||||||
|
return EditHubState(
|
||||||
|
status: status ?? this.status,
|
||||||
|
errorMessage: errorMessage ?? this.errorMessage,
|
||||||
|
successMessage: successMessage ?? this.successMessage,
|
||||||
|
successKey: successKey ?? this.successKey,
|
||||||
|
costCenters: costCenters ?? this.costCenters,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => <Object?>[
|
||||||
|
status,
|
||||||
|
errorMessage,
|
||||||
|
successMessage,
|
||||||
|
successKey,
|
||||||
|
costCenters,
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:krow_core/core.dart';
|
||||||
|
import '../../../domain/arguments/assign_nfc_tag_arguments.dart';
|
||||||
|
import '../../../domain/arguments/delete_hub_arguments.dart';
|
||||||
|
import '../../../domain/usecases/assign_nfc_tag_usecase.dart';
|
||||||
|
import '../../../domain/usecases/delete_hub_usecase.dart';
|
||||||
|
import 'hub_details_event.dart';
|
||||||
|
import 'hub_details_state.dart';
|
||||||
|
|
||||||
|
/// Bloc for managing hub details and operations like delete and NFC assignment.
|
||||||
|
class HubDetailsBloc extends Bloc<HubDetailsEvent, HubDetailsState>
|
||||||
|
with BlocErrorHandler<HubDetailsState> {
|
||||||
|
HubDetailsBloc({
|
||||||
|
required DeleteHubUseCase deleteHubUseCase,
|
||||||
|
required AssignNfcTagUseCase assignNfcTagUseCase,
|
||||||
|
}) : _deleteHubUseCase = deleteHubUseCase,
|
||||||
|
_assignNfcTagUseCase = assignNfcTagUseCase,
|
||||||
|
super(const HubDetailsState()) {
|
||||||
|
on<HubDetailsDeleteRequested>(_onDeleteRequested);
|
||||||
|
on<HubDetailsNfcTagAssignRequested>(_onNfcTagAssignRequested);
|
||||||
|
}
|
||||||
|
|
||||||
|
final DeleteHubUseCase _deleteHubUseCase;
|
||||||
|
final AssignNfcTagUseCase _assignNfcTagUseCase;
|
||||||
|
|
||||||
|
Future<void> _onDeleteRequested(
|
||||||
|
HubDetailsDeleteRequested event,
|
||||||
|
Emitter<HubDetailsState> emit,
|
||||||
|
) async {
|
||||||
|
emit(state.copyWith(status: HubDetailsStatus.loading));
|
||||||
|
|
||||||
|
await handleError(
|
||||||
|
emit: emit.call,
|
||||||
|
action: () async {
|
||||||
|
await _deleteHubUseCase.call(DeleteHubArguments(hubId: event.id));
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
status: HubDetailsStatus.deleted,
|
||||||
|
successKey: 'deleted',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onError: (String errorKey) => state.copyWith(
|
||||||
|
status: HubDetailsStatus.failure,
|
||||||
|
errorMessage: errorKey,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onNfcTagAssignRequested(
|
||||||
|
HubDetailsNfcTagAssignRequested event,
|
||||||
|
Emitter<HubDetailsState> emit,
|
||||||
|
) async {
|
||||||
|
emit(state.copyWith(status: HubDetailsStatus.loading));
|
||||||
|
|
||||||
|
await handleError(
|
||||||
|
emit: emit.call,
|
||||||
|
action: () async {
|
||||||
|
await _assignNfcTagUseCase.call(
|
||||||
|
AssignNfcTagArguments(hubId: event.hubId, nfcTagId: event.nfcTagId),
|
||||||
|
);
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
status: HubDetailsStatus.success,
|
||||||
|
successMessage: 'NFC tag assigned successfully',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onError: (String errorKey) => state.copyWith(
|
||||||
|
status: HubDetailsStatus.failure,
|
||||||
|
errorMessage: errorKey,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
|
||||||
|
/// Base class for all hub details events.
|
||||||
|
abstract class HubDetailsEvent extends Equatable {
|
||||||
|
const HubDetailsEvent();
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => <Object?>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Event triggered to delete a hub.
|
||||||
|
class HubDetailsDeleteRequested extends HubDetailsEvent {
|
||||||
|
const HubDetailsDeleteRequested(this.id);
|
||||||
|
final String id;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => <Object?>[id];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Event triggered to assign an NFC tag to a hub.
|
||||||
|
class HubDetailsNfcTagAssignRequested extends HubDetailsEvent {
|
||||||
|
const HubDetailsNfcTagAssignRequested({
|
||||||
|
required this.hubId,
|
||||||
|
required this.nfcTagId,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String hubId;
|
||||||
|
final String nfcTagId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => <Object?>[hubId, nfcTagId];
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
|
||||||
|
/// Status of the hub details operation.
|
||||||
|
enum HubDetailsStatus {
|
||||||
|
/// Initial state.
|
||||||
|
initial,
|
||||||
|
|
||||||
|
/// Operation in progress.
|
||||||
|
loading,
|
||||||
|
|
||||||
|
/// Operation succeeded.
|
||||||
|
success,
|
||||||
|
|
||||||
|
/// Operation failed.
|
||||||
|
failure,
|
||||||
|
|
||||||
|
/// Hub was deleted.
|
||||||
|
deleted,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// State for the hub details operation.
|
||||||
|
class HubDetailsState extends Equatable {
|
||||||
|
const HubDetailsState({
|
||||||
|
this.status = HubDetailsStatus.initial,
|
||||||
|
this.errorMessage,
|
||||||
|
this.successMessage,
|
||||||
|
this.successKey,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The status of the operation.
|
||||||
|
final HubDetailsStatus status;
|
||||||
|
|
||||||
|
/// The error message if the operation failed.
|
||||||
|
final String? errorMessage;
|
||||||
|
|
||||||
|
/// The success message if the operation succeeded.
|
||||||
|
final String? successMessage;
|
||||||
|
|
||||||
|
/// Localization key for success message: 'deleted'.
|
||||||
|
final String? successKey;
|
||||||
|
|
||||||
|
/// Create a copy of this state with the given fields replaced.
|
||||||
|
HubDetailsState copyWith({
|
||||||
|
HubDetailsStatus? status,
|
||||||
|
String? errorMessage,
|
||||||
|
String? successMessage,
|
||||||
|
String? successKey,
|
||||||
|
}) {
|
||||||
|
return HubDetailsState(
|
||||||
|
status: status ?? this.status,
|
||||||
|
errorMessage: errorMessage ?? this.errorMessage,
|
||||||
|
successMessage: successMessage ?? this.successMessage,
|
||||||
|
successKey: successKey ?? this.successKey,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => <Object?>[status, errorMessage, successMessage, successKey];
|
||||||
|
}
|
||||||
@@ -8,11 +8,10 @@ import 'package:krow_domain/krow_domain.dart';
|
|||||||
import '../blocs/client_hubs_bloc.dart';
|
import '../blocs/client_hubs_bloc.dart';
|
||||||
import '../blocs/client_hubs_event.dart';
|
import '../blocs/client_hubs_event.dart';
|
||||||
import '../blocs/client_hubs_state.dart';
|
import '../blocs/client_hubs_state.dart';
|
||||||
import '../widgets/add_hub_dialog.dart';
|
|
||||||
import '../widgets/hub_card.dart';
|
import '../widgets/hub_card.dart';
|
||||||
import '../widgets/hub_empty_state.dart';
|
import '../widgets/hub_empty_state.dart';
|
||||||
import '../widgets/hub_info_card.dart';
|
import '../widgets/hub_info_card.dart';
|
||||||
import '../widgets/identify_nfc_dialog.dart';
|
|
||||||
|
|
||||||
/// The main page for the client hubs feature.
|
/// The main page for the client hubs feature.
|
||||||
///
|
///
|
||||||
@@ -43,7 +42,8 @@ class ClientHubsPage extends StatelessWidget {
|
|||||||
context,
|
context,
|
||||||
).add(const ClientHubsMessageCleared());
|
).add(const ClientHubsMessageCleared());
|
||||||
}
|
}
|
||||||
if (state.successMessage != null && state.successMessage!.isNotEmpty) {
|
if (state.successMessage != null &&
|
||||||
|
state.successMessage!.isNotEmpty) {
|
||||||
UiSnackbar.show(
|
UiSnackbar.show(
|
||||||
context,
|
context,
|
||||||
message: state.successMessage!,
|
message: state.successMessage!,
|
||||||
@@ -57,105 +57,54 @@ class ClientHubsPage extends StatelessWidget {
|
|||||||
builder: (BuildContext context, ClientHubsState state) {
|
builder: (BuildContext context, ClientHubsState state) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: UiColors.bgMenu,
|
backgroundColor: UiColors.bgMenu,
|
||||||
floatingActionButton: FloatingActionButton(
|
body: CustomScrollView(
|
||||||
onPressed: () => BlocProvider.of<ClientHubsBloc>(
|
slivers: <Widget>[
|
||||||
context,
|
_buildAppBar(context),
|
||||||
).add(const ClientHubsAddDialogToggled(visible: true)),
|
SliverPadding(
|
||||||
shape: const RoundedRectangleBorder(
|
padding: const EdgeInsets.symmetric(
|
||||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
horizontal: UiConstants.space5,
|
||||||
),
|
vertical: UiConstants.space5,
|
||||||
child: const Icon(UiIcons.add),
|
).copyWith(bottom: 100),
|
||||||
),
|
sliver: SliverList(
|
||||||
body: Stack(
|
delegate: SliverChildListDelegate(<Widget>[
|
||||||
children: <Widget>[
|
const Padding(
|
||||||
CustomScrollView(
|
padding: EdgeInsets.only(bottom: UiConstants.space5),
|
||||||
slivers: <Widget>[
|
child: HubInfoCard(),
|
||||||
_buildAppBar(context),
|
|
||||||
SliverPadding(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: UiConstants.space5,
|
|
||||||
vertical: UiConstants.space5,
|
|
||||||
).copyWith(bottom: 100),
|
|
||||||
sliver: SliverList(
|
|
||||||
delegate: SliverChildListDelegate(<Widget>[
|
|
||||||
if (state.status == ClientHubsStatus.loading)
|
|
||||||
const Center(child: CircularProgressIndicator())
|
|
||||||
else if (state.hubs.isEmpty)
|
|
||||||
HubEmptyState(
|
|
||||||
onAddPressed: () =>
|
|
||||||
BlocProvider.of<ClientHubsBloc>(context).add(
|
|
||||||
const ClientHubsAddDialogToggled(
|
|
||||||
visible: true,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
else ...<Widget>[
|
|
||||||
...state.hubs.map(
|
|
||||||
(Hub hub) => HubCard(
|
|
||||||
hub: hub,
|
|
||||||
onNfcPressed: () =>
|
|
||||||
BlocProvider.of<ClientHubsBloc>(
|
|
||||||
context,
|
|
||||||
).add(
|
|
||||||
ClientHubsIdentifyDialogToggled(hub: hub),
|
|
||||||
),
|
|
||||||
onDeletePressed: () => _confirmDeleteHub(
|
|
||||||
context,
|
|
||||||
hub,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
const SizedBox(height: UiConstants.space5),
|
|
||||||
const HubInfoCard(),
|
|
||||||
]),
|
|
||||||
),
|
),
|
||||||
),
|
|
||||||
],
|
if (state.status == ClientHubsStatus.loading)
|
||||||
|
const Center(child: CircularProgressIndicator())
|
||||||
|
else if (state.hubs.isEmpty)
|
||||||
|
HubEmptyState(
|
||||||
|
onAddPressed: () async {
|
||||||
|
final bool? success = await Modular.to.toEditHub();
|
||||||
|
if (success == true && context.mounted) {
|
||||||
|
BlocProvider.of<ClientHubsBloc>(
|
||||||
|
context,
|
||||||
|
).add(const ClientHubsFetched());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
else ...<Widget>[
|
||||||
|
...state.hubs.map(
|
||||||
|
(Hub hub) => HubCard(
|
||||||
|
hub: hub,
|
||||||
|
onTap: () async {
|
||||||
|
final bool? success = await Modular.to
|
||||||
|
.toHubDetails(hub);
|
||||||
|
if (success == true && context.mounted) {
|
||||||
|
BlocProvider.of<ClientHubsBloc>(
|
||||||
|
context,
|
||||||
|
).add(const ClientHubsFetched());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
const SizedBox(height: UiConstants.space5),
|
||||||
|
]),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
if (state.showAddHubDialog)
|
|
||||||
AddHubDialog(
|
|
||||||
onCreate: (
|
|
||||||
String name,
|
|
||||||
String address, {
|
|
||||||
String? placeId,
|
|
||||||
double? latitude,
|
|
||||||
double? longitude,
|
|
||||||
}) {
|
|
||||||
BlocProvider.of<ClientHubsBloc>(context).add(
|
|
||||||
ClientHubsAddRequested(
|
|
||||||
name: name,
|
|
||||||
address: address,
|
|
||||||
placeId: placeId,
|
|
||||||
latitude: latitude,
|
|
||||||
longitude: longitude,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
onCancel: () => BlocProvider.of<ClientHubsBloc>(
|
|
||||||
context,
|
|
||||||
).add(const ClientHubsAddDialogToggled(visible: false)),
|
|
||||||
),
|
|
||||||
if (state.hubToIdentify != null)
|
|
||||||
IdentifyNfcDialog(
|
|
||||||
hub: state.hubToIdentify!,
|
|
||||||
onAssign: (String tagId) {
|
|
||||||
BlocProvider.of<ClientHubsBloc>(context).add(
|
|
||||||
ClientHubsNfcTagAssignRequested(
|
|
||||||
hubId: state.hubToIdentify!.id,
|
|
||||||
nfcTagId: tagId,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
onCancel: () => BlocProvider.of<ClientHubsBloc>(
|
|
||||||
context,
|
|
||||||
).add(const ClientHubsIdentifyDialogToggled()),
|
|
||||||
),
|
|
||||||
if (state.status == ClientHubsStatus.actionInProgress)
|
|
||||||
Container(
|
|
||||||
color: UiColors.black.withValues(alpha: 0.1),
|
|
||||||
child: const Center(child: CircularProgressIndicator()),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -166,7 +115,7 @@ class ClientHubsPage extends StatelessWidget {
|
|||||||
|
|
||||||
Widget _buildAppBar(BuildContext context) {
|
Widget _buildAppBar(BuildContext context) {
|
||||||
return SliverAppBar(
|
return SliverAppBar(
|
||||||
backgroundColor: UiColors.foreground, // Dark Slate equivalent
|
backgroundColor: UiColors.foreground,
|
||||||
automaticallyImplyLeading: false,
|
automaticallyImplyLeading: false,
|
||||||
expandedHeight: 140,
|
expandedHeight: 140,
|
||||||
pinned: true,
|
pinned: true,
|
||||||
@@ -202,20 +151,35 @@ class ClientHubsPage extends StatelessWidget {
|
|||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Column(
|
Expanded(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
child: Column(
|
||||||
children: <Widget>[
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
Text(
|
children: <Widget>[
|
||||||
t.client_hubs.title,
|
Text(
|
||||||
style: UiTypography.headline1m.white,
|
t.client_hubs.title,
|
||||||
),
|
style: UiTypography.headline1m.white,
|
||||||
Text(
|
|
||||||
t.client_hubs.subtitle,
|
|
||||||
style: UiTypography.body2r.copyWith(
|
|
||||||
color: UiColors.switchInactive,
|
|
||||||
),
|
),
|
||||||
),
|
Text(
|
||||||
],
|
t.client_hubs.subtitle,
|
||||||
|
style: UiTypography.body2r.copyWith(
|
||||||
|
color: UiColors.switchInactive,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
UiButton.primary(
|
||||||
|
onPressed: () async {
|
||||||
|
final bool? success = await Modular.to.toEditHub();
|
||||||
|
if (success == true && context.mounted) {
|
||||||
|
BlocProvider.of<ClientHubsBloc>(
|
||||||
|
context,
|
||||||
|
).add(const ClientHubsFetched());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
text: t.client_hubs.add_hub,
|
||||||
|
leadingIcon: UiIcons.add,
|
||||||
|
size: UiButtonSize.small,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -225,51 +189,4 @@ class ClientHubsPage extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _confirmDeleteHub(BuildContext context, Hub hub) async {
|
|
||||||
final String hubName = hub.name.isEmpty ? t.client_hubs.title : hub.name;
|
|
||||||
return showDialog<void>(
|
|
||||||
context: context,
|
|
||||||
barrierDismissible: false,
|
|
||||||
builder: (BuildContext dialogContext) {
|
|
||||||
return AlertDialog(
|
|
||||||
title: Text(t.client_hubs.delete_dialog.title),
|
|
||||||
content: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: <Widget>[
|
|
||||||
Text(t.client_hubs.delete_dialog.message(hubName: hubName)),
|
|
||||||
const SizedBox(height: UiConstants.space2),
|
|
||||||
Text(t.client_hubs.delete_dialog.undo_warning),
|
|
||||||
const SizedBox(height: UiConstants.space2),
|
|
||||||
Text(
|
|
||||||
t.client_hubs.delete_dialog.dependency_warning,
|
|
||||||
style: UiTypography.footnote1r.copyWith(
|
|
||||||
color: UiColors.textSecondary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
actions: <Widget>[
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Modular.to.pop(),
|
|
||||||
child: Text(t.client_hubs.delete_dialog.cancel),
|
|
||||||
),
|
|
||||||
TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
BlocProvider.of<ClientHubsBloc>(
|
|
||||||
context,
|
|
||||||
).add(ClientHubsDeleteRequested(hub.id));
|
|
||||||
Modular.to.pop();
|
|
||||||
},
|
|
||||||
style: TextButton.styleFrom(
|
|
||||||
foregroundColor: UiColors.destructive,
|
|
||||||
),
|
|
||||||
child: Text(t.client_hubs.delete_dialog.delete),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,99 +2,54 @@ import 'package:core_localization/core_localization.dart';
|
|||||||
import 'package:design_system/design_system.dart';
|
import 'package:design_system/design_system.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:google_places_flutter/model/prediction.dart';
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
import 'package:krow_domain/krow_domain.dart';
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
|
||||||
import '../blocs/client_hubs_bloc.dart';
|
import '../blocs/edit_hub/edit_hub_bloc.dart';
|
||||||
import '../blocs/client_hubs_event.dart';
|
import '../blocs/edit_hub/edit_hub_event.dart';
|
||||||
import '../blocs/client_hubs_state.dart';
|
import '../blocs/edit_hub/edit_hub_state.dart';
|
||||||
import '../widgets/hub_address_autocomplete.dart';
|
import '../widgets/hub_form_dialog.dart';
|
||||||
|
|
||||||
/// A dedicated full-screen page for editing an existing hub.
|
/// A wrapper page that shows the hub form in a modal-style layout.
|
||||||
///
|
|
||||||
/// Takes the parent [ClientHubsBloc] via [BlocProvider.value] so the
|
|
||||||
/// updated hub list is reflected on the hubs list page when the user
|
|
||||||
/// saves and navigates back.
|
|
||||||
class EditHubPage extends StatefulWidget {
|
class EditHubPage extends StatefulWidget {
|
||||||
const EditHubPage({
|
const EditHubPage({this.hub, required this.bloc, super.key});
|
||||||
required this.hub,
|
|
||||||
required this.bloc,
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
final Hub hub;
|
final Hub? hub;
|
||||||
final ClientHubsBloc bloc;
|
final EditHubBloc bloc;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<EditHubPage> createState() => _EditHubPageState();
|
State<EditHubPage> createState() => _EditHubPageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _EditHubPageState extends State<EditHubPage> {
|
class _EditHubPageState extends State<EditHubPage> {
|
||||||
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
|
|
||||||
late final TextEditingController _nameController;
|
|
||||||
late final TextEditingController _addressController;
|
|
||||||
late final FocusNode _addressFocusNode;
|
|
||||||
Prediction? _selectedPrediction;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_nameController = TextEditingController(text: widget.hub.name);
|
// Load available cost centers
|
||||||
_addressController = TextEditingController(text: widget.hub.address);
|
widget.bloc.add(const EditHubCostCentersLoadRequested());
|
||||||
_addressFocusNode = FocusNode();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_nameController.dispose();
|
|
||||||
_addressController.dispose();
|
|
||||||
_addressFocusNode.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onSave() {
|
|
||||||
if (!_formKey.currentState!.validate()) return;
|
|
||||||
|
|
||||||
if (_addressController.text.trim().isEmpty) {
|
|
||||||
UiSnackbar.show(
|
|
||||||
context,
|
|
||||||
message: t.client_hubs.add_hub_dialog.address_hint,
|
|
||||||
type: UiSnackbarType.error,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ReadContext(context).read<ClientHubsBloc>().add(
|
|
||||||
ClientHubsUpdateRequested(
|
|
||||||
id: widget.hub.id,
|
|
||||||
name: _nameController.text.trim(),
|
|
||||||
address: _addressController.text.trim(),
|
|
||||||
placeId: _selectedPrediction?.placeId,
|
|
||||||
latitude: double.tryParse(_selectedPrediction?.lat ?? ''),
|
|
||||||
longitude: double.tryParse(_selectedPrediction?.lng ?? ''),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocProvider<ClientHubsBloc>.value(
|
return BlocProvider<EditHubBloc>.value(
|
||||||
value: widget.bloc,
|
value: widget.bloc,
|
||||||
child: BlocListener<ClientHubsBloc, ClientHubsState>(
|
child: BlocListener<EditHubBloc, EditHubState>(
|
||||||
listenWhen: (ClientHubsState prev, ClientHubsState curr) =>
|
listenWhen: (EditHubState prev, EditHubState curr) =>
|
||||||
prev.status != curr.status || prev.successMessage != curr.successMessage,
|
prev.status != curr.status || prev.successKey != curr.successKey,
|
||||||
listener: (BuildContext context, ClientHubsState state) {
|
listener: (BuildContext context, EditHubState state) {
|
||||||
if (state.status == ClientHubsStatus.actionSuccess &&
|
if (state.status == EditHubStatus.success &&
|
||||||
state.successMessage != null) {
|
state.successKey != null) {
|
||||||
|
final String message = state.successKey == 'created'
|
||||||
|
? t.client_hubs.edit_hub.created_success
|
||||||
|
: t.client_hubs.edit_hub.updated_success;
|
||||||
UiSnackbar.show(
|
UiSnackbar.show(
|
||||||
context,
|
context,
|
||||||
message: state.successMessage!,
|
message: message,
|
||||||
type: UiSnackbarType.success,
|
type: UiSnackbarType.success,
|
||||||
);
|
);
|
||||||
// Pop back to details page with updated hub
|
Modular.to.pop(true);
|
||||||
Navigator.of(context).pop(true);
|
|
||||||
}
|
}
|
||||||
if (state.status == ClientHubsStatus.actionFailure &&
|
if (state.status == EditHubStatus.failure &&
|
||||||
state.errorMessage != null) {
|
state.errorMessage != null) {
|
||||||
UiSnackbar.show(
|
UiSnackbar.show(
|
||||||
context,
|
context,
|
||||||
@@ -103,89 +58,64 @@ class _EditHubPageState extends State<EditHubPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: BlocBuilder<ClientHubsBloc, ClientHubsState>(
|
child: BlocBuilder<EditHubBloc, EditHubState>(
|
||||||
builder: (BuildContext context, ClientHubsState state) {
|
builder: (BuildContext context, EditHubState state) {
|
||||||
final bool isSaving =
|
final bool isSaving = state.status == EditHubStatus.loading;
|
||||||
state.status == ClientHubsStatus.actionInProgress;
|
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: UiColors.bgMenu,
|
backgroundColor: UiColors.bgOverlay,
|
||||||
appBar: AppBar(
|
|
||||||
backgroundColor: UiColors.foreground,
|
|
||||||
leading: IconButton(
|
|
||||||
icon: const Icon(UiIcons.arrowLeft, color: UiColors.white),
|
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
|
||||||
),
|
|
||||||
title: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: <Widget>[
|
|
||||||
Text(
|
|
||||||
t.client_hubs.edit_hub.title,
|
|
||||||
style: UiTypography.headline3m.white,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
t.client_hubs.edit_hub.subtitle,
|
|
||||||
style: UiTypography.footnote1r.copyWith(
|
|
||||||
color: UiColors.white.withValues(alpha: 0.7),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
body: Stack(
|
body: Stack(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
SingleChildScrollView(
|
// Tap background to dismiss
|
||||||
padding: const EdgeInsets.all(UiConstants.space5),
|
GestureDetector(
|
||||||
child: Form(
|
onTap: () => Modular.to.pop(),
|
||||||
key: _formKey,
|
child: Container(color: Colors.transparent),
|
||||||
child: Column(
|
),
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: <Widget>[
|
// Dialog-style content centered
|
||||||
// ── Name field ──────────────────────────────────
|
Align(
|
||||||
_FieldLabel(t.client_hubs.edit_hub.name_label),
|
alignment: Alignment.center,
|
||||||
TextFormField(
|
child: HubFormDialog(
|
||||||
controller: _nameController,
|
hub: widget.hub,
|
||||||
style: UiTypography.body1r.textPrimary,
|
costCenters: state.costCenters,
|
||||||
textInputAction: TextInputAction.next,
|
onCancel: () => Modular.to.pop(),
|
||||||
validator: (String? value) {
|
onSave: ({
|
||||||
if (value == null || value.trim().isEmpty) {
|
required String name,
|
||||||
return 'Name is required';
|
required String address,
|
||||||
}
|
String? costCenterId,
|
||||||
return null;
|
String? placeId,
|
||||||
},
|
double? latitude,
|
||||||
decoration: _inputDecoration(
|
double? longitude,
|
||||||
t.client_hubs.edit_hub.name_hint,
|
}) {
|
||||||
|
if (widget.hub == null) {
|
||||||
|
widget.bloc.add(
|
||||||
|
EditHubAddRequested(
|
||||||
|
name: name,
|
||||||
|
address: address,
|
||||||
|
costCenterId: costCenterId,
|
||||||
|
placeId: placeId,
|
||||||
|
latitude: latitude,
|
||||||
|
longitude: longitude,
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
|
} else {
|
||||||
const SizedBox(height: UiConstants.space4),
|
widget.bloc.add(
|
||||||
|
EditHubUpdateRequested(
|
||||||
// ── Address field ────────────────────────────────
|
id: widget.hub!.id,
|
||||||
_FieldLabel(t.client_hubs.edit_hub.address_label),
|
name: name,
|
||||||
HubAddressAutocomplete(
|
address: address,
|
||||||
controller: _addressController,
|
costCenterId: costCenterId,
|
||||||
hintText: t.client_hubs.edit_hub.address_hint,
|
placeId: placeId,
|
||||||
focusNode: _addressFocusNode,
|
latitude: latitude,
|
||||||
onSelected: (Prediction prediction) {
|
longitude: longitude,
|
||||||
_selectedPrediction = prediction;
|
),
|
||||||
},
|
);
|
||||||
),
|
}
|
||||||
|
},
|
||||||
const SizedBox(height: UiConstants.space8),
|
|
||||||
|
|
||||||
// ── Save button ──────────────────────────────────
|
|
||||||
UiButton.primary(
|
|
||||||
onPressed: isSaving ? null : _onSave,
|
|
||||||
text: t.client_hubs.edit_hub.save_button,
|
|
||||||
),
|
|
||||||
|
|
||||||
const SizedBox(height: 40),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// ── Loading overlay ──────────────────────────────────────
|
// Global loading overlay if saving
|
||||||
if (isSaving)
|
if (isSaving)
|
||||||
Container(
|
Container(
|
||||||
color: UiColors.black.withValues(alpha: 0.1),
|
color: UiColors.black.withValues(alpha: 0.1),
|
||||||
@@ -199,42 +129,4 @@ class _EditHubPageState extends State<EditHubPage> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
InputDecoration _inputDecoration(String hint) {
|
|
||||||
return InputDecoration(
|
|
||||||
hintText: hint,
|
|
||||||
hintStyle: UiTypography.body2r.textPlaceholder,
|
|
||||||
filled: true,
|
|
||||||
fillColor: UiColors.input,
|
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: UiConstants.space4,
|
|
||||||
vertical: 14,
|
|
||||||
),
|
|
||||||
border: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
|
||||||
borderSide: const BorderSide(color: UiColors.border),
|
|
||||||
),
|
|
||||||
enabledBorder: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
|
||||||
borderSide: const BorderSide(color: UiColors.border),
|
|
||||||
),
|
|
||||||
focusedBorder: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
|
||||||
borderSide: const BorderSide(color: UiColors.ring, width: 2),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _FieldLabel extends StatelessWidget {
|
|
||||||
const _FieldLabel(this.text);
|
|
||||||
final String text;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.only(bottom: UiConstants.space2),
|
|
||||||
child: Text(text, style: UiTypography.body2m.textPrimary),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,137 +1,146 @@
|
|||||||
import 'package:core_localization/core_localization.dart';
|
import 'package:core_localization/core_localization.dart';
|
||||||
import 'package:design_system/design_system.dart';
|
import 'package:design_system/design_system.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
|
import 'package:krow_core/core.dart';
|
||||||
import 'package:krow_domain/krow_domain.dart';
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
|
||||||
import '../blocs/client_hubs_bloc.dart';
|
import '../blocs/hub_details/hub_details_bloc.dart';
|
||||||
import 'edit_hub_page.dart';
|
import '../blocs/hub_details/hub_details_event.dart';
|
||||||
|
import '../blocs/hub_details/hub_details_state.dart';
|
||||||
|
import '../widgets/hub_details/hub_details_bottom_actions.dart';
|
||||||
|
import '../widgets/hub_details/hub_details_header.dart';
|
||||||
|
import '../widgets/hub_details/hub_details_item.dart';
|
||||||
|
|
||||||
/// A read-only details page for a single [Hub].
|
/// A read-only details page for a single [Hub].
|
||||||
///
|
///
|
||||||
/// Shows hub name, address, and NFC tag assignment.
|
/// Shows hub name, address, and NFC tag assignment.
|
||||||
/// Tapping the edit button navigates to [EditHubPage] (a dedicated page,
|
|
||||||
/// not a dialog), satisfying the "separate edit hub page" acceptance criterion.
|
|
||||||
class HubDetailsPage extends StatelessWidget {
|
class HubDetailsPage extends StatelessWidget {
|
||||||
const HubDetailsPage({
|
const HubDetailsPage({required this.hub, required this.bloc, super.key});
|
||||||
required this.hub,
|
|
||||||
required this.bloc,
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
final Hub hub;
|
final Hub hub;
|
||||||
final ClientHubsBloc bloc;
|
final HubDetailsBloc bloc;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return BlocProvider<HubDetailsBloc>.value(
|
||||||
appBar: AppBar(
|
value: bloc,
|
||||||
title: Text(hub.name),
|
child: BlocListener<HubDetailsBloc, HubDetailsState>(
|
||||||
backgroundColor: UiColors.foreground,
|
listener: (BuildContext context, HubDetailsState state) {
|
||||||
leading: IconButton(
|
if (state.status == HubDetailsStatus.deleted) {
|
||||||
icon: const Icon(UiIcons.arrowLeft, color: UiColors.white),
|
final String message = state.successKey == 'deleted'
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
? t.client_hubs.hub_details.deleted_success
|
||||||
),
|
: (state.successMessage ?? t.client_hubs.hub_details.deleted_success);
|
||||||
actions: <Widget>[
|
UiSnackbar.show(
|
||||||
TextButton.icon(
|
context,
|
||||||
onPressed: () => _navigateToEditPage(context),
|
message: message,
|
||||||
icon: const Icon(UiIcons.edit, color: UiColors.white, size: 16),
|
type: UiSnackbarType.success,
|
||||||
label: Text(
|
);
|
||||||
t.client_hubs.hub_details.edit_button,
|
Modular.to.pop(true); // Return true to indicate change
|
||||||
style: const TextStyle(color: UiColors.white),
|
}
|
||||||
),
|
if (state.status == HubDetailsStatus.failure &&
|
||||||
),
|
state.errorMessage != null) {
|
||||||
],
|
UiSnackbar.show(
|
||||||
),
|
context,
|
||||||
backgroundColor: UiColors.bgMenu,
|
message: state.errorMessage!,
|
||||||
body: Padding(
|
type: UiSnackbarType.error,
|
||||||
padding: const EdgeInsets.all(UiConstants.space5),
|
);
|
||||||
child: Column(
|
}
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
},
|
||||||
children: <Widget>[
|
child: BlocBuilder<HubDetailsBloc, HubDetailsState>(
|
||||||
_buildDetailItem(
|
builder: (BuildContext context, HubDetailsState state) {
|
||||||
label: t.client_hubs.hub_details.name_label,
|
final bool isLoading = state.status == HubDetailsStatus.loading;
|
||||||
value: hub.name,
|
|
||||||
icon: UiIcons.home,
|
|
||||||
),
|
|
||||||
const SizedBox(height: UiConstants.space4),
|
|
||||||
_buildDetailItem(
|
|
||||||
label: t.client_hubs.hub_details.address_label,
|
|
||||||
value: hub.address,
|
|
||||||
icon: UiIcons.mapPin,
|
|
||||||
),
|
|
||||||
const SizedBox(height: UiConstants.space4),
|
|
||||||
_buildDetailItem(
|
|
||||||
label: t.client_hubs.hub_details.nfc_label,
|
|
||||||
value: hub.nfcTagId ?? t.client_hubs.hub_details.nfc_not_assigned,
|
|
||||||
icon: UiIcons.nfc,
|
|
||||||
isHighlight: hub.nfcTagId != null,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildDetailItem({
|
return Scaffold(
|
||||||
required String label,
|
appBar: const UiAppBar(showBackButton: true),
|
||||||
required String value,
|
bottomNavigationBar: HubDetailsBottomActions(
|
||||||
required IconData icon,
|
isLoading: isLoading,
|
||||||
bool isHighlight = false,
|
onDelete: () => _confirmDeleteHub(context),
|
||||||
}) {
|
onEdit: () => _navigateToEditPage(context),
|
||||||
return Container(
|
),
|
||||||
padding: const EdgeInsets.all(UiConstants.space4),
|
backgroundColor: UiColors.bgMenu,
|
||||||
decoration: BoxDecoration(
|
body: Stack(
|
||||||
color: UiColors.white,
|
children: <Widget>[
|
||||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
SingleChildScrollView(
|
||||||
boxShadow: const <BoxShadow>[
|
child: Column(
|
||||||
BoxShadow(
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
color: UiColors.popupShadow,
|
children: <Widget>[
|
||||||
blurRadius: 10,
|
// ── Header ──────────────────────────────────────────
|
||||||
offset: Offset(0, 4),
|
HubDetailsHeader(hub: hub),
|
||||||
),
|
const Divider(height: 1, thickness: 0.5),
|
||||||
],
|
|
||||||
),
|
Padding(
|
||||||
child: Row(
|
padding: const EdgeInsets.all(UiConstants.space5),
|
||||||
children: <Widget>[
|
child: Column(
|
||||||
Container(
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
padding: const EdgeInsets.all(UiConstants.space3),
|
children: <Widget>[
|
||||||
decoration: BoxDecoration(
|
HubDetailsItem(
|
||||||
color: isHighlight ? UiColors.tagInProgress : UiColors.bgInputField,
|
label: t.client_hubs.hub_details.nfc_label,
|
||||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
value:
|
||||||
),
|
hub.nfcTagId ??
|
||||||
child: Icon(
|
t.client_hubs.hub_details.nfc_not_assigned,
|
||||||
icon,
|
icon: UiIcons.nfc,
|
||||||
color: isHighlight ? UiColors.iconSuccess : UiColors.iconPrimary,
|
isHighlight: hub.nfcTagId != null,
|
||||||
size: 20,
|
),
|
||||||
),
|
const SizedBox(height: UiConstants.space4),
|
||||||
),
|
HubDetailsItem(
|
||||||
const SizedBox(width: UiConstants.space4),
|
label: t.client_hubs.hub_details.cost_center_label,
|
||||||
Expanded(
|
value: hub.costCenter != null
|
||||||
child: Column(
|
? '${hub.costCenter!.name} (${hub.costCenter!.code})'
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
: t.client_hubs.hub_details.cost_center_none,
|
||||||
children: <Widget>[
|
icon: UiIcons.bank, // Using bank icon for cost center
|
||||||
Text(label, style: UiTypography.footnote1r.textSecondary),
|
isHighlight: hub.costCenter != null,
|
||||||
const SizedBox(height: UiConstants.space1),
|
),
|
||||||
Text(value, style: UiTypography.body1m.textPrimary),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
|
),
|
||||||
|
if (isLoading)
|
||||||
|
Container(
|
||||||
|
color: UiColors.black.withValues(alpha: 0.1),
|
||||||
|
child: const Center(child: CircularProgressIndicator()),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _navigateToEditPage(BuildContext context) async {
|
Future<void> _navigateToEditPage(BuildContext context) async {
|
||||||
// Navigate to the dedicated edit page and await result.
|
final bool? saved = await Modular.to.toEditHub(hub: hub);
|
||||||
// If the page returns `true` (save succeeded), pop the details page too so
|
if (saved == true && context.mounted) {
|
||||||
// the user sees the refreshed hub list (the BLoC already holds updated data).
|
Modular.to.pop(true); // Return true to indicate change
|
||||||
final bool? saved = await Navigator.of(context).push<bool>(
|
}
|
||||||
MaterialPageRoute<bool>(
|
}
|
||||||
builder: (_) => EditHubPage(hub: hub, bloc: bloc),
|
|
||||||
|
Future<void> _confirmDeleteHub(BuildContext context) async {
|
||||||
|
final bool? confirm = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext context) => AlertDialog(
|
||||||
|
title: Text(t.client_hubs.delete_dialog.title),
|
||||||
|
content: Text(t.client_hubs.delete_dialog.message(hubName: hub.name)),
|
||||||
|
actions: <Widget>[
|
||||||
|
UiButton.text(
|
||||||
|
onPressed: () => Navigator.of(context).pop(false),
|
||||||
|
child: Text(t.client_hubs.delete_dialog.cancel),
|
||||||
|
),
|
||||||
|
UiButton.text(
|
||||||
|
onPressed: () => Navigator.of(context).pop(true),
|
||||||
|
style: TextButton.styleFrom(foregroundColor: UiColors.destructive),
|
||||||
|
child: Text(t.client_hubs.delete_dialog.delete),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
if (saved == true && context.mounted) {
|
|
||||||
Navigator.of(context).pop();
|
if (confirm == true) {
|
||||||
|
bloc.add(HubDetailsDeleteRequested(hub.id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,190 +0,0 @@
|
|||||||
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 {
|
|
||||||
|
|
||||||
/// Creates an [AddHubDialog].
|
|
||||||
const AddHubDialog({
|
|
||||||
required this.onCreate,
|
|
||||||
required this.onCancel,
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
/// Callback when the "Create Hub" button is pressed.
|
|
||||||
final void Function(
|
|
||||||
String name,
|
|
||||||
String address, {
|
|
||||||
String? placeId,
|
|
||||||
double? latitude,
|
|
||||||
double? longitude,
|
|
||||||
}) onCreate;
|
|
||||||
|
|
||||||
/// Callback when the dialog is cancelled.
|
|
||||||
final VoidCallback onCancel;
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<AddHubDialog> createState() => _AddHubDialogState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _AddHubDialogState extends State<AddHubDialog> {
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Container(
|
|
||||||
color: UiColors.bgOverlay,
|
|
||||||
child: Center(
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
child: Container(
|
|
||||||
width: MediaQuery.of(context).size.width * 0.9,
|
|
||||||
padding: const EdgeInsets.all(UiConstants.space5),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: UiColors.bgPopup,
|
|
||||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
|
||||||
boxShadow: const <BoxShadow>[
|
|
||||||
BoxShadow(color: UiColors.popupShadow, blurRadius: 20),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: Form(
|
|
||||||
key: _formKey,
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: <Widget>[
|
|
||||||
Text(
|
|
||||||
t.client_hubs.add_hub_dialog.title,
|
|
||||||
style: UiTypography.headline3m.textPrimary,
|
|
||||||
),
|
|
||||||
const SizedBox(height: UiConstants.space5),
|
|
||||||
_buildFieldLabel(t.client_hubs.add_hub_dialog.name_label),
|
|
||||||
TextFormField(
|
|
||||||
controller: _nameController,
|
|
||||||
style: UiTypography.body1r.textPrimary,
|
|
||||||
validator: (String? value) {
|
|
||||||
if (value == null || value.trim().isEmpty) {
|
|
||||||
return 'Name is required';
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
decoration: _buildInputDecoration(
|
|
||||||
t.client_hubs.add_hub_dialog.name_hint,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: UiConstants.space4),
|
|
||||||
_buildFieldLabel(t.client_hubs.add_hub_dialog.address_label),
|
|
||||||
// Assuming HubAddressAutocomplete is a custom widget wrapper.
|
|
||||||
// If it doesn't expose a validator, we might need to modify it or manually check _addressController.
|
|
||||||
// For now, let's just make sure we validate name. Address is tricky if it's a wrapper.
|
|
||||||
HubAddressAutocomplete(
|
|
||||||
controller: _addressController,
|
|
||||||
hintText: t.client_hubs.add_hub_dialog.address_hint,
|
|
||||||
focusNode: _addressFocusNode,
|
|
||||||
onSelected: (Prediction prediction) {
|
|
||||||
_selectedPrediction = prediction;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const SizedBox(height: UiConstants.space8),
|
|
||||||
Row(
|
|
||||||
children: <Widget>[
|
|
||||||
Expanded(
|
|
||||||
child: UiButton.secondary(
|
|
||||||
onPressed: widget.onCancel,
|
|
||||||
text: t.common.cancel,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: UiConstants.space3),
|
|
||||||
Expanded(
|
|
||||||
child: UiButton.primary(
|
|
||||||
onPressed: () {
|
|
||||||
if (_formKey.currentState!.validate()) {
|
|
||||||
// Manually check address if needed, or assume manual entry is ok.
|
|
||||||
if (_addressController.text.trim().isEmpty) {
|
|
||||||
// Show manual error or scaffold
|
|
||||||
UiSnackbar.show(context, message: 'Address is required', type: UiSnackbarType.error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
widget.onCreate(
|
|
||||||
_nameController.text,
|
|
||||||
_addressController.text,
|
|
||||||
placeId: _selectedPrediction?.placeId,
|
|
||||||
latitude: double.tryParse(
|
|
||||||
_selectedPrediction?.lat ?? '',
|
|
||||||
),
|
|
||||||
longitude: double.tryParse(
|
|
||||||
_selectedPrediction?.lng ?? '',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
text: t.client_hubs.add_hub_dialog.create_button,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildFieldLabel(String label) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.only(bottom: UiConstants.space2),
|
|
||||||
child: Text(label, style: UiTypography.body2m.textPrimary),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
InputDecoration _buildInputDecoration(String hint) {
|
|
||||||
return InputDecoration(
|
|
||||||
hintText: hint,
|
|
||||||
hintStyle: UiTypography.body2r.textPlaceholder,
|
|
||||||
filled: true,
|
|
||||||
fillColor: UiColors.input,
|
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: UiConstants.space4,
|
|
||||||
vertical: 14,
|
|
||||||
),
|
|
||||||
border: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
|
||||||
borderSide: const BorderSide(color: UiColors.border),
|
|
||||||
),
|
|
||||||
enabledBorder: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
|
||||||
borderSide: const BorderSide(color: UiColors.border),
|
|
||||||
),
|
|
||||||
focusedBorder: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
|
||||||
borderSide: const BorderSide(color: UiColors.ring, width: 2),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user