feat: add CLAUDE.md and GEMINI.md to provide project context for AI

feat: add bug reports and screenshots to bugs directory
feat(mobile): add device id to mobile makefile commands
fix(mobile): update mobile readme with device id instructions
fix(core_localization): update strings.g.dart with latest generation
chore: add flutter ephemeral files to gitignore

The CLAUDE.md and GEMINI.md files were added to provide AI assistants with the necessary context to understand the project structure, key commands, architecture patterns, and code conventions. This will help them generate more accurate and relevant code and documentation.

The bugs directory was added to store bug reports and screenshots, providing a central location for tracking and analyzing issues.

The mobile makefile commands were updated to include the device ID, allowing developers to specify the target device for running mobile apps. The mobile readme was updated with instructions on how to find the device ID.

The strings.g.dart file was updated with the latest generation to ensure that the localization data is up-to-date.

The flutter ephemeral files were added to gitignore to prevent them from being committed to the repository. These files are generated by Flutter for desktop platforms and should not be tracked.
This commit is contained in:
bwnyasse
2026-01-31 09:52:36 -05:00
parent 99f2030c22
commit 9517606e7a
16 changed files with 1225 additions and 11 deletions

6
.gitignore vendored
View File

@@ -126,6 +126,12 @@ build/
**/ios/Pods/ **/ios/Pods/
**/ios/.symlinks/ **/ios/.symlinks/
# Ephemeral files (generated by Flutter for desktop platforms)
**/linux/flutter/ephemeral/
**/windows/flutter/ephemeral/
**/macos/Flutter/ephemeral/
**/ios/Flutter/ephemeral/
# ============================================================================== # ==============================================================================
# FIREBASE & BACKEND # FIREBASE & BACKEND
# ============================================================================== # ==============================================================================

129
CLAUDE.md Normal file
View File

