This commit is contained in:
2026-10-01 16:03:32 +05:30
commit 3db47c4648
102 changed files with 5067 additions and 0 deletions
+228
View File
@@ -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();
}
}
+103
View File
@@ -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();
}
}
}