Merge branch '208-p0-auth-05-get-started-screen' of github.com:Oloodi/krow-workforce into 208-p0-auth-05-get-started-screen

This commit is contained in:
José Salazar
2026-01-22 16:03:35 -05:00
73 changed files with 22009 additions and 20426 deletions

1
apps/mobile-client/.keep Normal file
View File

@@ -0,0 +1 @@

1
apps/mobile-staff/.keep Normal file
View File

@@ -0,0 +1 @@

68
apps/mobile/README.md Normal file
View File

@@ -0,0 +1,68 @@
# KROW Workforce Mobile 📱
This folder holds the mobile app code for the KROW Workforce apps.
This project uses [Melos](https://melos.invertase.dev/) to manage multiple Flutter packages and applications.
## 📂 Project Structure
The project is organized into modular packages to ensure separation of concerns and maintainability.
- **`apps/`**: Main application entry points.
- `client`: The application for businesses/clients.
- `staff`: The application for workforce/staff.
- `design_system_viewer`: A gallery of our design system components.
- **`packages/`**: Shared logic and feature modules.
- `features/`: UI and business logic for specific features (e.g., Auth, Home, Hubs).
- `features/client`: Client specific features.
- `features/staff`: Staff specific features.
- `design_system/`: Shared UI components, tokens (colors, spacing), and core widgets.
- `domain/`: Shared business entities and repository interfaces.
- `data_connect/`: Data access layer (Mocks and Firebase Data Connect SDK).
- `core_localization/`: Internationalization using Slang.
- `core/`: Base utilities and common logic.
## 🚀 Getting Started
### 1. Prerequisites
Ensure you have the Flutter SDK installed and configured.
### 2. Initial Setup
Run the following command from the **project root** to install Melos, bootstrap all packages, and generate localization files:
```bash
# Using Makefile
make mobile-install
# Using Melos
melos bootstrap
```
### 3. Running the Apps
You can run the applications using Melos scripts or through the `Makefile`:
#### Client App
```bash
# Using Melos
melos run start:client -d android # or ios
# Using Makefile
make mobile-client-dev-android
```
#### Staff App
```bash
# Using Melos
melos run start:staff -d android # or ios
# Using Makefile
make mobile-staff-dev-android
```
## 🛠 Useful Commands
- **Bootstrap**: `melos bootstrap` (Installs all dependencies)
- **Generate All**: `melos run gen:all` (Localization + Code Generation)
- **Analyze**: `melos run analyze:all`
- **Help**: `melos run info` (Shows all available custom scripts)
## 🏗 Coding Principles
- **Clean Architecture**: We strictly follow Domain-Driven Design and Clean Architecture.
- **Modularity**: Every feature should be its own package in `packages/features/`. Client and staff specific features should be in their respective packages.
- **Consistency**: Use the `design_system` package for all UI elements to ensure a premium, unified look.

View File

@@ -0,0 +1,26 @@
include: package:flutter_lints/flutter.yaml
analyzer:
exclude:
- "**/dataconnect_generated/**"
- "**/*.g.dart"
- "**/*.freezed.dart"
- "**/*.config.dart"
errors:
# Set the severity of the always_specify_types rule to warning as requested.
always_specify_types: warning
linter:
rules:
# Every variable should have an explicit type.
- always_specify_types
# Additional common best practices not always enforced by default
- prefer_const_constructors
- prefer_const_declarations
- prefer_final_locals
- avoid_void_async
- unawaited_futures
- sort_constructors_first
- camel_case_types
- library_private_types_in_public_api

View File

@@ -1 +1 @@
include: ../../analytics_options.yaml
include: ../../analysis_options.yaml

View File

@@ -9,6 +9,8 @@ import 'package:client_authentication/client_authentication.dart'
import 'package:client_home/client_home.dart' as client_home;
import 'package:client_settings/client_settings.dart' as client_settings;
import 'package:client_hubs/client_hubs.dart' as client_hubs;
import 'package:client_create_order/client_create_order.dart'
as client_create_order;
import 'package:firebase_core/firebase_core.dart';
void main() async {
@@ -20,10 +22,10 @@ void main() async {
/// The main application module for the Client app.
class AppModule extends Module {
@override
List<Module> get imports => [core_localization.LocalizationModule()];
List<Module> get imports => <Module>[core_localization.LocalizationModule()];
@override
void routes(r) {
void routes(RouteManager r) {
// Initial route points to the client authentication flow
r.module('/', module: client_authentication.ClientAuthenticationModule());
@@ -38,6 +40,12 @@ class AppModule extends Module {
// Client hubs route
r.module('/client-hubs', module: client_hubs.ClientHubsModule());
// Client create order route
r.module(
'/client/create-order',
module: client_create_order.ClientCreateOrderModule(),
);
}
}
@@ -47,7 +55,7 @@ class AppWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider<core_localization.LocaleBloc>(
create: (context) =>
create: (BuildContext context) =>
Modular.get<core_localization.LocaleBloc>()
..add(const core_localization.LoadLocale()),
child:
@@ -55,23 +63,25 @@ class AppWidget extends StatelessWidget {
core_localization.LocaleBloc,
core_localization.LocaleState
>(
builder: (context, state) {
return core_localization.TranslationProvider(
child: MaterialApp.router(
debugShowCheckedModeBanner: false,
title: "Krow Client",
theme: UiTheme.light,
routerConfig: Modular.routerConfig,
locale: state.locale,
supportedLocales: state.supportedLocales,
localizationsDelegates: const [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
),
);
},
builder:
(BuildContext context, core_localization.LocaleState state) {
return core_localization.TranslationProvider(
child: MaterialApp.router(
debugShowCheckedModeBanner: false,
title: "Krow Client",
theme: UiTheme.light,
routerConfig: Modular.routerConfig,
locale: state.locale,
supportedLocales: state.supportedLocales,
localizationsDelegates:
const <LocalizationsDelegate<dynamic>>[
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
),
);
},
),
);
}

View File

@@ -10,23 +10,31 @@ environment:
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.8
# Architecture Packages
design_system:
path: ../../packages/design_system
core_localization:
path: ../../packages/core_localization
# Feature Packages
client_authentication:
path: ../../packages/features/client/authentication
client_home:
path: ../../packages/features/client/home
client_settings:
path: ../../packages/features/client/settings
core_localization:
path: ../../packages/core_localization
client_hubs:
path: ../../packages/features/client/hubs
client_create_order:
path: ../../packages/features/client/create_order
cupertino_icons: ^1.0.8
flutter_modular: ^6.3.2
flutter_bloc: ^8.1.3
flutter_localizations:
sdk: flutter
firebase_core: ^4.4.0
dev_dependencies:
flutter_test:

View File

@@ -1 +1 @@
include: ../../analytics_options.yaml
include: ../../analysis_options.yaml

View File

@@ -1 +1 @@
include: ../../analytics_options.yaml
include: ../../analysis_options.yaml

View File

@@ -13,12 +13,16 @@ dependencies:
sdk: flutter
cupertino_icons: ^1.0.8
flutter_modular: ^6.3.0
# Architecture Packages
design_system:
path: ../../packages/design_system
staff_authentication:
path: ../../packages/features/staff/authentication
core_localization:
path: ../../packages/core_localization
# Feature Packages
staff_authentication:
path: ../../packages/features/staff/authentication
dev_dependencies:
flutter_test:

View File

@@ -27,9 +27,16 @@ scripts:
echo " CODE GENERATION:"
echo " - melos run gen:l10n : Generate Slang l10n"
echo " - melos run gen:build : Run build_runner"
echo " - melos run gen:all : Run l10n and build_runner"
echo "============================================================"
description: "Display information about available custom Melos commands."
gen:all:
run: |
melos run gen:l10n
melos run gen:build
description: "Run both localization and build_runner generation across all packages."
gen:l10n:
exec: dart run slang
description: "Generate localization files using Slang across all packages."

View File

@@ -1,4 +1 @@
include: package:flutter_lints/flutter.yaml
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options
include: ../../analysis_options.yaml

View File

@@ -232,6 +232,20 @@
"tag_identified": "Tag Identified",
"assign_button": "Assign Tag"
}
},
"client_create_order": {
"title": "Create Order",
"section_title": "ORDER TYPE",
"types": {
"rapid": "RAPID",
"rapid_desc": "URGENT same-day Coverage",
"one_time": "One-Time",
"one_time_desc": "Single Event or Shift Request",
"recurring": "Recurring",
"recurring_desc": "Ongoing Weekly / Monthly Coverage",
"permanent": "Permanent",
"permanent_desc": "Long-Term Staffing Placement"
}
}
}

View File

@@ -232,5 +232,19 @@
"tag_identified": "Etiqueta Identificada",
"assign_button": "Asignar Etiqueta"
}
},
"client_create_order": {
"title": "Crear Orden",
"section_title": "TIPO DE ORDEN",
"types": {
"rapid": "RÁPIDO",
"rapid_desc": "Cobertura URGENTE mismo día",
"one_time": "Única Vez",
"one_time_desc": "Evento Único o Petición de Turno",
"recurring": "Recurrente",
"recurring_desc": "Cobertura Continua Semanal / Mensual",
"permanent": "Permanente",
"permanent_desc": "Colocación de Personal a Largo Plazo"
}
}
}

View File

@@ -1,16 +1,16 @@
# Basic Usage
```dart
ExampleConnector.instance.createDocument(createDocumentVariables).execute();
ExampleConnector.instance.updateDocument(updateDocumentVariables).execute();
ExampleConnector.instance.deleteDocument(deleteDocumentVariables).execute();
ExampleConnector.instance.createConversation(createConversationVariables).execute();
ExampleConnector.instance.updateConversation(updateConversationVariables).execute();
ExampleConnector.instance.updateConversationLastMessage(updateConversationLastMessageVariables).execute();
ExampleConnector.instance.deleteConversation(deleteConversationVariables).execute();
ExampleConnector.instance.listHubs().execute();
ExampleConnector.instance.getHubById(getHubByIdVariables).execute();
ExampleConnector.instance.getHubsByOwnerId(getHubsByOwnerIdVariables).execute();
ExampleConnector.instance.createTeamHudDepartment(createTeamHudDepartmentVariables).execute();
ExampleConnector.instance.updateTeamHudDepartment(updateTeamHudDepartmentVariables).execute();
ExampleConnector.instance.deleteTeamHudDepartment(deleteTeamHudDepartmentVariables).execute();
ExampleConnector.instance.listAssignments(listAssignmentsVariables).execute();
ExampleConnector.instance.getAssignmentById(getAssignmentByIdVariables).execute();
ExampleConnector.instance.listAssignmentsByWorkforceId(listAssignmentsByWorkforceIdVariables).execute();
ExampleConnector.instance.listAssignmentsByWorkforceIds(listAssignmentsByWorkforceIdsVariables).execute();
ExampleConnector.instance.listAssignmentsByShiftRole(listAssignmentsByShiftRoleVariables).execute();
ExampleConnector.instance.filterAssignments(filterAssignmentsVariables).execute();
ExampleConnector.instance.CreateCertificate(createCertificateVariables).execute();
```
@@ -23,8 +23,8 @@ Optional fields can be discovered based on classes that have `Optional` object t
This is an example of a mutation with an optional field:
```dart
await ExampleConnector.instance.filterVendorBenefitPlans({ ... })
.vendorId(...)
await ExampleConnector.instance.updateShift({ ... })
.title(...)
.execute();
```

View File

@@ -4,14 +4,30 @@ class CreateTeamHubVariablesBuilder {
String teamId;
String hubName;
String address;
String city;
String state;
String zipCode;
String managerName;
Optional<String> _city = Optional.optional(nativeFromJson, nativeToJson);
Optional<String> _state = Optional.optional(nativeFromJson, nativeToJson);
Optional<String> _zipCode = Optional.optional(nativeFromJson, nativeToJson);
Optional<String> _managerName = Optional.optional(nativeFromJson, nativeToJson);
Optional<bool> _isActive = Optional.optional(nativeFromJson, nativeToJson);
Optional<AnyValue> _departments = Optional.optional(AnyValue.fromJson, defaultSerializer);
final FirebaseDataConnect _dataConnect; CreateTeamHubVariablesBuilder isActive(bool? t) {
final FirebaseDataConnect _dataConnect; CreateTeamHubVariablesBuilder city(String? t) {
_city.value = t;
return this;
}
CreateTeamHubVariablesBuilder state(String? t) {
_state.value = t;
return this;
}
CreateTeamHubVariablesBuilder zipCode(String? t) {
_zipCode.value = t;
return this;
}
CreateTeamHubVariablesBuilder managerName(String? t) {
_managerName.value = t;
return this;
}
CreateTeamHubVariablesBuilder isActive(bool? t) {
_isActive.value = t;
return this;
}
@@ -20,7 +36,7 @@ class CreateTeamHubVariablesBuilder {
return this;
}
CreateTeamHubVariablesBuilder(this._dataConnect, {required this.teamId,required this.hubName,required this.address,required this.city,required this.state,required this.zipCode,required this.managerName,});
CreateTeamHubVariablesBuilder(this._dataConnect, {required this.teamId,required this.hubName,required this.address,});
Deserializer<CreateTeamHubData> dataDeserializer = (dynamic json) => CreateTeamHubData.fromJson(jsonDecode(json));
Serializer<CreateTeamHubVariables> varsSerializer = (CreateTeamHubVariables vars) => jsonEncode(vars.toJson());
Future<OperationResult<CreateTeamHubData, CreateTeamHubVariables>> execute() {
@@ -28,7 +44,7 @@ class CreateTeamHubVariablesBuilder {
}
MutationRef<CreateTeamHubData, CreateTeamHubVariables> ref() {
CreateTeamHubVariables vars= CreateTeamHubVariables(teamId: teamId,hubName: hubName,address: address,city: city,state: state,zipCode: zipCode,managerName: managerName,isActive: _isActive,departments: _departments,);
CreateTeamHubVariables vars= CreateTeamHubVariables(teamId: teamId,hubName: hubName,address: address,city: _city,state: _state,zipCode: _zipCode,managerName: _managerName,isActive: _isActive,departments: _departments,);
return _dataConnect.mutation("createTeamHub", dataDeserializer, varsSerializer, vars);
}
}
@@ -106,10 +122,10 @@ class CreateTeamHubVariables {
final String teamId;
final String hubName;
final String address;
final String city;
final String state;
final String zipCode;
final String managerName;
late final Optional<String>city;
late final Optional<String>state;
late final Optional<String>zipCode;
late final Optional<String>managerName;
late final Optional<bool>isActive;
late final Optional<AnyValue>departments;
@Deprecated('fromJson is deprecated for Variable classes as they are no longer required for deserialization.')
@@ -117,18 +133,26 @@ class CreateTeamHubVariables {
teamId = nativeFromJson<String>(json['teamId']),
hubName = nativeFromJson<String>(json['hubName']),
address = nativeFromJson<String>(json['address']),
city = nativeFromJson<String>(json['city']),
state = nativeFromJson<String>(json['state']),
zipCode = nativeFromJson<String>(json['zipCode']),
managerName = nativeFromJson<String>(json['managerName']) {
address = nativeFromJson<String>(json['address']) {
city = Optional.optional(nativeFromJson, nativeToJson);
city.value = json['city'] == null ? null : nativeFromJson<String>(json['city']);
state = Optional.optional(nativeFromJson, nativeToJson);
state.value = json['state'] == null ? null : nativeFromJson<String>(json['state']);
zipCode = Optional.optional(nativeFromJson, nativeToJson);
zipCode.value = json['zipCode'] == null ? null : nativeFromJson<String>(json['zipCode']);
managerName = Optional.optional(nativeFromJson, nativeToJson);
managerName.value = json['managerName'] == null ? null : nativeFromJson<String>(json['managerName']);
isActive = Optional.optional(nativeFromJson, nativeToJson);
@@ -169,10 +193,18 @@ class CreateTeamHubVariables {
json['teamId'] = nativeToJson<String>(teamId);
json['hubName'] = nativeToJson<String>(hubName);
json['address'] = nativeToJson<String>(address);
json['city'] = nativeToJson<String>(city);
json['state'] = nativeToJson<String>(state);
json['zipCode'] = nativeToJson<String>(zipCode);
json['managerName'] = nativeToJson<String>(managerName);
if(city.state == OptionalState.set) {
json['city'] = city.toJson();
}
if(state.state == OptionalState.set) {
json['state'] = state.toJson();
}
if(zipCode.state == OptionalState.set) {
json['zipCode'] = zipCode.toJson();
}
if(managerName.state == OptionalState.set) {
json['managerName'] = managerName.toJson();
}
if(isActive.state == OptionalState.set) {
json['isActive'] = isActive.toJson();
}

View File

@@ -23,10 +23,10 @@ class GetTeamHubByIdTeamHub {
final String teamId;
final String hubName;
final String address;
final String city;
final String state;
final String zipCode;
final String managerName;
final String? city;
final String? state;
final String? zipCode;
final String? managerName;
final bool isActive;
final AnyValue? departments;
final Timestamp? createdAt;
@@ -38,10 +38,10 @@ class GetTeamHubByIdTeamHub {
teamId = nativeFromJson<String>(json['teamId']),
hubName = nativeFromJson<String>(json['hubName']),
address = nativeFromJson<String>(json['address']),
city = nativeFromJson<String>(json['city']),
state = nativeFromJson<String>(json['state']),
zipCode = nativeFromJson<String>(json['zipCode']),
managerName = nativeFromJson<String>(json['managerName']),
city = json['city'] == null ? null : nativeFromJson<String>(json['city']),
state = json['state'] == null ? null : nativeFromJson<String>(json['state']),
zipCode = json['zipCode'] == null ? null : nativeFromJson<String>(json['zipCode']),
managerName = json['managerName'] == null ? null : nativeFromJson<String>(json['managerName']),
isActive = nativeFromJson<bool>(json['isActive']),
departments = json['departments'] == null ? null : AnyValue.fromJson(json['departments']),
createdAt = json['createdAt'] == null ? null : Timestamp.fromJson(json['createdAt']),
@@ -82,10 +82,18 @@ class GetTeamHubByIdTeamHub {
json['teamId'] = nativeToJson<String>(teamId);
json['hubName'] = nativeToJson<String>(hubName);
json['address'] = nativeToJson<String>(address);
json['city'] = nativeToJson<String>(city);
json['state'] = nativeToJson<String>(state);
json['zipCode'] = nativeToJson<String>(zipCode);
json['managerName'] = nativeToJson<String>(managerName);
if (city != null) {
json['city'] = nativeToJson<String?>(city);
}
if (state != null) {
json['state'] = nativeToJson<String?>(state);
}
if (zipCode != null) {
json['zipCode'] = nativeToJson<String?>(zipCode);
}
if (managerName != null) {
json['managerName'] = nativeToJson<String?>(managerName);
}
json['isActive'] = nativeToJson<bool>(isActive);
if (departments != null) {
json['departments'] = departments!.toJson();
@@ -107,10 +115,10 @@ class GetTeamHubByIdTeamHub {
required this.teamId,
required this.hubName,
required this.address,
required this.city,
required this.state,
required this.zipCode,
required this.managerName,
this.city,
this.state,
this.zipCode,
this.managerName,
required this.isActive,
this.departments,
this.createdAt,

View File

@@ -23,10 +23,10 @@ class GetTeamHubsByTeamIdTeamHubs {
final String teamId;
final String hubName;
final String address;
final String city;
final String state;
final String zipCode;
final String managerName;
final String? city;
final String? state;
final String? zipCode;
final String? managerName;
final bool isActive;
final AnyValue? departments;
final Timestamp? createdAt;
@@ -38,10 +38,10 @@ class GetTeamHubsByTeamIdTeamHubs {
teamId = nativeFromJson<String>(json['teamId']),
hubName = nativeFromJson<String>(json['hubName']),
address = nativeFromJson<String>(json['address']),
city = nativeFromJson<String>(json['city']),
state = nativeFromJson<String>(json['state']),
zipCode = nativeFromJson<String>(json['zipCode']),
managerName = nativeFromJson<String>(json['managerName']),
city = json['city'] == null ? null : nativeFromJson<String>(json['city']),
state = json['state'] == null ? null : nativeFromJson<String>(json['state']),
zipCode = json['zipCode'] == null ? null : nativeFromJson<String>(json['zipCode']),
managerName = json['managerName'] == null ? null : nativeFromJson<String>(json['managerName']),
isActive = nativeFromJson<bool>(json['isActive']),
departments = json['departments'] == null ? null : AnyValue.fromJson(json['departments']),
createdAt = json['createdAt'] == null ? null : Timestamp.fromJson(json['createdAt']),
@@ -82,10 +82,18 @@ class GetTeamHubsByTeamIdTeamHubs {
json['teamId'] = nativeToJson<String>(teamId);
json['hubName'] = nativeToJson<String>(hubName);
json['address'] = nativeToJson<String>(address);
json['city'] = nativeToJson<String>(city);
json['state'] = nativeToJson<String>(state);
json['zipCode'] = nativeToJson<String>(zipCode);
json['managerName'] = nativeToJson<String>(managerName);
if (city != null) {
json['city'] = nativeToJson<String?>(city);
}
if (state != null) {
json['state'] = nativeToJson<String?>(state);
}
if (zipCode != null) {
json['zipCode'] = nativeToJson<String?>(zipCode);
}
if (managerName != null) {
json['managerName'] = nativeToJson<String?>(managerName);
}
json['isActive'] = nativeToJson<bool>(isActive);
if (departments != null) {
json['departments'] = departments!.toJson();
@@ -107,10 +115,10 @@ class GetTeamHubsByTeamIdTeamHubs {
required this.teamId,
required this.hubName,
required this.address,
required this.city,
required this.state,
required this.zipCode,
required this.managerName,
this.city,
this.state,
this.zipCode,
this.managerName,
required this.isActive,
this.departments,
this.createdAt,

View File

@@ -22,10 +22,10 @@ class ListTeamHubsTeamHubs {
final String teamId;
final String hubName;
final String address;
final String city;
final String state;
final String zipCode;
final String managerName;
final String? city;
final String? state;
final String? zipCode;
final String? managerName;
final bool isActive;
final AnyValue? departments;
final Timestamp? createdAt;
@@ -37,10 +37,10 @@ class ListTeamHubsTeamHubs {
teamId = nativeFromJson<String>(json['teamId']),
hubName = nativeFromJson<String>(json['hubName']),
address = nativeFromJson<String>(json['address']),
city = nativeFromJson<String>(json['city']),
state = nativeFromJson<String>(json['state']),
zipCode = nativeFromJson<String>(json['zipCode']),
managerName = nativeFromJson<String>(json['managerName']),
city = json['city'] == null ? null : nativeFromJson<String>(json['city']),
state = json['state'] == null ? null : nativeFromJson<String>(json['state']),
zipCode = json['zipCode'] == null ? null : nativeFromJson<String>(json['zipCode']),
managerName = json['managerName'] == null ? null : nativeFromJson<String>(json['managerName']),
isActive = nativeFromJson<bool>(json['isActive']),
departments = json['departments'] == null ? null : AnyValue.fromJson(json['departments']),
createdAt = json['createdAt'] == null ? null : Timestamp.fromJson(json['createdAt']),
@@ -81,10 +81,18 @@ class ListTeamHubsTeamHubs {
json['teamId'] = nativeToJson<String>(teamId);
json['hubName'] = nativeToJson<String>(hubName);
json['address'] = nativeToJson<String>(address);
json['city'] = nativeToJson<String>(city);
json['state'] = nativeToJson<String>(state);
json['zipCode'] = nativeToJson<String>(zipCode);
json['managerName'] = nativeToJson<String>(managerName);
if (city != null) {
json['city'] = nativeToJson<String?>(city);
}
if (state != null) {
json['state'] = nativeToJson<String?>(state);
}
if (zipCode != null) {
json['zipCode'] = nativeToJson<String?>(zipCode);
}
if (managerName != null) {
json['managerName'] = nativeToJson<String?>(managerName);
}
json['isActive'] = nativeToJson<bool>(isActive);
if (departments != null) {
json['departments'] = departments!.toJson();
@@ -106,10 +114,10 @@ class ListTeamHubsTeamHubs {
required this.teamId,
required this.hubName,
required this.address,
required this.city,
required this.state,
required this.zipCode,
required this.managerName,
this.city,
this.state,
this.zipCode,
this.managerName,
required this.isActive,
this.departments,
this.createdAt,

View File

@@ -23,10 +23,10 @@ class ListTeamHubsByOwnerIdTeamHubs {
final String teamId;
final String hubName;
final String address;
final String city;
final String state;
final String zipCode;
final String managerName;
final String? city;
final String? state;
final String? zipCode;
final String? managerName;
final bool isActive;
final AnyValue? departments;
final Timestamp? createdAt;
@@ -36,10 +36,10 @@ class ListTeamHubsByOwnerIdTeamHubs {
teamId = nativeFromJson<String>(json['teamId']),
hubName = nativeFromJson<String>(json['hubName']),
address = nativeFromJson<String>(json['address']),
city = nativeFromJson<String>(json['city']),
state = nativeFromJson<String>(json['state']),
zipCode = nativeFromJson<String>(json['zipCode']),
managerName = nativeFromJson<String>(json['managerName']),
city = json['city'] == null ? null : nativeFromJson<String>(json['city']),
state = json['state'] == null ? null : nativeFromJson<String>(json['state']),
zipCode = json['zipCode'] == null ? null : nativeFromJson<String>(json['zipCode']),
managerName = json['managerName'] == null ? null : nativeFromJson<String>(json['managerName']),
isActive = nativeFromJson<bool>(json['isActive']),
departments = json['departments'] == null ? null : AnyValue.fromJson(json['departments']),
createdAt = json['createdAt'] == null ? null : Timestamp.fromJson(json['createdAt']);
@@ -76,10 +76,18 @@ class ListTeamHubsByOwnerIdTeamHubs {
json['teamId'] = nativeToJson<String>(teamId);
json['hubName'] = nativeToJson<String>(hubName);
json['address'] = nativeToJson<String>(address);
json['city'] = nativeToJson<String>(city);
json['state'] = nativeToJson<String>(state);
json['zipCode'] = nativeToJson<String>(zipCode);
json['managerName'] = nativeToJson<String>(managerName);
if (city != null) {
json['city'] = nativeToJson<String?>(city);
}
if (state != null) {
json['state'] = nativeToJson<String?>(state);
}
if (zipCode != null) {
json['zipCode'] = nativeToJson<String?>(zipCode);
}
if (managerName != null) {
json['managerName'] = nativeToJson<String?>(managerName);
}
json['isActive'] = nativeToJson<bool>(isActive);
if (departments != null) {
json['departments'] = departments!.toJson();
@@ -95,10 +103,10 @@ class ListTeamHubsByOwnerIdTeamHubs {
required this.teamId,
required this.hubName,
required this.address,
required this.city,
required this.state,
required this.zipCode,
required this.managerName,
this.city,
this.state,
this.zipCode,
this.managerName,
required this.isActive,
this.departments,
this.createdAt,

View File

@@ -1,3 +0,0 @@
## 0.0.1
* TODO: Describe initial release.

View File

@@ -1 +0,0 @@
TODO: Add your license here.

View File

@@ -1,39 +0,0 @@
<!--
This README describes the package. If you publish this package to pub.dev,
this README's contents appear on the landing page for your package.
For information about how to write a good package README, see the guide for
[writing package pages](https://dart.dev/tools/pub/writing-package-pages).
For general information about developing packages, see the Dart guide for
[creating packages](https://dart.dev/guides/libraries/create-packages)
and the Flutter guide for
[developing packages and plugins](https://flutter.dev/to/develop-packages).
-->
TODO: Put a short description of the package here that helps potential users
know whether this package might be useful for them.
## Features
TODO: List what your package can do. Maybe include images, gifs, or videos.
## Getting started
TODO: List prerequisites and provide or point to information on how to
start using the package.
## Usage
TODO: Include short and useful examples for package users. Add longer examples
to `/example` folder.
```dart
const like = 'sample';
```
## Additional information
TODO: Tell users more about the package: where to find more information, how to
contribute to the package, how to file issues, what response they can expect
from the package authors, and more.

View File

@@ -1 +1 @@
include: ../../analytics_options.yaml
include: ../../analysis_options.yaml

View File

@@ -5,11 +5,11 @@ class UiConstants {
UiConstants._();
// --- Border Radii ---
/// Base radius: 12px
static const double radiusBase = 12.0;
static final BorderRadius radiusLg = BorderRadius.circular(radiusBase);
/// Medium radius: 6px
static const double radiusMdValue = 6.0;
static final BorderRadius radiusMd = BorderRadius.circular(radiusMdValue);
@@ -19,12 +19,12 @@ class UiConstants {
/// Extra small radius: 2px
static final BorderRadius radiusXs = BorderRadius.circular(2.0);
/// Large/Full radius
static final BorderRadius radiusFull = BorderRadius.circular(999.0);
// --- Spacing ---
static const double space0 = 0.0;
static const double space1 = 4.0;
static const double space2 = 8.0;
@@ -35,4 +35,6 @@ class UiConstants {
static const double space8 = 32.0;
static const double space10 = 40.0;
static const double space12 = 48.0;
static const double space14 = 56.0;
static const double space16 = 64.0;
}

View File

@@ -1,5 +1,5 @@
import 'package:firebase_auth/firebase_auth.dart' as firebase;
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
import 'package:firebase_auth/firebase_auth.dart';
import 'package:krow_data_connect/krow_data_connect.dart';
import 'package:krow_domain/krow_domain.dart' as domain;
import '../../domain/repositories/auth_repository_interface.dart';
@@ -8,15 +8,15 @@ import '../../domain/repositories/auth_repository_interface.dart';
/// This implementation integrates with Firebase Authentication for user
/// identity management and Krow's Data Connect SDK for storing user profile data.
class AuthRepositoryImpl implements AuthRepositoryInterface {
final firebase.FirebaseAuth _firebaseAuth;
final dc.ExampleConnector _dataConnect;
final FirebaseAuth _firebaseAuth;
final ExampleConnector _dataConnect;
/// Creates an [AuthRepositoryImpl] with the real dependencies.
AuthRepositoryImpl({
required firebase.FirebaseAuth firebaseAuth,
required dc.ExampleConnector dataConnect,
}) : _firebaseAuth = firebaseAuth,
_dataConnect = dataConnect;
required FirebaseAuth firebaseAuth,
required ExampleConnector dataConnect,
}) : _firebaseAuth = firebaseAuth,
_dataConnect = dataConnect;
@override
Future<domain.User> signInWithEmail({
@@ -91,23 +91,25 @@ class AuthRepositoryImpl implements AuthRepositoryInterface {
// Client-specific business logic:
// 1. Create a `Business` entity.
// 2. Create a `User` entity associated with the business.
final createBusinessResponse = await _dataConnect.createBusiness(
businessName: companyName,
userId: firebaseUser.uid,
rateGroup: dc.BusinessRateGroup.STANDARD,
status: dc.BusinessStatus.PENDING,
).execute();
final createBusinessResponse = await _dataConnect
.createBusiness(
businessName: companyName,
userId: firebaseUser.uid,
rateGroup: BusinessRateGroup.STANDARD,
status: BusinessStatus.PENDING,
)
.execute();
final businessData = createBusinessResponse.data?.business_insert;
if (businessData == null) {
await firebaseUser.delete(); // Rollback if business creation fails
throw Exception('Business creation failed after Firebase user registration.');
throw Exception(
'Business creation failed after Firebase user registration.',
);
}
final createUserResponse = await _dataConnect.createUser(
id: firebaseUser.uid,
role: dc.UserBaseRole.USER,
)
final createUserResponse = await _dataConnect
.createUser(id: firebaseUser.uid, role: UserBaseRole.USER)
.email(email)
.userRole('BUSINESS')
.execute();
@@ -116,15 +118,16 @@ class AuthRepositoryImpl implements AuthRepositoryInterface {
if (newUserData == null) {
await firebaseUser.delete(); // Rollback if user profile creation fails
// TO-DO: Also delete the created Business if this fails
throw Exception('User profile creation failed after Firebase user registration.');
throw Exception(
'User profile creation failed after Firebase user registration.',
);
}
return _getUserProfile(
firebaseUserId: firebaseUser.uid,
fallbackEmail: firebaseUser.email ?? email,
);
} on firebase.FirebaseAuthException catch (e) {
} on FirebaseAuthException catch (e) {
if (e.code == 'weak-password') {
throw Exception('The password provided is too weak.');
} else if (e.code == 'email-already-in-use') {
@@ -133,7 +136,9 @@ class AuthRepositoryImpl implements AuthRepositoryInterface {
throw Exception('Sign-up error: ${e.message}');
}
} catch (e) {
throw Exception('Failed to sign up and create user data: ${e.toString()}');
throw Exception(
'Failed to sign up and create user data: ${e.toString()}',
);
}
}
@@ -148,14 +153,18 @@ class AuthRepositoryImpl implements AuthRepositoryInterface {
@override
Future<domain.User> signInWithSocial({required String provider}) {
throw UnimplementedError('Social authentication with $provider is not yet implemented.');
throw UnimplementedError(
'Social authentication with $provider is not yet implemented.',
);
}
Future<domain.User> _getUserProfile({
required String firebaseUserId,
required String? fallbackEmail,
}) async {
final response = await _dataConnect.getUserById(id: firebaseUserId).execute();
final response = await _dataConnect
.getUserById(id: firebaseUserId)
.execute();
final user = response.data?.user;
if (user == null) {
throw Exception('Authenticated user profile not found in database.');
@@ -166,10 +175,6 @@ class AuthRepositoryImpl implements AuthRepositoryInterface {
throw Exception('User email is missing in profile data.');
}
return domain.User(
id: user.id,
email: email,
role: user.role.stringValue,
);
return domain.User(id: user.id, email: email, role: user.role.stringValue);
}
}

View File

@@ -20,9 +20,13 @@ dependencies:
# Architecture Packages
design_system:
path: ../../../../design_system
path: ../../../design_system
core_localization:
path: ../../../core_localization
krow_data_connect:
path: ../../../data_connect
krow_domain:
path: ../../../domain
dev_dependencies:
flutter_test:

View File

@@ -0,0 +1,3 @@
library client_create_order;
export 'src/create_order_module.dart';

View File

@@ -0,0 +1,28 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_modular/flutter_modular.dart';
import 'presentation/blocs/client_create_order_bloc.dart';
import 'presentation/pages/create_order_page.dart';
import 'presentation/pages/one_time_order_page.dart';
import 'presentation/pages/permanent_order_page.dart';
import 'presentation/pages/rapid_order_page.dart';
import 'presentation/pages/recurring_order_page.dart';
class ClientCreateOrderModule extends Module {
@override
void binds(Injector i) {
i.addSingleton<ClientCreateOrderBloc>(ClientCreateOrderBloc.new);
}
@override
void routes(RouteManager r) {
r.child('/',
child: (BuildContext context) => const ClientCreateOrderPage());
r.child('/rapid', child: (BuildContext context) => const RapidOrderPage());
r.child('/one-time',
child: (BuildContext context) => const OneTimeOrderPage());
r.child('/recurring',
child: (BuildContext context) => const RecurringOrderPage());
r.child('/permanent',
child: (BuildContext context) => const PermanentOrderPage());
}
}

View File

@@ -0,0 +1,69 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:design_system/design_system.dart';
import 'client_create_order_event.dart';
import 'client_create_order_state.dart';
class ClientCreateOrderBloc
extends Bloc<ClientCreateOrderEvent, ClientCreateOrderState> {
ClientCreateOrderBloc() : super(const ClientCreateOrderInitial()) {
on<ClientCreateOrderTypesRequested>(_onTypesRequested);
}
void _onTypesRequested(
ClientCreateOrderTypesRequested event,
Emitter<ClientCreateOrderState> emit,
) {
// In a real app, this might come from a repository or config
final List<CreateOrderType> types = [
const CreateOrderType(
id: 'rapid',
icon: UiIcons.zap,
titleKey: 'client_create_order.types.rapid',
descriptionKey: 'client_create_order.types.rapid_desc',
backgroundColor: UiColors.tagError, // Red-ish background
borderColor: UiColors.destructive, // Red border
iconBackgroundColor: UiColors.tagError,
iconColor: UiColors.destructive,
textColor: UiColors.destructive,
descriptionColor: UiColors.textError,
),
const CreateOrderType(
id: 'one-time',
icon: UiIcons.calendar,
titleKey: 'client_create_order.types.one_time',
descriptionKey: 'client_create_order.types.one_time_desc',
backgroundColor: UiColors.tagInProgress, // Blue-ish
borderColor: UiColors.primary,
iconBackgroundColor: UiColors.tagInProgress,
iconColor: UiColors.primary,
textColor: UiColors.primary,
descriptionColor: UiColors.primary,
),
const CreateOrderType(
id: 'recurring',
icon: UiIcons.rotateCcw,
titleKey: 'client_create_order.types.recurring',
descriptionKey: 'client_create_order.types.recurring_desc',
backgroundColor: UiColors.tagRefunded, // Indigo-ish (Purple sub)
borderColor: UiColors.primary, // No purple, use primary or mix
iconBackgroundColor: UiColors.tagRefunded,
iconColor: UiColors.primary,
textColor: UiColors.primary,
descriptionColor: UiColors.textSecondary,
),
const CreateOrderType(
id: 'permanent',
icon: UiIcons.briefcase,
titleKey: 'client_create_order.types.permanent',
descriptionKey: 'client_create_order.types.permanent_desc',
backgroundColor: UiColors.tagSuccess, // Green
borderColor: UiColors.textSuccess,
iconBackgroundColor: UiColors.tagSuccess,
iconColor: UiColors.textSuccess,
textColor: UiColors.textSuccess,
descriptionColor: UiColors.textSuccess,
),
];
emit(ClientCreateOrderLoadSuccess(types));
}
}

View File

@@ -0,0 +1,21 @@
import 'package:equatable/equatable.dart';
abstract class ClientCreateOrderEvent extends Equatable {
const ClientCreateOrderEvent();
@override
List<Object?> get props => [];
}
class ClientCreateOrderTypesRequested extends ClientCreateOrderEvent {
const ClientCreateOrderTypesRequested();
}
class ClientCreateOrderTypeSelected extends ClientCreateOrderEvent {
final String typeId;
const ClientCreateOrderTypeSelected(this.typeId);
@override
List<Object?> get props => [typeId];
}

View File

@@ -0,0 +1,63 @@
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
/// Represents an available order type.
class CreateOrderType extends Equatable {
final String id;
final IconData icon;
final String titleKey; // Key for translation
final String descriptionKey; // Key for translation
final Color backgroundColor;
final Color borderColor;
final Color iconBackgroundColor;
final Color iconColor;
final Color textColor;
final Color descriptionColor;
const CreateOrderType({
required this.id,
required this.icon,
required this.titleKey,
required this.descriptionKey,
required this.backgroundColor,
required this.borderColor,
required this.iconBackgroundColor,
required this.iconColor,
required this.textColor,
required this.descriptionColor,
});
@override
List<Object?> get props => [
id,
icon,
titleKey,
descriptionKey,
backgroundColor,
borderColor,
iconBackgroundColor,
iconColor,
textColor,
descriptionColor,
];
}
abstract class ClientCreateOrderState extends Equatable {
const ClientCreateOrderState();
@override
List<Object?> get props => [];
}
class ClientCreateOrderInitial extends ClientCreateOrderState {
const ClientCreateOrderInitial();
}
class ClientCreateOrderLoadSuccess extends ClientCreateOrderState {
final List<CreateOrderType> orderTypes;
const ClientCreateOrderLoadSuccess(this.orderTypes);
@override
List<Object?> get props => [orderTypes];
}

View File

@@ -0,0 +1,23 @@
import 'package:flutter_modular/flutter_modular.dart';
extension ClientCreateOrderNavigator on IModularNavigator {
void pushCreateOrder() {
pushNamed('/client/create-order/');
}
void pushRapidOrder() {
pushNamed('/client/create-order/rapid');
}
void pushOneTimeOrder() {
pushNamed('/client/create-order/one-time');
}
void pushRecurringOrder() {
pushNamed('/client/create-order/recurring');
}
void pushPermanentOrder() {
pushNamed('/client/create-order/permanent');
}
}

View File

@@ -0,0 +1,214 @@
import 'package:core_localization/core_localization.dart';
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_modular/flutter_modular.dart';
import '../blocs/client_create_order_bloc.dart';
import '../blocs/client_create_order_event.dart';
import '../blocs/client_create_order_state.dart';
import '../navigation/client_create_order_navigator.dart';
/// One-time helper to map keys to translations since they are dynamic in BLoC state
String _getTranslation(String key) {
// Safe mapping - explicit keys expected
if (key == 'client_create_order.types.rapid') {
return t.client_create_order.types.rapid;
} else if (key == 'client_create_order.types.rapid_desc') {
return t.client_create_order.types.rapid_desc;
} else if (key == 'client_create_order.types.one_time') {
return t.client_create_order.types.one_time;
} else if (key == 'client_create_order.types.one_time_desc') {
return t.client_create_order.types.one_time_desc;
} else if (key == 'client_create_order.types.recurring') {
return t.client_create_order.types.recurring;
} else if (key == 'client_create_order.types.recurring_desc') {
return t.client_create_order.types.recurring_desc;
} else if (key == 'client_create_order.types.permanent') {
return t.client_create_order.types.permanent;
} else if (key == 'client_create_order.types.permanent_desc') {
return t.client_create_order.types.permanent_desc;
}
return key;
}
class ClientCreateOrderPage extends StatelessWidget {
const ClientCreateOrderPage({super.key});
@override
Widget build(BuildContext context) {
return BlocProvider<ClientCreateOrderBloc>(
create: (BuildContext context) => Modular.get<ClientCreateOrderBloc>()
..add(const ClientCreateOrderTypesRequested()),
child: const _CreateOrderView(),
);
}
}
class _CreateOrderView extends StatelessWidget {
const _CreateOrderView();
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: UiColors.background,
appBar: AppBar(
backgroundColor: UiColors.white,
elevation: 0,
bottom: PreferredSize(
preferredSize: const Size.fromHeight(1.0),
child: Container(color: UiColors.border, height: 1.0),
),
leading: IconButton(
icon: const Icon(UiIcons.chevronLeft, color: UiColors.iconSecondary),
onPressed: () => Modular.to.pop(),
),
title: Text(
t.client_create_order.title,
style: UiTypography.headline3m.textPrimary,
),
),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: UiConstants.space5,
vertical: UiConstants.space6,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(bottom: UiConstants.space6),
child: Text(
t.client_create_order.section_title,
style: UiTypography.footnote1m.copyWith(
color: UiColors.textDescription,
letterSpacing: 0.5,
),
),
),
Expanded(
child:
BlocBuilder<ClientCreateOrderBloc, ClientCreateOrderState>(
builder:
(BuildContext context, ClientCreateOrderState state) {
if (state is ClientCreateOrderLoadSuccess) {
return GridView.builder(
gridDelegate:
const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: UiConstants.space4,
crossAxisSpacing: UiConstants.space4,
childAspectRatio: 1,
),
itemCount: state.orderTypes.length,
itemBuilder: (BuildContext context, int index) {
final CreateOrderType type = state.orderTypes[index];
return _OrderTypeCard(
icon: type.icon,
title: _getTranslation(type.titleKey),
description: _getTranslation(
type.descriptionKey,
),
backgroundColor: type.backgroundColor,
borderColor: type.borderColor,
iconBackgroundColor: type.iconBackgroundColor,
iconColor: type.iconColor,
textColor: type.textColor,
descriptionColor: type.descriptionColor,
onTap: () {
switch (type.id) {
case 'rapid':
Modular.to.pushRapidOrder();
break;
case 'one-time':
Modular.to.pushOneTimeOrder();
break;
case 'recurring':
Modular.to.pushRecurringOrder();
break;
case 'permanent':
Modular.to.pushPermanentOrder();
break;
}
},
);
},
);
}
return const Center(child: CircularProgressIndicator());
},
),
),
],
),
),
),
);
}
}
class _OrderTypeCard extends StatelessWidget {
final IconData icon;
final String title;
final String description;
final Color backgroundColor;
final Color borderColor;
final Color iconBackgroundColor;
final Color iconColor;
final Color textColor;
final Color descriptionColor;
final VoidCallback onTap;
const _OrderTypeCard({
required this.icon,
required this.title,
required this.description,
required this.backgroundColor,
required this.borderColor,
required this.iconBackgroundColor,
required this.iconColor,
required this.textColor,
required this.descriptionColor,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.all(UiConstants.space5),
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
border: Border.all(color: borderColor, width: 2),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Container(
width: 48,
height: 48,
margin: const EdgeInsets.only(bottom: UiConstants.space3),
decoration: BoxDecoration(
color: iconBackgroundColor,
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
),
child: Icon(icon, color: iconColor, size: 24),
),
Text(
title,
style: UiTypography.body2b.copyWith(color: textColor),
),
const SizedBox(height: UiConstants.space1),
Text(
description,
style: UiTypography.footnote1r.copyWith(color: descriptionColor),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,32 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter_modular/flutter_modular.dart';
class OneTimeOrderPage extends StatelessWidget {
const OneTimeOrderPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: UiColors.background,
appBar: AppBar(
title:
Text('One-Time Order', style: UiTypography.headline3m.textPrimary),
leading: IconButton(
icon: const Icon(UiIcons.chevronLeft, color: UiColors.iconSecondary),
onPressed: () => Modular.to.pop(),
),
backgroundColor: UiColors.white,
elevation: 0,
bottom: PreferredSize(
preferredSize: const Size.fromHeight(1.0),
child: Container(color: UiColors.border, height: 1.0),
),
),
body: Center(
child: Text('One-Time Order Flow (WIP)',
style: UiTypography.body1r.textSecondary),
),
);
}
}

View File

@@ -0,0 +1,32 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter_modular/flutter_modular.dart';
class PermanentOrderPage extends StatelessWidget {
const PermanentOrderPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: UiColors.background,
appBar: AppBar(
title:
Text('Permanent Order', style: UiTypography.headline3m.textPrimary),
leading: IconButton(
icon: const Icon(UiIcons.chevronLeft, color: UiColors.iconSecondary),
onPressed: () => Modular.to.pop(),
),
backgroundColor: UiColors.white,
elevation: 0,
bottom: PreferredSize(
preferredSize: const Size.fromHeight(1.0),
child: Container(color: UiColors.border, height: 1.0),
),
),
body: Center(
child: Text('Permanent Order Flow (WIP)',
style: UiTypography.body1r.textSecondary),
),
);
}
}

View File

@@ -0,0 +1,31 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter_modular/flutter_modular.dart';
class RapidOrderPage extends StatelessWidget {
const RapidOrderPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: UiColors.background,
appBar: AppBar(
title: Text('Rapid Order', style: UiTypography.headline3m.textPrimary),
leading: IconButton(
icon: const Icon(UiIcons.chevronLeft, color: UiColors.iconSecondary),
onPressed: () => Modular.to.pop(),
),
backgroundColor: UiColors.white,
elevation: 0,
bottom: PreferredSize(
preferredSize: const Size.fromHeight(1.0),
child: Container(color: UiColors.border, height: 1.0),
),
),
body: Center(
child: Text('Rapid Order Flow (WIP)',
style: UiTypography.body1r.textSecondary),
),
);
}
}

View File

@@ -0,0 +1,32 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter_modular/flutter_modular.dart';
class RecurringOrderPage extends StatelessWidget {
const RecurringOrderPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: UiColors.background,
appBar: AppBar(
title:
Text('Recurring Order', style: UiTypography.headline3m.textPrimary),
leading: IconButton(
icon: const Icon(UiIcons.chevronLeft, color: UiColors.iconSecondary),
onPressed: () => Modular.to.pop(),
),
backgroundColor: UiColors.white,
elevation: 0,
bottom: PreferredSize(
preferredSize: const Size.fromHeight(1.0),
child: Container(color: UiColors.border, height: 1.0),
),
),
body: Center(
child: Text('Recurring Order Flow (WIP)',
style: UiTypography.body1r.textSecondary),
),
);
}
}

View File

@@ -0,0 +1,851 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
_fe_analyzer_shared:
dependency: transitive
description:
name: _fe_analyzer_shared
sha256: c209688d9f5a5f26b2fb47a188131a6fb9e876ae9e47af3737c0b4f58a93470d
url: "https://pub.dev"
source: hosted
version: "91.0.0"
analyzer:
dependency: transitive
description:
name: analyzer
sha256: f51c8499b35f9b26820cfe914828a6a98a94efd5cc78b37bb7d03debae3a1d08
url: "https://pub.dev"
source: hosted
version: "8.4.1"
args:
dependency: transitive
description:
name: args
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
url: "https://pub.dev"
source: hosted
version: "2.7.0"
async:
dependency: transitive
description:
name: async
sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
url: "https://pub.dev"
source: hosted
version: "2.13.0"
auto_injector:
dependency: transitive
description:
name: auto_injector
sha256: "1fc2624898e92485122eb2b1698dd42511d7ff6574f84a3a8606fc4549a1e8f8"
url: "https://pub.dev"
source: hosted
version: "2.1.1"
bloc:
dependency: transitive
description:
name: bloc
sha256: "106842ad6569f0b60297619e9e0b1885c2fb9bf84812935490e6c5275777804e"
url: "https://pub.dev"
source: hosted
version: "8.1.4"
bloc_test:
dependency: "direct dev"
description:
name: bloc_test
sha256: "165a6ec950d9252ebe36dc5335f2e6eb13055f33d56db0eeb7642768849b43d2"
url: "https://pub.dev"
source: hosted
version: "9.1.7"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
characters:
dependency: transitive
description:
name: characters
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
url: "https://pub.dev"
source: hosted
version: "1.4.0"
cli_config:
dependency: transitive
description:
name: cli_config
sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec
url: "https://pub.dev"
source: hosted
version: "0.2.0"
clock:
dependency: transitive
description:
name: clock
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
url: "https://pub.dev"
source: hosted
version: "1.1.2"
code_assets:
dependency: transitive
description:
name: code_assets
sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
collection:
dependency: transitive
description:
name: collection
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
url: "https://pub.dev"
source: hosted
version: "1.19.1"
convert:
dependency: transitive
description:
name: convert
sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
url: "https://pub.dev"
source: hosted
version: "3.1.2"
core_localization:
dependency: "direct main"
description:
path: "../../../core_localization"
relative: true
source: path
version: "0.0.1"
coverage:
dependency: transitive
description:
name: coverage
sha256: "5da775aa218eaf2151c721b16c01c7676fbfdd99cebba2bf64e8b807a28ff94d"
url: "https://pub.dev"
source: hosted
version: "1.15.0"
crypto:
dependency: transitive
description:
name: crypto
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
url: "https://pub.dev"
source: hosted
version: "3.0.7"
csv:
dependency: transitive
description:
name: csv
sha256: c6aa2679b2a18cb57652920f674488d89712efaf4d3fdf2e537215b35fc19d6c
url: "https://pub.dev"
source: hosted
version: "6.0.0"
design_system:
dependency: "direct main"
description:
path: "../../../design_system"
relative: true
source: path
version: "0.0.1"
diff_match_patch:
dependency: transitive
description:
name: diff_match_patch
sha256: "2efc9e6e8f449d0abe15be240e2c2a3bcd977c8d126cfd70598aee60af35c0a4"
url: "https://pub.dev"
source: hosted
version: "0.4.1"
equatable:
dependency: "direct main"
description:
name: equatable
sha256: "3e0141505477fd8ad55d6eb4e7776d3fe8430be8e497ccb1521370c3f21a3e2b"
url: "https://pub.dev"
source: hosted
version: "2.0.8"
fake_async:
dependency: transitive
description:
name: fake_async
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
url: "https://pub.dev"
source: hosted
version: "1.3.3"
ffi:
dependency: transitive
description:
name: ffi
sha256: d07d37192dbf97461359c1518788f203b0c9102cfd2c35a716b823741219542c
url: "https://pub.dev"
source: hosted
version: "2.1.5"
file:
dependency: transitive
description:
name: file
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
url: "https://pub.dev"
source: hosted
version: "7.0.1"
fixnum:
dependency: transitive
description:
name: fixnum
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
url: "https://pub.dev"
source: hosted
version: "1.1.1"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_bloc:
dependency: "direct main"
description:
name: flutter_bloc
sha256: b594505eac31a0518bdcb4b5b79573b8d9117b193cc80cc12e17d639b10aa27a
url: "https://pub.dev"
source: hosted
version: "8.1.6"
flutter_localizations:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
flutter_modular:
dependency: "direct main"
description:
name: flutter_modular
sha256: "33a63d9fe61429d12b3dfa04795ed890f17d179d3d38e988ba7969651fcd5586"
url: "https://pub.dev"
source: hosted
version: "6.4.1"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
flutter_web_plugins:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
font_awesome_flutter:
dependency: transitive
description:
name: font_awesome_flutter
sha256: b9011df3a1fa02993630b8fb83526368cf2206a711259830325bab2f1d2a4eb0
url: "https://pub.dev"
source: hosted
version: "10.12.0"
frontend_server_client:
dependency: transitive
description:
name: frontend_server_client
sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694
url: "https://pub.dev"
source: hosted
version: "4.0.0"
glob:
dependency: transitive
description:
name: glob
sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
url: "https://pub.dev"
source: hosted
version: "2.1.3"
google_fonts:
dependency: transitive
description:
name: google_fonts
sha256: "6996212014b996eaa17074e02b1b925b212f5e053832d9048970dc27255a8fb3"
url: "https://pub.dev"
source: hosted
version: "7.1.0"
hooks:
dependency: transitive
description:
name: hooks
sha256: "5d309c86e7ce34cd8e37aa71cb30cb652d3829b900ab145e4d9da564b31d59f7"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
http:
dependency: transitive
description:
name: http
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
url: "https://pub.dev"
source: hosted
version: "1.6.0"
http_multi_server:
dependency: transitive
description:
name: http_multi_server
sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8
url: "https://pub.dev"
source: hosted
version: "3.2.2"
http_parser:
dependency: transitive
description:
name: http_parser
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
url: "https://pub.dev"
source: hosted
version: "4.1.2"
intl:
dependency: transitive
description:
name: intl
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
url: "https://pub.dev"
source: hosted
version: "0.20.2"
io:
dependency: transitive
description:
name: io
sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b
url: "https://pub.dev"
source: hosted
version: "1.0.5"
js:
dependency: transitive
description:
name: js
sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc"
url: "https://pub.dev"
source: hosted
version: "0.7.2"
krow_core:
dependency: transitive
description:
path: "../../../core"
relative: true
source: path
version: "0.0.1"
krow_domain:
dependency: "direct main"
description:
path: "../../../domain"
relative: true
source: path
version: "0.0.1"
leak_tracker:
dependency: transitive
description:
name: leak_tracker
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
url: "https://pub.dev"
source: hosted
version: "11.0.2"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
url: "https://pub.dev"
source: hosted
version: "3.0.10"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
logging:
dependency: transitive
description:
name: logging
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
url: "https://pub.dev"
source: hosted
version: "1.3.0"
lucide_icons:
dependency: transitive
description:
name: lucide_icons
sha256: ad24d0fd65707e48add30bebada7d90bff2a1bba0a72d6e9b19d44246b0e83c4
url: "https://pub.dev"
source: hosted
version: "0.257.0"
matcher:
dependency: transitive
description:
name: matcher
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
url: "https://pub.dev"
source: hosted
version: "0.12.17"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
url: "https://pub.dev"
source: hosted
version: "0.11.1"
meta:
dependency: transitive
description:
name: meta
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
url: "https://pub.dev"
source: hosted
version: "1.17.0"
mime:
dependency: transitive
description:
name: mime
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
url: "https://pub.dev"
source: hosted
version: "2.0.0"
mocktail:
dependency: transitive
description:
name: mocktail
sha256: "890df3f9688106f25755f26b1c60589a92b3ab91a22b8b224947ad041bf172d8"
url: "https://pub.dev"
source: hosted
version: "1.0.4"
modular_core:
dependency: transitive
description:
name: modular_core
sha256: "1db0420a0dfb8a2c6dca846e7cbaa4ffeb778e247916dbcb27fb25aa566e5436"
url: "https://pub.dev"
source: hosted
version: "3.4.1"
native_toolchain_c:
dependency: transitive
description:
name: native_toolchain_c
sha256: "89e83885ba09da5fdf2cdacc8002a712ca238c28b7f717910b34bcd27b0d03ac"
url: "https://pub.dev"
source: hosted
version: "0.17.4"
nested:
dependency: transitive
description:
name: nested
sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
node_preamble:
dependency: transitive
description:
name: node_preamble
sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db"
url: "https://pub.dev"
source: hosted
version: "2.0.2"
objective_c:
dependency: transitive
description:
name: objective_c
sha256: "9922a1ad59ac5afb154cc948aa6ded01987a75003651d0a2866afc23f4da624e"
url: "https://pub.dev"
source: hosted
version: "9.2.3"
package_config:
dependency: transitive
description:
name: package_config
sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc
url: "https://pub.dev"
source: hosted
version: "2.2.0"
path:
dependency: transitive
description:
name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
url: "https://pub.dev"
source: hosted
version: "1.9.1"
path_provider:
dependency: transitive
description:
name: path_provider
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
url: "https://pub.dev"
source: hosted
version: "2.1.5"
path_provider_android:
dependency: transitive
description:
name: path_provider_android
sha256: f2c65e21139ce2c3dad46922be8272bb5963516045659e71bb16e151c93b580e
url: "https://pub.dev"
source: hosted
version: "2.2.22"
path_provider_foundation:
dependency: transitive
description:
name: path_provider_foundation
sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699"
url: "https://pub.dev"
source: hosted
version: "2.6.0"
path_provider_linux:
dependency: transitive
description:
name: path_provider_linux
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
url: "https://pub.dev"
source: hosted
version: "2.2.1"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
path_provider_windows:
dependency: transitive
description:
name: path_provider_windows
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
url: "https://pub.dev"
source: hosted
version: "2.3.0"
platform:
dependency: transitive
description:
name: platform
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
url: "https://pub.dev"
source: hosted
version: "3.1.6"
plugin_platform_interface:
dependency: transitive
description:
name: plugin_platform_interface
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
url: "https://pub.dev"
source: hosted
version: "2.1.8"
pool:
dependency: transitive
description:
name: pool
sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d"
url: "https://pub.dev"
source: hosted
version: "1.5.2"
provider:
dependency: transitive
description:
name: provider
sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272"
url: "https://pub.dev"
source: hosted
version: "6.1.5+1"
pub_semver:
dependency: transitive
description:
name: pub_semver
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
result_dart:
dependency: transitive
description:
name: result_dart
sha256: "0666b21fbdf697b3bdd9986348a380aa204b3ebe7c146d8e4cdaa7ce735e6054"
url: "https://pub.dev"
source: hosted
version: "2.1.1"
shared_preferences:
dependency: transitive
description:
name: shared_preferences
sha256: "2939ae520c9024cb197fc20dee269cd8cdbf564c8b5746374ec6cacdc5169e64"
url: "https://pub.dev"
source: hosted
version: "2.5.4"
shared_preferences_android:
dependency: transitive
description:
name: shared_preferences_android
sha256: "83af5c682796c0f7719c2bbf74792d113e40ae97981b8f266fa84574573556bc"
url: "https://pub.dev"
source: hosted
version: "2.4.18"
shared_preferences_foundation:
dependency: transitive
description:
name: shared_preferences_foundation
sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f"
url: "https://pub.dev"
source: hosted
version: "2.5.6"
shared_preferences_linux:
dependency: transitive
description:
name: shared_preferences_linux
sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
shared_preferences_platform_interface:
dependency: transitive
description:
name: shared_preferences_platform_interface
sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
shared_preferences_web:
dependency: transitive
description:
name: shared_preferences_web
sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019
url: "https://pub.dev"
source: hosted
version: "2.4.3"
shared_preferences_windows:
dependency: transitive
description:
name: shared_preferences_windows
sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
shelf:
dependency: transitive
description:
name: shelf
sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12
url: "https://pub.dev"
source: hosted
version: "1.4.2"
shelf_packages_handler:
dependency: transitive
description:
name: shelf_packages_handler
sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
shelf_static:
dependency: transitive
description:
name: shelf_static
sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3
url: "https://pub.dev"
source: hosted
version: "1.1.3"
shelf_web_socket:
dependency: transitive
description:
name: shelf_web_socket
sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925"
url: "https://pub.dev"
source: hosted
version: "3.0.0"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
slang:
dependency: transitive
description:
name: slang
sha256: "13e3b6f07adc51ab751e7889647774d294cbce7a3382f81d9e5029acfe9c37b2"
url: "https://pub.dev"
source: hosted
version: "4.12.0"
slang_flutter:
dependency: transitive
description:
name: slang_flutter
sha256: "0a4545cca5404d6b7487cf61cf1fe56c52daeb08de56a7574ee8381fbad035a0"
url: "https://pub.dev"
source: hosted
version: "4.12.0"
source_map_stack_trace:
dependency: transitive
description:
name: source_map_stack_trace
sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b
url: "https://pub.dev"
source: hosted
version: "2.1.2"
source_maps:
dependency: transitive
description:
name: source_maps
sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812"
url: "https://pub.dev"
source: hosted
version: "0.10.13"
source_span:
dependency: transitive
description:
name: source_span
sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c"
url: "https://pub.dev"
source: hosted
version: "1.10.1"
stack_trace:
dependency: transitive
description:
name: stack_trace
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
url: "https://pub.dev"
source: hosted
version: "1.12.1"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
string_scanner:
dependency: transitive
description:
name: string_scanner
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
url: "https://pub.dev"
source: hosted
version: "1.4.1"
term_glyph:
dependency: transitive
description:
name: term_glyph
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
url: "https://pub.dev"
source: hosted
version: "1.2.2"
test:
dependency: transitive
description:
name: test
sha256: "75906bf273541b676716d1ca7627a17e4c4070a3a16272b7a3dc7da3b9f3f6b7"
url: "https://pub.dev"
source: hosted
version: "1.26.3"
test_api:
dependency: transitive
description:
name: test_api
sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
url: "https://pub.dev"
source: hosted
version: "0.7.7"
test_core:
dependency: transitive
description:
name: test_core
sha256: "0cc24b5ff94b38d2ae73e1eb43cc302b77964fbf67abad1e296025b78deb53d0"
url: "https://pub.dev"
source: hosted
version: "0.6.12"
typed_data:
dependency: transitive
description:
name: typed_data
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
url: "https://pub.dev"
source: hosted
version: "1.4.0"
uuid:
dependency: transitive
description:
name: uuid
sha256: a11b666489b1954e01d992f3d601b1804a33937b5a8fe677bd26b8a9f96f96e8
url: "https://pub.dev"
source: hosted
version: "4.5.2"
vector_math:
dependency: transitive
description:
name: vector_math
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
url: "https://pub.dev"
source: hosted
version: "2.2.0"
vm_service:
dependency: transitive
description:
name: vm_service
sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60"
url: "https://pub.dev"
source: hosted
version: "15.0.2"
watcher:
dependency: transitive
description:
name: watcher
sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635"
url: "https://pub.dev"
source: hosted
version: "1.2.1"
web:
dependency: transitive
description:
name: web
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
url: "https://pub.dev"
source: hosted
version: "1.1.1"
web_socket:
dependency: transitive
description:
name: web_socket
sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c"
url: "https://pub.dev"
source: hosted
version: "1.0.1"
web_socket_channel:
dependency: transitive
description:
name: web_socket_channel
sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8
url: "https://pub.dev"
source: hosted
version: "3.0.3"
webkit_inspection_protocol:
dependency: transitive
description:
name: webkit_inspection_protocol
sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572"
url: "https://pub.dev"
source: hosted
version: "1.2.1"
xdg_directories:
dependency: transitive
description:
name: xdg_directories
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
yaml:
dependency: transitive
description:
name: yaml
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
url: "https://pub.dev"
source: hosted
version: "3.1.3"
sdks:
dart: ">=3.10.7 <4.0.0"
flutter: ">=3.38.4"

View File

@@ -0,0 +1,25 @@
name: client_create_order
description: Client create order feature
version: 0.0.1
publish_to: none
environment:
sdk: ">=3.0.0 <4.0.0"
dependencies:
flutter:
sdk: flutter
flutter_bloc: ^8.1.3
flutter_modular: ^6.3.2
equatable: ^2.0.5
design_system:
path: ../../../design_system
core_localization:
path: ../../../core_localization
krow_domain:
path: ../../../domain
dev_dependencies:
flutter_test:
sdk: flutter
bloc_test: ^9.1.5

View File

@@ -1,133 +0,0 @@
# Client Home Feature - Architecture Refactor Summary
## ✅ Completed Refactor
The `packages/features/client/home` feature has been successfully refactored to fully comply with KROW Clean Architecture principles.
## 📋 Changes Made
### 1. Domain Layer Improvements
**Created:**
- `lib/src/domain/entities/home_dashboard_data.dart`
- Proper domain entity to replace raw `Map<String, dynamic>`
- Immutable, equatable data class
- Clear field definitions with documentation
**Updated:**
- `lib/src/domain/repositories/home_repository_interface.dart`
- Changed from `abstract class` to `abstract interface class`
- Return type changed from `Map<String, dynamic>` to `HomeDashboardData`
- `lib/src/domain/usecases/get_dashboard_data_usecase.dart`
- Return type updated to `HomeDashboardData`
### 2. Data Layer Improvements
**Updated:**
- `lib/src/data/repositories_impl/home_repository_impl.dart`
- Returns `HomeDashboardData` entity instead of raw map
- Properly typed mock data
### 3. Presentation Layer Refactor
**Major Changes to `client_home_page.dart`:**
- ✅ Converted from `StatefulWidget` to `StatelessWidget`
- ✅ Removed local state management (moved to BLoC)
- ✅ BLoC lifecycle managed by `BlocProvider.create`
- ✅ All event dispatching uses `BlocProvider.of<ClientHomeBloc>(context)`
- ✅ Removed direct BLoC instance storage
- ✅ Fixed deprecated `withOpacity``withValues(alpha:)`
**Updated `client_home_state.dart`:**
- Replaced individual primitive fields with `HomeDashboardData` entity
- Simplified state structure
- Cleaner `copyWith` implementation
**Updated `client_home_bloc.dart`:**
- Simplified event handler to use entity directly
- No more manual field extraction from maps
**Widget Updates:**
- `coverage_widget.dart`: Now accepts typed parameters
- All widgets: Fixed deprecated `withOpacity` calls
- `shift_order_form_sheet.dart`: Fixed deprecated `value``initialValue`
## 🎯 Architecture Compliance
### ✅ Clean Architecture Rules
- [x] Domain layer is pure Dart (entities only)
- [x] Repository interfaces in domain, implementations in data
- [x] Use cases properly delegate to repositories
- [x] Presentation layer depends on domain abstractions
- [x] No feature-to-feature imports
### ✅ Presentation Rules
- [x] Page is `StatelessWidget`
- [x] State managed by BLoC
- [x] No business logic in page
- [x] BLoC lifecycle properly managed
- [x] Named parameters used throughout
### ✅ Code Quality
- [x] No deprecation warnings
- [x] All files have doc comments
- [x] Consistent naming conventions
- [x] `flutter analyze` passes with 0 issues
## 📊 Before vs After
### Before:
```dart
// StatefulWidget with local state
class ClientHomePage extends StatefulWidget {
late final ClientHomeBloc _homeBloc;
@override
void initState() {
_homeBloc = Modular.get<ClientHomeBloc>();
}
}
// Raw maps in domain
Future<Map<String, dynamic>> getDashboardData();
// Manual field extraction
weeklySpending: data['weeklySpending'] as double?,
```
### After:
```dart
// StatelessWidget, BLoC-managed
class ClientHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider<ClientHomeBloc>(
create: (context) => Modular.get<ClientHomeBloc>()..add(ClientHomeStarted()),
// ...
);
}
}
// Typed entities
Future<HomeDashboardData> getDashboardData();
// Direct entity usage
dashboardData: data,
```
## 🔍 Reference Alignment
The refactored code now matches the structure of `packages/features/staff/authentication`:
- StatelessWidget pages
- BLoC-managed state
- Typed domain entities
- Clean separation of concerns
## 🚀 Next Steps
The feature is now production-ready and follows all architectural guidelines. Future enhancements should:
1. Add unit tests for use cases
2. Add widget tests for pages
3. Add integration tests for complete flows
4. Consider extracting reusable widgets to design_system if used across features

View File

@@ -1,15 +1,15 @@
import 'package:flutter_modular/flutter_modular.dart';
/// Extension on [IModularNavigator] to provide strongly-typed navigation
/// for the client home feature.
extension ClientHomeNavigator on IModularNavigator {
/// Navigates to the client home page.
void pushClientHome() {
pushNamed('/client-home/');
}
/// Navigates to the settings page.
void pushSettings() {
pushNamed('/client-settings/');
}
void pushCreateOrder() {
pushNamed('/client/create-order/');
}
void pushRapidOrder() {
pushNamed('/client/create-order/rapid');
}
}

View File

@@ -324,8 +324,8 @@ class ClientHomePage extends StatelessWidget {
switch (id) {
case 'actions':
return ActionsWidget(
onRapidPressed: () {},
onCreateOrderPressed: () => _openOrderFormSheet(context, null),
onRapidPressed: () => Modular.to.pushRapidOrder(),
onCreateOrderPressed: () => Modular.to.pushCreateOrder(),
);
case 'reorder':
return ReorderWidget(

View File

@@ -16,6 +16,7 @@ dependencies:
equatable: ^2.0.5
lucide_icons: ^0.257.0
# Architecture Packages
design_system:
path: ../../../design_system
core_localization:

View File

@@ -2,6 +2,7 @@ library client_hubs;
import 'package:flutter_modular/flutter_modular.dart';
import 'package:krow_data_connect/krow_data_connect.dart';
import 'package:firebase_auth/firebase_auth.dart' as firebase;
import 'src/data/repositories_impl/hub_repository_impl.dart';
import 'src/domain/repositories/hub_repository_interface.dart';
import 'src/domain/usecases/assign_nfc_tag_usecase.dart';
@@ -22,7 +23,10 @@ class ClientHubsModule extends Module {
void binds(Injector i) {
// Repositories
i.addLazySingleton<HubRepositoryInterface>(
() => HubRepositoryImpl(mock: i.get<BusinessRepositoryMock>()),
() => HubRepositoryImpl(
firebaseAuth: firebase.FirebaseAuth.instance,
dataConnect: ExampleConnector.instance,
),
);
// UseCases

View File

@@ -1,45 +1,151 @@
import 'package:krow_data_connect/krow_data_connect.dart';
import 'package:krow_domain/krow_domain.dart';
import 'package:firebase_auth/firebase_auth.dart' as firebase;
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
import 'package:krow_domain/krow_domain.dart' as domain;
import '../../domain/repositories/hub_repository_interface.dart';
/// Implementation of [HubRepositoryInterface] that uses [BusinessRepositoryMock].
///
/// This class serves as a data adapter that bridges the domain repository interface
/// with the backend data source provided by the `data_connect` package. It strictly
/// delegates all operations to the [BusinessRepositoryMock], ensuring no business
/// logic resides in the data layer.
/// Implementation of [HubRepositoryInterface] backed by Data Connect.
class HubRepositoryImpl implements HubRepositoryInterface {
/// The business repository mock from data connect.
final BusinessRepositoryMock mock;
final firebase.FirebaseAuth _firebaseAuth;
final dc.ExampleConnector _dataConnect;
/// Creates a [HubRepositoryImpl] instance.
///
/// Takes a [BusinessRepositoryMock] as a dependency to perform data operations.
HubRepositoryImpl({required this.mock});
HubRepositoryImpl({
required firebase.FirebaseAuth firebaseAuth,
required dc.ExampleConnector dataConnect,
}) : _firebaseAuth = firebaseAuth,
_dataConnect = dataConnect;
@override
Future<List<Hub>> getHubs() {
// In a production environment, the business ID would be retrieved from
// a session manager or authentication state. For the current mock strategy,
// we use a hardcoded value 'biz_1' to represent the active client.
return mock.getHubs('biz_1');
Future<List<domain.Hub>> getHubs() async {
final business = await _getBusinessForCurrentUser();
final teamId = await _getOrCreateTeamId(business);
return _fetchHubsForTeam(teamId: teamId, businessId: business.id);
}
@override
Future<Hub> createHub({required String name, required String address}) {
// Delegates hub creation to the mock repository.
return mock.createHub(businessId: 'biz_1', name: name, address: address);
Future<domain.Hub> createHub({
required String name,
required String address,
}) async {
final business = await _getBusinessForCurrentUser();
final teamId = await _getOrCreateTeamId(business);
final city = business.city;
final result = await _dataConnect
.createTeamHub(
teamId: teamId,
hubName: name,
address: address,
)
.city(city?.isNotEmpty == true ? city : '')
.execute();
final createdId = result.data?.teamHub_insert.id;
if (createdId == null) {
throw Exception('Hub creation failed.');
}
final hubs = await _fetchHubsForTeam(
teamId: teamId,
businessId: business.id,
);
domain.Hub? createdHub;
for (final hub in hubs) {
if (hub.id == createdId) {
createdHub = hub;
break;
}
}
return createdHub ??
domain.Hub(
id: createdId,
businessId: business.id,
name: name,
address: address,
nfcTagId: null,
status: domain.HubStatus.active,
);
}
@override
Future<void> deleteHub(String id) {
// Delegates hub deletion to the mock repository.
return mock.deleteHub(id);
Future<void> deleteHub(String id) async {
await _dataConnect.deleteTeamHub(id: id).execute();
}
@override
Future<void> assignNfcTag({required String hubId, required String nfcTagId}) {
// Delegates NFC tag assignment to the mock repository.
return mock.assignNfcTag(hubId: hubId, nfcTagId: nfcTagId);
Future<void> assignNfcTag({
required String hubId,
required String nfcTagId,
}) {
throw UnimplementedError('NFC tag assignment is not supported for team hubs.');
}
Future<dc.GetBusinessesByUserIdBusinesses> _getBusinessForCurrentUser() async {
final user = _firebaseAuth.currentUser;
if (user == null) {
throw Exception('User is not authenticated.');
}
final result = await _dataConnect.getBusinessesByUserId(
userId: user.uid,
).execute();
if (result.data.businesses.isEmpty) {
await _firebaseAuth.signOut();
throw Exception('No business found for this user. Please sign in again.');
}
return result.data.businesses.first;
}
Future<String> _getOrCreateTeamId(
dc.GetBusinessesByUserIdBusinesses business,
) async {
final teamsResult = await _dataConnect.getTeamsByOwnerId(
ownerId: business.id,
).execute();
if (teamsResult.data.teams.isNotEmpty) {
return teamsResult.data.teams.first.id;
}
final createTeamBuilder = _dataConnect.createTeam(
teamName: '${business.businessName} Team',
ownerId: business.id,
ownerName: business.contactName ?? '',
ownerRole: 'OWNER',
);
if (business.email != null) {
createTeamBuilder.email(business.email);
}
final createTeamResult = await createTeamBuilder.execute();
final teamId = createTeamResult.data?.team_insert.id;
if (teamId == null) {
throw Exception('Team creation failed.');
}
return teamId;
}
Future<List<domain.Hub>> _fetchHubsForTeam({
required String teamId,
required String businessId,
}) async {
final hubsResult = await _dataConnect.getTeamHubsByTeamId(
teamId: teamId,
).execute();
return hubsResult.data.teamHubs
.map(
(hub) => domain.Hub(
id: hub.id,
businessId: businessId,
name: hub.hubName,
address: hub.address,
nfcTagId: null,
status:
hub.isActive
? domain.HubStatus.active
: domain.HubStatus.inactive,
),
)
.toList();
}
}

View File

@@ -2,7 +2,6 @@ import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_modular/flutter_modular.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:core_localization/core_localization.dart';
import '../blocs/client_hubs_bloc.dart';
import '../blocs/client_hubs_event.dart';
@@ -47,14 +46,26 @@ class ClientHubsPage extends StatelessWidget {
},
builder: (context, state) {
return Scaffold(
backgroundColor: const Color(0xFFF8FAFC), // slate-50
backgroundColor: UiColors.bgMenu,
floatingActionButton: FloatingActionButton(
onPressed: () => BlocProvider.of<ClientHubsBloc>(
context,
).add(const ClientHubsAddDialogToggled(visible: true)),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(8)),
),
child: const Icon(UiIcons.add),
),
body: Stack(
children: [
children: <Widget>[
CustomScrollView(
slivers: [
slivers: <Widget>[
_buildAppBar(context),
SliverPadding(
padding: const EdgeInsets.fromLTRB(20, 20, 20, 100),
padding: const EdgeInsets.symmetric(
horizontal: UiConstants.space5,
vertical: UiConstants.space5,
).copyWith(bottom: 100),
sliver: SliverList(
delegate: SliverChildListDelegate([
if (state.status == ClientHubsStatus.loading)
@@ -85,7 +96,7 @@ class ClientHubsPage extends StatelessWidget {
),
),
],
const SizedBox(height: 20),
const SizedBox(height: UiConstants.space5),
const HubInfoCard(),
]),
),
@@ -120,7 +131,7 @@ class ClientHubsPage extends StatelessWidget {
),
if (state.status == ClientHubsStatus.actionInProgress)
Container(
color: Colors.black.withValues(alpha: 0.1),
color: UiColors.black.withValues(alpha: 0.1),
child: const Center(child: CircularProgressIndicator()),
),
],
@@ -133,20 +144,19 @@ class ClientHubsPage extends StatelessWidget {
Widget _buildAppBar(BuildContext context) {
return SliverAppBar(
backgroundColor: const Color(0xFF121826),
backgroundColor: UiColors.foreground, // Dark Slate equivalent
automaticallyImplyLeading: false,
expandedHeight: 140,
pinned: true,
flexibleSpace: FlexibleSpaceBar(
background: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [Color(0xFF121826), Color(0xFF1E293B)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
color: UiColors.foreground,
padding: const EdgeInsets.fromLTRB(
UiConstants.space5,
UiConstants.space12,
UiConstants.space5,
0,
),
padding: const EdgeInsets.fromLTRB(20, 48, 20, 0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -156,51 +166,35 @@ class ClientHubsPage extends StatelessWidget {
width: 40,
height: 40,
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.2),
color: UiColors.white.withValues(alpha: 0.2),
shape: BoxShape.circle,
),
child: const Icon(
LucideIcons.arrowLeft,
color: Colors.white,
UiIcons.arrowLeft,
color: UiColors.white,
size: 20,
),
),
),
const SizedBox(height: 16),
const SizedBox(height: UiConstants.space4),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
children: <Widget>[
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
children: <Widget>[
Text(
t.client_hubs.title,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 24,
),
style: UiTypography.headline1m.white,
),
Text(
t.client_hubs.subtitle,
style: const TextStyle(
color: Color(0xFFCBD5E1), // slate-300
fontSize: 14,
style: UiTypography.body2r.copyWith(
color: UiColors.switchInactive,
),
),
],
),
UiButton.primary(
onPressed: () => BlocProvider.of<ClientHubsBloc>(
context,
).add(const ClientHubsAddDialogToggled(visible: true)),
text: t.client_hubs.add_hub,
leadingIcon: LucideIcons.plus,
style: ElevatedButton.styleFrom(
minimumSize: Size(0, 40),
maximumSize: Size.fromHeight(40),
),
),
],
),
],

View File

@@ -42,20 +42,17 @@ class _AddHubDialogState extends State<AddHubDialog> {
@override
Widget build(BuildContext context) {
return Container(
color: Colors.black.withValues(alpha: 0.5),
color: UiColors.bgOverlay,
child: Center(
child: SingleChildScrollView(
child: Container(
width: MediaQuery.of(context).size.width * 0.9,
padding: const EdgeInsets.all(24),
padding: const EdgeInsets.all(UiConstants.space5),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(24),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.1),
blurRadius: 20,
),
color: UiColors.bgPopup,
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
boxShadow: const [
BoxShadow(color: UiColors.popupShadow, blurRadius: 20),
],
),
child: Column(
@@ -64,29 +61,27 @@ class _AddHubDialogState extends State<AddHubDialog> {
children: [
Text(
t.client_hubs.add_hub_dialog.title,
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Color(0xFF0F172A),
),
style: UiTypography.headline3m.textPrimary,
),
const SizedBox(height: 24),
const SizedBox(height: UiConstants.space5),
_buildFieldLabel(t.client_hubs.add_hub_dialog.name_label),
TextField(
controller: _nameController,
style: UiTypography.body1r.textPrimary,
decoration: _buildInputDecoration(
t.client_hubs.add_hub_dialog.name_hint,
),
),
const SizedBox(height: 16),
const SizedBox(height: UiConstants.space4),
_buildFieldLabel(t.client_hubs.add_hub_dialog.address_label),
TextField(
controller: _addressController,
style: UiTypography.body1r.textPrimary,
decoration: _buildInputDecoration(
t.client_hubs.add_hub_dialog.address_hint,
),
),
const SizedBox(height: 32),
const SizedBox(height: UiConstants.space8),
Row(
children: [
Expanded(
@@ -95,7 +90,7 @@ class _AddHubDialogState extends State<AddHubDialog> {
text: t.common.cancel,
),
),
const SizedBox(width: 12),
const SizedBox(width: UiConstants.space3),
Expanded(
child: UiButton.primary(
onPressed: () {
@@ -121,36 +116,32 @@ class _AddHubDialogState extends State<AddHubDialog> {
Widget _buildFieldLabel(String label) {
return Padding(
padding: const EdgeInsets.only(bottom: 6),
child: Text(
label,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Color(0xFF0F172A),
),
),
padding: const EdgeInsets.only(bottom: UiConstants.space2),
child: Text(label, style: UiTypography.body2m.textPrimary),
);
}
InputDecoration _buildInputDecoration(String hint) {
return InputDecoration(
hintText: hint,
hintStyle: const TextStyle(color: Color(0xFF94A3B8), fontSize: 14),
hintStyle: UiTypography.body2r.textPlaceholder,
filled: true,
fillColor: const Color(0xFFF8FAFC),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
fillColor: UiColors.input,
contentPadding: const EdgeInsets.symmetric(
horizontal: UiConstants.space4,
vertical: 14,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: Color(0xFFE2E8F0)),
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
borderSide: const BorderSide(color: UiColors.border),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: Color(0xFFE2E8F0)),
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
borderSide: const BorderSide(color: UiColors.border),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: Color(0xFF2563EB), width: 2),
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
borderSide: const BorderSide(color: UiColors.ring, width: 2),
),
);
}

View File

@@ -1,6 +1,6 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:krow_domain/krow_domain.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:core_localization/core_localization.dart';
/// A card displaying information about a single hub.
@@ -27,68 +27,56 @@ class HubCard extends StatelessWidget {
final bool hasNfc = hub.nfcTagId != null;
return Container(
margin: const EdgeInsets.only(bottom: 12),
margin: const EdgeInsets.only(bottom: UiConstants.space3),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
color: UiColors.white,
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
boxShadow: const [
BoxShadow(
color: Colors.black.withValues(alpha: 0.04),
color: UiColors.popupShadow,
blurRadius: 10,
offset: const Offset(0, 4),
offset: Offset(0, 4),
),
],
),
child: Padding(
padding: const EdgeInsets.all(16),
padding: const EdgeInsets.all(UiConstants.space4),
child: Row(
children: [
Container(
width: 52,
height: 52,
decoration: BoxDecoration(
color: const Color(0xFFEFF6FF), // blue-50
borderRadius: BorderRadius.circular(16),
color: UiColors.tagInProgress,
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
),
child: Icon(
hasNfc ? LucideIcons.checkCircle : LucideIcons.nfc,
color: hasNfc
? const Color(0xFF16A34A)
: const Color(0xFF94A3B8), // green-600 or slate-400
hasNfc ? UiIcons.success : UiIcons.nfc,
color: hasNfc ? UiColors.iconSuccess : UiColors.iconThird,
size: 24,
),
),
const SizedBox(width: 16),
const SizedBox(width: UiConstants.space4),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
hub.name,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
color: Color(0xFF0F172A),
),
),
Text(hub.name, style: UiTypography.body1b.textPrimary),
if (hub.address.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 4),
padding: const EdgeInsets.only(top: UiConstants.space1),
child: Row(
children: [
const Icon(
LucideIcons.mapPin,
UiIcons.mapPin,
size: 12,
color: Color(0xFF94A3B8),
color: UiColors.iconThird,
),
const SizedBox(width: 4),
const SizedBox(width: UiConstants.space1),
Expanded(
child: Text(
hub.address,
style: const TextStyle(
color: Color(0xFF64748B),
fontSize: 12,
),
style: UiTypography.footnote1r.textSecondary,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
@@ -98,14 +86,12 @@ class HubCard extends StatelessWidget {
),
if (hasNfc)
Padding(
padding: const EdgeInsets.only(top: 4),
padding: const EdgeInsets.only(top: UiConstants.space1),
child: Text(
t.client_hubs.hub_card.tag_label(id: hub.nfcTagId!),
style: const TextStyle(
color: Color(0xFF16A34A),
fontSize: 12,
style: UiTypography.footnote1b.copyWith(
color: UiColors.textSuccess,
fontFamily: 'monospace',
fontWeight: FontWeight.bold,
),
),
),
@@ -117,20 +103,20 @@ class HubCard extends StatelessWidget {
IconButton(
onPressed: onNfcPressed,
icon: const Icon(
LucideIcons.nfc,
color: Color(0xFF2563EB),
UiIcons.nfc,
color: UiColors.primary,
size: 20,
),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
splashRadius: 20,
),
const SizedBox(width: 8),
const SizedBox(width: UiConstants.space2),
IconButton(
onPressed: onDeletePressed,
icon: const Icon(
LucideIcons.trash2,
color: Color(0xFFDC2626),
UiIcons.delete,
color: UiColors.destructive,
size: 20,
),
padding: EdgeInsets.zero,

View File

@@ -1,6 +1,5 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:core_localization/core_localization.dart';
/// Widget displayed when there are no hubs.
@@ -14,15 +13,15 @@ class HubEmptyState extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(32),
padding: const EdgeInsets.all(UiConstants.space8),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
color: UiColors.bgBanner,
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
boxShadow: const [
BoxShadow(
color: Colors.black.withValues(alpha: 0.04),
color: UiColors.popupShadow,
blurRadius: 10,
offset: const Offset(0, 4),
offset: Offset(0, 4),
),
],
),
@@ -32,35 +31,27 @@ class HubEmptyState extends StatelessWidget {
width: 64,
height: 64,
decoration: const BoxDecoration(
color: Color(0xFFF1F5F9), // slate-100
color: UiColors.secondary,
shape: BoxShape.circle,
),
child: const Icon(
LucideIcons.nfc,
size: 32,
color: Color(0xFF94A3B8),
),
child: const Icon(UiIcons.nfc, size: 32, color: UiColors.iconThird),
),
const SizedBox(height: 16),
const SizedBox(height: UiConstants.space4),
Text(
t.client_hubs.empty_state.title,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Color(0xFF0F172A),
),
style: UiTypography.title1m.textPrimary,
),
const SizedBox(height: 8),
const SizedBox(height: UiConstants.space2),
Text(
t.client_hubs.empty_state.description,
textAlign: TextAlign.center,
style: const TextStyle(color: Color(0xFF64748B), fontSize: 14),
style: UiTypography.body2r.textSecondary,
),
const SizedBox(height: 24),
const SizedBox(height: UiConstants.space5),
UiButton.primary(
onPressed: onAddPressed,
text: t.client_hubs.empty_state.button,
leadingIcon: LucideIcons.plus,
leadingIcon: UiIcons.add,
),
],
),

View File

@@ -1,5 +1,5 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:core_localization/core_localization.dart';
/// A card with information about how hubs work.
@@ -10,34 +10,29 @@ class HubInfoCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(16),
padding: const EdgeInsets.all(UiConstants.space4),
decoration: BoxDecoration(
color: const Color(0xFFEFF6FF), // blue-50
borderRadius: BorderRadius.circular(16),
color: UiColors.tagInProgress,
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Icon(LucideIcons.nfc, size: 20, color: Color(0xFF2563EB)),
const SizedBox(width: 12),
const Icon(UiIcons.nfc, size: 20, color: UiColors.primary),
const SizedBox(width: UiConstants.space3),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
t.client_hubs.about_hubs.title,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
color: Color(0xFF0F172A),
),
style: UiTypography.body2b.textPrimary,
),
const SizedBox(height: 4),
const SizedBox(height: UiConstants.space1),
Text(
t.client_hubs.about_hubs.description,
style: const TextStyle(
color: Color(0xFF334155),
fontSize: 12,
style: UiTypography.footnote1r.copyWith(
color: UiColors.textSecondary,
height: 1.4,
),
),

View File

@@ -1,6 +1,5 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:krow_domain/krow_domain.dart';
import 'package:core_localization/core_localization.dart';
@@ -40,20 +39,17 @@ class _IdentifyNfcDialogState extends State<IdentifyNfcDialog> {
@override
Widget build(BuildContext context) {
return Container(
color: Colors.black.withValues(alpha: 0.5),
color: UiColors.bgOverlay,
child: Center(
child: SingleChildScrollView(
child: Container(
width: MediaQuery.of(context).size.width * 0.9,
padding: const EdgeInsets.all(24),
padding: const EdgeInsets.all(UiConstants.space5),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(24),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.1),
blurRadius: 20,
),
color: UiColors.bgPopup,
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
boxShadow: const [
BoxShadow(color: UiColors.popupShadow, blurRadius: 20),
],
),
child: Column(
@@ -61,57 +57,45 @@ class _IdentifyNfcDialogState extends State<IdentifyNfcDialog> {
children: [
Text(
t.client_hubs.nfc_dialog.title,
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Color(0xFF0F172A),
),
style: UiTypography.headline3m.textPrimary,
),
const SizedBox(height: 32),
const SizedBox(height: UiConstants.space8),
Container(
width: 80,
height: 80,
decoration: const BoxDecoration(
color: Color(0xFFEFF6FF), // blue-50
color: UiColors.tagInProgress,
shape: BoxShape.circle,
),
child: const Icon(
LucideIcons.nfc,
UiIcons.nfc,
size: 40,
color: Color(0xFF2563EB),
color: UiColors.primary,
),
),
const SizedBox(height: 16),
Text(
widget.hub.name,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
color: Color(0xFF0F172A),
),
),
const SizedBox(height: 8),
const SizedBox(height: UiConstants.space4),
Text(widget.hub.name, style: UiTypography.body1b.textPrimary),
const SizedBox(height: UiConstants.space2),
Text(
t.client_hubs.nfc_dialog.instruction,
textAlign: TextAlign.center,
style: const TextStyle(
color: Color(0xFF64748B),
fontSize: 14,
),
style: UiTypography.body2m.textSecondary,
),
const SizedBox(height: 24),
const SizedBox(height: UiConstants.space5),
UiButton.secondary(
onPressed: _simulateNFCScan,
text: t.client_hubs.nfc_dialog.scan_button,
leadingIcon: LucideIcons.nfc,
leadingIcon: UiIcons.nfc,
),
if (_nfcTagId != null) ...[
const SizedBox(height: 24),
const SizedBox(height: UiConstants.space6),
Container(
padding: const EdgeInsets.all(16),
padding: const EdgeInsets.all(UiConstants.space4),
decoration: BoxDecoration(
color: const Color(0xFFF0FDF4), // green-50
borderRadius: BorderRadius.circular(16),
color: UiColors.tagSuccess,
borderRadius: BorderRadius.circular(
UiConstants.radiusBase,
),
),
child: Column(
children: [
@@ -119,39 +103,35 @@ class _IdentifyNfcDialogState extends State<IdentifyNfcDialog> {
mainAxisSize: MainAxisSize.min,
children: [
const Icon(
LucideIcons.checkCircle,
UiIcons.success,
size: 20,
color: Color(0xFF16A34A),
color: UiColors.textSuccess,
),
const SizedBox(width: 8),
const SizedBox(width: UiConstants.space2),
Text(
t.client_hubs.nfc_dialog.tag_identified,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
color: Color(0xFF0F172A),
),
style: UiTypography.body2b.textPrimary,
),
],
),
const SizedBox(height: 8),
const SizedBox(height: UiConstants.space2),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
horizontal: UiConstants.space3,
vertical: UiConstants.space2,
),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: const Color(0xFFDCE8E0)),
color: UiColors.white,
borderRadius: BorderRadius.circular(
UiConstants.radiusMdValue + 2,
),
border: Border.all(color: UiColors.border),
),
child: Text(
_nfcTagId!,
style: const TextStyle(
style: UiTypography.footnote1b.copyWith(
fontFamily: 'monospace',
fontWeight: FontWeight.bold,
fontSize: 12,
color: Color(0xFF334155),
color: UiColors.textPrimary,
),
),
),
@@ -159,7 +139,7 @@ class _IdentifyNfcDialogState extends State<IdentifyNfcDialog> {
),
),
],
const SizedBox(height: 32),
const SizedBox(height: UiConstants.space8),
Row(
children: [
Expanded(
@@ -168,7 +148,7 @@ class _IdentifyNfcDialogState extends State<IdentifyNfcDialog> {
text: t.common.cancel,
),
),
const SizedBox(width: 12),
const SizedBox(width: UiConstants.space3),
Expanded(
child: UiButton.primary(
onPressed: _nfcTagId != null

View File

@@ -16,7 +16,7 @@ dependencies:
equatable: ^2.0.5
lucide_icons: ^0.257.0
# KROW Packages
# Architecture Packages
krow_core:
path: ../../../core
krow_domain:

View File

@@ -1,5 +1,5 @@
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter_modular/flutter_modular.dart';
import 'package:krow_data_connect/krow_data_connect.dart';
import 'src/data/repositories_impl/settings_repository_impl.dart';
import 'src/domain/repositories/settings_repository_interface.dart';
import 'src/domain/usecases/sign_out_usecase.dart';
@@ -8,14 +8,11 @@ import 'src/presentation/pages/client_settings_page.dart';
/// A [Module] for the client settings feature.
class ClientSettingsModule extends Module {
@override
List<Module> get imports => [DataConnectModule()];
@override
void binds(Injector i) {
// Repositories
i.addLazySingleton<SettingsRepositoryInterface>(
() => SettingsRepositoryImpl(mock: i.get<AuthRepositoryMock>()),
() => SettingsRepositoryImpl(firebaseAuth: FirebaseAuth.instance),
);
// UseCases

View File

@@ -1,19 +1,23 @@
import 'package:krow_data_connect/krow_data_connect.dart';
import 'package:firebase_auth/firebase_auth.dart';
import '../../domain/repositories/settings_repository_interface.dart';
/// Implementation of [SettingsRepositoryInterface].
///
/// This implementation delegates data access to the [AuthRepositoryMock]
/// from the `data_connect` package.
/// This implementation delegates authentication operations to [FirebaseAuth].
class SettingsRepositoryImpl implements SettingsRepositoryInterface {
/// The auth mock from data connect.
final AuthRepositoryMock mock;
/// The Firebase Auth instance.
final FirebaseAuth _firebaseAuth;
/// Creates a [SettingsRepositoryImpl] with the required [mock].
SettingsRepositoryImpl({required this.mock});
/// Creates a [SettingsRepositoryImpl] with the required [_firebaseAuth].
SettingsRepositoryImpl({required FirebaseAuth firebaseAuth})
: _firebaseAuth = firebaseAuth;
@override
Future<void> signOut() {
return mock.signOut();
Future<void> signOut() async {
try {
await _firebaseAuth.signOut();
} catch (e) {
throw Exception('Error signing out: ${e.toString()}');
}
}
}

View File

@@ -17,12 +17,16 @@ class ClientSettingsPage extends StatelessWidget {
const ClientSettingsPage({super.key});
@override
/// Builds the client settings page UI.
Widget build(BuildContext context) {
return BlocProvider<ClientSettingsBloc>(
create: (context) => Modular.get<ClientSettingsBloc>(),
child: BlocListener<ClientSettingsBloc, ClientSettingsState>(
listener: (context, state) {
if (state is ClientSettingsSignOutSuccess) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Signed out successfully')),
);
Modular.to.navigate('/');
}
if (state is ClientSettingsError) {

View File

@@ -1,7 +1,9 @@
import 'package:client_settings/src/presentation/navigation/client_settings_navigator.dart';
import 'package:core_localization/core_localization.dart';
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_modular/flutter_modular.dart';
import '../../blocs/client_settings_bloc.dart';
/// A widget that displays the primary actions for the settings page.
@@ -10,27 +12,38 @@ class SettingsActions extends StatelessWidget {
const SettingsActions({super.key});
@override
/// Builds the settings actions UI.
Widget build(BuildContext context) {
final labels = t.client_settings.profile;
// Get the translations for the client settings profile.
final TranslationsClientSettingsProfileEn labels =
t.client_settings.profile;
return SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space5),
sliver: SliverList(
delegate: SliverChildListDelegate([
delegate: SliverChildListDelegate(<Widget>[
const SizedBox(height: UiConstants.space5),
// Edit profile button
UiButton.primary(text: labels.edit_profile, onPressed: () {}),
const SizedBox(height: UiConstants.space4),
UiButton.primary(text: labels.hubs, onPressed: () {}),
// Hubs button
UiButton.primary(
text: labels.hubs,
onPressed: () => Modular.to.pushHubs(),
),
const SizedBox(height: UiConstants.space4),
// Log out button
BlocBuilder<ClientSettingsBloc, ClientSettingsState>(
builder: (context, state) {
builder: (BuildContext context, ClientSettingsState state) {
return UiButton.secondary(
text: labels.log_out,
onPressed: state is ClientSettingsLoading
? null
: () => BlocProvider.of<ClientSettingsBloc>(
context,
).add(const ClientSettingsSignOutRequested()),
: () => _showSignOutDialog(context),
);
},
),
@@ -38,4 +51,42 @@ class SettingsActions extends StatelessWidget {
),
);
}
/// Shows a confirmation dialog for signing out.
Future<void> _showSignOutDialog(BuildContext context) {
return showDialog(
context: context,
builder: (BuildContext context) => AlertDialog(
backgroundColor: UiColors.bgPopup,
elevation: 0,
shape: RoundedRectangleBorder(borderRadius: UiConstants.radiusLg),
title: Text(
t.client_settings.profile.log_out,
style: UiTypography.headline3m.textPrimary,
),
content: Text(
'Are you sure you want to log out?',
style: UiTypography.body2r.textSecondary,
),
actions: <Widget>[
// Log out button
UiButton.secondary(
text: t.client_settings.profile.log_out,
onPressed: () {
Modular.to.pop();
BlocProvider.of<ClientSettingsBloc>(
context,
).add(const ClientSettingsSignOutRequested());
},
),
// Cancel button
UiButton.secondary(
text: t.common.cancel,
onPressed: () => Modular.to.pop(),
),
],
),
);
}
}

View File

@@ -9,68 +9,66 @@ class SettingsProfileHeader extends StatelessWidget {
const SettingsProfileHeader({super.key});
@override
/// Builds the profile header UI.
Widget build(BuildContext context) {
final labels = t.client_settings.profile;
return SliverAppBar(
backgroundColor: UiColors.primary,
expandedHeight: 200,
backgroundColor: UiColors.bgSecondary,
expandedHeight: 140,
pinned: true,
elevation: 0,
shape: const Border(bottom: BorderSide(color: UiColors.border, width: 1)),
leading: IconButton(
icon: const Icon(UiIcons.chevronLeft, color: UiColors.textSecondary),
onPressed: () => Modular.to.pop(),
),
flexibleSpace: FlexibleSpaceBar(
background: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [UiColors.primary, Color(0xFF0047FF)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space8),
margin: const EdgeInsets.only(top: UiConstants.space16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.start,
spacing: UiConstants.space4,
children: [
const SizedBox(height: UiConstants.space5 * 2),
Container(
width: 80,
height: 80,
width: 64,
height: 64,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: UiColors.white.withValues(alpha: 0.24),
width: 4,
),
border: Border.all(color: UiColors.border, width: 2),
color: UiColors.white,
),
child: const Center(
child: Center(
child: Text(
'C',
style: TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
style: UiTypography.headline1m.copyWith(
color: UiColors.primary,
),
),
),
),
const SizedBox(height: UiConstants.space3),
Text(
'Your Company',
style: UiTypography.body1b.copyWith(color: UiColors.white),
),
const SizedBox(height: UiConstants.space1),
Row(
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
UiIcons.mail,
size: 14,
color: UiColors.white.withValues(alpha: 0.7),
),
const SizedBox(width: UiConstants.space2),
Text(
'client@example.com',
style: UiTypography.footnote1r.copyWith(
color: UiColors.white.withValues(alpha: 0.7),
),
Text('Your Company', style: UiTypography.body1b.textPrimary),
const SizedBox(height: UiConstants.space1),
Row(
mainAxisAlignment: MainAxisAlignment.start,
spacing: UiConstants.space1,
children: [
Icon(
UiIcons.mail,
size: 14,
color: UiColors.textSecondary,
),
Text(
'client@example.com',
style: UiTypography.footnote1r.textSecondary,
),
],
),
],
),
@@ -78,14 +76,7 @@ class SettingsProfileHeader extends StatelessWidget {
),
),
),
leading: IconButton(
icon: const Icon(UiIcons.arrowLeft, color: UiColors.white),
onPressed: () => Modular.to.pop(),
),
title: Text(
labels.title,
style: UiTypography.body1b.copyWith(color: UiColors.white),
),
title: Text(labels.title, style: UiTypography.body1b.textPrimary),
);
}
}

View File

@@ -10,6 +10,7 @@ class SettingsQuickLinks extends StatelessWidget {
const SettingsQuickLinks({super.key});
@override
/// Builds the quick links UI.
Widget build(BuildContext context) {
final labels = t.client_settings.profile;
@@ -54,10 +55,16 @@ class SettingsQuickLinks extends StatelessWidget {
/// Internal widget for a single quick link item.
class _QuickLinkItem extends StatelessWidget {
/// The icon to display.
final IconData icon;
/// The title of the link.
final String title;
/// Callback when the link is tapped.
final VoidCallback onTap;
/// Creates a [_QuickLinkItem].
const _QuickLinkItem({
required this.icon,
required this.title,
@@ -65,6 +72,7 @@ class _QuickLinkItem extends StatelessWidget {
});
@override
/// Builds the quick link item UI.
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,

View File

@@ -15,7 +15,9 @@ dependencies:
flutter_modular: ^6.3.0
equatable: ^2.0.5
lucide_icons: ^0.257.0
firebase_auth: ^6.1.2
# Architecture Packages
design_system:
path: ../../../design_system
core_localization:

View File

@@ -185,6 +185,13 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.4.2"
client_create_order:
dependency: transitive
description:
path: "packages/features/client/create_order"
relative: true
source: path
version: "0.0.1"
clock:
dependency: transitive
description:
@@ -415,7 +422,7 @@ packages:
source: hosted
version: "8.1.6"
flutter_lints:
dependency: transitive
dependency: "direct dev"
description:
name: flutter_lints
sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1"

View File

@@ -20,6 +20,7 @@ workspace:
dev_dependencies:
melos: ^7.3.0
flutter_lints: ^6.0.0
melos:
scripts: