Add SRS app: results, shops, orders and users, wired to srs-api
Flutter web/PWA for the lottery agency, built on Riverpod and go_router,
talking to srs-api over HTTP (HttpApi, token kept in SharedPreferences).
- Results, Shops, Orders and Users tabs (Users for admins only), with
phone, tablet and desktop layouts and shared widgets and theme tokens
- Users: list with search and Active/Inactive filters; user detail with
shops, sign-in details, and an Access card
- WhatsApp results switch on each user (sendResults on
PATCH /api/users/:id). Shows the WhatsApp logo (Simple Icons, CC0,
drawn with flutter_svg), green only when results will really go out.
Can't be turned on without a phone number ("Add a phone number first"
opens the phone sheet), and is paused while the user is inactive. The
users list marks people who get results with the logo.
- Each Access switch has a one-line description; the explanation
paragraphs are gone
- Remove demo mode: the app always uses the real AP
test/support and is used only by tests.
118 lines
3.2 KiB
Dart
118 lines
3.2 KiB
Dart
const _weekdays = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
|
|
const _months = [
|
|
'Jan',
|
|
'Feb',
|
|
'Mar',
|
|
'Apr',
|
|
'May',
|
|
'Jun',
|
|
'Jul',
|
|
'Aug',
|
|
'Sep',
|
|
'Oct',
|
|
'Nov',
|
|
'Dec',
|
|
];
|
|
const _monthsLong = [
|
|
'January',
|
|
'February',
|
|
'March',
|
|
'April',
|
|
'May',
|
|
'June',
|
|
'July',
|
|
'August',
|
|
'September',
|
|
'October',
|
|
'November',
|
|
'December',
|
|
];
|
|
|
|
String inr(int n) {
|
|
final neg = n < 0;
|
|
final s = n.abs().toString();
|
|
if (s.length <= 3) return '${neg ? '-' : ''}$s';
|
|
final last = s.substring(s.length - 3);
|
|
var rest = s.substring(0, s.length - 3);
|
|
final parts = <String>[];
|
|
while (rest.length > 2) {
|
|
parts.insert(0, rest.substring(rest.length - 2));
|
|
rest = rest.substring(0, rest.length - 2);
|
|
}
|
|
if (rest.isNotEmpty) parts.insert(0, rest);
|
|
return '${neg ? '-' : ''}${parts.join(',')},$last';
|
|
}
|
|
|
|
String rupees(int n) => '₹${inr(n)}';
|
|
|
|
DateTime dateOnly(DateTime d) => DateTime(d.year, d.month, d.day);
|
|
bool sameDay(DateTime a, DateTime b) =>
|
|
a.year == b.year && a.month == b.month && a.day == b.day;
|
|
DateTime addDays(DateTime d, int n) => DateTime(d.year, d.month, d.day + n);
|
|
|
|
/// Fri, 16 Oct 2026
|
|
String dateLong(DateTime d) =>
|
|
'${_weekdays[d.weekday - 1]}, ${d.day} ${_months[d.month - 1]} ${d.year}';
|
|
|
|
/// Fri, 16 Oct
|
|
String dateShort(DateTime d) =>
|
|
'${_weekdays[d.weekday - 1]}, ${d.day} ${_months[d.month - 1]}';
|
|
|
|
/// October 2026
|
|
String monthYear(DateTime d) => '${_monthsLong[d.month - 1]} ${d.year}';
|
|
|
|
/// 12:41 PM
|
|
String time12(DateTime d) {
|
|
final h = d.hour % 12 == 0 ? 12 : d.hour % 12;
|
|
final m = d.minute.toString().padLeft(2, '0');
|
|
return '$h:$m ${d.hour >= 12 ? 'PM' : 'AM'}';
|
|
}
|
|
|
|
String isoDate(DateTime d) =>
|
|
'${d.year.toString().padLeft(4, '0')}-${d.month.toString().padLeft(2, '0')}-${d.day.toString().padLeft(2, '0')}';
|
|
|
|
String plural(int n, String one, [String? many]) =>
|
|
'${inr(n)} ${n == 1 ? one : (many ?? '${one}s')}';
|
|
|
|
/// 919000012345 -> +91 90000 12345
|
|
String fmtPhone(String? digits) {
|
|
if (digits == null || digits.isEmpty) return '';
|
|
if (digits.length == 12 && digits.startsWith('91')) {
|
|
return '+91 ${digits.substring(2, 7)} ${digits.substring(7)}';
|
|
}
|
|
return '+$digits';
|
|
}
|
|
|
|
String initials(String name) {
|
|
final parts = name.split(RegExp(r'[._\- ]+')).where((p) => p.isNotEmpty);
|
|
final list = parts.toList();
|
|
if (list.isEmpty) return '?';
|
|
if (list.length == 1) {
|
|
final w = list.first;
|
|
return w.substring(0, w.length >= 2 ? 2 : 1).toUpperCase();
|
|
}
|
|
return (list[0][0] + list[1][0]).toUpperCase();
|
|
}
|
|
|
|
String tierLabel(int tier) => '${tier}th prize';
|
|
|
|
/// "5th prize", or "5th + 8th prize" for an ending in two tiers.
|
|
String tiersLabel(List<int> tiers) {
|
|
final sorted = [...tiers]..sort();
|
|
if (sorted.length < 2) return sorted.isEmpty ? '' : tierLabel(sorted.first);
|
|
return '${sorted.map((t) => '${t}th').join(' + ')} prize';
|
|
}
|
|
|
|
/// A server instant (UTC) as the Indian wall-clock time the screens show.
|
|
DateTime istWallClock(DateTime instant) {
|
|
final t = instant.toUtc().add(const Duration(hours: 5, minutes: 30));
|
|
return DateTime(t.year, t.month, t.day, t.hour, t.minute, t.second);
|
|
}
|
|
|
|
/// Parses a YYYY-MM-DD calendar date.
|
|
DateTime parseDate(String iso) => DateTime(
|
|
int.parse(iso.substring(0, 4)),
|
|
int.parse(iso.substring(5, 7)),
|
|
int.parse(iso.substring(8, 10)),
|
|
);
|