Eliminar creche_app/lib/features/settings/settings_screen.dart
This commit is contained in:
parent
5cd496cd43
commit
d07875afd3
|
|
@ -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)),
|
||||
),
|
||||
);
|
||||
}
|
||||
Loading…
Reference in New Issue