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

View File

@@ -7,8 +7,9 @@ KROW is a comprehensive workforce management platform designed to streamline ope
### 📦 Apps (`/apps`) ### 📦 Apps (`/apps`)
These are the production-ready applications for our users: These are the production-ready applications for our users:
- **`web-dashboard/`**: The primary React/Vite dashboard for Admin, Vendors, and Clients. - **`web-dashboard/`**: The primary React/Vite dashboard for Admin, Vendors, and Clients.
- **`mobile-client/`**: Flutter application for final clients to manage orders and billing. - **`mobile/`**: Flutter applications for client and staff.
- **`mobile-staff/`**: Flutter application for staff members (scheduling, clock-in/out, earnings). - `client`: The application for final clients to manage orders and billing.
- `staff`: The application for staff members (scheduling, clock-in/out, earnings).
### ⚙️ Backend (`/backend`) ### ⚙️ Backend (`/backend`)
The core data engine powering all applications: The core data engine powering all applications:

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

View File

@@ -10,23 +10,31 @@ environment:
dependencies: dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
cupertino_icons: ^1.0.8
# Architecture Packages
design_system: design_system:
path: ../../packages/design_system path: ../../packages/design_system
core_localization:
path: ../../packages/core_localization
# Feature Packages
client_authentication: client_authentication:
path: ../../packages/features/client/authentication path: ../../packages/features/client/authentication
client_home: client_home:
path: ../../packages/features/client/home path: ../../packages/features/client/home
client_settings: client_settings:
path: ../../packages/features/client/settings path: ../../packages/features/client/settings
core_localization:
path: ../../packages/core_localization
client_hubs: client_hubs:
path: ../../packages/features/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_modular: ^6.3.2
flutter_bloc: ^8.1.3 flutter_bloc: ^8.1.3
flutter_localizations: flutter_localizations:
sdk: flutter sdk: flutter
firebase_core: ^4.4.0
dev_dependencies: dev_dependencies:
flutter_test: 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,13 +13,17 @@ dependencies:
sdk: flutter sdk: flutter
cupertino_icons: ^1.0.8 cupertino_icons: ^1.0.8
flutter_modular: ^6.3.0 flutter_modular: ^6.3.0
# Architecture Packages
design_system: design_system:
path: ../../packages/design_system path: ../../packages/design_system
staff_authentication:
path: ../../packages/features/staff/authentication
core_localization: core_localization:
path: ../../packages/core_localization path: ../../packages/core_localization
# Feature Packages
staff_authentication:
path: ../../packages/features/staff/authentication
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter

View File

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

View File

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

View File

@@ -232,6 +232,20 @@
"tag_identified": "Tag Identified", "tag_identified": "Tag Identified",
"assign_button": "Assign Tag" "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", "tag_identified": "Etiqueta Identificada",
"assign_button": "Asignar Etiqueta" "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 # Basic Usage
```dart ```dart
ExampleConnector.instance.createDocument(createDocumentVariables).execute(); ExampleConnector.instance.createTeamHudDepartment(createTeamHudDepartmentVariables).execute();
ExampleConnector.instance.updateDocument(updateDocumentVariables).execute(); ExampleConnector.instance.updateTeamHudDepartment(updateTeamHudDepartmentVariables).execute();
ExampleConnector.instance.deleteDocument(deleteDocumentVariables).execute(); ExampleConnector.instance.deleteTeamHudDepartment(deleteTeamHudDepartmentVariables).execute();
ExampleConnector.instance.createConversation(createConversationVariables).execute(); ExampleConnector.instance.listAssignments(listAssignmentsVariables).execute();
ExampleConnector.instance.updateConversation(updateConversationVariables).execute(); ExampleConnector.instance.getAssignmentById(getAssignmentByIdVariables).execute();
ExampleConnector.instance.updateConversationLastMessage(updateConversationLastMessageVariables).execute(); ExampleConnector.instance.listAssignmentsByWorkforceId(listAssignmentsByWorkforceIdVariables).execute();
ExampleConnector.instance.deleteConversation(deleteConversationVariables).execute(); ExampleConnector.instance.listAssignmentsByWorkforceIds(listAssignmentsByWorkforceIdsVariables).execute();
ExampleConnector.instance.listHubs().execute(); ExampleConnector.instance.listAssignmentsByShiftRole(listAssignmentsByShiftRoleVariables).execute();
ExampleConnector.instance.getHubById(getHubByIdVariables).execute(); ExampleConnector.instance.filterAssignments(filterAssignmentsVariables).execute();
ExampleConnector.instance.getHubsByOwnerId(getHubsByOwnerIdVariables).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: This is an example of a mutation with an optional field:
```dart ```dart
await ExampleConnector.instance.filterVendorBenefitPlans({ ... }) await ExampleConnector.instance.updateShift({ ... })
.vendorId(...) .title(...)
.execute(); .execute();
``` ```

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

