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
+215 -47
View File
@@ -5,6 +5,7 @@ import 'package:kooltab2/models/closed_tab_item.dart';
import 'package:kooltab2/models/payment_method.dart';
import '../../l10n/app_localizations.dart';
import '../../utils/history_grouping.dart';
class ClosedTabCard extends StatefulWidget {
final ClosedTab closedTab;
@@ -31,6 +32,10 @@ class _ClosedTabCardState extends State<ClosedTabCard> {
final l10n = AppLocalizations.of(context);
final locale = Localizations.localeOf(context).toLanguageTag();
final dateFormat = DateFormat.yMMMd(locale).add_jm();
final dayGroups = groupClosedTabItemsByDay(
closedTab.items,
fallbackDate: closedTab.closedAt,
);
final scheme = Theme.of(context).colorScheme;
return Container(
@@ -40,17 +45,16 @@ class _ClosedTabCardState extends State<ClosedTabCard> {
border: Border.all(color: scheme.onSurface.withValues(alpha: 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(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Material(
color: Colors.transparent,
child: InkWell(
onTap: () => setState(() => _expanded = !_expanded),
enableFeedback: true,
splashFactory: NoSplash.splashFactory,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 14,
@@ -137,27 +141,163 @@ class _ClosedTabCardState extends State<ClosedTabCard> {
],
),
),
AnimatedCrossFade(
duration: const Duration(milliseconds: 150),
crossFadeState: _expanded
? CrossFadeState.showFirst
: CrossFadeState.showSecond,
firstChild: Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 14),
child: Column(
),
),
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),
for (var index = 0; index < dayGroups.length; index++)
_ClosedTabDayDropdown(
key: ValueKey(
'${dayGroups[index].items.first.closedTabId}-'
'${dayGroups[index].day.toIso8601String()}',
),
group: dayGroups[index],
fallbackDate: closedTab.closedAt,
initiallyExpanded: index == 0,
),
],
),
),
secondChild: const SizedBox(width: double.infinity),
),
],
),
);
}
}
class _ClosedTabDayDropdown extends StatefulWidget {
final ClosedTabItemDayGroup group;
final DateTime fallbackDate;
final bool initiallyExpanded;
const _ClosedTabDayDropdown({
super.key,
required this.group,
required this.fallbackDate,
required this.initiallyExpanded,
});
@override
State<_ClosedTabDayDropdown> createState() => _ClosedTabDayDropdownState();
}
class _ClosedTabDayDropdownState extends State<_ClosedTabDayDropdown> {
late bool _expanded = widget.initiallyExpanded;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
final locale = Localizations.localeOf(context).toLanguageTag();
final dayFormat = DateFormat.yMMMMd(locale);
final scheme = Theme.of(context).colorScheme;
return Padding(
padding: const EdgeInsets.only(top: 8),
child: AnimatedContainer(
duration: const Duration(milliseconds: 160),
decoration: BoxDecoration(
color: _expanded
? scheme.primary.withValues(alpha: 0.08)
: scheme.onSurface.withValues(alpha: 0.035),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: _expanded
? scheme.primary.withValues(alpha: 0.28)
: scheme.onSurface.withValues(alpha: 0.08),
),
),
clipBehavior: Clip.antiAlias,
child: Column(
children: [
Material(
color: Colors.transparent,
child: InkWell(
onTap: () => setState(() => _expanded = !_expanded),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 10,
),
child: Row(
children: [
const Divider(height: 1),
const SizedBox(height: 8),
...closedTab.items.map(
(item) => _ClosedTabItemRow(item: item),
Container(
width: 30,
height: 30,
decoration: BoxDecoration(
color: scheme.primary.withValues(alpha: 0.14),
borderRadius: BorderRadius.circular(9),
),
child: Icon(
Icons.calendar_today_rounded,
size: 16,
color: scheme.primary,
),
),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
dayFormat.format(widget.group.day),
style: const TextStyle(
fontWeight: FontWeight.w700,
),
),
Text(
l10n.tabItemCount(widget.group.itemCount),
style: Theme.of(context).textTheme.bodySmall,
),
],
),
),
Icon(
_expanded
? Icons.keyboard_arrow_up_rounded
: Icons.keyboard_arrow_down_rounded,
color: _expanded
? scheme.primary
: scheme.onSurface.withValues(alpha: 0.5),
),
],
),
),
secondChild: const SizedBox(width: double.infinity),
),
],
),
),
AnimatedSize(
duration: const Duration(milliseconds: 160),
curve: Curves.easeOut,
child: _expanded
? Padding(
padding: const EdgeInsets.fromLTRB(12, 0, 12, 8),
child: Column(
children: [
Divider(
height: 1,
color: scheme.primary.withValues(alpha: 0.18),
),
for (final item in widget.group.items)
_ClosedTabItemRow(
item: item,
purchasedAt:
item.purchasedAt ?? widget.fallbackDate,
),
],
),
)
: const SizedBox.shrink(),
),
],
),
),
);
@@ -166,12 +306,15 @@ class _ClosedTabCardState extends State<ClosedTabCard> {
class _ClosedTabItemRow extends StatelessWidget {
final ClosedTabItem item;
final DateTime purchasedAt;
const _ClosedTabItemRow({required this.item});
const _ClosedTabItemRow({required this.item, required this.purchasedAt});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
final locale = Localizations.localeOf(context).toLanguageTag();
final timeFormat = DateFormat.jm(locale);
final unitPrice = NumberFormat.simpleCurrency(
locale: locale,
).format(item.unitPriceInCents / 100);
@@ -185,32 +328,57 @@ class _ClosedTabItemRow extends StatelessWidget {
child: Row(
children: [
Expanded(
child: Text(
item.productName,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontWeight: FontWeight.w600,
color: scheme.onSurface,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item.productName,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontWeight: FontWeight.w600,
color: item.isRemoved
? scheme.onSurface.withValues(alpha: 0.5)
: scheme.onSurface,
decoration: item.isRemoved
? TextDecoration.lineThrough
: null,
),
),
Text(
timeFormat.format(purchasedAt),
style: Theme.of(context).textTheme.bodySmall,
),
],
),
),
Text(
'${item.quantity} × $unitPrice',
style: Theme.of(context).textTheme.bodySmall,
),
const SizedBox(width: 12),
SizedBox(
width: 64,
child: Text(
lineTotal,
textAlign: TextAlign.end,
if (item.isRemoved)
Text(
l10n.removed,
style: TextStyle(
color: scheme.error,
fontSize: 12,
fontWeight: FontWeight.w700,
color: scheme.onSurface,
),
)
else ...[
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,
),
),
),
),
],
],
),
);