diff --git a/apps/mobile/packages/core_localization/lib/src/l10n/en.i18n.json b/apps/mobile/packages/core_localization/lib/src/l10n/en.i18n.json index 0d5db935..f97c1777 100644 --- a/apps/mobile/packages/core_localization/lib/src/l10n/en.i18n.json +++ b/apps/mobile/packages/core_localization/lib/src/l10n/en.i18n.json @@ -311,6 +311,35 @@ "orders": "Orders", "reports": "Reports" } + }, + "client_view_orders": { + "title": "Orders", + "post_button": "Post", + "post_order": "Post an Order", + "no_orders": "No orders for $date", + "tabs": { + "up_next": "Up Next", + "active": "Active", + "completed": "Completed" + }, + "card": { + "open": "OPEN", + "filled": "FILLED", + "confirmed": "CONFIRMED", + "in_progress": "IN PROGRESS", + "completed": "COMPLETED", + "cancelled": "CANCELLED", + "get_direction": "Get direction", + "total": "Total", + "hrs": "HRS", + "workers": "workers", + "clock_in": "CLOCK IN", + "clock_out": "CLOCK OUT", + "coverage": "Coverage", + "workers_label": "$filled/$needed Workers", + "confirmed_workers": "Workers Confirmed", + "no_workers": "No workers confirmed yet." + } } } diff --git a/apps/mobile/packages/core_localization/lib/src/l10n/es.i18n.json b/apps/mobile/packages/core_localization/lib/src/l10n/es.i18n.json index fcabb08d..c141a406 100644 --- a/apps/mobile/packages/core_localization/lib/src/l10n/es.i18n.json +++ b/apps/mobile/packages/core_localization/lib/src/l10n/es.i18n.json @@ -311,5 +311,34 @@ "orders": "Órdenes", "reports": "Reportes" } + }, + "client_view_orders": { + "title": "Órdenes", + "post_button": "Publicar", + "post_order": "Publicar una Orden", + "no_orders": "No hay órdenes para $date", + "tabs": { + "up_next": "Próximos", + "active": "Activos", + "completed": "Completados" + }, + "card": { + "open": "ABIERTO", + "filled": "LLENO", + "confirmed": "CONFIRMADO", + "in_progress": "EN PROGRESO", + "completed": "COMPLETADO", + "cancelled": "CANCELADO", + "get_direction": "Obtener dirección", + "total": "Total", + "hrs": "HRS", + "workers": "trabajadores", + "clock_in": "ENTRADA", + "clock_out": "SALIDA", + "coverage": "Cobertura", + "workers_label": "$filled/$needed Trabajadores", + "confirmed_workers": "Trabajadores Confirmados", + "no_workers": "Ningún trabajador confirmado aún." + } } } diff --git a/apps/mobile/packages/data_connect/lib/src/mocks/order_repository_mock.dart b/apps/mobile/packages/data_connect/lib/src/mocks/order_repository_mock.dart index 8e7979ea..95c6f025 100644 --- a/apps/mobile/packages/data_connect/lib/src/mocks/order_repository_mock.dart +++ b/apps/mobile/packages/data_connect/lib/src/mocks/order_repository_mock.dart @@ -7,7 +7,7 @@ import 'package:krow_domain/krow_domain.dart'; class OrderRepositoryMock { /// Returns a list of available [OrderType]s. Future> getOrderTypes() async { - await Future.delayed(const Duration(milliseconds: 500)); + await Future.delayed(const Duration(milliseconds: 500)); return const [ OrderType( id: 'rapid', @@ -34,11 +34,122 @@ class OrderRepositoryMock { /// Simulates creating a one-time order. Future createOneTimeOrder(OneTimeOrder order) async { - await Future.delayed(const Duration(milliseconds: 800)); + await Future.delayed(const Duration(milliseconds: 800)); } /// Simulates creating a rapid order. Future createRapidOrder(String description) async { - await Future.delayed(const Duration(seconds: 1)); + await Future.delayed(const Duration(seconds: 1)); + } + + /// Returns a mock list of client orders. + Future> getOrders() async { + await Future.delayed(const Duration(milliseconds: 500)); + return [ + 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>.generate( + 10, + (int index) => { + '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>.generate( + 4, + (int index) => { + '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>.generate( + 15, + (int index) => { + '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>.generate( + 2, + (int index) => { + 'id': 'app_d_$index', + 'worker_id': 'w_d_$index', + 'worker_name': 'Checker ${index + 1}', + 'status': 'confirmed', + 'check_in_time': '16:50', + }, + ), + ), + ]; } } diff --git a/apps/mobile/packages/design_system/lib/src/ui_icons.dart b/apps/mobile/packages/design_system/lib/src/ui_icons.dart index c24c5140..2b3d3669 100644 --- a/apps/mobile/packages/design_system/lib/src/ui_icons.dart +++ b/apps/mobile/packages/design_system/lib/src/ui_icons.dart @@ -34,6 +34,9 @@ class UiIcons { /// User icon for profile static const IconData user = _IconLib.user; + /// Users icon for groups or staff + static const IconData users = _IconLib.users; + /// Settings icon static const IconData settings = _IconLib.settings; @@ -81,6 +84,9 @@ class UiIcons { /// Chevron down icon static const IconData chevronDown = _IconLib.chevronDown; + /// Chevron up icon + static const IconData chevronUp = _IconLib.chevronUp; + // --- Status & Feedback --- /// Info icon @@ -139,6 +145,9 @@ class UiIcons { /// Sparkles icon for features or AI static const IconData sparkles = _IconLib.sparkles; + /// Navigation/Compass icon + static const IconData navigation = _IconLib.navigation; + /// Star icon for ratings static const IconData star = _IconLib.star; diff --git a/apps/mobile/packages/domain/lib/krow_domain.dart b/apps/mobile/packages/domain/lib/krow_domain.dart index 07c99633..f4d6110b 100644 --- a/apps/mobile/packages/domain/lib/krow_domain.dart +++ b/apps/mobile/packages/domain/lib/krow_domain.dart @@ -31,6 +31,7 @@ export 'src/entities/events/work_session.dart'; export 'src/entities/orders/order_type.dart'; export 'src/entities/orders/one_time_order.dart'; export 'src/entities/orders/one_time_order_position.dart'; +export 'src/entities/orders/order_item.dart'; // Skills & Certs export 'src/entities/skills/skill.dart'; diff --git a/apps/mobile/packages/domain/lib/src/entities/orders/order_item.dart b/apps/mobile/packages/domain/lib/src/entities/orders/order_item.dart new file mode 100644 index 00000000..6950c7b6 --- /dev/null +++ b/apps/mobile/packages/domain/lib/src/entities/orders/order_item.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 >[], + }); + + /// 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> confirmedApps; + + @override + List get props => [ + id, + title, + clientName, + status, + date, + startTime, + endTime, + location, + locationAddress, + filled, + workersNeeded, + hourlyRate, + confirmedApps, + ]; +} diff --git a/apps/mobile/packages/features/client/client_main/lib/src/client_main_module.dart b/apps/mobile/packages/features/client/client_main/lib/src/client_main_module.dart index 60337e31..2569e7fd 100644 --- a/apps/mobile/packages/features/client/client_main/lib/src/client_main_module.dart +++ b/apps/mobile/packages/features/client/client_main/lib/src/client_main_module.dart @@ -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( - '/orders', - child: (BuildContext context) => - const PlaceholderPage(title: 'Orders'), - ), + ModuleRoute('/orders', module: ViewOrdersModule()), ChildRoute( '/reports', child: (BuildContext context) => diff --git a/apps/mobile/packages/features/client/client_main/pubspec.yaml b/apps/mobile/packages/features/client/client_main/pubspec.yaml index 48a037b6..c2ac9a6f 100644 --- a/apps/mobile/packages/features/client/client_main/pubspec.yaml +++ b/apps/mobile/packages/features/client/client_main/pubspec.yaml @@ -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 diff --git a/apps/mobile/packages/features/client/view_orders/lib/src/data/repositories/view_orders_repository_impl.dart b/apps/mobile/packages/features/client/view_orders/lib/src/data/repositories/view_orders_repository_impl.dart new file mode 100644 index 00000000..cdc54d00 --- /dev/null +++ b/apps/mobile/packages/features/client/view_orders/lib/src/data/repositories/view_orders_repository_impl.dart @@ -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> getOrders() { + return _orderRepositoryMock.getOrders(); + } +} diff --git a/apps/mobile/packages/features/client/view_orders/lib/src/domain/repositories/i_view_orders_repository.dart b/apps/mobile/packages/features/client/view_orders/lib/src/domain/repositories/i_view_orders_repository.dart new file mode 100644 index 00000000..d6b129ed --- /dev/null +++ b/apps/mobile/packages/features/client/view_orders/lib/src/domain/repositories/i_view_orders_repository.dart @@ -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> getOrders(); +} diff --git a/apps/mobile/packages/features/client/view_orders/lib/src/domain/usecases/get_orders_use_case.dart b/apps/mobile/packages/features/client/view_orders/lib/src/domain/usecases/get_orders_use_case.dart new file mode 100644 index 00000000..3f0018e2 --- /dev/null +++ b/apps/mobile/packages/features/client/view_orders/lib/src/domain/usecases/get_orders_use_case.dart @@ -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> { + final IViewOrdersRepository _repository; + + /// Creates a [GetOrdersUseCase] with the required [IViewOrdersRepository]. + GetOrdersUseCase(this._repository); + + @override + Future> call() { + return _repository.getOrders(); + } +} diff --git a/apps/mobile/packages/features/client/view_orders/lib/src/presentation/blocs/view_orders_cubit.dart b/apps/mobile/packages/features/client/view_orders/lib/src/presentation/blocs/view_orders_cubit.dart new file mode 100644 index 00000000..72ecfb6c --- /dev/null +++ b/apps/mobile/packages/features/client/view_orders/lib/src/presentation/blocs/view_orders_cubit.dart @@ -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 { + 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 loadOrders() async { + emit(state.copyWith(status: ViewOrdersStatus.loading)); + try { + final List 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 calendarDays = _calculateCalendarDays(newWeekOffset); + emit(state.copyWith(weekOffset: newWeekOffset, calendarDays: calendarDays)); + _updateDerivedState(); + } + + void _updateDerivedState() { + final List 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 _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.generate( + 7, + (int index) => startDate.add(Duration(days: index)), + ); + } + + List _calculateFilteredOrders(ViewOrdersState state) { + if (state.selectedDate == null) return []; + + final String selectedDateStr = DateFormat( + 'yyyy-MM-dd', + ).format(state.selectedDate!); + + // Filter by date + final List 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) => + ['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 []; + } + + int _calculateCategoryCount(String category) { + if (state.selectedDate == null) return 0; + final String selectedDateStr = DateFormat( + 'yyyy-MM-dd', + ).format(state.selectedDate!); + final List 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 ordersOnDate = state.orders + .where((OrderItem s) => s.date == selectedDateStr) + .toList(); + return ordersOnDate + .where( + (OrderItem s) => + ['open', 'filled', 'confirmed'].contains(s.status), + ) + .length; + } +} diff --git a/apps/mobile/packages/features/client/view_orders/lib/src/presentation/blocs/view_orders_state.dart b/apps/mobile/packages/features/client/view_orders/lib/src/presentation/blocs/view_orders_state.dart new file mode 100644 index 00000000..af67fa19 --- /dev/null +++ b/apps/mobile/packages/features/client/view_orders/lib/src/presentation/blocs/view_orders_state.dart @@ -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 [], + this.filteredOrders = const [], + this.calendarDays = const [], + this.selectedDate, + this.filterTab = 'all', + this.weekOffset = 0, + this.activeCount = 0, + this.completedCount = 0, + this.upNextCount = 0, + }); + + final ViewOrdersStatus status; + final List orders; + final List filteredOrders; + final List calendarDays; + final DateTime? selectedDate; + final String filterTab; + final int weekOffset; + final int activeCount; + final int completedCount; + final int upNextCount; + + ViewOrdersState copyWith({ + ViewOrdersStatus? status, + List? orders, + List? filteredOrders, + List? 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 get props => [ + status, + orders, + filteredOrders, + calendarDays, + selectedDate, + filterTab, + weekOffset, + activeCount, + completedCount, + upNextCount, + ]; +} diff --git a/apps/mobile/packages/features/client/view_orders/lib/src/presentation/navigation/view_orders_navigator.dart b/apps/mobile/packages/features/client/view_orders/lib/src/presentation/navigation/view_orders_navigator.dart new file mode 100644 index 00000000..7160bb59 --- /dev/null +++ b/apps/mobile/packages/features/client/view_orders/lib/src/presentation/navigation/view_orders_navigator.dart @@ -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'); + } +} diff --git a/apps/mobile/packages/features/client/view_orders/lib/src/presentation/pages/view_orders_page.dart b/apps/mobile/packages/features/client/view_orders/lib/src/presentation/pages/view_orders_page.dart new file mode 100644 index 00000000..c47b8518 --- /dev/null +++ b/apps/mobile/packages/features/client/view_orders/lib/src/presentation/pages/view_orders_page.dart @@ -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( + create: (BuildContext context) => Modular.get(), + 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( + builder: (BuildContext context, ViewOrdersState state) { + final List calendarDays = state.calendarDays; + final List 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: [ + // Background Gradient + Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [UiColors.bgSecondary, UiColors.white], + stops: [0.0, 0.3], + ), + ), + ), + + SafeArea( + child: Column( + children: [ + // 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: [ + if (filteredOrders.isNotEmpty) + Padding( + padding: const EdgeInsets.only( + bottom: UiConstants.space3, + ), + child: Row( + children: [ + 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 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: [ + // Top Bar + Padding( + padding: const EdgeInsets.fromLTRB( + UiConstants.space5, + UiConstants.space5, + UiConstants.space5, + UiConstants.space3, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + 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: [ + _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: [ + IconButton( + icon: const Icon( + UiIcons.chevronLeft, + size: 20, + color: UiColors.iconSecondary, + ), + onPressed: () => BlocProvider.of( + 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( + 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( + 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( + color: UiColors.primary.withValues( + alpha: 0.25, + ), + blurRadius: 12, + offset: const Offset(0, 4), + ), + ] + : null, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + 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) ...[ + 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(context).selectFilterTab(tabId), + child: Column( + children: [ + 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: [ + 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); + } +} diff --git a/apps/mobile/packages/features/client/view_orders/lib/src/presentation/widgets/view_order_card.dart b/apps/mobile/packages/features/client/view_orders/lib/src/presentation/widgets/view_order_card.dart new file mode 100644 index 00000000..cec480ca --- /dev/null +++ b/apps/mobile/packages/features/client/view_orders/lib/src/presentation/widgets/view_order_card.dart @@ -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 createState() => _ViewOrderCardState(); +} + +class _ViewOrderCardState extends State { + 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( + color: UiColors.primary.withValues(alpha: 0.08), + blurRadius: 3, + offset: const Offset(0, 1), + ), + ], + ), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(UiConstants.space4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header Row + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Status Dot & Label + Row( + children: [ + 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: [ + 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: [ + 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: [ + 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: [ + _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: [ + 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: [ + 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') ...[ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + 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( + coveragePercent == 100 + ? UiColors.textSuccess + : UiColors.primary, + ), + minHeight: 4, + ), + ), + ], + ], + ), + ), + + // Worker Avatars and more details (Expanded section) + if (_expanded) ...[ + const Divider(height: 1, color: UiColors.separatorSecondary), + Padding( + padding: const EdgeInsets.all(UiConstants.space4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + 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 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: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + 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: [ + 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, + ), + ), + ], + ), + ); + } +} diff --git a/apps/mobile/packages/features/client/view_orders/lib/src/view_orders_module.dart b/apps/mobile/packages/features/client/view_orders/lib/src/view_orders_module.dart new file mode 100644 index 00000000..32536f0b --- /dev/null +++ b/apps/mobile/packages/features/client/view_orders/lib/src/view_orders_module.dart @@ -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 get imports => [DataConnectModule()]; + + @override + void binds(Injector i) { + // Repositories + i.addLazySingleton( + () => ViewOrdersRepositoryImpl( + orderRepositoryMock: i.get(), + ), + ); + + // UseCases + i.addLazySingleton(GetOrdersUseCase.new); + + // BLoCs + i.addSingleton(ViewOrdersCubit.new); + } + + @override + void routes(RouteManager r) { + r.child('/', child: (BuildContext context) => const ViewOrdersPage()); + } +} diff --git a/apps/mobile/packages/features/client/view_orders/lib/view_orders.dart b/apps/mobile/packages/features/client/view_orders/lib/view_orders.dart new file mode 100644 index 00000000..87ab3a35 --- /dev/null +++ b/apps/mobile/packages/features/client/view_orders/lib/view_orders.dart @@ -0,0 +1,3 @@ +library; + +export 'src/view_orders_module.dart'; diff --git a/apps/mobile/packages/features/client/view_orders/pubspec.yaml b/apps/mobile/packages/features/client/view_orders/pubspec.yaml new file mode 100644 index 00000000..5cbf0541 --- /dev/null +++ b/apps/mobile/packages/features/client/view_orders/pubspec.yaml @@ -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 diff --git a/apps/mobile/pubspec.lock b/apps/mobile/pubspec.lock index 903fdd09..22b2d153 100644 --- a/apps/mobile/pubspec.lock +++ b/apps/mobile/pubspec.lock @@ -1114,6 +1114,70 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + url_launcher: + dependency: transitive + description: + name: url_launcher + sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 + url: "https://pub.dev" + source: hosted + version: "6.3.2" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "767344bf3063897b5cf0db830e94f904528e6dd50a6dfaf839f0abf509009611" + url: "https://pub.dev" + source: hosted + version: "6.3.28" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: cfde38aa257dae62ffe79c87fab20165dfdf6988c1d31b58ebf59b9106062aad + url: "https://pub.dev" + source: hosted + version: "6.3.6" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a + url: "https://pub.dev" + source: hosted + version: "3.2.2" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18" + url: "https://pub.dev" + source: hosted + version: "3.2.5" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: d0412fcf4c6b31ecfdb7762359b7206ffba3bbffd396c6d9f9c4616ece476c1f + url: "https://pub.dev" + source: hosted + version: "2.4.2" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f" + url: "https://pub.dev" + source: hosted + version: "3.1.5" uuid: dependency: transitive description: diff --git a/apps/mobile/pubspec.yaml b/apps/mobile/pubspec.yaml index 1f8f5ec3..33599721 100644 --- a/apps/mobile/pubspec.yaml +++ b/apps/mobile/pubspec.yaml @@ -15,6 +15,7 @@ workspace: - packages/features/client/settings - packages/features/client/hubs - packages/features/client/create_order + - packages/features/client/view_orders - packages/features/client/client_main - apps/staff - apps/client