feat: Integrate ViewOrdersHeader and ViewOrdersFilterTab components for improved UI in ViewOrdersPage

This commit is contained in:
Achintha Isuru
2026-02-01 21:14:01 -05:00
parent 82e479b4c0
commit e7d5c29c00
4 changed files with 400 additions and 362 deletions

View File

@@ -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<ViewOrdersView> {
}
return Scaffold(
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],
),
body: SafeArea(
child: Column(
children: <Widget>[
// Header + Filter + Calendar (Sticky behavior)
ViewOrdersHeader(
state: state,
calendarDays: calendarDays,
),
),
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),
),
// 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,
),
),
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: <Widget>[
_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: <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,

View File

@@ -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.

View File

@@ -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<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),
],
),
);
}
}

View File

@@ -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<DateTime> 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: <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,
),
),
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: <Widget>[
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: <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),
],
),
),
),
);
}
}