merge(master): intégration develop — ticket #50, documents légaux et validation CGU
Squash merge de la branche develop dans master : un seul commit sur master pour regrouper la livraison liée au ticket #50 (affichage dynamique des CGU et de la politique de confidentialité à l'inscription) ainsi que la documentation et les ajustements associés. Frontend (P'titsPas) : modale de validation bloquante avec onglets CGU / confidentialité, chargement des PDF depuis l'API des documents légaux actifs, affichage via pdfx (PdfViewPinch et barre de progression latérale sur le web, repli PdfView sous Windows). Branchement dans le flux d'inscription (écran de présentation), enrichissement du UserService et correction des chemins média pour les URL absolues. Documentation: jeux de fichiers juridiques (markdown et PDF de référence), réorganisation (dossier docs/juridique, archives, renommage du briefing), mises à jour index, liste de tickets et décisions projet. Scripts et métadonnées mineures (Gitea, pubspec, index web). Made-with: Cursor
This commit is contained in:
@@ -27,6 +27,12 @@ class ApiConfig {
|
||||
return u;
|
||||
}
|
||||
final base = baseUrl.replaceAll(RegExp(r'/+$'), '');
|
||||
// Le back renvoie parfois des chemins déjà préfixés `/api/v1/...` (ex. documents
|
||||
// légaux). Ne pas les coller à [baseUrl] sous peine de doubler `/api/v1`.
|
||||
if (u.startsWith('/api/')) {
|
||||
final origin = apiOrigin.replaceAll(RegExp(r'/+$'), '');
|
||||
return '$origin$u';
|
||||
}
|
||||
return u.startsWith('/') ? '$base$u' : '$base/$u';
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,39 @@ import 'package:p_tits_pas/models/dossier_unifie.dart';
|
||||
import 'package:p_tits_pas/services/api/api_config.dart';
|
||||
import 'package:p_tits_pas/services/api/tokenService.dart';
|
||||
|
||||
class DocumentActifInfo {
|
||||
final String id;
|
||||
final String type;
|
||||
final int version;
|
||||
final String url;
|
||||
|
||||
const DocumentActifInfo({
|
||||
required this.id,
|
||||
required this.type,
|
||||
required this.version,
|
||||
required this.url,
|
||||
});
|
||||
|
||||
factory DocumentActifInfo.fromJson(Map<String, dynamic> json) {
|
||||
return DocumentActifInfo(
|
||||
id: (json['id'] ?? '').toString(),
|
||||
type: (json['type'] ?? '').toString(),
|
||||
version: int.tryParse((json['version'] ?? '').toString()) ?? 0,
|
||||
url: (json['url'] ?? '').toString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DocumentsActifsInfo {
|
||||
final DocumentActifInfo cgu;
|
||||
final DocumentActifInfo privacy;
|
||||
|
||||
const DocumentsActifsInfo({
|
||||
required this.cgu,
|
||||
required this.privacy,
|
||||
});
|
||||
}
|
||||
|
||||
class UserService {
|
||||
static Future<Map<String, String>> _headers() async {
|
||||
final token = await TokenService.getToken();
|
||||
@@ -34,6 +67,46 @@ class UserService {
|
||||
return _toStr(err) ?? 'Erreur inconnue';
|
||||
}
|
||||
|
||||
/// Documents légaux actifs (CGU + privacy). GET /documents-legaux/actifs.
|
||||
static Future<DocumentsActifsInfo> getDocumentsLegauxActifs() async {
|
||||
final response = await http.get(
|
||||
Uri.parse('${ApiConfig.baseUrl}/documents-legaux/actifs'),
|
||||
headers: ApiConfig.headers,
|
||||
);
|
||||
if (response.statusCode != 200) {
|
||||
try {
|
||||
final err = jsonDecode(response.body);
|
||||
throw Exception(_errMessage(err is Map ? err['message'] : err));
|
||||
} catch (e) {
|
||||
if (e is Exception) rethrow;
|
||||
throw Exception(
|
||||
'Erreur chargement documents légaux (${response.statusCode})',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
final decoded = jsonDecode(response.body);
|
||||
if (decoded is! Map<String, dynamic>) {
|
||||
throw const FormatException('Réponse invalide');
|
||||
}
|
||||
final cgu = decoded['cgu'];
|
||||
final privacy = decoded['privacy'];
|
||||
if (cgu is! Map || privacy is! Map) {
|
||||
throw const FormatException('Documents actifs manquants');
|
||||
}
|
||||
return DocumentsActifsInfo(
|
||||
cgu: DocumentActifInfo.fromJson(Map<String, dynamic>.from(cgu)),
|
||||
privacy: DocumentActifInfo.fromJson(Map<String, dynamic>.from(privacy)),
|
||||
);
|
||||
} catch (e) {
|
||||
if (e is FormatException) rethrow;
|
||||
throw Exception(
|
||||
'Réponse invalide (documents légaux): ${e is Exception ? e.toString() : "format inattendu"}',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Utilisateurs en attente de validation (GET /users/pending). Ticket #107.
|
||||
static Future<List<AppUser>> getPendingUsers({String? role}) async {
|
||||
final query = role != null ? '?role=$role' : '';
|
||||
|
||||
@@ -0,0 +1,449 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:pdfx/pdfx.dart';
|
||||
import 'package:universal_platform/universal_platform.dart';
|
||||
|
||||
import '../services/api/api_config.dart';
|
||||
import '../services/user_service.dart';
|
||||
|
||||
/// Tons **turquoise / menthe** (charte : `#8AD0C8`) — plus doux que le vert
|
||||
/// Material « gazon ».
|
||||
abstract final class _ModaleCouleur {
|
||||
static const Color turquoise = Color(0xFF8AD0C8);
|
||||
static const Color turquoiseFonce = Color(0xFF5A9D94);
|
||||
static const Color mentheTresClair = Color(0xFFF3FAF8);
|
||||
static const Color mentheClair = Color(0xFFE2F3EF);
|
||||
static const Color ivoire = Color(0xFFFFFEF9);
|
||||
static const Color encre = Color(0xFF2F2F2F);
|
||||
}
|
||||
|
||||
enum LegalDocType {
|
||||
cgu,
|
||||
privacy,
|
||||
}
|
||||
|
||||
class CguPrivacyValidationDialog extends StatefulWidget {
|
||||
const CguPrivacyValidationDialog({super.key});
|
||||
|
||||
@override
|
||||
State<CguPrivacyValidationDialog> createState() =>
|
||||
_CguPrivacyValidationDialogState();
|
||||
}
|
||||
|
||||
class _CguPrivacyValidationDialogState extends State<CguPrivacyValidationDialog> {
|
||||
bool _loadingLegalDocs = false;
|
||||
DocumentActifInfo? _cguDoc;
|
||||
DocumentActifInfo? _privacyDoc;
|
||||
LegalDocType _selectedDocType = LegalDocType.cgu;
|
||||
|
||||
static const double _cornerRadius = 16;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadActiveLegalDocs();
|
||||
}
|
||||
|
||||
Future<void> _loadActiveLegalDocs() async {
|
||||
setState(() => _loadingLegalDocs = true);
|
||||
try {
|
||||
final docs = await UserService.getDocumentsLegauxActifs();
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_cguDoc = docs.cgu;
|
||||
_privacyDoc = docs.privacy;
|
||||
});
|
||||
} catch (_) {
|
||||
// Silencieux : message dans la zone PDF si besoin.
|
||||
} finally {
|
||||
if (mounted) setState(() => _loadingLegalDocs = false);
|
||||
}
|
||||
}
|
||||
|
||||
/// Sur le web, [PdfDocument.openFile] n’est pas implémenté : on charge les
|
||||
/// octets puis [PdfDocument.openData].
|
||||
Future<Uint8List> _fetchPdfBytes(String pathOrUrl) async {
|
||||
final absolute = ApiConfig.absoluteMediaUrl(pathOrUrl);
|
||||
final response = await http.get(Uri.parse(absolute));
|
||||
if (response.statusCode != 200) {
|
||||
throw StateError('PDF indisponible (HTTP ${response.statusCode})');
|
||||
}
|
||||
return response.bodyBytes;
|
||||
}
|
||||
|
||||
Future<PdfDocument>? get _currentPdfDocumentFuture {
|
||||
switch (_selectedDocType) {
|
||||
case LegalDocType.cgu:
|
||||
if (_cguDoc == null) return null;
|
||||
return PdfDocument.openData(_fetchPdfBytes(_cguDoc!.url));
|
||||
case LegalDocType.privacy:
|
||||
if (_privacyDoc == null) return null;
|
||||
return PdfDocument.openData(_fetchPdfBytes(_privacyDoc!.url));
|
||||
}
|
||||
}
|
||||
|
||||
String get _dialogTitle {
|
||||
switch (_selectedDocType) {
|
||||
case LegalDocType.cgu:
|
||||
return 'Conditions Générales d’Utilisation';
|
||||
case LegalDocType.privacy:
|
||||
return 'Politique de Confidentialité';
|
||||
}
|
||||
}
|
||||
|
||||
bool get _canValidate {
|
||||
return (_cguDoc != null && _privacyDoc != null) ||
|
||||
(_cguDoc != null &&
|
||||
_privacyDoc == null &&
|
||||
_selectedDocType == LegalDocType.cgu) ||
|
||||
(_privacyDoc != null &&
|
||||
_cguDoc == null &&
|
||||
_selectedDocType == LegalDocType.privacy);
|
||||
}
|
||||
|
||||
/// [PdfViewPinch] n’est pas supporté sur Windows (pdfx) : repli [PdfView].
|
||||
Widget _buildPdfBody() {
|
||||
final doc = _currentPdfDocumentFuture!;
|
||||
final key = ValueKey(_selectedDocType);
|
||||
if (UniversalPlatform.isWindows) {
|
||||
return PdfView(
|
||||
key: key,
|
||||
scrollDirection: Axis.vertical,
|
||||
controller: PdfController(document: doc),
|
||||
backgroundDecoration: const BoxDecoration(color: Colors.white),
|
||||
);
|
||||
}
|
||||
return _PdfPaneWithScrollbar(
|
||||
key: key,
|
||||
documentFuture: doc,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final size = MediaQuery.sizeOf(context);
|
||||
final dialogW = size.width * 0.92;
|
||||
final dialogH = size.height * 0.88;
|
||||
|
||||
return Dialog(
|
||||
insetPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 20),
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(_cornerRadius),
|
||||
child: Material(
|
||||
color: _ModaleCouleur.ivoire,
|
||||
elevation: 6,
|
||||
shadowColor: _ModaleCouleur.encre.withValues(alpha: 0.12),
|
||||
child: SizedBox(
|
||||
width: dialogW,
|
||||
height: dialogH,
|
||||
child: Column(
|
||||
children: [
|
||||
_buildHeader(context),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 8, 12, 4),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
_tabButton(
|
||||
context,
|
||||
label: 'CGU',
|
||||
selected: _selectedDocType == LegalDocType.cgu,
|
||||
enabled: !_loadingLegalDocs && _cguDoc != null,
|
||||
onTap: () => setState(
|
||||
() => _selectedDocType = LegalDocType.cgu,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
_tabButton(
|
||||
context,
|
||||
label: 'Confidentialité',
|
||||
selected: _selectedDocType == LegalDocType.privacy,
|
||||
enabled: !_loadingLegalDocs && _privacyDoc != null,
|
||||
onTap: () => setState(
|
||||
() => _selectedDocType = LegalDocType.privacy,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 4, 12, 8),
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: _ModaleCouleur.turquoiseFonce.withValues(
|
||||
alpha: 0.35,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: _loadingLegalDocs
|
||||
? const Center(
|
||||
child: CircularProgressIndicator(
|
||||
color: _ModaleCouleur.turquoiseFonce,
|
||||
strokeWidth: 3,
|
||||
),
|
||||
)
|
||||
: _currentPdfDocumentFuture != null
|
||||
? _buildPdfBody()
|
||||
: Center(
|
||||
child: Text(
|
||||
'Document non disponible.',
|
||||
style: GoogleFonts.merienda(
|
||||
color: _ModaleCouleur.encre
|
||||
.withValues(alpha: 0.75),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
||||
child: Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 520),
|
||||
child: FilledButton(
|
||||
onPressed: _canValidate
|
||||
? () => Navigator.of(context).pop(true)
|
||||
: null,
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: _ModaleCouleur.turquoiseFonce,
|
||||
foregroundColor: Colors.white,
|
||||
disabledBackgroundColor:
|
||||
_ModaleCouleur.mentheClair.withValues(alpha: 0.7),
|
||||
disabledForegroundColor:
|
||||
_ModaleCouleur.encre.withValues(alpha: 0.45),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 20,
|
||||
vertical: 14,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
'Valider les CGU et la Politique '
|
||||
'de Confidentialité',
|
||||
textAlign: TextAlign.center,
|
||||
style: GoogleFonts.merienda(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader(BuildContext context) {
|
||||
return DecoratedBox(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
_ModaleCouleur.mentheTresClair,
|
||||
_ModaleCouleur.mentheClair,
|
||||
],
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 8),
|
||||
child: Text(
|
||||
_dialogTitle,
|
||||
style: GoogleFonts.merienda(
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: _ModaleCouleur.encre,
|
||||
height: 1.25,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
tooltip: 'Fermer',
|
||||
icon: Icon(
|
||||
Icons.close,
|
||||
color: _ModaleCouleur.encre.withValues(alpha: 0.55),
|
||||
),
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _tabButton(
|
||||
BuildContext context, {
|
||||
required String label,
|
||||
required bool selected,
|
||||
required bool enabled,
|
||||
required VoidCallback onTap,
|
||||
}) {
|
||||
return OutlinedButton(
|
||||
onPressed: enabled ? onTap : null,
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: _ModaleCouleur.encre,
|
||||
backgroundColor: selected
|
||||
? _ModaleCouleur.turquoise
|
||||
: _ModaleCouleur.mentheTresClair,
|
||||
side: BorderSide(
|
||||
color: _ModaleCouleur.turquoiseFonce.withValues(alpha: 0.55),
|
||||
width: 1.5,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 10),
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
style: GoogleFonts.merienda(
|
||||
fontSize: 13,
|
||||
fontWeight: selected ? FontWeight.w600 : FontWeight.w500,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// [PdfViewPinch] s’appuie sur un [InteractiveViewer], sans vrai [Scrollable] :
|
||||
/// barre verticale reliée à [PdfControllerPinch.documentProgress] pour que le
|
||||
/// « chariot » reste visible (surtout sur le web où la barre OS est souvent
|
||||
/// masquée).
|
||||
class _PdfPaneWithScrollbar extends StatefulWidget {
|
||||
const _PdfPaneWithScrollbar({
|
||||
super.key,
|
||||
required this.documentFuture,
|
||||
});
|
||||
|
||||
final Future<PdfDocument> documentFuture;
|
||||
|
||||
@override
|
||||
State<_PdfPaneWithScrollbar> createState() => _PdfPaneWithScrollbarState();
|
||||
}
|
||||
|
||||
class _PdfPaneWithScrollbarState extends State<_PdfPaneWithScrollbar> {
|
||||
static const double _barW = 14;
|
||||
static const double _thumbW = 9;
|
||||
static const double _minThumbH = 44;
|
||||
|
||||
late final PdfControllerPinch _ctrl;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_ctrl = PdfControllerPinch(document: widget.documentFuture);
|
||||
_ctrl.addListener(_onCtrl);
|
||||
}
|
||||
|
||||
void _onCtrl() {
|
||||
// [PdfViewPinch] met à jour documentProgress dans son propre listener :
|
||||
// replanifier pour lire la valeur après ce tour de notifications.
|
||||
Future.microtask(() {
|
||||
if (mounted) setState(() {});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_ctrl.removeListener(_onCtrl);
|
||||
_ctrl.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final raw = _ctrl.documentProgress;
|
||||
final safeP = (!raw.isFinite || raw.isNaN) ? 0.0 : raw.clamp(0.0, 1.0);
|
||||
|
||||
return Stack(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: _barW),
|
||||
child: PdfViewPinch(
|
||||
padding: 10,
|
||||
scrollDirection: Axis.vertical,
|
||||
minScale: 1,
|
||||
maxScale: 4,
|
||||
controller: _ctrl,
|
||||
backgroundDecoration: const BoxDecoration(color: Colors.white),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 6,
|
||||
right: 1,
|
||||
bottom: 6,
|
||||
width: _barW,
|
||||
child: LayoutBuilder(
|
||||
builder: (context, c) {
|
||||
final trackH = c.maxHeight;
|
||||
final thumbH = (trackH * 0.2).clamp(_minThumbH, trackH * 0.5);
|
||||
final maxTop = (trackH - thumbH).clamp(0.0, double.infinity);
|
||||
final top = safeP * maxTop;
|
||||
|
||||
return Semantics(
|
||||
label: 'Position dans le document',
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: _ModaleCouleur.turquoise.withValues(
|
||||
alpha: 0.35,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: top,
|
||||
left: (_barW - _thumbW) / 2,
|
||||
child: Container(
|
||||
width: _thumbW,
|
||||
height: thumbH,
|
||||
decoration: BoxDecoration(
|
||||
color: _ModaleCouleur.turquoiseFonce,
|
||||
borderRadius: BorderRadius.circular(_thumbW / 2),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x40000000),
|
||||
blurRadius: 3,
|
||||
offset: Offset(0, 1),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'custom_decorated_text_field.dart';
|
||||
import 'app_custom_checkbox.dart';
|
||||
@@ -9,6 +10,7 @@ import 'custom_navigation_button.dart';
|
||||
import 'hover_relief_widget.dart';
|
||||
import '../models/card_assets.dart';
|
||||
import '../config/display_config.dart';
|
||||
import 'cgu_privacy_validation_dialog.dart';
|
||||
|
||||
/// Widget générique pour le formulaire de présentation avec texte libre + CGU
|
||||
/// Supporte mode éditable et readonly, responsive mobile/desktop
|
||||
@@ -75,6 +77,52 @@ class _PresentationFormScreenState extends State<PresentationFormScreen> {
|
||||
widget.onSubmit(_textController.text, _cguAccepted);
|
||||
}
|
||||
|
||||
Future<void> _openCguValidationDialog() async {
|
||||
final validated = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => const CguPrivacyValidationDialog(),
|
||||
);
|
||||
|
||||
if (validated == true && mounted) {
|
||||
setState(() => _cguAccepted = true);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildCguValidationControl({
|
||||
required double fontSize,
|
||||
bool compact = false,
|
||||
}) {
|
||||
final label = compact
|
||||
? 'J’ai validé les CGU et la\nPolitique de confidentialité'
|
||||
: 'J’ai validé les Conditions Générales\nd’Utilisation et la Politique de confidentialité';
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Checkbox(
|
||||
value: _cguAccepted,
|
||||
onChanged: null,
|
||||
),
|
||||
Flexible(
|
||||
child: Text(
|
||||
label,
|
||||
style: GoogleFonts.merienda(fontSize: fontSize),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
TextButton.icon(
|
||||
onPressed: _openCguValidationDialog,
|
||||
icon: const Icon(Icons.open_in_new, size: 18),
|
||||
label: const Text('Consulter et valider les CGU'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final screenSize = MediaQuery.of(context).size;
|
||||
@@ -451,11 +499,13 @@ class _PresentationFormScreenState extends State<PresentationFormScreen> {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
AppCustomCheckbox(
|
||||
label: 'J\'accepte les Conditions Générales\nd\'Utilisation et la Politique de confidentialité',
|
||||
value: _cguAccepted,
|
||||
onChanged: config.isReadonly ? (v) {} : (value) => setState(() => _cguAccepted = value ?? false),
|
||||
),
|
||||
config.isReadonly
|
||||
? AppCustomCheckbox(
|
||||
label: 'J\'accepte les Conditions Générales\nd\'Utilisation et la Politique de confidentialité',
|
||||
value: _cguAccepted,
|
||||
onChanged: (v) {},
|
||||
)
|
||||
: _buildCguValidationControl(fontSize: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -507,12 +557,17 @@ class _PresentationFormScreenState extends State<PresentationFormScreen> {
|
||||
const SizedBox(height: 16),
|
||||
// Checkbox en bas
|
||||
Transform.scale(
|
||||
scale: 0.85,
|
||||
child: AppCustomCheckbox(
|
||||
label: 'J\'accepte les CGU et la\nPolitique de confidentialité',
|
||||
value: _cguAccepted,
|
||||
onChanged: config.isReadonly ? (v) {} : (value) => setState(() => _cguAccepted = value ?? false),
|
||||
),
|
||||
scale: 0.95,
|
||||
child: config.isReadonly
|
||||
? AppCustomCheckbox(
|
||||
label: 'J\'accepte les CGU et la\nPolitique de confidentialité',
|
||||
value: _cguAccepted,
|
||||
onChanged: (v) {},
|
||||
)
|
||||
: _buildCguValidationControl(
|
||||
fontSize: 14,
|
||||
compact: true,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -549,13 +604,19 @@ class _PresentationFormScreenState extends State<PresentationFormScreen> {
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: HoverReliefWidget(
|
||||
child: CustomNavigationButton(
|
||||
text: 'Suivant',
|
||||
style: NavigationButtonStyle.green,
|
||||
onPressed: _handleSubmit,
|
||||
width: double.infinity,
|
||||
height: 50,
|
||||
fontSize: 16,
|
||||
child: Opacity(
|
||||
opacity: _cguAccepted ? 1 : 0.55,
|
||||
child: AbsorbPointer(
|
||||
absorbing: !_cguAccepted,
|
||||
child: CustomNavigationButton(
|
||||
text: 'Suivant',
|
||||
style: NavigationButtonStyle.green,
|
||||
onPressed: _handleSubmit,
|
||||
width: double.infinity,
|
||||
height: 50,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user