feat: add more detailed history and group per day

This commit is contained in:
2026-08-07 05:07:41 +02:00
parent 1dd7e69aa8
commit 799c1c4045
23 changed files with 2506 additions and 346 deletions
@@ -0,0 +1,256 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import '../../l10n/app_localizations.dart';
import '../../models/bar_tab.dart';
import '../../models/tab_item_purchase.dart';
class OpenTabHistoryCard extends StatefulWidget {
final BarTab tab;
const OpenTabHistoryCard({super.key, required this.tab});
@override
State<OpenTabHistoryCard> createState() => _OpenTabHistoryCardState();
}
class _OpenTabHistoryCardState extends State<OpenTabHistoryCard> {
bool _expanded = false;
List<_OpenPurchase> _purchases(BarTab tab) {
final purchases = <_OpenPurchase>[];
for (final item in tab.items) {
final records = item.purchases.isEmpty
? List<TabItemPurchase>.generate(
item.quantity,
(index) => TabItemPurchase(
id: '${item.id}-$index',
tabItemId: item.id,
purchasedAt: tab.openedAt,
),
)
: item.purchases;
purchases.addAll(
records.map(
(purchase) => _OpenPurchase(
productName: item.productName,
unitPriceInCents: item.unitPriceInCents,
purchasedAt: purchase.purchasedAt,
isRemoved: purchase.isRemoved,
),
),
);
}
purchases.sort((a, b) {
final byTime = b.purchasedAt.compareTo(a.purchasedAt);
if (byTime != 0) return byTime;
return b.productName.compareTo(a.productName);
});
return purchases;
}
@override
Widget build(BuildContext context) {
final tab = widget.tab;
final l10n = AppLocalizations.of(context);
final locale = Localizations.localeOf(context).toLanguageTag();
final dateFormat = DateFormat.yMMMd(locale).add_jm();
final scheme = Theme.of(context).colorScheme;
return Container(
decoration: BoxDecoration(
color: scheme.primary.withValues(alpha: 0.06),
borderRadius: BorderRadius.circular(14),
border: Border.all(color: scheme.primary.withValues(alpha: 0.2)),
),
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: [
Row(
children: [
Expanded(
child: Text(
tab.customerName,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w800,
color: scheme.onSurface,
),
),
),
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 3,
),
decoration: BoxDecoration(
color: scheme.primary.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(999),
),
child: Text(
l10n.openTabs,
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w700,
color: scheme.primary,
),
),
),
],
),
const SizedBox(height: 2),
Text(
'${dateFormat.format(tab.openedAt)} · ${l10n.tabItemCount(tab.itemCount)}',
style: Theme.of(context).textTheme.bodySmall,
),
],
),
),
const SizedBox(width: 8),
Text(
tab.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.withValues(alpha: 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),
..._purchases(
tab,
).map((purchase) => _OpenPurchaseRow(purchase: purchase)),
],
),
),
secondChild: const SizedBox(width: double.infinity),
),
],
),
),
),
);
}
}
class _OpenPurchase {
final String productName;
final int unitPriceInCents;
final DateTime purchasedAt;
final bool isRemoved;
const _OpenPurchase({
required this.productName,
required this.unitPriceInCents,
required this.purchasedAt,
required this.isRemoved,
});
}
class _OpenPurchaseRow extends StatelessWidget {
final _OpenPurchase purchase;
const _OpenPurchaseRow({required this.purchase});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
final locale = Localizations.localeOf(context).toLanguageTag();
final dateFormat = DateFormat.yMMMd(locale).add_jm();
final unitPrice = NumberFormat.simpleCurrency(
locale: locale,
).format(purchase.unitPriceInCents / 100);
final scheme = Theme.of(context).colorScheme;
return Padding(
padding: const EdgeInsets.symmetric(vertical: 6),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
purchase.productName,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontWeight: FontWeight.w600,
color: purchase.isRemoved
? scheme.onSurface.withValues(alpha: 0.5)
: scheme.onSurface,
decoration: purchase.isRemoved
? TextDecoration.lineThrough
: null,
),
),
Text(
dateFormat.format(purchase.purchasedAt),
style: Theme.of(context).textTheme.bodySmall,
),
],
),
),
if (purchase.isRemoved)
Text(
l10n.removed,
style: TextStyle(
color: scheme.error,
fontSize: 12,
fontWeight: FontWeight.w700,
),
)
else
Text(
'1 × $unitPrice',
style: Theme.of(context).textTheme.bodySmall,
),
],
),
);
}
}