Update: 2026-08-08 12:58:16
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
// fuel_admin_controller.dart — تحكم شاشات محفظة الوقود (لوحة إدارة سيرو)
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import 'fuel_admin_models.dart';
|
||||
import 'fuel_admin_service.dart';
|
||||
|
||||
class FuelAdminController extends GetxController {
|
||||
bool isLoading = false;
|
||||
List<FuelStation> stations = [];
|
||||
|
||||
/// حالة المنتج — لا حالة الشاشة. الشاشة تعمل دائماً؛ المنتج قد يكون
|
||||
/// مُجهَّزاً وغير مُشغَّل حتى يتم الاتفاق مع سلسلة المحطات.
|
||||
bool productActive = false;
|
||||
|
||||
String searchQuery = '';
|
||||
bool isSaving = false;
|
||||
|
||||
bool isLoadingDrawdowns = false;
|
||||
FuelDrawdownsPage? drawdowns;
|
||||
|
||||
String? lastError;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
fetchStations();
|
||||
}
|
||||
|
||||
Future<void> fetchStations() async {
|
||||
isLoading = true;
|
||||
update();
|
||||
|
||||
final res = await FuelAdminService.listStations(search: searchQuery);
|
||||
if (res.success && res.data != null) {
|
||||
stations = res.data!.stations;
|
||||
productActive = res.data!.productActive;
|
||||
lastError = null;
|
||||
} else {
|
||||
lastError = res.message;
|
||||
}
|
||||
|
||||
isLoading = false;
|
||||
update();
|
||||
}
|
||||
|
||||
/// يُرجع المفتاح الجديد عند الإنشاء، أو null عند التعديل/الفشل.
|
||||
///
|
||||
/// المفتاح يظهر مرة واحدة فقط — مخزَّن مُهشَّراً على الخادم ولا سبيل
|
||||
/// لاستعادته. الشاشة مسؤولة عن عرضه فوراً قبل أي انتقال.
|
||||
Future<String?> saveStation({
|
||||
int? stationId,
|
||||
required String nameAr,
|
||||
String? chainName,
|
||||
String? city,
|
||||
String? address,
|
||||
double? lat,
|
||||
double? lng,
|
||||
String? phone,
|
||||
required double discountPercent,
|
||||
required bool isActive,
|
||||
}) async {
|
||||
isSaving = true;
|
||||
update();
|
||||
|
||||
final res = await FuelAdminService.saveStation(
|
||||
stationId: stationId,
|
||||
nameAr: nameAr,
|
||||
chainName: chainName,
|
||||
city: city,
|
||||
address: address,
|
||||
lat: lat,
|
||||
lng: lng,
|
||||
phone: phone,
|
||||
discountPercent: discountPercent,
|
||||
isActive: isActive,
|
||||
);
|
||||
|
||||
isSaving = false;
|
||||
lastError = res.success ? null : res.message;
|
||||
update();
|
||||
|
||||
if (!res.success) return null;
|
||||
|
||||
await fetchStations();
|
||||
return res.data?['api_key']?.toString();
|
||||
}
|
||||
|
||||
Future<String?> rotateKey(int stationId) async {
|
||||
final res = await FuelAdminService.rotateKey(stationId);
|
||||
if (!res.success) {
|
||||
lastError = res.message;
|
||||
update();
|
||||
return null;
|
||||
}
|
||||
return res.data?['api_key']?.toString();
|
||||
}
|
||||
|
||||
/// تشغيل المنتج أو إيقافه. الخادم يرفض التشغيل بلا محطة فعّالة واحدة —
|
||||
/// سائق يفعّل محفظته ثم لا يجد أين يصرف قسيمته تجربةٌ تُفقد الثقة.
|
||||
Future<bool> toggleProduct(bool value) async {
|
||||
final res = await FuelAdminService.toggleProduct(value);
|
||||
if (res.success) {
|
||||
productActive = res.data ?? value;
|
||||
lastError = null;
|
||||
update();
|
||||
return true;
|
||||
}
|
||||
lastError = res.message;
|
||||
update();
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<void> fetchDrawdowns({int? stationId, String? status}) async {
|
||||
isLoadingDrawdowns = true;
|
||||
update();
|
||||
|
||||
final res =
|
||||
await FuelAdminService.listDrawdowns(stationId: stationId, status: status);
|
||||
if (res.success) {
|
||||
drawdowns = res.data;
|
||||
lastError = null;
|
||||
} else {
|
||||
lastError = res.message;
|
||||
}
|
||||
|
||||
isLoadingDrawdowns = false;
|
||||
update();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
// fuel_admin_models.dart — نماذج بيانات محفظة الوقود (لوحة إدارة سيرو)
|
||||
|
||||
class FuelStation {
|
||||
final int id;
|
||||
final String nameAr;
|
||||
final String? chainName;
|
||||
final String? city;
|
||||
final String? address;
|
||||
final double? lat;
|
||||
final double? lng;
|
||||
final String? phone;
|
||||
final double discountPercent;
|
||||
final bool isActive;
|
||||
|
||||
/// أرقام الأداء: محطة بلا صرف اسمٌ في جدول لا شريك يعمل.
|
||||
final int redeemCount;
|
||||
final double redeemTotal;
|
||||
final String? lastRedeemAt;
|
||||
|
||||
FuelStation({
|
||||
required this.id,
|
||||
required this.nameAr,
|
||||
required this.discountPercent,
|
||||
required this.isActive,
|
||||
this.chainName,
|
||||
this.city,
|
||||
this.address,
|
||||
this.lat,
|
||||
this.lng,
|
||||
this.phone,
|
||||
this.redeemCount = 0,
|
||||
this.redeemTotal = 0,
|
||||
this.lastRedeemAt,
|
||||
});
|
||||
|
||||
factory FuelStation.fromJson(Map<String, dynamic> j) => FuelStation(
|
||||
id: int.tryParse(j['id'].toString()) ?? 0,
|
||||
nameAr: j['name_ar']?.toString() ?? '',
|
||||
chainName: j['chain_name']?.toString(),
|
||||
city: j['city']?.toString(),
|
||||
address: j['address']?.toString(),
|
||||
lat: double.tryParse(j['lat']?.toString() ?? ''),
|
||||
lng: double.tryParse(j['lng']?.toString() ?? ''),
|
||||
phone: j['phone']?.toString(),
|
||||
discountPercent:
|
||||
double.tryParse(j['discount_percent']?.toString() ?? '0') ?? 0,
|
||||
isActive: j['is_active'] == true,
|
||||
redeemCount: int.tryParse(j['redeem_count']?.toString() ?? '0') ?? 0,
|
||||
redeemTotal: double.tryParse(j['redeem_total']?.toString() ?? '0') ?? 0,
|
||||
lastRedeemAt: j['last_redeem_at']?.toString(),
|
||||
);
|
||||
}
|
||||
|
||||
class FuelStationsPage {
|
||||
final List<FuelStation> stations;
|
||||
|
||||
/// حالة المنتج نفسه. قرار المالك: تُجهَّز الشاشات ولا يُشغَّل المنتج إلا
|
||||
/// بعد إتمام الاتفاق مع سلسلة المحطات — فالشاشة تعمل كاملةً بينما
|
||||
/// المنتج مُطفأ، والفرق يجب أن يكون ظاهراً لا مخفياً.
|
||||
final bool productActive;
|
||||
|
||||
FuelStationsPage({required this.stations, required this.productActive});
|
||||
|
||||
factory FuelStationsPage.fromJson(Map<String, dynamic> j) => FuelStationsPage(
|
||||
stations: (j['stations'] is List)
|
||||
? (j['stations'] as List)
|
||||
.map((s) => FuelStation.fromJson(Map<String, dynamic>.from(s)))
|
||||
.toList()
|
||||
: <FuelStation>[],
|
||||
productActive: j['product_active'] == true,
|
||||
);
|
||||
}
|
||||
|
||||
class FuelVoucher {
|
||||
final int id;
|
||||
final String driverId;
|
||||
final String code;
|
||||
final double amountAuthorized;
|
||||
final double amountRedeemed;
|
||||
final String currency;
|
||||
final String status;
|
||||
final String? station;
|
||||
final String? redeemedAt;
|
||||
final String? createdAt;
|
||||
|
||||
FuelVoucher({
|
||||
required this.id,
|
||||
required this.driverId,
|
||||
required this.code,
|
||||
required this.amountAuthorized,
|
||||
required this.amountRedeemed,
|
||||
required this.currency,
|
||||
required this.status,
|
||||
this.station,
|
||||
this.redeemedAt,
|
||||
this.createdAt,
|
||||
});
|
||||
|
||||
factory FuelVoucher.fromJson(Map<String, dynamic> j) => FuelVoucher(
|
||||
id: int.tryParse(j['id'].toString()) ?? 0,
|
||||
driverId: j['driver_id']?.toString() ?? '',
|
||||
code: j['code']?.toString() ?? '',
|
||||
amountAuthorized:
|
||||
double.tryParse(j['amount_authorized']?.toString() ?? '0') ?? 0,
|
||||
amountRedeemed:
|
||||
double.tryParse(j['amount_redeemed']?.toString() ?? '0') ?? 0,
|
||||
currency: j['currency']?.toString() ?? '',
|
||||
status: j['status']?.toString() ?? '',
|
||||
station: j['station']?.toString(),
|
||||
redeemedAt: j['redeemed_at']?.toString(),
|
||||
createdAt: j['created_at']?.toString(),
|
||||
);
|
||||
}
|
||||
|
||||
class FuelDrawdownsPage {
|
||||
final List<FuelVoucher> vouchers;
|
||||
final double lentTotal;
|
||||
final double collectedTotal;
|
||||
final double outstandingTotal;
|
||||
final int driversCount;
|
||||
|
||||
FuelDrawdownsPage({
|
||||
required this.vouchers,
|
||||
required this.lentTotal,
|
||||
required this.collectedTotal,
|
||||
required this.outstandingTotal,
|
||||
required this.driversCount,
|
||||
});
|
||||
|
||||
factory FuelDrawdownsPage.fromJson(Map<String, dynamic> j) {
|
||||
final s = Map<String, dynamic>.from(j['summary'] ?? {});
|
||||
return FuelDrawdownsPage(
|
||||
vouchers: (j['vouchers'] is List)
|
||||
? (j['vouchers'] as List)
|
||||
.map((v) => FuelVoucher.fromJson(Map<String, dynamic>.from(v)))
|
||||
.toList()
|
||||
: <FuelVoucher>[],
|
||||
lentTotal: double.tryParse(s['lent_total']?.toString() ?? '0') ?? 0,
|
||||
collectedTotal:
|
||||
double.tryParse(s['collected_total']?.toString() ?? '0') ?? 0,
|
||||
outstandingTotal:
|
||||
double.tryParse(s['outstanding_total']?.toString() ?? '0') ?? 0,
|
||||
driversCount: int.tryParse(s['drivers_count']?.toString() ?? '0') ?? 0,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
// fuel_admin_service.dart — طبقة الاتصال بـ backend/Admin/fuel
|
||||
//
|
||||
// محفظة الوقود: السائق يتزوّد بلا نقد، والقيمة تُقيَّد ديناً يُخصم من
|
||||
// أرباحه. هذه الشاشة تدير المحطات الشريكة وتشغّل المنتج أو توقفه.
|
||||
import '../functions/crud.dart';
|
||||
import '../../constant/links.dart';
|
||||
import 'fuel_admin_models.dart';
|
||||
|
||||
class FuelApiResult<T> {
|
||||
final bool success;
|
||||
final T? data;
|
||||
final String message;
|
||||
FuelApiResult(this.success, this.data, this.message);
|
||||
}
|
||||
|
||||
class FuelAdminService {
|
||||
static String get _base => '${AppLink.server}/Admin/fuel';
|
||||
|
||||
static String _errMsg(dynamic res) {
|
||||
if (res == 'no_internet') return 'تحقق من اتصالك بالإنترنت';
|
||||
if (res == 'token_expired') return 'انتهت الجلسة، حاول مجدداً';
|
||||
if (res is Map && res['message'] is String) return res['message'];
|
||||
return 'حدث خطأ، حاول مجدداً';
|
||||
}
|
||||
|
||||
static Future<FuelApiResult<FuelStationsPage>> listStations({
|
||||
String? search,
|
||||
String? city,
|
||||
bool onlyActive = false,
|
||||
}) async {
|
||||
final payload = <String, dynamic>{};
|
||||
if (search != null && search.isNotEmpty) payload['search'] = search;
|
||||
if (city != null && city.isNotEmpty) payload['city'] = city;
|
||||
if (onlyActive) payload['only_active'] = '1';
|
||||
|
||||
final res = await CRUD().post(link: '$_base/list.php', payload: payload);
|
||||
|
||||
if (res is Map && res['status'] == 'success' && res['message'] is Map) {
|
||||
return FuelApiResult(
|
||||
true,
|
||||
FuelStationsPage.fromJson(Map<String, dynamic>.from(res['message'])),
|
||||
'ok',
|
||||
);
|
||||
}
|
||||
return FuelApiResult(false, null, _errMsg(res));
|
||||
}
|
||||
|
||||
/// يُرجع `api_key` عند الإنشاء فقط — مخزَّن مُهشَّراً على الخادم، فلا
|
||||
/// سبيل لعرضه ثانيةً. الشاشة مسؤولة عن إظهاره فوراً وبوضوح.
|
||||
static Future<FuelApiResult<Map<String, dynamic>>> saveStation({
|
||||
int? stationId,
|
||||
required String nameAr,
|
||||
String? chainName,
|
||||
String? city,
|
||||
String? address,
|
||||
double? lat,
|
||||
double? lng,
|
||||
String? phone,
|
||||
required double discountPercent,
|
||||
required bool isActive,
|
||||
}) async {
|
||||
final res = await CRUD().post(link: '$_base/save.php', payload: {
|
||||
if (stationId != null) 'station_id': stationId.toString(),
|
||||
'name_ar': nameAr,
|
||||
if (chainName != null && chainName.isNotEmpty) 'chain_name': chainName,
|
||||
if (city != null && city.isNotEmpty) 'city': city,
|
||||
if (address != null && address.isNotEmpty) 'address': address,
|
||||
if (lat != null) 'lat': lat.toString(),
|
||||
if (lng != null) 'lng': lng.toString(),
|
||||
if (phone != null && phone.isNotEmpty) 'phone': phone,
|
||||
'discount_percent': discountPercent.toString(),
|
||||
'is_active': isActive ? '1' : '0',
|
||||
});
|
||||
|
||||
if (res is Map && res['status'] == 'success') {
|
||||
return FuelApiResult(
|
||||
true,
|
||||
Map<String, dynamic>.from(res['message'] ?? {}),
|
||||
res['message'] is Map ? 'ok' : (res['message']?.toString() ?? 'ok'),
|
||||
);
|
||||
}
|
||||
return FuelApiResult(false, null, _errMsg(res));
|
||||
}
|
||||
|
||||
static Future<FuelApiResult<Map<String, dynamic>>> rotateKey(int stationId) async {
|
||||
final res = await CRUD().post(
|
||||
link: '$_base/rotate_key.php',
|
||||
payload: {'station_id': stationId.toString()},
|
||||
);
|
||||
if (res is Map && res['status'] == 'success' && res['message'] is Map) {
|
||||
return FuelApiResult(true, Map<String, dynamic>.from(res['message']), 'ok');
|
||||
}
|
||||
return FuelApiResult(false, null, _errMsg(res));
|
||||
}
|
||||
|
||||
static Future<FuelApiResult<bool>> toggleProduct(bool isActive) async {
|
||||
final res = await CRUD().post(
|
||||
link: '$_base/toggle_product.php',
|
||||
payload: {'is_active': isActive ? '1' : '0'},
|
||||
);
|
||||
if (res is Map && res['status'] == 'success' && res['message'] is Map) {
|
||||
return FuelApiResult(
|
||||
true, res['message']['product_active'] == true, 'ok');
|
||||
}
|
||||
return FuelApiResult(false, null, _errMsg(res));
|
||||
}
|
||||
|
||||
static Future<FuelApiResult<FuelDrawdownsPage>> listDrawdowns({
|
||||
int? stationId,
|
||||
String? status,
|
||||
}) async {
|
||||
final payload = <String, dynamic>{};
|
||||
if (stationId != null) payload['station_id'] = stationId.toString();
|
||||
if (status != null && status.isNotEmpty) payload['status'] = status;
|
||||
|
||||
final res = await CRUD().post(link: '$_base/drawdowns.php', payload: payload);
|
||||
|
||||
if (res is Map && res['status'] == 'success' && res['message'] is Map) {
|
||||
return FuelApiResult(
|
||||
true,
|
||||
FuelDrawdownsPage.fromJson(Map<String, dynamic>.from(res['message'])),
|
||||
'ok',
|
||||
);
|
||||
}
|
||||
return FuelApiResult(false, null, _errMsg(res));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user