Eliminar creche_app/lib/features/settings/settings_screen.dart

This commit is contained in:
Alberto 2026-03-11 19:26:02 +00:00
parent 5cd496cd43
commit d07875afd3
1 changed files with 0 additions and 329 deletions

View File

@ -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<SettingsScreen> createState() => _SettingsScreenState();
}
class _SettingsScreenState extends ConsumerState<SettingsScreen> {
final _nameCtrl = TextEditingController();
final _addrCtrl = TextEditingController();
final _slogCtrl = TextEditingController();
final _latCtrl = TextEditingController();
final _lngCtrl = TextEditingController();
final _radCtrl = TextEditingController();
final _ipCtrl = TextEditingController();
List<String> _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<void> _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<void> _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)),
),
);
}