Merge branch 'dev' into backend/5-event-schema

This commit is contained in:
José Salazar
2025-11-17 12:14:42 -05:00
80 changed files with 12356 additions and 134 deletions

View File

@@ -0,0 +1,18 @@
# This file specifies files that are *not* uploaded to Google Cloud
# using gcloud. It follows the same syntax as .gitignore, with the addition of
# "#!include" directives (which insert the entries of the given .gitignore-style
# file at that point).
#
# For more information, run:
# $ gcloud topic gcloudignore
#
.gcloudignore
# If you would like to upload your .git directory, .gitignore file or files
# from your .gitignore file, remove the corresponding line
# below:
.git
.gitignore
# Node.js dependencies:
node_modules/
*.log

View File

@@ -0,0 +1,28 @@
# Utiliser nginx pour servir les fichiers statiques
FROM nginx:alpine
# Copier les fichiers statiques
COPY index.html /usr/share/nginx/html/
COPY assets /usr/share/nginx/html/assets/
COPY favicon.svg /usr/share/nginx/html/
COPY logo.svg /usr/share/nginx/html/
# Configuration nginx pour le routing SPA
RUN echo 'server { \
listen 8080; \
server_name _; \
root /usr/share/nginx/html; \
index index.html; \
location / { \
try_files $uri $uri/ /index.html; \
} \
# Headers de sécurité \
add_header X-Frame-Options "SAMEORIGIN" always; \
add_header X-Content-Type-Options "nosniff" always; \
add_header X-XSS-Protection "1; mode=block" always; \
}' > /etc/nginx/conf.d/default.conf
# Nginx écoute sur le port 8080 (requis par Cloud Run)
EXPOSE 8080
CMD ["nginx", "-g", "daemon off;"]

View File

@@ -58,5 +58,53 @@
"title": "Legacy App Use Cases",
"type": "mermaid",
"icon": "bi-diagram-2"
},
{
"path": "assets/diagrams/legacy/staff-mobile-application/api-map.mermaid",
"title": "Legacy App API Map",
"type": "mermaid",
"icon": "bi-phone"
},
{
"path": "assets/diagrams/legacy/staff-mobile-application/use-case-flows.mermaid",
"title": "Legacy App Use Cases BE Chart",
"type": "mermaid",
"icon": "bi-diagram-2"
},
{
"path": "assets/diagrams/legacy/staff-mobile-application/backend-architecture.mermaid",
"title": "Legacy App Backend Architecture",
"type": "mermaid",
"icon": "bi-diagram-2"
},
{
"path": "assets/diagrams/legacy/client-mobile-application/overview.mermaid",
"title": "Legacy App Overview",
"type": "mermaid",
"icon": "bi-phone"
},
{
"path": "assets/diagrams/legacy/client-mobile-application/use-case-flowchart.mermaid",
"title": "Legacy App Use Cases",
"type": "mermaid",
"icon": "bi-diagram-2"
},
{
"path": "assets/diagrams/legacy/client-mobile-application/api-map.mermaid",
"title": "Legacy App API Map",
"type": "mermaid",
"icon": "bi-phone"
},
{
"path": "assets/diagrams/legacy/client-mobile-application/use-case-flows.mermaid",
"title": "Legacy App Use Cases BE Chart",
"type": "mermaid",
"icon": "bi-diagram-2"
},
{
"path": "assets/diagrams/legacy/client-mobile-application/backend-architecture.mermaid",
"title": "Legacy App Backend Architecture",
"type": "mermaid",
"icon": "bi-diagram-2"
}
]

View File

@@ -0,0 +1,55 @@
flowchart TD
subgraph "GraphQL API"
direction LR
subgraph "Queries"
Q1[getEvents]
Q2[getEventDetails]
Q3[getInvoices]
Q4[getInvoiceDetails]
Q5[getNotifications]
Q6[getNotificationDetails]
Q7[getProfile]
Q8[getAssignedStaff]
end
subgraph "Mutations"
M1[createEvent]
M2[updateProfile]
M3[rateStaff]
M4[clockIn]
M5[clockOut]
M6[uploadProfilePicture]
end
end
subgraph "Firebase"
direction LR
subgraph "Firestore Collections"
FS1[events]
FS2[invoices]
FS3[notifications]
FS4[users]
end
subgraph "Firebase Storage"
FB1[Profile Pictures]
end
end
Q1 --> FS1
Q2 --> FS1
Q3 --> FS2
Q4 --> FS2
Q5 --> FS3
Q6 --> FS3
Q7 --> FS4
Q8 --> FS1
Q8 --> FS4
M1 --> FS1
M2 --> FS4
M3 --> FS1
M3 --> FS4
M4 --> FS1
M5 --> FS1
M6 --> FB1

View File

@@ -0,0 +1,28 @@
flowchart TD
subgraph "Client"
A[Flutter App]
end
subgraph "Backend"
B[GraphQL Server - Node.js]
C[Firebase]
end
subgraph "Firebase Services"
C1[Firebase Auth]
C2[Firebase Firestore]
C3[Firebase Storage]
end
A -- "GraphQL Queries/Mutations" --> B
A -- "Authentication" --> C1
B -- "Data Operations" --> C2
B -- "File Operations" --> C3
C1 -- "User Tokens" --> A
C2 -- "Data" --> B
C3 -- "Files" --> B
B -- "Data/Files" --> A

View File

