feat(#95): implémenter la gestion Relais admin et le rattachement gestionnaire
Ajoute la section Paramètres territoriaux avec CRUD Relais, modale de saisie structurée, états visuels harmonisés, et rattachement d'un relais principal aux gestionnaires via l'API. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,13 +1,19 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:p_tits_pas/services/configuration_service.dart';
|
||||
import 'package:p_tits_pas/widgets/admin/relais_management_panel.dart';
|
||||
|
||||
/// Panneau Paramètres admin : Email (SMTP), Personnalisation, Avancé.
|
||||
class ParametresPanel extends StatefulWidget {
|
||||
/// Si true, après sauvegarde on redirige vers le login (première config). Sinon on reste sur la page.
|
||||
final bool redirectToLoginAfterSave;
|
||||
final int selectedSettingsTabIndex;
|
||||
|
||||
const ParametresPanel({super.key, this.redirectToLoginAfterSave = false});
|
||||
const ParametresPanel({
|
||||
super.key,
|
||||
this.redirectToLoginAfterSave = false,
|
||||
this.selectedSettingsTabIndex = 0,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ParametresPanel> createState() => _ParametresPanelState();
|
||||
@@ -33,10 +39,18 @@ class _ParametresPanelState extends State<ParametresPanel> {
|
||||
|
||||
void _createControllers() {
|
||||
final keys = [
|
||||
'smtp_host', 'smtp_port', 'smtp_user', 'smtp_password',
|
||||
'email_from_name', 'email_from_address',
|
||||
'app_name', 'app_url', 'app_logo_url',
|
||||
'password_reset_token_expiry_days', 'jwt_expiry_hours', 'max_upload_size_mb',
|
||||
'smtp_host',
|
||||
'smtp_port',
|
||||
'smtp_user',
|
||||
'smtp_password',
|
||||
'email_from_name',
|
||||
'email_from_address',
|
||||
'app_name',
|
||||
'app_url',
|
||||
'app_logo_url',
|
||||
'password_reset_token_expiry_days',
|
||||
'jwt_expiry_hours',
|
||||
'max_upload_size_mb',
|
||||
];
|
||||
for (final k in keys) {
|
||||
_controllers[k] = TextEditingController();
|
||||
@@ -93,18 +107,29 @@ class _ParametresPanelState extends State<ParametresPanel> {
|
||||
payload['smtp_auth_required'] = _smtpAuthRequired;
|
||||
payload['smtp_user'] = _controllers['smtp_user']!.text.trim();
|
||||
final pwd = _controllers['smtp_password']!.text.trim();
|
||||
if (pwd.isNotEmpty && pwd != '***********') payload['smtp_password'] = pwd;
|
||||
if (pwd.isNotEmpty && pwd != '***********') {
|
||||
payload['smtp_password'] = pwd;
|
||||
}
|
||||
payload['email_from_name'] = _controllers['email_from_name']!.text.trim();
|
||||
payload['email_from_address'] = _controllers['email_from_address']!.text.trim();
|
||||
payload['email_from_address'] =
|
||||
_controllers['email_from_address']!.text.trim();
|
||||
payload['app_name'] = _controllers['app_name']!.text.trim();
|
||||
payload['app_url'] = _controllers['app_url']!.text.trim();
|
||||
payload['app_logo_url'] = _controllers['app_logo_url']!.text.trim();
|
||||
final tokenDays = int.tryParse(_controllers['password_reset_token_expiry_days']!.text.trim());
|
||||
if (tokenDays != null) payload['password_reset_token_expiry_days'] = tokenDays;
|
||||
final jwtHours = int.tryParse(_controllers['jwt_expiry_hours']!.text.trim());
|
||||
if (jwtHours != null) payload['jwt_expiry_hours'] = jwtHours;
|
||||
final tokenDays = int.tryParse(
|
||||
_controllers['password_reset_token_expiry_days']!.text.trim());
|
||||
if (tokenDays != null) {
|
||||
payload['password_reset_token_expiry_days'] = tokenDays;
|
||||
}
|
||||
final jwtHours =
|
||||
int.tryParse(_controllers['jwt_expiry_hours']!.text.trim());
|
||||
if (jwtHours != null) {
|
||||
payload['jwt_expiry_hours'] = jwtHours;
|
||||
}
|
||||
final maxMb = int.tryParse(_controllers['max_upload_size_mb']!.text.trim());
|
||||
if (maxMb != null) payload['max_upload_size_mb'] = maxMb;
|
||||
if (maxMb != null) {
|
||||
payload['max_upload_size_mb'] = maxMb;
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
@@ -191,6 +216,10 @@ class _ParametresPanelState extends State<ParametresPanel> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (widget.selectedSettingsTabIndex == 1) {
|
||||
return const RelaisManagementPanel();
|
||||
}
|
||||
|
||||
if (_isLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
@@ -214,7 +243,8 @@ class _ParametresPanelState extends State<ParametresPanel> {
|
||||
}
|
||||
|
||||
final isSuccess = _message != null &&
|
||||
(_message!.startsWith('Configuration') || _message!.startsWith('Connexion'));
|
||||
(_message!.startsWith('Configuration') ||
|
||||
_message!.startsWith('Connexion'));
|
||||
|
||||
return Form(
|
||||
key: _formKey,
|
||||
@@ -234,12 +264,21 @@ class _ParametresPanelState extends State<ParametresPanel> {
|
||||
context,
|
||||
icon: Icons.email_outlined,
|
||||
title: 'Configuration Email (SMTP)',
|
||||
child: Column(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildField('smtp_host', 'Serveur SMTP', hint: 'mail.example.com'),
|
||||
_buildField(
|
||||
'smtp_host',
|
||||
'Serveur SMTP',
|
||||
hint: 'mail.example.com',
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
_buildField('smtp_port', 'Port SMTP', keyboard: TextInputType.number, hint: '25, 465, 587'),
|
||||
_buildField(
|
||||
'smtp_port',
|
||||
'Port SMTP',
|
||||
keyboard: TextInputType.number,
|
||||
hint: '25, 465, 587',
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 14),
|
||||
@@ -247,14 +286,17 @@ class _ParametresPanelState extends State<ParametresPanel> {
|
||||
children: [
|
||||
Checkbox(
|
||||
value: _smtpSecure,
|
||||
onChanged: (v) => setState(() => _smtpSecure = v ?? false),
|
||||
onChanged: (v) =>
|
||||
setState(() => _smtpSecure = v ?? false),
|
||||
activeColor: const Color(0xFF9CC5C0),
|
||||
),
|
||||
const Text('SSL/TLS (secure)'),
|
||||
const SizedBox(width: 24),
|
||||
Checkbox(
|
||||
value: _smtpAuthRequired,
|
||||
onChanged: (v) => setState(() => _smtpAuthRequired = v ?? false),
|
||||
onChanged: (v) => setState(
|
||||
() => _smtpAuthRequired = v ?? false,
|
||||
),
|
||||
activeColor: const Color(0xFF9CC5C0),
|
||||
),
|
||||
const Text('Authentification requise'),
|
||||
@@ -263,11 +305,19 @@ class _ParametresPanelState extends State<ParametresPanel> {
|
||||
),
|
||||
_buildField('smtp_user', 'Utilisateur SMTP'),
|
||||
const SizedBox(height: 14),
|
||||
_buildField('smtp_password', 'Mot de passe SMTP', obscure: true),
|
||||
_buildField(
|
||||
'smtp_password',
|
||||
'Mot de passe SMTP',
|
||||
obscure: true,
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
_buildField('email_from_name', 'Nom expéditeur'),
|
||||
const SizedBox(height: 14),
|
||||
_buildField('email_from_address', 'Email expéditeur', hint: 'no-reply@example.com'),
|
||||
_buildField(
|
||||
'email_from_address',
|
||||
'Email expéditeur',
|
||||
hint: 'no-reply@example.com',
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
@@ -277,8 +327,13 @@ class _ParametresPanelState extends State<ParametresPanel> {
|
||||
label: const Text('Tester la connexion SMTP'),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: const Color(0xFF2D6A4F),
|
||||
side: const BorderSide(color: Color(0xFF9CC5C0)),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
||||
side: const BorderSide(
|
||||
color: Color(0xFF9CC5C0),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 20,
|
||||
vertical: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -290,14 +345,22 @@ class _ParametresPanelState extends State<ParametresPanel> {
|
||||
context,
|
||||
icon: Icons.palette_outlined,
|
||||
title: 'Personnalisation',
|
||||
child: Column(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildField('app_name', 'Nom de l\'application'),
|
||||
const SizedBox(height: 14),
|
||||
_buildField('app_url', 'URL de l\'application', hint: 'https://app.example.com'),
|
||||
_buildField(
|
||||
'app_url',
|
||||
'URL de l\'application',
|
||||
hint: 'https://app.example.com',
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
_buildField('app_logo_url', 'URL du logo', hint: '/assets/logo.png'),
|
||||
_buildField(
|
||||
'app_logo_url',
|
||||
'URL du logo',
|
||||
hint: '/assets/logo.png',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -309,11 +372,23 @@ class _ParametresPanelState extends State<ParametresPanel> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildField('password_reset_token_expiry_days', 'Validité token MDP (jours)', keyboard: TextInputType.number),
|
||||
_buildField(
|
||||
'password_reset_token_expiry_days',
|
||||
'Validité token MDP (jours)',
|
||||
keyboard: TextInputType.number,
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
_buildField('jwt_expiry_hours', 'Validité session JWT (heures)', keyboard: TextInputType.number),
|
||||
_buildField(
|
||||
'jwt_expiry_hours',
|
||||
'Validité session JWT (heures)',
|
||||
keyboard: TextInputType.number,
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
_buildField('max_upload_size_mb', 'Taille max upload (MB)', keyboard: TextInputType.number),
|
||||
_buildField(
|
||||
'max_upload_size_mb',
|
||||
'Taille max upload (MB)',
|
||||
keyboard: TextInputType.number,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -327,7 +402,14 @@ class _ParametresPanelState extends State<ParametresPanel> {
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
child: _isSaving
|
||||
? const SizedBox(width: 24, height: 24, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white))
|
||||
? const SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: Colors.white,
|
||||
),
|
||||
)
|
||||
: const Text('Sauvegarder la configuration'),
|
||||
),
|
||||
),
|
||||
@@ -339,7 +421,8 @@ class _ParametresPanelState extends State<ParametresPanel> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSectionCard(BuildContext context, {required IconData icon, required String title, required Widget child}) {
|
||||
Widget _buildSectionCard(BuildContext context,
|
||||
{required IconData icon, required String title, required Widget child}) {
|
||||
return Card(
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
@@ -369,7 +452,8 @@ class _ParametresPanelState extends State<ParametresPanel> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildField(String key, String label, {bool obscure = false, TextInputType? keyboard, String? hint}) {
|
||||
Widget _buildField(String key, String label,
|
||||
{bool obscure = false, TextInputType? keyboard, String? hint}) {
|
||||
final c = _controllers[key];
|
||||
if (c == null) return const SizedBox.shrink();
|
||||
return TextFormField(
|
||||
@@ -381,7 +465,8 @@ class _ParametresPanelState extends State<ParametresPanel> {
|
||||
labelText: label,
|
||||
hintText: hint,
|
||||
border: const OutlineInputBorder(),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user