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
@@ -42,6 +42,7 @@ import 'analytics/live_analytics_page.dart';
import 'package:siro_admin/views/widgets/responsive_layout.dart';
import 'package:siro_admin/views/widgets/web_sidebar.dart';
import '../transit/org_list_page.dart';
import 'fuel/fuel_stations_page.dart';
import 'package:siro_admin/views/widgets/glass_container.dart';
class AdminHomePage extends StatefulWidget {
@@ -1352,6 +1353,13 @@ class _AdminHomePageState extends State<AdminHomePage>
() => Get.to(() => const TransitOrgListPage())),
],
),
ActionCategory(
title: 'اقتصاد السائق',
items: [
ActionItem('محطات الوقود', Icons.local_gas_station_rounded, const Color(0xFF10B981),
() => Get.to(() => const FuelStationsPageView())),
],
),
ActionCategory(
title: 'إدارة النظام الجديد',
items: [
@@ -0,0 +1,174 @@
// fuel_drawdowns_page.dart — حركة القسائم وملخّص الائتمان
//
// ‏الشاشة التي تُجيب: كم أقرضنا؟ كم استُرد؟ وكم بقي في الشارع؟
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../controller/fuel/fuel_admin_controller.dart';
import '../../../controller/fuel/fuel_admin_models.dart';
class FuelDrawdownsPageView extends StatefulWidget {
const FuelDrawdownsPageView({super.key});
@override
State<FuelDrawdownsPageView> createState() => _FuelDrawdownsPageViewState();
}
class _FuelDrawdownsPageViewState extends State<FuelDrawdownsPageView> {
String? _status;
@override
void initState() {
super.initState();
Get.find<FuelAdminController>().fetchDrawdowns();
}
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return GetBuilder<FuelAdminController>(
builder: (c) => Scaffold(
backgroundColor: cs.surface,
appBar: AppBar(
backgroundColor: cs.surface,
foregroundColor: cs.onSurface,
title: const Text('حركة القسائم'),
),
body: Column(
children: [
if (c.drawdowns != null) _summary(c.drawdowns!, cs),
_filters(c, cs),
Expanded(
child: c.isLoadingDrawdowns
? Center(child: CircularProgressIndicator(color: cs.primary))
: (c.drawdowns?.vouchers.isEmpty ?? true)
? Center(
child: Text('لا توجد قسائم',
style: TextStyle(color: cs.onSurfaceVariant)))
: ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 12),
itemCount: c.drawdowns!.vouchers.length,
itemBuilder: (_, i) =>
_voucherCard(c.drawdowns!.vouchers[i], cs),
),
),
],
),
),
);
}
/// ‏الملخّص على كامل المنتج لا على القائمة المعروضة: رقم يتغيّر بتغيّر
/// ‏الفلتر يُقرأ كأنه حقيقة عن المنصة وهو ليس كذلك.
Widget _summary(FuelDrawdownsPage d, ColorScheme cs) => Container(
margin: const EdgeInsets.fromLTRB(12, 12, 12, 4),
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(14),
),
child: Row(
children: [
_stat('أُقرض', d.lentTotal.toString(), cs.onSurface, cs),
_stat('استُرد', d.collectedTotal.toString(), cs.primary, cs),
_stat('قائم', d.outstandingTotal.toString(), cs.tertiary, cs),
_stat('سائق', d.driversCount.toString(), cs.onSurfaceVariant, cs),
],
),
);
Widget _stat(String label, String value, Color color, ColorScheme cs) =>
Expanded(
child: Column(
children: [
Text(value,
style: TextStyle(
color: color, fontSize: 16, fontWeight: FontWeight.w700)),
const SizedBox(height: 2),
Text(label,
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 11)),
],
),
);
Widget _filters(FuelAdminController c, ColorScheme cs) {
const options = {
null: 'الكل',
'issued': 'سارية',
'redeemed': 'مصروفة',
'expired': 'منتهية',
'cancelled': 'ملغاة',
};
return SizedBox(
height: 46,
child: ListView(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
children: options.entries.map((e) {
final selected = _status == e.key;
return Padding(
padding: const EdgeInsets.only(left: 8),
child: ChoiceChip(
label: Text(e.value),
selected: selected,
onSelected: (_) {
setState(() => _status = e.key);
c.fetchDrawdowns(status: e.key);
},
),
);
}).toList(),
),
);
}
Widget _voucherCard(FuelVoucher v, ColorScheme cs) {
final (color, label) = switch (v.status) {
'redeemed' => (cs.primary, 'مصروفة'),
'issued' => (cs.tertiary, 'سارية'),
'cancelled' => (cs.error, 'ملغاة'),
_ => (cs.onSurfaceVariant, 'منتهية'),
};
// ‏المصروف لا المأذون: السائق قد يطلب عشرين ويتزوّد بخمسة عشر،
// ‏والدَّين على ما صُرف. عرض المأذون هنا يضخّم الأرقام في عين القارئ.
final amount = v.status == 'redeemed' ? v.amountRedeemed : v.amountAuthorized;
return Card(
color: cs.surfaceContainerHighest,
margin: const EdgeInsets.only(bottom: 8),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: ListTile(
title: Row(
children: [
Text('$amount ${v.currency}',
style: TextStyle(
color: cs.onSurface, fontWeight: FontWeight.bold)),
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(6),
),
child: Text(label, style: TextStyle(color: color, fontSize: 11)),
),
],
),
subtitle: Padding(
padding: const EdgeInsets.only(top: 4),
child: Text(
[
'سائق ${v.driverId}',
if (v.station != null && v.station!.isNotEmpty) v.station!,
v.redeemedAt ?? v.createdAt ?? '',
].where((s) => s.isNotEmpty).join(' · '),
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 11),
),
),
),
);
}
}
@@ -0,0 +1,212 @@
// fuel_station_form.dart — إضافة محطة أو تعديلها
//
// ‏يُرجع مفتاح المحطة الجديد عبر Get.back عند الإنشاء، وnull عند التعديل.
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../controller/fuel/fuel_admin_controller.dart';
import '../../../controller/fuel/fuel_admin_models.dart';
class FuelStationForm extends StatefulWidget {
final FuelStation? station;
const FuelStationForm({super.key, this.station});
@override
State<FuelStationForm> createState() => _FuelStationFormState();
}
class _FuelStationFormState extends State<FuelStationForm> {
final _formKey = GlobalKey<FormState>();
late final TextEditingController _name;
late final TextEditingController _chain;
late final TextEditingController _city;
late final TextEditingController _address;
late final TextEditingController _phone;
late final TextEditingController _lat;
late final TextEditingController _lng;
late final TextEditingController _discount;
late bool _isActive;
bool get _isEdit => widget.station != null;
@override
void initState() {
super.initState();
final s = widget.station;
_name = TextEditingController(text: s?.nameAr ?? '');
_chain = TextEditingController(text: s?.chainName ?? '');
_city = TextEditingController(text: s?.city ?? '');
_address = TextEditingController(text: s?.address ?? '');
_phone = TextEditingController(text: s?.phone ?? '');
_lat = TextEditingController(text: s?.lat?.toString() ?? '');
_lng = TextEditingController(text: s?.lng?.toString() ?? '');
_discount = TextEditingController(text: (s?.discountPercent ?? 0).toString());
_isActive = s?.isActive ?? true;
}
@override
void dispose() {
for (final c in [_name, _chain, _city, _address, _phone, _lat, _lng, _discount]) {
c.dispose();
}
super.dispose();
}
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
final c = Get.find<FuelAdminController>();
return Scaffold(
backgroundColor: cs.surface,
appBar: AppBar(
backgroundColor: cs.surface,
foregroundColor: cs.onSurface,
title: Text(_isEdit ? 'تعديل محطة' : 'محطة جديدة'),
),
body: Form(
key: _formKey,
child: ListView(
padding: const EdgeInsets.all(16),
children: [
_field(cs, _name, 'اسم المحطة *', required: true),
_field(cs, _chain, 'السلسلة (إن كانت فرعاً)'),
_field(cs, _city, 'المدينة'),
_field(cs, _address, 'العنوان'),
_field(cs, _phone, 'الهاتف', keyboard: TextInputType.phone),
Row(
children: [
Expanded(
child: _field(cs, _lat, 'خط العرض',
keyboard: TextInputType.number)),
const SizedBox(width: 10),
Expanded(
child: _field(cs, _lng, 'خط الطول',
keyboard: TextInputType.number)),
],
),
_field(
cs,
_discount,
'نسبة خصم الشريك (٪)',
keyboard: TextInputType.number,
validator: (v) {
final d = double.tryParse(v ?? '');
if (d == null) return 'أدخل رقماً';
if (d < 0 || d > 100) return 'بين 0 و 100';
return null;
},
),
// ‏الخصم يُسجَّل مع كل سحبة لا يُقرأ لاحقاً من هنا: النسبة
// ‏تتغيّر بالتفاوض، وحساب تسويات الماضي بنسبة اليوم تزوير.
Padding(
padding: const EdgeInsets.only(right: 4, bottom: 12),
child: Text(
'يُسجَّل مع كل عملية صرف. تعديله لا يغيّر عمليات سابقة.',
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 11),
),
),
SwitchListTile(
value: _isActive,
activeThumbColor: cs.primary,
contentPadding: EdgeInsets.zero,
title: Text('محطة فعّالة', style: TextStyle(color: cs.onSurface)),
subtitle: Text(
_isActive
? 'تظهر للسائقين وتستطيع تأكيد القسائم'
: 'مخفيّة عن السائقين ولا تستطيع تأكيد أي قسيمة',
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 11),
),
onChanged: (v) => setState(() => _isActive = v),
),
const SizedBox(height: 20),
SizedBox(
height: 48,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: cs.primary,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)),
),
onPressed: c.isSaving ? null : () => _submit(c),
child: c.isSaving
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2))
: Text(_isEdit ? 'حفظ التعديلات' : 'إنشاء المحطة',
style: TextStyle(color: cs.onPrimary)),
),
),
if (!_isEdit)
Padding(
padding: const EdgeInsets.only(top: 12),
child: Text(
'سيُولَّد مفتاح خاص بالمحطة عند الإنشاء، ويظهر مرة واحدة فقط.',
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 11),
textAlign: TextAlign.center,
),
),
],
),
),
);
}
Widget _field(
ColorScheme cs,
TextEditingController ctrl,
String label, {
bool required = false,
TextInputType? keyboard,
String? Function(String?)? validator,
}) =>
Padding(
padding: const EdgeInsets.only(bottom: 12),
child: TextFormField(
controller: ctrl,
keyboardType: keyboard,
style: TextStyle(color: cs.onSurface),
validator: validator ??
(required
? (v) => (v == null || v.trim().isEmpty) ? 'مطلوب' : null
: null),
decoration: InputDecoration(
labelText: label,
labelStyle: TextStyle(color: cs.onSurfaceVariant),
filled: true,
fillColor: cs.surfaceContainerHighest,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
),
),
);
Future<void> _submit(FuelAdminController c) async {
if (!(_formKey.currentState?.validate() ?? false)) return;
final key = await c.saveStation(
stationId: widget.station?.id,
nameAr: _name.text.trim(),
chainName: _chain.text.trim(),
city: _city.text.trim(),
address: _address.text.trim(),
lat: double.tryParse(_lat.text.trim()),
lng: double.tryParse(_lng.text.trim()),
phone: _phone.text.trim(),
discountPercent: double.tryParse(_discount.text.trim()) ?? 0,
isActive: _isActive,
);
if (c.lastError != null) {
Get.snackbar('تعذّر الحفظ', c.lastError!);
return;
}
Get.back(result: key);
}
}
@@ -0,0 +1,402 @@
// fuel_stations_page.dart — إدارة محطات الوقود الشريكة
//
// ‏الشاشة تعمل كاملةً حتى والمنتج مُطفأ: قرار المالك أن تُجهَّز الأمور
// ‏وتُبنى الشاشات، ولا يُشغَّل المنتج إلا بعد إتمام الاتفاق مع سلسلة
// ‏المحطات. لذلك اللافتة العلوية تقول الحالة صراحةً بدل إخفائها.
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import '../../../controller/fuel/fuel_admin_controller.dart';
import '../../../controller/fuel/fuel_admin_models.dart';
import 'fuel_drawdowns_page.dart';
import 'fuel_station_form.dart';
class FuelStationsPageView extends StatefulWidget {
const FuelStationsPageView({super.key});
@override
State<FuelStationsPageView> createState() => _FuelStationsPageViewState();
}
class _FuelStationsPageViewState extends State<FuelStationsPageView> {
final _searchCtrl = TextEditingController();
@override
void initState() {
super.initState();
Get.put(FuelAdminController());
}
@override
void dispose() {
_searchCtrl.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return GetBuilder<FuelAdminController>(
builder: (c) => Scaffold(
backgroundColor: cs.surface,
body: Column(
children: [
Container(
padding: const EdgeInsets.fromLTRB(16, 50, 16, 12),
child: Row(
children: [
GestureDetector(
onTap: () => Get.back(),
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: cs.outline),
),
child: Icon(Icons.arrow_back_ios_new_rounded,
color: cs.onSurfaceVariant, size: 16),
),
),
const SizedBox(width: 12),
Text('محفظة الوقود — المحطات',
style: TextStyle(
color: cs.onSurface,
fontSize: 18,
fontWeight: FontWeight.w700)),
const Spacer(),
IconButton(
icon: Icon(Icons.receipt_long_rounded, color: cs.tertiary),
tooltip: 'حركة القسائم',
onPressed: () => Get.to(() => const FuelDrawdownsPageView()),
),
IconButton(
icon: Icon(Icons.add, color: cs.primary),
tooltip: 'إضافة محطة',
onPressed: () => _openForm(c, null),
),
],
),
),
_productBanner(c, cs),
Padding(
padding: const EdgeInsets.all(12),
child: TextField(
controller: _searchCtrl,
style: TextStyle(color: cs.onSurface),
onSubmitted: (v) {
c.searchQuery = v;
c.fetchStations();
},
decoration: InputDecoration(
hintText: 'ابحث باسم المحطة أو السلسلة أو العنوان...',
hintStyle: TextStyle(color: cs.onSurfaceVariant),
prefixIcon: Icon(Icons.search, color: cs.onSurfaceVariant),
filled: true,
fillColor: cs.surfaceContainerHighest,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
),
),
),
Expanded(
child: c.isLoading
? Center(child: CircularProgressIndicator(color: cs.primary))
: c.stations.isEmpty
? _emptyState(cs)
: RefreshIndicator(
onRefresh: c.fetchStations,
child: ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 12),
itemCount: c.stations.length,
itemBuilder: (_, i) =>
_stationCard(c, c.stations[i], cs),
),
),
),
],
),
),
);
}
/// ‏لافتة الحالة. «مُجهَّز وغير مُشغَّل» حالة مقصودة لا عطل — وإخفاؤها
/// ‏يجعل من يعرض المنصة يظنّ المنتج حياً، أو يظنّه معطوباً.
Widget _productBanner(FuelAdminController c, ColorScheme cs) {
final active = c.productActive;
final color = active ? cs.primary : cs.tertiary;
return Container(
margin: const EdgeInsets.symmetric(horizontal: 12),
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(14),
border: Border.all(color: color.withValues(alpha: 0.4)),
),
child: Row(
children: [
Icon(active ? Icons.check_circle_rounded : Icons.pause_circle_rounded,
color: color, size: 20),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(active ? 'المنتج مُشغَّل' : 'مُجهَّز — غير مُشغَّل',
style: TextStyle(
color: color, fontWeight: FontWeight.w700, fontSize: 14)),
const SizedBox(height: 2),
Text(
active
? 'السائقون المؤهَّلون يستطيعون التفعيل وإصدار القسائم'
: 'الشاشة تعمل للتجهيز والعرض. لا تفعيل ولا قسائم للسائقين.',
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 11),
),
],
),
),
Switch(
value: active,
activeThumbColor: cs.primary,
onChanged: (v) => _confirmToggle(c, v),
),
],
),
);
}
/// ‏تأكيد قبل التشغيل لا بعده. التشغيل يفتح باب إقراض حقيقي بمال
/// ‏الشركة، ومفتاح يُقلب بالخطأ في عرضٍ أمام مستثمر ليس أمراً هيّناً.
Future<void> _confirmToggle(FuelAdminController c, bool value) async {
final cs = Theme.of(context).colorScheme;
final ok = await Get.dialog<bool>(
AlertDialog(
backgroundColor: cs.surfaceContainerHighest,
title: Text(value ? 'تشغيل محفظة الوقود؟' : 'إيقاف محفظة الوقود؟',
style: TextStyle(color: cs.onSurface)),
content: Text(
value
? 'سيتمكّن السائقون المؤهَّلون من التزوّد بالدَّين فوراً. لا تُشغّله قبل إتمام الاتفاق مع المحطات.'
: 'سيتوقّف التفعيل وإصدار القسائم. الالتزامات القائمة ودفترها لا تتأثر.',
style: TextStyle(color: cs.onSurfaceVariant),
),
actions: [
TextButton(
onPressed: () => Get.back(result: false),
child: const Text('إلغاء')),
TextButton(
onPressed: () => Get.back(result: true),
child: Text(value ? 'تشغيل' : 'إيقاف',
style: TextStyle(color: value ? cs.primary : cs.error)),
),
],
),
);
if (ok != true) return;
final done = await c.toggleProduct(value);
if (!done && c.lastError != null) {
Get.snackbar('تعذّر التنفيذ', c.lastError!,
backgroundColor: cs.error.withValues(alpha: 0.15));
}
}
Widget _emptyState(ColorScheme cs) => Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.local_gas_station_rounded,
size: 48, color: cs.onSurfaceVariant),
const SizedBox(height: 12),
Text('لا توجد محطات بعد',
style: TextStyle(color: cs.onSurface, fontSize: 16)),
const SizedBox(height: 6),
Text('أضف أول محطة شريكة لتجهيز المنتج',
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 12)),
],
),
);
Widget _stationCard(FuelAdminController c, FuelStation s, ColorScheme cs) {
final statusColor = s.isActive ? cs.primary : cs.onSurfaceVariant;
return Card(
color: cs.surfaceContainerHighest,
margin: const EdgeInsets.only(bottom: 10),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
child: ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
title: Row(
children: [
Expanded(
child: Text(s.nameAr,
style: TextStyle(
color: cs.onSurface, fontWeight: FontWeight.bold)),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: statusColor.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(6),
),
child: Text(s.isActive ? 'فعّالة' : 'موقوفة',
style: TextStyle(color: statusColor, fontSize: 11)),
),
],
),
subtitle: Padding(
padding: const EdgeInsets.only(top: 6),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
[
if (s.chainName != null && s.chainName!.isNotEmpty) s.chainName!,
if (s.city != null && s.city!.isNotEmpty) s.city!,
].join(' · '),
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 12),
),
const SizedBox(height: 6),
Row(
children: [
Icon(Icons.percent_rounded, size: 13, color: cs.tertiary),
const SizedBox(width: 2),
Text('خصم ${s.discountPercent}',
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 12)),
const SizedBox(width: 12),
Icon(Icons.local_gas_station, size: 13, color: cs.onSurfaceVariant),
const SizedBox(width: 2),
Text('${s.redeemCount} صرف',
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 12)),
const SizedBox(width: 12),
Text('${s.redeemTotal}',
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 12)),
],
),
],
),
),
trailing: PopupMenuButton<String>(
color: cs.surfaceContainerHighest,
icon: Icon(Icons.more_vert, color: cs.onSurfaceVariant, size: 18),
onSelected: (v) async {
if (v == 'edit') {
_openForm(c, s);
} else if (v == 'rotate') {
await _rotateKey(c, s);
}
},
itemBuilder: (_) => [
PopupMenuItem(
value: 'edit',
child: Text('تعديل', style: TextStyle(color: cs.onSurface))),
PopupMenuItem(
value: 'rotate',
child: Text('توليد مفتاح جديد',
style: TextStyle(color: cs.error))),
],
),
onTap: () => _openForm(c, s),
),
);
}
Future<void> _openForm(FuelAdminController c, FuelStation? station) async {
final apiKey = await Get.to<String?>(
() => FuelStationForm(station: station),
);
if (apiKey != null && apiKey.isNotEmpty) {
await _showKey(apiKey, station?.nameAr ?? 'المحطة الجديدة');
}
}
Future<void> _rotateKey(FuelAdminController c, FuelStation s) async {
final cs = Theme.of(context).colorScheme;
final ok = await Get.dialog<bool>(
AlertDialog(
backgroundColor: cs.surfaceContainerHighest,
title: Text('توليد مفتاح جديد؟', style: TextStyle(color: cs.onSurface)),
content: Text(
'المفتاح الحالي لـ«${s.nameAr}» سيتوقّف فوراً، ولن تستطيع المحطة تأكيد أي قسيمة حتى تستلم الجديد.',
style: TextStyle(color: cs.onSurfaceVariant),
),
actions: [
TextButton(
onPressed: () => Get.back(result: false),
child: const Text('إلغاء')),
TextButton(
onPressed: () => Get.back(result: true),
child: Text('توليد', style: TextStyle(color: cs.error))),
],
),
);
if (ok != true) return;
final key = await c.rotateKey(s.id);
if (key != null) {
await _showKey(key, s.nameAr);
} else if (c.lastError != null) {
Get.snackbar('تعذّر التوليد', c.lastError!,
backgroundColor: cs.error.withValues(alpha: 0.15));
}
}
/// ‏المفتاح يظهر مرة واحدة — مخزَّن مُهشَّراً على الخادم فلا سبيل
/// ‏لاستعادته. الحوار لا يُغلق باللمس خارجه: إغلاقه بالخطأ قبل النسخ
/// ‏يعني محطة بلا مفتاح ودورة توليد جديدة.
Future<void> _showKey(String key, String stationName) async {
final cs = Theme.of(context).colorScheme;
await Get.dialog<void>(
barrierDismissible: false,
AlertDialog(
backgroundColor: cs.surfaceContainerHighest,
title: Text('مفتاح $stationName',
style: TextStyle(color: cs.onSurface, fontSize: 16)),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: cs.surface,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: cs.outline),
),
child: SelectableText(key,
style: TextStyle(
color: cs.onSurface, fontSize: 12, fontFamily: 'monospace')),
),
const SizedBox(height: 10),
Text(
'انسخه الآن وسلّمه للمحطة. لن يظهر مرة أخرى — المفتاح مخزَّن مشفّراً ولا يمكن استرجاعه.',
style: TextStyle(color: cs.error, fontSize: 11),
),
],
),
actions: [
TextButton(
onPressed: () {
Clipboard.setData(ClipboardData(text: key));
Get.snackbar('نُسخ', 'المفتاح في الحافظة');
},
child: Text('نسخ', style: TextStyle(color: cs.primary)),
),
TextButton(
onPressed: () => Get.back(), child: const Text('تم')),
],
),
);
}
}