@@ -0,0 +1,65 @@
flowchart TD
subgraph "App Initialization"
A[main.dart] --> B(KrowApp);
B --> C{MaterialApp.router};
C --> D[Initial Route: /];
end
subgraph "Authentication Flow"
D --> E(SplashRoute);
E --> F{SplashRedirectGuard};
F -- Authenticated --> G(HomeRoute);
F -- Unauthenticated/Error --> H(SignInFlowRoute);
end
subgraph "Sign-In/Welcome Flow"
H --> I[WelcomeRoute];
I --> J[SignInRoute];
J --> K[ResetPassRoute];
L[Deeplink with oobCode] --> M[EnterNewPassRoute];
J -- Forgot Password --> K;
I -- Sign In --> J;
end
subgraph "Main Application (Home)"
G --> G_Tabs((Bottom Navigation));
G_Tabs -- Events --> Events;
G_Tabs -- Invoices --> Invoices;
G_Tabs -- Notifications --> Notifications;
G_Tabs -- Profile --> Profile;
G_Tabs -- Create Event --> CreateEvent;
end
subgraph "Events Flow"
Events[EventsFlow] --> Events_List(EventsListMainRoute);
Events_List --> Events_Event_Details(EventDetailsRoute);
Events_Event_Details --> Events_Assigned_Staff(AssignedStaffRoute);
Events_Assigned_Staff --> Events_Clock_Manual(ClockManualRoute);
Events_Event_Details --> Events_Rate_Staff(RateStaffRoute);
end
subgraph "Create Event Flow"
CreateEvent[CreateEventFlow] --> Create_Event_Edit(CreateEventRoute);
Create_Event_Edit --> Create_Event_Preview(EventDetailsRoute);
end
subgraph "Invoice Flow"
Invoices[InvoiceFlow] --> Invoice_List(InvoicesListMainRoute);
Invoice_List --> Invoice_Details(InvoiceDetailsRoute);
Invoice_Details --> Invoice_Event_Details(EventDetailsRoute);
end
subgraph "Notifications Flow"
Notifications[NotificationFlow] --> Notification_List(NotificationsListRoute);
Notification_List --> Notification_Details(NotificationDetailsRoute);
end
subgraph "Profile Flow"
Profile[ProfileFlow] --> Profile_Preview(ProfilePreviewRoute);
Profile_Preview --> Profile_Edit(PersonalInfoRoute);
end
style F fill:#f9f,stroke:#333,stroke-width:2px
style G fill:#bbf,stroke:#333,stroke-width:2px
style H fill:#bbf,stroke:#333,stroke-width:2px
style L fill:#f9f,stroke:#333,stroke-width:2px

View File

@@ -0,0 +1,80 @@
flowchart TD
subgraph "User"
U((User))
end
subgraph "Authentication Use Cases"
UC1(Sign In)
UC2(Sign Out)
UC3(Password Reset)
end
subgraph "Event Management Use Cases"
UC4(Create Event)
UC5(View Event Details)
UC6(List Events)
end
subgraph "Invoice Management Use Cases"
UC7(List Invoices)
UC8(View Invoice Details)
end
subgraph "Staff Management Use Cases"
UC9(View Assigned Staff)
UC10(Rate Staff)
UC11(Manual Clock In/Out)
end
subgraph "Profile Management Use Cases"
UC12(View Profile)
UC13(Edit Profile)
end
subgraph "Notification Use Cases"
UC14(List Notifications)
UC15(View Notification Details)
end
U --> UC1
UC1 -- Success --> UC6
UC1 -- Forgot Password --> UC3
UC6 --> UC5
UC5 --> UC9
UC9 --> UC11
UC5 --> UC10
U --> UC4
UC4 -- Success --> UC5
UC6 -- Triggers --> UC7
UC7 --> UC8
UC8 -- View Event --> UC5
U --> UC12
UC12 --> UC13
UC13 -- Success --> UC12
U --> UC14
UC14 --> UC15
UC12 -- Sign Out --> UC2
UC2 -- Success --> UC1
%% Styling
style UC1 fill:#f9f,stroke:#333,stroke-width:2px
style UC2 fill:#f9f,stroke:#333,stroke-width:2px
style UC3 fill:#f9f,stroke:#333,stroke-width:2px
style UC4 fill:#bbf,stroke:#333,stroke-width:2px
style UC5 fill:#bbf,stroke:#333,stroke-width:2px
style UC6 fill:#bbf,stroke:#333,stroke-width:2px
style UC7 fill:#bbf,stroke:#333,stroke-width:2px
style UC8 fill:#bbf,stroke:#333,stroke-width:2px
style UC9 fill:#bbf,stroke:#333,stroke-width:2px
style UC10 fill:#bbf,stroke:#333,stroke-width:2px
style UC11 fill:#bbf,stroke:#333,stroke-width:2px
style UC12 fill:#bbf,stroke:#333,stroke-width:2px
style UC13 fill:#bbf,stroke:#333,stroke-width:2px
style UC14 fill:#bbf,stroke:#333,stroke-width:2px
style UC15 fill:#bbf,stroke:#333,stroke-width:2px

View File

@@ -0,0 +1,45 @@
flowchart TD
subgraph "Sign-In Flow"
A1[User enters credentials] --> B1{SignInBloc};
B1 --> C1[Firebase Auth: signInWithEmailAndPassword];
C1 -- Success --> D1[Navigate to Home];
C1 -- Failure --> E1[Show error message];
end
subgraph "Password Reset Flow"
A2[User requests password reset] --> B2{SignInBloc};
B2 --> C2[Firebase Auth: sendPasswordResetEmail];
C2 -- Email Sent --> D2[User clicks deep link];
D2 --> E2[UI with new password fields];
E2 --> F2{SignInBloc};
F2 --> G2[Firebase Auth: confirmPasswordReset];
G2 -- Success --> H2[Show success message];
G2 -- Failure --> I2[Show error message];
end
subgraph "Event Listing Flow"
A3[User navigates to Events screen] --> B3{EventsBloc};
B3 --> C3[GraphQL Query: getEvents];
C3 --> D3[Firestore: events collection];
D3 -- Returns event data --> C3;
C3 -- Returns data --> B3;
B3 --> E3[Display list of events];
end
subgraph "Create Event Flow"
A4[User submits new event form] --> B4{CreateEventBloc};
B4 --> C4[GraphQL Mutation: createEvent];
C4 --> D4[Firestore: events collection];
D4 -- Success --> C4;
C4 -- Returns success --> B4;
B4 --> E4[Navigate to event details];
end
subgraph "Profile Viewing Flow"
A5[User navigates to Profile screen] --> B5{ProfileBloc};
B5 --> C5[GraphQL Query: getProfile];
C5 --> D5[Firestore: users collection];
D5 -- Returns profile data --> C5;
C5 -- Returns data --> B5;
B5 --> E5[Display profile information];
end

