Merge branch '208-p0-auth-05-get-started-screen' into rapid_order_client

This commit is contained in:
José Salazar
2026-01-23 14:31:25 -05:00
43 changed files with 3226 additions and 387 deletions

View File

@@ -311,6 +311,35 @@
"orders": "Orders",
"reports": "Reports"
}
},
"client_view_orders": {
"title": "Orders",
"post_button": "Post",
"post_order": "Post an Order",
"no_orders": "No orders for $date",
"tabs": {
"up_next": "Up Next",
"active": "Active",
"completed": "Completed"
},
"card": {
"open": "OPEN",
"filled": "FILLED",
"confirmed": "CONFIRMED",
"in_progress": "IN PROGRESS",
"completed": "COMPLETED",
"cancelled": "CANCELLED",
"get_direction": "Get direction",
"total": "Total",
"hrs": "HRS",
"workers": "workers",
"clock_in": "CLOCK IN",
"clock_out": "CLOCK OUT",
"coverage": "Coverage",
"workers_label": "$filled/$needed Workers",
"confirmed_workers": "Workers Confirmed",
"no_workers": "No workers confirmed yet."
}
}
}

View File

@@ -311,5 +311,34 @@
"orders": "Órdenes",
"reports": "Reportes"
}
},
"client_view_orders": {
"title": "Órdenes",
"post_button": "Publicar",
"post_order": "Publicar una Orden",
"no_orders": "No hay órdenes para $date",
"tabs": {
"up_next": "Próximos",
"active": "Activos",
"completed": "Completados"
},
"card": {
"open": "ABIERTO",
"filled": "LLENO",
"confirmed": "CONFIRMADO",
"in_progress": "EN PROGRESO",
"completed": "COMPLETADO",
"cancelled": "CANCELADO",
"get_direction": "Obtener dirección",
"total": "Total",
"hrs": "HRS",
"workers": "trabajadores",
"clock_in": "ENTRADA",
"clock_out": "SALIDA",
"coverage": "Cobertura",
"workers_label": "$filled/$needed Trabajadores",
"confirmed_workers": "Trabajadores Confirmados",
"no_workers": "Ningún trabajador confirmado aún."
}
}
}

View File

