Skip to content

Commit 00debc9

Browse files
feat: implement custom modal date range picker in LogbookScreen for improved user experience
1 parent 6adb198 commit 00debc9

1 file changed

Lines changed: 264 additions & 4 deletions

File tree

dashboard/lib/screens/logbook_screen.dart

Lines changed: 264 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -182,11 +182,22 @@ class _LogbookScreenState extends State<LogbookScreen> {
182182
final initialStart =
183183
_customRange?.start ?? now.subtract(const Duration(days: 7));
184184
final initialEnd = _customRange?.end ?? now;
185-
final picked = await showDateRangePicker(
185+
final picked = await showModalBottomSheet<DateTimeRange>(
186186
context: context,
187-
firstDate: DateTime(2020),
188-
lastDate: now.add(const Duration(days: 365)),
189-
initialDateRange: DateTimeRange(start: initialStart, end: initialEnd),
187+
showDragHandle: true,
188+
isScrollControlled: true,
189+
builder: (ctx) {
190+
final base = Theme.of(ctx);
191+
final themed = base.copyWith(
192+
colorScheme: base.colorScheme.copyWith(primary: AppTheme.primary),
193+
);
194+
return Theme(
195+
data: themed,
196+
child: _CompactRangePickerSheet(
197+
initial: DateTimeRange(start: initialStart, end: initialEnd),
198+
),
199+
);
200+
},
190201
);
191202
if (picked == null) return;
192203
if (!context.mounted) return;
@@ -281,6 +292,22 @@ class _HistoryHeader extends StatelessWidget {
281292
),
282293
),
283294
SegmentedButton<_HistoryRange>(
295+
style: ButtonStyle(
296+
side: const WidgetStatePropertyAll(BorderSide.none),
297+
backgroundColor: WidgetStateProperty.resolveWith(
298+
(states) => states.contains(WidgetState.selected)
299+
? AppTheme.primary.withValues(alpha: 0.18)
300+
: AppTheme.surfaceLightFor(context),
301+
),
302+
foregroundColor: WidgetStateProperty.resolveWith(
303+
(states) => states.contains(WidgetState.selected)
304+
? AppTheme.primary
305+
: AppTheme.textMutedFor(context),
306+
),
307+
overlayColor: WidgetStatePropertyAll(
308+
AppTheme.primary.withValues(alpha: 0.10),
309+
),
310+
),
284311
segments: const [
285312
ButtonSegment(value: _HistoryRange.day, label: Text('Day')),
286313
ButtonSegment(value: _HistoryRange.week, label: Text('Week')),
@@ -294,6 +321,10 @@ class _HistoryHeader extends StatelessWidget {
294321
IconButton.filledTonal(
295322
tooltip: 'Pick date range',
296323
onPressed: onPickCustomRange,
324+
style: IconButton.styleFrom(
325+
backgroundColor: AppTheme.primary.withValues(alpha: 0.18),
326+
foregroundColor: AppTheme.primary,
327+
),
297328
icon: const Icon(Icons.date_range_outlined),
298329
),
299330
IconButton(
@@ -341,6 +372,235 @@ class _HistoryHeader extends StatelessWidget {
341372
}
342373
}
343374

375+
class _CompactRangePickerSheet extends StatefulWidget {
376+
final DateTimeRange initial;
377+
const _CompactRangePickerSheet({required this.initial});
378+
379+
@override
380+
State<_CompactRangePickerSheet> createState() =>
381+
_CompactRangePickerSheetState();
382+
}
383+
384+
class _CompactRangePickerSheetState extends State<_CompactRangePickerSheet> {
385+
late DateTime _start;
386+
late DateTime _end;
387+
bool _pickingStart = true;
388+
389+
@override
390+
void initState() {
391+
super.initState();
392+
_start = DateTime(
393+
widget.initial.start.year,
394+
widget.initial.start.month,
395+
widget.initial.start.day,
396+
);
397+
_end = DateTime(
398+
widget.initial.end.year,
399+
widget.initial.end.month,
400+
widget.initial.end.day,
401+
);
402+
if (_end.isBefore(_start)) _end = _start;
403+
}
404+
405+
@override
406+
Widget build(BuildContext context) {
407+
final maxH = MediaQuery.of(context).size.height * 0.72;
408+
return SafeArea(
409+
child: ConstrainedBox(
410+
constraints: BoxConstraints(maxHeight: maxH),
411+
child: Padding(
412+
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
413+
child: Column(
414+
children: [
415+
Row(
416+
children: [
417+
Text(
418+
'Date range',
419+
style: TextStyle(
420+
color: AppTheme.textPrimaryFor(context),
421+
fontWeight: FontWeight.w700,
422+
fontSize: 14,
423+
),
424+
),
425+
const Spacer(),
426+
TextButton(
427+
onPressed: () {
428+
final now = DateTime.now();
429+
setState(() {
430+
_start = DateTime(now.year, now.month, now.day);
431+
_end = _start;
432+
_pickingStart = true;
433+
});
434+
},
435+
child: Text('Today',
436+
style: TextStyle(color: AppTheme.primary)),
437+
),
438+
const SizedBox(width: 6),
439+
TextButton(
440+
onPressed: () {
441+
final now = DateTime.now();
442+
setState(() {
443+
_end = DateTime(now.year, now.month, now.day);
444+
_start = _end.subtract(const Duration(days: 7));
445+
_pickingStart = false;
446+
});
447+
},
448+
child:
449+
Text('7d', style: TextStyle(color: AppTheme.primary)),
450+
),
451+
const SizedBox(width: 6),
452+
TextButton(
453+
onPressed: () {
454+
final now = DateTime.now();
455+
setState(() {
456+
_end = DateTime(now.year, now.month, now.day);
457+
_start = _end.subtract(const Duration(days: 30));
458+
_pickingStart = false;
459+
});
460+
},
461+
child:
462+
Text('30d', style: TextStyle(color: AppTheme.primary)),
463+
),
464+
],
465+
),
466+
const SizedBox(height: 8),
467+
Container(
468+
padding: const EdgeInsets.all(12),
469+
decoration: BoxDecoration(
470+
color: AppTheme.surfaceLightFor(context),
471+
borderRadius: BorderRadius.circular(12),
472+
),
473+
child: Row(
474+
children: [
475+
_RangeChip(
476+
label: 'Start',
477+
value: _fmt(_start),
478+
active: _pickingStart,
479+
onTap: () => setState(() => _pickingStart = true),
480+
),
481+
const SizedBox(width: 10),
482+
_RangeChip(
483+
label: 'End',
484+
value: _fmt(_end),
485+
active: !_pickingStart,
486+
onTap: () => setState(() => _pickingStart = false),
487+
),
488+
const Spacer(),
489+
FilledButton(
490+
onPressed: () => Navigator.pop(
491+
context,
492+
DateTimeRange(start: _start, end: _end),
493+
),
494+
child: const Text('Apply'),
495+
),
496+
],
497+
),
498+
),
499+
const SizedBox(height: 12),
500+
Expanded(
501+
child: CalendarDatePicker(
502+
initialDate: _pickingStart ? _start : _end,
503+
firstDate: DateTime(2020),
504+
lastDate: DateTime.now().add(const Duration(days: 365)),
505+
onDateChanged: (d) {
506+
setState(() {
507+
final day = DateTime(d.year, d.month, d.day);
508+
if (_pickingStart) {
509+
_start = day;
510+
if (_end.isBefore(_start)) _end = _start;
511+
} else {
512+
_end = day;
513+
if (_end.isBefore(_start)) _start = _end;
514+
}
515+
});
516+
},
517+
),
518+
),
519+
Row(
520+
children: [
521+
TextButton(
522+
onPressed: () => Navigator.pop(context),
523+
child: const Text('Cancel'),
524+
),
525+
const Spacer(),
526+
Text(
527+
'${_fmt(_start)} → ${_fmt(_end)}',
528+
style: TextStyle(
529+
color: AppTheme.textMutedFor(context),
530+
fontSize: 11,
531+
),
532+
),
533+
],
534+
),
535+
],
536+
),
537+
),
538+
),
539+
);
540+
}
541+
542+
String _fmt(DateTime dt) {
543+
String two(int v) => v < 10 ? '0$v' : '$v';
544+
return '${dt.year}-${two(dt.month)}-${two(dt.day)}';
545+
}
546+
}
547+
548+
class _RangeChip extends StatelessWidget {
549+
final String label;
550+
final String value;
551+
final bool active;
552+
final VoidCallback onTap;
553+
554+
const _RangeChip({
555+
required this.label,
556+
required this.value,
557+
required this.active,
558+
required this.onTap,
559+
});
560+
561+
@override
562+
Widget build(BuildContext context) {
563+
final bg = active
564+
? AppTheme.primary.withValues(alpha: 0.16)
565+
: Theme.of(context).colorScheme.surface;
566+
final fg = active ? AppTheme.primary : AppTheme.textSecondaryFor(context);
567+
return InkWell(
568+
borderRadius: BorderRadius.circular(12),
569+
onTap: onTap,
570+
child: Container(
571+
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
572+
decoration: BoxDecoration(
573+
color: bg,
574+
borderRadius: BorderRadius.circular(12),
575+
),
576+
child: Column(
577+
crossAxisAlignment: CrossAxisAlignment.start,
578+
mainAxisSize: MainAxisSize.min,
579+
children: [
580+
Text(
581+
label.toUpperCase(),
582+
style: TextStyle(
583+
color: AppTheme.textMutedFor(context),
584+
fontSize: 9,
585+
fontWeight: FontWeight.w700,
586+
letterSpacing: 0.6,
587+
),
588+
),
589+
const SizedBox(height: 2),
590+
Text(
591+
value,
592+
style: TextStyle(
593+
color: fg,
594+
fontWeight: FontWeight.w700,
595+
),
596+
),
597+
],
598+
),
599+
),
600+
);
601+
}
602+
}
603+
344604
class _EmptyHistory extends StatelessWidget {
345605
final bool loaded;
346606
const _EmptyHistory({required this.loaded});

0 commit comments

Comments
 (0)