View File

@@ -0,0 +1,57 @@
graph TD
subgraph GraphQL API
subgraph Queries
Q1[getStaffStatus]
Q2[getMe]
Q3[getStaffPersonalInfo]
Q4[getStaffProfileRoles]
Q5[getShifts]
Q6[staffNoBreakShifts]
Q7[getShiftPosition]
end
subgraph Mutations
M1[updateStaffPersonalInfo]
M2[updateStaffPersonalInfoWithAvatar]
M3[uploadStaffAvatar]
M4[acceptShift]
M5[trackStaffClockin]
M6[trackStaffClockout]
M7[trackStaffBreak]
M8[submitNoBreakStaffShift]
M9[cancelStaffShift]
M10[declineShift]
end
end
subgraph Firebase Services
FS[Firebase Storage]
FF[Firebase Firestore]
FA[Firebase Auth]
end
M2 --> FS;
M3 --> FS;
Q1 --> FF;
Q2 --> FF;
Q3 --> FF;
Q4 --> FF;
Q5 --> FF;
Q6 --> FF;
Q7 --> FF;
M1 --> FF;
M2 --> FF;
M4 --> FF;
M5 --> FF;
M6 --> FF;
M7 --> FF;
M8 --> FF;
M9 --> FF;
M10 --> FF;
Q1 --> FA;
Q2 --> FA;
Q3 --> FA;

View File

@@ -0,0 +1,37 @@
graph TD
subgraph Flutter App
A[Flutter UI]
B[GraphQL Client]
C[Firebase SDK]
end
subgraph Backend
D[GraphQL Server]
E[Firebase]
end
subgraph Firebase
F[Firebase Auth]
G[Firebase Firestore]
H[Firebase Storage]
I[Firebase Cloud Functions]
J[Firebase Cloud Messaging]
K[Firebase Remote Config]
end
A --> B;
A --> C;
B --> D;
C --> F;
C --> J;
C --> K;
D --> G;
D --> H;
D --> I;
D --> F;
I --> G;
I --> H;
I --> J;

View File

@@ -0,0 +1,39 @@
graph TD
subgraph User Authentication
direction LR
UA1[Flutter App] -->|Phone Number| UA2[Firebase Auth];
UA2 -->|Verification Code| UA1;
UA1 -->|Verification Code| UA2;
UA2 -->|Auth Token| UA1;
UA1 -->|Auth Token| UA3[GraphQL Server];
UA3 -->|User Data| UA1;
end
subgraph User Onboarding
direction LR
UO1[Flutter App] -->|Personal Info| UO2[GraphQL Server];
UO2 -->|update_staff_personal_info| UO3[Firebase Firestore];
UO2 -->|User Data| UO1;
end
subgraph Shift Management
direction LR
SM1[Flutter App] -->|Get Shifts| SM2[GraphQL Server];
SM2 -->|getShifts| SM3[Firebase Firestore];
SM3 -->|Shift Data| SM2;
SM2 -->|Shift Data| SM1;
SM1 -->|Accept Shift| SM2;
SM2 -->|accept_shift| SM3;
SM3 -->|Updated Shift| SM2;
SM2 -->|Updated Shift| SM1;
end
subgraph Profile Update with Avatar
direction LR
PU1[Flutter App] -->|Image| PU2[Firebase Storage];
PU2 -->|Image URL| PU1;
PU1 -->|Image URL & Personal Info| PU3[GraphQL Server];
PU3 -->|update_staff_personal_info & upload_staff_avatar| PU4[Firebase Firestore];
PU3 -->|User Data| PU1;
end

View File

@@ -0,0 +1,10 @@
[
{
"title": "Architecture Document & Migration Plan",
"path": "./assets/documents/legacy/staff-mobile-application/architecture.md"
},
{
"title": "Architecture Document & Migration Plan",
"path": "./assets/documents/legacy/client-mobile-application/architecture.md"
}
]

View File

