Update: 2026-05-07 22:19:17
This commit is contained in:
@@ -17,6 +17,7 @@ import '../../features/invoices/views/invoice_detail_view.dart';
|
|||||||
import '../../features/onboarding/views/onboarding_view.dart';
|
import '../../features/onboarding/views/onboarding_view.dart';
|
||||||
import '../../features/onboarding/controllers/onboarding_controller.dart';
|
import '../../features/onboarding/controllers/onboarding_controller.dart';
|
||||||
import '../../core/storage/secure_storage.dart';
|
import '../../core/storage/secure_storage.dart';
|
||||||
|
import '../../features/companies/views/companies_management_view.dart';
|
||||||
|
|
||||||
part 'app_routes.dart';
|
part 'app_routes.dart';
|
||||||
|
|
||||||
@@ -131,5 +132,9 @@ class AppPages {
|
|||||||
Get.put(OnboardingController());
|
Get.put(OnboardingController());
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
GetPage(
|
||||||
|
name: AppRoutes.COMPANIES_MANAGEMENT,
|
||||||
|
page: () => const CompaniesManagementView(),
|
||||||
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,4 +17,5 @@ abstract class AppRoutes {
|
|||||||
static const PAYMENT_RECEIPT = '/payment-receipt';
|
static const PAYMENT_RECEIPT = '/payment-receipt';
|
||||||
static const INVOICE_DETAIL = '/invoice-detail';
|
static const INVOICE_DETAIL = '/invoice-detail';
|
||||||
static const ONBOARDING = '/onboarding';
|
static const ONBOARDING = '/onboarding';
|
||||||
|
static const COMPANIES_MANAGEMENT = '/companies-management';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import '../../../core/network/dio_client.dart';
|
||||||
|
import '../../../core/utils/app_snackbar.dart';
|
||||||
|
import 'companies_management_controller.dart';
|
||||||
|
|
||||||
|
class AddCompanyController extends GetxController {
|
||||||
|
final nameController = TextEditingController();
|
||||||
|
final tinController = TextEditingController();
|
||||||
|
final crnController = TextEditingController();
|
||||||
|
|
||||||
|
var isSubmitting = false.obs;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onClose() {
|
||||||
|
nameController.dispose();
|
||||||
|
tinController.dispose();
|
||||||
|
crnController.dispose();
|
||||||
|
super.onClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> submit() async {
|
||||||
|
final name = nameController.text.trim();
|
||||||
|
final tin = tinController.text.trim();
|
||||||
|
|
||||||
|
if (name.isEmpty || tin.isEmpty) {
|
||||||
|
AppSnackbar.showError('خطأ', 'الرجاء إدخال اسم الشركة والرقم الضريبي');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
isSubmitting.value = true;
|
||||||
|
final dio = DioClient().client;
|
||||||
|
final response = await dio.post('companies', data: {
|
||||||
|
'name': name,
|
||||||
|
'tax_identification_number': tin,
|
||||||
|
'commercial_registration_number': crnController.text.trim(),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data['success'] == true) {
|
||||||
|
AppSnackbar.showSuccess('نجاح', 'تمت إضافة الشركة بنجاح');
|
||||||
|
|
||||||
|
// Refresh list if controller exists
|
||||||
|
if (Get.isRegistered<CompaniesManagementController>()) {
|
||||||
|
Get.find<CompaniesManagementController>().fetchCompanies();
|
||||||
|
}
|
||||||
|
|
||||||
|
Get.back();
|
||||||
|
}
|
||||||
|
} on DioException catch (e) {
|
||||||
|
if (e.response?.statusCode == 403) {
|
||||||
|
AppSnackbar.showError('خطأ', 'لقد وصلت للحد الأقصى المسموح به للشركات في باقتك');
|
||||||
|
} else {
|
||||||
|
AppSnackbar.showError('خطأ', 'تعذر إضافة الشركة');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
AppSnackbar.showError('خطأ', 'حدث خطأ غير متوقع');
|
||||||
|
} finally {
|
||||||
|
isSubmitting.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import '../../../core/network/dio_client.dart';
|
||||||
|
import '../../../core/utils/app_snackbar.dart';
|
||||||
|
import '../../../core/utils/logger.dart';
|
||||||
|
|
||||||
|
class CompaniesManagementController extends GetxController {
|
||||||
|
final Dio _dio = DioClient().client;
|
||||||
|
|
||||||
|
var isLoading = true.obs;
|
||||||
|
var companies = <Map<String, dynamic>>[].obs;
|
||||||
|
var employees = <Map<String, dynamic>>[].obs;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onInit() {
|
||||||
|
super.onInit();
|
||||||
|
fetchCompanies();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> fetchCompanies() async {
|
||||||
|
try {
|
||||||
|
isLoading.value = true;
|
||||||
|
final response = await _dio.get('companies');
|
||||||
|
if (response.data['success'] == true) {
|
||||||
|
companies.value = List<Map<String, dynamic>>.from(response.data['data']);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
AppLogger.error('Failed to fetch companies', e);
|
||||||
|
AppSnackbar.showError('خطأ', 'تعذر تحميل قائمة الشركات');
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> deleteCompany(String id) async {
|
||||||
|
try {
|
||||||
|
final response = await _dio.delete('companies/$id');
|
||||||
|
if (response.data['success'] == true) {
|
||||||
|
companies.removeWhere((c) => c['id'] == id);
|
||||||
|
AppSnackbar.showSuccess('نجاح', 'تم حذف الشركة بنجاح');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
AppLogger.error('Failed to delete company', e);
|
||||||
|
AppSnackbar.showError('خطأ', 'تعذر حذف الشركة');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
108
musadaq-app/lib/features/companies/views/add_company_view.dart
Normal file
108
musadaq-app/lib/features/companies/views/add_company_view.dart
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import '../controllers/add_company_controller.dart';
|
||||||
|
|
||||||
|
class AddCompanyView extends StatelessWidget {
|
||||||
|
const AddCompanyView({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final controller = Get.put(AddCompanyController());
|
||||||
|
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('إضافة شركة', style: TextStyle(fontFamily: 'El Messiri')),
|
||||||
|
centerTitle: true,
|
||||||
|
backgroundColor: const Color(0xFF0F4C81),
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
),
|
||||||
|
body: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'بيانات الشركة الأساسية',
|
||||||
|
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
_buildTextField(
|
||||||
|
controller: controller.nameController,
|
||||||
|
label: 'اسم الشركة',
|
||||||
|
icon: Icons.business,
|
||||||
|
isDark: isDark,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
_buildTextField(
|
||||||
|
controller: controller.tinController,
|
||||||
|
label: 'الرقم الضريبي',
|
||||||
|
icon: Icons.numbers,
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
isDark: isDark,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
_buildTextField(
|
||||||
|
controller: controller.crnController,
|
||||||
|
label: 'رقم السجل التجاري (اختياري)',
|
||||||
|
icon: Icons.article,
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
isDark: isDark,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 40),
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 54,
|
||||||
|
child: Obx(
|
||||||
|
() => ElevatedButton(
|
||||||
|
onPressed: controller.isSubmitting.value ? null : controller.submit,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: const Color(0xFF0F4C81),
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
|
||||||
|
),
|
||||||
|
child: controller.isSubmitting.value
|
||||||
|
? const CircularProgressIndicator(color: Colors.white)
|
||||||
|
: const Text(
|
||||||
|
'حفظ وإضافة',
|
||||||
|
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.white),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildTextField({
|
||||||
|
required TextEditingController controller,
|
||||||
|
required String label,
|
||||||
|
required IconData icon,
|
||||||
|
TextInputType? keyboardType,
|
||||||
|
required bool isDark,
|
||||||
|
}) {
|
||||||
|
return TextField(
|
||||||
|
controller: controller,
|
||||||
|
keyboardType: keyboardType,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: label,
|
||||||
|
prefixIcon: Icon(icon, color: const Color(0xFF0F4C81)),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
borderSide: BorderSide(color: isDark ? Colors.white24 : Colors.grey.shade300),
|
||||||
|
),
|
||||||
|
enabledBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
borderSide: BorderSide(color: isDark ? Colors.white24 : Colors.grey.shade300),
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
borderSide: const BorderSide(color: Color(0xFF0F4C81), width: 2),
|
||||||
|
),
|
||||||
|
filled: true,
|
||||||
|
fillColor: isDark ? Colors.white.withValues(alpha: 0.05) : Colors.white,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import '../controllers/companies_management_controller.dart';
|
||||||
|
import 'add_company_view.dart';
|
||||||
|
|
||||||
|
class CompaniesManagementView extends StatelessWidget {
|
||||||
|
const CompaniesManagementView({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
// Put controller directly so we don't strictly need a binding for this nested route
|
||||||
|
final controller = Get.put(CompaniesManagementController());
|
||||||
|
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('إدارة الشركات', style: TextStyle(fontFamily: 'El Messiri')),
|
||||||
|
centerTitle: true,
|
||||||
|
backgroundColor: const Color(0xFF0F4C81),
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
elevation: 0,
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.add),
|
||||||
|
onPressed: () {
|
||||||
|
Get.to(() => const AddCompanyView());
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: Obx(() {
|
||||||
|
if (controller.isLoading.value) {
|
||||||
|
return const Center(child: CircularProgressIndicator());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (controller.companies.isEmpty) {
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.business_center_outlined, size: 80, color: Colors.grey.shade400),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
const Text('لا يوجد شركات مسجلة', style: TextStyle(fontSize: 18, color: Colors.grey)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return RefreshIndicator(
|
||||||
|
onRefresh: controller.fetchCompanies,
|
||||||
|
child: ListView.builder(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
itemCount: controller.companies.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final company = controller.companies[index];
|
||||||
|
return Card(
|
||||||
|
elevation: 2,
|
||||||
|
margin: const EdgeInsets.only(bottom: 16),
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(10),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFF0F4C81).withValues(alpha: 0.1),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: const Icon(Icons.business, color: Color(0xFF0F4C81)),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
company['name'] ?? 'شركة',
|
||||||
|
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
PopupMenuButton(
|
||||||
|
icon: const Icon(Icons.more_vert),
|
||||||
|
itemBuilder: (context) => [
|
||||||
|
const PopupMenuItem(value: 'edit', child: Text('تعديل البيانات')),
|
||||||
|
const PopupMenuItem(value: 'employees', child: Text('إدارة الموظفين')),
|
||||||
|
const PopupMenuItem(value: 'delete', child: Text('حذف الشركة', style: TextStyle(color: Colors.red))),
|
||||||
|
],
|
||||||
|
onSelected: (value) {
|
||||||
|
if (value == 'delete') {
|
||||||
|
_confirmDelete(context, controller, company['id']);
|
||||||
|
} else {
|
||||||
|
Get.snackbar('قريباً', 'الواجهة قيد البرمجة');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
_buildDetailChip(Icons.numbers, company['tax_identification_number'] ?? 'غير محدد', isDark),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
if (company['is_jofotara_connected'] == true || company['is_jofotara_connected'] == 1)
|
||||||
|
_buildDetailChip(Icons.link, 'مرتبطة بجوفوتارا', isDark, color: const Color(0xFF10B981)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
TextButton.icon(
|
||||||
|
onPressed: () => Get.snackbar('إحصائيات', 'عرض إحصائيات الشركة'),
|
||||||
|
icon: const Icon(Icons.bar_chart, size: 18),
|
||||||
|
label: const Text('الإحصائيات'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildDetailChip(IconData icon, String text, bool isDark, {Color? color}) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: (color ?? Colors.grey).withValues(alpha: 0.1),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(icon, size: 14, color: color ?? (isDark ? Colors.white70 : Colors.black54)),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Text(
|
||||||
|
text,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: color ?? (isDark ? Colors.white70 : Colors.black87),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _confirmDelete(BuildContext context, CompaniesManagementController controller, String id) {
|
||||||
|
Get.defaultDialog(
|
||||||
|
title: 'حذف الشركة',
|
||||||
|
middleText: 'هل أنت متأكد من رغبتك في حذف هذه الشركة نهائياً؟',
|
||||||
|
textConfirm: 'حذف',
|
||||||
|
textCancel: 'إلغاء',
|
||||||
|
confirmTextColor: Colors.white,
|
||||||
|
buttonColor: Colors.red,
|
||||||
|
onConfirm: () {
|
||||||
|
Get.back();
|
||||||
|
controller.deleteCompany(id);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,6 +21,8 @@ class ScannerController extends GetxController {
|
|||||||
var processedImagesCount = 0.obs;
|
var processedImagesCount = 0.obs;
|
||||||
var totalImagesCount = 0.obs;
|
var totalImagesCount = 0.obs;
|
||||||
var isBatchDone = false.obs;
|
var isBatchDone = false.obs;
|
||||||
|
var selectedCompanyId = ''.obs;
|
||||||
|
var selectedCompanyName = ''.obs;
|
||||||
|
|
||||||
final InvoiceUploadService _uploadService = InvoiceUploadService();
|
final InvoiceUploadService _uploadService = InvoiceUploadService();
|
||||||
final UploadProgressService _progressService =
|
final UploadProgressService _progressService =
|
||||||
@@ -90,49 +92,28 @@ class ScannerController extends GetxController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> uploadBatch(String fallbackCompanyId) async {
|
Future<void> uploadBatch() async {
|
||||||
if (capturedImages.isEmpty) {
|
if (capturedImages.isEmpty) {
|
||||||
AppSnackbar.showWarning('تنبيه', 'الرجاء تصوير فاتورة واحدة على الأقل');
|
AppSnackbar.showWarning('تنبيه', 'الرجاء تصوير فاتورة واحدة على الأقل');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (selectedCompanyId.isEmpty) {
|
||||||
|
AppSnackbar.showWarning('تنبيه', 'الرجاء اختيار الشركة أولاً');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
isProcessing.value = true;
|
isProcessing.value = true;
|
||||||
uploadProgress.value = 0.0;
|
uploadProgress.value = 0.0;
|
||||||
|
|
||||||
String companyId = fallbackCompanyId;
|
|
||||||
String companyName = 'شركة غير محددة';
|
|
||||||
|
|
||||||
if (companyId == 'mock_company_id_123' || companyId.isEmpty) {
|
|
||||||
if (companies.isNotEmpty) {
|
|
||||||
companyId = companies[0]['id'];
|
|
||||||
companyName = companies[0]['name'] ?? 'شركتي';
|
|
||||||
} else {
|
|
||||||
final res = await DioClient().client.get('companies');
|
|
||||||
if (res.data['success'] == true &&
|
|
||||||
res.data['data'] != null &&
|
|
||||||
res.data['data'].isNotEmpty) {
|
|
||||||
companyId = res.data['data'][0]['id'];
|
|
||||||
companyName = res.data['data'][0]['name'] ?? 'شركتي';
|
|
||||||
} else {
|
|
||||||
AppSnackbar.showError('خطأ', 'لا توجد شركات مسجلة في حسابك');
|
|
||||||
isProcessing.value = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
final comp = companies.firstWhereOrNull((c) => c['id'] == companyId);
|
|
||||||
if (comp != null) companyName = comp['name'] ?? 'شركتي';
|
|
||||||
}
|
|
||||||
|
|
||||||
AppLogger.print(
|
AppLogger.print(
|
||||||
'Uploading batch of ${capturedImages.length} images to company $companyId...');
|
'Uploading batch of ${capturedImages.length} images to company ${selectedCompanyId.value}...');
|
||||||
|
|
||||||
// Start global progress
|
// Start global progress
|
||||||
_progressService.startUpload(companyName, capturedImages.length);
|
_progressService.startUpload(selectedCompanyName.value, capturedImages.length);
|
||||||
|
|
||||||
final batchId = await _uploadService.uploadBatch(
|
final batchId = await _uploadService.uploadBatch(
|
||||||
companyId: companyId,
|
companyId: selectedCompanyId.value,
|
||||||
images: capturedImages,
|
images: capturedImages,
|
||||||
onProgress: (current, total) {
|
onProgress: (current, total) {
|
||||||
uploadProgress.value = current / total;
|
uploadProgress.value = current / total;
|
||||||
@@ -149,6 +130,8 @@ class ScannerController extends GetxController {
|
|||||||
capturedImages.clear();
|
capturedImages.clear();
|
||||||
uploadProgress.value = 0.0;
|
uploadProgress.value = 0.0;
|
||||||
isProcessing.value = false;
|
isProcessing.value = false;
|
||||||
|
selectedCompanyId.value = '';
|
||||||
|
selectedCompanyName.value = '';
|
||||||
|
|
||||||
_progressService.startProcessing();
|
_progressService.startProcessing();
|
||||||
Get.back(); // Go back to dashboard, progress will show in overlay
|
Get.back(); // Go back to dashboard, progress will show in overlay
|
||||||
@@ -167,6 +150,11 @@ class ScannerController extends GetxController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void selectCompany(String id, String name) {
|
||||||
|
selectedCompanyId.value = id;
|
||||||
|
selectedCompanyName.value = name;
|
||||||
|
}
|
||||||
|
|
||||||
Future<String> getSavePath() async {
|
Future<String> getSavePath() async {
|
||||||
final directory = await getTemporaryDirectory();
|
final directory = await getTemporaryDirectory();
|
||||||
final fileName = 'invoice_${DateTime.now().millisecondsSinceEpoch}.jpg';
|
final fileName = 'invoice_${DateTime.now().millisecondsSinceEpoch}.jpg';
|
||||||
|
|||||||
@@ -9,6 +9,72 @@ class ScannerView extends GetView<ScannerController> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
return Obx(() {
|
||||||
|
if (controller.selectedCompanyId.value.isEmpty) {
|
||||||
|
return _buildCompanySelection(context);
|
||||||
|
}
|
||||||
|
return _buildScanner(context);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildCompanySelection(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('اختر الشركة أولاً',
|
||||||
|
style: TextStyle(fontFamily: 'El Messiri')),
|
||||||
|
centerTitle: true,
|
||||||
|
backgroundColor: const Color(0xFF0F4C81),
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
),
|
||||||
|
body: Obx(() {
|
||||||
|
if (controller.isLoadingCompanies.value) {
|
||||||
|
return const Center(child: CircularProgressIndicator());
|
||||||
|
}
|
||||||
|
if (controller.companies.isEmpty) {
|
||||||
|
return const Center(
|
||||||
|
child: Text('لا توجد شركات مسجلة في حسابك.\nيرجى إضافة شركة أولاً.',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(fontSize: 16)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return ListView.separated(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
itemCount: controller.companies.length,
|
||||||
|
separatorBuilder: (_, __) => const SizedBox(height: 12),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final company = controller.companies[index];
|
||||||
|
return Card(
|
||||||
|
elevation: 2,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12)),
|
||||||
|
child: ListTile(
|
||||||
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16, vertical: 8),
|
||||||
|
leading: Container(
|
||||||
|
padding: const EdgeInsets.all(10),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFF0F4C81).withValues(alpha: 0.1),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: const Icon(Icons.business, color: Color(0xFF0F4C81)),
|
||||||
|
),
|
||||||
|
title: Text(company['name'] ?? '',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold, fontSize: 16)),
|
||||||
|
subtitle: Text('الرقم الضريبي: ${company['tax_identification_number'] ?? 'غير محدد'}'),
|
||||||
|
trailing: const Icon(Icons.arrow_forward_ios, size: 16),
|
||||||
|
onTap: () {
|
||||||
|
controller.selectCompany(company['id'], company['name'] ?? '');
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildScanner(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: Stack(
|
body: Stack(
|
||||||
children: [
|
children: [
|
||||||
@@ -131,27 +197,15 @@ class ScannerView extends GetView<ScannerController> {
|
|||||||
|
|
||||||
// 3. Upload Button
|
// 3. Upload Button
|
||||||
Positioned(
|
Positioned(
|
||||||
top: 20,
|
top: 40,
|
||||||
left: 80,
|
left: 20,
|
||||||
right: 80,
|
right: 20,
|
||||||
child: Obx(() => controller.capturedImages.isEmpty
|
child: Obx(() => controller.capturedImages.isEmpty
|
||||||
? const SizedBox()
|
? const SizedBox()
|
||||||
: ElevatedButton.icon(
|
: ElevatedButton.icon(
|
||||||
onPressed: controller.isProcessing.value
|
onPressed: controller.isProcessing.value
|
||||||
? null
|
? null
|
||||||
: () {
|
: () => controller.uploadBatch(),
|
||||||
if (controller.companies.isEmpty) {
|
|
||||||
AppSnackbar.showError(
|
|
||||||
'خطأ', 'لا توجد شركات مسجلة في حسابك');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (controller.companies.length == 1) {
|
|
||||||
controller
|
|
||||||
.uploadBatch(controller.companies[0]['id']);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
_showCompanySelectionDialog(context);
|
|
||||||
},
|
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: const Color(0xFF0F4C81),
|
backgroundColor: const Color(0xFF0F4C81),
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
@@ -170,9 +224,9 @@ class ScannerView extends GetView<ScannerController> {
|
|||||||
strokeWidth: 2))
|
strokeWidth: 2))
|
||||||
: const Icon(Icons.cloud_upload, color: Colors.white),
|
: const Icon(Icons.cloud_upload, color: Colors.white),
|
||||||
label: Text(
|
label: Text(
|
||||||
'رفع ${controller.capturedImages.length} فواتير',
|
'رفع ${controller.capturedImages.length} فواتير لـ ${controller.selectedCompanyName.value}',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: Colors.white),
|
color: Colors.white),
|
||||||
),
|
),
|
||||||
@@ -183,34 +237,5 @@ class ScannerView extends GetView<ScannerController> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showCompanySelectionDialog(BuildContext context) {
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => AlertDialog(
|
|
||||||
title: const Text('اختر الشركة',
|
|
||||||
textAlign: TextAlign.right,
|
|
||||||
style: TextStyle(fontFamily: 'El Messiri')),
|
|
||||||
content: SizedBox(
|
|
||||||
width: double.maxFinite,
|
|
||||||
child: ListView.builder(
|
|
||||||
shrinkWrap: true,
|
|
||||||
itemCount: controller.companies.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final company = controller.companies[index];
|
|
||||||
return ListTile(
|
|
||||||
title: Text(company['name'] ?? '', textAlign: TextAlign.right),
|
|
||||||
subtitle: Text(company['tax_identification_number'] ?? '',
|
|
||||||
textAlign: TextAlign.right),
|
|
||||||
leading: const Icon(Icons.business, color: Color(0xFF0F4C81)),
|
|
||||||
onTap: () {
|
|
||||||
Navigator.pop(context);
|
|
||||||
controller.uploadBatch(company['id']);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,20 +14,27 @@ class SettingsView extends GetView<SettingsController> {
|
|||||||
children: [
|
children: [
|
||||||
// Custom Top Bar
|
// Custom Top Bar
|
||||||
Container(
|
Container(
|
||||||
padding: EdgeInsets.only(top: MediaQuery.of(context).padding.top, left: 8, right: 8, bottom: 12),
|
padding: EdgeInsets.only(
|
||||||
|
top: MediaQuery.of(context).padding.top,
|
||||||
|
left: 8,
|
||||||
|
right: 8,
|
||||||
|
bottom: 12),
|
||||||
color: isDark ? const Color(0xFF1E1E2E) : const Color(0xFF0F4C81),
|
color: isDark ? const Color(0xFF1E1E2E) : const Color(0xFF0F4C81),
|
||||||
child: Row(
|
child: const Row(
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(width: 48),
|
SizedBox(width: 48),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
'الإعدادات',
|
'الإعدادات',
|
||||||
style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold),
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 48),
|
SizedBox(width: 48),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -51,7 +58,8 @@ class SettingsView extends GetView<SettingsController> {
|
|||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
_buildSectionTitle('الإشعارات', Icons.notifications_rounded, isDark),
|
_buildSectionTitle(
|
||||||
|
'الإشعارات', Icons.notifications_rounded, isDark),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
_buildSettingsCard(isDark, [
|
_buildSettingsCard(isDark, [
|
||||||
_buildSwitchTile(
|
_buildSwitchTile(
|
||||||
@@ -64,6 +72,34 @@ class SettingsView extends GetView<SettingsController> {
|
|||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
|
// Admin Section
|
||||||
|
Obx(() {
|
||||||
|
if (controller.userRole.value == 'admin' ||
|
||||||
|
controller.userRole.value == 'super_admin') {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
_buildSectionTitle('إدارة المكتب',
|
||||||
|
Icons.admin_panel_settings_rounded, isDark),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
_buildSettingsCard(isDark, [
|
||||||
|
_buildInfoTile(
|
||||||
|
icon: Icons.business_rounded,
|
||||||
|
title: 'الشركات والموظفين',
|
||||||
|
trailing: 'إدارة →',
|
||||||
|
isDark: isDark,
|
||||||
|
onTap: () {
|
||||||
|
Get.toNamed(AppRoutes.COMPANIES_MANAGEMENT);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}),
|
||||||
|
|
||||||
_buildSectionTitle('حول التطبيق', Icons.info_rounded, isDark),
|
_buildSectionTitle('حول التطبيق', Icons.info_rounded, isDark),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
_buildSettingsCard(isDark, [
|
_buildSettingsCard(isDark, [
|
||||||
@@ -105,7 +141,10 @@ class SettingsView extends GetView<SettingsController> {
|
|||||||
onPressed: () => _confirmDeleteAccount(context),
|
onPressed: () => _confirmDeleteAccount(context),
|
||||||
child: const Text(
|
child: const Text(
|
||||||
'حذف الحساب',
|
'حذف الحساب',
|
||||||
style: TextStyle(color: Colors.red, fontSize: 13, decoration: TextDecoration.underline),
|
style: TextStyle(
|
||||||
|
color: Colors.red,
|
||||||
|
fontSize: 13,
|
||||||
|
decoration: TextDecoration.underline),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -139,8 +178,14 @@ class SettingsView extends GetView<SettingsController> {
|
|||||||
),
|
),
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
(controller.userName.value.isNotEmpty ? controller.userName.value[0] : 'م').toUpperCase(),
|
(controller.userName.value.isNotEmpty
|
||||||
style: const TextStyle(color: Colors.white, fontSize: 24, fontWeight: FontWeight.bold),
|
? controller.userName.value[0]
|
||||||
|
: 'م')
|
||||||
|
.toUpperCase(),
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -150,27 +195,40 @@ class SettingsView extends GetView<SettingsController> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
controller.userName.value.isNotEmpty ? controller.userName.value : 'مستخدم مُصادَق',
|
controller.userName.value.isNotEmpty
|
||||||
style: const TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold),
|
? controller.userName.value
|
||||||
|
: 'مستخدم مُصادَق',
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Text(
|
Text(
|
||||||
controller.userPhone.value,
|
controller.userPhone.value,
|
||||||
style: TextStyle(color: Colors.white.withOpacity(0.7), fontSize: 13, fontFamily: 'monospace'),
|
style: TextStyle(
|
||||||
|
color: Colors.white.withOpacity(0.7),
|
||||||
|
fontSize: 13,
|
||||||
|
fontFamily: 'monospace'),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
padding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFD4AF37).withOpacity(0.2),
|
color: const Color(0xFFD4AF37).withOpacity(0.2),
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
border: Border.all(color: const Color(0xFFD4AF37).withOpacity(0.5)),
|
border: Border.all(
|
||||||
|
color: const Color(0xFFD4AF37).withOpacity(0.5)),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
controller.roleName,
|
controller.roleName,
|
||||||
style: const TextStyle(color: Color(0xFFF0D060), fontSize: 11, fontWeight: FontWeight.w600),
|
style: const TextStyle(
|
||||||
|
color: Color(0xFFF0D060),
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: FontWeight.w600),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -183,7 +241,11 @@ class SettingsView extends GetView<SettingsController> {
|
|||||||
children: [
|
children: [
|
||||||
Icon(icon, size: 18, color: const Color(0xFF0F4C81)),
|
Icon(icon, size: 18, color: const Color(0xFF0F4C81)),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text(title, style: TextStyle(fontSize: 15, fontWeight: FontWeight.w700, color: isDark ? Colors.white70 : const Color(0xFF0F4C81))),
|
Text(title,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: isDark ? Colors.white70 : const Color(0xFF0F4C81))),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -193,7 +255,8 @@ class SettingsView extends GetView<SettingsController> {
|
|||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: isDark ? const Color(0xFF1E1E2E) : Colors.white,
|
color: isDark ? const Color(0xFF1E1E2E) : Colors.white,
|
||||||
borderRadius: BorderRadius.circular(14),
|
borderRadius: BorderRadius.circular(14),
|
||||||
border: Border.all(color: isDark ? Colors.white10 : Colors.grey.shade200),
|
border:
|
||||||
|
Border.all(color: isDark ? Colors.white10 : Colors.grey.shade200),
|
||||||
),
|
),
|
||||||
child: Column(children: children),
|
child: Column(children: children),
|
||||||
);
|
);
|
||||||
@@ -218,8 +281,13 @@ class SettingsView extends GetView<SettingsController> {
|
|||||||
),
|
),
|
||||||
child: Icon(icon, color: const Color(0xFF0F4C81), size: 20),
|
child: Icon(icon, color: const Color(0xFF0F4C81), size: 20),
|
||||||
),
|
),
|
||||||
title: Text(title, style: TextStyle(fontWeight: FontWeight.w600, color: isDark ? Colors.white : Colors.black87)),
|
title: Text(title,
|
||||||
subtitle: Text(subtitle, style: TextStyle(fontSize: 12, color: isDark ? Colors.white38 : Colors.grey)),
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: isDark ? Colors.white : Colors.black87)),
|
||||||
|
subtitle: Text(subtitle,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12, color: isDark ? Colors.white38 : Colors.grey)),
|
||||||
trailing: Switch.adaptive(
|
trailing: Switch.adaptive(
|
||||||
value: value,
|
value: value,
|
||||||
onChanged: onChanged,
|
onChanged: onChanged,
|
||||||
@@ -247,8 +315,13 @@ class SettingsView extends GetView<SettingsController> {
|
|||||||
),
|
),
|
||||||
child: Icon(icon, color: const Color(0xFF0F4C81), size: 20),
|
child: Icon(icon, color: const Color(0xFF0F4C81), size: 20),
|
||||||
),
|
),
|
||||||
title: Text(title, style: TextStyle(fontWeight: FontWeight.w600, color: isDark ? Colors.white : Colors.black87)),
|
title: Text(title,
|
||||||
trailing: Text(trailing, style: TextStyle(fontSize: 13, color: isDark ? Colors.white38 : Colors.grey)),
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: isDark ? Colors.white : Colors.black87)),
|
||||||
|
trailing: Text(trailing,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13, color: isDark ? Colors.white38 : Colors.grey)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -262,10 +335,12 @@ class SettingsView extends GetView<SettingsController> {
|
|||||||
backgroundColor: const Color(0xFFFEE2E2),
|
backgroundColor: const Color(0xFFFEE2E2),
|
||||||
foregroundColor: const Color(0xFFDC2626),
|
foregroundColor: const Color(0xFFDC2626),
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
|
shape:
|
||||||
|
RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
|
||||||
),
|
),
|
||||||
icon: const Icon(Icons.logout_rounded),
|
icon: const Icon(Icons.logout_rounded),
|
||||||
label: const Text('تسجيل الخروج', style: TextStyle(fontWeight: FontWeight.w700, fontSize: 16)),
|
label: const Text('تسجيل الخروج',
|
||||||
|
style: TextStyle(fontWeight: FontWeight.w700, fontSize: 16)),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -287,7 +362,8 @@ class SettingsView extends GetView<SettingsController> {
|
|||||||
void _confirmDeleteAccount(BuildContext context) {
|
void _confirmDeleteAccount(BuildContext context) {
|
||||||
Get.defaultDialog(
|
Get.defaultDialog(
|
||||||
title: '⚠️ حذف الحساب',
|
title: '⚠️ حذف الحساب',
|
||||||
middleText: 'سيتم حذف جميع بياناتك نهائياً. هذا الإجراء لا يمكن التراجع عنه.',
|
middleText:
|
||||||
|
'سيتم حذف جميع بياناتك نهائياً. هذا الإجراء لا يمكن التراجع عنه.',
|
||||||
textConfirm: 'حذف نهائي',
|
textConfirm: 'حذف نهائي',
|
||||||
textCancel: 'إلغاء',
|
textCancel: 'إلغاء',
|
||||||
confirmTextColor: Colors.white,
|
confirmTextColor: Colors.white,
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
|
class VoiceResult {
|
||||||
|
final String action;
|
||||||
|
final Map<String, dynamic> params;
|
||||||
|
final String confirmation;
|
||||||
|
|
||||||
|
VoiceResult({
|
||||||
|
required this.action,
|
||||||
|
required this.params,
|
||||||
|
required this.confirmation,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class VoiceController extends GetxController {
|
||||||
|
var isListening = false.obs;
|
||||||
|
var isProcessing = false.obs;
|
||||||
|
var hasPermission = true.obs;
|
||||||
|
var recognizedText = ''.obs;
|
||||||
|
var errorMessage = ''.obs;
|
||||||
|
Rx<VoiceResult?> lastResult = Rx<VoiceResult?>(null);
|
||||||
|
|
||||||
|
void startListening() {
|
||||||
|
isListening.value = true;
|
||||||
|
errorMessage.value = '';
|
||||||
|
// Mock implementation
|
||||||
|
}
|
||||||
|
|
||||||
|
void stopListening() {
|
||||||
|
isListening.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user