Files
kooltab/lib/views/history_screen_view.dart
T
2026-07-12 02:49:08 +02:00

329 lines
9.9 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
import '../app/router.dart';
import '../models/closed_tab.dart';
import '../models/closed_tab_item.dart';
import '../viewmodels/history_view_model.dart';
class HistoryScreenView extends StatefulWidget {
const HistoryScreenView({super.key});
@override
State<HistoryScreenView> createState() => _HistoryScreenViewState();
}
class _HistoryScreenViewState extends State<HistoryScreenView> with RouteAware {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<HistoryViewModel>().ensureLoaded();
});
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<HistoryViewModel>().load();
});
}
@override
void didPopNext() {
// Called when you come back to this screen
context.read<HistoryViewModel>().load();
}
@override
void dispose() {
routeObserver.unsubscribe(this);
super.dispose();
}
@override
Widget build(BuildContext context) {
final viewModel = context.watch<HistoryViewModel>();
return Scaffold(
appBar: AppBar(
title: Row(
children: [
IconButton(onPressed: (){
context.go('/bar');
}, icon: const Icon(Icons.arrow_back)),
const SizedBox(width: 5),
const Text('Tab History'),
],
),
actions: [
IconButton(
tooltip: 'Refresh',
onPressed: viewModel.load,
icon: const Icon(Icons.refresh_rounded),
),
const SizedBox(width: 8),
],
),
body: Builder(
builder: (context) {
if (viewModel.isLoading && !viewModel.hasLoaded) {
return const Center(
child: CircularProgressIndicator(strokeWidth: 2.5),
);
}
if (viewModel.errorMessage != null) {
return Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.error_outline_rounded,
size: 40,
color: Theme.of(context).colorScheme.error,
),
const SizedBox(height: 12),
Text(
viewModel.errorMessage!,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyLarge,
),
],
),
),
);
}
return Column(
children: [
Padding(
padding: const EdgeInsets.fromLTRB(20, 16, 20, 8),
child: TextField(
onChanged: viewModel.search,
decoration: const InputDecoration(
hintText: 'Search by name…',
prefixIcon: Icon(Icons.search_rounded, size: 20),
isDense: true,
),
),
),
Expanded(
child: viewModel.closedTabs.isEmpty
? _EmptyState()
: ListView.separated(
padding: const EdgeInsets.fromLTRB(20, 8, 20, 20),
itemCount: viewModel.closedTabs.length,
separatorBuilder: (_, __) => const SizedBox(height: 10),
itemBuilder: (context, index) {
final closedTab = viewModel.closedTabs[index];
return _ClosedTabCard(closedTab: closedTab);
},
),
),
],
);
},
),
);
}
}
class _EmptyState extends StatelessWidget {
@override
Widget build(BuildContext context) {
final scheme = Theme.of(context).colorScheme;
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: scheme.onSurface.withOpacity(0.05),
),
child: Icon(
Icons.history_rounded,
size: 40,
color: scheme.onSurface.withOpacity(0.3),
),
),
const SizedBox(height: 16),
Text(
'No closed tabs yet',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 4),
Text(
'Tabs you close will show up here.',
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
);
}
}
class _ClosedTabCard extends StatefulWidget {
final ClosedTab closedTab;
const _ClosedTabCard({required this.closedTab});
@override
State<_ClosedTabCard> createState() => _ClosedTabCardState();
}
class _ClosedTabCardState extends State<_ClosedTabCard> {
bool _expanded = false;
@override
Widget build(BuildContext context) {
final closedTab = widget.closedTab;
final dateFormat = DateFormat('MMM d, y · h:mm a');
final scheme = Theme.of(context).colorScheme;
return Container(
decoration: BoxDecoration(
color: scheme.onSurface.withOpacity(0.04),
borderRadius: BorderRadius.circular(14),
border: Border.all(color: scheme.onSurface.withOpacity(0.06)),
),
clipBehavior: Clip.antiAlias,
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () => setState(() => _expanded = !_expanded),
enableFeedback: true,
splashFactory: NoSplash.splashFactory,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 14,
),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
closedTab.customerName,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w800,
color: scheme.onSurface,
),
),
const SizedBox(height: 2),
Text(
'${dateFormat.format(closedTab.closedAt)} · ${closedTab.itemCount} items',
style: Theme.of(context).textTheme.bodySmall,
),
],
),
),
const SizedBox(width: 8),
Text(
closedTab.formattedTotal,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w800,
color: scheme.onSurface,
),
),
const SizedBox(width: 4),
Icon(
_expanded
? Icons.expand_less_rounded
: Icons.expand_more_rounded,
color: scheme.onSurface.withOpacity(0.5),
),
],
),
),
AnimatedCrossFade(
duration: const Duration(milliseconds: 150),
crossFadeState: _expanded
? CrossFadeState.showFirst
: CrossFadeState.showSecond,
firstChild: Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 14),
child: Column(
children: [
const Divider(height: 1),
const SizedBox(height: 8),
...closedTab.items.map(
(item) => _ClosedTabItemRow(item: item),
),
],
),
),
secondChild: const SizedBox(width: double.infinity),
),
],
),
),
),
);
}
}
class _ClosedTabItemRow extends StatelessWidget {
final ClosedTabItem item;
const _ClosedTabItemRow({required this.item});
@override
Widget build(BuildContext context) {
final unitPrice =
NumberFormat.simpleCurrency().format(item.unitPriceInCents / 100);
final lineTotal =
NumberFormat.simpleCurrency().format(item.lineTotalInCents / 100);
final scheme = Theme.of(context).colorScheme;
return Padding(
padding: const EdgeInsets.symmetric(vertical: 6),
child: Row(
children: [
Expanded(
child: Text(
item.productName,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(fontWeight: FontWeight.w600, color: scheme.onSurface),
),
),
Text(
'${item.quantity} × $unitPrice',
style: Theme.of(context).textTheme.bodySmall,
),
const SizedBox(width: 12),
SizedBox(
width: 64,
child: Text(
lineTotal,
textAlign: TextAlign.end,
style: TextStyle(fontWeight: FontWeight.w700, color: scheme.onSurface),
),
),
],
),
);
}
}