@@ -0,0 +1,252 @@
# Krow Mobile Client App Architecture Document
## A. Introduction
This document provides a comprehensive overview of the Krow mobile client application's architecture. The Krow app is a Flutter-based mobile application designed to connect staff with work opportunities. It includes features for event management, invoicing, staff rating, and profile management.
The core purpose of the app is to provide a seamless experience for staff to find, manage, and get paid for work, while allowing clients to manage their events and staff effectively.
## B. Full Architecture Overview
The Krow app is built using a layered architecture that separates concerns and promotes modularity. The main layers are the **Presentation Layer**, the **Domain Layer**, and the **Data Layer**, organized into feature-based modules.
### Key Modules and Layers
* **Features:** The `lib/features` directory contains the main features of the app, such as `sign_in`, `events`, `profile`, etc. Each feature directory is further divided into `presentation` and `domain` layers.
* **Presentation Layer:** This layer is responsible for the UI and user interaction. It contains the screens (widgets) and the BLoCs (Business Logic Components) that manage the state of the UI.
* **Domain Layer:** This layer contains the core business logic of the application. It includes the BLoCs, which are responsible for orchestrating the flow of data between the UI and the data layer, and the business objects (entities).
* **Data Layer:** This layer is responsible for all data-related operations. It includes the repositories that fetch data from the backend and the data sources themselves (e.g., GraphQL API, local cache).
* **Core:** The `lib/core` directory contains shared code that is used across multiple features, such as the API client, dependency injection setup, routing, and common widgets.
### Integration Points
* **UI to Domain:** The UI (widgets) dispatches events to the BLoCs in the domain layer based on user interactions.
* **Domain to Data:** The BLoCs in the domain layer call methods on the repositories in the data layer to fetch or update data.
* **Data to Backend:** The repositories in the data layer use the `ApiClient` to make GraphQL calls to the backend.
## C. Backend Architecture
The backend of the Krow app is a hybrid system that leverages both a **GraphQL server** and **Firebase services**.
```mermaid
flowchart TD
subgraph "Client"
A[Flutter App]
end
subgraph "Backend"
B[GraphQL Server (e.g., Node.js)]
C[Firebase]
end
subgraph "Firebase Services"
C1[Firebase Auth]
C2[Firebase Firestore]
C3[Firebase Storage]
end
A -- "GraphQL Queries/Mutations" --> B
A -- "Authentication" --> C1
B -- "Data Operations" --> C2
B -- "File Operations" --> C3
C1 -- "User Tokens" --> A
C2 -- "Data" --> B
C3 -- "Files" --> B
B -- "Data/Files" --> A
```
### GraphQL
The GraphQL server acts as an intermediary between the Flutter app and the Firebase services. It exposes a set of queries and mutations that the app can use to interact with the backend. This provides a single, unified API for the app to consume, simplifying data fetching and manipulation.
### Firebase Integration
* **Firebase Auth:** Firebase Auth is used for user authentication. The Flutter app interacts directly with Firebase Auth to handle user sign-in, sign-up, and password reset flows. Once authenticated, the app retrieves a Firebase ID token, which is then used to authenticate with the GraphQL server.
* **Firebase Firestore:** Firestore is the primary database for the application. The GraphQL server is responsible for all interactions with Firestore, including fetching, creating, updating, and deleting data.
* **Firebase Storage:** Firebase Storage is used for storing user-generated content, such as profile pictures. The GraphQL server handles file uploads and retrieves file URLs that are then sent to the app.
### End-to-End Communication Flow
1. The Flutter app authenticates the user with Firebase Auth.
2. The app receives a Firebase ID token.
3. For all subsequent API requests, the app sends the Firebase ID token in the authorization header of the GraphQL request.
4. The GraphQL server verifies the token and then executes the requested query or mutation.
5. The GraphQL server interacts with Firestore or Firebase Storage to fulfill the request.
6. The GraphQL server returns the requested data to the app.
## D. API Layer
The API layer is responsible for all communication with the backend. It is built around the `graphql_flutter` package and a custom `ApiClient`.
```mermaid
flowchart TD
subgraph "GraphQL API"
direction LR
subgraph "Queries"
Q1[getEvents]
Q2[getEventDetails]
Q3[getInvoices]
Q4[getInvoiceDetails]
Q5[getNotifications]
Q6[getNotificationDetails]
Q7[getProfile]
Q8[getAssignedStaff]
end
subgraph "Mutations"
M1[createEvent]
M2[updateProfile]
M3[rateStaff]
M4[clockIn]
M5[clockOut]
M6[uploadProfilePicture]
end
end
subgraph "Firebase"
direction LR
subgraph "Firestore Collections"
FS1[events]
FS2[invoices]
FS3[notifications]
FS4[users]
end
subgraph "Firebase Storage"
FB1[Profile Pictures]
end
end
Q1 --> FS1
Q2 --> FS1
Q3 --> FS2
Q4 --> FS2
Q5 --> FS3
Q6 --> FS3
Q7 --> FS4
Q8 --> FS1
Q8 --> FS4
M1 --> FS1
M2 --> FS4
M3 --> FS1
M3 --> FS4
M4 --> FS1
M5 --> FS1
M6 --> FB1
```
### API Handling
* **Error Handling:** The `ApiClient` uses the `ErrorPolicy.all` policy to catch all GraphQL errors. The BLoCs are responsible for catching these errors and updating the UI state accordingly.
* **Caching:** The `GraphQLCache` with `HiveStore` is used to cache GraphQL query results. The `fetchPolicy` is set to `cacheAndNetwork` to provide a fast user experience while keeping the data up-to-date.
* **Parsing:** The app uses the `json_serializable` package to parse the JSON responses from the GraphQL server into Dart objects.
## E. State Management
The Krow app uses the **BLoC (Business Logic Component)** pattern for state management, powered by the `flutter_bloc` package.
### Why BLoC?
* **Separation of Concerns:** BLoC separates the business logic from the UI, making the code more organized, testable, and maintainable.
* **Testability:** BLoCs are easy to test in isolation from the UI.
* **Reactivity:** BLoC uses streams to manage state, which makes it easy to update the UI in response to state changes.
### State Flow
1. The UI dispatches an event to the BLoC.
2. The BLoC receives the event and interacts with the data layer (repositories) to fetch or update data.
3. The data layer returns data or a success/failure status to the BLoC.
4. The BLoC updates its state based on the result from the data layer.
5. The UI rebuilds itself in response to the new state.
### Integration with the API Layer
The BLoCs do not interact directly with the `ApiClient`. Instead, they go through a repository layer, which abstracts the data source. This makes it possible to switch out the backend without having to change the BLoCs.
## F. Use-Case Flows
The following diagrams illustrate the flow for some of the major use cases in the app.
```mermaid
flowchart TD
subgraph "Sign-In Flow"
A1[User enters credentials] --> B1{SignInBloc};
B1 --> C1[Firebase Auth: signInWithEmailAndPassword];
C1 -- Success --> D1[Navigate to Home];
C1 -- Failure --> E1[Show error message];
end
subgraph "Password Reset Flow"
A2[User requests password reset] --> B2{SignInBloc};
B2 --> C2[Firebase Auth: sendPasswordResetEmail];
C2 -- Email Sent --> D2[User clicks deep link];
D2 --> E2[UI with new password fields];
E2 --> F2{SignInBloc};
F2 --> G2[Firebase Auth: confirmPasswordReset];
G2 -- Success --> H2[Show success message];
G2 -- Failure --> I2[Show error message];
end
subgraph "Event Listing Flow"
A3[User navigates to Events screen] --> B3{EventsBloc};
B3 --> C3[GraphQL Query: getEvents];
C3 --> D3[Firestore: events collection];
D3 -- Returns event data --> C3;
C3 -- Returns data --> B3;
B3 --> E3[Display list of events];
end
subgraph "Create Event Flow"
A4[User submits new event form] --> B4{CreateEventBloc};
B4 --> C4[GraphQL Mutation: createEvent];
C4 --> D4[Firestore: events collection];
D4 -- Success --> C4;
C4 -- Returns success --> B4;
B4 --> E4[Navigate to event details];
end
subgraph "Profile Viewing Flow"
A5[User navigates to Profile screen] --> B5{ProfileBloc};
B5 --> C5[GraphQL Query: getProfile];
C5 --> D5[Firestore: users collection];
D5 -- Returns profile data --> C5;
C5 -- Returns data --> B5;
B5 --> E5[Display profile information];
end
```
## G. Replacing or Plugging in a New Backend: Considerations & Recommendations
This section provides guidance on how to replace the current GraphQL + Firebase backend with a different backend solution.
### Tightly Coupled Components
* **`ApiClient`:** This class is tightly coupled to `graphql_flutter`.
* **Firebase Auth:** The authentication logic is directly tied to the `firebase_auth` package.
* **BLoCs:** Some BLoCs might have direct dependencies on Firebase or GraphQL-specific models.
### Abstraction Recommendations
To make the architecture more backend-agnostic, the following abstractions should be implemented:
* **Repositories:** Create an abstract `Repository` class for each feature in the `domain` layer. The implementation of this repository will be in the `data` layer. The BLoCs should only depend on the abstract repository.
* **Authentication Service:** Create an abstract `AuthService` class that defines the methods for authentication (e.g., `signIn`, `signOut`, `getToken`). The implementation of this service will be in the `data` layer and will use the specific authentication provider (e.g., Firebase Auth, OAuth).
* **Data Transfer Objects (DTOs):** Use DTOs to transfer data between the data layer and the domain layer. This will prevent the domain layer from having dependencies on backend-specific models.
### Suggested Design Improvements
* **Formalize Clean Architecture:** While the current architecture has elements of Clean Architecture, it could be more formally implemented by creating a clear separation between the `domain`, `data`, and `presentation` layers for all features.
* **Introduce Use Cases:** Introduce `UseCase` classes in the `domain` layer to encapsulate specific business operations. This will make the BLoCs simpler and more focused on state management.
### Migration Strategies
To replace the current backend with a new one (e.g., REST API, Supabase), follow these steps:
1. **Implement New Repositories:** Create new implementations of the repository interfaces for the new backend.
2. **Implement New Auth Service:** Create a new implementation of the `AuthService` interface for the new authentication provider.
3. **Update Dependency Injection:** Use dependency injection (e.g., `get_it` and `injectable`) to provide the new repository and auth service implementations to the BLoCs.
4. **Gradual Migration:** If possible, migrate one feature at a time to the new backend. This will reduce the risk of breaking the entire application at once.

