import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import '/models/creche_settings.dart'; const _bg = Color(0xFF0D1117); const _card = Color(0xFF161B22); const _blue = Color(0xFF4FC3F7); const _green = Color(0xFF2ECC71); const _red = Color(0xFFE74C3C); class SettingsScreen extends ConsumerStatefulWidget { const SettingsScreen({super.key}); @override ConsumerState createState() => _SettingsScreenState(); } class _SettingsScreenState extends ConsumerState { final _nameCtrl = TextEditingController(); final _addrCtrl = TextEditingController(); final _slogCtrl = TextEditingController(); final _latCtrl = TextEditingController(); final _lngCtrl = TextEditingController(); final _radCtrl = TextEditingController(); final _ipCtrl = TextEditingController(); List _ips = []; bool _loading = true; bool _saving = false; String? _error; @override void initState() { super.initState(); _load(); } @override void dispose() { _nameCtrl.dispose(); _addrCtrl.dispose(); _slogCtrl.dispose(); _latCtrl.dispose(); _lngCtrl.dispose(); _radCtrl.dispose(); _ipCtrl.dispose(); super.dispose(); } Future _load() async { setState(() { _loading = true; _error = null; }); try { final sb = Supabase.instance.client; var data = await sb.from('creche_settings').select().limit(1).maybeSingle(); if (data == null) { // Criar linha de configurações default await sb.from('creche_settings').upsert({ 'id': 1, 'name': 'Creche e Berçário Sementes do Futuro', 'slogan': 'Conforto, cuidado e aprendizagem', 'geofence_radius_meters': 150, 'allowed_ips': [], }); data = await sb.from('creche_settings').select().eq('id', 1).maybeSingle(); } if (data != null) { final s = CrecheSettings.fromMap(data); _nameCtrl.text = s.name; _addrCtrl.text = s.address ?? ''; _slogCtrl.text = s.slogan; _latCtrl.text = s.geofenceLat?.toString() ?? ''; _lngCtrl.text = s.geofenceLng?.toString() ?? ''; _radCtrl.text = s.geofenceRadiusMeters.toString(); _ips = List.from(s.allowedIps); } } catch (e) { if (mounted) setState(() => _error = e.toString()); } finally { if (mounted) setState(() => _loading = false); } } Future _save() async { setState(() { _saving = true; _error = null; }); try { await Supabase.instance.client.from('creche_settings').upsert({ 'id': 1, 'name': _nameCtrl.text.trim(), 'address': _addrCtrl.text.trim().isEmpty ? null : _addrCtrl.text.trim(), 'slogan': _slogCtrl.text.trim(), 'geofence_lat': double.tryParse(_latCtrl.text), 'geofence_lng': double.tryParse(_lngCtrl.text), 'geofence_radius_meters': int.tryParse(_radCtrl.text) ?? 150, 'allowed_ips': _ips, }); if (mounted) _snack('Configurações guardadas! ✓', ok: true); } catch (e) { if (mounted) setState(() => _error = e.toString()); } finally { if (mounted) setState(() => _saving = false); } } void _addIp() { final ip = _ipCtrl.text.trim(); if (ip.isNotEmpty && !_ips.contains(ip)) { setState(() { _ips.add(ip); _ipCtrl.clear(); }); } } void _snack(String msg, {bool ok = false}) => ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text(msg, style: const TextStyle(color: Colors.white)), backgroundColor: ok ? _green : _red, behavior: SnackBarBehavior.floating, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), )); @override Widget build(BuildContext context) { return Scaffold( backgroundColor: _bg, appBar: AppBar( backgroundColor: _card, elevation: 0, title: const Text('Configurações', style: TextStyle(color: _blue, fontWeight: FontWeight.bold)), actions: [ IconButton(icon: const Icon(Icons.refresh, color: _blue), onPressed: _load), ], ), body: _loading ? const Center(child: CircularProgressIndicator(color: _blue)) : _error != null ? _buildError() : _buildForm(), ); } // ── Error inline (sem widget separado que pode ter layout issues) ── Widget _buildError() => SingleChildScrollView( padding: const EdgeInsets.all(24), child: Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const SizedBox(height: 40), Container( padding: const EdgeInsets.all(18), decoration: BoxDecoration( color: _red.withOpacity(0.08), borderRadius: BorderRadius.circular(16), border: Border.all(color: _red.withOpacity(0.3))), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ const Row(children: [ Icon(Icons.error_outline, color: _red, size: 22), SizedBox(width: 8), Expanded(child: Text('Erro ao carregar configurações', style: TextStyle(color: _red, fontWeight: FontWeight.bold))), ]), const SizedBox(height: 10), Text(_error!, style: const TextStyle(color: Color(0xFFFF6B6B), fontSize: 11, fontFamily: 'monospace')), ]), ), const SizedBox(height: 16), // Diagnóstico Container( padding: const EdgeInsets.all(14), decoration: BoxDecoration(color: _card, borderRadius: BorderRadius.circular(14), border: Border.all(color: Colors.orange.withOpacity(0.3))), child: const Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('🔒 Possível causa: RLS em falta', style: TextStyle(color: Colors.orange, fontWeight: FontWeight.bold)), SizedBox(height: 8), Text('Corre o ficheiro FIX_COMPLETO_V3.sql no Supabase SQL Editor.', style: TextStyle(color: Color(0xFFAAAAAA), fontSize: 12, height: 1.5)), ]), ), const SizedBox(height: 24), // Botão com constraints explícitas — evita layout error SizedBox( width: double.infinity, height: 50, child: ElevatedButton.icon( onPressed: _load, icon: const Icon(Icons.refresh), label: const Text('Tentar novamente'), style: ElevatedButton.styleFrom( backgroundColor: _blue, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), ), ), ), ]), ); Widget _buildForm() => SingleChildScrollView( padding: const EdgeInsets.all(20), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ // ── DADOS DA CRECHE ────────────────────────────────────── _sec('🏫', 'Dados da Creche'), const SizedBox(height: 14), _field(_nameCtrl, 'Nome da Creche', Icons.business), const SizedBox(height: 12), _field(_addrCtrl, 'Endereço completo', Icons.location_city), const SizedBox(height: 12), _field(_slogCtrl, 'Slogan', Icons.format_quote), const SizedBox(height: 28), // ── GEOFENCE ───────────────────────────────────────────── _sec('📍', 'Geofence — Área de acesso'), const SizedBox(height: 8), Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: _blue.withOpacity(0.06), borderRadius: BorderRadius.circular(10), border: Border.all(color: _blue.withOpacity(0.2))), child: const Text( 'Define a área onde os funcionários podem fazer login.\nDeixa em branco para desactivar o geofence.', style: TextStyle(color: Color(0xFF888888), fontSize: 12, height: 1.5)), ), const SizedBox(height: 14), Row(children: [ Expanded(child: _field(_latCtrl, 'Latitude', Icons.explore, type: TextInputType.number)), const SizedBox(width: 12), Expanded(child: _field(_lngCtrl, 'Longitude', Icons.explore, type: TextInputType.number)), ]), const SizedBox(height: 12), _field(_radCtrl, 'Raio em metros (ex: 150)', Icons.radar, type: TextInputType.number), const SizedBox(height: 28), // ── IPs PERMITIDOS ──────────────────────────────────────── _sec('🔒', 'IPs Permitidos'), const SizedBox(height: 8), const Text('Restringe o login a IPs específicos. Deixa vazio para não restringir.', style: TextStyle(color: Color(0xFF888888), fontSize: 12)), const SizedBox(height: 12), if (_ips.isNotEmpty) ...[ Wrap( spacing: 8, runSpacing: 8, children: _ips.map((ip) => Chip( label: Text(ip, style: const TextStyle(color: Colors.white, fontSize: 12)), backgroundColor: const Color(0xFF1C2233), side: BorderSide(color: _blue.withOpacity(0.4)), deleteIcon: const Icon(Icons.close, size: 14, color: _red), onDeleted: () => setState(() => _ips.remove(ip)), )).toList(), ), const SizedBox(height: 12), ], // Row com TextField + botão — usando IntrinsicHeight para evitar layout issues Row(crossAxisAlignment: CrossAxisAlignment.center, children: [ Expanded( child: TextField( controller: _ipCtrl, style: const TextStyle(color: Colors.white, fontSize: 14), onSubmitted: (_) => _addIp(), decoration: InputDecoration( hintText: 'Ex: 192.168.1.1', hintStyle: const TextStyle(color: Color(0xFF555577), fontSize: 13), prefixIcon: const Icon(Icons.lan, color: _blue, size: 20), filled: true, fillColor: _card, enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: BorderSide(color: Colors.white.withOpacity(0.1))), focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: const BorderSide(color: _blue)), contentPadding: const EdgeInsets.symmetric(vertical: 14, horizontal: 12), ), ), ), const SizedBox(width: 10), // SizedBox explícito evita o bug w=Infinity no ElevatedButton dentro de Row SizedBox( height: 50, width: 80, child: ElevatedButton( onPressed: _addIp, style: ElevatedButton.styleFrom( backgroundColor: _blue, padding: EdgeInsets.zero, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), ), child: const Text('+ Add', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 13)), ), ), ]), const SizedBox(height: 36), // ── BOTÃO GUARDAR ───────────────────────────────────────── // SizedBox com width explícita previne BoxConstraints(w=Infinity) SizedBox( width: double.infinity, height: 54, child: DecoratedBox( decoration: BoxDecoration( gradient: LinearGradient(colors: _saving ? [const Color(0xFF1A3A4A), const Color(0xFF1A3A4A)] : [_blue, const Color(0xFF0288D1)]), borderRadius: BorderRadius.circular(14), boxShadow: _saving ? [] : [BoxShadow(color: _blue.withOpacity(0.25), blurRadius: 16, offset: const Offset(0, 6))], ), child: Material( color: Colors.transparent, child: InkWell( borderRadius: BorderRadius.circular(14), onTap: _saving ? null : _save, child: Center(child: _saving ? const SizedBox(height: 22, width: 22, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2.5)) : const Row(mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.save_outlined, color: Colors.white, size: 20), SizedBox(width: 10), Text('Guardar Configurações', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16)), ]), ), ), ), ), )]), ); Widget _sec(String icon, String title) => Padding( padding: const EdgeInsets.only(bottom: 4), child: Row(children: [ Text(icon, style: const TextStyle(fontSize: 20)), const SizedBox(width: 10), Text(title, style: const TextStyle(color: Colors.white, fontSize: 17, fontWeight: FontWeight.bold)), ]), ); Widget _field(TextEditingController c, String label, IconData icon, {TextInputType type = TextInputType.text}) => TextField( controller: c, keyboardType: type, style: const TextStyle(color: Colors.white, fontSize: 14), decoration: InputDecoration( labelText: label, labelStyle: const TextStyle(color: Color(0xFF888888), fontSize: 13), prefixIcon: Icon(icon, color: _blue, size: 20), filled: true, fillColor: _card, enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: Colors.white.withOpacity(0.1))), focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: const BorderSide(color: _blue, width: 1.5)), ), ); }