@@ -20,9 +20,13 @@ dependencies:
# Architecture Packages # Architecture Packages
design_system: design_system:
path: ../../../../design_system path: ../../../design_system
core_localization: core_localization:
path: ../../../core_localization path: ../../../core_localization
krow_data_connect:
path: ../../../data_connect
krow_domain:
path: ../../../domain
dev_dependencies: dev_dependencies:
flutter_test: 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'; import 'package:flutter_modular/flutter_modular.dart';
/// Extension on [IModularNavigator] to provide strongly-typed navigation
/// for the client home feature.
extension ClientHomeNavigator on IModularNavigator { extension ClientHomeNavigator on IModularNavigator {
/// Navigates to the client home page.
void pushClientHome() {
pushNamed('/client-home/');
}
/// Navigates to the settings page.
void pushSettings() { void pushSettings() {
pushNamed('/client-settings/'); 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) { switch (id) {
case 'actions': case 'actions':
return ActionsWidget( return ActionsWidget(
onRapidPressed: () {}, onRapidPressed: () => Modular.to.pushRapidOrder(),
onCreateOrderPressed: () => _openOrderFormSheet(context, null), onCreateOrderPressed: () => Modular.to.pushCreateOrder(),
); );
case 'reorder': case 'reorder':
return ReorderWidget( return ReorderWidget(

View File

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

View File

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

View File

@@ -1,45 +1,151 @@
import 'package:krow_data_connect/krow_data_connect.dart'; import 'package:firebase_auth/firebase_auth.dart' as firebase;
import 'package:krow_domain/krow_domain.dart'; 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'; import '../../domain/repositories/hub_repository_interface.dart';
/// Implementation of [HubRepositoryInterface] that uses [BusinessRepositoryMock]. /// Implementation of [HubRepositoryInterface] backed by Data Connect.
///
/// 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.
class HubRepositoryImpl implements HubRepositoryInterface { class HubRepositoryImpl implements HubRepositoryInterface {
/// The business repository mock from data connect. final firebase.FirebaseAuth _firebaseAuth;
final BusinessRepositoryMock mock; final dc.ExampleConnector _dataConnect;
/// Creates a [HubRepositoryImpl] instance. HubRepositoryImpl({
/// required firebase.FirebaseAuth firebaseAuth,
/// Takes a [BusinessRepositoryMock] as a dependency to perform data operations. required dc.ExampleConnector dataConnect,
HubRepositoryImpl({required this.mock}); }) : _firebaseAuth = firebaseAuth,
_dataConnect = dataConnect;
@override @override
Future<List<Hub>> getHubs() { Future<List<domain.Hub>> getHubs() async {
// In a production environment, the business ID would be retrieved from final business = await _getBusinessForCurrentUser();
// a session manager or authentication state. For the current mock strategy, final teamId = await _getOrCreateTeamId(business);
// we use a hardcoded value 'biz_1' to represent the active client. return _fetchHubsForTeam(teamId: teamId, businessId: business.id);
return mock.getHubs('biz_1');
} }
@override @override
Future<Hub> createHub({required String name, required String address}) { Future<domain.Hub> createHub({
// Delegates hub creation to the mock repository. required String name,
return mock.createHub(businessId: 'biz_1', name: name, address: address); 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 @override
Future<void> deleteHub(String id) { Future<void> deleteHub(String id) async {
// Delegates hub deletion to the mock repository. await _dataConnect.deleteTeamHub(id: id).execute();
return mock.deleteHub(id);
} }
@override @override
Future<void> assignNfcTag({required String hubId, required String nfcTagId}) { Future<void> assignNfcTag({
// Delegates NFC tag assignment to the mock repository. required String hubId,
return mock.assignNfcTag(hubId: hubId, nfcTagId: nfcTagId); 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/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_modular/flutter_modular.dart'; import 'package:flutter_modular/flutter_modular.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:core_localization/core_localization.dart'; import 'package:core_localization/core_localization.dart';
import '../blocs/client_hubs_bloc.dart'; import '../blocs/client_hubs_bloc.dart';
import '../blocs/client_hubs_event.dart'; import '../blocs/client_hubs_event.dart';
@@ -47,14 +46,26 @@ class ClientHubsPage extends StatelessWidget {
}, },
builder: (context, state) { builder: (context, state) {
return Scaffold( 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( body: Stack(
children: [ children: <Widget>[
CustomScrollView( CustomScrollView(
slivers: [ slivers: <Widget>[
_buildAppBar(context), _buildAppBar(context),
SliverPadding( SliverPadding(
padding: const EdgeInsets.fromLTRB(20, 20, 20, 100), padding: const EdgeInsets.symmetric(
horizontal: UiConstants.space5,
vertical: UiConstants.space5,
).copyWith(bottom: 100),
sliver: SliverList( sliver: SliverList(
delegate: SliverChildListDelegate([ delegate: SliverChildListDelegate([
if (state.status == ClientHubsStatus.loading) if (state.status == ClientHubsStatus.loading)
@@ -85,7 +96,7 @@ class ClientHubsPage extends StatelessWidget {
), ),
), ),
], ],
const SizedBox(height: 20), const SizedBox(height: UiConstants.space5),
const HubInfoCard(), const HubInfoCard(),
]), ]),
), ),
@@ -120,7 +131,7 @@ class ClientHubsPage extends StatelessWidget {
), ),
if (state.status == ClientHubsStatus.actionInProgress) if (state.status == ClientHubsStatus.actionInProgress)
Container( Container(
color: Colors.black.withValues(alpha: 0.1), color: UiColors.black.withValues(alpha: 0.1),
child: const Center(child: CircularProgressIndicator()), child: const Center(child: CircularProgressIndicator()),
), ),
], ],
@@ -133,20 +144,19 @@ class ClientHubsPage extends StatelessWidget {
Widget _buildAppBar(BuildContext context) { Widget _buildAppBar(BuildContext context) {
return SliverAppBar( return SliverAppBar(
backgroundColor: const Color(0xFF121826), backgroundColor: UiColors.foreground, // Dark Slate equivalent
automaticallyImplyLeading: false, automaticallyImplyLeading: false,
expandedHeight: 140, expandedHeight: 140,
pinned: true, pinned: true,
flexibleSpace: FlexibleSpaceBar( flexibleSpace: FlexibleSpaceBar(
background: Container( background: Container(
decoration: const BoxDecoration( color: UiColors.foreground,
gradient: LinearGradient( padding: const EdgeInsets.fromLTRB(
colors: [Color(0xFF121826), Color(0xFF1E293B)], UiConstants.space5,
begin: Alignment.topLeft, UiConstants.space12,
end: Alignment.bottomRight, UiConstants.space5,
0,
), ),
),
padding: const EdgeInsets.fromLTRB(20, 48, 20, 0),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@@ -156,51 +166,35 @@ class ClientHubsPage extends StatelessWidget {
width: 40, width: 40,
height: 40, height: 40,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.2), color: UiColors.white.withValues(alpha: 0.2),
shape: BoxShape.circle, shape: BoxShape.circle,
), ),
child: const Icon( child: const Icon(
LucideIcons.arrowLeft, UiIcons.arrowLeft,
color: Colors.white, color: UiColors.white,
size: 20, size: 20,
), ),
), ),
), ),
const SizedBox(height: 16), const SizedBox(height: UiConstants.space4),
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: <Widget>[
Column( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: <Widget>[
Text( Text(
t.client_hubs.title, t.client_hubs.title,
style: const TextStyle( style: UiTypography.headline1m.white,
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 24,
),
), ),
Text( Text(
t.client_hubs.subtitle, t.client_hubs.subtitle,
style: const TextStyle( style: UiTypography.body2r.copyWith(
color: Color(0xFFCBD5E1), // slate-300 color: UiColors.switchInactive,
fontSize: 14,
), ),
), ),
], ],
), ),
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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Container(
color: Colors.black.withValues(alpha: 0.5), color: UiColors.bgOverlay,
child: Center( child: Center(
child: SingleChildScrollView( child: SingleChildScrollView(
child: Container( child: Container(
width: MediaQuery.of(context).size.width * 0.9, width: MediaQuery.of(context).size.width * 0.9,
padding: const EdgeInsets.all(24), padding: const EdgeInsets.all(UiConstants.space5),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: UiColors.bgPopup,
borderRadius: BorderRadius.circular(24), borderRadius: BorderRadius.circular(UiConstants.radiusBase),
boxShadow: [ boxShadow: const [
BoxShadow( BoxShadow(color: UiColors.popupShadow, blurRadius: 20),
color: Colors.black.withValues(alpha: 0.1),
blurRadius: 20,
),
], ],
), ),
child: Column( child: Column(
@@ -64,29 +61,27 @@ class _AddHubDialogState extends State<AddHubDialog> {
children: [ children: [
Text( Text(
t.client_hubs.add_hub_dialog.title, t.client_hubs.add_hub_dialog.title,
style: const TextStyle( style: UiTypography.headline3m.textPrimary,
fontSize: 20,
fontWeight: FontWeight.bold,
color: Color(0xFF0F172A),
), ),
), const SizedBox(height: UiConstants.space5),
const SizedBox(height: 24),
_buildFieldLabel(t.client_hubs.add_hub_dialog.name_label), _buildFieldLabel(t.client_hubs.add_hub_dialog.name_label),
TextField( TextField(
controller: _nameController, controller: _nameController,
style: UiTypography.body1r.textPrimary,
decoration: _buildInputDecoration( decoration: _buildInputDecoration(
t.client_hubs.add_hub_dialog.name_hint, 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), _buildFieldLabel(t.client_hubs.add_hub_dialog.address_label),
TextField( TextField(
controller: _addressController, controller: _addressController,
style: UiTypography.body1r.textPrimary,
decoration: _buildInputDecoration( decoration: _buildInputDecoration(
t.client_hubs.add_hub_dialog.address_hint, t.client_hubs.add_hub_dialog.address_hint,
), ),
), ),
const SizedBox(height: 32), const SizedBox(height: UiConstants.space8),
Row( Row(
children: [ children: [
Expanded( Expanded(
@@ -95,7 +90,7 @@ class _AddHubDialogState extends State<AddHubDialog> {
text: t.common.cancel, text: t.common.cancel,
), ),
), ),
const SizedBox(width: 12), const SizedBox(width: UiConstants.space3),
Expanded( Expanded(
child: UiButton.primary( child: UiButton.primary(
onPressed: () { onPressed: () {
@@ -121,36 +116,32 @@ class _AddHubDialogState extends State<AddHubDialog> {
Widget _buildFieldLabel(String label) { Widget _buildFieldLabel(String label) {
return Padding( return Padding(
padding: const EdgeInsets.only(bottom: 6), padding: const EdgeInsets.only(bottom: UiConstants.space2),
child: Text( child: Text(label, style: UiTypography.body2m.textPrimary),
label,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Color(0xFF0F172A),
),
),
); );
} }
InputDecoration _buildInputDecoration(String hint) { InputDecoration _buildInputDecoration(String hint) {
return InputDecoration( return InputDecoration(
hintText: hint, hintText: hint,
hintStyle: const TextStyle(color: Color(0xFF94A3B8), fontSize: 14), hintStyle: UiTypography.body2r.textPlaceholder,
filled: true, filled: true,
fillColor: const Color(0xFFF8FAFC), fillColor: UiColors.input,
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), contentPadding: const EdgeInsets.symmetric(
horizontal: UiConstants.space4,
vertical: 14,
),
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(UiConstants.radiusBase),
borderSide: const BorderSide(color: Color(0xFFE2E8F0)), borderSide: const BorderSide(color: UiColors.border),
), ),
enabledBorder: OutlineInputBorder( enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(UiConstants.radiusBase),
borderSide: const BorderSide(color: Color(0xFFE2E8F0)), borderSide: const BorderSide(color: UiColors.border),
), ),
focusedBorder: OutlineInputBorder( focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(UiConstants.radiusBase),
borderSide: const BorderSide(color: Color(0xFF2563EB), width: 2), 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:flutter/material.dart';
import 'package:krow_domain/krow_domain.dart'; import 'package:krow_domain/krow_domain.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:core_localization/core_localization.dart'; import 'package:core_localization/core_localization.dart';
/// A card displaying information about a single hub. /// A card displaying information about a single hub.
@@ -27,68 +27,56 @@ class HubCard extends StatelessWidget {
final bool hasNfc = hub.nfcTagId != null; final bool hasNfc = hub.nfcTagId != null;
return Container( return Container(
margin: const EdgeInsets.only(bottom: 12), margin: const EdgeInsets.only(bottom: UiConstants.space3),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: UiColors.white,
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(UiConstants.radiusBase),
boxShadow: [ boxShadow: const [
BoxShadow( BoxShadow(
color: Colors.black.withValues(alpha: 0.04), color: UiColors.popupShadow,
blurRadius: 10, blurRadius: 10,
offset: const Offset(0, 4), offset: Offset(0, 4),
), ),
], ],
), ),
child: Padding( child: Padding(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(UiConstants.space4),
child: Row( child: Row(
children: [ children: [
Container( Container(
width: 52, width: 52,
height: 52, height: 52,
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFEFF6FF), // blue-50 color: UiColors.tagInProgress,
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(UiConstants.radiusBase),
), ),
child: Icon( child: Icon(
hasNfc ? LucideIcons.checkCircle : LucideIcons.nfc, hasNfc ? UiIcons.success : UiIcons.nfc,
color: hasNfc color: hasNfc ? UiColors.iconSuccess : UiColors.iconThird,
? const Color(0xFF16A34A)
: const Color(0xFF94A3B8), // green-600 or slate-400
size: 24, size: 24,
), ),
), ),
const SizedBox(width: 16), const SizedBox(width: UiConstants.space4),
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(hub.name, style: UiTypography.body1b.textPrimary),
hub.name,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
color: Color(0xFF0F172A),
),
),
if (hub.address.isNotEmpty) if (hub.address.isNotEmpty)
Padding( Padding(
padding: const EdgeInsets.only(top: 4), padding: const EdgeInsets.only(top: UiConstants.space1),
child: Row( child: Row(
children: [ children: [
const Icon( const Icon(
LucideIcons.mapPin, UiIcons.mapPin,
size: 12, size: 12,
color: Color(0xFF94A3B8), color: UiColors.iconThird,
), ),
const SizedBox(width: 4), const SizedBox(width: UiConstants.space1),
Expanded( Expanded(
child: Text( child: Text(
hub.address, hub.address,
style: const TextStyle( style: UiTypography.footnote1r.textSecondary,
color: Color(0xFF64748B),
fontSize: 12,
),
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
@@ -98,14 +86,12 @@ class HubCard extends StatelessWidget {
), ),
if (hasNfc) if (hasNfc)
Padding( Padding(
padding: const EdgeInsets.only(top: 4), padding: const EdgeInsets.only(top: UiConstants.space1),
child: Text( child: Text(
t.client_hubs.hub_card.tag_label(id: hub.nfcTagId!), t.client_hubs.hub_card.tag_label(id: hub.nfcTagId!),
style: const TextStyle( style: UiTypography.footnote1b.copyWith(
color: Color(0xFF16A34A), color: UiColors.textSuccess,
fontSize: 12,
fontFamily: 'monospace', fontFamily: 'monospace',
fontWeight: FontWeight.bold,
), ),
), ),
), ),
@@ -117,20 +103,20 @@ class HubCard extends StatelessWidget {
IconButton( IconButton(
onPressed: onNfcPressed, onPressed: onNfcPressed,
icon: const Icon( icon: const Icon(
LucideIcons.nfc, UiIcons.nfc,
color: Color(0xFF2563EB), color: UiColors.primary,
size: 20, size: 20,
), ),
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
constraints: const BoxConstraints(), constraints: const BoxConstraints(),
splashRadius: 20, splashRadius: 20,
), ),
const SizedBox(width: 8), const SizedBox(width: UiConstants.space2),
IconButton( IconButton(
onPressed: onDeletePressed, onPressed: onDeletePressed,
icon: const Icon( icon: const Icon(
LucideIcons.trash2, UiIcons.delete,
color: Color(0xFFDC2626), color: UiColors.destructive,
size: 20, size: 20,
), ),
padding: EdgeInsets.zero, padding: EdgeInsets.zero,

View File

@@ -1,6 +1,5 @@
import 'package:design_system/design_system.dart'; import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:core_localization/core_localization.dart'; import 'package:core_localization/core_localization.dart';
/// Widget displayed when there are no hubs. /// Widget displayed when there are no hubs.
@@ -14,15 +13,15 @@ class HubEmptyState extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Container(
padding: const EdgeInsets.all(32), padding: const EdgeInsets.all(UiConstants.space8),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: UiColors.bgBanner,
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(UiConstants.radiusBase),
boxShadow: [ boxShadow: const [
BoxShadow( BoxShadow(
color: Colors.black.withValues(alpha: 0.04), color: UiColors.popupShadow,
blurRadius: 10, blurRadius: 10,
offset: const Offset(0, 4), offset: Offset(0, 4),
), ),
], ],
), ),
@@ -32,35 +31,27 @@ class HubEmptyState extends StatelessWidget {
width: 64, width: 64,
height: 64, height: 64,
decoration: const BoxDecoration( decoration: const BoxDecoration(
color: Color(0xFFF1F5F9), // slate-100 color: UiColors.secondary,
shape: BoxShape.circle, shape: BoxShape.circle,
), ),
child: const Icon( child: const Icon(UiIcons.nfc, size: 32, color: UiColors.iconThird),
LucideIcons.nfc,
size: 32,
color: Color(0xFF94A3B8),
), ),
), const SizedBox(height: UiConstants.space4),
const SizedBox(height: 16),
Text( Text(
t.client_hubs.empty_state.title, t.client_hubs.empty_state.title,
style: const TextStyle( style: UiTypography.title1m.textPrimary,
fontSize: 18,
fontWeight: FontWeight.bold,
color: Color(0xFF0F172A),
), ),
), const SizedBox(height: UiConstants.space2),
const SizedBox(height: 8),
Text( Text(
t.client_hubs.empty_state.description, t.client_hubs.empty_state.description,
textAlign: TextAlign.center, 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( UiButton.primary(
onPressed: onAddPressed, onPressed: onAddPressed,
text: t.client_hubs.empty_state.button, 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:flutter/material.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:core_localization/core_localization.dart'; import 'package:core_localization/core_localization.dart';
/// A card with information about how hubs work. /// A card with information about how hubs work.
@@ -10,34 +10,29 @@ class HubInfoCard extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Container(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(UiConstants.space4),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFEFF6FF), // blue-50 color: UiColors.tagInProgress,
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(UiConstants.radiusBase),
), ),
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const Icon(LucideIcons.nfc, size: 20, color: Color(0xFF2563EB)), const Icon(UiIcons.nfc, size: 20, color: UiColors.primary),
const SizedBox(width: 12), const SizedBox(width: UiConstants.space3),
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
t.client_hubs.about_hubs.title, t.client_hubs.about_hubs.title,
style: const TextStyle( style: UiTypography.body2b.textPrimary,
fontWeight: FontWeight.bold,
fontSize: 14,
color: Color(0xFF0F172A),
), ),
), const SizedBox(height: UiConstants.space1),
const SizedBox(height: 4),
Text( Text(
t.client_hubs.about_hubs.description, t.client_hubs.about_hubs.description,
style: const TextStyle( style: UiTypography.footnote1r.copyWith(
color: Color(0xFF334155), color: UiColors.textSecondary,
fontSize: 12,
height: 1.4, height: 1.4,
), ),
), ),

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter_modular/flutter_modular.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/data/repositories_impl/settings_repository_impl.dart';
import 'src/domain/repositories/settings_repository_interface.dart'; import 'src/domain/repositories/settings_repository_interface.dart';
import 'src/domain/usecases/sign_out_usecase.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. /// A [Module] for the client settings feature.
class ClientSettingsModule extends Module { class ClientSettingsModule extends Module {
@override
List<Module> get imports => [DataConnectModule()];
@override @override
void binds(Injector i) { void binds(Injector i) {
// Repositories // Repositories
i.addLazySingleton<SettingsRepositoryInterface>( i.addLazySingleton<SettingsRepositoryInterface>(
() => SettingsRepositoryImpl(mock: i.get<AuthRepositoryMock>()), () => SettingsRepositoryImpl(firebaseAuth: FirebaseAuth.instance),
); );
// UseCases // 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'; import '../../domain/repositories/settings_repository_interface.dart';
/// Implementation of [SettingsRepositoryInterface]. /// Implementation of [SettingsRepositoryInterface].
/// ///
/// This implementation delegates data access to the [AuthRepositoryMock] /// This implementation delegates authentication operations to [FirebaseAuth].
/// from the `data_connect` package.
class SettingsRepositoryImpl implements SettingsRepositoryInterface { class SettingsRepositoryImpl implements SettingsRepositoryInterface {
/// The auth mock from data connect. /// The Firebase Auth instance.
final AuthRepositoryMock mock; final FirebaseAuth _firebaseAuth;
/// Creates a [SettingsRepositoryImpl] with the required [mock]. /// Creates a [SettingsRepositoryImpl] with the required [_firebaseAuth].
SettingsRepositoryImpl({required this.mock}); SettingsRepositoryImpl({required FirebaseAuth firebaseAuth})
: _firebaseAuth = firebaseAuth;
@override @override
Future<void> signOut() { Future<void> signOut() async {
return mock.signOut(); 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}); const ClientSettingsPage({super.key});
@override @override
/// Builds the client settings page UI.
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider<ClientSettingsBloc>( return BlocProvider<ClientSettingsBloc>(
create: (context) => Modular.get<ClientSettingsBloc>(), create: (context) => Modular.get<ClientSettingsBloc>(),
child: BlocListener<ClientSettingsBloc, ClientSettingsState>( child: BlocListener<ClientSettingsBloc, ClientSettingsState>(
listener: (context, state) { listener: (context, state) {
if (state is ClientSettingsSignOutSuccess) { if (state is ClientSettingsSignOutSuccess) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Signed out successfully')),
);
Modular.to.navigate('/'); Modular.to.navigate('/');
} }
if (state is ClientSettingsError) { 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:core_localization/core_localization.dart';
import 'package:design_system/design_system.dart'; import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_modular/flutter_modular.dart';
import '../../blocs/client_settings_bloc.dart'; import '../../blocs/client_settings_bloc.dart';
/// A widget that displays the primary actions for the settings page. /// A widget that displays the primary actions for the settings page.
@@ -10,27 +12,38 @@ class SettingsActions extends StatelessWidget {
const SettingsActions({super.key}); const SettingsActions({super.key});
@override @override
/// Builds the settings actions UI.
Widget build(BuildContext context) { 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( return SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space5), padding: const EdgeInsets.symmetric(horizontal: UiConstants.space5),
sliver: SliverList( sliver: SliverList(
delegate: SliverChildListDelegate([ delegate: SliverChildListDelegate(<Widget>[
const SizedBox(height: UiConstants.space5), const SizedBox(height: UiConstants.space5),
// Edit profile button
UiButton.primary(text: labels.edit_profile, onPressed: () {}), UiButton.primary(text: labels.edit_profile, onPressed: () {}),
const SizedBox(height: UiConstants.space4), 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), const SizedBox(height: UiConstants.space4),
// Log out button
BlocBuilder<ClientSettingsBloc, ClientSettingsState>( BlocBuilder<ClientSettingsBloc, ClientSettingsState>(
builder: (context, state) { builder: (BuildContext context, ClientSettingsState state) {
return UiButton.secondary( return UiButton.secondary(
text: labels.log_out, text: labels.log_out,
onPressed: state is ClientSettingsLoading onPressed: state is ClientSettingsLoading
? null ? null
: () => BlocProvider.of<ClientSettingsBloc>( : () => _showSignOutDialog(context),
context,
).add(const ClientSettingsSignOutRequested()),
); );
}, },
), ),
@@ -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}); const SettingsProfileHeader({super.key});
@override @override
/// Builds the profile header UI.
Widget build(BuildContext context) { Widget build(BuildContext context) {
final labels = t.client_settings.profile; final labels = t.client_settings.profile;
return SliverAppBar( return SliverAppBar(
backgroundColor: UiColors.primary, backgroundColor: UiColors.bgSecondary,
expandedHeight: 200, expandedHeight: 140,
pinned: true, 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( flexibleSpace: FlexibleSpaceBar(
background: Container( background: Container(
decoration: const BoxDecoration( padding: const EdgeInsets.symmetric(horizontal: UiConstants.space8),
gradient: LinearGradient( margin: const EdgeInsets.only(top: UiConstants.space16),
colors: [UiColors.primary, Color(0xFF0047FF)], child: Row(
begin: Alignment.topLeft, crossAxisAlignment: CrossAxisAlignment.center,
end: Alignment.bottomRight, mainAxisAlignment: MainAxisAlignment.start,
), spacing: UiConstants.space4,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
const SizedBox(height: UiConstants.space5 * 2),
Container( Container(
width: 80, width: 64,
height: 80, height: 64,
decoration: BoxDecoration( decoration: BoxDecoration(
shape: BoxShape.circle, shape: BoxShape.circle,
border: Border.all( border: Border.all(color: UiColors.border, width: 2),
color: UiColors.white.withValues(alpha: 0.24),
width: 4,
),
color: UiColors.white, color: UiColors.white,
), ),
child: const Center( child: Center(
child: Text( child: Text(
'C', 'C',
style: TextStyle( style: UiTypography.headline1m.copyWith(
fontSize: 32,
fontWeight: FontWeight.bold,
color: UiColors.primary, color: UiColors.primary,
), ),
), ),
), ),
), ),
const SizedBox(height: UiConstants.space3), Column(
Text( crossAxisAlignment: CrossAxisAlignment.start,
'Your Company', mainAxisAlignment: MainAxisAlignment.center,
style: UiTypography.body1b.copyWith(color: UiColors.white), children: [
), Text('Your Company', style: UiTypography.body1b.textPrimary),
const SizedBox(height: UiConstants.space1), const SizedBox(height: UiConstants.space1),
Row( Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.start,
spacing: UiConstants.space1,
children: [ children: [
Icon( Icon(
UiIcons.mail, UiIcons.mail,
size: 14, size: 14,
color: UiColors.white.withValues(alpha: 0.7), color: UiColors.textSecondary,
), ),
const SizedBox(width: UiConstants.space2),
Text( Text(
'client@example.com', 'client@example.com',
style: UiTypography.footnote1r.copyWith( style: UiTypography.footnote1r.textSecondary,
color: UiColors.white.withValues(alpha: 0.7),
), ),
],
), ),
], ],
), ),
@@ -78,14 +76,7 @@ class SettingsProfileHeader extends StatelessWidget {
), ),
), ),
), ),
leading: IconButton( title: Text(labels.title, style: UiTypography.body1b.textPrimary),
icon: const Icon(UiIcons.arrowLeft, color: UiColors.white),
onPressed: () => Modular.to.pop(),
),
title: Text(
labels.title,
style: UiTypography.body1b.copyWith(color: UiColors.white),
),
); );
} }
} }

View File

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

View File

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

View File

@@ -185,6 +185,13 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.4.2" 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: clock:
dependency: transitive dependency: transitive
description: description:
@@ -415,7 +422,7 @@ packages:
source: hosted source: hosted
version: "8.1.6" version: "8.1.6"
flutter_lints: flutter_lints:
dependency: transitive dependency: "direct dev"
description: description:
name: flutter_lints name: flutter_lints
sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1"

View File

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

View File

@@ -1,5 +1,5 @@
connectorId: example connectorId: example
generate: generate:
dartSdk: dartSdk:
- outputDir: ../../../apps/packages/data_connect/lib/src/dataconnect_generated - outputDir: ../../../apps/mobile/packages/data_connect/lib/src/dataconnect_generated
package: dataconnect_generated/generated.dart package: dataconnect_generated/generated.dart

View File

@@ -1,6 +1,6 @@
mutation createTeam( mutation createTeam(
$teamName: String! $teamName: String!
$ownerId: String! $ownerId: UUID!
$ownerName: String! $ownerName: String!
$ownerRole: String! $ownerRole: String!
$email: String $email: String

View File

@@ -44,7 +44,7 @@ query getTeamById($id: UUID!) @auth(level: USER) {
} }
} }
query getTeamsByOwnerId($ownerId: String!) @auth(level: USER) { query getTeamsByOwnerId($ownerId: UUID!) @auth(level: USER) {
teams(where: { ownerId: { eq: $ownerId } }) { teams(where: { ownerId: { eq: $ownerId } }) {
id id
teamName teamName

View File

@@ -2,10 +2,10 @@ mutation createTeamHub(
$teamId: UUID! $teamId: UUID!
$hubName: String! $hubName: String!
$address: String! $address: String!
$city: String! $city: String
$state: String! $state: String
$zipCode: String! $zipCode: String
$managerName: String! $managerName: String
$isActive: Boolean $isActive: Boolean
$departments: Any $departments: Any
) @auth(level: USER) { ) @auth(level: USER) {

View File

@@ -56,7 +56,7 @@ query getTeamHubsByTeamId($teamId: UUID!) @auth(level: USER) {
# LIST TEAM HUBS BY OWNER (Vendor/Business) # LIST TEAM HUBS BY OWNER (Vendor/Business)
# ------------------------------------------------------------ # ------------------------------------------------------------
query listTeamHubsByOwnerId( query listTeamHubsByOwnerId(
$ownerId: String! $ownerId: UUID!
) @auth(level: USER) { ) @auth(level: USER) {
teamHubs( teamHubs(
where: { where: {

View File

@@ -3,7 +3,7 @@ type Team @table(name: "teams") {
id: UUID! @default(expr: "uuidV4()") id: UUID! @default(expr: "uuidV4()")
teamName: String! teamName: String!
ownerId: String! #vendor/business ownerId: UUID! #vendor/business
ownerName: String! ownerName: String!
ownerRole: String! ownerRole: String!
email: String email: String

View File

@@ -6,10 +6,10 @@ type TeamHub @table(name: "team_hubs") {
hubName: String! hubName: String!
address: String! address: String!
city: String! city: String
state: String! state: String
zipCode: String! zipCode: String
managerName: String! managerName: String
isActive: Boolean! @default(expr: "true") isActive: Boolean! @default(expr: "true")
departments: Any departments: Any