srs/lib/features/orders/order_detail_screen.dart
Aswin Biju 749d18192a
All checks were successful
deploy / deploy (push) Successful in 4m58s
refactor: updated UI and other trivial fixes
2026-11-05 10:13:32 +05:30

343 lines
10 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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';
class OrderDetailScreen extends ConsumerStatefulWidget {
const OrderDetailScreen({super.key, required this.orderId});
final int orderId;
@override
ConsumerState<OrderDetailScreen> createState() => _OrderDetailScreenState();
}
class _OrderDetailScreenState extends ConsumerState<OrderDetailScreen> {
bool _full = false;
bool _busy = false;
Future<void> _run(Future<void> Function() fn, {String? done}) async {
if (_busy) return;
setState(() => _busy = true);
try {
await mutate(ref, fn);
if (mounted && done != null) toast(context, done);
} catch (e) {
if (mounted) toast(context, errText(e));
} finally {
if (mounted) setState(() => _busy = false);
}
}
Future<void> _setQty(Order o, EndingQty l, int qty) {
if (qty < 1 || qty > 9999) return Future.value();
return _run(() => ref.read(apiProvider).setLineQty(o.id, l.ending, qty));
}
Future<void> _remove(Order o, EndingQty l) async {
final last = o.lines.length == 1;
final ok = await confirm(
context,
title: last ? 'Remove last ending?' : 'Remove ${l.ending}?',
body: last
? 'This is the only ending in this order. Removing it will delete the order.'
: '${plural(l.qty, 'ticket')} will be removed from this order and the shop totals.',
confirmLabel: 'Remove',
);
if (!ok || !mounted) return;
await _run(
() => ref.read(apiProvider).removeLine(o.id, l.ending),
done: last ? 'Order deleted' : 'Ending removed',
);
if (last && mounted) context.pop();
}
Future<void> _delete(Order o) async {
final ok = await confirm(
context,
title: 'Delete this order?',
body:
'${plural(o.tickets, 'ticket')} will be removed from ${o.shop.name}’s totals and results. This action cannot be undone.',
confirmLabel: 'Delete order',
);
if (!ok || !mounted) return;
await _run(
() => ref.read(apiProvider).cancelOrder(o.id),
done: 'Order deleted',
);
if (mounted) context.pop();
}
@override
Widget build(BuildContext context) {
final order = ref.watch(orderProvider(widget.orderId));
return Scaffold(
body: SafeArea(
child: asyncView(
order,
(o) => Column(
children: [
const ScreenHeader(title: 'Order details', back: true),
Expanded(child: _body(o)),
],
),
retry: () => ref.invalidate(orderProvider(widget.orderId)),
),
),
);
}
Widget _body(Order o) {
final out =
o.lines.any((l) => l.wins) ||
o.drawDate.isBefore(ref.read(apiProvider).now);
final lines = (o.rawText ?? '').split('\n');
final shown = _full ? lines : lines.take(4).toList();
return PaneList(
top: 0,
breakpoint: 820,
startFlex: 1,
endFlex: 1,
gap: S.s24,
start: [
SrsCard(
padding: const EdgeInsets.all(S.s16),
child: Row(
children: [
CodeTile(o.shop.code, size: 52, dark: true),
const SizedBox(width: S.s14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Flexible(
child: Text(
'${inr(o.tickets)} tickets',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: ts(18, w: w7),
),
),
if (o.edited) ...[
const SizedBox(width: S.s8),
const Pill('Edited', bg: C.goldTint, fg: C.goldText),
],
],
),
const SizedBox(height: S.s4),
Text(
'${o.shop.code} · ${time12(o.time)} · ${o.kind}',
style: ts(12, c: C.muted),
),
const SizedBox(height: S.s2),
Text(dateLong(o.drawDate), style: ts(12, c: C.muted)),
],
),
),
if (o.prize > 0)
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
rupees(o.prize),
style: ts(16, w: w7, c: C.green),
),
Text('won', style: ts(11, c: C.muted)),
],
),
],
),
),
const SectionTitle('Original message'),
if (o.isPhoto)
Container(
height: 150,
alignment: Alignment.center,
decoration: BoxDecoration(
color: C.chip,
borderRadius: BorderRadius.circular(R.card),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.photo_outlined, size: 36, color: C.navy),
const SizedBox(height: S.s8),
Text(
'Handwritten slip',
style: ts(13, w: w6, c: C.navy),
),
const SizedBox(height: S.s2),
Text('Received via WhatsApp', style: ts(12, c: C.muted)),
],
),
),
if (o.isPhoto && o.hasText) const SizedBox(height: S.s10),
if (o.hasText)
SrsCard(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
shown.join('\n'),
style: ts(13, w: w5, c: C.ink, h: 1.6),
),
if (lines.length > 4) ...[
const SizedBox(height: S.s10),
Pressable(
onTap: () => setState(() => _full = !_full),
radius: R.sm,
child: Text(
_full ? 'Show less' : 'Show full message',
style: ts(13, w: w6, c: C.navy),
),
),
],
],
),
),
],
end: [
SectionTitle(
'Endings',
trailing:
'${plural(o.lines.length, 'ending')} · ${plural(o.tickets, 'ticket')}',
),
for (final l in o.lines) ...[
_LineRow(
line: l,
busy: _busy,
onMinus: () => _setQty(o, l, l.qty - 1),
onPlus: () => _setQty(o, l, l.qty + 1),
onRemove: () => _remove(o, l),
),
const SizedBox(height: S.s8),
],
const SizedBox(height: S.s6),
Padding(
padding: const EdgeInsets.symmetric(horizontal: S.s4),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Icon(Icons.info_outline_rounded, size: 15, color: C.muted),
const SizedBox(width: S.s8),
Expanded(
child: Text(
out
? 'Changes are reflected in shop totals and results.'
: 'Changes are reflected in shop totals and, once published, in results.',
style: ts(12, c: C.muted, h: 1.45),
),
),
],
),
),
const SizedBox(height: S.s22),
BigButton(
'Delete order',
outline: true,
danger: true,
icon: Icons.delete_outline_rounded,
onPressed: _busy ? null : () => _delete(o),
),
],
);
}
}
class _LineRow extends StatelessWidget {
const _LineRow({
required this.line,
required this.busy,
required this.onMinus,
required this.onPlus,
required this.onRemove,
});
final EndingQty line;
final bool busy;
final VoidCallback onMinus, onPlus, onRemove;
@override
Widget build(BuildContext context) {
final win = line.wins;
return Container(
padding: const EdgeInsets.fromLTRB(S.s14, S.s10, S.s8, S.s10),
decoration: BoxDecoration(
color: win ? C.greenTint : Colors.white,
borderRadius: BorderRadius.circular(R.button),
border: Border.all(color: win ? C.green.withAlpha(110) : C.border),
),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
line.ending,
style: ts(18, w: w7, c: win ? C.green : C.ink, ls: 1),
),
if (win) ...[
const SizedBox(height: S.s2),
Text(
'${line.tierText} · ${rupees(line.prize)}',
style: ts(12, w: w6, c: C.green),
),
],
],
),
),
_step(
Icons.remove_rounded,
'Fewer tickets',
line.qty > 1 && !busy ? onMinus : null,
),
SizedBox(
width: 44,
child: Center(
child: Text('${line.qty}', style: ts(16, w: w7)),
),
),
_step(
Icons.add_rounded,
'More tickets',
line.qty < 9999 && !busy ? onPlus : null,
),
const SizedBox(width: S.s6),
IconButton(
onPressed: busy ? null : onRemove,
icon: const Icon(
Icons.delete_outline_rounded,
size: 21,
color: C.red,
),
tooltip: 'Remove ending',
),
],
),
);
}
Widget _step(IconData icon, String label, VoidCallback? tap) => Pressable(
onTap: tap,
label: label,
radius: 18,
builder: (_, hover) => AnimatedContainer(
duration: const Duration(milliseconds: 140),
width: 36,
height: 36,
decoration: BoxDecoration(
color: tap == null ? C.disabled : (hover ? C.border2 : C.chip),
shape: BoxShape.circle,
),
child: Icon(icon, size: 18, color: tap == null ? C.placeholder : C.navy),
),
);
}