@@ -7,7 +7,7 @@ import 'package:krow_domain/krow_domain.dart';
class OrderRepositoryMock {
/// Returns a list of available [OrderType]s.
Future<List<OrderType>> getOrderTypes() async {
await Future.delayed(const Duration(milliseconds: 500));
await Future<void>.delayed(const Duration(milliseconds: 500));
return const <OrderType>[
OrderType(
id: 'rapid',
@@ -34,11 +34,122 @@ class OrderRepositoryMock {
/// Simulates creating a one-time order.
Future<void> createOneTimeOrder(OneTimeOrder order) async {
await Future.delayed(const Duration(milliseconds: 800));
await Future<void>.delayed(const Duration(milliseconds: 800));
}
/// Simulates creating a rapid order.
Future<void> createRapidOrder(String description) async {
await Future.delayed(const Duration(seconds: 1));
await Future<void>.delayed(const Duration(seconds: 1));
}
/// Returns a mock list of client orders.
Future<List<OrderItem>> getOrders() async {
await Future<void>.delayed(const Duration(milliseconds: 500));
return <OrderItem>[
OrderItem(
id: '1',
title: 'Server - Wedding',
clientName: 'Grand Plaza Hotel',
status: 'filled',
date: DateTime.now()
.add(const Duration(days: 1))
.toIso8601String()
.split('T')[0],
startTime: '16:00',
endTime: '23:00',
location: 'Grand Plaza Hotel, 123 Main St',
locationAddress: 'Grand Plaza Hotel, 123 Main St',
filled: 10,
workersNeeded: 10,
hourlyRate: 22.0,
confirmedApps: List<Map<String, dynamic>>.generate(
10,
(int index) => <String, dynamic>{
'id': 'app_$index',
'worker_id': 'w_$index',
'worker_name': 'Worker ${String.fromCharCode(65 + index)}',
'status': 'confirmed',
'check_in_time': index < 5 ? '15:55' : null,
},
),
),
OrderItem(
id: '2',
title: 'Bartender - Private Event',
clientName: 'Taste of the Town',
status: 'open',
date: DateTime.now()
.add(const Duration(days: 1))
.toIso8601String()
.split('T')[0],
startTime: '18:00',
endTime: '02:00',
location: 'Downtown Loft, 456 High St',
locationAddress: 'Downtown Loft, 456 High St',
filled: 4,
workersNeeded: 5,
hourlyRate: 28.0,
confirmedApps: List<Map<String, dynamic>>.generate(
4,
(int index) => <String, dynamic>{
'id': 'app_b_$index',
'worker_id': 'w_b_$index',
'worker_name': 'Bartender ${index + 1}',
'status': 'confirmed',
},
),
),
OrderItem(
id: '3',
title: 'Event Staff',
clientName: 'City Center',
status: 'in_progress',
date: DateTime.now().toIso8601String().split('T')[0],
startTime: '08:00',
endTime: '16:00',
location: 'Convention Center, 789 Blvd',
locationAddress: 'Convention Center, 789 Blvd',
filled: 15,
workersNeeded: 15,
hourlyRate: 20.0,
confirmedApps: List<Map<String, dynamic>>.generate(
15,
(int index) => <String, dynamic>{
'id': 'app_c_$index',
'worker_id': 'w_c_$index',
'worker_name': 'Staff ${index + 1}',
'status': 'confirmed',
'check_in_time': '07:55',
},
),
),
OrderItem(
id: '4',
title: 'Coat Check',
clientName: 'The Met Museum',
status: 'completed',
date: DateTime.now()
.subtract(const Duration(days: 1))
.toIso8601String()
.split('T')[0],
startTime: '17:00',
endTime: '22:00',
location: 'The Met Museum, 1000 5th Ave',
locationAddress: 'The Met Museum, 1000 5th Ave',
filled: 2,
workersNeeded: 2,
hourlyRate: 18.0,
confirmedApps: List<Map<String, dynamic>>.generate(
2,
(int index) => <String, dynamic>{
'id': 'app_d_$index',
'worker_id': 'w_d_$index',
'worker_name': 'Checker ${index + 1}',
'status': 'confirmed',
'check_in_time': '16:50',
},
),
),
];
}
}

View File

@@ -34,6 +34,9 @@ class UiIcons {
/// User icon for profile
static const IconData user = _IconLib.user;
/// Users icon for groups or staff
static const IconData users = _IconLib.users;
/// Settings icon
static const IconData settings = _IconLib.settings;
@@ -81,6 +84,9 @@ class UiIcons {
/// Chevron down icon
static const IconData chevronDown = _IconLib.chevronDown;
/// Chevron up icon
static const IconData chevronUp = _IconLib.chevronUp;
// --- Status & Feedback ---
/// Info icon
@@ -139,6 +145,9 @@ class UiIcons {
/// Sparkles icon for features or AI
static const IconData sparkles = _IconLib.sparkles;
/// Navigation/Compass icon
static const IconData navigation = _IconLib.navigation;
/// Star icon for ratings
static const IconData star = _IconLib.star;
@@ -154,6 +163,12 @@ class UiIcons {
/// Eye off icon for hidden visibility
static const IconData eyeOff = _IconLib.eyeOff;
/// Phone icon for calls
static const IconData phone = _IconLib.phone;
/// Message circle icon for chat
static const IconData messageCircle = _IconLib.messageCircle;
/// Building icon for companies
static const IconData building = _IconLib.building2;

View File

@@ -12,8 +12,8 @@ class UiTheme {
/// Returns the light theme for the Staff application.
static ThemeData get light {
final colorScheme = UiColors.colorScheme;
final textTheme = UiTypography.textTheme;
final ColorScheme colorScheme = UiColors.colorScheme;
final TextTheme textTheme = UiTypography.textTheme;
return ThemeData(
useMaterial3: true,
@@ -68,7 +68,6 @@ class UiTheme {
horizontal: UiConstants.space6,
vertical: UiConstants.space3,
),
minimumSize: const Size(double.infinity, 54),
maximumSize: const Size(double.infinity, 54),
).copyWith(
side: WidgetStateProperty.resolveWith<BorderSide?>((states) {
@@ -99,7 +98,6 @@ class UiTheme {
horizontal: UiConstants.space4,
vertical: UiConstants.space2,
),
minimumSize: const Size(double.infinity, 52),
maximumSize: const Size(double.infinity, 52),
),
),
@@ -117,7 +115,6 @@ class UiTheme {
horizontal: UiConstants.space4,
vertical: UiConstants.space3,
),
minimumSize: const Size(double.infinity, 52),
maximumSize: const Size(double.infinity, 52),
),
),

View File

@@ -320,6 +320,15 @@ class UiTypography {
color: UiColors.textPrimary,
);
/// Body 3 Medium - Font: Instrument Sans, Size: 14, Height: 1.5, Spacing: -0.1 (#121826)
static final TextStyle body3m = _primaryBase.copyWith(
fontWeight: FontWeight.w500,
fontSize: 12,
height: 1.5,
letterSpacing: -0.1,
color: UiColors.textPrimary,
);
/// Body 4 Regular - Font: Instrument Sans, Size: 14, Height: 1.5, Spacing: 0.05 (#121826)
static final TextStyle body4r = _primaryBase.copyWith(
fontWeight: FontWeight.w400,

View File

@@ -27,6 +27,9 @@ class UiButton extends StatelessWidget {
/// The size of the button.
final UiButtonSize size;
/// Whether the button should take up the full width of its container.
final bool fullWidth;
/// The button widget to use (ElevatedButton, OutlinedButton, or TextButton).
final Widget Function(
BuildContext context,
@@ -48,6 +51,7 @@ class UiButton extends StatelessWidget {
this.style,
this.iconSize = 20,
this.size = UiButtonSize.medium,
this.fullWidth = false,
}) : assert(
text != null || child != null,
'Either text or child must be provided',
@@ -64,6 +68,7 @@ class UiButton extends StatelessWidget {
this.style,
this.iconSize = 20,
this.size = UiButtonSize.medium,
this.fullWidth = false,
}) : buttonBuilder = _elevatedButtonBuilder,
assert(
text != null || child != null,
@@ -81,6 +86,7 @@ class UiButton extends StatelessWidget {
this.style,
this.iconSize = 20,
this.size = UiButtonSize.medium,
this.fullWidth = false,
}) : buttonBuilder = _outlinedButtonBuilder,
assert(
text != null || child != null,
@@ -98,6 +104,25 @@ class UiButton extends StatelessWidget {
this.style,
this.iconSize = 20,
this.size = UiButtonSize.medium,
this.fullWidth = false,
}) : buttonBuilder = _textButtonBuilder,
assert(
text != null || child != null,
'Either text or child must be provided',
);
/// Creates a ghost button (transparent background).
UiButton.ghost({
super.key,
this.text,
this.child,
this.onPressed,
this.leadingIcon,
this.trailingIcon,
this.style,
this.iconSize = 20,
this.size = UiButtonSize.medium,
this.fullWidth = false,
}) : buttonBuilder = _textButtonBuilder,
assert(
text != null || child != null,
@@ -107,7 +132,18 @@ class UiButton extends StatelessWidget {
@override
/// Builds the button UI.
Widget build(BuildContext context) {
return buttonBuilder(context, onPressed, style, _buildButtonContent());
final Widget button = buttonBuilder(
context,
onPressed,
style,
_buildButtonContent(),
);
if (fullWidth) {
return SizedBox(width: double.infinity, child: button);
}
return button;
}
/// Builds the button content with optional leading and trailing icons.
@@ -116,27 +152,40 @@ class UiButton extends StatelessWidget {
return child!;
}
// Single icon or text case
final String buttonText = text ?? '';
// Optimization: If no icons, return plain text to avoid Row layout overhead
if (leadingIcon == null && trailingIcon == null) {
return Text(text!);
return Text(buttonText, textAlign: TextAlign.center);
}
if (leadingIcon != null && text == null && trailingIcon == null) {
return Icon(leadingIcon, size: iconSize);
}
// Multiple elements case
// Multiple elements case: Use a Row with MainAxisSize.min
final List<Widget> children = [];
if (leadingIcon != null) {
children.add(Icon(leadingIcon, size: iconSize));
children.add(const SizedBox(width: UiConstants.space2));
}
children.add(Text(text!));
if (buttonText.isNotEmpty) {
if (leadingIcon != null) {
children.add(const SizedBox(width: UiConstants.space2));
}
// Use flexible to ensure text doesn't force infinite width in flex parents
children.add(
Flexible(
child: Text(
buttonText,
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
),
),
);
}
if (trailingIcon != null) {
children.add(const SizedBox(width: UiConstants.space2));
if (buttonText.isNotEmpty || leadingIcon != null) {
children.add(const SizedBox(width: UiConstants.space2));
}
children.add(Icon(trailingIcon, size: iconSize));
}

View File

@@ -19,6 +19,7 @@ export 'src/entities/business/business_setting.dart';
export 'src/entities/business/hub.dart';
export 'src/entities/business/hub_department.dart';
export 'src/entities/business/biz_contract.dart';
export 'src/entities/business/vendor.dart';
// Events & Shifts
export 'src/entities/events/event.dart';
@@ -31,6 +32,7 @@ export 'src/entities/events/work_session.dart';
export 'src/entities/orders/order_type.dart';
export 'src/entities/orders/one_time_order.dart';
export 'src/entities/orders/one_time_order_position.dart';
export 'src/entities/orders/order_item.dart';
// Skills & Certs
export 'src/entities/skills/skill.dart';

View File

@@ -0,0 +1,15 @@
import 'package:equatable/equatable.dart';
/// Represents a staffing vendor.
class Vendor extends Equatable {
const Vendor({required this.id, required this.name, required this.rates});
final String id;
final String name;
/// A map of role names to hourly rates.
final Map<String, double> rates;
@override
List<Object?> get props => <Object?>[id, name, rates];
}

View File

@@ -0,0 +1,80 @@
import 'package:equatable/equatable.dart';
/// Represents a customer's view of an order or shift.
///
/// This entity captures the details necessary for the dashboard/view orders screen,
/// including status and worker assignments.
class OrderItem extends Equatable {
/// Creates an [OrderItem].
const OrderItem({
required this.id,
required this.title,
required this.clientName,
required this.status,
required this.date,
required this.startTime,
required this.endTime,
required this.location,
required this.locationAddress,
required this.filled,
required this.workersNeeded,
required this.hourlyRate,
this.confirmedApps = const <Map<String, dynamic>>[],
});
/// Unique identifier of the order.
final String id;
/// Title or name of the role.
final String title;
/// Name of the client company.
final String clientName;
/// status of the order (e.g., 'open', 'filled', 'completed').
final String status;
/// Date of the shift (ISO format).
final String date;
/// Start time of the shift.
final String startTime;
/// End time of the shift.
final String endTime;
/// Location name.
final String location;
/// Full address of the location.
final String locationAddress;
/// Number of workers currently filled.
final int filled;
/// Total number of workers required.
final int workersNeeded;
/// Hourly pay rate.
final double hourlyRate;
/// List of confirmed worker applications.
final List<Map<String, dynamic>> confirmedApps;
@override
List<Object?> get props => <Object?>[
id,
title,
clientName,
status,
date,
startTime,
endTime,
location,
locationAddress,
filled,
workersNeeded,
hourlyRate,
confirmedApps,
];
}

View File

@@ -97,6 +97,7 @@ class ClientGetStartedPage extends StatelessWidget {
.get_started_page
.sign_in_button,
onPressed: () => Modular.to.pushClientSignIn(),
fullWidth: true,
),
const SizedBox(height: UiConstants.space3),
@@ -108,6 +109,7 @@ class ClientGetStartedPage extends StatelessWidget {
.get_started_page
.create_account_button,
onPressed: () => Modular.to.pushClientSignUp(),
fullWidth: true,
),
],
),

View File

@@ -96,6 +96,7 @@ class _ClientSignInFormState extends State<ClientSignInForm> {
UiButton.primary(
text: widget.isLoading ? null : i18n.sign_in_button,
onPressed: widget.isLoading ? null : _handleSubmit,
fullWidth: true,
child: widget.isLoading
? const SizedBox(
height: 24,

View File

@@ -1,6 +1,7 @@
import 'package:client_home/client_home.dart';
import 'package:flutter/material.dart';
import 'package:flutter_modular/flutter_modular.dart';
import 'package:view_orders/view_orders.dart';
import 'presentation/blocs/client_main_cubit.dart';
import 'presentation/pages/client_main_page.dart';
@@ -30,11 +31,7 @@ class ClientMainModule extends Module {
child: (BuildContext context) =>
const PlaceholderPage(title: 'Billing'),
),
ChildRoute<dynamic>(
'/orders',
child: (BuildContext context) =>
const PlaceholderPage(title: 'Orders'),
),
ModuleRoute<dynamic>('/orders', module: ViewOrdersModule()),
ChildRoute<dynamic>(
'/reports',
child: (BuildContext context) =>

View File

@@ -23,6 +23,8 @@ dependencies:
path: ../../../core_localization
client_home:
path: ../home
view_orders:
path: ../view_orders
# Intentionally commenting these out as they might not exist yet
# client_settings:
# path: ../settings

View File

@@ -8,7 +8,7 @@ import 'client_create_order_state.dart';
class ClientCreateOrderBloc
extends Bloc<ClientCreateOrderEvent, ClientCreateOrderState> {
ClientCreateOrderBloc(this._getOrderTypesUseCase)
: super(const ClientCreateOrderInitial()) {
: super(const ClientCreateOrderInitial()) {
on<ClientCreateOrderTypesRequested>(_onTypesRequested);
}
final GetOrderTypesUseCase _getOrderTypesUseCase;

View File

@@ -8,16 +8,71 @@ import 'one_time_order_state.dart';
/// BLoC for managing the multi-step one-time order creation form.
class OneTimeOrderBloc extends Bloc<OneTimeOrderEvent, OneTimeOrderState> {
OneTimeOrderBloc(this._createOneTimeOrderUseCase)
: super(OneTimeOrderState.initial()) {
: super(OneTimeOrderState.initial()) {
on<OneTimeOrderVendorsLoaded>(_onVendorsLoaded);
on<OneTimeOrderVendorChanged>(_onVendorChanged);
on<OneTimeOrderDateChanged>(_onDateChanged);
on<OneTimeOrderLocationChanged>(_onLocationChanged);
on<OneTimeOrderPositionAdded>(_onPositionAdded);
on<OneTimeOrderPositionRemoved>(_onPositionRemoved);
on<OneTimeOrderPositionUpdated>(_onPositionUpdated);
on<OneTimeOrderSubmitted>(_onSubmitted);
// Initial load of mock vendors
add(
const OneTimeOrderVendorsLoaded(<Vendor>[
Vendor(
id: 'v1',
name: 'Elite Staffing',
rates: <String, double>{
'Server': 25.0,
'Bartender': 30.0,
'Cook': 28.0,
'Busser': 18.0,
'Host': 20.0,
'Barista': 22.0,
'Dishwasher': 17.0,
'Event Staff': 19.0,
},
),
Vendor(
id: 'v2',
name: 'Premier Workforce',
rates: <String, double>{
'Server': 22.0,
'Bartender': 28.0,
'Cook': 25.0,
'Busser': 16.0,
'Host': 18.0,
'Barista': 20.0,
'Dishwasher': 15.0,
'Event Staff': 18.0,
},
),
]),
);
}
final CreateOneTimeOrderUseCase _createOneTimeOrderUseCase;
void _onVendorsLoaded(
OneTimeOrderVendorsLoaded event,
Emitter<OneTimeOrderState> emit,
) {
emit(
state.copyWith(
vendors: event.vendors,
selectedVendor: event.vendors.isNotEmpty ? event.vendors.first : null,
),
);
}
void _onVendorChanged(
OneTimeOrderVendorChanged event,
Emitter<OneTimeOrderState> emit,
) {
emit(state.copyWith(selectedVendor: event.vendor));
}
void _onDateChanged(
OneTimeOrderDateChanged event,
Emitter<OneTimeOrderState> emit,
@@ -37,13 +92,14 @@ class OneTimeOrderBloc extends Bloc<OneTimeOrderEvent, OneTimeOrderState> {
Emitter<OneTimeOrderState> emit,
) {
final List<OneTimeOrderPosition> newPositions =
List<OneTimeOrderPosition>.from(state.positions)
..add(const OneTimeOrderPosition(
List<OneTimeOrderPosition>.from(state.positions)..add(
const OneTimeOrderPosition(
role: '',
count: 1,
startTime: '',
endTime: '',
));
startTime: '09:00',
endTime: '17:00',
),
);
emit(state.copyWith(positions: newPositions));
}
@@ -79,14 +135,17 @@ class OneTimeOrderBloc extends Bloc<OneTimeOrderEvent, OneTimeOrderState> {
date: state.date,
location: state.location,
positions: state.positions,
// In a real app, we'd pass the vendorId here
);
await _createOneTimeOrderUseCase(OneTimeOrderArguments(order: order));
emit(state.copyWith(status: OneTimeOrderStatus.success));
} catch (e) {
emit(state.copyWith(
status: OneTimeOrderStatus.failure,
errorMessage: e.toString(),
));
emit(
state.copyWith(
status: OneTimeOrderStatus.failure,
errorMessage: e.toString(),
),
);
}
}
}

View File

@@ -8,6 +8,22 @@ abstract class OneTimeOrderEvent extends Equatable {
List<Object?> get props => <Object?>[];
}
class OneTimeOrderVendorsLoaded extends OneTimeOrderEvent {
const OneTimeOrderVendorsLoaded(this.vendors);
final List<Vendor> vendors;
@override
List<Object?> get props => <Object?>[vendors];
}
class OneTimeOrderVendorChanged extends OneTimeOrderEvent {
const OneTimeOrderVendorChanged(this.vendor);
final Vendor vendor;
@override
List<Object?> get props => <Object?>[vendor];
}
class OneTimeOrderDateChanged extends OneTimeOrderEvent {
const OneTimeOrderDateChanged(this.date);
final DateTime date;

View File

@@ -10,6 +10,8 @@ class OneTimeOrderState extends Equatable {
required this.positions,
this.status = OneTimeOrderStatus.initial,
this.errorMessage,
this.vendors = const <Vendor>[],
this.selectedVendor,
});
factory OneTimeOrderState.initial() {
@@ -17,13 +19,9 @@ class OneTimeOrderState extends Equatable {
date: DateTime.now(),
location: '',
positions: const <OneTimeOrderPosition>[
OneTimeOrderPosition(
role: '',
count: 1,
startTime: '',
endTime: '',
),
OneTimeOrderPosition(role: '', count: 1, startTime: '', endTime: ''),
],
vendors: const <Vendor>[],
);
}
final DateTime date;
@@ -31,6 +29,8 @@ class OneTimeOrderState extends Equatable {
final List<OneTimeOrderPosition> positions;
final OneTimeOrderStatus status;
final String? errorMessage;
final List<Vendor> vendors;
final Vendor? selectedVendor;
OneTimeOrderState copyWith({
DateTime? date,
@@ -38,6 +38,8 @@ class OneTimeOrderState extends Equatable {
List<OneTimeOrderPosition>? positions,
OneTimeOrderStatus? status,
String? errorMessage,
List<Vendor>? vendors,
Vendor? selectedVendor,
}) {
return OneTimeOrderState(
date: date ?? this.date,
@@ -45,15 +47,19 @@ class OneTimeOrderState extends Equatable {
positions: positions ?? this.positions,
status: status ?? this.status,
errorMessage: errorMessage ?? this.errorMessage,
vendors: vendors ?? this.vendors,
selectedVendor: selectedVendor ?? this.selectedVendor,
);
}
@override
List<Object?> get props => <Object?>[
date,
location,
positions,
status,
errorMessage,
];
date,
location,
positions,
status,
errorMessage,
vendors,
selectedVendor,
];
}

View File

@@ -7,15 +7,15 @@ import 'rapid_order_state.dart';
/// BLoC for managing the rapid (urgent) order creation flow.
class RapidOrderBloc extends Bloc<RapidOrderEvent, RapidOrderState> {
RapidOrderBloc(this._createRapidOrderUseCase)
: super(
const RapidOrderInitial(
examples: <String>[
'"We had a call out. Need 2 cooks ASAP"',
'"Need 5 bartenders ASAP until 5am"',
'"Emergency! Need 3 servers right now till midnight"',
],
),
) {
: super(
const RapidOrderInitial(
examples: <String>[
'"We had a call out. Need 2 cooks ASAP"',
'"Need 5 bartenders ASAP until 5am"',
'"Emergency! Need 3 servers right now till midnight"',
],
),
) {
on<RapidOrderMessageChanged>(_onMessageChanged);
on<RapidOrderVoiceToggled>(_onVoiceToggled);
on<RapidOrderSubmitted>(_onSubmitted);
@@ -68,7 +68,8 @@ class RapidOrderBloc extends Bloc<RapidOrderEvent, RapidOrderState> {
try {
await _createRapidOrderUseCase(
RapidOrderArguments(description: message));
RapidOrderArguments(description: message),
);
emit(const RapidOrderSuccess());
} catch (e) {
emit(RapidOrderFailure(e.toString()));

View File

@@ -17,8 +17,9 @@ class ClientCreateOrderPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider<ClientCreateOrderBloc>(
create: (BuildContext context) => Modular.get<ClientCreateOrderBloc>()
..add(const ClientCreateOrderTypesRequested()),
create: (BuildContext context) =>
Modular.get<ClientCreateOrderBloc>()
..add(const ClientCreateOrderTypesRequested()),
child: const CreateOrderView(),
);
}

View File

@@ -65,59 +65,58 @@ class CreateOrderView extends StatelessWidget {
),
),
Expanded(
child:
BlocBuilder<ClientCreateOrderBloc, ClientCreateOrderState>(
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 OrderType type = state.orderTypes[index];
final OrderTypeUiMetadata ui =
OrderTypeUiMetadata.fromId(id: type.id);
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 OrderType type = state.orderTypes[index];
final OrderTypeUiMetadata ui =
OrderTypeUiMetadata.fromId(id: type.id);
return OrderTypeCard(
icon: ui.icon,
title: _getTranslation(key: type.titleKey),
description: _getTranslation(
key: type.descriptionKey,
),
backgroundColor: ui.backgroundColor,
borderColor: ui.borderColor,
iconBackgroundColor: ui.iconBackgroundColor,
iconColor: ui.iconColor,
textColor: ui.textColor,
descriptionColor: ui.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 OrderTypeCard(
icon: ui.icon,
title: _getTranslation(key: type.titleKey),
description: _getTranslation(
key: type.descriptionKey,
),
backgroundColor: ui.backgroundColor,
borderColor: ui.borderColor,
iconBackgroundColor: ui.iconBackgroundColor,
iconColor: ui.iconColor,
textColor: ui.textColor,
descriptionColor: ui.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());
},
}
return const Center(child: CircularProgressIndicator());
},
),
),
],

View File

@@ -54,9 +54,7 @@ class OneTimeOrderHeader extends StatelessWidget {
children: <Widget>[
Text(
title,
style: UiTypography.headline3m.copyWith(
color: UiColors.white,
),
style: UiTypography.headline3m.copyWith(color: UiColors.white),
),
Text(
subtitle,

View File

@@ -19,6 +19,7 @@ class OneTimeOrderPositionCard extends StatelessWidget {
required this.startLabel,
required this.endLabel,
required this.lunchLabel,
this.vendor,
super.key,
});
@@ -55,6 +56,9 @@ class OneTimeOrderPositionCard extends StatelessWidget {
/// Label for the lunch break.
final String lunchLabel;
/// The current selected vendor to determine rates.
final Vendor? vendor;
@override
Widget build(BuildContext context) {
return Container(
@@ -99,8 +103,10 @@ class OneTimeOrderPositionCard extends StatelessWidget {
child: DropdownButtonHideUnderline(
child: DropdownButton<String>(
isExpanded: true,
hint:
Text(roleLabel, style: UiTypography.body2r.textPlaceholder),
hint: Text(
roleLabel,
style: UiTypography.body2r.textPlaceholder,
),
value: position.role.isEmpty ? null : position.role,
icon: const Icon(
UiIcons.chevronDown,
@@ -112,26 +118,26 @@ class OneTimeOrderPositionCard extends StatelessWidget {
onUpdated(position.copyWith(role: val));
}
},
items: <String>[
'Server',
'Bartender',
'Cook',
'Busser',
'Host',
'Barista',
'Dishwasher',
'Event Staff'
].map((String role) {
// Mock rates for UI matching
final int rate = _getMockRate(role);
return DropdownMenuItem<String>(
value: role,
child: Text(
'$role - \$$rate/hr',
style: UiTypography.body2r.textPrimary,
),
);
}).toList(),
items:
<String>{
...(vendor?.rates.keys ?? <String>[]),
if (position.role.isNotEmpty &&
!(vendor?.rates.keys.contains(position.role) ??
false))
position.role,
}.map((String role) {
final double? rate = vendor?.rates[role];
final String label = rate == null
? role
: '$role - \$${rate.toStringAsFixed(0)}/hr';
return DropdownMenuItem<String>(
value: role,
child: Text(
label,
style: UiTypography.body2r.textPrimary,
),
);
}).toList(),
),
),
),
@@ -153,7 +159,8 @@ class OneTimeOrderPositionCard extends StatelessWidget {
);
if (picked != null && context.mounted) {
onUpdated(
position.copyWith(startTime: picked.format(context)));
position.copyWith(startTime: picked.format(context)),
);
}
},
),
@@ -172,7 +179,8 @@ class OneTimeOrderPositionCard extends StatelessWidget {
);
if (picked != null && context.mounted) {
onUpdated(
position.copyWith(endTime: picked.format(context)));
position.copyWith(endTime: picked.format(context)),
);
}
},
),
@@ -198,10 +206,13 @@ class OneTimeOrderPositionCard extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
GestureDetector(
onTap: () => onUpdated(position.copyWith(
count: (position.count > 1)
? position.count - 1
: 1)),
onTap: () {
if (position.count > 1) {
onUpdated(
position.copyWith(count: position.count - 1),
);
}
},
child: const Icon(UiIcons.minus, size: 12),
),
Text(
@@ -209,8 +220,11 @@ class OneTimeOrderPositionCard extends StatelessWidget {
style: UiTypography.body2b.textPrimary,
),
GestureDetector(
onTap: () => onUpdated(
position.copyWith(count: position.count + 1)),
onTap: () {
onUpdated(
position.copyWith(count: position.count + 1),
);
},
child: const Icon(UiIcons.add, size: 12),
),
],
@@ -223,74 +237,12 @@ class OneTimeOrderPositionCard extends StatelessWidget {
),
const SizedBox(height: UiConstants.space4),
// Optional Location Override
if (position.location == null)
GestureDetector(
onTap: () => onUpdated(position.copyWith(location: '')),
child: Row(
children: <Widget>[
const Icon(UiIcons.mapPin, size: 14, color: UiColors.primary),
const SizedBox(width: UiConstants.space1),
Text(
t.client_create_order.one_time.different_location,
style: UiTypography.footnote1m.copyWith(
color: UiColors.primary,
),
),
],
),
)
else
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Row(
children: <Widget>[
const Icon(UiIcons.mapPin,
size: 14, color: UiColors.iconSecondary),
const SizedBox(width: UiConstants.space1),
Text(
t.client_create_order.one_time
.different_location_title,
style: UiTypography.footnote1m.textSecondary,
),
],
),
GestureDetector(
onTap: () => onUpdated(position.copyWith(location: null)),
child: const Icon(
UiIcons.close,
size: 14,
color: UiColors.destructive,
),
),
],
),
const SizedBox(height: UiConstants.space2),
_PositionLocationInput(
value: position.location ?? '',
onChanged: (String val) =>
onUpdated(position.copyWith(location: val)),
hintText:
t.client_create_order.one_time.different_location_hint,
),
],
),
const SizedBox(height: UiConstants.space3),
// Lunch Break
Text(
lunchLabel,
style: UiTypography.footnote2r.textSecondary,
),
Text(lunchLabel, style: UiTypography.footnote2r.textSecondary),
const SizedBox(height: UiConstants.space1),
Container(
height: 44,
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space3),
height: 44,
decoration: BoxDecoration(
borderRadius: UiConstants.radiusMd,
border: Border.all(color: UiColors.border),
@@ -309,43 +261,15 @@ class OneTimeOrderPositionCard extends StatelessWidget {
onUpdated(position.copyWith(lunchBreak: val));
}
},
items: <DropdownMenuItem<int>>[
DropdownMenuItem<int>(
value: 0,
child: Text(t.client_create_order.one_time.no_break,
style: UiTypography.body2r.textPrimary),
),
DropdownMenuItem<int>(
value: 10,
items: <int>[0, 15, 30, 45, 60].map((int mins) {
return DropdownMenuItem<int>(
value: mins,
child: Text(
'10 ${t.client_create_order.one_time.paid_break}',
style: UiTypography.body2r.textPrimary),
),
DropdownMenuItem<int>(
value: 15,
child: Text(
'15 ${t.client_create_order.one_time.paid_break}',
style: UiTypography.body2r.textPrimary),
),
DropdownMenuItem<int>(
value: 30,
child: Text(
'30 ${t.client_create_order.one_time.unpaid_break}',
style: UiTypography.body2r.textPrimary),
),
DropdownMenuItem<int>(
value: 45,
child: Text(
'45 ${t.client_create_order.one_time.unpaid_break}',
style: UiTypography.body2r.textPrimary),
),
DropdownMenuItem<int>(
value: 60,
child: Text(
'60 ${t.client_create_order.one_time.unpaid_break}',
style: UiTypography.body2r.textPrimary),
),
],
mins == 0 ? 'No Break' : '$mins mins',
style: UiTypography.body2r.textPrimary,
),
);
}).toList(),
),
),
),
@@ -360,83 +284,37 @@ class OneTimeOrderPositionCard extends StatelessWidget {
required String value,
required VoidCallback onTap,
}) {
return UiTextField(
label: label,
controller: TextEditingController(text: value),
readOnly: true,
onTap: onTap,
hintText: '--:--',
);
}
int _getMockRate(String role) {
switch (role) {
case 'Server':
return 18;
case 'Bartender':
return 22;
case 'Cook':
return 20;
case 'Busser':
return 16;
case 'Host':
return 17;
case 'Barista':
return 16;
case 'Dishwasher':
return 15;
case 'Event Staff':
return 20;
default:
return 15;
}
}
}
class _PositionLocationInput extends StatefulWidget {
const _PositionLocationInput({
required this.value,
required this.hintText,
required this.onChanged,
});
final String value;
final String hintText;
final ValueChanged<String> onChanged;
@override
State<_PositionLocationInput> createState() => _PositionLocationInputState();
}
class _PositionLocationInputState extends State<_PositionLocationInput> {
late final TextEditingController _controller;
@override
void initState() {
super.initState();
_controller = TextEditingController(text: widget.value);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
void didUpdateWidget(_PositionLocationInput oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.value != _controller.text) {
_controller.text = widget.value;
}
}
@override
Widget build(BuildContext context) {
return UiTextField(
controller: _controller,
onChanged: widget.onChanged,
hintText: widget.hintText,
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(label, style: UiTypography.footnote2r.textSecondary),
const SizedBox(height: UiConstants.space1),
GestureDetector(
onTap: onTap,
child: Container(
height: 40,
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space3),
decoration: BoxDecoration(
borderRadius: UiConstants.radiusSm,
border: Border.all(color: UiColors.border),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Text(
value.isEmpty ? '--:--' : value,
style: UiTypography.body2r.textPrimary,
),
const Icon(
UiIcons.clock,
size: 14,
color: UiColors.iconSecondary,
),
],
),
),
),
],
);
}
}

View File

@@ -27,14 +27,28 @@ class OneTimeOrderSectionHeader extends StatelessWidget {
children: <Widget>[
Text(title, style: UiTypography.headline4m.textPrimary),
if (actionLabel != null && onAction != null)
UiButton.text(
TextButton(
onPressed: onAction,
leadingIcon: UiIcons.add,
text: actionLabel!,
iconSize: 16,
style: TextButton.styleFrom(
minimumSize: const Size(0, 24),
maximumSize: const Size(0, 24),
padding: EdgeInsets.zero,
minimumSize: Size.zero,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
const Icon(UiIcons.add, size: 16, color: Color(0xFF0032A0)),
const SizedBox(width: UiConstants.space2),
Text(
actionLabel!,
style: const TextStyle(
color: Color(0xFF0032A0),
fontSize: 14,
fontWeight:
FontWeight.w500, // Added to match typical button text
),
),
],
),
),
],

View File

@@ -35,6 +35,47 @@ class OneTimeOrderView extends StatelessWidget {
);
}
if (state.vendors.isEmpty &&
state.status != OneTimeOrderStatus.loading) {
return Scaffold(
backgroundColor: UiColors.bgPrimary,
body: Column(
children: <Widget>[
OneTimeOrderHeader(
title: labels.title,
subtitle: labels.subtitle,
onBack: () => Modular.to.pop(),
),
Expanded(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Icon(
UiIcons.search,
size: 64,
color: UiColors.iconInactive,
),
const SizedBox(height: UiConstants.space4),
Text(
'No Vendors Available',
style: UiTypography.headline3m.textPrimary,
),
const SizedBox(height: UiConstants.space2),
Text(
'There are no staffing vendors associated with your account.',
style: UiTypography.body2r.textSecondary,
textAlign: TextAlign.center,
),
],
),
),
),
],
),
);
}
return Scaffold(
backgroundColor: UiColors.bgPrimary,
body: Column(
@@ -58,8 +99,9 @@ class OneTimeOrderView extends StatelessWidget {
? labels.creating
: labels.create_order,
isLoading: state.status == OneTimeOrderStatus.loading,
onPressed: () => BlocProvider.of<OneTimeOrderBloc>(context)
.add(const OneTimeOrderSubmitted()),
onPressed: () => BlocProvider.of<OneTimeOrderBloc>(
context,
).add(const OneTimeOrderSubmitted()),
),
],
),
@@ -87,37 +129,78 @@ class _OneTimeOrderForm extends StatelessWidget {
),
const SizedBox(height: UiConstants.space4),
// Vendor Selection
Text('SELECT VENDOR', style: UiTypography.footnote2r.textSecondary),
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space3),
height: 48,
decoration: BoxDecoration(
color: UiColors.white,
borderRadius: UiConstants.radiusMd,
border: Border.all(color: UiColors.border),
),
child: DropdownButtonHideUnderline(
child: DropdownButton<Vendor>(
isExpanded: true,
value: state.selectedVendor,
icon: const Icon(
UiIcons.chevronDown,
size: 18,
color: UiColors.iconSecondary,
),
onChanged: (Vendor? vendor) {
if (vendor != null) {
BlocProvider.of<OneTimeOrderBloc>(
context,
).add(OneTimeOrderVendorChanged(vendor));
}
},
items: state.vendors.map((Vendor vendor) {
return DropdownMenuItem<Vendor>(
value: vendor,
child: Text(
vendor.name,
style: UiTypography.body2m.textPrimary,
),
);
}).toList(),
),
),
),
const SizedBox(height: UiConstants.space4),
OneTimeOrderDatePicker(
label: labels.date_label,
value: state.date,
onChanged: (DateTime date) =>
BlocProvider.of<OneTimeOrderBloc>(context)
.add(OneTimeOrderDateChanged(date)),
onChanged: (DateTime date) => BlocProvider.of<OneTimeOrderBloc>(
context,
).add(OneTimeOrderDateChanged(date)),
),
const SizedBox(height: UiConstants.space4),
OneTimeOrderLocationInput(
label: labels.location_label,
value: state.location,
onChanged: (String location) =>
BlocProvider.of<OneTimeOrderBloc>(context)
.add(OneTimeOrderLocationChanged(location)),
onChanged: (String location) => BlocProvider.of<OneTimeOrderBloc>(
context,
).add(OneTimeOrderLocationChanged(location)),
),
const SizedBox(height: UiConstants.space6),
OneTimeOrderSectionHeader(
title: labels.positions_title,
actionLabel: labels.add_position,
onAction: () => BlocProvider.of<OneTimeOrderBloc>(context)
.add(const OneTimeOrderPositionAdded()),
onAction: () => BlocProvider.of<OneTimeOrderBloc>(
context,
).add(const OneTimeOrderPositionAdded()),
),
const SizedBox(height: UiConstants.space3),
// Positions List
...state.positions
.asMap()
.entries
.map((MapEntry<int, OneTimeOrderPosition> entry) {
...state.positions.asMap().entries.map((
MapEntry<int, OneTimeOrderPosition> entry,
) {
final int index = entry.key;
final OneTimeOrderPosition position = entry.value;
return Padding(
@@ -132,14 +215,16 @@ class _OneTimeOrderForm extends StatelessWidget {
startLabel: labels.start_label,
endLabel: labels.end_label,
lunchLabel: labels.lunch_break_label,
vendor: state.selectedVendor,
onUpdated: (OneTimeOrderPosition updated) {
BlocProvider.of<OneTimeOrderBloc>(context).add(
OneTimeOrderPositionUpdated(index, updated),
);
BlocProvider.of<OneTimeOrderBloc>(
context,
).add(OneTimeOrderPositionUpdated(index, updated));
},
onRemoved: () {
BlocProvider.of<OneTimeOrderBloc>(context)
.add(OneTimeOrderPositionRemoved(index));
BlocProvider.of<OneTimeOrderBloc>(
context,
).add(OneTimeOrderPositionRemoved(index));
},
),
);

View File

@@ -73,16 +73,14 @@ class OrderTypeCard extends StatelessWidget {
),
child: Icon(icon, color: iconColor, size: 24),
),
Text(
title,
style: UiTypography.body2b.copyWith(color: textColor),
),
Text(title, style: UiTypography.body2b.copyWith(color: textColor)),
const SizedBox(height: UiConstants.space1),
Expanded(
child: Text(
description,
style:
UiTypography.footnote1r.copyWith(color: descriptionColor),
style: UiTypography.footnote1r.copyWith(
color: descriptionColor,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),

View File

@@ -47,10 +47,7 @@ class RapidOrderExampleCard extends StatelessWidget {
text: TextSpan(
style: UiTypography.body2r.textPrimary,
children: <InlineSpan>[
TextSpan(
text: label,
style: UiTypography.body2b.textPrimary,
),
TextSpan(text: label, style: UiTypography.body2b.textPrimary),
TextSpan(text: ' $example'),
],
),

View File

@@ -74,11 +74,7 @@ class RapidOrderHeader extends StatelessWidget {
children: <Widget>[
Row(
children: <Widget>[
const Icon(
UiIcons.zap,
color: UiColors.accent,
size: 18,
),
const Icon(UiIcons.zap, color: UiColors.accent, size: 18),
const SizedBox(width: UiConstants.space2),
Text(
title,

View File

@@ -42,8 +42,9 @@ class RapidOrderSuccessView extends StatelessWidget {
child: SafeArea(
child: Center(
child: Container(
margin:
const EdgeInsets.symmetric(horizontal: UiConstants.space10),
margin: const EdgeInsets.symmetric(
horizontal: UiConstants.space10,
),
padding: const EdgeInsets.all(UiConstants.space8),
decoration: BoxDecoration(
color: UiColors.white,
@@ -75,10 +76,7 @@ class RapidOrderSuccessView extends StatelessWidget {
),
),
const SizedBox(height: UiConstants.space6),
Text(
title,
style: UiTypography.headline1m.textPrimary,
),
Text(title, style: UiTypography.headline1m.textPrimary),
const SizedBox(height: UiConstants.space3),
Text(
message,

View File

@@ -153,27 +153,27 @@ class _RapidOrderFormState extends State<_RapidOrderForm> {
// Examples
if (initialState != null)
...initialState.examples
.asMap()
.entries
.map((MapEntry<int, String> entry) {
...initialState.examples.asMap().entries.map((
MapEntry<int, String> entry,
) {
final int index = entry.key;
final String example = entry.value;
final bool isHighlighted = index == 0;
return Padding(
padding: const EdgeInsets.only(
bottom: UiConstants.space2),
bottom: UiConstants.space2,
),
child: RapidOrderExampleCard(
example: example,
isHighlighted: isHighlighted,
label: labels.example,
onTap: () =>
BlocProvider.of<RapidOrderBloc>(
context)
.add(
RapidOrderExampleSelected(example),
),
context,
).add(
RapidOrderExampleSelected(example),
),
),
);
}),
@@ -184,9 +184,9 @@ class _RapidOrderFormState extends State<_RapidOrderForm> {
controller: _messageController,
maxLines: 4,
onChanged: (String value) {
BlocProvider.of<RapidOrderBloc>(context).add(
RapidOrderMessageChanged(value),
);
BlocProvider.of<RapidOrderBloc>(
context,
).add(RapidOrderMessageChanged(value));
},
hintText: labels.hint,
),
@@ -197,7 +197,8 @@ class _RapidOrderFormState extends State<_RapidOrderForm> {
labels: labels,
isSubmitting: isSubmitting,
isListening: initialState?.isListening ?? false,
isMessageEmpty: initialState != null &&
isMessageEmpty:
initialState != null &&
initialState.message.trim().isEmpty,
),
],
@@ -242,11 +243,7 @@ class _AnimatedZapIcon extends StatelessWidget {
),
],
),
child: const Icon(
UiIcons.zap,
color: UiColors.white,
size: 32,
),
child: const Icon(UiIcons.zap, color: UiColors.white, size: 32),
);
}
}
@@ -271,9 +268,9 @@ class _RapidOrderActions extends StatelessWidget {
child: UiButton.secondary(
text: isListening ? labels.listening : labels.speak,
leadingIcon: UiIcons.bell, // Placeholder for mic
onPressed: () => BlocProvider.of<RapidOrderBloc>(context).add(
const RapidOrderVoiceToggled(),
),
onPressed: () => BlocProvider.of<RapidOrderBloc>(
context,
).add(const RapidOrderVoiceToggled()),
style: OutlinedButton.styleFrom(
backgroundColor: isListening
? UiColors.destructive.withValues(alpha: 0.05)
@@ -291,9 +288,9 @@ class _RapidOrderActions extends StatelessWidget {
trailingIcon: UiIcons.arrowRight,
onPressed: isSubmitting || isMessageEmpty
? null
: () => BlocProvider.of<RapidOrderBloc>(context).add(
const RapidOrderSubmitted(),
),
: () => BlocProvider.of<RapidOrderBloc>(
context,
).add(const RapidOrderSubmitted()),
),
),
],

View File

@@ -0,0 +1,17 @@
import 'package:krow_data_connect/krow_data_connect.dart';
import 'package:krow_domain/krow_domain.dart';
import '../../domain/repositories/i_view_orders_repository.dart';
/// Implementation of [IViewOrdersRepository] providing data from [OrderRepositoryMock].
class ViewOrdersRepositoryImpl implements IViewOrdersRepository {
final OrderRepositoryMock _orderRepositoryMock;
/// Creates a [ViewOrdersRepositoryImpl] with the given [OrderRepositoryMock].
ViewOrdersRepositoryImpl({required OrderRepositoryMock orderRepositoryMock})
: _orderRepositoryMock = orderRepositoryMock;
@override
Future<List<OrderItem>> getOrders() {
return _orderRepositoryMock.getOrders();
}
}

View File

@@ -0,0 +1,7 @@
import 'package:krow_domain/krow_domain.dart';
/// Repository interface for fetching and managing client orders.
abstract class IViewOrdersRepository {
/// Fetches a list of [OrderItem] for the client.
Future<List<OrderItem>> getOrders();
}

View File

@@ -0,0 +1,19 @@
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
import '../repositories/i_view_orders_repository.dart';
/// Use case for retrieving the list of client orders.
///
/// This use case encapsulates the business rule of fetching orders
/// and delegates the data retrieval to the [IViewOrdersRepository].
class GetOrdersUseCase implements NoInputUseCase<List<OrderItem>> {
final IViewOrdersRepository _repository;
/// Creates a [GetOrdersUseCase] with the required [IViewOrdersRepository].
GetOrdersUseCase(this._repository);
@override
Future<List<OrderItem>> call() {
return _repository.getOrders();
}
}

View File

@@ -0,0 +1,156 @@
import 'package:intl/intl.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:krow_domain/krow_domain.dart';
import '../../domain/usecases/get_orders_use_case.dart';
import 'view_orders_state.dart';
/// Cubit for managing the state of the View Orders feature.
///
/// This Cubit handles loading orders, date selection, and tab filtering.
class ViewOrdersCubit extends Cubit<ViewOrdersState> {
ViewOrdersCubit({required GetOrdersUseCase getOrdersUseCase})
: _getOrdersUseCase = getOrdersUseCase,
super(ViewOrdersState(selectedDate: DateTime.now())) {
_init();
}
final GetOrdersUseCase _getOrdersUseCase;
void _init() {
updateWeekOffset(0); // Initialize calendar days
loadOrders();
}
/// Loads the list of orders using the [GetOrdersUseCase].
Future<void> loadOrders() async {
emit(state.copyWith(status: ViewOrdersStatus.loading));
try {
final List<OrderItem> orders = await _getOrdersUseCase();
emit(state.copyWith(status: ViewOrdersStatus.success, orders: orders));
_updateDerivedState();
} catch (_) {
emit(state.copyWith(status: ViewOrdersStatus.failure));
}
}
void selectDate(DateTime date) {
emit(state.copyWith(selectedDate: date));
_updateDerivedState();
}
void selectFilterTab(String tabId) {
emit(state.copyWith(filterTab: tabId));
_updateDerivedState();
}
void updateWeekOffset(int offset) {
final int newWeekOffset = state.weekOffset + offset;
final List<DateTime> calendarDays = _calculateCalendarDays(newWeekOffset);
emit(state.copyWith(weekOffset: newWeekOffset, calendarDays: calendarDays));
_updateDerivedState();
}
void _updateDerivedState() {
final List<OrderItem> filteredOrders = _calculateFilteredOrders(state);
final int activeCount = _calculateCategoryCount('active');
final int completedCount = _calculateCategoryCount('completed');
final int upNextCount = _calculateUpNextCount();
emit(
state.copyWith(
filteredOrders: filteredOrders,
activeCount: activeCount,
completedCount: completedCount,
upNextCount: upNextCount,
),
);
}
List<DateTime> _calculateCalendarDays(int weekOffset) {
final DateTime now = DateTime.now();
final int jsDay = now.weekday == 7 ? 0 : now.weekday;
final int daysSinceFriday = (jsDay + 2) % 7;
final DateTime startDate = DateTime(now.year, now.month, now.day)
.subtract(Duration(days: daysSinceFriday))
.add(Duration(days: weekOffset * 7));
return List<DateTime>.generate(
7,
(int index) => startDate.add(Duration(days: index)),
);
}
List<OrderItem> _calculateFilteredOrders(ViewOrdersState state) {
if (state.selectedDate == null) return <OrderItem>[];
final String selectedDateStr = DateFormat(
'yyyy-MM-dd',
).format(state.selectedDate!);
// Filter by date
final List<OrderItem> ordersOnDate = state.orders
.where((OrderItem s) => s.date == selectedDateStr)
.toList();
// Sort by start time
ordersOnDate.sort(
(OrderItem a, OrderItem b) => a.startTime.compareTo(b.startTime),
);
if (state.filterTab == 'all') {
return ordersOnDate
.where(
(OrderItem s) =>
<String>['open', 'filled', 'confirmed'].contains(s.status),
)
.toList();
} else if (state.filterTab == 'active') {
return ordersOnDate
.where((OrderItem s) => s.status == 'in_progress')
.toList();
} else if (state.filterTab == 'completed') {
return ordersOnDate
.where((OrderItem s) => s.status == 'completed')
.toList();
}
return <OrderItem>[];
}
int _calculateCategoryCount(String category) {
if (state.selectedDate == null) return 0;
final String selectedDateStr = DateFormat(
'yyyy-MM-dd',
).format(state.selectedDate!);
final List<OrderItem> ordersOnDate = state.orders
.where((OrderItem s) => s.date == selectedDateStr)
.toList();
if (category == 'active') {
return ordersOnDate
.where((OrderItem s) => s.status == 'in_progress')
.length;
} else if (category == 'completed') {
return ordersOnDate
.where((OrderItem s) => s.status == 'completed')
.length;
}
return 0;
}
int _calculateUpNextCount() {
if (state.selectedDate == null) return 0;
final String selectedDateStr = DateFormat(
'yyyy-MM-dd',
).format(state.selectedDate!);
final List<OrderItem> ordersOnDate = state.orders
.where((OrderItem s) => s.date == selectedDateStr)
.toList();
return ordersOnDate
.where(
(OrderItem s) =>
<String>['open', 'filled', 'confirmed'].contains(s.status),
)
.length;
}
}

View File

@@ -0,0 +1,70 @@
import 'package:equatable/equatable.dart';
import 'package:krow_domain/krow_domain.dart';
enum ViewOrdersStatus { initial, loading, success, failure }
class ViewOrdersState extends Equatable {
const ViewOrdersState({
this.status = ViewOrdersStatus.initial,
this.orders = const <OrderItem>[],
this.filteredOrders = const <OrderItem>[],
this.calendarDays = const <DateTime>[],
this.selectedDate,
this.filterTab = 'all',
this.weekOffset = 0,
this.activeCount = 0,
this.completedCount = 0,
this.upNextCount = 0,
});
final ViewOrdersStatus status;
final List<OrderItem> orders;
final List<OrderItem> filteredOrders;
final List<DateTime> calendarDays;
final DateTime? selectedDate;
final String filterTab;
final int weekOffset;
final int activeCount;
final int completedCount;
final int upNextCount;
ViewOrdersState copyWith({
ViewOrdersStatus? status,
List<OrderItem>? orders,
List<OrderItem>? filteredOrders,
List<DateTime>? calendarDays,
DateTime? selectedDate,
String? filterTab,
int? weekOffset,
int? activeCount,
int? completedCount,
int? upNextCount,
}) {
return ViewOrdersState(
status: status ?? this.status,
orders: orders ?? this.orders,
filteredOrders: filteredOrders ?? this.filteredOrders,
calendarDays: calendarDays ?? this.calendarDays,
selectedDate: selectedDate ?? this.selectedDate,
filterTab: filterTab ?? this.filterTab,
weekOffset: weekOffset ?? this.weekOffset,
activeCount: activeCount ?? this.activeCount,
completedCount: completedCount ?? this.completedCount,
upNextCount: upNextCount ?? this.upNextCount,
);
}
@override
List<Object?> get props => <Object?>[
status,
orders,
filteredOrders,
calendarDays,
selectedDate,
filterTab,
weekOffset,
activeCount,
completedCount,
upNextCount,
];
}

View File

@@ -0,0 +1,14 @@
import 'package:flutter_modular/flutter_modular.dart';
/// Extension to provide typed navigation for the View Orders feature.
extension ViewOrdersNavigator on IModularNavigator {
/// Navigates to the Create Order feature.
void navigateToCreateOrder() {
pushNamed('/client/create-order/');
}
/// Navigates to the Order Details (placeholder for now).
void navigateToOrderDetails(String orderId) {
// pushNamed('/view-orders/$orderId');
}
}

View File

@@ -0,0 +1,472 @@
import 'dart:ui';
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_modular/flutter_modular.dart';
import 'package:intl/intl.dart';
import 'package:core_localization/core_localization.dart';
import '../blocs/view_orders_cubit.dart';
import '../blocs/view_orders_state.dart';
import 'package:krow_domain/krow_domain.dart';
import '../widgets/view_order_card.dart';
import '../navigation/view_orders_navigator.dart';
/// The main page for viewing client orders.
///
/// This page follows the KROW Clean Architecture by:
/// - Being a [StatelessWidget].
/// - Using [ViewOrdersCubit] for state management.
/// - Adhering to the project's Design System.
class ViewOrdersPage extends StatelessWidget {
/// Creates a [ViewOrdersPage].
const ViewOrdersPage({super.key});
@override
Widget build(BuildContext context) {
return BlocProvider<ViewOrdersCubit>(
create: (BuildContext context) => Modular.get<ViewOrdersCubit>(),
child: const ViewOrdersView(),
);
}
}
/// The internal view implementation for [ViewOrdersPage].
class ViewOrdersView extends StatelessWidget {
/// Creates a [ViewOrdersView].
const ViewOrdersView({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<ViewOrdersCubit, ViewOrdersState>(
builder: (BuildContext context, ViewOrdersState state) {
final List<DateTime> calendarDays = state.calendarDays;
final List<OrderItem> filteredOrders = state.filteredOrders;
// Header Colors logic from prototype
String sectionTitle = '';
Color dotColor = UiColors.transparent;
if (state.filterTab == 'all') {
sectionTitle = t.client_view_orders.tabs.up_next;
dotColor = UiColors.primary;
} else if (state.filterTab == 'active') {
sectionTitle = t.client_view_orders.tabs.active;
dotColor = UiColors.textWarning;
} else if (state.filterTab == 'completed') {
sectionTitle = t.client_view_orders.tabs.completed;
dotColor =
UiColors.primary; // Reverting to primary blue for consistency
}
return Scaffold(
backgroundColor: UiColors.white,
body: Stack(
children: <Widget>[
// Background Gradient
Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: <Color>[UiColors.bgSecondary, UiColors.white],
stops: <double>[0.0, 0.3],
),
),
),
SafeArea(
child: Column(
children: <Widget>[
// Header + Filter + Calendar (Sticky behavior)
_buildHeader(
context: context,
state: state,
calendarDays: calendarDays,
),
// Content List
Expanded(
child: filteredOrders.isEmpty
? _buildEmptyState(context: context, state: state)
: ListView(
padding: const EdgeInsets.fromLTRB(
UiConstants.space5,
UiConstants.space4,
UiConstants.space5,
100,
),
children: <Widget>[
if (filteredOrders.isNotEmpty)
Padding(
padding: const EdgeInsets.only(
bottom: UiConstants.space3,
),
child: Row(
children: <Widget>[
Container(
width: 8,
height: 8,
decoration: BoxDecoration(
color: dotColor,
shape: BoxShape.circle,
),
),
const SizedBox(
width: UiConstants.space2,
),
Text(
sectionTitle.toUpperCase(),
style: UiTypography.titleUppercase2m
.copyWith(
color: UiColors.textPrimary,
),
),
const SizedBox(
width: UiConstants.space1,
),
Text(
'(${filteredOrders.length})',
style: UiTypography.footnote1r
.copyWith(
color: UiColors.textSecondary,
),
),
],
),
),
...filteredOrders.map(
(OrderItem order) => Padding(
padding: const EdgeInsets.only(
bottom: UiConstants.space3,
),
child: ViewOrderCard(order: order),
),
),
],
),
),
],
),
),
],
),
);
},
);
}
/// Builds the sticky header section.
Widget _buildHeader({
required BuildContext context,
required ViewOrdersState state,
required List<DateTime> calendarDays,
}) {
return ClipRect(
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
child: Container(
decoration: const BoxDecoration(
color: Color(0xCCFFFFFF), // White with 0.8 alpha
border: Border(
bottom: BorderSide(color: UiColors.separatorSecondary),
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
// Top Bar
Padding(
padding: const EdgeInsets.fromLTRB(
UiConstants.space5,
UiConstants.space5,
UiConstants.space5,
UiConstants.space3,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Text(
t.client_view_orders.title,
style: UiTypography.headline3m.copyWith(
color: UiColors.textPrimary,
fontWeight: FontWeight.bold,
),
),
UiButton.primary(
text: t.client_view_orders.post_button,
leadingIcon: UiIcons.add,
onPressed: () => Modular.to.navigateToCreateOrder(),
size: UiButtonSize.small,
style: ElevatedButton.styleFrom(
minimumSize: const Size(0, 48),
maximumSize: const Size(0, 48),
),
),
],
),
),
// Filter Tabs
Padding(
padding: const EdgeInsets.only(bottom: UiConstants.space3),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
_buildFilterTab(
context,
label: t.client_view_orders.tabs.up_next,
isSelected: state.filterTab == 'all',
tabId: 'all',
),
const SizedBox(width: UiConstants.space6),
_buildFilterTab(
context,
label: t.client_view_orders.tabs.active,
isSelected: state.filterTab == 'active',
tabId: 'active',
count: state.activeCount + state.upNextCount,
),
const SizedBox(width: UiConstants.space6),
_buildFilterTab(
context,
label: t.client_view_orders.tabs.completed,
isSelected: state.filterTab == 'completed',
tabId: 'completed',
count: state.completedCount,
),
],
),
),
// Calendar Header controls
Padding(
padding: const EdgeInsets.symmetric(
horizontal: UiConstants.space5,
vertical: UiConstants.space2,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
IconButton(
icon: const Icon(
UiIcons.chevronLeft,
size: 20,
color: UiColors.iconSecondary,
),
onPressed: () => BlocProvider.of<ViewOrdersCubit>(
context,
).updateWeekOffset(-1),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
splashRadius: 20,
),
Text(
DateFormat('MMMM yyyy').format(calendarDays.first),
style: UiTypography.body2m.copyWith(
color: UiColors.textSecondary,
),
),
IconButton(
icon: const Icon(
UiIcons.chevronRight,
size: 20,
color: UiColors.iconSecondary,
),
onPressed: () => BlocProvider.of<ViewOrdersCubit>(
context,
).updateWeekOffset(1),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
splashRadius: 20,
),
],
),
),
// Calendar Grid
SizedBox(
height: 72,
child: ListView.separated(
padding: const EdgeInsets.symmetric(
horizontal: UiConstants.space5,
),
scrollDirection: Axis.horizontal,
itemCount: 7,
separatorBuilder: (BuildContext context, int index) =>
const SizedBox(width: UiConstants.space2),
itemBuilder: (BuildContext context, int index) {
final DateTime date = calendarDays[index];
final bool isSelected =
state.selectedDate != null &&
date.year == state.selectedDate!.year &&
date.month == state.selectedDate!.month &&
date.day == state.selectedDate!.day;
// Check if this date has any shifts
final String dateStr = DateFormat(
'yyyy-MM-dd',
).format(date);
final bool hasShifts = state.orders.any(
(OrderItem s) => s.date == dateStr,
);
return GestureDetector(
onTap: () => BlocProvider.of<ViewOrdersCubit>(
context,
).selectDate(date),
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
width: 48,
decoration: BoxDecoration(
color: isSelected ? UiColors.primary : UiColors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: isSelected
? UiColors.primary
: UiColors.separatorPrimary,
),
boxShadow: isSelected
? <BoxShadow>[
BoxShadow(
color: UiColors.primary.withValues(
alpha: 0.25,
),
blurRadius: 12,
offset: const Offset(0, 4),
),
]
: null,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
DateFormat('dd').format(date),
style: UiTypography.title2b.copyWith(
fontSize: 18,
color: isSelected
? UiColors.white
: UiColors.textPrimary,
),
),
Text(
DateFormat('E').format(date),
style: UiTypography.footnote2m.copyWith(
color: isSelected
? UiColors.white.withValues(alpha: 0.8)
: UiColors.textSecondary,
),
),
if (hasShifts) ...<Widget>[
const SizedBox(height: UiConstants.space1),
Container(
width: 6,
height: 6,
decoration: BoxDecoration(
color: isSelected
? UiColors.white
: UiColors.primary,
shape: BoxShape.circle,
),
),
],
],
),
),
);
},
),
),
const SizedBox(height: UiConstants.space4),
],
),
),
),
);
}
/// Builds a single filter tab.
Widget _buildFilterTab(
BuildContext context, {
required String label,
required bool isSelected,
required String tabId,
int? count,
}) {
String text = label;
if (count != null) {
text = '$label ($count)';
}
return GestureDetector(
onTap: () =>
BlocProvider.of<ViewOrdersCubit>(context).selectFilterTab(tabId),
child: Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.only(bottom: UiConstants.space2),
child: Text(
text,
style: UiTypography.body2m.copyWith(
color: isSelected ? UiColors.primary : UiColors.textSecondary,
fontWeight: FontWeight.w600,
),
),
),
AnimatedContainer(
duration: const Duration(milliseconds: 200),
height: 2,
width: isSelected ? 40 : 0,
decoration: BoxDecoration(
color: UiColors.primary,
borderRadius: BorderRadius.circular(2),
),
),
if (!isSelected) const SizedBox(height: 2),
],
),
);
}
/// Builds the empty state view.
Widget _buildEmptyState({
required BuildContext context,
required ViewOrdersState state,
}) {
final String dateStr = state.selectedDate != null
? _formatDateHeader(state.selectedDate!)
: 'this date';
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Icon(UiIcons.calendar, size: 48, color: UiColors.iconInactive),
const SizedBox(height: UiConstants.space3),
Text(
t.client_view_orders.no_orders(date: dateStr),
style: UiTypography.body2r.copyWith(color: UiColors.textSecondary),
),
const SizedBox(height: UiConstants.space4),
UiButton.primary(
text: t.client_view_orders.post_order,
leadingIcon: UiIcons.add,
onPressed: () => Modular.to.navigateToCreateOrder(),
),
],
),
);
}
static String _formatDateHeader(DateTime date) {
final DateTime now = DateTime.now();
final DateTime today = DateTime(now.year, now.month, now.day);
final DateTime tomorrow = today.add(const Duration(days: 1));
final DateTime checkDate = DateTime(date.year, date.month, date.day);
if (checkDate == today) return 'Today';
if (checkDate == tomorrow) return 'Tomorrow';
return DateFormat('EEE, MMM d').format(date);
}
}

