Update: 2026-08-08 12:58:16

This commit is contained in:
Hamza-Ayed
2026-08-08 12:58:17 +03:00
parent 611b5e616d
commit fae0e4a38a
55 changed files with 5771 additions and 1820 deletions
@@ -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));
}
}