srs/lib/core/http_api.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

365 lines
10 KiB
Dart

import 'dart:async';
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart';
import 'api.dart';
import 'format.dart';
import 'models.dart';
class HttpApi implements Api {
HttpApi({
required String baseUrl,
required SharedPreferences prefs,
http.Client? client,
this.timeout = const Duration(seconds: 20),
}) : _base = baseUrl.replaceAll(RegExp(r'/+$'), ''),
_prefs = prefs,
_client = client ?? http.Client();
static const _tokenKey = 'token';
static const _unreachable =
'Unable to reach the server. Check your connection and try again.';
final String _base;
final SharedPreferences _prefs;
final http.Client _client;
final Duration timeout;
final _ended = StreamController<void>.broadcast();
@override
DateTime get now => istWallClock(DateTime.now());
@override
Stream<void> get sessionEnded => _ended.stream;
// ---- transport ---------------------------------------------------------
Uri _uri(String path, [Map<String, String>? query]) {
final origin = _base.isEmpty ? Uri.base.origin : _base;
return Uri.parse('$origin/api$path').replace(queryParameters: query);
}
/// Sends a request and returns the decoded JSON body (null for 204).
/// Any failure becomes an [ApiException] whose message can be shown as is.
Future<Object?> _send(
String method,
String path, {
Map<String, String>? query,
Object? body,
bool signedIn = true,
}) async {
final request = http.Request(method, _uri(path, query));
final token = _prefs.getString(_tokenKey);
if (signedIn && token != null) {
request.headers['authorization'] = 'Bearer $token';
}
if (body != null) {
request.headers['content-type'] = 'application/json';
request.body = jsonEncode(body);
}
final http.Response response;
try {
response = await http.Response.fromStream(
await _client.send(request).timeout(timeout),
);
} on Exception {
throw ApiException(_unreachable);
}
if (response.statusCode >= 200 && response.statusCode < 300) {
if (response.body.isEmpty) return null;
try {
return jsonDecode(response.body);
} on FormatException {
throw ApiException(_unreachable);
}
}
if (response.statusCode == 401 && signedIn) _endSession();
throw ApiException(_errorMessage(response));
}
String _errorMessage(http.Response r) {
try {
final m = (jsonDecode(r.body) as Map<String, dynamic>)['message'];
if (m is String && m.isNotEmpty) return m;
} on Object {
// ignore: avoid_catching_errors
}
return r.statusCode >= 500
? 'Something went wrong on the server. Try again shortly.'
: 'The request could not be completed.';
}
void _endSession() {
_prefs.remove(_tokenKey);
_ended.add(null);
}
Future<Map<String, dynamic>> _object(
String method,
String path, {
Map<String, String>? query,
Object? body,
bool signedIn = true,
}) async =>
await _send(method, path, query: query, body: body, signedIn: signedIn)
as Map<String, dynamic>;
Future<List<Map<String, dynamic>>> _list(
String path,
String key, {
Map<String, String>? query,
}) async {
final json = await _object('GET', path, query: query);
return (json[key] as List).cast<Map<String, dynamic>>();
}
// ---- mapping -----------------------------------------------------------
AppUser _user(Map<String, dynamic> j) => AppUser(
id: j['id'] as int,
username: j['username'] as String,
phone: j['phone'] as String?,
isAdmin: j['isAdmin'] as bool,
active: j['active'] as bool,
shopIds: (j['shopIds'] as List).cast<int>(),
sendResults: j['sendResults'] as bool? ?? false,
);
Shop _shop(Map<String, dynamic> j) => Shop(
id: j['id'] as int,
code: j['code'] as String,
name: j['name'] as String,
active: j['active'] as bool,
);
DrawInfo _draw(Map<String, dynamic> j) => DrawInfo(
date: parseDate(j['date'] as String),
scheme: j['scheme'] as String?,
code: j['code'] as String?,
published: j['published'] as bool,
hasWin: j['hasWin'] as bool,
);
EndingQty _line(Map<String, dynamic> j) => EndingQty(
ending: j['ending'] as String,
qty: j['qty'] as int,
hits: [
for (final h in (j['hits'] as List).cast<Map<String, dynamic>>())
PrizeHit(tier: h['tier'] as int, amount: h['amount'] as int),
],
);
Order _order(Map<String, dynamic> j) => Order(
id: j['id'] as int,
shop: _shop(j['shop'] as Map<String, dynamic>),
drawDate: parseDate(j['drawDate'] as String),
time: istWallClock(DateTime.parse(j['createdAt'] as String)),
isPhoto: j['isPhoto'] as bool,
edited: j['edited'] as bool,
rawText: j['rawText'] as String?,
lines: [
for (final l in (j['lines'] as List).cast<Map<String, dynamic>>())
_line(l),
],
);
String _day(DateTime d) => isoDate(d);
// ---- auth --------------------------------------------------------------
@override
Future<DateTime> latestDrawDate() async =>
parseDate((await _object('GET', '/draws/latest'))['date'] as String);
@override
Future<AppUser> login(String username, String password) async {
final json = await _object(
'POST',
'/auth/login',
body: {'username': username.trim(), 'password': password},
signedIn: false,
);
await _prefs.setString(_tokenKey, json['token'] as String);
return _user(json['user'] as Map<String, dynamic>);
}
@override
Future<AppUser?> restore() async {
if (_prefs.getString(_tokenKey) == null) return null;
try {
return _user(await _object('GET', '/me'));
} on ApiException {
return null;
}
}
@override
void logout() => _prefs.remove(_tokenKey);
// ---- results -----------------------------------------------------------
@override
Future<List<DrawInfo>> month(int year, int month) async {
final text = '$year-${month.toString().padLeft(2, '0')}';
return [
for (final d in await _list('/draws', 'days', query: {'month': text}))
_draw(d),
];
}
@override
Future<DrawResults> results(DateTime date) async {
final json = await _object('GET', '/draws/${_day(date)}/results');
return DrawResults(
draw: _draw(json['draw'] as Map<String, dynamic>),
shops: [
for (final s in (json['shops'] as List).cast<Map<String, dynamic>>())
ShopResult(
shop: _shop(s['shop'] as Map<String, dynamic>),
tickets: s['tickets'] as int,
orders: s['orders'] as int,
winTickets: s['winTickets'] as int,
prize: s['prize'] as int,
),
],
);
}
// ---- shops -------------------------------------------------------------
@override
Future<List<ShopDay>> shops(DateTime date) async => [
for (final s in await _list('/shops', 'shops', query: {'date': _day(date)}))
ShopDay(
shop: _shop(s['shop'] as Map<String, dynamic>),
tickets: s['tickets'] as int,
orders: s['orders'] as int,
endings: s['endings'] as int,
),
];
@override
Future<ShopDetail> shopDetail(int shopId, DateTime date) async {
final json = await _object('GET', '/shops/$shopId/draws/${_day(date)}');
return ShopDetail(
shop: _shop(json['shop'] as Map<String, dynamic>),
draw: _draw(json['draw'] as Map<String, dynamic>),
endings: [
for (final e in (json['endings'] as List).cast<Map<String, dynamic>>())
_line(e),
],
orders: [
for (final o in (json['orders'] as List).cast<Map<String, dynamic>>())
_order(o),
],
);
}
@override
Future<List<Shop>> activeShops() async => [
for (final s in await _list('/shops/options', 'shops')) _shop(s),
];
@override
Future<Shop> createShop(String name, String code) async => _shop(
await _object('POST', '/shops', body: {'name': name, 'code': code}),
);
@override
Future<void> renameShop(int shopId, String name) =>
_send('PATCH', '/shops/$shopId', body: {'name': name});
@override
Future<void> setShopActive(int shopId, bool active) =>
_send('PATCH', '/shops/$shopId', body: {'active': active});
// ---- orders ------------------------------------------------------------
@override
Future<List<Order>> orders(DateTime date) async => [
for (final o in await _list(
'/orders',
'orders',
query: {'date': _day(date)},
))
_order(o),
];
@override
Future<Order> order(int orderId) async =>
_order(await _object('GET', '/orders/$orderId'));
@override
Future<void> setLineQty(int orderId, String ending, int qty) =>
_send('PUT', '/orders/$orderId/lines/$ending', body: {'qty': qty});
@override
Future<void> removeLine(int orderId, String ending) =>
_send('DELETE', '/orders/$orderId/lines/$ending');
@override
Future<void> cancelOrder(int orderId) => _send('DELETE', '/orders/$orderId');
// ---- users -------------------------------------------------------------
@override
Future<List<AppUser>> users() async => [
for (final u in await _list('/users', 'users')) _user(u),
];
@override
Future<AppUser> user(int id) async =>
_user(await _object('GET', '/users/$id'));
@override
Future<AppUser> createUser({
required String username,
String? phone,
required String password,
required List<int> shopIds,
}) async => _user(
await _object(
'POST',
'/users',
body: {
'username': username,
'phone': phone,
'password': password,
'shopIds': shopIds,
},
),
);
@override
Future<void> updateUser(
int id, {
String? username,
String? phone,
bool clearPhone = false,
String? password,
bool? active,
bool? sendResults,
}) => _send(
'PATCH',
'/users/$id',
body: {
'username': ?username,
if (clearPhone) 'phone': null else 'phone': ?phone,
'password': ?password,
'active': ?active,
'sendResults': ?sendResults,
},
);
@override
Future<void> setUserShops(int id, List<int> shopIds) =>
_send('PUT', '/users/$id/shops', body: {'shopIds': shopIds});
@override
Future<void> deleteUser(int id) => _send('DELETE', '/users/$id');
}