196 lines
5.5 KiB
Dart
196 lines
5.5 KiB
Dart
import 'package:flutter/material.dart';
|
||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||
import 'package:go_router/go_router.dart';
|
||
|
||
import '../../core/format.dart';
|
||
import '../../core/models.dart';
|
||
import '../../core/providers.dart';
|
||
import '../../core/theme.dart';
|
||
import '../../widgets/ui.dart';
|
||
import '../orders/order_card.dart';
|
||
|
||
class TicketsScreen extends ConsumerStatefulWidget {
|
||
const TicketsScreen({super.key, required this.shopId});
|
||
final int shopId;
|
||
|
||
@override
|
||
ConsumerState<TicketsScreen> createState() => _TicketsScreenState();
|
||
}
|
||
|
||
class _TicketsScreenState extends ConsumerState<TicketsScreen> {
|
||
bool _byEnding = true;
|
||
String _q = '';
|
||
|
||
void _mode(bool byEnding) => setState(() {
|
||
_byEnding = byEnding;
|
||
_q = '';
|
||
});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final date = ref.watch(selectedDateProvider);
|
||
final detail = ref.watch(shopDetailProvider((widget.shopId, date)));
|
||
return Scaffold(
|
||
body: SafeArea(
|
||
child: asyncView(
|
||
detail,
|
||
(d) => Column(
|
||
children: [
|
||
ScreenHeader(
|
||
title: '${d.shop.name} tickets',
|
||
subtitle: '${plural(d.tickets, 'ticket')} · ${dateShort(date)}',
|
||
back: true,
|
||
),
|
||
PagePad(
|
||
top: S.s4,
|
||
bottom: S.s10,
|
||
child: Container(
|
||
constraints: const BoxConstraints(maxWidth: 420),
|
||
padding: const EdgeInsets.all(S.s4),
|
||
decoration: BoxDecoration(
|
||
color: C.chip,
|
||
borderRadius: BorderRadius.circular(R.input),
|
||
),
|
||
child: Row(
|
||
children: [
|
||
_seg('By ending', _byEnding, () => _mode(true)),
|
||
_seg('By order', !_byEnding, () => _mode(false)),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
PagePad(
|
||
bottom: S.s8,
|
||
child: SearchField(
|
||
key: ValueKey(_byEnding),
|
||
hint: _byEnding
|
||
? 'Search endings (e.g. 0987)'
|
||
: 'Search message or ending',
|
||
onChanged: (v) => setState(() => _q = v.trim()),
|
||
),
|
||
),
|
||
Expanded(child: _byEnding ? _endings(d) : _orders(d)),
|
||
],
|
||
),
|
||
retry: () =>
|
||
ref.invalidate(shopDetailProvider((widget.shopId, date))),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _seg(String t, bool on, VoidCallback tap) => Expanded(
|
||
child: Pressable(
|
||
onTap: tap,
|
||
radius: R.md,
|
||
builder: (_, hover) => AnimatedContainer(
|
||
duration: const Duration(milliseconds: 160),
|
||
height: 38,
|
||
alignment: Alignment.center,
|
||
decoration: BoxDecoration(
|
||
color: on ? Colors.white : (hover ? C.hover : Colors.transparent),
|
||
borderRadius: BorderRadius.circular(R.md),
|
||
),
|
||
child: Text(
|
||
t,
|
||
style: ts(13, w: on ? w7 : w5, c: on ? C.navy : C.muted),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
Widget _endings(ShopDetail d) {
|
||
final list = d.endings
|
||
.where((e) => _q.isEmpty || e.ending.contains(_q))
|
||
.toList();
|
||
if (list.isEmpty) {
|
||
return ListView(
|
||
children: const [
|
||
EmptyState(
|
||
icon: Icons.search_off_rounded,
|
||
title: 'No matching endings',
|
||
),
|
||
],
|
||
);
|
||
}
|
||
return PageList(
|
||
children: [
|
||
LayoutBuilder(
|
||
builder: (context, c) {
|
||
const gap = 8.0, min = 104.0;
|
||
final cols = ((c.maxWidth + gap) / (min + gap)).floor().clamp(
|
||
1,
|
||
12,
|
||
);
|
||
final w = (c.maxWidth - gap * (cols - 1)) / cols;
|
||
return Wrap(
|
||
alignment: WrapAlignment.center,
|
||
spacing: gap,
|
||
runSpacing: gap,
|
||
children: [for (final e in list) _chip(e, w)],
|
||
);
|
||
},
|
||
),
|
||
],
|
||
);
|
||
}
|
||
|
||
Widget _chip(EndingQty e, double width) {
|
||
final win = e.wins;
|
||
return Container(
|
||
width: width,
|
||
alignment: Alignment.center,
|
||
padding: const EdgeInsets.symmetric(horizontal: S.s8, vertical: S.s10),
|
||
decoration: BoxDecoration(
|
||
color: win ? C.greenTint : Colors.white,
|
||
borderRadius: BorderRadius.circular(R.md),
|
||
border: Border.all(color: win ? C.green : C.border),
|
||
),
|
||
child: Row(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
Text(
|
||
e.ending,
|
||
style: ts(14, w: w7, c: win ? C.green : C.ink, ls: 0.4),
|
||
),
|
||
const SizedBox(width: S.s6),
|
||
Text(
|
||
'×${inr(e.qty)}',
|
||
style: ts(12, w: w6, c: win ? C.green : C.muted),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _orders(ShopDetail d) {
|
||
final q = _q.toLowerCase();
|
||
final orders = [
|
||
for (final o in d.orders)
|
||
if (o.matches(q)) o,
|
||
];
|
||
if (orders.isEmpty) {
|
||
return ListView(
|
||
children: [
|
||
EmptyState(
|
||
icon: q.isEmpty
|
||
? Icons.receipt_long_outlined
|
||
: Icons.search_off_rounded,
|
||
title: q.isEmpty ? 'No orders' : 'No matching orders',
|
||
),
|
||
],
|
||
);
|
||
}
|
||
return PageList(
|
||
children: [
|
||
CardGrid(
|
||
children: [
|
||
for (final o in orders)
|
||
OrderCard(order: o, onTap: () => context.push('/orders/${o.id}')),
|
||
],
|
||
),
|
||
],
|
||
);
|
||
}
|
||
}
|