266 lines
8.1 KiB
Dart
266 lines
8.1 KiB
Dart
import 'package:flutter/material.dart';
|
||
|
||
import '../../l10n/app_localizations.dart';
|
||
import '../../models/bar_tab.dart';
|
||
import '../../models/tab_item.dart';
|
||
import '../../utils/money.dart';
|
||
import '../../utils/tab_orders.dart';
|
||
import '../widgets/order_divider.dart';
|
||
|
||
/// Changes how many of [item] are on the tab: [delta] > 0 rings up more,
|
||
/// [delta] < 0 takes drinks of order [orderId] off.
|
||
typedef ItemQuantityChanged =
|
||
Future<void> Function(TabItem item, int delta, String? orderId);
|
||
|
||
class SelectedTabDetails extends StatelessWidget {
|
||
final BarTab tab;
|
||
final ItemQuantityChanged onItemQuantityChanged;
|
||
final VoidCallback onCloseTabPressed;
|
||
|
||
const SelectedTabDetails({
|
||
super.key,
|
||
required this.tab,
|
||
required this.onItemQuantityChanged,
|
||
required this.onCloseTabPressed,
|
||
});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final scheme = Theme.of(context).colorScheme;
|
||
final l10n = AppLocalizations.of(context);
|
||
|
||
return Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Expanded(
|
||
child: tab.items.isEmpty
|
||
? Center(
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
Icon(
|
||
Icons.local_bar_outlined,
|
||
size: 32,
|
||
color: scheme.onSurface.withValues(alpha: 0.25),
|
||
),
|
||
const SizedBox(height: 8),
|
||
Text(
|
||
l10n.tapProductsToAdd,
|
||
style: Theme.of(context).textTheme.bodyMedium,
|
||
),
|
||
],
|
||
),
|
||
)
|
||
: _OrderList(
|
||
// Another tab starts with its own orders folded
|
||
key: ValueKey(tab.id),
|
||
orders: ordersOfTab(tab),
|
||
onItemQuantityChanged: onItemQuantityChanged,
|
||
),
|
||
),
|
||
const SizedBox(height: 8),
|
||
Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||
decoration: BoxDecoration(
|
||
color: Theme.of(
|
||
context,
|
||
).colorScheme.primary.withValues(alpha: 0.12),
|
||
borderRadius: BorderRadius.circular(16),
|
||
border: Border.all(
|
||
color: Theme.of(
|
||
context,
|
||
).colorScheme.primary.withValues(alpha: 0.3),
|
||
),
|
||
),
|
||
child: Row(
|
||
children: [
|
||
Expanded(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
l10n.total,
|
||
style: Theme.of(context).textTheme.bodySmall,
|
||
),
|
||
Text(
|
||
tab.totalInCents.asEuro,
|
||
style: Theme.of(
|
||
context,
|
||
).textTheme.headlineMedium?.copyWith(fontSize: 24),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
FilledButton(
|
||
onPressed: tab.items.isEmpty ? null : onCloseTabPressed,
|
||
child: Text(l10n.closeTab),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
}
|
||
|
||
/// The tab's orders, oldest first, each under the time it started.
|
||
class _OrderList extends StatefulWidget {
|
||
final List<TabOrder<TabItem>> orders;
|
||
final ItemQuantityChanged onItemQuantityChanged;
|
||
|
||
const _OrderList({
|
||
super.key,
|
||
required this.orders,
|
||
required this.onItemQuantityChanged,
|
||
});
|
||
|
||
@override
|
||
State<_OrderList> createState() => _OrderListState();
|
||
}
|
||
|
||
/// The current (latest) order is open, earlier ones are folded away under
|
||
/// their time and total; tapping a header flips it.
|
||
class _OrderListState extends State<_OrderList> {
|
||
/// Orders opened or closed by hand, by [_key].
|
||
final _toggled = <String, bool>{};
|
||
|
||
String _key(TabOrder<TabItem> order) =>
|
||
order.orderId ?? order.startedAt.toIso8601String();
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final orders = widget.orders;
|
||
final children = <Widget>[];
|
||
for (final (orderIndex, order) in orders.indexed) {
|
||
final key = _key(order);
|
||
final expanded = _toggled[key] ?? orderIndex == orders.length - 1;
|
||
children.add(
|
||
OrderDivider(
|
||
key: ValueKey('order-$key'),
|
||
startedAt: order.startedAt,
|
||
totalInCents: order.rows.fold<int>(
|
||
0,
|
||
(total, row) => total + row.quantity * row.line.unitPriceInCents,
|
||
),
|
||
expanded: expanded,
|
||
onTap: () => setState(() => _toggled[key] = !expanded),
|
||
),
|
||
);
|
||
if (!expanded) continue;
|
||
|
||
for (final (index, row) in order.rows.indexed) {
|
||
if (index > 0) children.add(const Divider(height: 1));
|
||
children.add(
|
||
TabItemRow(
|
||
key: ValueKey('${order.orderId ?? order.startedAt}-${row.line.id}'),
|
||
item: row.line,
|
||
quantity: row.quantity,
|
||
orderId: order.orderId,
|
||
onQuantityChanged: widget.onItemQuantityChanged,
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
return ListView(children: children);
|
||
}
|
||
}
|
||
|
||
class TabItemRow extends StatelessWidget {
|
||
final TabItem item;
|
||
|
||
/// How many of [item] are in this order.
|
||
final int quantity;
|
||
final String? orderId;
|
||
final ItemQuantityChanged onQuantityChanged;
|
||
|
||
const TabItemRow({
|
||
super.key,
|
||
required this.item,
|
||
required this.quantity,
|
||
required this.orderId,
|
||
required this.onQuantityChanged,
|
||
});
|
||
|
||
Future<void> _changeQuantity(BuildContext context, int delta) async {
|
||
final l10n = AppLocalizations.of(context);
|
||
|
||
try {
|
||
await onQuantityChanged(item, delta, orderId);
|
||
} catch (e) {
|
||
if (!context.mounted) return;
|
||
ScaffoldMessenger.of(
|
||
context,
|
||
).showSnackBar(SnackBar(content: Text(l10n.couldNotUpdateQuantity)));
|
||
}
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final scheme = Theme.of(context).colorScheme;
|
||
|
||
return Padding(
|
||
padding: const EdgeInsets.symmetric(vertical: 10),
|
||
child: Row(
|
||
children: [
|
||
Expanded(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
item.productName,
|
||
maxLines: 1,
|
||
overflow: TextOverflow.ellipsis,
|
||
style: const TextStyle(fontWeight: FontWeight.w600),
|
||
),
|
||
const SizedBox(height: 2),
|
||
Text(
|
||
'$quantity × ${item.unitPriceInCents.asEuro}',
|
||
style: Theme.of(context).textTheme.bodySmall,
|
||
),
|
||
],
|
||
),
|
||
),
|
||
Container(
|
||
decoration: BoxDecoration(
|
||
color: scheme.onSurface.withValues(alpha: 0.05),
|
||
borderRadius: BorderRadius.circular(999),
|
||
),
|
||
child: Row(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
IconButton(
|
||
visualDensity: VisualDensity.compact,
|
||
onPressed: () => _changeQuantity(context, -1),
|
||
icon: const Icon(Icons.remove_rounded, size: 18),
|
||
),
|
||
SizedBox(
|
||
width: 22,
|
||
child: Text(
|
||
'$quantity',
|
||
textAlign: TextAlign.center,
|
||
style: const TextStyle(fontWeight: FontWeight.w700),
|
||
),
|
||
),
|
||
IconButton(
|
||
visualDensity: VisualDensity.compact,
|
||
onPressed: () => _changeQuantity(context, 1),
|
||
icon: const Icon(Icons.add_rounded, size: 18),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
SizedBox(
|
||
width: 72,
|
||
child: Text(
|
||
(quantity * item.unitPriceInCents).asEuro,
|
||
textAlign: TextAlign.end,
|
||
style: const TextStyle(fontWeight: FontWeight.w700),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|