import 'package:flutter/material.dart'; import 'package:hmg_patient_app/dev_tools/environment_config.dart'; /// Public class — can be used directly with showModalBottomSheet. class EnvironmentConfigSheet extends StatefulWidget { const EnvironmentConfigSheet({Key? key}) : super(key: key); @override State createState() => _EnvironmentConfigSheetState(); } /// Convenience function — opens the sheet and returns true if app restart needed. Future showEnvironmentConfigSheet(BuildContext context) async { final result = await showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, builder: (_) => const EnvironmentConfigSheet(), ); return result ?? false; } class _EnvironmentConfigSheetState extends State { final _envService = EnvironmentConfigService.instance; AppEnvironmentPreset? _selectedPreset; bool _isSaving = false; bool _isCustomMode = false; // Controllers for custom URL inputs final _baseUrlController = TextEditingController(); final _rcBaseUrlController = TextEditingController(); final _pharmacyBaseUrlController = TextEditingController(); final _exaCartBaseUrlController = TextEditingController(); String _selectedPayfortMode = 'production'; @override void initState() { super.initState(); _loadCurrent(); } @override void dispose() { _baseUrlController.dispose(); _rcBaseUrlController.dispose(); _pharmacyBaseUrlController.dispose(); _exaCartBaseUrlController.dispose(); super.dispose(); } Future _loadCurrent() async { final preset = await _envService.getCurrentPreset(); final currentBaseUrl = await _envService.getCurrentBaseUrl(); final currentRcUrl = await _envService.getCurrentRcUrl(); final currentPharmacyUrl = await _envService.getCurrentPharmacyUrl(); final currentExaCartUrl = await _envService.getCurrentExaCartUrl(); final currentPayfortMode = await _envService.getCurrentPayfortMode(); if (!mounted) return; setState(() { // If no preset is stored, fall back to production (first preset) _selectedPreset = preset ?? kEnvironmentPresets.first; // Load current values into text controllers _baseUrlController.text = currentBaseUrl; _rcBaseUrlController.text = currentRcUrl; _pharmacyBaseUrlController.text = currentPharmacyUrl; _exaCartBaseUrlController.text = currentExaCartUrl; _selectedPayfortMode = currentPayfortMode; // Check if current config is custom (not matching any preset) _isCustomMode = preset == null; }); } void _selectPreset(AppEnvironmentPreset p) { setState(() { _selectedPreset = p; _isCustomMode = false; // Update text fields with preset values _baseUrlController.text = p.baseUrl; _rcBaseUrlController.text = p.rcBaseUrl; _pharmacyBaseUrlController.text = p.pharmacyBaseUrl; _exaCartBaseUrlController.text = p.exaCartBaseUrl; _selectedPayfortMode = p.payfortMode; }); } void _enableCustomMode() { setState(() { _isCustomMode = true; _selectedPreset = null; }); } Future _save() async { setState(() => _isSaving = true); try { if (_isCustomMode) { // Save custom URLs directly await _envService.applyCustomEnvironment( baseUrl: _baseUrlController.text.trim(), rcBaseUrl: _rcBaseUrlController.text.trim(), pharmacyBaseUrl: _pharmacyBaseUrlController.text.trim(), exaCartBaseUrl: _exaCartBaseUrlController.text.trim(), payfortMode: _selectedPayfortMode, ); } else { // Save preset if (_selectedPreset == null) { setState(() => _isSaving = false); return; } await _envService.applyPreset(_selectedPreset!); } if (mounted) Navigator.of(context).pop(true); } catch (e) { setState(() => _isSaving = false); ScaffoldMessenger.of(context) .showSnackBar(SnackBar(content: Text('Failed to save: $e'))); } } Future _resetToDefault() async { setState(() => _isSaving = true); try { await _envService.resetToDefault(); if (mounted) Navigator.of(context).pop(true); } catch (e) { setState(() => _isSaving = false); ScaffoldMessenger.of(context) .showSnackBar(SnackBar(content: Text('Failed to reset: $e'))); } } @override Widget build(BuildContext context) { final mq = MediaQuery.of(context); return Container( height: mq.size.height * 0.85, decoration: const BoxDecoration( color: Colors.white, borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), child: Column( children: [ // Handle bar Container( margin: const EdgeInsets.only(top: 12, bottom: 4), width: 40, height: 4, decoration: BoxDecoration( color: Colors.grey.shade300, borderRadius: BorderRadius.circular(2)), ), // Title row Padding( padding: const EdgeInsets.fromLTRB(20, 12, 20, 4), child: Row( children: [ const Icon(Icons.tune, color: Color(0xFF2B353E)), const SizedBox(width: 10), const Expanded( child: Text('Environment Configuration', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w700, color: Color(0xFF2B353E))), ), IconButton( icon: const Icon(Icons.close), onPressed: () => Navigator.of(context).pop(false)), ], ), ), const Divider(height: 1), // Scrollable body Expanded( child: SingleChildScrollView( padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // ── Preset chips ────────────────────────────────────── const _SectionLabel('Select Environment'), const SizedBox(height: 10), Wrap( spacing: 8, runSpacing: 8, children: [ ...kEnvironmentPresets .map((preset) => _Chip( label: preset.label, selected: !_isCustomMode && _selectedPreset?.label == preset.label, onTap: () => _selectPreset(preset), )) .toList(), _Chip( label: 'Custom', selected: _isCustomMode, onTap: _enableCustomMode, isCustom: true, ), ], ), const SizedBox(height: 24), // ── URL configuration (editable in custom mode) ──────────────────────────── const _SectionLabel('Environment URLs'), const SizedBox(height: 10), _UrlTextField( label: 'Main Base URL', controller: _baseUrlController, enabled: _isCustomMode, ), const SizedBox(height: 12), _UrlTextField( label: 'RC Base URL', controller: _rcBaseUrlController, enabled: _isCustomMode, ), const SizedBox(height: 12), _UrlTextField( label: 'Pharmacy Base URL', controller: _pharmacyBaseUrlController, enabled: _isCustomMode, ), const SizedBox(height: 12), _UrlTextField( label: 'ExaCart (Offers) URL', controller: _exaCartBaseUrlController, enabled: _isCustomMode, ), const SizedBox(height: 12), // PayFort Mode selector Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('PayFort Mode', style: TextStyle( fontSize: 11, fontWeight: FontWeight.w600, color: Colors.grey)), const SizedBox(height: 6), Row( children: [ Expanded( child: _RadioOption( label: 'Production', value: 'production', groupValue: _selectedPayfortMode, enabled: _isCustomMode, onChanged: (val) => setState(() => _selectedPayfortMode = val!), ), ), const SizedBox(width: 12), Expanded( child: _RadioOption( label: 'Sandbox', value: 'sandbox', groupValue: _selectedPayfortMode, enabled: _isCustomMode, onChanged: (val) => setState(() => _selectedPayfortMode = val!), ), ), ], ), ], ), const SizedBox(height: 24), // ── Reset to Default ────────────────────────────────── InkWell( onTap: _isSaving ? null : _resetToDefault, borderRadius: BorderRadius.circular(10), child: Container( width: double.infinity, padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 14), decoration: BoxDecoration( border: Border.all(color: const Color(0xFFE74C3C)), borderRadius: BorderRadius.circular(10), color: const Color(0xFFFFF5F5)), child: Row( children: const [ Icon(Icons.restore, color: Color(0xFFE74C3C), size: 20), SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Reset to Default', style: TextStyle( fontSize: 13, fontWeight: FontWeight.w700, color: Color(0xFFE74C3C))), SizedBox(height: 2), Text( 'Clears saved config and uses the URLs defined in code (Production)', style: TextStyle( fontSize: 11, color: Color(0xFFE74C3C))), ], ), ), ], ), ), ), const SizedBox(height: 20), // ── Warning ─────────────────────────────────────────── Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: const Color(0xFFFFF3CD), borderRadius: BorderRadius.circular(8), border: Border.all(color: const Color(0xFFFFD700))), child: const Row(children: [ Icon(Icons.warning_amber_rounded, color: Color(0xFFE6A817), size: 18), SizedBox(width: 8), Expanded( child: Text( 'After saving, the app will restart to apply the new environment.', style: TextStyle( fontSize: 12, color: Color(0xFF856404)), ), ), ]), ), const SizedBox(height: 80), ], ), ), ), // ── Save button ─────────────────────────────────────────────── Container( padding: EdgeInsets.fromLTRB(20, 12, 20, 12 + mq.padding.bottom), decoration: BoxDecoration( color: Colors.white, boxShadow: [ BoxShadow( color: Colors.black.withAlpha(15), blurRadius: 8, offset: const Offset(0, -2)) ], ), child: SizedBox( width: double.infinity, height: 50, child: ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF2B353E), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10)), ), onPressed: _isSaving ? null : _save, child: _isSaving ? const SizedBox( width: 22, height: 22, child: CircularProgressIndicator( color: Colors.white, strokeWidth: 2)) : const Text('Save & Restart App', style: TextStyle( fontSize: 15, fontWeight: FontWeight.w600, color: Colors.white)), ), ), ), ], ), ); } } // ─── Small private helpers ──────────────────────────────────────────────────── class _SectionLabel extends StatelessWidget { final String text; const _SectionLabel(this.text); @override Widget build(BuildContext context) => Text(text, style: const TextStyle( fontSize: 13, fontWeight: FontWeight.w700, color: Color(0xFF2B353E))); } class _Chip extends StatelessWidget { final String label; final bool selected; final VoidCallback onTap; final bool isCustom; const _Chip( {required this.label, required this.selected, required this.onTap, this.isCustom = false}); @override Widget build(BuildContext context) { return GestureDetector( onTap: onTap, child: AnimatedContainer( duration: const Duration(milliseconds: 200), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8), decoration: BoxDecoration( color: selected ? (isCustom ? const Color(0xFF6C5CE7) : const Color(0xFF2B353E)) : Colors.white, borderRadius: BorderRadius.circular(20), border: Border.all( color: selected ? (isCustom ? const Color(0xFF6C5CE7) : const Color(0xFF2B353E)) : Colors.grey.shade300, width: selected ? 2 : 1), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ if (isCustom && selected) ...[ const Icon(Icons.edit, size: 14, color: Colors.white), const SizedBox(width: 4), ], Text(label, style: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: selected ? Colors.white : Colors.black87)), ], ), ), ); } } class _UrlTextField extends StatelessWidget { final String label; final TextEditingController controller; final bool enabled; const _UrlTextField({ required this.label, required this.controller, required this.enabled, }); @override Widget build(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(label, style: const TextStyle( fontSize: 11, fontWeight: FontWeight.w600, color: Colors.grey)), const SizedBox(height: 6), TextField( controller: controller, enabled: enabled, style: TextStyle( fontSize: 12, color: enabled ? const Color(0xFF2B353E) : Colors.grey.shade600), decoration: InputDecoration( filled: true, fillColor: enabled ? Colors.white : Colors.grey.shade100, contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: BorderSide(color: Colors.grey.shade300), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: BorderSide(color: Colors.grey.shade300), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: const BorderSide(color: Color(0xFF6C5CE7), width: 2), ), disabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: BorderSide(color: Colors.grey.shade200), ), ), ), ], ); } } class _RadioOption extends StatelessWidget { final String label; final String value; final String groupValue; final bool enabled; final Function(String?) onChanged; const _RadioOption({ required this.label, required this.value, required this.groupValue, required this.enabled, required this.onChanged, }); @override Widget build(BuildContext context) { final isSelected = value == groupValue; return GestureDetector( onTap: enabled ? () => onChanged(value) : null, child: Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), decoration: BoxDecoration( color: enabled ? (isSelected ? const Color(0xFF6C5CE7).withAlpha(25) : Colors.white) : Colors.grey.shade100, borderRadius: BorderRadius.circular(8), border: Border.all( color: enabled ? (isSelected ? const Color(0xFF6C5CE7) : Colors.grey.shade300) : Colors.grey.shade200, width: isSelected ? 2 : 1, ), ), child: Row( children: [ Container( width: 18, height: 18, decoration: BoxDecoration( shape: BoxShape.circle, border: Border.all( color: enabled ? (isSelected ? const Color(0xFF6C5CE7) : Colors.grey.shade400) : Colors.grey.shade300, width: 2, ), color: isSelected && enabled ? const Color(0xFF6C5CE7) : Colors.transparent, ), child: isSelected && enabled ? const Icon(Icons.circle, size: 10, color: Colors.white) : null, ), const SizedBox(width: 8), Text( label, style: TextStyle( fontSize: 12, fontWeight: FontWeight.w500, color: enabled ? (isSelected ? const Color(0xFF6C5CE7) : Colors.black87) : Colors.grey.shade500, ), ), ], ), ), ); } }