import 'dart:io'; import 'package:dio/dio.dart'; import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/app_router.dart'; import 'package:eatvio_app/core/firebase_api.dart'; import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/utils/toasts.dart'; import 'package:eatvio_app/core/widgets/custom_outlined_button.dart'; import 'package:eatvio_app/core/widgets/custom_text_form_field.dart'; import 'package:eatvio_app/features/dev_tools/test_cases.dart'; import 'package:eatvio_app/core/env_config.dart'; import 'package:eatvio_app/features/diary/presentation/pages/barcode_scanner_screen.dart'; import 'package:eatvio_app/features/barcode/repositories/barcode_repository.dart'; import 'package:eatvio_app/features/barcode/models/barcode_product_model.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'dart:async'; class DevToolsScreen extends StatelessWidget { const DevToolsScreen({super.key}); static const name = 'devToolsScreen'; static const path = '/dev-tools-screen'; @override Widget build(BuildContext context) { return const DevToolsScreenContent(); } } class DevToolsScreenContent extends StatefulWidget { const DevToolsScreenContent({super.key}); @override State createState() => _DevToolsScreenContentState(); } class _DevToolsScreenContentState extends State { String baseUrlForTest = ''; final List testCases = [ Test850task(), GetChatRoomPreviews(), GetChatRoom(), SendMessage(), GetChatMessages(), GetChatRoomIdByUserSlug(), GetBlogPostList(), GetRecipes(), CreateRecipeTask(), GetDiets(), GetPlans(), GetBlogPostComments(), SendBlogPostComment(), GetDietsComments(), SendDietsComment(), GetPlansComments(), SendPlansComment(), GetRecipesComments(), AuthServiceTest(), DiaryServiceTest(), BlockedUserServiceTest(), PlanServiceTest(), AboutRecipeServiceTest(), BlogPostServiceTest(), CalendarServiceTest(), CardsServiceTest(), ConsultantServiceTest(), ConsultationServiceTest(), CreateDietServiceTest(), CreatePlanServiceTest(), GlossaryServiceTest(), MyDietsServiceTest(), MyPlansServiceTest(), MyRecipesServiceTest(), NotificationsServiceTest(), PlansServiceTest(), ProfileServiceTest(), RegistrationServiceTest(), ResetPasswordServiceTest(), SettingsServiceTest(), WsServiceTest(), ]; final controller = TextEditingController(); late final List testsStates; @override void initState() { super.initState(); setState(() { baseUrlForTest = sl().baseUrl; }); testsStates = List.generate(testCases.length, (index) => TestState.notRun); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Для разработчиков')), body: SingleChildScrollView( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 10), child: Column( children: [ Padding( padding: const EdgeInsets.all(8.0), child: Row(children: [Text('BaseUrl: $baseUrlForTest')]), ), Row( children: [ Expanded( child: CustomTextFormField(controller: controller, hintText: 'type new base url'), ), IconButton( onPressed: () { setState(() { baseUrlForTest = controller.text; sl().setBaseUrl(baseUrlForTest.toLowerCase()); sl().options.baseUrl = baseUrlForTest.toLowerCase(); controller.clear(); }); }, icon: const Icon(Icons.done), ), ], ), const SizedBox(height: 10), CustomOutlinedButton( text: sl().baseUrl == EnvConfig.devBaseUrl ? 'Set production base url' : 'Set dev2 base url', onPressed: () { setState(() { if (sl().baseUrl == EnvConfig.baseUrl) { baseUrlForTest = EnvConfig.devBaseUrl; } else { baseUrlForTest = EnvConfig.baseUrl; } sl().setBaseUrl(baseUrlForTest.toLowerCase()); sl().options.baseUrl = baseUrlForTest.toLowerCase(); controller.clear(); }); }, buttonStyle: const ButtonStyle(), ), SizedBox(height: baseUrlForTest != sl().baseUrl ? 40 : 10), if (sl().baseUrl.toLowerCase() != EnvConfig.baseUrl.toLowerCase()) ListView.builder( shrinkWrap: true, itemCount: testCases.length, physics: const NeverScrollableScrollPhysics(), itemBuilder: (context, index) => Padding( padding: const EdgeInsets.only(bottom: 10.0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded(child: Text('${index + 1}. ${testCases[index].name}')), Row( mainAxisSize: MainAxisSize.min, children: [ CustomOutlinedButton( text: 'run', height: 20, width: 60, onPressed: () async { if (testsStates[index] == TestState.loading) return; setState(() { testsStates[index] = TestState.loading; }); try { final result = await testCases[index].run(baseUrl: baseUrlForTest); if (result.success) { setState(() { testsStates[index] = TestState.success; }); showSuccessToast('${testCases[index].name} passed'); } else { setState(() { testsStates[index] = TestState.fail; }); showErrorToast('${testCases[index].name} failed'); // Show AlertDialog with error description if (context.mounted) { showDialog( context: AppRouter.navigatorKey.currentContext!, builder: (context) => AlertDialog( title: const Text('Test Failed'), content: SingleChildScrollView(child: Text(result.error ?? 'No error details.')), actions: [TextButton(onPressed: () => Navigator.of(context).pop(), child: const Text('OK'))], ), ); } } } catch (e, stack) { setState(() { testsStates[index] = TestState.fail; }); showErrorToast('${testCases[index].name} failed'); // Show AlertDialog with error description if (context.mounted) { showDialog( context: AppRouter.navigatorKey.currentContext!, builder: (context) => AlertDialog( title: const Text('Test Failed'), content: SingleChildScrollView(child: Text('$e\n$stack')), actions: [TextButton(onPressed: () => Navigator.of(context).pop(), child: const Text('OK'))], ), ); } } }, buttonStyle: const ButtonStyle(), ), const SizedBox(width: 5), switch (testsStates[index]) { TestState.success => Container(width: 20, height: 20, color: Colors.green), TestState.fail => Container(width: 20, height: 20, color: Colors.red), TestState.notRun => Container(width: 20, height: 20, color: Colors.grey), TestState.loading => const SizedBox( width: 20, height: 20, child: SizedBox(height: 20, width: 20, child: Center(child: CircularProgressIndicator())), ), }, ], ), ], ), ), ), const SizedBox(height: 10), CustomOutlinedButton( text: 'add token to server', buttonStyle: const ButtonStyle(), onPressed: () async { try { final dio = sl(); final String platform; if (Platform.isAndroid) { platform = 'android'; } else { platform = 'ios'; } await dio.post('/api/profile/notification/token/register', data: {'token': await FirebaseApi().getToken(), 'platform': platform}); showInfoToast('Токен добавлен'); } catch (e) { showInfoToast('Не удалось добавть токен'); } }, ), const SizedBox(height: 10), CustomOutlinedButton( text: 'copy token', buttonStyle: const ButtonStyle(), onPressed: () async { try { final token = await FirebaseApi().getToken(); if (token != null) { Clipboard.setData(ClipboardData(text: token)); showInfoToast('Токен скопирован'); } else { showInfoToast('Token is null'); } } catch (e) { showInfoToast('Не получилось скопировать токен'); } }, ), const SizedBox(height: 10), CustomOutlinedButton( text: 'remove token from server', buttonStyle: const ButtonStyle(), onPressed: () async { try { final dio = sl(); final String platform; if (Platform.isAndroid) { platform = 'android'; } else { platform = 'ios'; } await dio.post('/api/profile/notification/token/unregister', data: {'token': await FirebaseApi().getToken(), 'platform': platform}); showInfoToast('Токен удалён'); } catch (e) { showInfoToast('Не удалось удалить токен'); } }, ), const SizedBox(height: 10), CustomOutlinedButton( text: 'send notification', buttonStyle: const ButtonStyle(), onPressed: () async { try { final dio = sl(); Future.delayed(const Duration(seconds: 3), () async { await dio.post('/api/profile/notification/token/test'); }); } catch (e) { showInfoToast('Не удалось добавть токен'); } }, ), const SizedBox(height: 10), CustomOutlinedButton( text: 'Сканирование QR/штрихкода', buttonStyle: const ButtonStyle(), onPressed: () async { if (!context.mounted) return; final barcode = await Navigator.of(context).push( MaterialPageRoute(builder: (context) => const BarcodeScannerScreen()), ); if (barcode != null && barcode.isNotEmpty && context.mounted) { await _handleBarcodeScanned(barcode, context); } }, ), const SizedBox(height: 300), ], ), ), ), ); } Future _handleBarcodeScanned(String barcode, BuildContext context) async { try { final barcodeRepository = sl(); showDialog( context: context, barrierDismissible: false, builder: (context) => const AlertDialog( content: Row( children: [ CircularProgressIndicator(), SizedBox(width: 16), Text('Загрузка данных...'), ], ), ), ); final result = await barcodeRepository.getProductByBarcode(barcode); if (!context.mounted) return; Navigator.of(context).pop(); // Закрываем индикатор загрузки if (result.type == BarcodeResultType.success && result.product != null) { // Данные получены сразу _showProductDialog(context, result.product!, barcode); } else if (result.type == BarcodeResultType.processing && result.taskId != null) { // Запущена задача обработки, нужно опрашивать статус await _pollTaskStatus(context, result.taskId!, barcode); } } catch (e) { if (!context.mounted) return; Navigator.of(context).pop(); // Закрываем индикатор загрузки если открыт showErrorToast('Ошибка: $e'); _showBarcodeDialog(context, barcode, null); } } Future _pollTaskStatus(BuildContext context, String taskId, String barcode) async { final barcodeRepository = sl(); showDialog( context: context, barrierDismissible: false, builder: (context) => AlertDialog( content: Column( mainAxisSize: MainAxisSize.min, children: [ const CircularProgressIndicator(), const SizedBox(height: 16), const Text('Обработка штрихкода...'), const SizedBox(height: 8), Text('Это может занять некоторое время', style: TextStyle(fontSize: 12, color: Colors.grey[600])), ], ), ), ); final completer = Completer(); int attempts = 0; const maxAttempts = 30; // Максимум 30 попыток (примерно 5 минут при интервале 10 секунд) // ignore: unused_local_variable final timer = Timer.periodic(const Duration(seconds: 10), (timer) async { attempts++; try { final status = await barcodeRepository.getTaskStatus(taskId); if (status.type == BarcodeTaskStatusType.completed && status.product != null) { timer.cancel(); if (!context.mounted) return; Navigator.of(context).pop(); // Закрываем индикатор загрузки _showProductDialog(context, status.product!, barcode); completer.complete(); } else if (status.type == BarcodeTaskStatusType.failed) { timer.cancel(); if (!context.mounted) return; Navigator.of(context).pop(); // Закрываем индикатор загрузки showErrorToast('Не удалось получить данные о продукте'); _showBarcodeDialog(context, barcode, null); completer.complete(); } else if (attempts >= maxAttempts) { timer.cancel(); if (!context.mounted) return; Navigator.of(context).pop(); // Закрываем индикатор загрузки showErrorToast('Превышено время ожидания'); _showBarcodeDialog(context, barcode, null); completer.complete(); } } catch (e) { timer.cancel(); if (!context.mounted) return; Navigator.of(context).pop(); // Закрываем индикатор загрузки showErrorToast('Ошибка при проверке статуса: $e'); _showBarcodeDialog(context, barcode, null); completer.complete(); } }); await completer.future; } void _showProductDialog(BuildContext context, BarcodeProductModel product, String barcode) { showDialog( context: context, builder: (context) => AlertDialog( title: const Text('Информация о продукте'), content: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ _buildInfoRow('Штрихкод:', barcode), if (product.brandName != null) _buildInfoRow('Бренд:', product.brandName!), if (product.description != null) _buildInfoRow('Описание:', product.description!), if (product.quantity != null) _buildInfoRow('Количество:', product.quantity!), if (product.market != null) _buildInfoRow('Рынок сбыта:', product.market!), if (product.calories != null) _buildInfoRow('Калорийность:', '${product.calories} ккал'), if (product.proteins != null) _buildInfoRow('Белки:', '${product.proteins} г'), if (product.fats != null) _buildInfoRow('Жиры:', '${product.fats} г'), if (product.carbohydrates != null) _buildInfoRow('Углеводы:', '${product.carbohydrates} г'), ], ), ), actions: [ TextButton( onPressed: () { Clipboard.setData(ClipboardData(text: barcode)); showInfoToast('Штрихкод скопирован в буфер обмена'); }, child: const Text('Скопировать код'), ), TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('Закрыть'), ), ], ), ); } void _showBarcodeDialog(BuildContext context, String barcode, BarcodeProductModel? product) { showDialog( context: context, builder: (context) => AlertDialog( title: const Text('Отсканированный код'), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(barcode), if (product == null) ...[ const SizedBox(height: 16), const Text('Данные о продукте не найдены', style: TextStyle(color: Colors.grey)), ], ], ), actions: [ TextButton( onPressed: () { Clipboard.setData(ClipboardData(text: barcode)); Navigator.of(context).pop(); showInfoToast('Код скопирован в буфер обмена'); }, child: const Text('Скопировать'), ), TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('Закрыть'), ), ], ), ); } Widget _buildInfoRow(String label, String value) { return Padding( padding: const EdgeInsets.only(bottom: 8), child: RichText( text: TextSpan( style: const TextStyle(color: Colors.black, fontSize: 14), children: [ TextSpan(text: '$label ', style: const TextStyle(fontWeight: FontWeight.bold)), TextSpan(text: value), ], ), ), ); } } enum TestState { success, fail, notRun, loading }