You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
HMG_Patient_App/lib/dev_tools/environment_config_sheet.dart

351 lines
13 KiB
Dart

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<EnvironmentConfigSheet> createState() => _EnvironmentConfigSheetState();
}
/// Convenience function — opens the sheet and returns true if app restart needed.
Future<bool> showEnvironmentConfigSheet(BuildContext context) async {
final result = await showModalBottomSheet<bool>(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (_) => const EnvironmentConfigSheet(),
);
return result ?? false;
}
class _EnvironmentConfigSheetState extends State<EnvironmentConfigSheet> {
final _envService = EnvironmentConfigService.instance;
AppEnvironmentPreset? _selectedPreset;
bool _isSaving = false;
@override
void initState() {
super.initState();
_loadCurrent();
}
Future<void> _loadCurrent() async {
final preset = await _envService.getCurrentPreset();
if (!mounted) return;
setState(() {
// If no preset is stored, fall back to production (first preset)
_selectedPreset = preset ?? kEnvironmentPresets.first;
});
}
void _selectPreset(AppEnvironmentPreset p) {
setState(() => _selectedPreset = p);
}
Future<void> _save() async {
if (_selectedPreset == null) return;
setState(() => _isSaving = true);
try {
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<void> _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);
final p = _selectedPreset;
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: _selectedPreset?.label == preset.label,
onTap: () => _selectPreset(preset),
))
.toList(),
),
const SizedBox(height: 24),
// ── URL preview (read-only) ────────────────────────────
if (p != null) ...[
const _SectionLabel('URLs for selected environment'),
const SizedBox(height: 10),
_UrlDisplay(label: 'Main Base URL', value: p.baseUrl),
const SizedBox(height: 8),
_UrlDisplay(label: 'RC Base URL', value: p.rcBaseUrl),
const SizedBox(height: 8),
_UrlDisplay(label: 'Pharmacy Base URL', value: p.pharmacyBaseUrl),
const SizedBox(height: 8),
_UrlDisplay(label: 'ExaCart (Offers) URL', value: p.exaCartBaseUrl),
const SizedBox(height: 8),
_UrlDisplay(label: 'PayFort Mode', value: p.payfortMode),
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;
const _Chip(
{required this.label, required this.selected, required this.onTap});
@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 ? const Color(0xFF2B353E) : Colors.white,
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: selected
? const Color(0xFF2B353E)
: Colors.grey.shade300,
width: selected ? 2 : 1),
),
child: Text(label,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: selected ? Colors.white : Colors.black87)),
),
);
}
}
class _UrlDisplay extends StatelessWidget {
final String label;
final String value;
const _UrlDisplay({required this.label, required this.value});
@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: 3),
Container(
width: double.infinity,
padding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 9),
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.grey.shade200),
),
child: Text(value,
style: const TextStyle(
fontSize: 12, color: Color(0xFF2B353E))),
),
],
);
}
}