View File

@@ -0,0 +1,120 @@
# Krow Mobile Staff App - Architecture Document
## A. Introduction
This document outlines the architecture of the Krow Mobile Staff App, a Flutter application designed to connect staff with job opportunities. The app provides features for staff to manage their profiles, view and apply for shifts, track earnings, and complete necessary paperwork.
The core purpose of the app is to streamline the process of finding and managing temporary work, providing a seamless experience for staff from onboarding to payment.
## B. Full Architecture Overview
The application follows a **Clean Architecture** pattern, separating concerns into three main layers: **Presentation**, **Domain**, and **Data**. This layered approach promotes a separation of concerns, making the codebase more maintainable, scalable, and testable.
- **Presentation Layer:** This layer is responsible for the UI and user interaction. It consists of widgets, screens, and Blocs that manage the UI state. The Presentation Layer depends on the Domain Layer to execute business logic.
- **Domain Layer:** This layer contains the core business logic of the application. It consists of use cases (interactors), entities (business objects), and repository interfaces. The Domain Layer is independent of the other layers.
- **Data Layer:** This layer is responsible for data retrieval and storage. It consists of repository implementations, data sources (API clients, local database), and data transfer objects (DTOs). The Data Layer depends on the Domain Layer and implements the repository interfaces defined in it.
### Integration Points
- **UI → Domain:** The UI (e.g., a button press) triggers a method in a Bloc. The Bloc then calls a use case in the Domain Layer to execute the business logic.
- **Domain → Data:** The use case calls a method on a repository interface.
- **Data → External:** The repository implementation, located in the Data Layer, communicates with external data sources (GraphQL API, Firebase, local storage) to retrieve or store data.
## C. Backend Architecture
The backend is built on a combination of a **GraphQL server** and **Firebase services**.
- **GraphQL Server:** The primary endpoint for the Flutter app. It handles most of the business logic and data aggregation. The server is responsible for communicating with Firebase services to fulfill requests.
- **Firebase Services:**
- **Firebase Auth:** Used for user authentication, primarily with phone number verification.
- **Firebase Firestore:** The main database for storing application data, such as user profiles, shifts, and earnings.
- **Firebase Storage:** Used for storing user-generated content, such as profile avatars.
- **Firebase Cloud Messaging:** Used for sending push notifications to users.
- **Firebase Remote Config:** Used for remotely configuring app parameters.
### API Flow
1. **Flutter App to GraphQL:** The Flutter app sends GraphQL queries and mutations to the GraphQL server.
2. **GraphQL to Firebase:** The GraphQL server resolves these operations by interacting with Firebase services. For example, a `getShifts` query will fetch data from Firestore, and an `updateStaffPersonalInfoWithAvatar` mutation will update a document in Firestore and upload a file to Firebase Storage.
3. **Response Flow:** The data flows back from Firebase to the GraphQL server, which then sends it back to the Flutter app.
## D. API Layer
The API layer is responsible for all communication with the backend.
- **GraphQL Operations:** The app uses the `graphql_flutter` package to interact with the GraphQL server. Queries, mutations, and subscriptions are defined in `.dart` files within each feature's `data` directory.
- **API Error Handling:** The `ApiClient` class is responsible for handling API errors. It catches exceptions and returns a `Failure` object, which is then handled by the Bloc in the Presentation Layer to show an appropriate error message to the user.
- **Caching:** The `graphql_flutter` client provides caching capabilities. The app uses a `HiveStore` to cache GraphQL responses, reducing the number of network requests and improving performance.
- **Parsing:** JSON responses from the API are parsed into Dart objects using the `json_serializable` package.
## E. State Management
The application uses the **Bloc** library for state management.
- **Why Bloc?** Bloc is a predictable state management library that helps to separate business logic from the UI. It enforces a unidirectional data flow, making the app's state changes predictable and easier to debug.
- **State Flow:**
1. **UI Event:** The UI dispatches an event to the Bloc.
2. **Bloc Logic:** The Bloc receives the event, executes the necessary business logic (often by calling a use case), and emits a new state.
3. **UI Update:** The UI listens to the Bloc's state changes and rebuilds itself to reflect the new state.
- **Integration with API Layer:** Blocs interact with the API layer through use cases. When a Bloc needs to fetch data from the backend, it calls a use case, which in turn calls a repository that communicates with the API.
## F. Use-Case Flows
### User Authentication
1. **UI:** The user enters their phone number.
2. **Logic:** The `AuthBloc` sends the phone number to Firebase Auth for verification.
3. **Backend:** Firebase Auth sends a verification code to the user's phone.
4. **UI:** The user enters the verification code.
5. **Logic:** The `AuthBloc` verifies the code with Firebase Auth.
6. **Backend:** Firebase Auth returns an auth token.
7. **Logic:** The app sends the auth token to the GraphQL server to get the user's profile.
8. **Response:** The GraphQL server returns the user's data, and the app navigates to the home screen.
### Shift Management
1. **UI:** The user navigates to the shifts screen.
2. **Logic:** The `ShiftsBloc` requests a list of shifts.
3. **Backend:** The use case calls the `ShiftsRepository`, which sends a `getShifts` query to the GraphQL server. The server fetches the shifts from Firestore.
4. **Response:** The GraphQL server returns the list of shifts, which is then displayed on the UI.
## G. Replacing or Plugging in a New Backend: Considerations & Recommendations
This section provides guidance on how to replace the current GraphQL + Firebase backend with a different solution (e.g., REST, Supabase, Hasura).
### Tightly Coupled Components
- **Data Layer:** The current `ApiProvider` implementations are tightly coupled to the GraphQL API.
- **Authentication:** The authentication flow is tightly coupled to Firebase Auth.
- **DTOs:** The data transfer objects are generated based on the GraphQL schema.
### Abstraction Recommendations
To make the architecture more backend-agnostic, the following components should be abstracted:
- **Repositories:** The repository interfaces in the Domain Layer should remain unchanged. The implementations in the Data Layer will need to be rewritten for the new backend.
- **Services:** Services like authentication should be abstracted behind an interface. For example, an `AuthService` interface can be defined in the Domain Layer, with a `FirebaseAuthService` implementation in the Data Layer.
- **DTOs:** The DTOs should be mapped to domain entities in the Data Layer. This ensures that the Domain Layer is not affected by changes in the backend's data model.
- **Error Handling:** A generic error handling mechanism should be implemented to handle different types of backend errors.
### Suggested Design Improvements
- **Introduce a Service Locator:** Use a service locator like `get_it` to decouple the layers and make it easier to swap out implementations.
- **Define Abstract Data Sources:** Instead of directly calling the API client in the repository implementations, introduce abstract data source interfaces (e.g., `UserRemoteDataSource`). This adds another layer of abstraction and makes the repositories more testable.
### Migration Strategies
1. **Define Interfaces:** Start by defining abstract interfaces for all backend interactions (repositories, services).
2. **Implement New Data Layer:** Create a new implementation of the Data Layer for the new backend. This will involve writing new repository implementations, API clients, and DTOs.
3. **Swap Implementations:** Use the service locator to swap the old Data Layer implementation with the new one.
4. **Test:** Thoroughly test the application to ensure that everything works as expected with the new backend.
By following these recommendations, the Krow Mobile Staff App can be migrated to a new backend with minimal impact on the overall architecture and business logic.