View File

@@ -0,0 +1,39 @@
import 'package:flutter/material.dart';
import 'package:flutter_modular/flutter_modular.dart';
import 'package:krow_data_connect/krow_data_connect.dart';
import 'data/repositories/view_orders_repository_impl.dart';
import 'domain/repositories/i_view_orders_repository.dart';
import 'domain/usecases/get_orders_use_case.dart';
import 'presentation/blocs/view_orders_cubit.dart';
import 'presentation/pages/view_orders_page.dart';
/// Module for the View Orders feature.
///
/// This module sets up Dependency Injection for repositories, use cases,
/// and BLoCs, and defines the feature's navigation routes.
class ViewOrdersModule extends Module {
@override
List<Module> get imports => <Module>[DataConnectModule()];
@override
void binds(Injector i) {
// Repositories
i.addLazySingleton<IViewOrdersRepository>(
() => ViewOrdersRepositoryImpl(
orderRepositoryMock: i.get<OrderRepositoryMock>(),
),
);
// UseCases
i.addLazySingleton(GetOrdersUseCase.new);
// BLoCs
i.addSingleton(ViewOrdersCubit.new);
}
@override
void routes(RouteManager r) {
r.child('/', child: (BuildContext context) => const ViewOrdersPage());
}
}

View File

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

