Files
musadaq-saas/musadaq-app/lib/features/ar_scanner/views/ar_scanner_view.dart
2026-05-08 05:24:38 +03:00

490 lines
21 KiB
Dart

import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../controllers/ar_scanner_controller.dart';
/// WiFi Scanner View
/// Connect to external physical scanners over WiFi.
/// Discover → Connect → Scan → Upload
class WifiScannerView extends GetView<WifiScannerController> {
const WifiScannerView({super.key});
static const _navy = Color(0xFF0F4C81);
static const _gold = Color(0xFFD4AF37);
static const _green = Color(0xFF10B981);
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return Scaffold(
appBar: AppBar(
title: const Text('ماسح ضوئي WiFi', style: TextStyle(fontFamily: 'El Messiri')),
centerTitle: true,
backgroundColor: _navy,
foregroundColor: Colors.white,
actions: [
Obx(() => controller.isConnected.value
? IconButton(
onPressed: controller.disconnect,
icon: const Icon(Icons.link_off),
tooltip: 'قطع الاتصال',
)
: const SizedBox()),
],
),
body: Obx(() {
if (controller.selectedCompanyId.value.isEmpty) {
return _buildCompanySelection();
}
if (!controller.isConnected.value) {
return _buildScannerDiscovery(isDark);
}
return _buildScannerInterface(isDark);
}),
);
}
// ─── Step 1: Company Selection ────────────────────
Widget _buildCompanySelection() {
return Obx(() {
if (controller.isLoadingCompanies.value) {
return const Center(child: CircularProgressIndicator());
}
if (controller.companies.isEmpty) {
return const Center(
child: Padding(
padding: EdgeInsets.all(32),
child: Text('لا توجد شركات.\nأضف شركة أولاً من الإعدادات.',
textAlign: TextAlign.center, style: TextStyle(fontSize: 16)),
),
);
}
return ListView(
padding: const EdgeInsets.all(16),
children: [
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: LinearGradient(colors: [_navy, _navy.withValues(alpha: 0.8)]),
borderRadius: BorderRadius.circular(16),
),
child: const Column(
children: [
Icon(Icons.scanner, size: 48, color: Colors.white),
SizedBox(height: 12),
Text('ماسح ضوئي خارجي', style: TextStyle(color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold)),
SizedBox(height: 4),
Text('اربط جهاز المسح الضوئي عبر WiFi واستورد الفواتير مباشرة',
textAlign: TextAlign.center,
style: TextStyle(color: Colors.white70, fontSize: 13)),
],
),
),
const SizedBox(height: 20),
const Text('اختر الشركة أولاً:', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
const SizedBox(height: 12),
...controller.companies.map((c) => Card(
elevation: 2,
margin: const EdgeInsets.only(bottom: 10),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: ListTile(
leading: CircleAvatar(
backgroundColor: _navy.withValues(alpha: 0.1),
child: const Icon(Icons.business, color: _navy),
),
title: Text(c['name'] ?? '', style: const TextStyle(fontWeight: FontWeight.bold)),
subtitle: Text('الرقم الضريبي: ${c['tax_identification_number'] ?? '-'}'),
trailing: const Icon(Icons.arrow_forward_ios, size: 16),
onTap: () => controller.selectCompany(c['id'], c['name'] ?? ''),
),
)),
],
);
});
}
// ─── Step 2: Scanner Discovery ────────────────────
Widget _buildScannerDiscovery(bool isDark) {
return ListView(
padding: const EdgeInsets.all(16),
children: [
// Status card
Obx(() => Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: isDark ? const Color(0xFF1E1E2E) : Colors.white,
borderRadius: BorderRadius.circular(14),
border: Border.all(color: _navy.withValues(alpha: 0.2)),
),
child: Row(
children: [
Icon(
controller.isScanning.value ? Icons.search : Icons.wifi_find,
color: _navy, size: 32,
),
const SizedBox(width: 12),
Expanded(
child: Text(controller.statusMessage.value,
style: const TextStyle(fontSize: 14)),
),
],
),
)),
const SizedBox(height: 16),
// Auto-discover button
Obx(() => ElevatedButton.icon(
onPressed: controller.isScanning.value ? null : controller.discoverScanners,
icon: controller.isScanning.value
? const SizedBox(
width: 18, height: 18,
child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white))
: const Icon(Icons.search, color: Colors.white),
label: Text(
controller.isScanning.value ? 'جارٍ البحث...' : '🔍 بحث تلقائي عن ماسحات',
style: const TextStyle(fontWeight: FontWeight.bold, color: Colors.white),
),
style: ElevatedButton.styleFrom(
backgroundColor: _navy,
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
)),
const SizedBox(height: 20),
// Manual IP entry
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: isDark ? const Color(0xFF1E1E2E) : const Color(0xFFF8FAFC),
borderRadius: BorderRadius.circular(14),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('أو أدخل IP يدوياً:', style: TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(height: 10),
Row(
children: [
Expanded(
child: TextField(
controller: controller.manualIpController,
keyboardType: TextInputType.number,
decoration: InputDecoration(
hintText: '192.168.1.100',
hintStyle: const TextStyle(color: Colors.grey),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(10)),
contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
prefixIcon: const Icon(Icons.lan),
),
),
),
const SizedBox(width: 10),
ElevatedButton(
onPressed: controller.connectManualIp,
style: ElevatedButton.styleFrom(
backgroundColor: _gold,
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
),
child: const Text('اتصل', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold)),
),
],
),
],
),
),
const SizedBox(height: 20),
// Discovered scanners list
Obx(() => controller.discoveredScanners.isEmpty
? const SizedBox()
: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('ماسحات مكتشفة:', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
...controller.discoveredScanners.map((s) => Card(
elevation: 2,
margin: const EdgeInsets.only(bottom: 8),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: ListTile(
leading: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: _green.withValues(alpha: 0.1),
shape: BoxShape.circle,
),
child: const Icon(Icons.scanner, color: _green),
),
title: Text(s['name'] ?? '', style: const TextStyle(fontWeight: FontWeight.bold)),
subtitle: Text('${s['ip']}:${s['port']}${s['protocol']}'),
trailing: ElevatedButton(
onPressed: () => controller.connectToScanner(s),
style: ElevatedButton.styleFrom(backgroundColor: _green),
child: const Text('اتصل', style: TextStyle(color: Colors.white)),
),
),
)),
],
)),
],
);
}
// ─── Step 3: Connected — Scan Interface ───────────
Widget _buildScannerInterface(bool isDark) {
return Column(
children: [
// Connection status bar
Obx(() => Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
color: _green.withValues(alpha: 0.1),
child: Row(
children: [
const Icon(Icons.check_circle, color: _green, size: 18),
const SizedBox(width: 8),
Expanded(
child: Text(controller.connectionStatus.value,
style: const TextStyle(color: _green, fontWeight: FontWeight.w600, fontSize: 13)),
),
Text(controller.selectedCompanyName.value,
style: const TextStyle(fontSize: 12, color: Colors.grey)),
],
),
)),
// Scan settings
Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
// Resolution
Expanded(
child: Obx(() => _buildSettingChip(
'${controller.scanResolution.value} DPI',
Icons.high_quality,
() {
final current = controller.scanResolution.value;
controller.scanResolution.value =
current == 150 ? 300 : (current == 300 ? 600 : 150);
},
)),
),
const SizedBox(width: 8),
// Color mode
Expanded(
child: Obx(() => _buildSettingChip(
controller.scanColorMode.value == 'Color' ? 'ألوان' : 'رمادي',
Icons.palette,
() {
controller.scanColorMode.value =
controller.scanColorMode.value == 'Color' ? 'Grayscale' : 'Color';
},
)),
),
],
),
),
// Status message
Obx(() => Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(controller.statusMessage.value,
textAlign: TextAlign.center,
style: TextStyle(fontSize: 13, color: isDark ? Colors.white54 : Colors.grey)),
)),
const SizedBox(height: 16),
// Scan Button
Obx(() => Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: controller.isScanningDocument.value ? null : controller.triggerScan,
icon: controller.isScanningDocument.value
? const SizedBox(
width: 22, height: 22,
child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white))
: const Icon(Icons.scanner, color: Colors.white, size: 24),
label: Text(
controller.isScanningDocument.value ? 'جارٍ المسح...' : '📄 مسح فاتورة',
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.white),
),
style: ElevatedButton.styleFrom(
backgroundColor: _navy,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
elevation: 4,
),
),
),
)),
const SizedBox(height: 16),
// Scanned images queue
Expanded(
child: Obx(() {
if (controller.scannedImages.isEmpty) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.inbox, size: 64, color: Colors.grey.withValues(alpha: 0.3)),
const SizedBox(height: 12),
Text('لا توجد صور ممسوحة بعد',
style: TextStyle(color: Colors.grey.withValues(alpha: 0.5))),
const Text('اضغط "مسح فاتورة" لبدء المسح',
style: TextStyle(fontSize: 12, color: Colors.grey)),
],
),
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
Text('الطابور (${controller.scannedImages.length})',
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 15)),
const Spacer(),
TextButton.icon(
onPressed: controller.clearQueue,
icon: const Icon(Icons.delete_sweep, size: 18, color: Colors.red),
label: const Text('مسح الكل', style: TextStyle(color: Colors.red, fontSize: 12)),
),
],
),
),
Expanded(
child: GridView.builder(
padding: const EdgeInsets.all(16),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3, crossAxisSpacing: 10, mainAxisSpacing: 10, childAspectRatio: 0.75,
),
itemCount: controller.scannedImages.length,
itemBuilder: (_, index) {
return Stack(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(10),
child: Image.file(controller.scannedImages[index],
fit: BoxFit.cover, width: double.infinity, height: double.infinity),
),
Positioned(
top: 4, right: 4,
child: GestureDetector(
onTap: () => controller.removeImage(index),
child: Container(
padding: const EdgeInsets.all(4),
decoration: const BoxDecoration(color: Colors.red, shape: BoxShape.circle),
child: const Icon(Icons.close, size: 14, color: Colors.white),
),
),
),
Positioned(
bottom: 4, left: 4,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: Colors.black54, borderRadius: BorderRadius.circular(8),
),
child: Text('${index + 1}', style: const TextStyle(color: Colors.white, fontSize: 11)),
),
),
],
);
},
),
),
],
);
}),
),
// Upload Bar
Obx(() => controller.scannedImages.isEmpty
? const SizedBox()
: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: isDark ? const Color(0xFF1E1E2E) : Colors.white,
boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 8, offset: const Offset(0, -2))],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (controller.isUploading.value)
Padding(
padding: const EdgeInsets.only(bottom: 10),
child: ClipRRect(
borderRadius: BorderRadius.circular(4),
child: LinearProgressIndicator(
value: controller.uploadProgress.value,
backgroundColor: Colors.grey.withValues(alpha: 0.2),
color: _gold,
minHeight: 6,
),
),
),
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: controller.isUploading.value ? null : controller.uploadQueue,
icon: controller.isUploading.value
? const SizedBox(width: 20, height: 20,
child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white))
: const Icon(Icons.cloud_upload_rounded, color: Colors.white),
label: Text(
controller.isUploading.value
? 'جارٍ الرفع... ${(controller.uploadProgress.value * 100).toInt()}%'
: '☁️ رفع ${controller.scannedImages.length} فاتورة لمُصادَق',
style: const TextStyle(fontSize: 15, fontWeight: FontWeight.bold, color: Colors.white),
),
style: ElevatedButton.styleFrom(
backgroundColor: _gold,
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
),
),
],
),
)),
],
);
}
Widget _buildSettingChip(String label, IconData icon, VoidCallback onTap) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 12),
decoration: BoxDecoration(
color: _navy.withValues(alpha: 0.08),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: _navy.withValues(alpha: 0.2)),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, size: 16, color: _navy),
const SizedBox(width: 6),
Text(label, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600)),
],
),
),
);
}
}