Merge branch '208-p0-auth-05-get-started-screen' into rapid_order_client
This commit is contained in:
@@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
),
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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,
|
||||
];
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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()));
|
||||
|
||||
@@ -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(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -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));
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
@@ -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'),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
];
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
library;
|
||||
|
||||
export 'src/view_orders_module.dart';
|
||||
@@ -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
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user