From e7d5c29c000a1e75a6d583d4c86143ccb6bf251a Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Sun, 1 Feb 2026 21:14:01 -0500 Subject: [PATCH] feat: Integrate ViewOrdersHeader and ViewOrdersFilterTab components for improved UI in ViewOrdersPage --- .../presentation/pages/view_orders_page.dart | 434 +++--------------- .../presentation/widgets/view_order_card.dart | 1 + .../widgets/view_orders_filter_tab.dart | 68 +++ .../widgets/view_orders_header.dart | 259 +++++++++++ 4 files changed, 400 insertions(+), 362 deletions(-) create mode 100644 apps/mobile/packages/features/client/view_orders/lib/src/presentation/widgets/view_orders_filter_tab.dart create mode 100644 apps/mobile/packages/features/client/view_orders/lib/src/presentation/widgets/view_orders_header.dart 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 index 27ca4dc2..fd256e8c 100644 --- 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 @@ -1,4 +1,3 @@ -import 'dart:ui'; import 'package:design_system/design_system.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -10,6 +9,7 @@ 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 '../widgets/view_orders_header.dart'; import '../navigation/view_orders_navigator.dart'; /// The main page for viewing client orders. @@ -22,6 +22,7 @@ class ViewOrdersPage extends StatelessWidget { /// Creates a [ViewOrdersPage]. const ViewOrdersPage({super.key, this.initialDate}); + /// The initial date to display orders for. final DateTime? initialDate; @override @@ -37,7 +38,8 @@ class ViewOrdersPage extends StatelessWidget { class ViewOrdersView extends StatefulWidget { /// Creates a [ViewOrdersView]. const ViewOrdersView({super.key, this.initialDate}); - + + /// The initial date to display orders for. final DateTime? initialDate; @override @@ -88,376 +90,84 @@ class _ViewOrdersViewState extends State { } return Scaffold( - 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], - ), + body: SafeArea( + child: Column( + children: [ + // Header + Filter + Calendar (Sticky behavior) + ViewOrdersHeader( + state: state, + calendarDays: calendarDays, ), - ), - - 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), - ), + + // 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, - ), - ), - if (state.filteredOrders.isNotEmpty) - 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', - count: state.upNextCount, - ), - const SizedBox(width: UiConstants.space6), - _buildFilterTab( - context, - label: t.client_view_orders.tabs.active, - isSelected: state.filterTab == 'active', - tabId: 'active', - count: state.activeCount, - ), - 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, 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 index 6886cfe0..7ded64a7 100644 --- 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 @@ -8,6 +8,7 @@ import 'package:intl/intl.dart'; import 'package:krow_data_connect/krow_data_connect.dart' as dc; import 'package:krow_domain/krow_domain.dart'; import 'package:url_launcher/url_launcher.dart'; + import '../blocs/view_orders_cubit.dart'; /// A rich card displaying details of a client order/shift. diff --git a/apps/mobile/packages/features/client/view_orders/lib/src/presentation/widgets/view_orders_filter_tab.dart b/apps/mobile/packages/features/client/view_orders/lib/src/presentation/widgets/view_orders_filter_tab.dart new file mode 100644 index 00000000..661face0 --- /dev/null +++ b/apps/mobile/packages/features/client/view_orders/lib/src/presentation/widgets/view_orders_filter_tab.dart @@ -0,0 +1,68 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../blocs/view_orders_cubit.dart'; + +/// A single filter tab for the View Orders page. +/// +/// Displays a label with an optional count and shows a selection indicator +/// when the tab is active. +class ViewOrdersFilterTab extends StatelessWidget { + /// Creates a [ViewOrdersFilterTab]. + const ViewOrdersFilterTab({ + required this.label, + required this.isSelected, + required this.tabId, + this.count, + super.key, + }); + + /// The label text to display. + final String label; + + /// Whether this tab is currently selected. + final bool isSelected; + + /// The unique identifier for this tab. + final String tabId; + + /// Optional count to display next to the label. + final int? count; + + @override + Widget build(BuildContext context) { + 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), + ], + ), + ); + } +} diff --git a/apps/mobile/packages/features/client/view_orders/lib/src/presentation/widgets/view_orders_header.dart b/apps/mobile/packages/features/client/view_orders/lib/src/presentation/widgets/view_orders_header.dart new file mode 100644 index 00000000..c53cf6f0 --- /dev/null +++ b/apps/mobile/packages/features/client/view_orders/lib/src/presentation/widgets/view_orders_header.dart @@ -0,0 +1,259 @@ +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 'package:krow_domain/krow_domain.dart'; +import '../blocs/view_orders_cubit.dart'; +import '../blocs/view_orders_state.dart'; +import '../navigation/view_orders_navigator.dart'; +import 'view_orders_filter_tab.dart'; + +/// The sticky header section for the View Orders page. +/// +/// This widget contains: +/// - Top bar with title and post button +/// - Filter tabs (Up Next, Active, Completed) +/// - Calendar navigation controls +/// - Horizontal calendar grid +class ViewOrdersHeader extends StatelessWidget { + /// Creates a [ViewOrdersHeader]. + const ViewOrdersHeader({ + required this.state, + required this.calendarDays, + super.key, + }); + + /// The current state of the view orders feature. + final ViewOrdersState state; + + /// The list of calendar days to display. + final List calendarDays; + + @override + Widget build(BuildContext context) { + 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, + ), + ), + if (state.filteredOrders.isNotEmpty) + 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: [ + ViewOrdersFilterTab( + label: t.client_view_orders.tabs.up_next, + isSelected: state.filterTab == 'all', + tabId: 'all', + count: state.upNextCount, + ), + const SizedBox(width: UiConstants.space6), + ViewOrdersFilterTab( + label: t.client_view_orders.tabs.active, + isSelected: state.filterTab == 'active', + tabId: 'active', + count: state.activeCount, + ), + const SizedBox(width: UiConstants.space6), + ViewOrdersFilterTab( + 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), + ], + ), + ), + ), + ); + } +}