@@ -0,0 +1,129 @@
# CLAUDE.md - Project Context for AI Assistants
This file provides context for Claude Code and other AI assistants working on this codebase.
## Project Overview
**KROW Workforce** is a workforce management platform connecting businesses with temporary/on-demand workers. It consists of:
- **Client App**: For businesses to create orders, manage hubs, handle billing
- **Staff App**: For workers to manage availability, clock in/out, view earnings
- **Web Dashboard**: Admin portal (React/Vite - WIP)
- **Backend**: Firebase Data Connect + PostgreSQL on Cloud SQL
## Monorepo Structure
```
/apps
/mobile # Flutter apps (managed by Melos)
/apps
/client # krowwithus_client - Business app
/staff # krowwithus_staff - Worker app
/design_system_viewer
/packages
/core # Base utilities
/domain # Business entities, repository interfaces
/data_connect # Data layer, Firebase Data Connect SDK
/design_system # Shared UI components
/core_localization # i18n (Slang)
/features
/client/* # Client-specific features
/staff/* # Staff-specific features
/web-dashboard # React web app (WIP)
/backend
/dataconnect # GraphQL schemas, Firebase Data Connect config
/cloud-functions # Serverless functions (placeholder)
/internal
/launchpad # Internal DevOps portal
/api-harness # API testing tool
/makefiles # Modular Make targets
/docs # Project documentation
```
## Key Commands
### Mobile Development
```bash
# Install dependencies
make mobile-install
# Run client app (specify your device ID)
make mobile-client-dev-android DEVICE=<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 Normal file
View File

@@ -0,0 +1,138 @@
# GEMINI.md - Project Context for AI Assistants
This file provides context for Gemini and other AI assistants working on this codebase.
## Project Overview
**KROW Workforce** is a workforce management platform connecting businesses with temporary/on-demand workers. It consists of:
- **Client App**: For businesses to create orders, manage hubs, handle billing
- **Staff App**: For workers to manage availability, clock in/out, view earnings
- **Web Dashboard**: Admin portal (React/Vite - WIP)
- **Backend**: Firebase Data Connect + PostgreSQL on Cloud SQL
## Monorepo Structure
```
/apps
/mobile # Flutter apps (managed by Melos)
/apps
/client # krowwithus_client - Business app
/staff # krowwithus_staff - Worker app
/design_system_viewer
/packages
/core # Base utilities
/domain # Business entities, repository interfaces
/data_connect # Data layer, Firebase Data Connect SDK
/design_system # Shared UI components
/core_localization # i18n (Slang)
/features
/client/* # Client-specific features
/staff/* # Staff-specific features
/web-dashboard # React web app (WIP)
/backend
/dataconnect # GraphQL schemas, Firebase Data Connect config
/cloud-functions # Serverless functions (placeholder)
/internal
/launchpad # Internal DevOps portal
/api-harness # API testing tool
/makefiles # Modular Make targets
/docs # Project documentation
/bugs # Bug reports and screenshots
```
## Key Commands
### Mobile Development
```bash
# Install dependencies
make mobile-install
# Run client app (specify your device ID)
make mobile-client-dev-android DEVICE=<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)

View File

@@ -39,20 +39,25 @@ melos bootstrap
### 3. Running the Apps ### 3. Running the Apps
You can run the applications using Melos scripts or through the `Makefile`: You can run the applications using Melos scripts or through the `Makefile`:
First, find your device ID:
```bash
flutter devices
```
#### Client App #### Client App
```bash ```bash
# Using Melos # Using Melos
melos run start:client -d android # or ios melos run start:client -- -d <device_id>
# Using Makefile # Using Makefile (DEVICE defaults to 'android' if not specified)
make mobile-client-dev-android make mobile-client-dev-android DEVICE=<device_id>
``` ```
#### Staff App #### Staff App
```bash ```bash
# Using Melos # Using Melos
melos run start:staff -d android # or ios melos run start:staff -- -d <device_id>
# Using Makefile # Using Makefile (DEVICE defaults to 'android' if not specified)
make mobile-staff-dev-android make mobile-staff-dev-android DEVICE=<device_id>
``` ```
## 🛠 Useful Commands ## 🛠 Useful Commands

View File

@@ -6,7 +6,7 @@
/// Locales: 2 /// Locales: 2
/// Strings: 1044 (522 per locale) /// Strings: 1044 (522 per locale)
/// ///
/// Built on 2026-01-30 at 23:09 UTC /// Built on 2026-01-31 at 13:17 UTC
// coverage:ignore-file // coverage:ignore-file
// ignore_for_file: type=lint, unused_import // ignore_for_file: type=lint, unused_import

BIN
bugs/1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

BIN
bugs/2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

BIN
bugs/3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

BIN
bugs/4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

BIN
bugs/5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

BIN
bugs/6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

BIN
bugs/7.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

BIN
bugs/8.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

BIN
bugs/9.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 KiB

View File

@@ -0,0 +1,932 @@
# Bug Report & Technical Debt Analysis
**Branch:** `fix/check-boris`
**Apps Tested:** Client Mobile (Android)
---
## Housekeeping: Git Hygiene Issue
### Problem
Several Flutter-generated ephemeral files were incorrectly committed to the repository. These files are platform-specific symlinks and generated configs that should be ignored.
### Affected Files
```
apps/mobile/apps/*/linux/flutter/ephemeral/.plugin_symlinks/*
apps/mobile/apps/*/windows/flutter/ephemeral/.plugin_symlinks/*
apps/mobile/apps/*/macos/Flutter/ephemeral/*
apps/mobile/apps/*/ios/Flutter/ephemeral/*
```
### Fix Applied
1. Updated `.gitignore` to include:
```gitignore
# Ephemeral files (generated by Flutter for desktop platforms)
**/linux/flutter/ephemeral/
**/windows/flutter/ephemeral/
**/macos/Flutter/ephemeral/
**/ios/Flutter/ephemeral/
```
2. Run these commands to remove from tracking (files stay on disk):
```bash
git rm -r --cached apps/mobile/apps/client/linux/flutter/ephemeral/
git rm -r --cached apps/mobile/apps/client/windows/flutter/ephemeral/
git rm -r --cached apps/mobile/apps/client/macos/Flutter/ephemeral/
git rm -r --cached apps/mobile/apps/client/ios/Flutter/ephemeral/
git rm -r --cached apps/mobile/apps/staff/linux/flutter/ephemeral/
git rm -r --cached apps/mobile/apps/staff/windows/flutter/ephemeral/
git rm -r --cached apps/mobile/apps/staff/ios/Flutter/ephemeral/
git rm -r --cached apps/mobile/apps/design_system_viewer/ios/Flutter/ephemeral/
git rm -r --cached apps/mobile/apps/design_system_viewer/macos/Flutter/ephemeral/
```
### Note on `strings.g.dart`
The file `apps/mobile/packages/core_localization/lib/src/l10n/strings.g.dart` is auto-generated by Slang. It is listed in `apps/mobile/.gitignore` as `*.g.dart` but was committed before the gitignore rule existed.
**Recommendation:** Remove from tracking with `git rm --cached` and regenerate via `melos run gen:all` after each pull.
---
## Executive Summary
This report documents critical bugs discovered during manual testing of the Client mobile application, along with architectural issues requiring immediate attention. The analysis covers authentication flow, data model design, UI state management, and error handling practices.
| Priority | Issue | Severity | Effort |
|----------|-------|----------|--------|
| P0 | Auth/User Sync Issue | Critical | Medium |
| P1 | Error Handling Architecture | High | High |
| P1 | Order Display Logic | Medium | Low |
| P2 | Hub Delete UI Freeze | Medium | Low |
| P2 | Hub Name vs Address Display | Low | Low |
---
## Bug 1: Authentication & User Sync Issue
### Status: CRITICAL
### Description
Users who attempt to create an account may end up in an inconsistent state where:
- Firebase Authentication has their account (email/password stored)
- PostgreSQL database does NOT have their user profile
This results in:
- "Account already exists" when trying to register again
- "Incorrect email or password" when trying to log in (even with correct credentials)
### Screenshots
- `1.png` - Registration attempt with boris@bwnyasse.net showing "Account already exists"
- `2.png` - Login attempt showing "Incorrect email or password"
- `3.png`, `4.png` - Additional registration failures
### Root Cause
**File:** `apps/mobile/packages/features/client/authentication/lib/src/data/repositories_impl/auth_repository_impl.dart`
The registration flow (lines 57-120) performs three sequential operations:
```dart
// Step 1: Create Firebase Auth user
final credential = await _firebaseAuth.createUserWithEmailAndPassword(...);
// Step 2: Create Business in PostgreSQL
final createBusinessResponse = await _dataConnect.createBusiness(...);
// Step 3: Create User in PostgreSQL
final createUserResponse = await _dataConnect.createUser(...);
```
**Problem:** If Step 2 or Step 3 fails AFTER Step 1 succeeds:
- The rollback `firebaseUser.delete()` may fail silently
- User exists in Firebase Auth but NOT in PostgreSQL
- Login fails because `_getUserProfile()` cannot find the user (line 142-145)
**Additionally, line 100 has an incomplete TODO:**
```dart
// TO-DO: Also delete the created Business if this fails
```
If `createUser` fails, the orphaned Business record remains in PostgreSQL.
### Technical Flow
```
Registration Attempt:
├─ Firebase Auth: createUserWithEmailAndPassword() ✓ (User created)
├─ Data Connect: createBusiness() ✓ (Business created)
└─ Data Connect: createUser() ✗ (FAILS - network error, constraint violation, etc.)
└─ Rollback: firebaseUser.delete() ✗ (May fail silently)
Result:
- Firebase Auth: User EXISTS
- PostgreSQL users table: User MISSING
- PostgreSQL businesses table: Orphaned Business record
Subsequent Login:
├─ Firebase Auth: signInWithEmailAndPassword() ✓ (Credentials valid)
└─ _getUserProfile(): getUserById() returns NULL
└─ Throws: "Authenticated user profile not found in database."
└─ But error shown to user: "Incorrect email or password." (misleading!)
```
### Recommended Fix
1. **Implement transactional rollback:**
```dart
Future<domain.User> signUpWithEmail({...}) async {
firebase.User? firebaseUser;
String? businessId;
try {
// Step 1
final credential = await _firebaseAuth.createUserWithEmailAndPassword(...);
firebaseUser = credential.user;
// Step 2
final businessResult = await _dataConnect.createBusiness(...);
businessId = businessResult.data?.business_insert.id;
// Step 3
final userResult = await _dataConnect.createUser(...);
if (userResult.data?.user_insert == null) {
throw Exception('User creation failed');
}
return _getUserProfile(...);
} catch (e) {
// Full rollback
if (businessId != null) {
await _dataConnect.deleteBusiness(id: businessId).execute();
}
if (firebaseUser != null) {
await firebaseUser.delete();
}
rethrow;
}
}
```
2. **Add admin tool to reconcile orphaned accounts**
3. **Add retry mechanism with idempotency checks**
---
## Bug 2: Hub Name vs Address Display
### Status: LOW PRIORITY
### Description
Order cards display the hub's street address instead of the hub name, creating inconsistency with the hub management screen.
### Screenshots
- `5.png` - Order showing "6800 San Jose Street, Granada Hills, CA, USA"
- `6.png` - Order showing "San Jose Street"
- Compare with `7.png` showing hub names: "Downtown Operations Hub", "Central Operations Hub"
### Root Cause
**File:** `apps/mobile/packages/features/client/view_orders/lib/src/presentation/widgets/view_order_card.dart`
Lines 213-219 display `order.locationAddress` instead of `order.location` (hub name):
```dart
Expanded(
child: Text(
order.locationAddress, // Shows address, not hub name
style: UiTypography.footnote2r.textSecondary,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
```
The data is correctly stored during order creation in `client_create_order_repository_impl.dart`:
- Line 104: `.location(hub.name)` - Hub name
- Line 105: `.locationAddress(hub.address)` - Address
### Recommended Fix
Update `view_order_card.dart` to show hub name as primary, address as secondary:
```dart
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
order.location, // Hub name
style: UiTypography.body2m.textPrimary,
),
Text(
order.locationAddress, // Address as subtitle
style: UiTypography.footnote2r.textSecondary,
),
],
),
```
---
## Bug 3: Hub Delete UI Freeze
### Status: MEDIUM PRIORITY
### Description
After attempting to delete a hub that has orders (which correctly shows an error), subsequent delete attempts on ANY hub cause the UI to freeze with a loading overlay that never disappears.
### Screenshot
- `7.png` - Error message "Sorry this hub has orders, it can't be deleted."
### Root Cause
**File:** `apps/mobile/packages/features/client/hubs/lib/src/presentation/blocs/client_hubs_state.dart`
The `copyWith` method cannot reset `errorMessage` to `null` due to Dart's null-coalescing behavior:
```dart
ClientHubsState copyWith({
String? errorMessage,
String? successMessage,
// ...
}) {
return ClientHubsState(
// BUG: null ?? this.errorMessage keeps the OLD value!
errorMessage: errorMessage ?? this.errorMessage,
successMessage: successMessage ?? this.successMessage,
// ...
);
}
```
**Flow of the bug:**
1. Delete Hub A → Error "Sorry this hub has orders..."
2. `_onMessageCleared` calls `copyWith(errorMessage: null)`
3. `null ?? this.errorMessage` = OLD ERROR MESSAGE (not cleared!)
4. Delete Hub B → Same error message
5. `listenWhen` checks: `previous.errorMessage != current.errorMessage`
6. Both are "Sorry this hub has orders..." → FALSE, listener not called
7. `MessageCleared` never sent → Status never resets → Overlay stays forever
**Proof the team knows this pattern:** They correctly implemented `clearHubToIdentify` flag (lines 45, 53-54) but forgot to do the same for messages.
### Recommended Fix
**File:** `client_hubs_state.dart`
```dart
ClientHubsState copyWith({
ClientHubsStatus? status,
List<Hub>? hubs,
String? errorMessage,
String? successMessage,
bool? showAddHubDialog,
Hub? hubToIdentify,
bool clearHubToIdentify = false,
bool clearErrorMessage = false, // ADD THIS
bool clearSuccessMessage = false, // ADD THIS
}) {
return ClientHubsState(
status: status ?? this.status,
hubs: hubs ?? this.hubs,
errorMessage: clearErrorMessage
? null
: (errorMessage ?? this.errorMessage),
successMessage: clearSuccessMessage
? null
: (successMessage ?? this.successMessage),
showAddHubDialog: showAddHubDialog ?? this.showAddHubDialog,
hubToIdentify: clearHubToIdentify
? null
: (hubToIdentify ?? this.hubToIdentify),
);
}
```
**File:** `client_hubs_bloc.dart` - Update `_onMessageCleared`:
```dart
void _onMessageCleared(
ClientHubsMessageCleared event,
Emitter<ClientHubsState> emit,
) {
emit(
state.copyWith(
clearErrorMessage: true, // USE FLAG
clearSuccessMessage: true, // USE FLAG
status: state.status == ClientHubsStatus.actionSuccess ||
state.status == ClientHubsStatus.actionFailure
? ClientHubsStatus.success
: state.status,
),
);
}
```
---
## Bug 4: Order Display Shows Positions as Separate Orders
### Status: MEDIUM PRIORITY - DESIGN DECISION NEEDED
### Description
When creating an order with multiple positions (e.g., 1 Cook + 1 Bartender), the Orders list shows them as separate cards instead of a single order with multiple positions.
### Screenshot
- `8.png` - Shows "Cook - Boris Test Order 1" and "Bartender - Boris Test Order 1" as separate cards
### Analysis
**This is NOT a data bug.** The data model is correct:
- 1 Order → 1 Shift → N ShiftRoles (positions)
The issue is in the **display logic**.
**File:** `apps/mobile/packages/features/client/view_orders/lib/src/data/repositories/view_orders_repository_impl.dart`
Lines 32-89 query `ShiftRoles` and create one `OrderItem` per role:
```dart
// Query returns ShiftRoles, not Orders
final result = await _dataConnect
.listShiftRolesByBusinessAndDateRange(...)
.execute();
// Each ShiftRole becomes a card
return result.data.shiftRoles.map((shiftRole) {
return domain.OrderItem(
title: '${shiftRole.role.name} - $eventName', // "Cook - Boris Test Order 1"
// ...
);
}).toList();
```
### Options
**Option A: Keep current behavior (per-position cards)**
- Pros: Granular view of each position's status
- Cons: Confusing for users who think they created one order
**Option B: Group by Order (recommended)**
- Pros: Matches user mental model
- Cons: Requires refactoring view layer
### Recommended Fix (Option B)
```dart
Future<List<domain.OrderItem>> getOrdersForRange({...}) async {
final result = await _dataConnect
.listShiftRolesByBusinessAndDateRange(...)
.execute();
// Group ShiftRoles by Order ID
final Map<String, List<ShiftRole>> orderGroups = {};
for (final shiftRole in result.data.shiftRoles) {
final orderId = shiftRole.shift.order.id;
orderGroups.putIfAbsent(orderId, () => []);
orderGroups[orderId]!.add(shiftRole);
}
// Create one OrderItem per Order with positions summary
return orderGroups.entries.map((entry) {
final roles = entry.value;
final firstRole = roles.first;
final positionsSummary = roles.map((r) => r.role.name).join(', ');
final totalWorkers = roles.fold(0, (sum, r) => sum + r.count);
return domain.OrderItem(
id: entry.key,
orderId: entry.key,
title: firstRole.shift.order.eventName ?? 'Order',
subtitle: positionsSummary, // "Cook, Bartender"
workersNeeded: totalWorkers,
// ... aggregate other fields
);
}).toList();
}
```
---
## Bug 5: Mock Data in Production Views
### Status: INFORMATIONAL
### Description
The Coverage screen shows "Jose Salazar - Checked in at 9:00 AM" which appears to be test/mock data.
### Screenshot
- `9.png` - Daily Coverage showing mock worker data
### Recommendation
Ensure mock data is clearly separated and not visible in builds distributed for testing. Consider adding a visual indicator (e.g., "TEST DATA" banner) when using mock repositories.
---
## Architectural Issue: Error Handling
### Status: HIGH PRIORITY - TECHNICAL DEBT
### Description
The application exposes raw technical error messages to end users. This is unprofessional and potentially a security concern.
### Evidence
Found **60+ instances** of `throw Exception('technical message')` across the codebase:
```dart
// Examples of problematic error messages shown to users:
throw Exception('Authenticated user profile not found in database.');
throw Exception('Business creation failed after Firebase user registration.');
throw Exception('Staff profile not found for User ID: ${user.uid}');
throw Exception('Failed to fetch certificates: $e'); // Exposes stack trace!
throw Exception('Error signing out: ${e.toString()}');
```
### Current State
- No centralized error handling system
- No custom exception classes
- Technical messages shown directly to users
- No i18n support for error messages
- No error codes for logging/tracking
### Recommended Architecture
```
┌─────────────────────────────────────────────────────────────────┐
│ DOMAIN LAYER │
│ │
│ packages/domain/lib/src/exceptions/app_exception.dart │
│ │
│ sealed class AppException implements Exception { │
│ const AppException({required this.code, this.technical}); │
│ final String code; // For logging: "AUTH_001" │
│ final String? technical; // For devs only │
│ String get messageKey; // For i18n: "errors.auth.x" │
│ } │
│ │
│ class InvalidCredentialsException extends AuthException { │
│ String get messageKey => 'errors.auth.invalid_credentials'; │
│ } │
│ │
│ class HubHasOrdersException extends HubException { │
│ String get messageKey => 'errors.hub.has_orders'; │
│ } │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ PRESENTATION LAYER │
│ │
│ } on AppException catch (e) { │
│ log('Error ${e.code}: ${e.technical}'); // Dev logging │
│ emit(state.copyWith(errorKey: e.messageKey)); │
│ } catch (e) { │
│ log('Unexpected: $e'); │
│ emit(state.copyWith(errorKey: 'errors.generic.unknown')); │
│ } │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ LOCALIZATION (i18n) │
│ │
│ errors: │
│ auth: │
│ invalid_credentials: "Email or password is incorrect" │
│ account_exists: "An account with this email exists" │
│ session_expired: "Please sign in again" │
│ hub: │
│ has_orders: "This hub has active orders" │
│ order: │
│ missing_hub: "Please select a location" │
│ generic: │
│ unknown: "Something went wrong. Please try again." │
│ no_connection: "No internet connection" │
└─────────────────────────────────────────────────────────────────┘
```
### Implementation Plan
#### Phase 1: Create Exception Classes (1 hour)
**Create:** `packages/domain/lib/src/exceptions/app_exception.dart`
```dart
/// Base sealed class for all application exceptions.
sealed class AppException implements Exception {
const AppException({
required this.code,
this.technicalMessage,
});
/// Unique error code for logging/tracking (e.g., "AUTH_001")
final String code;
/// Technical details for developers (never shown to users)
final String? technicalMessage;
/// Returns the localization key for user-friendly message
String get messageKey;
@override
String toString() => 'AppException($code): $technicalMessage';
}
// ============================================================
// AUTH EXCEPTIONS
// ============================================================
sealed class AuthException extends AppException {
const AuthException({required super.code, super.technicalMessage});
}
class InvalidCredentialsException extends AuthException {
const InvalidCredentialsException({String? technicalMessage})
: super(code: 'AUTH_001', technicalMessage: technicalMessage);
@override
String get messageKey => 'errors.auth.invalid_credentials';
}
class AccountExistsException extends AuthException {
const AccountExistsException({String? technicalMessage})
: super(code: 'AUTH_002', technicalMessage: technicalMessage);
@override
String get messageKey => 'errors.auth.account_exists';
}
class SessionExpiredException extends AuthException {
const SessionExpiredException({String? technicalMessage})
: super(code: 'AUTH_003', technicalMessage: technicalMessage);
@override
String get messageKey => 'errors.auth.session_expired';
}
class UserNotFoundException extends AuthException {
const UserNotFoundException({String? technicalMessage})
: super(code: 'AUTH_004', technicalMessage: technicalMessage);
@override
String get messageKey => 'errors.auth.user_not_found';
}
class UnauthorizedAppException extends AuthException {
const UnauthorizedAppException({String? technicalMessage})
: super(code: 'AUTH_005', technicalMessage: technicalMessage);
@override
String get messageKey => 'errors.auth.unauthorized_app';
}
class WeakPasswordException extends AuthException {
const WeakPasswordException({String? technicalMessage})
: super(code: 'AUTH_006', technicalMessage: technicalMessage);
@override
String get messageKey => 'errors.auth.weak_password';
}
// ============================================================
// HUB EXCEPTIONS
// ============================================================
sealed class HubException extends AppException {
const HubException({required super.code, super.technicalMessage});
}
class HubHasOrdersException extends HubException {
const HubHasOrdersException({String? technicalMessage})
: super(code: 'HUB_001', technicalMessage: technicalMessage);
@override
String get messageKey => 'errors.hub.has_orders';
}
class HubNotFoundException extends HubException {
const HubNotFoundException({String? technicalMessage})
: super(code: 'HUB_002', technicalMessage: technicalMessage);
@override
String get messageKey => 'errors.hub.not_found';
}
class HubCreationFailedException extends HubException {
const HubCreationFailedException({String? technicalMessage})
: super(code: 'HUB_003', technicalMessage: technicalMessage);
@override
String get messageKey => 'errors.hub.creation_failed';
}
// ============================================================
// ORDER EXCEPTIONS
// ============================================================
sealed class OrderException extends AppException {
const OrderException({required super.code, super.technicalMessage});
}
class OrderMissingHubException extends OrderException {
const OrderMissingHubException({String? technicalMessage})
: super(code: 'ORDER_001', technicalMessage: technicalMessage);
@override
String get messageKey => 'errors.order.missing_hub';
}
class OrderMissingVendorException extends OrderException {
const OrderMissingVendorException({String? technicalMessage})
: super(code: 'ORDER_002', technicalMessage: technicalMessage);
@override
String get messageKey => 'errors.order.missing_vendor';
}
class OrderCreationFailedException extends OrderException {
const OrderCreationFailedException({String? technicalMessage})
: super(code: 'ORDER_003', technicalMessage: technicalMessage);
@override
String get messageKey => 'errors.order.creation_failed';
}
class ShiftCreationFailedException extends OrderException {
const ShiftCreationFailedException({String? technicalMessage})
: super(code: 'ORDER_004', technicalMessage: technicalMessage);
@override
String get messageKey => 'errors.order.shift_creation_failed';
}
// ============================================================
// PROFILE EXCEPTIONS
// ============================================================
sealed class ProfileException extends AppException {
const ProfileException({required super.code, super.technicalMessage});
}
class StaffProfileNotFoundException extends ProfileException {
const StaffProfileNotFoundException({String? technicalMessage})
: super(code: 'PROFILE_001', technicalMessage: technicalMessage);
@override
String get messageKey => 'errors.profile.staff_not_found';
}
class BusinessNotFoundException extends ProfileException {
const BusinessNotFoundException({String? technicalMessage})
: super(code: 'PROFILE_002', technicalMessage: technicalMessage);
@override
String get messageKey => 'errors.profile.business_not_found';
}
// ============================================================
// NETWORK/GENERIC EXCEPTIONS
// ============================================================
class NetworkException extends AppException {
const NetworkException({String? technicalMessage})
: super(code: 'NET_001', technicalMessage: technicalMessage);
@override
String get messageKey => 'errors.generic.no_connection';
}
class UnknownException extends AppException {
const UnknownException({String? technicalMessage})
: super(code: 'UNKNOWN', technicalMessage: technicalMessage);
@override
String get messageKey => 'errors.generic.unknown';
}
```
#### Phase 2: Add Localization Keys (30 minutes)
**Update:** `packages/core_localization/lib/src/l10n/strings.i18n.yaml`
```yaml
errors:
auth:
invalid_credentials: "The email or password you entered is incorrect."
account_exists: "An account with this email already exists. Try signing in instead."
session_expired: "Your session has expired. Please sign in again."
user_not_found: "We couldn't find your account. Please check your email and try again."
unauthorized_app: "This account is not authorized for this app."
weak_password: "Please choose a stronger password with at least 8 characters."
hub:
has_orders: "This hub has active orders and cannot be deleted."
not_found: "The hub you're looking for doesn't exist."
creation_failed: "We couldn't create the hub. Please try again."
order:
missing_hub: "Please select a location for your order."
missing_vendor: "Please select a vendor for your order."
creation_failed: "We couldn't create your order. Please try again."
shift_creation_failed: "We couldn't schedule the shift. Please try again."
profile:
staff_not_found: "Your profile couldn't be loaded. Please sign in again."
business_not_found: "Your business profile couldn't be loaded. Please sign in again."
generic:
unknown: "Something went wrong. Please try again."
no_connection: "No internet connection. Please check your network and try again."
```
#### Phase 3: Migrate Repositories (4-6 hours)
**Example migration for auth_repository_impl.dart:**
```dart
// BEFORE
if (e.code == 'invalid-credential' || e.code == 'wrong-password') {
throw Exception('Incorrect email or password.');
}
// AFTER
if (e.code == 'invalid-credential' || e.code == 'wrong-password') {
throw InvalidCredentialsException(
technicalMessage: 'Firebase error: ${e.code}',
);
}
```
```dart
// BEFORE
if (e.code == 'email-already-in-use') {
throw Exception('An account already exists for that email address.');
}
// AFTER
if (e.code == 'email-already-in-use') {
throw AccountExistsException(
technicalMessage: 'Email: $email',
);
}
```
```dart
// BEFORE
if (user == null) {
throw Exception('Authenticated user profile not found in database.');
}
// AFTER
if (user == null) {
throw UserNotFoundException(
technicalMessage: 'Firebase UID: $firebaseUserId not found in users table',
);
}
```
#### Phase 4: Update BLoCs (2-3 hours)
**Example for client_hubs_bloc.dart:**
```dart
// BEFORE
Future<void> _onDeleteRequested(...) async {
emit(state.copyWith(status: ClientHubsStatus.actionInProgress));
try {
await _deleteHubUseCase(DeleteHubArguments(hubId: event.hubId));
final List<Hub> hubs = await _getHubsUseCase();
emit(state.copyWith(
status: ClientHubsStatus.actionSuccess,
hubs: hubs,
successMessage: 'Hub deleted successfully',
));
} catch (e) {
emit(state.copyWith(
status: ClientHubsStatus.actionFailure,
errorMessage: e.toString(), // EXPOSES TECHNICAL ERROR!
));
}
}
// AFTER
Future<void> _onDeleteRequested(...) async {
emit(state.copyWith(status: ClientHubsStatus.actionInProgress));
try {
await _deleteHubUseCase(DeleteHubArguments(hubId: event.hubId));
final List<Hub> hubs = await _getHubsUseCase();
emit(state.copyWith(
status: ClientHubsStatus.actionSuccess,
hubs: hubs,
successMessageKey: 'success.hub.deleted',
));
} on AppException catch (e) {
// Log technical details for debugging
debugPrint('Error ${e.code}: ${e.technicalMessage}');
emit(state.copyWith(
status: ClientHubsStatus.actionFailure,
errorMessageKey: e.messageKey,
));
} catch (e, stackTrace) {
// Unexpected error - log full details
debugPrint('Unexpected error: $e\n$stackTrace');
emit(state.copyWith(
status: ClientHubsStatus.actionFailure,
errorMessageKey: 'errors.generic.unknown',
));
}
}
```
#### Phase 5: Update UI Layer (1-2 hours)
**Example for client_hubs_page.dart:**
```dart
// BEFORE
listener: (context, state) {
if (state.errorMessage != null && state.errorMessage!.isNotEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(state.errorMessage!)),
);
// ...
}
}
// AFTER
listener: (context, state) {
if (state.errorMessageKey != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(t[state.errorMessageKey!])),
);
// ...
}
}
```
---
## Summary of Required Changes
### Immediate Fixes (This Sprint)
| File | Change | Priority |
|------|--------|----------|
| `client_hubs_state.dart` | Add `clearErrorMessage`/`clearSuccessMessage` flags | P2 |
| `client_hubs_bloc.dart` | Use clear flags in `_onMessageCleared` | P2 |
| `view_order_card.dart` | Display `location` instead of `locationAddress` | P2 |
### Short-term (Next Sprint)
| Task | Effort |
|------|--------|
| Create `AppException` sealed class in domain | 1h |
| Add error localization keys | 30min |
| Migrate auth repositories | 2h |
| Migrate hub repositories | 1h |
### Medium-term (Next 2-3 Sprints)
| Task | Effort |
|------|--------|
| Migrate all repositories to AppException | 4-6h |
| Update all BLoCs for proper error handling | 2-3h |
| Update all UI components for localized errors | 1-2h |
| Add admin tool to reconcile orphaned Firebase accounts | 4h |
---
## Appendix: Files Requiring Changes
### Repositories with `throw Exception()` to migrate:
1. `packages/features/client/authentication/lib/src/data/repositories_impl/auth_repository_impl.dart` (13 instances)
2. `packages/features/client/hubs/lib/src/data/repositories_impl/hub_repository_impl.dart` (6 instances)
3. `packages/features/client/create_order/lib/src/data/repositories_impl/client_create_order_repository_impl.dart` (6 instances)
4. `packages/features/client/view_orders/lib/src/data/repositories/view_orders_repository_impl.dart` (2 instances)
5. `packages/features/client/settings/lib/src/data/repositories_impl/settings_repository_impl.dart` (1 instance)
6. `packages/features/staff/authentication/lib/src/data/repositories_impl/auth_repository_impl.dart` (7 instances)
7. `packages/features/staff/shifts/lib/src/data/repositories_impl/shifts_repository_impl.dart` (2 instances)
8. `packages/features/staff/profile/lib/src/data/repositories/profile_repository_impl.dart` (3 instances)
9. `packages/features/staff/availability/lib/src/data/repositories_impl/availability_repository_impl.dart` (2 instances)
10. `packages/features/staff/clock_in/lib/src/data/repositories_impl/clock_in_repository_impl.dart` (3 instances)
11. And 10+ more files in staff profile sections...
---
**Report prepared for development team review.**

View File

@@ -4,6 +4,10 @@
MOBILE_DIR := apps/mobile MOBILE_DIR := apps/mobile
# Device ID for running mobile apps (override with DEVICE=<id>)
# Find your device ID with: flutter devices
DEVICE ?= android
# --- General --- # --- General ---
mobile-install: install-melos mobile-install: install-melos
@echo "--> Bootstrapping mobile workspace (Melos)..." @echo "--> Bootstrapping mobile workspace (Melos)..."
@@ -17,8 +21,8 @@ mobile-info:
# --- Client App --- # --- Client App ---
mobile-client-dev-android: mobile-client-dev-android:
@echo "--> Running client app on Android..." @echo "--> Running client app on Android (device: $(DEVICE))..."
@cd $(MOBILE_DIR) && melos run start:client -- -d android @cd $(MOBILE_DIR) && melos run start:client -- -d $(DEVICE)
mobile-client-build: mobile-client-build:
@if [ -z "$(PLATFORM)" ]; then \ @if [ -z "$(PLATFORM)" ]; then \
@@ -33,8 +37,8 @@ mobile-client-build:
# --- Staff App --- # --- Staff App ---
mobile-staff-dev-android: mobile-staff-dev-android:
@echo "--> Running staff app on Android..." @echo "--> Running staff app on Android (device: $(DEVICE))..."
@cd $(MOBILE_DIR) && melos run start:staff -- -d android @cd $(MOBILE_DIR) && melos run start:staff -- -d $(DEVICE)
mobile-staff-build: mobile-staff-build:
@if [ -z "$(PLATFORM)" ]; then \ @if [ -z "$(PLATFORM)" ]; then \