feat(frontend): #50 — validation CGU et politique de confidentialité
- Modale de validation avec chargement des PDF distants, PdfViewPinch et barre de progression latérale (repli PdfView sous Windows). - Service documents légaux actifs, correction des URLs média, intégration au formulaire de présentation. - Documentation juridique et réorganisation (archive, index, tickets). Made-with: Cursor
This commit is contained in:
Binary file not shown.
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -57,6 +57,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.6"
|
||||
extension:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: extension
|
||||
sha256: be3a6b7f8adad2f6e2e8c63c895d19811fcf203e23466c6296267941d0ff4f24
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.6.0"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -113,6 +121,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.9.3+4"
|
||||
fixnum:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: fixnum
|
||||
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
@@ -397,6 +413,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
pdfx:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: pdfx
|
||||
sha256: "29db9b71d46bf2335e001f91693f2c3fbbf0760e4c2eb596bf4bafab211471c1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.9.2"
|
||||
photo_view:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: photo_view
|
||||
sha256: "1fc3d970a91295fbd1364296575f854c9863f225505c28c46e0a03e48960c75e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.15.0"
|
||||
platform:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -514,6 +546,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.1"
|
||||
synchronized:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: synchronized
|
||||
sha256: "0669c70faae6270521ee4f05bffd2919892d42d1276e6c495be80174b6bc0ef6"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.3.1"
|
||||
term_glyph:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -538,6 +578,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
universal_platform:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: universal_platform
|
||||
sha256: "64e16458a0ea9b99260ceb5467a214c1f298d647c659af1bff6d3bf82536b1ec"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
url_launcher:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -602,6 +650,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.4"
|
||||
uuid:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: uuid
|
||||
sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.5.3"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -20,6 +20,8 @@ dependencies:
|
||||
url_launcher: ^6.2.4
|
||||
http: ^1.2.2
|
||||
# flutter_secure_storage: ^9.0.0
|
||||
pdfx: ^2.5.0
|
||||
universal_platform: ^1.1.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
@@ -32,6 +34,7 @@ flutter:
|
||||
assets:
|
||||
- assets/images/ # Déclarer le dossier entier
|
||||
- assets/cards/ # Nouveau dossier de cartes
|
||||
- assets/documents/
|
||||
|
||||
fonts:
|
||||
- family: Merienda
|
||||
|
||||
@@ -47,6 +47,16 @@
|
||||
<script src="flutter.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<script src='https://cdn.jsdelivr.net/npm/pdfjs-dist@4.6.82/build/pdf.min.mjs' type='module'></script>
|
||||
<script type='module'>
|
||||
var { pdfjsLib } = globalThis;
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdn.jsdelivr.net/npm/pdfjs-dist@4.6.82/build/pdf.worker.mjs';
|
||||
|
||||
var pdfRenderOptions = {
|
||||
cMapUrl: 'https://cdn.jsdelivr.net/npm/pdfjs-dist@4.6.82/cmaps/',
|
||||
cMapPacked: true,
|
||||
}
|
||||
</script>
|
||||
<script>
|
||||
window.addEventListener('load', function(ev) {
|
||||
// Download main.dart.js
|
||||
|
||||
Reference in New Issue
Block a user