Add client view orders feature module
Introduces the client 'View Orders' feature, including domain entity, repository, use case, Cubit, state, navigation extension, UI page, and widget. Integrates the feature into the client main module, updates localization files for English and Spanish, and adds supporting icons to the design system. Also updates the mock repository to provide sample order data.
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;
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,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,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,
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,525 @@
|
|||||||
|
import 'package:core_localization/core_localization.dart';
|
||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
|
||||||
|
/// A rich card displaying details of a client order/shift.
|
||||||
|
///
|
||||||
|
/// This widget complies with the KROW Design System by using
|
||||||
|
/// tokens from `package:design_system`.
|
||||||
|
class ViewOrderCard extends StatefulWidget {
|
||||||
|
/// Creates a [ViewOrderCard] for the given [order].
|
||||||
|
const ViewOrderCard({required this.order, super.key});
|
||||||
|
|
||||||
|
/// The order item to display.
|
||||||
|
final OrderItem order;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ViewOrderCard> createState() => _ViewOrderCardState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ViewOrderCardState extends State<ViewOrderCard> {
|
||||||
|
bool _expanded = false;
|
||||||
|
|
||||||
|
/// Returns the semantic color for the given status.
|
||||||
|
Color _getStatusColor({required String status}) {
|
||||||
|
switch (status) {
|
||||||
|
case 'open':
|
||||||
|
return UiColors.primary;
|
||||||
|
case 'filled':
|
||||||
|
case 'confirmed':
|
||||||
|
return UiColors.textSuccess;
|
||||||
|
case 'in_progress':
|
||||||
|
return UiColors.textWarning;
|
||||||
|
case 'completed':
|
||||||
|
return UiColors.primary;
|
||||||
|
case 'cancelled':
|
||||||
|
return UiColors.destructive;
|
||||||
|
default:
|
||||||
|
return UiColors.textSecondary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the localized label for the given status.
|
||||||
|
String _getStatusLabel({required String status}) {
|
||||||
|
switch (status) {
|
||||||
|
case 'open':
|
||||||
|
return t.client_view_orders.card.open;
|
||||||
|
case 'filled':
|
||||||
|
return t.client_view_orders.card.filled;
|
||||||
|
case 'confirmed':
|
||||||
|
return t.client_view_orders.card.confirmed;
|
||||||
|
case 'in_progress':
|
||||||
|
return t.client_view_orders.card.in_progress;
|
||||||
|
case 'completed':
|
||||||
|
return t.client_view_orders.card.completed;
|
||||||
|
case 'cancelled':
|
||||||
|
return t.client_view_orders.card.cancelled;
|
||||||
|
default:
|
||||||
|
return status.toUpperCase();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Formats the date string for display.
|
||||||
|
String _formatDate({required String dateStr}) {
|
||||||
|
try {
|
||||||
|
final DateTime date = DateTime.parse(dateStr);
|
||||||
|
return DateFormat('EEE, MMM d').format(date);
|
||||||
|
} catch (_) {
|
||||||
|
return dateStr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Formats the time string for display.
|
||||||
|
String _formatTime({required String timeStr}) {
|
||||||
|
return timeStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final OrderItem order = widget.order;
|
||||||
|
final Color statusColor = _getStatusColor(status: order.status);
|
||||||
|
final String statusLabel = _getStatusLabel(status: order.status);
|
||||||
|
final int coveragePercent = order.workersNeeded > 0
|
||||||
|
? ((order.filled / order.workersNeeded) * 100).round()
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
// Simulation of cost/hours calculation
|
||||||
|
const double hours = 8.0;
|
||||||
|
final double cost =
|
||||||
|
order.hourlyRate *
|
||||||
|
hours *
|
||||||
|
(order.filled > 0 ? order.filled : order.workersNeeded);
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: UiColors.white,
|
||||||
|
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||||
|
border: Border.all(
|
||||||
|
color: UiColors.primary.withValues(alpha: 0.12),
|
||||||
|
width: 1.5,
|
||||||
|
),
|
||||||
|
boxShadow: <BoxShadow>[
|
||||||
|
BoxShadow(
|
||||||
|
color: UiColors.primary.withValues(alpha: 0.08),
|
||||||
|
blurRadius: 3,
|
||||||
|
offset: const Offset(0, 1),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(UiConstants.space4),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
// Header Row
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
// Status Dot & Label
|
||||||
|
Row(
|
||||||
|
children: <Widget>[
|
||||||
|
Container(
|
||||||
|
width: 6,
|
||||||
|
height: 6,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: statusColor,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: UiConstants.space2),
|
||||||
|
Text(
|
||||||
|
statusLabel,
|
||||||
|
style: UiTypography.footnote2b.copyWith(
|
||||||
|
color: statusColor,
|
||||||
|
letterSpacing: 0.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space1),
|
||||||
|
// Title
|
||||||
|
Text(
|
||||||
|
order.title,
|
||||||
|
style: UiTypography.body1b.copyWith(
|
||||||
|
color: UiColors.textPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space1),
|
||||||
|
// Client & Date
|
||||||
|
Row(
|
||||||
|
children: <Widget>[
|
||||||
|
Text(
|
||||||
|
order.clientName,
|
||||||
|
style: UiTypography.body3r.copyWith(
|
||||||
|
color: UiColors.textSecondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: UiConstants.space1,
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'•',
|
||||||
|
style: UiTypography.body3r.copyWith(
|
||||||
|
color: UiColors.textInactive,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
_formatDate(dateStr: order.date),
|
||||||
|
style: UiTypography.body3r.copyWith(
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: UiColors.textSecondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space1),
|
||||||
|
// Address
|
||||||
|
Row(
|
||||||
|
children: <Widget>[
|
||||||
|
const Icon(
|
||||||
|
UiIcons.mapPin,
|
||||||
|
size: 12,
|
||||||
|
color: UiColors.iconSecondary,
|
||||||
|
),
|
||||||
|
const SizedBox(width: UiConstants.space1),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
order.locationAddress,
|
||||||
|
style: UiTypography.footnote2r.copyWith(
|
||||||
|
color: UiColors.textSecondary,
|
||||||
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: UiConstants.space1),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
// TODO: Get directions
|
||||||
|
},
|
||||||
|
child: Row(
|
||||||
|
children: <Widget>[
|
||||||
|
const Icon(
|
||||||
|
UiIcons.navigation,
|
||||||
|
size: 12,
|
||||||
|
color: UiColors.primary,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 2),
|
||||||
|
Text(
|
||||||
|
t.client_view_orders.card.get_direction,
|
||||||
|
style: UiTypography.footnote2m.copyWith(
|
||||||
|
color: UiColors.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Actions
|
||||||
|
Row(
|
||||||
|
children: <Widget>[
|
||||||
|
_buildHeaderIconButton(
|
||||||
|
icon: UiIcons.edit,
|
||||||
|
color: UiColors.primary,
|
||||||
|
bgColor: UiColors.tagInProgress,
|
||||||
|
onTap: () {
|
||||||
|
// TODO: Open edit sheet
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(width: UiConstants.space2),
|
||||||
|
_buildHeaderIconButton(
|
||||||
|
icon: _expanded
|
||||||
|
? UiIcons.chevronUp
|
||||||
|
: UiIcons.chevronDown,
|
||||||
|
color: UiColors.iconSecondary,
|
||||||
|
bgColor: UiColors.bgSecondary,
|
||||||
|
onTap: () => setState(() => _expanded = !_expanded),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: UiConstants.space3),
|
||||||
|
const Divider(height: 1, color: UiColors.separatorSecondary),
|
||||||
|
const SizedBox(height: UiConstants.space3),
|
||||||
|
|
||||||
|
// Stats Row
|
||||||
|
Row(
|
||||||
|
children: <Widget>[
|
||||||
|
Expanded(
|
||||||
|
child: _buildStatItem(
|
||||||
|
icon: UiIcons.dollar,
|
||||||
|
value: '\$${cost.round()}',
|
||||||
|
label: t.client_view_orders.card.total,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
width: 1,
|
||||||
|
height: 32,
|
||||||
|
color: UiColors.separatorSecondary,
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: _buildStatItem(
|
||||||
|
icon: UiIcons.clock,
|
||||||
|
value: hours.toStringAsFixed(1),
|
||||||
|
label: t.client_view_orders.card.hrs,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
width: 1,
|
||||||
|
height: 32,
|
||||||
|
color: UiColors.separatorSecondary,
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: _buildStatItem(
|
||||||
|
icon: UiIcons.users,
|
||||||
|
value:
|
||||||
|
'${order.filled > 0 ? order.filled : order.workersNeeded}',
|
||||||
|
label: t.client_view_orders.card.workers,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: UiConstants.space4),
|
||||||
|
|
||||||
|
// Clock In/Out Boxes
|
||||||
|
Row(
|
||||||
|
children: <Widget>[
|
||||||
|
Expanded(
|
||||||
|
child: _buildTimeBox(
|
||||||
|
label: t.client_view_orders.card.clock_in,
|
||||||
|
time: _formatTime(timeStr: order.startTime),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: UiConstants.space3),
|
||||||
|
Expanded(
|
||||||
|
child: _buildTimeBox(
|
||||||
|
label: t.client_view_orders.card.clock_out,
|
||||||
|
time: _formatTime(timeStr: order.endTime),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: UiConstants.space3),
|
||||||
|
|
||||||
|
// Coverage Bar
|
||||||
|
if (order.status != 'completed') ...<Widget>[
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: <Widget>[
|
||||||
|
Row(
|
||||||
|
children: <Widget>[
|
||||||
|
Text(
|
||||||
|
t.client_view_orders.card.coverage,
|
||||||
|
style: UiTypography.footnote2b.copyWith(
|
||||||
|
color: UiColors.textSecondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: UiConstants.space1),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 4,
|
||||||
|
vertical: 1,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: coveragePercent == 100
|
||||||
|
? UiColors.tagSuccess
|
||||||
|
: UiColors.tagInProgress,
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'$coveragePercent%',
|
||||||
|
style: UiTypography.footnote2b.copyWith(
|
||||||
|
fontSize: 9,
|
||||||
|
color: coveragePercent == 100
|
||||||
|
? UiColors.textSuccess
|
||||||
|
: UiColors.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
t.client_view_orders.card.workers_label(
|
||||||
|
filled: order.filled,
|
||||||
|
needed: order.workersNeeded,
|
||||||
|
),
|
||||||
|
style: UiTypography.footnote2m.copyWith(
|
||||||
|
color: UiColors.textSecondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(2),
|
||||||
|
child: LinearProgressIndicator(
|
||||||
|
value: coveragePercent / 100,
|
||||||
|
backgroundColor: UiColors.separatorSecondary,
|
||||||
|
valueColor: AlwaysStoppedAnimation<Color>(
|
||||||
|
coveragePercent == 100
|
||||||
|
? UiColors.textSuccess
|
||||||
|
: UiColors.primary,
|
||||||
|
),
|
||||||
|
minHeight: 4,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Worker Avatars and more details (Expanded section)
|
||||||
|
if (_expanded) ...<Widget>[
|
||||||
|
const Divider(height: 1, color: UiColors.separatorSecondary),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(UiConstants.space4),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
Text(
|
||||||
|
t.client_view_orders.card.confirmed_workers,
|
||||||
|
style: UiTypography.body2b.copyWith(
|
||||||
|
color: UiColors.textPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space3),
|
||||||
|
if (order.confirmedApps.isEmpty)
|
||||||
|
Text(
|
||||||
|
t.client_view_orders.card.no_workers,
|
||||||
|
style: UiTypography.body3r.copyWith(
|
||||||
|
color: UiColors.textInactive,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
Wrap(
|
||||||
|
spacing: -8,
|
||||||
|
children: order.confirmedApps
|
||||||
|
.map(
|
||||||
|
(Map<String, dynamic> app) => Tooltip(
|
||||||
|
message: app['worker_name'] as String,
|
||||||
|
child: CircleAvatar(
|
||||||
|
radius: 14,
|
||||||
|
backgroundColor: UiColors.white,
|
||||||
|
child: CircleAvatar(
|
||||||
|
radius: 12,
|
||||||
|
backgroundColor: UiColors.bgSecondary,
|
||||||
|
child: Text(
|
||||||
|
(app['worker_name'] as String).substring(
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
),
|
||||||
|
style: UiTypography.footnote2b,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds a small icon button used in row headers.
|
||||||
|
Widget _buildHeaderIconButton({
|
||||||
|
required IconData icon,
|
||||||
|
required Color color,
|
||||||
|
required Color bgColor,
|
||||||
|
required VoidCallback onTap,
|
||||||
|
}) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: onTap,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: bgColor,
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
child: Icon(icon, size: 14, color: color),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds a single stat item (e.g., Cost, Hours, Workers).
|
||||||
|
Widget _buildStatItem({
|
||||||
|
required IconData icon,
|
||||||
|
required String value,
|
||||||
|
required String label,
|
||||||
|
}) {
|
||||||
|
return Column(
|
||||||
|
children: <Widget>[
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
Icon(icon, size: 10, color: UiColors.iconSecondary),
|
||||||
|
const SizedBox(width: UiConstants.space1),
|
||||||
|
Text(
|
||||||
|
value,
|
||||||
|
style: UiTypography.body2b.copyWith(color: UiColors.textPrimary),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
label.toUpperCase(),
|
||||||
|
style: UiTypography.titleUppercase4m.copyWith(
|
||||||
|
color: UiColors.textInactive,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds a box displaying a label and a time value.
|
||||||
|
Widget _buildTimeBox({required String label, required String time}) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: UiColors.bgSecondary,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: UiTypography.titleUppercase4m.copyWith(
|
||||||
|
color: UiColors.textSecondary,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
Text(
|
||||||
|
time,
|
||||||
|
style: UiTypography.body2m.copyWith(
|
||||||
|
color: UiColors.foreground,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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