View File

@@ -0,0 +1,42 @@
name: view_orders
description: Client View Orders feature package
publish_to: 'none'
version: 1.0.0+1
resolution: workspace
environment:
sdk: '>=3.10.0 <4.0.0'
dependencies:
flutter:
sdk: flutter
# Architecture
flutter_modular: ^6.3.2
flutter_bloc: ^8.1.3
equatable: ^2.0.5
# Shared packages
design_system:
path: ../../../design_system
core_localization:
path: ../../../core_localization
krow_domain:
path: ../../../domain
krow_core:
path: ../../../core
# UI
lucide_icons: ^0.257.0
intl: ^0.20.1
url_launcher: ^6.3.1
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^6.0.0
bloc_test: ^9.1.5
mocktail: ^1.0.1
flutter:
uses-material-design: true

View File

@@ -1114,6 +1114,70 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.4.0"
url_launcher:
dependency: transitive
description:
name: url_launcher
sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8
url: "https://pub.dev"
source: hosted
version: "6.3.2"
url_launcher_android:
dependency: transitive
description:
name: url_launcher_android
sha256: "767344bf3063897b5cf0db830e94f904528e6dd50a6dfaf839f0abf509009611"
url: "https://pub.dev"
source: hosted
version: "6.3.28"
url_launcher_ios:
dependency: transitive
description:
name: url_launcher_ios
sha256: cfde38aa257dae62ffe79c87fab20165dfdf6988c1d31b58ebf59b9106062aad
url: "https://pub.dev"
source: hosted
version: "6.3.6"
url_launcher_linux:
dependency: transitive
description:
name: url_launcher_linux
sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a
url: "https://pub.dev"
source: hosted
version: "3.2.2"
url_launcher_macos:
dependency: transitive
description:
name: url_launcher_macos
sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18"
url: "https://pub.dev"
source: hosted
version: "3.2.5"
url_launcher_platform_interface:
dependency: transitive
description:
name: url_launcher_platform_interface
sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
url_launcher_web:
dependency: transitive
description:
name: url_launcher_web
sha256: d0412fcf4c6b31ecfdb7762359b7206ffba3bbffd396c6d9f9c4616ece476c1f
url: "https://pub.dev"
source: hosted
version: "2.4.2"
url_launcher_windows:
dependency: transitive
description:
name: url_launcher_windows
sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f"
url: "https://pub.dev"
source: hosted
version: "3.1.5"
uuid:
dependency: transitive
description:

View File

@@ -15,6 +15,7 @@ workspace:
- packages/features/client/settings
- packages/features/client/hubs
- packages/features/client/create_order
- packages/features/client/view_orders
- packages/features/client/client_main
- apps/staff
- apps/client