feat: sales range date

This commit is contained in:
efrilm
2025-08-18 16:43:07 +07:00
parent 2d25e15380
commit 1f6e5e9a2b
10 changed files with 1286 additions and 190 deletions
@@ -0,0 +1,363 @@
import 'package:flutter/material.dart';
import 'package:syncfusion_flutter_datepicker/datepicker.dart';
class DateRangePickerBottomSheet {
static Future<DateRangePickerSelectionChangedArgs?> show({
required BuildContext context,
String title = 'Pilih Rentang Tanggal',
DateTime? initialStartDate,
DateTime? initialEndDate,
DateTime? minDate,
DateTime? maxDate,
String confirmText = 'Pilih',
String cancelText = 'Batal',
Color primaryColor = Colors.blue,
Function(DateTime? startDate, DateTime? endDate)? onChanged,
}) async {
return await showModalBottomSheet<DateRangePickerSelectionChangedArgs?>(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
isDismissible: false,
enableDrag: false,
builder: (BuildContext context) => _DateRangePickerBottomSheet(
title: title,
initialStartDate: initialStartDate,
initialEndDate: initialEndDate,
minDate: minDate,
maxDate: maxDate,
confirmText: confirmText,
cancelText: cancelText,
primaryColor: primaryColor,
onChanged: onChanged,
),
);
}
}
class _DateRangePickerBottomSheet extends StatefulWidget {
final String title;
final DateTime? initialStartDate;
final DateTime? initialEndDate;
final DateTime? minDate;
final DateTime? maxDate;
final String confirmText;
final String cancelText;
final Color primaryColor;
final Function(DateTime? startDate, DateTime? endDate)? onChanged;
const _DateRangePickerBottomSheet({
required this.title,
this.initialStartDate,
this.initialEndDate,
this.minDate,
this.maxDate,
required this.confirmText,
required this.cancelText,
required this.primaryColor,
this.onChanged,
});
@override
State<_DateRangePickerBottomSheet> createState() =>
_DateRangePickerBottomSheetState();
}
class _DateRangePickerBottomSheetState
extends State<_DateRangePickerBottomSheet>
with TickerProviderStateMixin {
DateRangePickerSelectionChangedArgs? _selectionChangedArgs;
late AnimationController _animationController;
late Animation<double> _slideAnimation;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: this,
);
_slideAnimation = Tween<double>(begin: 1.0, end: 0.0).animate(
CurvedAnimation(parent: _animationController, curve: Curves.easeOutCubic),
);
_animationController.forward();
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
void _onSelectionChanged(DateRangePickerSelectionChangedArgs args) {
setState(() {
_selectionChangedArgs = args;
});
}
String _getSelectionText() {
if (_selectionChangedArgs?.value is PickerDateRange) {
final PickerDateRange range = _selectionChangedArgs!.value;
if (range.startDate != null && range.endDate != null) {
return '${_formatDate(range.startDate!)} - ${_formatDate(range.endDate!)}';
} else if (range.startDate != null) {
return _formatDate(range.startDate!);
}
}
return 'Belum ada tanggal dipilih';
}
String _formatDate(DateTime date) {
final months = [
'Jan',
'Feb',
'Mar',
'Apr',
'Mei',
'Jun',
'Jul',
'Agu',
'Sep',
'Okt',
'Nov',
'Des',
];
return '${date.day} ${months[date.month - 1]} ${date.year}';
}
bool get _isValidSelection {
if (_selectionChangedArgs?.value is PickerDateRange) {
final PickerDateRange range = _selectionChangedArgs!.value;
return range.startDate != null && range.endDate != null;
}
return false;
}
@override
Widget build(BuildContext context) {
final screenHeight = MediaQuery.of(context).size.height;
final bottomSheetHeight = screenHeight * 0.75;
return AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
return Transform.translate(
offset: Offset(0, _slideAnimation.value * bottomSheetHeight),
child: Container(
height: bottomSheetHeight,
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(24),
topRight: Radius.circular(24),
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Drag Handle
Container(
margin: const EdgeInsets.only(top: 12, bottom: 8),
width: 40,
height: 4,
decoration: BoxDecoration(
color: Colors.grey.shade300,
borderRadius: BorderRadius.circular(2),
),
),
// Content
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Column(
children: [
// Selection Info
Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: widget.primaryColor.withOpacity(0.08),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: widget.primaryColor.withOpacity(0.2),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Tanggal Terpilih:',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: widget.primaryColor,
),
),
const SizedBox(height: 6),
Text(
_getSelectionText(),
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
],
),
),
const SizedBox(height: 20),
// Date Picker
Container(
height: 320,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: Colors.grey.withOpacity(0.2),
),
),
child: SfDateRangePicker(
onSelectionChanged: _onSelectionChanged,
selectionMode: DateRangePickerSelectionMode.range,
initialSelectedRange:
(widget.initialStartDate != null &&
widget.initialEndDate != null)
? PickerDateRange(
widget.initialStartDate,
widget.initialEndDate,
)
: null,
minDate: widget.minDate,
maxDate: widget.maxDate,
startRangeSelectionColor: widget.primaryColor,
endRangeSelectionColor: widget.primaryColor,
rangeSelectionColor: widget.primaryColor
.withOpacity(0.2),
todayHighlightColor: widget.primaryColor,
headerStyle: DateRangePickerHeaderStyle(
backgroundColor: Colors.transparent,
textAlign: TextAlign.center,
textStyle: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
monthViewSettings: DateRangePickerMonthViewSettings(
viewHeaderStyle: DateRangePickerViewHeaderStyle(
backgroundColor: Colors.grey.withOpacity(0.1),
textStyle: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: widget.primaryColor,
),
),
),
selectionTextStyle: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 14,
),
rangeTextStyle: TextStyle(
color: widget.primaryColor,
fontWeight: FontWeight.w500,
fontSize: 14,
),
),
),
],
),
),
),
// Bottom Fixed Action Buttons
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
border: Border(
top: BorderSide(
color: Colors.grey.withOpacity(0.2),
width: 1,
),
),
),
child: SafeArea(
child: Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: () => Navigator.of(context).pop(),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
side: BorderSide(color: Colors.grey.shade400),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: Text(
widget.cancelText,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: Colors.grey,
),
),
),
),
const SizedBox(width: 16),
Expanded(
child: ElevatedButton(
onPressed: _isValidSelection
? () {
// Call onChanged when confirm button is pressed
if (widget.onChanged != null &&
_selectionChangedArgs?.value
is PickerDateRange) {
final PickerDateRange range =
_selectionChangedArgs!.value;
widget.onChanged!(
range.startDate,
range.endDate,
);
}
Navigator.of(
context,
).pop(_selectionChangedArgs);
}
: null,
style: ElevatedButton.styleFrom(
backgroundColor: widget.primaryColor,
padding: const EdgeInsets.symmetric(vertical: 16),
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
disabledBackgroundColor: Colors.grey.shade300,
),
child: Text(
widget.confirmText,
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: _isValidSelection
? Colors.white
: Colors.grey.shade600,
),
),
),
),
],
),
),
),
],
),
),
);
},
);
}
}
@@ -0,0 +1,531 @@
import 'package:flutter/material.dart';
import 'package:syncfusion_flutter_datepicker/datepicker.dart';
import '../../../common/theme/theme.dart';
import '../bottom_sheet/date_range_bottom_sheet.dart';
class DateRangePickerField extends StatefulWidget {
final String? label;
final String placeholder;
final DateTime? startDate;
final DateTime? endDate;
final DateTime? minDate;
final DateTime? maxDate;
final Function(DateTime? startDate, DateTime? endDate)? onChanged;
final Color primaryColor;
final bool enabled;
final String? errorText;
final EdgeInsetsGeometry? padding;
final TextStyle? textStyle;
final TextStyle? placeholderStyle;
final BoxDecoration? decoration;
final double height;
const DateRangePickerField({
Key? key,
this.label,
this.placeholder = 'Pilih rentang tanggal',
this.startDate,
this.endDate,
this.minDate,
this.maxDate,
this.onChanged,
this.primaryColor = AppColor.primary,
this.enabled = true,
this.errorText,
this.padding,
this.textStyle,
this.placeholderStyle,
this.decoration,
this.height = 52.0,
}) : super(key: key);
@override
State<DateRangePickerField> createState() => _DateRangePickerFieldState();
}
class _DateRangePickerFieldState extends State<DateRangePickerField> {
bool _isPressed = false;
String get _displayText {
if (widget.startDate != null && widget.endDate != null) {
return '${_formatDate(widget.startDate!)} - ${_formatDate(widget.endDate!)}';
} else if (widget.startDate != null) {
return _formatDate(widget.startDate!);
}
return widget.placeholder;
}
bool get _hasValue {
return widget.startDate != null || widget.endDate != null;
}
String _formatDate(DateTime date) {
final months = [
'Jan',
'Feb',
'Mar',
'Apr',
'Mei',
'Jun',
'Jul',
'Agu',
'Sep',
'Okt',
'Nov',
'Des',
];
return '${date.day} ${months[date.month - 1]} ${date.year}';
}
Future<void> _showDateRangePicker() async {
if (!widget.enabled) return;
final result = await DateRangePickerBottomSheet.show(
context: context,
title: widget.label ?? 'Pilih Rentang Tanggal',
initialStartDate: widget.startDate,
initialEndDate: widget.endDate,
minDate: widget.minDate,
maxDate: widget.maxDate,
primaryColor: widget.primaryColor,
onChanged: widget.onChanged,
);
if (result != null && widget.onChanged != null) {
if (result.value is PickerDateRange) {
final PickerDateRange range = result.value;
widget.onChanged!(range.startDate, range.endDate);
}
}
}
@override
Widget build(BuildContext context) {
final hasError = widget.errorText != null && widget.errorText!.isNotEmpty;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
// Label
if (widget.label != null) ...[
Text(
widget.label!,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: hasError ? AppColor.error : AppColor.textSecondary,
),
),
const SizedBox(height: 8),
],
// Input Field
GestureDetector(
onTap: _showDateRangePicker,
onTapDown: widget.enabled
? (_) => setState(() => _isPressed = true)
: null,
onTapUp: widget.enabled
? (_) => setState(() => _isPressed = false)
: null,
onTapCancel: widget.enabled
? () => setState(() => _isPressed = false)
: null,
child: AnimatedContainer(
duration: const Duration(milliseconds: 150),
height: widget.height,
padding:
widget.padding ?? const EdgeInsets.symmetric(horizontal: 16),
decoration:
widget.decoration ??
BoxDecoration(
color: widget.enabled
? (_isPressed ? AppColor.backgroundLight : AppColor.white)
: AppColor.background,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: hasError
? AppColor.error
: (_isPressed ? widget.primaryColor : AppColor.border),
width: _isPressed ? 2 : 1,
),
boxShadow: _isPressed && widget.enabled
? [
BoxShadow(
color: widget.primaryColor.withOpacity(0.1),
blurRadius: 8,
offset: const Offset(0, 2),
),
]
: null,
),
child: Row(
children: [
// Date Text
Expanded(
child: Text(
_displayText,
style:
widget.textStyle ??
TextStyle(
fontSize: 15,
fontWeight: _hasValue
? FontWeight.w500
: FontWeight.w400,
color: widget.enabled
? (_hasValue
? AppColor.textPrimary
: AppColor.textSecondary)
: AppColor.textLight,
),
),
),
// Calendar Icon
Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: widget.primaryColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
Icons.calendar_today_rounded,
size: 20,
color: widget.enabled
? widget.primaryColor
: AppColor.textLight,
),
),
],
),
),
),
// Error Text
if (hasError) ...[
const SizedBox(height: 6),
Text(
widget.errorText!,
style: TextStyle(
fontSize: 12,
color: AppColor.error,
fontWeight: FontWeight.w400,
),
),
],
],
);
}
}
// Variasi dengan style yang berbeda
class DateRangePickerFieldOutlined extends StatefulWidget {
final String? label;
final String placeholder;
final DateTime? startDate;
final DateTime? endDate;
final DateTime? minDate;
final DateTime? maxDate;
final Function(DateTime? startDate, DateTime? endDate)? onChanged;
final Color primaryColor;
final bool enabled;
final String? errorText;
const DateRangePickerFieldOutlined({
Key? key,
this.label,
this.placeholder = 'Pilih rentang tanggal',
this.startDate,
this.endDate,
this.minDate,
this.maxDate,
this.onChanged,
this.primaryColor = AppColor.primary,
this.enabled = true,
this.errorText,
}) : super(key: key);
@override
State<DateRangePickerFieldOutlined> createState() =>
_DateRangePickerFieldOutlinedState();
}
class _DateRangePickerFieldOutlinedState
extends State<DateRangePickerFieldOutlined> {
bool _isFocused = false;
String get _displayText {
if (widget.startDate != null && widget.endDate != null) {
return '${_formatDate(widget.startDate!)} - ${_formatDate(widget.endDate!)}';
} else if (widget.startDate != null) {
return _formatDate(widget.startDate!);
}
return widget.placeholder;
}
bool get _hasValue {
return widget.startDate != null || widget.endDate != null;
}
String _formatDate(DateTime date) {
final months = [
'Jan',
'Feb',
'Mar',
'Apr',
'Mei',
'Jun',
'Jul',
'Agu',
'Sep',
'Okt',
'Nov',
'Des',
];
return '${date.day} ${months[date.month - 1]} ${date.year}';
}
Future<void> _showDateRangePicker() async {
if (!widget.enabled) return;
setState(() => _isFocused = true);
final result = await DateRangePickerBottomSheet.show(
context: context,
title: widget.label ?? 'Pilih Rentang Tanggal',
initialStartDate: widget.startDate,
initialEndDate: widget.endDate,
minDate: widget.minDate,
maxDate: widget.maxDate,
primaryColor: widget.primaryColor,
onChanged: widget.onChanged,
);
setState(() => _isFocused = false);
if (result != null && widget.onChanged != null) {
if (result.value is PickerDateRange) {
final PickerDateRange range = result.value;
widget.onChanged!(range.startDate, range.endDate);
}
}
}
@override
Widget build(BuildContext context) {
final hasError = widget.errorText != null && widget.errorText!.isNotEmpty;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
GestureDetector(
onTap: _showDateRangePicker,
child: Container(
height: 56,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: hasError
? AppColor.error
: (_isFocused || _hasValue
? widget.primaryColor
: AppColor.border),
width: _isFocused ? 2 : 1,
),
),
child: Row(
children: [
const SizedBox(width: 16),
// Date Text
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (widget.label != null && (_isFocused || _hasValue))
Text(
widget.label!,
style: TextStyle(
fontSize: 12,
color: hasError
? AppColor.error
: widget.primaryColor,
fontWeight: FontWeight.w500,
),
),
Text(
_hasValue
? _displayText
: (widget.label ?? widget.placeholder),
style: TextStyle(
fontSize: _hasValue ? 16 : 16,
fontWeight: FontWeight.w400,
color: widget.enabled
? (_hasValue
? AppColor.textPrimary
: AppColor.textSecondary)
: AppColor.textLight,
),
),
],
),
),
// Calendar Icon
Padding(
padding: const EdgeInsets.only(right: 16),
child: Icon(
Icons.calendar_today_rounded,
size: 24,
color: widget.enabled
? (_isFocused
? widget.primaryColor
: AppColor.textSecondary)
: AppColor.textLight,
),
),
],
),
),
),
// Error Text
if (hasError) ...[
const SizedBox(height: 6),
Padding(
padding: const EdgeInsets.only(left: 16),
child: Text(
widget.errorText!,
style: TextStyle(
fontSize: 12,
color: AppColor.error,
fontWeight: FontWeight.w400,
),
),
),
],
],
);
}
}
// Usage Example Widget
class DateRangePickerExample extends StatefulWidget {
@override
_DateRangePickerExampleState createState() => _DateRangePickerExampleState();
}
class _DateRangePickerExampleState extends State<DateRangePickerExample> {
DateTime? _startDate;
DateTime? _endDate;
DateTime? _startDate2;
DateTime? _endDate2;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Date Range Picker Example'),
backgroundColor: AppColor.primary,
foregroundColor: AppColor.white,
),
body: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Default Style',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppColor.textPrimary,
),
),
const SizedBox(height: 16),
DateRangePickerField(
label: 'Periode Laporan',
placeholder: 'Pilih tanggal mulai - selesai',
startDate: _startDate,
endDate: _endDate,
primaryColor: AppColor.primary,
onChanged: (start, end) {
setState(() {
_startDate = start;
_endDate = end;
});
},
),
const SizedBox(height: 32),
Text(
'Outlined Style',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppColor.textPrimary,
),
),
const SizedBox(height: 16),
DateRangePickerFieldOutlined(
label: 'Rentang Waktu',
placeholder: 'Pilih rentang tanggal',
startDate: _startDate2,
endDate: _endDate2,
primaryColor: AppColor.secondary,
onChanged: (start, end) {
setState(() {
_startDate2 = start;
_endDate2 = end;
});
},
),
const SizedBox(height: 24),
// Display selected dates
if (_startDate != null ||
_endDate != null ||
_startDate2 != null ||
_endDate2 != null)
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColor.background,
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Selected Dates:',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
const SizedBox(height: 8),
if (_startDate != null)
Text(
'Default: ${_startDate!} - ${_endDate ?? 'Not selected'}',
),
if (_startDate2 != null)
Text(
'Outlined: ${_startDate2!} - ${_endDate2 ?? 'Not selected'}',
),
],
),
),
],
),
),
);
}
}
+110 -159
View File
@@ -10,6 +10,7 @@ import '../../../common/theme/theme.dart';
import '../../../domain/analytic/analytic.dart';
import '../../../injection.dart';
import '../../components/appbar/appbar.dart';
import '../../components/field/date_range_picker_field.dart';
import '../../components/spacer/spacer.dart';
import 'widgets/summary_card.dart';
@@ -79,143 +80,125 @@ class _SalesPageState extends State<SalesPage> with TickerProviderStateMixin {
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColor.background,
body: BlocBuilder<SalesLoaderBloc, SalesLoaderState>(
builder: (context, state) {
return CustomScrollView(
slivers: [
// App Bar
SliverAppBar(
expandedHeight: 120,
floating: false,
pinned: true,
backgroundColor: AppColor.primary,
flexibleSpace: CustomAppBar(title: 'Penjualan'),
),
// Date Range Header
SliverToBoxAdapter(
child: SlideTransition(
position: slideAnimation,
child: FadeTransition(
opacity: fadeAnimation,
child: state.isFetching
? _buildDateRangeShimmer()
: _buildDateRangeHeader(),
),
body: BlocListener<SalesLoaderBloc, SalesLoaderState>(
listenWhen: (previous, current) =>
previous.dateFrom != current.dateFrom &&
previous.dateTo != current.dateTo,
listener: (context, state) {
context.read<SalesLoaderBloc>().add(SalesLoaderEvent.fectched());
},
child: BlocBuilder<SalesLoaderBloc, SalesLoaderState>(
builder: (context, state) {
return CustomScrollView(
slivers: [
// App Bar
SliverAppBar(
expandedHeight: 120,
floating: false,
pinned: true,
backgroundColor: AppColor.primary,
flexibleSpace: CustomAppBar(title: 'Penjualan'),
),
),
// Summary Cards
SliverToBoxAdapter(
child: SlideTransition(
position: slideAnimation,
child: FadeTransition(
opacity: fadeAnimation,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Summary',
style: AppStyle.xxl.copyWith(
fontWeight: FontWeight.bold,
color: AppColor.textPrimary,
),
),
const SpaceHeight(16),
state.isFetching
? _buildSummaryShimmer()
: _buildSummaryCards(state),
],
),
),
),
),
),
// Net Sales Card
SliverToBoxAdapter(
child: SlideTransition(
position: slideAnimation,
child: FadeTransition(
opacity: fadeAnimation,
child: state.isFetching
? _buildNetSalesShimmer()
: _buildNetSalesCard(state),
),
),
),
// Daily Sales Section Header
SliverToBoxAdapter(
child: SlideTransition(
position: slideAnimation,
child: FadeTransition(
opacity: fadeAnimation,
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
child: Text(
'Daily Breakdown',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: AppColor.textPrimary,
// Date Range Header
SliverToBoxAdapter(
child: SlideTransition(
position: slideAnimation,
child: FadeTransition(
opacity: fadeAnimation,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: DateRangePickerField(
maxDate: DateTime.now(),
startDate: state.dateFrom,
endDate: state.dateTo,
onChanged: (startDate, endDate) {
context.read<SalesLoaderBloc>().add(
SalesLoaderEvent.rangeDateChanged(
startDate!,
endDate!,
),
);
},
),
),
),
),
),
),
// Daily Sales List
state.isFetching
? _buildDailySalesShimmer()
: _buildDailySalesList(state),
// Bottom Padding
const SliverToBoxAdapter(child: SpaceHeight(32)),
],
);
},
),
);
}
// Shimmer Components
Widget _buildDateRangeShimmer() {
return Container(
margin: const EdgeInsets.all(16),
child: Shimmer.fromColors(
baseColor: Colors.grey[300]!,
highlightColor: Colors.grey[100]!,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
Container(
width: 20,
height: 20,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(4),
// Summary Cards
SliverToBoxAdapter(
child: SlideTransition(
position: slideAnimation,
child: FadeTransition(
opacity: fadeAnimation,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Summary',
style: AppStyle.xxl.copyWith(
fontWeight: FontWeight.bold,
color: AppColor.textPrimary,
),
),
const SpaceHeight(16),
state.isFetching
? _buildSummaryShimmer()
: _buildSummaryCards(state),
],
),
),
),
),
),
),
SpaceWidth(8),
Container(
width: 150,
height: 16,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(4),
// Net Sales Card
SliverToBoxAdapter(
child: SlideTransition(
position: slideAnimation,
child: FadeTransition(
opacity: fadeAnimation,
child: state.isFetching
? _buildNetSalesShimmer()
: _buildNetSalesCard(state),
),
),
),
),
],
),
// Daily Sales Section Header
SliverToBoxAdapter(
child: SlideTransition(
position: slideAnimation,
child: FadeTransition(
opacity: fadeAnimation,
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
child: Text(
'Daily Breakdown',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: AppColor.textPrimary,
),
),
),
),
),
),
// Daily Sales List
state.isFetching
? _buildDailySalesShimmer()
: _buildDailySalesList(state),
// Bottom Padding
const SliverToBoxAdapter(child: SpaceHeight(32)),
],
);
},
),
),
);
@@ -415,38 +398,6 @@ class _SalesPageState extends State<SalesPage> with TickerProviderStateMixin {
);
}
// Original Components (preserved)
Widget _buildDateRangeHeader() {
return Container(
margin: const EdgeInsets.all(16),
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
decoration: BoxDecoration(
color: AppColor.surface,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Row(
children: [
Icon(Icons.date_range, color: AppColor.primary, size: 20),
SpaceWidth(8),
Text(
'Aug 1 - Aug 15, 2025',
style: AppStyle.md.copyWith(
color: AppColor.textPrimary,
fontWeight: FontWeight.w500,
),
),
],
),
);
}
Widget _buildSummaryCards(SalesLoaderState state) {
return Column(
children: [