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:
@@ -1,6 +1,7 @@
|
||||
import 'package:client_home/client_home.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
import 'package:view_orders/view_orders.dart';
|
||||
|
||||
import 'presentation/blocs/client_main_cubit.dart';
|
||||
import 'presentation/pages/client_main_page.dart';
|
||||
@@ -30,11 +31,7 @@ class ClientMainModule extends Module {
|
||||
child: (BuildContext context) =>
|
||||
const PlaceholderPage(title: 'Billing'),
|
||||
),
|
||||
ChildRoute<dynamic>(
|
||||
'/orders',
|
||||
child: (BuildContext context) =>
|
||||
const PlaceholderPage(title: 'Orders'),
|
||||
),
|
||||
ModuleRoute<dynamic>('/orders', module: ViewOrdersModule()),
|
||||
ChildRoute<dynamic>(
|
||||
'/reports',
|
||||
child: (BuildContext context) =>
|
||||
|
||||
@@ -23,6 +23,8 @@ dependencies:
|
||||
path: ../../../core_localization
|
||||
client_home:
|
||||
path: ../home
|
||||
view_orders:
|
||||
path: ../view_orders
|
||||
# Intentionally commenting these out as they might not exist yet
|
||||
# client_settings:
|
||||
# path: ../settings
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user