Merge branch '208-p0-auth-05-get-started-screen' into rapid_order_client
This commit is contained in:
@@ -311,6 +311,35 @@
|
|||||||
"orders": "Orders",
|
"orders": "Orders",
|
||||||
"reports": "Reports"
|
"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",
|
"orders": "Órdenes",
|
||||||
"reports": "Reportes"
|
"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 {
|
class OrderRepositoryMock {
|
||||||
/// Returns a list of available [OrderType]s.
|
/// Returns a list of available [OrderType]s.
|
||||||
Future<List<OrderType>> getOrderTypes() async {
|
Future<List<OrderType>> getOrderTypes() async {
|
||||||
await Future.delayed(const Duration(milliseconds: 500));
|
await Future<void>.delayed(const Duration(milliseconds: 500));
|
||||||
return const <OrderType>[
|
return const <OrderType>[
|
||||||
OrderType(
|
OrderType(
|
||||||
id: 'rapid',
|
id: 'rapid',
|
||||||
@@ -34,11 +34,122 @@ class OrderRepositoryMock {
|
|||||||
|
|
||||||
/// Simulates creating a one-time order.
|
/// Simulates creating a one-time order.
|
||||||
Future<void> createOneTimeOrder(OneTimeOrder order) async {
|
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.
|
/// Simulates creating a rapid order.
|
||||||
Future<void> createRapidOrder(String description) async {
|
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
|
/// User icon for profile
|
||||||
static const IconData user = _IconLib.user;
|
static const IconData user = _IconLib.user;
|
||||||
|
|
||||||
|
/// Users icon for groups or staff
|
||||||
|
static const IconData users = _IconLib.users;
|
||||||
|
|
||||||
/// Settings icon
|
/// Settings icon
|
||||||
static const IconData settings = _IconLib.settings;
|
static const IconData settings = _IconLib.settings;
|
||||||
|
|
||||||
@@ -81,6 +84,9 @@ class UiIcons {
|
|||||||
/// Chevron down icon
|
/// Chevron down icon
|
||||||
static const IconData chevronDown = _IconLib.chevronDown;
|
static const IconData chevronDown = _IconLib.chevronDown;
|
||||||
|
|
||||||
|
/// Chevron up icon
|
||||||
|
static const IconData chevronUp = _IconLib.chevronUp;
|
||||||
|
|
||||||
// --- Status & Feedback ---
|
// --- Status & Feedback ---
|
||||||
|
|
||||||
/// Info icon
|
/// Info icon
|
||||||
@@ -139,6 +145,9 @@ class UiIcons {
|
|||||||
/// Sparkles icon for features or AI
|
/// Sparkles icon for features or AI
|
||||||
static const IconData sparkles = _IconLib.sparkles;
|
static const IconData sparkles = _IconLib.sparkles;
|
||||||
|
|
||||||
|
/// Navigation/Compass icon
|
||||||
|
static const IconData navigation = _IconLib.navigation;
|
||||||
|
|
||||||
/// Star icon for ratings
|
/// Star icon for ratings
|
||||||
static const IconData star = _IconLib.star;
|
static const IconData star = _IconLib.star;
|
||||||
|
|
||||||
@@ -154,6 +163,12 @@ class UiIcons {
|
|||||||
/// Eye off icon for hidden visibility
|
/// Eye off icon for hidden visibility
|
||||||
static const IconData eyeOff = _IconLib.eyeOff;
|
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
|
/// Building icon for companies
|
||||||
static const IconData building = _IconLib.building2;
|
static const IconData building = _IconLib.building2;
|
||||||
|
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ class UiTheme {
|
|||||||
|
|
||||||
/// Returns the light theme for the Staff application.
|
/// Returns the light theme for the Staff application.
|
||||||
static ThemeData get light {
|
static ThemeData get light {
|
||||||
final colorScheme = UiColors.colorScheme;
|
final ColorScheme colorScheme = UiColors.colorScheme;
|
||||||
final textTheme = UiTypography.textTheme;
|
final TextTheme textTheme = UiTypography.textTheme;
|
||||||
|
|
||||||
return ThemeData(
|
return ThemeData(
|
||||||
useMaterial3: true,
|
useMaterial3: true,
|
||||||
@@ -68,7 +68,6 @@ class UiTheme {
|
|||||||
horizontal: UiConstants.space6,
|
horizontal: UiConstants.space6,
|
||||||
vertical: UiConstants.space3,
|
vertical: UiConstants.space3,
|
||||||
),
|
),
|
||||||
minimumSize: const Size(double.infinity, 54),
|
|
||||||
maximumSize: const Size(double.infinity, 54),
|
maximumSize: const Size(double.infinity, 54),
|
||||||
).copyWith(
|
).copyWith(
|
||||||
side: WidgetStateProperty.resolveWith<BorderSide?>((states) {
|
side: WidgetStateProperty.resolveWith<BorderSide?>((states) {
|
||||||
@@ -99,7 +98,6 @@ class UiTheme {
|
|||||||
horizontal: UiConstants.space4,
|
horizontal: UiConstants.space4,
|
||||||
vertical: UiConstants.space2,
|
vertical: UiConstants.space2,
|
||||||
),
|
),
|
||||||
minimumSize: const Size(double.infinity, 52),
|
|
||||||
maximumSize: const Size(double.infinity, 52),
|
maximumSize: const Size(double.infinity, 52),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -117,7 +115,6 @@ class UiTheme {
|
|||||||
horizontal: UiConstants.space4,
|
horizontal: UiConstants.space4,
|
||||||
vertical: UiConstants.space3,
|
vertical: UiConstants.space3,
|
||||||
),
|
),
|
||||||
minimumSize: const Size(double.infinity, 52),
|
|
||||||
maximumSize: const Size(double.infinity, 52),
|
maximumSize: const Size(double.infinity, 52),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -320,6 +320,15 @@ class UiTypography {
|
|||||||
color: UiColors.textPrimary,
|
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)
|
/// Body 4 Regular - Font: Instrument Sans, Size: 14, Height: 1.5, Spacing: 0.05 (#121826)
|
||||||
static final TextStyle body4r = _primaryBase.copyWith(
|
static final TextStyle body4r = _primaryBase.copyWith(
|
||||||
fontWeight: FontWeight.w400,
|
fontWeight: FontWeight.w400,
|
||||||
|
|||||||
@@ -27,6 +27,9 @@ class UiButton extends StatelessWidget {
|
|||||||
/// The size of the button.
|
/// The size of the button.
|
||||||
final UiButtonSize size;
|
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).
|
/// The button widget to use (ElevatedButton, OutlinedButton, or TextButton).
|
||||||
final Widget Function(
|
final Widget Function(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
@@ -48,6 +51,7 @@ class UiButton extends StatelessWidget {
|
|||||||
this.style,
|
this.style,
|
||||||
this.iconSize = 20,
|
this.iconSize = 20,
|
||||||
this.size = UiButtonSize.medium,
|
this.size = UiButtonSize.medium,
|
||||||
|
this.fullWidth = false,
|
||||||
}) : assert(
|
}) : assert(
|
||||||
text != null || child != null,
|
text != null || child != null,
|
||||||
'Either text or child must be provided',
|
'Either text or child must be provided',
|
||||||
@@ -64,6 +68,7 @@ class UiButton extends StatelessWidget {
|
|||||||
this.style,
|
this.style,
|
||||||
this.iconSize = 20,
|
this.iconSize = 20,
|
||||||
this.size = UiButtonSize.medium,
|
this.size = UiButtonSize.medium,
|
||||||
|
this.fullWidth = false,
|
||||||
}) : buttonBuilder = _elevatedButtonBuilder,
|
}) : buttonBuilder = _elevatedButtonBuilder,
|
||||||
assert(
|
assert(
|
||||||
text != null || child != null,
|
text != null || child != null,
|
||||||
@@ -81,6 +86,7 @@ class UiButton extends StatelessWidget {
|
|||||||
this.style,
|
this.style,
|
||||||
this.iconSize = 20,
|
this.iconSize = 20,
|
||||||
this.size = UiButtonSize.medium,
|
this.size = UiButtonSize.medium,
|
||||||
|
this.fullWidth = false,
|
||||||
}) : buttonBuilder = _outlinedButtonBuilder,
|
}) : buttonBuilder = _outlinedButtonBuilder,
|
||||||
assert(
|
assert(
|
||||||
text != null || child != null,
|
text != null || child != null,
|
||||||
@@ -98,6 +104,25 @@ class UiButton extends StatelessWidget {
|
|||||||
this.style,
|
this.style,
|
||||||
this.iconSize = 20,
|
this.iconSize = 20,
|
||||||
this.size = UiButtonSize.medium,
|
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,
|
}) : buttonBuilder = _textButtonBuilder,
|
||||||
assert(
|
assert(
|
||||||
text != null || child != null,
|
text != null || child != null,
|
||||||
@@ -107,7 +132,18 @@ class UiButton extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
/// Builds the button UI.
|
/// Builds the button UI.
|
||||||
Widget build(BuildContext context) {
|
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.
|
/// Builds the button content with optional leading and trailing icons.
|
||||||
@@ -116,27 +152,40 @@ class UiButton extends StatelessWidget {
|
|||||||
return child!;
|
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) {
|
if (leadingIcon == null && trailingIcon == null) {
|
||||||
return Text(text!);
|
return Text(buttonText, textAlign: TextAlign.center);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (leadingIcon != null && text == null && trailingIcon == null) {
|
// Multiple elements case: Use a Row with MainAxisSize.min
|
||||||
return Icon(leadingIcon, size: iconSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Multiple elements case
|
|
||||||
final List<Widget> children = [];
|
final List<Widget> children = [];
|
||||||
|
|
||||||
if (leadingIcon != null) {
|
if (leadingIcon != null) {
|
||||||
children.add(Icon(leadingIcon, size: iconSize));
|
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) {
|
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));
|
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.dart';
|
||||||
export 'src/entities/business/hub_department.dart';
|
export 'src/entities/business/hub_department.dart';
|
||||||
export 'src/entities/business/biz_contract.dart';
|
export 'src/entities/business/biz_contract.dart';
|
||||||
|
export 'src/entities/business/vendor.dart';
|
||||||
|
|
||||||
// Events & Shifts
|
// Events & Shifts
|
||||||
export 'src/entities/events/event.dart';
|
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/order_type.dart';
|
||||||
export 'src/entities/orders/one_time_order.dart';
|
export 'src/entities/orders/one_time_order.dart';
|
||||||
export 'src/entities/orders/one_time_order_position.dart';
|
export 'src/entities/orders/one_time_order_position.dart';
|
||||||
|
export 'src/entities/orders/order_item.dart';
|
||||||
|
|
||||||
// Skills & Certs
|
// Skills & Certs
|
||||||
export 'src/entities/skills/skill.dart';
|
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
|
.get_started_page
|
||||||
.sign_in_button,
|
.sign_in_button,
|
||||||
onPressed: () => Modular.to.pushClientSignIn(),
|
onPressed: () => Modular.to.pushClientSignIn(),
|
||||||
|
fullWidth: true,
|
||||||
),
|
),
|
||||||
|
|
||||||
const SizedBox(height: UiConstants.space3),
|
const SizedBox(height: UiConstants.space3),
|
||||||
@@ -108,6 +109,7 @@ class ClientGetStartedPage extends StatelessWidget {
|
|||||||
.get_started_page
|
.get_started_page
|
||||||
.create_account_button,
|
.create_account_button,
|
||||||
onPressed: () => Modular.to.pushClientSignUp(),
|
onPressed: () => Modular.to.pushClientSignUp(),
|
||||||
|
fullWidth: true,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ class _ClientSignInFormState extends State<ClientSignInForm> {
|
|||||||
UiButton.primary(
|
UiButton.primary(
|
||||||
text: widget.isLoading ? null : i18n.sign_in_button,
|
text: widget.isLoading ? null : i18n.sign_in_button,
|
||||||
onPressed: widget.isLoading ? null : _handleSubmit,
|
onPressed: widget.isLoading ? null : _handleSubmit,
|
||||||
|
fullWidth: true,
|
||||||
child: widget.isLoading
|
child: widget.isLoading
|
||||||
? const SizedBox(
|
? const SizedBox(
|
||||||
height: 24,
|
height: 24,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:client_home/client_home.dart';
|
import 'package:client_home/client_home.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_modular/flutter_modular.dart';
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
|
import 'package:view_orders/view_orders.dart';
|
||||||
|
|
||||||
import 'presentation/blocs/client_main_cubit.dart';
|
import 'presentation/blocs/client_main_cubit.dart';
|
||||||
import 'presentation/pages/client_main_page.dart';
|
import 'presentation/pages/client_main_page.dart';
|
||||||
@@ -30,11 +31,7 @@ class ClientMainModule extends Module {
|
|||||||
child: (BuildContext context) =>
|
child: (BuildContext context) =>
|
||||||
const PlaceholderPage(title: 'Billing'),
|
const PlaceholderPage(title: 'Billing'),
|
||||||
),
|
),
|
||||||
ChildRoute<dynamic>(
|
ModuleRoute<dynamic>('/orders', module: ViewOrdersModule()),
|
||||||
'/orders',
|
|
||||||
child: (BuildContext context) =>
|
|
||||||
const PlaceholderPage(title: 'Orders'),
|
|
||||||
),
|
|
||||||
ChildRoute<dynamic>(
|
ChildRoute<dynamic>(
|
||||||
'/reports',
|
'/reports',
|
||||||
child: (BuildContext context) =>
|
child: (BuildContext context) =>
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ dependencies:
|
|||||||
path: ../../../core_localization
|
path: ../../../core_localization
|
||||||
client_home:
|
client_home:
|
||||||
path: ../home
|
path: ../home
|
||||||
|
view_orders:
|
||||||
|
path: ../view_orders
|
||||||
# Intentionally commenting these out as they might not exist yet
|
# Intentionally commenting these out as they might not exist yet
|
||||||
# client_settings:
|
# client_settings:
|
||||||
# path: ../settings
|
# path: ../settings
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import 'client_create_order_state.dart';
|
|||||||
class ClientCreateOrderBloc
|
class ClientCreateOrderBloc
|
||||||
extends Bloc<ClientCreateOrderEvent, ClientCreateOrderState> {
|
extends Bloc<ClientCreateOrderEvent, ClientCreateOrderState> {
|
||||||
ClientCreateOrderBloc(this._getOrderTypesUseCase)
|
ClientCreateOrderBloc(this._getOrderTypesUseCase)
|
||||||
: super(const ClientCreateOrderInitial()) {
|
: super(const ClientCreateOrderInitial()) {
|
||||||
on<ClientCreateOrderTypesRequested>(_onTypesRequested);
|
on<ClientCreateOrderTypesRequested>(_onTypesRequested);
|
||||||
}
|
}
|
||||||
final GetOrderTypesUseCase _getOrderTypesUseCase;
|
final GetOrderTypesUseCase _getOrderTypesUseCase;
|
||||||
|
|||||||
@@ -8,16 +8,71 @@ import 'one_time_order_state.dart';
|
|||||||
/// BLoC for managing the multi-step one-time order creation form.
|
/// BLoC for managing the multi-step one-time order creation form.
|
||||||
class OneTimeOrderBloc extends Bloc<OneTimeOrderEvent, OneTimeOrderState> {
|
class OneTimeOrderBloc extends Bloc<OneTimeOrderEvent, OneTimeOrderState> {
|
||||||
OneTimeOrderBloc(this._createOneTimeOrderUseCase)
|
OneTimeOrderBloc(this._createOneTimeOrderUseCase)
|
||||||
: super(OneTimeOrderState.initial()) {
|
: super(OneTimeOrderState.initial()) {
|
||||||
|
on<OneTimeOrderVendorsLoaded>(_onVendorsLoaded);
|
||||||
|
on<OneTimeOrderVendorChanged>(_onVendorChanged);
|
||||||
on<OneTimeOrderDateChanged>(_onDateChanged);
|
on<OneTimeOrderDateChanged>(_onDateChanged);
|
||||||
on<OneTimeOrderLocationChanged>(_onLocationChanged);
|
on<OneTimeOrderLocationChanged>(_onLocationChanged);
|
||||||
on<OneTimeOrderPositionAdded>(_onPositionAdded);
|
on<OneTimeOrderPositionAdded>(_onPositionAdded);
|
||||||
on<OneTimeOrderPositionRemoved>(_onPositionRemoved);
|
on<OneTimeOrderPositionRemoved>(_onPositionRemoved);
|
||||||
on<OneTimeOrderPositionUpdated>(_onPositionUpdated);
|
on<OneTimeOrderPositionUpdated>(_onPositionUpdated);
|
||||||
on<OneTimeOrderSubmitted>(_onSubmitted);
|
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;
|
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(
|
void _onDateChanged(
|
||||||
OneTimeOrderDateChanged event,
|
OneTimeOrderDateChanged event,
|
||||||
Emitter<OneTimeOrderState> emit,
|
Emitter<OneTimeOrderState> emit,
|
||||||
@@ -37,13 +92,14 @@ class OneTimeOrderBloc extends Bloc<OneTimeOrderEvent, OneTimeOrderState> {
|
|||||||
Emitter<OneTimeOrderState> emit,
|
Emitter<OneTimeOrderState> emit,
|
||||||
) {
|
) {
|
||||||
final List<OneTimeOrderPosition> newPositions =
|
final List<OneTimeOrderPosition> newPositions =
|
||||||
List<OneTimeOrderPosition>.from(state.positions)
|
List<OneTimeOrderPosition>.from(state.positions)..add(
|
||||||
..add(const OneTimeOrderPosition(
|
const OneTimeOrderPosition(
|
||||||
role: '',
|
role: '',
|
||||||
count: 1,
|
count: 1,
|
||||||
startTime: '',
|
startTime: '09:00',
|
||||||
endTime: '',
|
endTime: '17:00',
|
||||||
));
|
),
|
||||||
|
);
|
||||||
emit(state.copyWith(positions: newPositions));
|
emit(state.copyWith(positions: newPositions));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,14 +135,17 @@ class OneTimeOrderBloc extends Bloc<OneTimeOrderEvent, OneTimeOrderState> {
|
|||||||
date: state.date,
|
date: state.date,
|
||||||
location: state.location,
|
location: state.location,
|
||||||
positions: state.positions,
|
positions: state.positions,
|
||||||
|
// In a real app, we'd pass the vendorId here
|
||||||
);
|
);
|
||||||
await _createOneTimeOrderUseCase(OneTimeOrderArguments(order: order));
|
await _createOneTimeOrderUseCase(OneTimeOrderArguments(order: order));
|
||||||
emit(state.copyWith(status: OneTimeOrderStatus.success));
|
emit(state.copyWith(status: OneTimeOrderStatus.success));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
emit(state.copyWith(
|
emit(
|
||||||
status: OneTimeOrderStatus.failure,
|
state.copyWith(
|
||||||
errorMessage: e.toString(),
|
status: OneTimeOrderStatus.failure,
|
||||||
));
|
errorMessage: e.toString(),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,22 @@ abstract class OneTimeOrderEvent extends Equatable {
|
|||||||
List<Object?> get props => <Object?>[];
|
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 {
|
class OneTimeOrderDateChanged extends OneTimeOrderEvent {
|
||||||
const OneTimeOrderDateChanged(this.date);
|
const OneTimeOrderDateChanged(this.date);
|
||||||
final DateTime date;
|
final DateTime date;
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ class OneTimeOrderState extends Equatable {
|
|||||||
required this.positions,
|
required this.positions,
|
||||||
this.status = OneTimeOrderStatus.initial,
|
this.status = OneTimeOrderStatus.initial,
|
||||||
this.errorMessage,
|
this.errorMessage,
|
||||||
|
this.vendors = const <Vendor>[],
|
||||||
|
this.selectedVendor,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory OneTimeOrderState.initial() {
|
factory OneTimeOrderState.initial() {
|
||||||
@@ -17,13 +19,9 @@ class OneTimeOrderState extends Equatable {
|
|||||||
date: DateTime.now(),
|
date: DateTime.now(),
|
||||||
location: '',
|
location: '',
|
||||||
positions: const <OneTimeOrderPosition>[
|
positions: const <OneTimeOrderPosition>[
|
||||||
OneTimeOrderPosition(
|
OneTimeOrderPosition(role: '', count: 1, startTime: '', endTime: ''),
|
||||||
role: '',
|
|
||||||
count: 1,
|
|
||||||
startTime: '',
|
|
||||||
endTime: '',
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
|
vendors: const <Vendor>[],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
final DateTime date;
|
final DateTime date;
|
||||||
@@ -31,6 +29,8 @@ class OneTimeOrderState extends Equatable {
|
|||||||
final List<OneTimeOrderPosition> positions;
|
final List<OneTimeOrderPosition> positions;
|
||||||
final OneTimeOrderStatus status;
|
final OneTimeOrderStatus status;
|
||||||
final String? errorMessage;
|
final String? errorMessage;
|
||||||
|
final List<Vendor> vendors;
|
||||||
|
final Vendor? selectedVendor;
|
||||||
|
|
||||||
OneTimeOrderState copyWith({
|
OneTimeOrderState copyWith({
|
||||||
DateTime? date,
|
DateTime? date,
|
||||||
@@ -38,6 +38,8 @@ class OneTimeOrderState extends Equatable {
|
|||||||
List<OneTimeOrderPosition>? positions,
|
List<OneTimeOrderPosition>? positions,
|
||||||
OneTimeOrderStatus? status,
|
OneTimeOrderStatus? status,
|
||||||
String? errorMessage,
|
String? errorMessage,
|
||||||
|
List<Vendor>? vendors,
|
||||||
|
Vendor? selectedVendor,
|
||||||
}) {
|
}) {
|
||||||
return OneTimeOrderState(
|
return OneTimeOrderState(
|
||||||
date: date ?? this.date,
|
date: date ?? this.date,
|
||||||
@@ -45,15 +47,19 @@ class OneTimeOrderState extends Equatable {
|
|||||||
positions: positions ?? this.positions,
|
positions: positions ?? this.positions,
|
||||||
status: status ?? this.status,
|
status: status ?? this.status,
|
||||||
errorMessage: errorMessage ?? this.errorMessage,
|
errorMessage: errorMessage ?? this.errorMessage,
|
||||||
|
vendors: vendors ?? this.vendors,
|
||||||
|
selectedVendor: selectedVendor ?? this.selectedVendor,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => <Object?>[
|
List<Object?> get props => <Object?>[
|
||||||
date,
|
date,
|
||||||
location,
|
location,
|
||||||
positions,
|
positions,
|
||||||
status,
|
status,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
];
|
vendors,
|
||||||
|
selectedVendor,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,15 +7,15 @@ import 'rapid_order_state.dart';
|
|||||||
/// BLoC for managing the rapid (urgent) order creation flow.
|
/// BLoC for managing the rapid (urgent) order creation flow.
|
||||||
class RapidOrderBloc extends Bloc<RapidOrderEvent, RapidOrderState> {
|
class RapidOrderBloc extends Bloc<RapidOrderEvent, RapidOrderState> {
|
||||||
RapidOrderBloc(this._createRapidOrderUseCase)
|
RapidOrderBloc(this._createRapidOrderUseCase)
|
||||||
: super(
|
: super(
|
||||||
const RapidOrderInitial(
|
const RapidOrderInitial(
|
||||||
examples: <String>[
|
examples: <String>[
|
||||||
'"We had a call out. Need 2 cooks ASAP"',
|
'"We had a call out. Need 2 cooks ASAP"',
|
||||||
'"Need 5 bartenders ASAP until 5am"',
|
'"Need 5 bartenders ASAP until 5am"',
|
||||||
'"Emergency! Need 3 servers right now till midnight"',
|
'"Emergency! Need 3 servers right now till midnight"',
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
on<RapidOrderMessageChanged>(_onMessageChanged);
|
on<RapidOrderMessageChanged>(_onMessageChanged);
|
||||||
on<RapidOrderVoiceToggled>(_onVoiceToggled);
|
on<RapidOrderVoiceToggled>(_onVoiceToggled);
|
||||||
on<RapidOrderSubmitted>(_onSubmitted);
|
on<RapidOrderSubmitted>(_onSubmitted);
|
||||||
@@ -68,7 +68,8 @@ class RapidOrderBloc extends Bloc<RapidOrderEvent, RapidOrderState> {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await _createRapidOrderUseCase(
|
await _createRapidOrderUseCase(
|
||||||
RapidOrderArguments(description: message));
|
RapidOrderArguments(description: message),
|
||||||
|
);
|
||||||
emit(const RapidOrderSuccess());
|
emit(const RapidOrderSuccess());
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
emit(RapidOrderFailure(e.toString()));
|
emit(RapidOrderFailure(e.toString()));
|
||||||
|
|||||||
@@ -17,8 +17,9 @@ class ClientCreateOrderPage extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocProvider<ClientCreateOrderBloc>(
|
return BlocProvider<ClientCreateOrderBloc>(
|
||||||
create: (BuildContext context) => Modular.get<ClientCreateOrderBloc>()
|
create: (BuildContext context) =>
|
||||||
..add(const ClientCreateOrderTypesRequested()),
|
Modular.get<ClientCreateOrderBloc>()
|
||||||
|
..add(const ClientCreateOrderTypesRequested()),
|
||||||
child: const CreateOrderView(),
|
child: const CreateOrderView(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,59 +65,58 @@ class CreateOrderView extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child:
|
child: BlocBuilder<ClientCreateOrderBloc, ClientCreateOrderState>(
|
||||||
BlocBuilder<ClientCreateOrderBloc, ClientCreateOrderState>(
|
|
||||||
builder:
|
builder:
|
||||||
(BuildContext context, ClientCreateOrderState state) {
|
(BuildContext context, ClientCreateOrderState state) {
|
||||||
if (state is ClientCreateOrderLoadSuccess) {
|
if (state is ClientCreateOrderLoadSuccess) {
|
||||||
return GridView.builder(
|
return GridView.builder(
|
||||||
gridDelegate:
|
gridDelegate:
|
||||||
const SliverGridDelegateWithFixedCrossAxisCount(
|
const SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
crossAxisCount: 2,
|
crossAxisCount: 2,
|
||||||
mainAxisSpacing: UiConstants.space4,
|
mainAxisSpacing: UiConstants.space4,
|
||||||
crossAxisSpacing: UiConstants.space4,
|
crossAxisSpacing: UiConstants.space4,
|
||||||
childAspectRatio: 1,
|
childAspectRatio: 1,
|
||||||
),
|
),
|
||||||
itemCount: state.orderTypes.length,
|
itemCount: state.orderTypes.length,
|
||||||
itemBuilder: (BuildContext context, int index) {
|
itemBuilder: (BuildContext context, int index) {
|
||||||
final OrderType type = state.orderTypes[index];
|
final OrderType type = state.orderTypes[index];
|
||||||
final OrderTypeUiMetadata ui =
|
final OrderTypeUiMetadata ui =
|
||||||
OrderTypeUiMetadata.fromId(id: type.id);
|
OrderTypeUiMetadata.fromId(id: type.id);
|
||||||
|
|
||||||
return OrderTypeCard(
|
return OrderTypeCard(
|
||||||
icon: ui.icon,
|
icon: ui.icon,
|
||||||
title: _getTranslation(key: type.titleKey),
|
title: _getTranslation(key: type.titleKey),
|
||||||
description: _getTranslation(
|
description: _getTranslation(
|
||||||
key: type.descriptionKey,
|
key: type.descriptionKey,
|
||||||
),
|
),
|
||||||
backgroundColor: ui.backgroundColor,
|
backgroundColor: ui.backgroundColor,
|
||||||
borderColor: ui.borderColor,
|
borderColor: ui.borderColor,
|
||||||
iconBackgroundColor: ui.iconBackgroundColor,
|
iconBackgroundColor: ui.iconBackgroundColor,
|
||||||
iconColor: ui.iconColor,
|
iconColor: ui.iconColor,
|
||||||
textColor: ui.textColor,
|
textColor: ui.textColor,
|
||||||
descriptionColor: ui.descriptionColor,
|
descriptionColor: ui.descriptionColor,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
switch (type.id) {
|
switch (type.id) {
|
||||||
case 'rapid':
|
case 'rapid':
|
||||||
Modular.to.pushRapidOrder();
|
Modular.to.pushRapidOrder();
|
||||||
break;
|
break;
|
||||||
case 'one-time':
|
case 'one-time':
|
||||||
Modular.to.pushOneTimeOrder();
|
Modular.to.pushOneTimeOrder();
|
||||||
break;
|
break;
|
||||||
case 'recurring':
|
case 'recurring':
|
||||||
Modular.to.pushRecurringOrder();
|
Modular.to.pushRecurringOrder();
|
||||||
break;
|
break;
|
||||||
case 'permanent':
|
case 'permanent':
|
||||||
Modular.to.pushPermanentOrder();
|
Modular.to.pushPermanentOrder();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
);
|
return const Center(child: CircularProgressIndicator());
|
||||||
}
|
},
|
||||||
return const Center(child: CircularProgressIndicator());
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -54,9 +54,7 @@ class OneTimeOrderHeader extends StatelessWidget {
|
|||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Text(
|
Text(
|
||||||
title,
|
title,
|
||||||
style: UiTypography.headline3m.copyWith(
|
style: UiTypography.headline3m.copyWith(color: UiColors.white),
|
||||||
color: UiColors.white,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
subtitle,
|
subtitle,
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ class OneTimeOrderPositionCard extends StatelessWidget {
|
|||||||
required this.startLabel,
|
required this.startLabel,
|
||||||
required this.endLabel,
|
required this.endLabel,
|
||||||
required this.lunchLabel,
|
required this.lunchLabel,
|
||||||
|
this.vendor,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -55,6 +56,9 @@ class OneTimeOrderPositionCard extends StatelessWidget {
|
|||||||
/// Label for the lunch break.
|
/// Label for the lunch break.
|
||||||
final String lunchLabel;
|
final String lunchLabel;
|
||||||
|
|
||||||
|
/// The current selected vendor to determine rates.
|
||||||
|
final Vendor? vendor;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Container(
|
return Container(
|
||||||
@@ -99,8 +103,10 @@ class OneTimeOrderPositionCard extends StatelessWidget {
|
|||||||
child: DropdownButtonHideUnderline(
|
child: DropdownButtonHideUnderline(
|
||||||
child: DropdownButton<String>(
|
child: DropdownButton<String>(
|
||||||
isExpanded: true,
|
isExpanded: true,
|
||||||
hint:
|
hint: Text(
|
||||||
Text(roleLabel, style: UiTypography.body2r.textPlaceholder),
|
roleLabel,
|
||||||
|
style: UiTypography.body2r.textPlaceholder,
|
||||||
|
),
|
||||||
value: position.role.isEmpty ? null : position.role,
|
value: position.role.isEmpty ? null : position.role,
|
||||||
icon: const Icon(
|
icon: const Icon(
|
||||||
UiIcons.chevronDown,
|
UiIcons.chevronDown,
|
||||||
@@ -112,26 +118,26 @@ class OneTimeOrderPositionCard extends StatelessWidget {
|
|||||||
onUpdated(position.copyWith(role: val));
|
onUpdated(position.copyWith(role: val));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
items: <String>[
|
items:
|
||||||
'Server',
|
<String>{
|
||||||
'Bartender',
|
...(vendor?.rates.keys ?? <String>[]),
|
||||||
'Cook',
|
if (position.role.isNotEmpty &&
|
||||||
'Busser',
|
!(vendor?.rates.keys.contains(position.role) ??
|
||||||
'Host',
|
false))
|
||||||
'Barista',
|
position.role,
|
||||||
'Dishwasher',
|
}.map((String role) {
|
||||||
'Event Staff'
|
final double? rate = vendor?.rates[role];
|
||||||
].map((String role) {
|
final String label = rate == null
|
||||||
// Mock rates for UI matching
|
? role
|
||||||
final int rate = _getMockRate(role);
|
: '$role - \$${rate.toStringAsFixed(0)}/hr';
|
||||||
return DropdownMenuItem<String>(
|
return DropdownMenuItem<String>(
|
||||||
value: role,
|
value: role,
|
||||||
child: Text(
|
child: Text(
|
||||||
'$role - \$$rate/hr',
|
label,
|
||||||
style: UiTypography.body2r.textPrimary,
|
style: UiTypography.body2r.textPrimary,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}).toList(),
|
}).toList(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -153,7 +159,8 @@ class OneTimeOrderPositionCard extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
if (picked != null && context.mounted) {
|
if (picked != null && context.mounted) {
|
||||||
onUpdated(
|
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) {
|
if (picked != null && context.mounted) {
|
||||||
onUpdated(
|
onUpdated(
|
||||||
position.copyWith(endTime: picked.format(context)));
|
position.copyWith(endTime: picked.format(context)),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -198,10 +206,13 @@ class OneTimeOrderPositionCard extends StatelessWidget {
|
|||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () => onUpdated(position.copyWith(
|
onTap: () {
|
||||||
count: (position.count > 1)
|
if (position.count > 1) {
|
||||||
? position.count - 1
|
onUpdated(
|
||||||
: 1)),
|
position.copyWith(count: position.count - 1),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
child: const Icon(UiIcons.minus, size: 12),
|
child: const Icon(UiIcons.minus, size: 12),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
@@ -209,8 +220,11 @@ class OneTimeOrderPositionCard extends StatelessWidget {
|
|||||||
style: UiTypography.body2b.textPrimary,
|
style: UiTypography.body2b.textPrimary,
|
||||||
),
|
),
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () => onUpdated(
|
onTap: () {
|
||||||
position.copyWith(count: position.count + 1)),
|
onUpdated(
|
||||||
|
position.copyWith(count: position.count + 1),
|
||||||
|
);
|
||||||
|
},
|
||||||
child: const Icon(UiIcons.add, size: 12),
|
child: const Icon(UiIcons.add, size: 12),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -223,74 +237,12 @@ class OneTimeOrderPositionCard extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: UiConstants.space4),
|
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
|
// Lunch Break
|
||||||
Text(
|
Text(lunchLabel, style: UiTypography.footnote2r.textSecondary),
|
||||||
lunchLabel,
|
|
||||||
style: UiTypography.footnote2r.textSecondary,
|
|
||||||
),
|
|
||||||
const SizedBox(height: UiConstants.space1),
|
const SizedBox(height: UiConstants.space1),
|
||||||
Container(
|
Container(
|
||||||
height: 44,
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space3),
|
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space3),
|
||||||
|
height: 44,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: UiConstants.radiusMd,
|
borderRadius: UiConstants.radiusMd,
|
||||||
border: Border.all(color: UiColors.border),
|
border: Border.all(color: UiColors.border),
|
||||||
@@ -309,43 +261,15 @@ class OneTimeOrderPositionCard extends StatelessWidget {
|
|||||||
onUpdated(position.copyWith(lunchBreak: val));
|
onUpdated(position.copyWith(lunchBreak: val));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
items: <DropdownMenuItem<int>>[
|
items: <int>[0, 15, 30, 45, 60].map((int mins) {
|
||||||
DropdownMenuItem<int>(
|
return DropdownMenuItem<int>(
|
||||||
value: 0,
|
value: mins,
|
||||||
child: Text(t.client_create_order.one_time.no_break,
|
|
||||||
style: UiTypography.body2r.textPrimary),
|
|
||||||
),
|
|
||||||
DropdownMenuItem<int>(
|
|
||||||
value: 10,
|
|
||||||
child: Text(
|
child: Text(
|
||||||
'10 ${t.client_create_order.one_time.paid_break}',
|
mins == 0 ? 'No Break' : '$mins mins',
|
||||||
style: UiTypography.body2r.textPrimary),
|
style: UiTypography.body2r.textPrimary,
|
||||||
),
|
),
|
||||||
DropdownMenuItem<int>(
|
);
|
||||||
value: 15,
|
}).toList(),
|
||||||
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),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -360,83 +284,37 @@ class OneTimeOrderPositionCard extends StatelessWidget {
|
|||||||
required String value,
|
required String value,
|
||||||
required VoidCallback onTap,
|
required VoidCallback onTap,
|
||||||
}) {
|
}) {
|
||||||
return UiTextField(
|
return Column(
|
||||||
label: label,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
controller: TextEditingController(text: value),
|
children: <Widget>[
|
||||||
readOnly: true,
|
Text(label, style: UiTypography.footnote2r.textSecondary),
|
||||||
onTap: onTap,
|
const SizedBox(height: UiConstants.space1),
|
||||||
hintText: '--:--',
|
GestureDetector(
|
||||||
);
|
onTap: onTap,
|
||||||
}
|
child: Container(
|
||||||
|
height: 40,
|
||||||
int _getMockRate(String role) {
|
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space3),
|
||||||
switch (role) {
|
decoration: BoxDecoration(
|
||||||
case 'Server':
|
borderRadius: UiConstants.radiusSm,
|
||||||
return 18;
|
border: Border.all(color: UiColors.border),
|
||||||
case 'Bartender':
|
),
|
||||||
return 22;
|
child: Row(
|
||||||
case 'Cook':
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
return 20;
|
children: <Widget>[
|
||||||
case 'Busser':
|
Text(
|
||||||
return 16;
|
value.isEmpty ? '--:--' : value,
|
||||||
case 'Host':
|
style: UiTypography.body2r.textPrimary,
|
||||||
return 17;
|
),
|
||||||
case 'Barista':
|
const Icon(
|
||||||
return 16;
|
UiIcons.clock,
|
||||||
case 'Dishwasher':
|
size: 14,
|
||||||
return 15;
|
color: UiColors.iconSecondary,
|
||||||
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,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,14 +27,28 @@ class OneTimeOrderSectionHeader extends StatelessWidget {
|
|||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Text(title, style: UiTypography.headline4m.textPrimary),
|
Text(title, style: UiTypography.headline4m.textPrimary),
|
||||||
if (actionLabel != null && onAction != null)
|
if (actionLabel != null && onAction != null)
|
||||||
UiButton.text(
|
TextButton(
|
||||||
onPressed: onAction,
|
onPressed: onAction,
|
||||||
leadingIcon: UiIcons.add,
|
|
||||||
text: actionLabel!,
|
|
||||||
iconSize: 16,
|
|
||||||
style: TextButton.styleFrom(
|
style: TextButton.styleFrom(
|
||||||
minimumSize: const Size(0, 24),
|
padding: EdgeInsets.zero,
|
||||||
maximumSize: const Size(0, 24),
|
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(
|
return Scaffold(
|
||||||
backgroundColor: UiColors.bgPrimary,
|
backgroundColor: UiColors.bgPrimary,
|
||||||
body: Column(
|
body: Column(
|
||||||
@@ -58,8 +99,9 @@ class OneTimeOrderView extends StatelessWidget {
|
|||||||
? labels.creating
|
? labels.creating
|
||||||
: labels.create_order,
|
: labels.create_order,
|
||||||
isLoading: state.status == OneTimeOrderStatus.loading,
|
isLoading: state.status == OneTimeOrderStatus.loading,
|
||||||
onPressed: () => BlocProvider.of<OneTimeOrderBloc>(context)
|
onPressed: () => BlocProvider.of<OneTimeOrderBloc>(
|
||||||
.add(const OneTimeOrderSubmitted()),
|
context,
|
||||||
|
).add(const OneTimeOrderSubmitted()),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -87,37 +129,78 @@ class _OneTimeOrderForm extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: UiConstants.space4),
|
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(
|
OneTimeOrderDatePicker(
|
||||||
label: labels.date_label,
|
label: labels.date_label,
|
||||||
value: state.date,
|
value: state.date,
|
||||||
onChanged: (DateTime date) =>
|
onChanged: (DateTime date) => BlocProvider.of<OneTimeOrderBloc>(
|
||||||
BlocProvider.of<OneTimeOrderBloc>(context)
|
context,
|
||||||
.add(OneTimeOrderDateChanged(date)),
|
).add(OneTimeOrderDateChanged(date)),
|
||||||
),
|
),
|
||||||
const SizedBox(height: UiConstants.space4),
|
const SizedBox(height: UiConstants.space4),
|
||||||
|
|
||||||
OneTimeOrderLocationInput(
|
OneTimeOrderLocationInput(
|
||||||
label: labels.location_label,
|
label: labels.location_label,
|
||||||
value: state.location,
|
value: state.location,
|
||||||
onChanged: (String location) =>
|
onChanged: (String location) => BlocProvider.of<OneTimeOrderBloc>(
|
||||||
BlocProvider.of<OneTimeOrderBloc>(context)
|
context,
|
||||||
.add(OneTimeOrderLocationChanged(location)),
|
).add(OneTimeOrderLocationChanged(location)),
|
||||||
),
|
),
|
||||||
const SizedBox(height: UiConstants.space6),
|
const SizedBox(height: UiConstants.space6),
|
||||||
|
|
||||||
OneTimeOrderSectionHeader(
|
OneTimeOrderSectionHeader(
|
||||||
title: labels.positions_title,
|
title: labels.positions_title,
|
||||||
actionLabel: labels.add_position,
|
actionLabel: labels.add_position,
|
||||||
onAction: () => BlocProvider.of<OneTimeOrderBloc>(context)
|
onAction: () => BlocProvider.of<OneTimeOrderBloc>(
|
||||||
.add(const OneTimeOrderPositionAdded()),
|
context,
|
||||||
|
).add(const OneTimeOrderPositionAdded()),
|
||||||
),
|
),
|
||||||
const SizedBox(height: UiConstants.space3),
|
const SizedBox(height: UiConstants.space3),
|
||||||
|
|
||||||
// Positions List
|
// Positions List
|
||||||
...state.positions
|
...state.positions.asMap().entries.map((
|
||||||
.asMap()
|
MapEntry<int, OneTimeOrderPosition> entry,
|
||||||
.entries
|
) {
|
||||||
.map((MapEntry<int, OneTimeOrderPosition> entry) {
|
|
||||||
final int index = entry.key;
|
final int index = entry.key;
|
||||||
final OneTimeOrderPosition position = entry.value;
|
final OneTimeOrderPosition position = entry.value;
|
||||||
return Padding(
|
return Padding(
|
||||||
@@ -132,14 +215,16 @@ class _OneTimeOrderForm extends StatelessWidget {
|
|||||||
startLabel: labels.start_label,
|
startLabel: labels.start_label,
|
||||||
endLabel: labels.end_label,
|
endLabel: labels.end_label,
|
||||||
lunchLabel: labels.lunch_break_label,
|
lunchLabel: labels.lunch_break_label,
|
||||||
|
vendor: state.selectedVendor,
|
||||||
onUpdated: (OneTimeOrderPosition updated) {
|
onUpdated: (OneTimeOrderPosition updated) {
|
||||||
BlocProvider.of<OneTimeOrderBloc>(context).add(
|
BlocProvider.of<OneTimeOrderBloc>(
|
||||||
OneTimeOrderPositionUpdated(index, updated),
|
context,
|
||||||
);
|
).add(OneTimeOrderPositionUpdated(index, updated));
|
||||||
},
|
},
|
||||||
onRemoved: () {
|
onRemoved: () {
|
||||||
BlocProvider.of<OneTimeOrderBloc>(context)
|
BlocProvider.of<OneTimeOrderBloc>(
|
||||||
.add(OneTimeOrderPositionRemoved(index));
|
context,
|
||||||
|
).add(OneTimeOrderPositionRemoved(index));
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -73,16 +73,14 @@ class OrderTypeCard extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
child: Icon(icon, color: iconColor, size: 24),
|
child: Icon(icon, color: iconColor, size: 24),
|
||||||
),
|
),
|
||||||
Text(
|
Text(title, style: UiTypography.body2b.copyWith(color: textColor)),
|
||||||
title,
|
|
||||||
style: UiTypography.body2b.copyWith(color: textColor),
|
|
||||||
),
|
|
||||||
const SizedBox(height: UiConstants.space1),
|
const SizedBox(height: UiConstants.space1),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
description,
|
description,
|
||||||
style:
|
style: UiTypography.footnote1r.copyWith(
|
||||||
UiTypography.footnote1r.copyWith(color: descriptionColor),
|
color: descriptionColor,
|
||||||
|
),
|
||||||
maxLines: 2,
|
maxLines: 2,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -47,10 +47,7 @@ class RapidOrderExampleCard extends StatelessWidget {
|
|||||||
text: TextSpan(
|
text: TextSpan(
|
||||||
style: UiTypography.body2r.textPrimary,
|
style: UiTypography.body2r.textPrimary,
|
||||||
children: <InlineSpan>[
|
children: <InlineSpan>[
|
||||||
TextSpan(
|
TextSpan(text: label, style: UiTypography.body2b.textPrimary),
|
||||||
text: label,
|
|
||||||
style: UiTypography.body2b.textPrimary,
|
|
||||||
),
|
|
||||||
TextSpan(text: ' $example'),
|
TextSpan(text: ' $example'),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -74,11 +74,7 @@ class RapidOrderHeader extends StatelessWidget {
|
|||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Row(
|
Row(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
const Icon(
|
const Icon(UiIcons.zap, color: UiColors.accent, size: 18),
|
||||||
UiIcons.zap,
|
|
||||||
color: UiColors.accent,
|
|
||||||
size: 18,
|
|
||||||
),
|
|
||||||
const SizedBox(width: UiConstants.space2),
|
const SizedBox(width: UiConstants.space2),
|
||||||
Text(
|
Text(
|
||||||
title,
|
title,
|
||||||
|
|||||||
@@ -42,8 +42,9 @@ class RapidOrderSuccessView extends StatelessWidget {
|
|||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Container(
|
child: Container(
|
||||||
margin:
|
margin: const EdgeInsets.symmetric(
|
||||||
const EdgeInsets.symmetric(horizontal: UiConstants.space10),
|
horizontal: UiConstants.space10,
|
||||||
|
),
|
||||||
padding: const EdgeInsets.all(UiConstants.space8),
|
padding: const EdgeInsets.all(UiConstants.space8),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: UiColors.white,
|
color: UiColors.white,
|
||||||
@@ -75,10 +76,7 @@ class RapidOrderSuccessView extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: UiConstants.space6),
|
const SizedBox(height: UiConstants.space6),
|
||||||
Text(
|
Text(title, style: UiTypography.headline1m.textPrimary),
|
||||||
title,
|
|
||||||
style: UiTypography.headline1m.textPrimary,
|
|
||||||
),
|
|
||||||
const SizedBox(height: UiConstants.space3),
|
const SizedBox(height: UiConstants.space3),
|
||||||
Text(
|
Text(
|
||||||
message,
|
message,
|
||||||
|
|||||||
@@ -153,27 +153,27 @@ class _RapidOrderFormState extends State<_RapidOrderForm> {
|
|||||||
|
|
||||||
// Examples
|
// Examples
|
||||||
if (initialState != null)
|
if (initialState != null)
|
||||||
...initialState.examples
|
...initialState.examples.asMap().entries.map((
|
||||||
.asMap()
|
MapEntry<int, String> entry,
|
||||||
.entries
|
) {
|
||||||
.map((MapEntry<int, String> entry) {
|
|
||||||
final int index = entry.key;
|
final int index = entry.key;
|
||||||
final String example = entry.value;
|
final String example = entry.value;
|
||||||
final bool isHighlighted = index == 0;
|
final bool isHighlighted = index == 0;
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(
|
padding: const EdgeInsets.only(
|
||||||
bottom: UiConstants.space2),
|
bottom: UiConstants.space2,
|
||||||
|
),
|
||||||
child: RapidOrderExampleCard(
|
child: RapidOrderExampleCard(
|
||||||
example: example,
|
example: example,
|
||||||
isHighlighted: isHighlighted,
|
isHighlighted: isHighlighted,
|
||||||
label: labels.example,
|
label: labels.example,
|
||||||
onTap: () =>
|
onTap: () =>
|
||||||
BlocProvider.of<RapidOrderBloc>(
|
BlocProvider.of<RapidOrderBloc>(
|
||||||
context)
|
context,
|
||||||
.add(
|
).add(
|
||||||
RapidOrderExampleSelected(example),
|
RapidOrderExampleSelected(example),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
@@ -184,9 +184,9 @@ class _RapidOrderFormState extends State<_RapidOrderForm> {
|
|||||||
controller: _messageController,
|
controller: _messageController,
|
||||||
maxLines: 4,
|
maxLines: 4,
|
||||||
onChanged: (String value) {
|
onChanged: (String value) {
|
||||||
BlocProvider.of<RapidOrderBloc>(context).add(
|
BlocProvider.of<RapidOrderBloc>(
|
||||||
RapidOrderMessageChanged(value),
|
context,
|
||||||
);
|
).add(RapidOrderMessageChanged(value));
|
||||||
},
|
},
|
||||||
hintText: labels.hint,
|
hintText: labels.hint,
|
||||||
),
|
),
|
||||||
@@ -197,7 +197,8 @@ class _RapidOrderFormState extends State<_RapidOrderForm> {
|
|||||||
labels: labels,
|
labels: labels,
|
||||||
isSubmitting: isSubmitting,
|
isSubmitting: isSubmitting,
|
||||||
isListening: initialState?.isListening ?? false,
|
isListening: initialState?.isListening ?? false,
|
||||||
isMessageEmpty: initialState != null &&
|
isMessageEmpty:
|
||||||
|
initialState != null &&
|
||||||
initialState.message.trim().isEmpty,
|
initialState.message.trim().isEmpty,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -242,11 +243,7 @@ class _AnimatedZapIcon extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
child: const Icon(
|
child: const Icon(UiIcons.zap, color: UiColors.white, size: 32),
|
||||||
UiIcons.zap,
|
|
||||||
color: UiColors.white,
|
|
||||||
size: 32,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -271,9 +268,9 @@ class _RapidOrderActions extends StatelessWidget {
|
|||||||
child: UiButton.secondary(
|
child: UiButton.secondary(
|
||||||
text: isListening ? labels.listening : labels.speak,
|
text: isListening ? labels.listening : labels.speak,
|
||||||
leadingIcon: UiIcons.bell, // Placeholder for mic
|
leadingIcon: UiIcons.bell, // Placeholder for mic
|
||||||
onPressed: () => BlocProvider.of<RapidOrderBloc>(context).add(
|
onPressed: () => BlocProvider.of<RapidOrderBloc>(
|
||||||
const RapidOrderVoiceToggled(),
|
context,
|
||||||
),
|
).add(const RapidOrderVoiceToggled()),
|
||||||
style: OutlinedButton.styleFrom(
|
style: OutlinedButton.styleFrom(
|
||||||
backgroundColor: isListening
|
backgroundColor: isListening
|
||||||
? UiColors.destructive.withValues(alpha: 0.05)
|
? UiColors.destructive.withValues(alpha: 0.05)
|
||||||
@@ -291,9 +288,9 @@ class _RapidOrderActions extends StatelessWidget {
|
|||||||
trailingIcon: UiIcons.arrowRight,
|
trailingIcon: UiIcons.arrowRight,
|
||||||
onPressed: isSubmitting || isMessageEmpty
|
onPressed: isSubmitting || isMessageEmpty
|
||||||
? null
|
? null
|
||||||
: () => BlocProvider.of<RapidOrderBloc>(context).add(
|
: () => BlocProvider.of<RapidOrderBloc>(
|
||||||
const RapidOrderSubmitted(),
|
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"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.0"
|
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:
|
uuid:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ workspace:
|
|||||||
- packages/features/client/settings
|
- packages/features/client/settings
|
||||||
- packages/features/client/hubs
|
- packages/features/client/hubs
|
||||||
- packages/features/client/create_order
|
- packages/features/client/create_order
|
||||||
|
- packages/features/client/view_orders
|
||||||
- packages/features/client/client_main
|
- packages/features/client/client_main
|
||||||
- apps/staff
|
- apps/staff
|
||||||
- apps/client
|
- apps/client
|
||||||
|
|||||||
Reference in New Issue
Block a user