feat: New Open Tab dialog with search and optional customer email. Drinks are now grouped per order. Payments trigger cha-ching and money rain.
This commit is contained in:
@@ -6,8 +6,9 @@ import '../../models/closed_tab.dart';
|
||||
import '../../models/closed_tab_item.dart';
|
||||
import '../../models/payment_method.dart';
|
||||
import '../../utils/easter_eggs.dart';
|
||||
import '../../utils/history_grouping.dart';
|
||||
import '../../utils/money.dart';
|
||||
import '../../utils/tab_orders.dart';
|
||||
import 'order_history_list.dart';
|
||||
|
||||
class ClosedTabCard extends StatefulWidget {
|
||||
final ClosedTab closedTab;
|
||||
@@ -35,10 +36,6 @@ 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(
|
||||
@@ -159,16 +156,12 @@ class _ClosedTabCardState extends State<ClosedTabCard> {
|
||||
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,
|
||||
),
|
||||
OrderHistoryList(
|
||||
orders: ordersOfClosedTab(closedTab),
|
||||
productName: (ClosedTabItem item) => item.productName,
|
||||
unitPriceInCents: (ClosedTabItem item) =>
|
||||
item.unitPriceInCents,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -179,213 +172,3 @@ class _ClosedTabCardState extends State<ClosedTabCard> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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: [
|
||||
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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
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(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ClosedTabItemRow extends StatelessWidget {
|
||||
final ClosedTabItem item;
|
||||
final DateTime purchasedAt;
|
||||
|
||||
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);
|
||||
final lineTotal = NumberFormat.simpleCurrency(
|
||||
locale: locale,
|
||||
).format(item.lineTotalInCents / 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(
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (item.isRemoved)
|
||||
Text(
|
||||
l10n.removed,
|
||||
style: TextStyle(
|
||||
color: scheme.error,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
)
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
const _emojis = ['💲', '🤑', '💸', '💰', '💵', '💶', '💷'];
|
||||
const _dropCount = 160;
|
||||
const _duration = Duration(seconds: 3);
|
||||
|
||||
/// Lets money emojis rain down over the whole screen for three seconds, for
|
||||
/// when a tab gets paid. Taps go straight through to the app underneath.
|
||||
void showMoneyRain(BuildContext context) {
|
||||
final overlay = Overlay.of(context, rootOverlay: true);
|
||||
late final OverlayEntry entry;
|
||||
entry = OverlayEntry(
|
||||
builder: (_) => _MoneyRain(onDone: () => entry.remove()),
|
||||
);
|
||||
overlay.insert(entry);
|
||||
}
|
||||
|
||||
class _MoneyRain extends StatefulWidget {
|
||||
final VoidCallback onDone;
|
||||
|
||||
const _MoneyRain({required this.onDone});
|
||||
|
||||
@override
|
||||
State<_MoneyRain> createState() => _MoneyRainState();
|
||||
}
|
||||
|
||||
class _MoneyRainState extends State<_MoneyRain>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final AnimationController _controller = AnimationController(
|
||||
vsync: this,
|
||||
duration: _duration,
|
||||
)..forward().whenComplete(widget.onDone);
|
||||
|
||||
final _drops = List.generate(_dropCount, (_) => _Drop.random(Random()));
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return IgnorePointer(
|
||||
child: CustomPaint(
|
||||
size: Size.infinite,
|
||||
painter: _MoneyRainPainter(_controller, _drops),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// One falling emoji. Times are fractions of the whole rain, so every drop
|
||||
/// has left the screen by the time the rain ends.
|
||||
class _Drop {
|
||||
final int emoji;
|
||||
final double x;
|
||||
final double start;
|
||||
final double fall;
|
||||
final double size;
|
||||
final double spin;
|
||||
final double wobble;
|
||||
|
||||
const _Drop({
|
||||
required this.emoji,
|
||||
required this.x,
|
||||
required this.start,
|
||||
required this.fall,
|
||||
required this.size,
|
||||
required this.spin,
|
||||
required this.wobble,
|
||||
});
|
||||
|
||||
factory _Drop.random(Random random) {
|
||||
final fall = 0.3 + random.nextDouble() * 0.17;
|
||||
return _Drop(
|
||||
emoji: random.nextInt(_emojis.length),
|
||||
x: random.nextDouble(),
|
||||
start: random.nextDouble() * (1 - fall),
|
||||
fall: fall,
|
||||
size: 28 + random.nextDouble() * 36,
|
||||
spin: (random.nextDouble() - 0.5) * 4 * pi,
|
||||
wobble: random.nextDouble() * 2 * pi,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _MoneyRainPainter extends CustomPainter {
|
||||
static const _baseSize = 64.0;
|
||||
|
||||
final Animation<double> animation;
|
||||
final List<_Drop> drops;
|
||||
final List<TextPainter> _glyphs = [
|
||||
for (final emoji in _emojis)
|
||||
TextPainter(
|
||||
text: TextSpan(
|
||||
text: emoji,
|
||||
style: const TextStyle(fontSize: _baseSize),
|
||||
),
|
||||
textDirection: TextDirection.ltr,
|
||||
)..layout(),
|
||||
];
|
||||
|
||||
_MoneyRainPainter(this.animation, this.drops) : super(repaint: animation);
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final t = animation.value;
|
||||
|
||||
for (final drop in drops) {
|
||||
final progress = (t - drop.start) / drop.fall;
|
||||
if (progress < 0 || progress > 1) continue;
|
||||
|
||||
final glyph = _glyphs[drop.emoji];
|
||||
final scale = drop.size / _baseSize;
|
||||
final dx =
|
||||
drop.x * size.width + sin(progress * 3 * pi + drop.wobble) * 24;
|
||||
final dy = -drop.size + progress * (size.height + drop.size * 2);
|
||||
|
||||
canvas
|
||||
..save()
|
||||
..translate(dx, dy)
|
||||
..rotate(drop.spin * progress)
|
||||
..scale(scale);
|
||||
glyph.paint(canvas, Offset(-glyph.width / 2, -glyph.height / 2));
|
||||
canvas.restore();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(_MoneyRainPainter oldDelegate) =>
|
||||
oldDelegate.drops != drops;
|
||||
}
|
||||
@@ -3,9 +3,11 @@ import 'package:intl/intl.dart';
|
||||
|
||||
import '../../l10n/app_localizations.dart';
|
||||
import '../../models/bar_tab.dart';
|
||||
import '../../models/tab_item_purchase.dart';
|
||||
import '../../models/tab_item.dart';
|
||||
import '../../utils/easter_eggs.dart';
|
||||
import '../../utils/money.dart';
|
||||
import '../../utils/tab_orders.dart';
|
||||
import 'order_history_list.dart';
|
||||
|
||||
class OpenTabHistoryCard extends StatefulWidget {
|
||||
final BarTab tab;
|
||||
@@ -19,42 +21,6 @@ class OpenTabHistoryCard extends StatefulWidget {
|
||||
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;
|
||||
@@ -163,9 +129,12 @@ class _OpenTabHistoryCardState extends State<OpenTabHistoryCard> {
|
||||
children: [
|
||||
const Divider(height: 1),
|
||||
const SizedBox(height: 8),
|
||||
..._purchases(
|
||||
tab,
|
||||
).map((purchase) => _OpenPurchaseRow(purchase: purchase)),
|
||||
OrderHistoryList(
|
||||
orders: ordersOfTab(tab, includeRemoved: true),
|
||||
productName: (TabItem item) => item.productName,
|
||||
unitPriceInCents: (TabItem item) =>
|
||||
item.unitPriceInCents,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -178,81 +147,3 @@ class _OpenTabHistoryCardState extends State<OpenTabHistoryCard> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../l10n/app_localizations.dart';
|
||||
import '../../utils/money.dart';
|
||||
import '../../utils/tab_orders.dart';
|
||||
|
||||
/// The line above the drinks of one order: "—— 12:32 today ——". With
|
||||
/// [totalInCents] it adds what the order costs; with [onTap] it opens and
|
||||
/// closes the order, a chevron showing whether it is [expanded].
|
||||
class OrderDivider extends StatelessWidget {
|
||||
final DateTime startedAt;
|
||||
final int? totalInCents;
|
||||
final bool expanded;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
const OrderDivider({
|
||||
super.key,
|
||||
required this.startedAt,
|
||||
this.totalInCents,
|
||||
this.expanded = true,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final locale = Localizations.localeOf(context).toLanguageTag();
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
final line = Expanded(
|
||||
child: Divider(color: scheme.onSurface.withValues(alpha: 0.18)),
|
||||
);
|
||||
final label = [
|
||||
orderTimeLabel(l10n, locale, startedAt),
|
||||
if (totalInCents != null) totalInCents!.asEuro,
|
||||
].join(' – ');
|
||||
final style = Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
color: scheme.onSurface.withValues(alpha: 0.6),
|
||||
);
|
||||
|
||||
final row = Padding(
|
||||
padding: const EdgeInsets.only(top: 6, bottom: 2),
|
||||
child: Row(
|
||||
children: [
|
||||
line,
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(label, style: style),
|
||||
if (onTap != null) ...[
|
||||
const SizedBox(width: 2),
|
||||
Icon(
|
||||
expanded
|
||||
? Icons.keyboard_arrow_up_rounded
|
||||
: Icons.keyboard_arrow_down_rounded,
|
||||
size: 18,
|
||||
color: style?.color,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
line,
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (onTap == null) return row;
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: row,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../l10n/app_localizations.dart';
|
||||
import '../../utils/money.dart';
|
||||
import '../../utils/tab_orders.dart';
|
||||
import 'order_divider.dart';
|
||||
|
||||
/// The drinks of a tab in history, per order: "2 × Duvel € 8,00", with the
|
||||
/// drinks taken off again struck through.
|
||||
class OrderHistoryList<L> extends StatelessWidget {
|
||||
final List<TabOrder<L>> orders;
|
||||
final String Function(L line) productName;
|
||||
final int Function(L line) unitPriceInCents;
|
||||
|
||||
const OrderHistoryList({
|
||||
super.key,
|
||||
required this.orders,
|
||||
required this.productName,
|
||||
required this.unitPriceInCents,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
for (final order in orders) ...[
|
||||
OrderDivider(startedAt: order.startedAt),
|
||||
for (final row in order.rows) ...[
|
||||
if (row.quantity > 0)
|
||||
_HistoryRow(
|
||||
name: productName(row.line),
|
||||
quantity: row.quantity,
|
||||
unitPriceInCents: unitPriceInCents(row.line),
|
||||
),
|
||||
if (row.removed > 0)
|
||||
_HistoryRow(
|
||||
name: productName(row.line),
|
||||
quantity: row.removed,
|
||||
unitPriceInCents: unitPriceInCents(row.line),
|
||||
removed: true,
|
||||
),
|
||||
],
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _HistoryRow extends StatelessWidget {
|
||||
final String name;
|
||||
final int quantity;
|
||||
final int unitPriceInCents;
|
||||
final bool removed;
|
||||
|
||||
const _HistoryRow({
|
||||
required this.name,
|
||||
required this.quantity,
|
||||
required this.unitPriceInCents,
|
||||
this.removed = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 5),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
'$quantity × $name',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: removed
|
||||
? scheme.onSurface.withValues(alpha: 0.5)
|
||||
: scheme.onSurface,
|
||||
decoration: removed ? TextDecoration.lineThrough : null,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
if (removed)
|
||||
Text(
|
||||
l10n.removedCount(quantity),
|
||||
style: TextStyle(
|
||||
color: scheme.error,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
)
|
||||
else
|
||||
Text(
|
||||
(quantity * unitPriceInCents).asEuro,
|
||||
style: const TextStyle(fontWeight: FontWeight.w700),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user