diff --git a/creche_app/lib/features/settings/settings_screen.dart b/creche_app/lib/features/settings/settings_screen.dart deleted file mode 100644 index 689ebaf..0000000 --- a/creche_app/lib/features/settings/settings_screen.dart +++ /dev/null @@ -1,329 +0,0 @@ -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)), - ), - ); -}