View File

@@ -0,0 +1,9 @@
# List of authorized users for the Internal Launchpad
# Format: one email per line, lines starting with # are comments
#
# IMPORTANT: These users must belong to the 'krowwithus.com' organization.
# This is a known limitation of enabling IAP directly on Cloud Run.
# See: https://docs.cloud.google.com/run/docs/securing/identity-aware-proxy-cloud-run#known_limitations
user:admin@krowwithus.com
# user:boris@oloodi.com # External users are not supported with this IAP method

View File

@@ -13,6 +13,9 @@
<!-- Mermaid -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/mermaid/10.9.1/mermaid.min.js"></script>
<!-- Marked.js for Markdown parsing -->
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<!-- Custom Tailwind Config -->
<script>
tailwind.config = {
@@ -90,6 +93,170 @@
#diagram-container:active {
cursor: grabbing;
}
/* Modal styles */
.modal-overlay {
backdrop-filter: blur(4px);
animation: fadeIn 0.2s ease-out;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.modal-content {
animation: slideUp 0.3s ease-out;
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Markdown styling */
.markdown-content {
line-height: 1.7;
color: #374151;
}
.markdown-content h1 {
font-size: 2em;
font-weight: 700;
margin-top: 1.5em;
margin-bottom: 0.5em;
padding-bottom: 0.3em;
border-bottom: 2px solid #e5e7eb;
color: #111827;
}
.markdown-content h1:first-child {
margin-top: 0;
}
.markdown-content h2 {
font-size: 1.5em;
font-weight: 600;
margin-top: 1.5em;
margin-bottom: 0.5em;
padding-bottom: 0.2em;
border-bottom: 1px solid #e5e7eb;
color: #111827;
}
.markdown-content h3 {
font-size: 1.25em;
font-weight: 600;
margin-top: 1.2em;
margin-bottom: 0.5em;
color: #111827;
}
.markdown-content h4 {
font-size: 1.1em;
font-weight: 600;
margin-top: 1em;
margin-bottom: 0.5em;
color: #111827;
}
.markdown-content p {
margin-bottom: 1em;
}
.markdown-content ul, .markdown-content ol {
margin-bottom: 1em;
padding-left: 2em;
}
.markdown-content ul {
list-style-type: disc;
}
.markdown-content ol {
list-style-type: decimal;
}
.markdown-content li {
margin-bottom: 0.5em;
}
.markdown-content code {
background-color: #f3f4f6;
padding: 0.2em 0.4em;
border-radius: 0.25em;
font-size: 0.9em;
font-family: 'Courier New', monospace;
color: #dc2626;
}
.markdown-content pre {
background-color: #1f2937;
color: #f9fafb;
padding: 1em;
border-radius: 0.5em;
overflow-x: auto;
margin-bottom: 1em;
}
.markdown-content pre code {
background-color: transparent;
padding: 0;
color: inherit;
}
.markdown-content blockquote {
border-left: 4px solid #3b82f6;
padding-left: 1em;
margin: 1em 0;
color: #6b7280;
font-style: italic;
}
.markdown-content a {
color: #3b82f6;
text-decoration: underline;
}
.markdown-content a:hover {
color: #2563eb;
}
.markdown-content table {
width: 100%;
border-collapse: collapse;
margin-bottom: 1em;
}
.markdown-content th, .markdown-content td {
border: 1px solid #e5e7eb;
padding: 0.5em;
text-align: left;
}
.markdown-content th {
background-color: #f9fafb;
font-weight: 600;
}
.markdown-content img {
max-width: 100%;
height: auto;
border-radius: 0.5em;
margin: 1em 0;
}
.markdown-content hr {
border: none;
border-top: 2px solid #e5e7eb;
margin: 2em 0;
}
</style>
</head>
@@ -106,7 +273,7 @@
</div>
<div>
<h1 class="text-xl font-bold text-gray-900">KROW</h1>
<p class="text-xs text-gray-500">Workforce Platform</p>
<p class="text-xs text-gray-500">Launchpad</p>
</div>
</div>
</div>
@@ -123,6 +290,9 @@
<!-- Dynamic diagrams section - ALL diagrams loaded here -->
<div id="dynamic-diagrams-section"></div>
<!-- Documentation section -->
<div id="documentation-section"></div>
</nav>
<!-- Footer -->
@@ -350,6 +520,17 @@
</div>
</div>
<!-- Document Viewer -->
<div id="document-viewer" class="hidden h-full flex flex-col p-8">
<div class="mb-6">
<h3 id="document-title" class="text-2xl font-bold text-gray-900"></h3>
</div>
<div id="document-container"
class="flex-1 bg-white rounded-2xl shadow-xl border border-gray-200 overflow-y-auto p-8 markdown-content">
<!-- Document content will be loaded here -->
</div>
</div>
</main>
</div>
@@ -358,10 +539,14 @@
<script>
let allDiagrams = [];
let allDocuments = [];
const homeView = document.getElementById('home-view');
const diagramViewer = document.getElementById('diagram-viewer');
const diagramContainer = document.getElementById('diagram-container');
const diagramTitle = document.getElementById('diagram-title');
const documentViewer = document.getElementById('document-viewer');
const documentContainer = document.getElementById('document-container');
const documentTitle = document.getElementById('document-title');
const zoomInBtn = document.getElementById('zoomInBtn');
const zoomOutBtn = document.getElementById('zoomOutBtn');
const resetBtn = document.getElementById('resetBtn');
@@ -415,6 +600,41 @@
return hierarchy;
}
// Build hierarchical structure from paths (for documents)
function buildDocumentHierarchy(documents) {
const hierarchy = {};
documents.forEach(doc => {
const parts = doc.path.split('/');
const relevantParts = parts.slice(2, -1); // Remove 'assets/documents/' and filename
let current = hierarchy;
relevantParts.forEach(part => {
if (!current[part]) {
current[part] = { _items: [], _children: {} };
}
current = current[part]._children;
});
// Add the item to appropriate level
if (relevantParts.length > 0) {
let parent = hierarchy[relevantParts[0]];
for (let i = 1; i < relevantParts.length; i++) {
parent = parent._children[relevantParts[i]];
}
parent._items.push(doc);
} else {
// Root level documents
if (!hierarchy._root) {
hierarchy._root = { _items: [], _children: {} };
}
hierarchy._root._items.push(doc);
}
});
return hierarchy;
}
// Create navigation from hierarchy
function createNavigation(hierarchy, parentElement, level = 0) {
// First, show root level items if any
@@ -454,6 +674,64 @@
});
}
// Create document navigation from hierarchy
function createDocumentNavigation(hierarchy, parentElement, level = 0) {
// First, show root level items if any
if (hierarchy._root && hierarchy._root._items.length > 0) {
const mainHeading = document.createElement('div');
mainHeading.className = 'px-4 text-xs font-semibold text-gray-500 uppercase tracking-wider mt-6 mb-3';
mainHeading.textContent = 'Documentation';
parentElement.appendChild(mainHeading);
hierarchy._root._items.forEach(doc => {
createDocumentLink(doc, parentElement, 0);
});
}
// Then process nested categories
Object.keys(hierarchy).forEach(key => {
if (key === '_items' || key === '_children' || key === '_root') return;
const section = hierarchy[key];
const heading = document.createElement('div');
heading.className = 'px-4 text-xs font-semibold text-gray-500 uppercase tracking-wider ' +
(level === 0 ? 'mt-6 mb-3' : 'mt-4 mb-2 pl-8');
heading.textContent = key.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
parentElement.appendChild(heading);
// Add items in this section
if (section._items && section._items.length > 0) {
section._items.forEach(doc => {
createDocumentLink(doc, parentElement, level);
});
}
// Recursively add children
if (section._children && Object.keys(section._children).length > 0) {
createDocumentNavigation(section._children, parentElement, level + 1);
}
});
}
// Helper function to create a document link
function createDocumentLink(doc, parentElement, level) {
const link = document.createElement('a');
link.href = '#';
link.className = 'nav-item flex items-center space-x-3 px-4 py-3 rounded-xl text-gray-700 hover:bg-gray-50 mb-1' +
(level > 0 ? ' pl-8' : '');
link.onclick = (e) => {
e.preventDefault();
showView('document', link, doc.path, doc.title);
};
const iconSvg = `<svg class="w-5 h-5 text-gray-400 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
</svg>`;
link.innerHTML = `${iconSvg}<span class="text-sm">${doc.title}</span>`;
parentElement.appendChild(link);
}
// Helper function to create a diagram link
function createDiagramLink(diagram, parentElement, level) {
const link = document.createElement('a');
@@ -477,7 +755,7 @@
</svg>`;
}
link.innerHTML = `${iconSvg}<span class="text-sm">${diagram.title}</span>`;
link.innerHTML = iconSvg + '<span class="text-sm">' + diagram.title + '</span>';
parentElement.appendChild(link);
}
@@ -501,7 +779,6 @@
}
} catch (error) {
console.error('Error loading diagrams configuration:', error);
// Show a helpful message in the UI
const errorDiv = document.createElement('div');
errorDiv.className = 'px-4 py-3 mx-2 mt-4 text-xs text-amber-600 bg-amber-50 rounded-lg border border-amber-200';
errorDiv.innerHTML = `
@@ -512,9 +789,40 @@
dynamicSection.appendChild(errorDiv);
}
}
// Load all documentation from config
async function loadAllDocuments() {
const documentationSection = document.getElementById('documentation-section');
try {
const response = await fetch('./assets/documents/documents-config.json');
if (!response.ok) {
throw new Error(`Failed to load documents config: ${response.status}`);
}
const text = await response.text();
console.log('Loaded documents config:', text);
allDocuments = JSON.parse(text);
if (allDocuments && allDocuments.length > 0) {
const hierarchy = buildDocumentHierarchy(allDocuments);
createDocumentNavigation(hierarchy, documentationSection);
}
} catch (error) {
console.error('Error loading documents configuration:', error);
const errorDiv = document.createElement('div');
errorDiv.className = 'px-4 py-3 mx-2 mt-4 text-xs text-amber-600 bg-amber-50 rounded-lg border border-amber-200';
errorDiv.innerHTML = `
<div class="font-semibold mb-1">⚠️ Documentation</div>
<div>Unable to load documents-config.json</div>
<div class="mt-1 text-amber-500">${error.message}</div>
`;
documentationSection.appendChild(errorDiv);
}
}
function setActiveNav(activeLink) {
document.querySelectorAll('.sidebar a').forEach(link => {
document.querySelectorAll('#sidebar-nav a').forEach(link => {
link.classList.remove('bg-primary-50', 'border', 'border-primary-200', 'text-primary-700');
link.classList.add('text-gray-700');
});
@@ -529,14 +837,17 @@
panzoomInstance = null;
}
diagramContainer.innerHTML = '';
documentContainer.innerHTML = '';
currentScale = 1;
if (viewName === 'home') {
homeView.classList.remove('hidden');
diagramViewer.classList.add('hidden');
documentViewer.classList.add('hidden');
} else if (viewName === 'diagram') {
homeView.classList.add('hidden');
diagramViewer.classList.remove('hidden');
documentViewer.classList.add('hidden');
diagramTitle.textContent = title;
diagramContainer.innerHTML = `
<div class="flex flex-col items-center space-y-3">
@@ -608,6 +919,41 @@
</div>
`;
}
} else if (viewName === 'document') {
homeView.classList.add('hidden');
diagramViewer.classList.add('hidden');
documentViewer.classList.remove('hidden');
documentTitle.textContent = title;
documentContainer.innerHTML = `
<div class="flex flex-col items-center justify-center space-y-3 py-12">
<div class="w-12 h-12 border-4 border-primary-200 border-t-primary-600 rounded-full animate-spin"></div>
<p class="text-gray-600 font-medium">Loading document...</p>
</div>
`;
try {
const response = await fetch(filePath);
if (!response.ok) {
throw new Error(`Failed to load document: ${response.status}`);
}
const markdownText = await response.text();
const htmlContent = marked.parse(markdownText);
documentContainer.innerHTML = htmlContent;
} catch (error) {
console.error('Error loading document:', error);
documentContainer.innerHTML = `
<div class="bg-red-50 border border-red-200 rounded-xl p-6">
<div class="flex items-center space-x-3 mb-2">
<svg class="w-6 h-6 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<h4 class="font-bold text-red-900">Failed to load document</h4>
</div>
<p class="text-red-700 text-sm">${error.message}</p>
</div>
`;
}
}
}
@@ -634,6 +980,7 @@
document.addEventListener('DOMContentLoaded', () => {
loadAllDiagrams();
loadAllDocuments();
showView('home', document.getElementById('nav-home'));
});
</script>