init
This commit is contained in:
@@ -0,0 +1,228 @@
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
import '../config/api_config.dart';
|
||||
import '../models/models.dart';
|
||||
|
||||
class ApiException implements Exception {
|
||||
ApiException(this.message, {this.statusCode});
|
||||
|
||||
final String message;
|
||||
final int? statusCode;
|
||||
|
||||
@override
|
||||
String toString() => message;
|
||||
}
|
||||
|
||||
class ApiClient {
|
||||
ApiClient() {
|
||||
dio = Dio(
|
||||
BaseOptions(
|
||||
baseUrl: resolveApiBaseUrl(),
|
||||
connectTimeout: const Duration(seconds: 15),
|
||||
receiveTimeout: const Duration(seconds: 15),
|
||||
headers: const {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
),
|
||||
);
|
||||
dio.interceptors.add(
|
||||
InterceptorsWrapper(
|
||||
onRequest: (options, handler) {
|
||||
if (token != null && token!.isNotEmpty) {
|
||||
options.headers['Authorization'] = 'Bearer $token';
|
||||
}
|
||||
handler.next(options);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
late final Dio dio;
|
||||
String? token;
|
||||
|
||||
Future<({String token, AppUser user})> staffLogin(String phone, String password) {
|
||||
return _send(
|
||||
dio.post('/auth/staff-login', data: {'phone': phone, 'password': password}),
|
||||
(data) {
|
||||
final map = data as Map<String, dynamic>;
|
||||
return (
|
||||
token: map['token'] as String,
|
||||
user: AppUser.fromJson(map['user'] as Map<String, dynamic>),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<({String requestId, String secret})> dealerLogin(String phone) {
|
||||
return _send(dio.post('/auth/dealer-login', data: {'phone': phone}), (data) {
|
||||
final map = data as Map<String, dynamic>;
|
||||
return (requestId: map['requestId'] as String, secret: map['secret'] as String);
|
||||
});
|
||||
}
|
||||
|
||||
Future<DealerLoginStatus> dealerLoginStatus(String requestId, String secret) {
|
||||
return _send(
|
||||
dio.get('/auth/dealer-login/$requestId', queryParameters: {'secret': secret}),
|
||||
(data) => DealerLoginStatus.fromJson(data as Map<String, dynamic>),
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<LoginRequestItem>> loginRequests() {
|
||||
return _send(dio.get('/auth/login-requests'), (data) => _list(data, LoginRequestItem.fromJson));
|
||||
}
|
||||
|
||||
Future<void> approveLogin(String id) {
|
||||
return _send(dio.post('/auth/login-requests/$id/approve'), (_) {});
|
||||
}
|
||||
|
||||
Future<void> rejectLogin(String id) {
|
||||
return _send(dio.post('/auth/login-requests/$id/reject'), (_) {});
|
||||
}
|
||||
|
||||
Future<AppUser> me() {
|
||||
return _send(
|
||||
dio.get('/auth/me'),
|
||||
(data) => AppUser.fromJson(data as Map<String, dynamic>),
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<Dealer>> dealers() {
|
||||
return _send(dio.get('/dealers'), (data) => _list(data, Dealer.fromJson));
|
||||
}
|
||||
|
||||
Future<List<StockItem>> stocks({String? dealerId}) {
|
||||
return _send(
|
||||
dio.get(
|
||||
'/stocks',
|
||||
queryParameters: {
|
||||
if (dealerId != null && dealerId.isNotEmpty) 'dealerId': dealerId,
|
||||
},
|
||||
),
|
||||
(data) => _list(data, StockItem.fromJson),
|
||||
);
|
||||
}
|
||||
|
||||
Future<StockItem> createStock(Map<String, dynamic> body) {
|
||||
return _send(
|
||||
dio.post('/stocks', data: body),
|
||||
(data) => StockItem.fromJson((data as Map<String, dynamic>)['data'] as Map<String, dynamic>),
|
||||
);
|
||||
}
|
||||
|
||||
Future<StockItem> updateStage(String chassisNumber, Map<String, dynamic> body) {
|
||||
return _send(
|
||||
dio.patch('/stocks/$chassisNumber/stage', data: body),
|
||||
(data) => StockItem.fromJson((data as Map<String, dynamic>)['data'] as Map<String, dynamic>),
|
||||
);
|
||||
}
|
||||
|
||||
Future<StockItem> transferStock(String chassisNumber, String dealerId) {
|
||||
return _send(
|
||||
dio.post('/stocks/$chassisNumber/transfer', data: {'dealerId': dealerId}),
|
||||
(data) => StockItem.fromJson((data as Map<String, dynamic>)['data'] as Map<String, dynamic>),
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<EventPlan>> events({String? dealerId, String? planType}) {
|
||||
return _send(
|
||||
dio.get(
|
||||
'/events',
|
||||
queryParameters: {
|
||||
if (dealerId != null && dealerId.isNotEmpty) 'dealerId': dealerId,
|
||||
if (planType != null && planType.isNotEmpty) 'planType': planType,
|
||||
},
|
||||
),
|
||||
(data) => _list(data, EventPlan.fromJson),
|
||||
);
|
||||
}
|
||||
|
||||
Future<EventPlan> createEvent(Map<String, dynamic> body) {
|
||||
return _send(
|
||||
dio.post('/events', data: body),
|
||||
(data) => EventPlan.fromJson((data as Map<String, dynamic>)['data'] as Map<String, dynamic>),
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<ServicePoint>> servicePoints({String? district, String? search}) {
|
||||
return _send(
|
||||
dio.get(
|
||||
'/service-points',
|
||||
queryParameters: {
|
||||
if (district != null && district.isNotEmpty) 'district': district,
|
||||
if (search != null && search.isNotEmpty) 'search': search,
|
||||
},
|
||||
),
|
||||
(data) => _list(data, ServicePoint.fromJson),
|
||||
);
|
||||
}
|
||||
|
||||
Future<ServicePoint> createServicePoint(Map<String, dynamic> body) {
|
||||
return _send(
|
||||
dio.post('/service-points', data: body),
|
||||
(data) => ServicePoint.fromJson((data as Map<String, dynamic>)['data'] as Map<String, dynamic>),
|
||||
);
|
||||
}
|
||||
|
||||
Future<Dealer> createDealer(Map<String, dynamic> body) {
|
||||
return _send(
|
||||
dio.post('/admin/dealers', data: body),
|
||||
(data) => Dealer.fromJson((data as Map<String, dynamic>)['data'] as Map<String, dynamic>),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> deleteDealer(String id) {
|
||||
return _send(dio.delete('/admin/dealers/$id'), (_) {});
|
||||
}
|
||||
|
||||
Future<List<Dealer>> adminDealers() {
|
||||
return _send(dio.get('/admin/dealers'), (data) => _list(data, Dealer.fromJson));
|
||||
}
|
||||
|
||||
Future<List<AppUser>> salesReps() {
|
||||
return _send(dio.get('/admin/sales-reps'), (data) => _list(data, AppUser.fromJson));
|
||||
}
|
||||
|
||||
Future<AppUser> createSalesRep(Map<String, dynamic> body) {
|
||||
return _send(
|
||||
dio.post('/admin/sales-reps', data: body),
|
||||
(data) => AppUser.fromJson((data as Map<String, dynamic>)['data'] as Map<String, dynamic>),
|
||||
);
|
||||
}
|
||||
|
||||
Future<AppUser> assignDealer({
|
||||
required String salesRepId,
|
||||
required String dealerId,
|
||||
required bool assigned,
|
||||
}) {
|
||||
return _send(
|
||||
dio.patch(
|
||||
'/admin/assign-dealer',
|
||||
data: {
|
||||
'salesRepId': salesRepId,
|
||||
'dealerId': dealerId,
|
||||
'assigned': assigned,
|
||||
},
|
||||
),
|
||||
(data) => AppUser.fromJson((data as Map<String, dynamic>)['data'] as Map<String, dynamic>),
|
||||
);
|
||||
}
|
||||
|
||||
Future<T> _send<T>(Future<Response<dynamic>> request, T Function(dynamic data) parse) async {
|
||||
try {
|
||||
final response = await request;
|
||||
return parse(response.data);
|
||||
} on DioException catch (error) {
|
||||
final data = error.response?.data;
|
||||
final message = data is Map && data['message'] is String
|
||||
? data['message'] as String
|
||||
: 'Network request failed';
|
||||
throw ApiException(message, statusCode: error.response?.statusCode);
|
||||
}
|
||||
}
|
||||
|
||||
List<T> _list<T>(dynamic data, T Function(Map<String, dynamic>) parse) {
|
||||
final raw = (data as Map<String, dynamic>)['data'] as List<dynamic>;
|
||||
return raw.map((item) => parse(item as Map<String, dynamic>)).toList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
|
||||
import '../models/models.dart';
|
||||
import 'api_client.dart';
|
||||
|
||||
class AuthController extends ChangeNotifier {
|
||||
AuthController({FlutterSecureStorage? storage, ApiClient? apiClient})
|
||||
: _storage = storage ?? const FlutterSecureStorage(),
|
||||
api = apiClient ?? ApiClient();
|
||||
|
||||
final FlutterSecureStorage _storage;
|
||||
final ApiClient api;
|
||||
|
||||
AppUser? user;
|
||||
bool ready = false;
|
||||
bool busy = false;
|
||||
String? error;
|
||||
|
||||
Future<void> bootstrap() async {
|
||||
try {
|
||||
final stored = await _storage.read(key: 'jwt_token');
|
||||
if (stored == null || stored.isEmpty) return;
|
||||
api.token = stored;
|
||||
user = await api.me();
|
||||
} catch (_) {
|
||||
api.token = null;
|
||||
user = null;
|
||||
await _storage.delete(key: 'jwt_token');
|
||||
} finally {
|
||||
ready = true;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> staffLogin(String phone, String password) async {
|
||||
await _run(() async {
|
||||
final result = await api.staffLogin(phone, password);
|
||||
await _storeSession(result.token, result.user);
|
||||
});
|
||||
}
|
||||
|
||||
Future<({String requestId, String secret})> startDealerLogin(String phone) async {
|
||||
late ({String requestId, String secret}) started;
|
||||
await _run(() async {
|
||||
started = await api.dealerLogin(phone);
|
||||
});
|
||||
return started;
|
||||
}
|
||||
|
||||
Future<String> pollDealerLogin(String requestId, String secret) async {
|
||||
final status = await api.dealerLoginStatus(requestId, secret);
|
||||
if (status.status == 'approved' && status.token != null && status.user != null) {
|
||||
await _storeSession(status.token!, status.user!);
|
||||
notifyListeners();
|
||||
} else if (status.status == 'rejected') {
|
||||
error = 'Login was rejected';
|
||||
notifyListeners();
|
||||
} else if (status.status == 'expired' || status.status == 'consumed') {
|
||||
error = 'Login request expired. Try again.';
|
||||
notifyListeners();
|
||||
}
|
||||
return status.status;
|
||||
}
|
||||
|
||||
Future<void> _storeSession(String token, AppUser profile) async {
|
||||
api.token = token;
|
||||
await _storage.write(key: 'jwt_token', value: token);
|
||||
user = profile;
|
||||
}
|
||||
|
||||
Future<void> refreshProfile() async {
|
||||
if (api.token == null) return;
|
||||
user = await api.me();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> logout() async {
|
||||
api.token = null;
|
||||
user = null;
|
||||
error = null;
|
||||
await _storage.delete(key: 'jwt_token');
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> _run(Future<void> Function() action) async {
|
||||
busy = true;
|
||||
error = null;
|
||||
notifyListeners();
|
||||
try {
|
||||
await action();
|
||||
} on ApiException catch (error) {
|
||||
this.error = error.message;
|
||||
rethrow;
|
||||
} catch (error) {
|
||||
this.error = 'Network request failed';
|
||||
rethrow;
|
||||
} finally {
|
||||
busy = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user