fix(#156): caler la modale AM sur les TF et unifier create/review.
ValidationFormMetrics (titres, TF, écarts) pilote la hauteur ; photo étirée sur le corps ; mêmes widgets create et validation. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -33,7 +33,8 @@ class _AmDossierCreateModalState extends State<AmDossierCreateModal> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static const double _modalWidth = 930;
|
static const double _modalWidth = 930;
|
||||||
static const double _bodyHeight = 435;
|
/// Hauteur calculée depuis 4 lignes de TF (voir [AmDossierWizard.shellBodyHeight]).
|
||||||
|
static double get _bodyHeight => AmDossierWizard.shellBodyHeight;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|||||||
@@ -76,6 +76,10 @@ class AmDossierWizard extends StatefulWidget {
|
|||||||
|
|
||||||
bool get isCreate => mode == AmDossierWizardMode.create;
|
bool get isCreate => mode == AmDossierWizardMode.create;
|
||||||
|
|
||||||
|
/// Hauteur corps modale AM — dérivée de [ValidationFormMetrics] (4 lignes).
|
||||||
|
static double get shellBodyHeight =>
|
||||||
|
ValidationFormMetrics.shellBodyHeightForRows(4);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<AmDossierWizard> createState() => _AmDossierWizardState();
|
State<AmDossierWizard> createState() => _AmDossierWizardState();
|
||||||
}
|
}
|
||||||
@@ -584,56 +588,45 @@ class _AmDossierWizardState extends State<AmDossierWizard> {
|
|||||||
|
|
||||||
Widget _buildStep0() {
|
Widget _buildStep0() {
|
||||||
if (_isCreate) {
|
if (_isCreate) {
|
||||||
return LayoutBuilder(
|
return IdentityBlock.editable(
|
||||||
builder: (context, constraints) {
|
title: 'Identité et coordonnées',
|
||||||
return SingleChildScrollView(
|
nomController: _nomCtrl,
|
||||||
child: ConstrainedBox(
|
prenomController: _prenomCtrl,
|
||||||
constraints: BoxConstraints(minWidth: constraints.maxWidth),
|
telephoneController: _telCtrl,
|
||||||
child: IdentityBlock.editable(
|
emailController: _emailCtrl,
|
||||||
title: 'Identité et coordonnées',
|
adresseController: _adresseCtrl,
|
||||||
nomController: _nomCtrl,
|
codePostalController: _cpCtrl,
|
||||||
prenomController: _prenomCtrl,
|
villeController: _villeCtrl,
|
||||||
telephoneController: _telCtrl,
|
|
||||||
emailController: _emailCtrl,
|
|
||||||
adresseController: _adresseCtrl,
|
|
||||||
codePostalController: _cpCtrl,
|
|
||||||
villeController: _villeCtrl,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
final u = _dossier.user;
|
final u = _dossier.user;
|
||||||
return LayoutBuilder(
|
return IdentityBlock.readOnlyFromUser(
|
||||||
builder: (context, constraints) {
|
u,
|
||||||
return SingleChildScrollView(
|
title: 'Identité et coordonnées',
|
||||||
child: ConstrainedBox(
|
emptyLabel: '–',
|
||||||
constraints: BoxConstraints(minWidth: constraints.maxWidth),
|
|
||||||
child: IdentityBlock.readOnlyFromUser(
|
|
||||||
u,
|
|
||||||
title: 'Identité et coordonnées',
|
|
||||||
emptyLabel: '–',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildStep1() {
|
Widget _buildStep1() {
|
||||||
|
// Modale calée sur les TF ; photo étirée sur toute la hauteur utile
|
||||||
|
// (largeur = [AdminAmPhotoFrame.columnWidthForHeight], cadre inclus).
|
||||||
return LayoutBuilder(
|
return LayoutBuilder(
|
||||||
builder: (context, c) {
|
builder: (context, c) {
|
||||||
final maxRowW = c.maxWidth;
|
final maxRowW = c.maxWidth;
|
||||||
final maxRowH = c.maxHeight;
|
final maxRowH = c.maxHeight.clamp(0.0, double.infinity);
|
||||||
const photoHeaderH = 0.0;
|
|
||||||
final bodyH = (maxRowH - photoHeaderH).clamp(0.0, double.infinity);
|
|
||||||
final idealPhotoW = bodyH * _idPhotoAspectRatio + 16;
|
|
||||||
final maxPhotoW = (maxRowW - _photoProGap - _proColumnMinWidth)
|
final maxPhotoW = (maxRowW - _photoProGap - _proColumnMinWidth)
|
||||||
.clamp(0.0, double.infinity);
|
.clamp(0.0, double.infinity);
|
||||||
var photoW = idealPhotoW.clamp(_photoColumnMinWidth, 360.0);
|
var photoW = AdminAmPhotoFrame.columnWidthForHeight(maxRowH)
|
||||||
|
.clamp(_photoColumnMinWidth, 360.0);
|
||||||
if (photoW > maxPhotoW) photoW = maxPhotoW;
|
if (photoW > maxPhotoW) photoW = maxPhotoW;
|
||||||
photoW = photoW.clamp(0.0, maxRowW - _photoProGap);
|
|
||||||
|
final form = _isCreate
|
||||||
|
? _buildCreateProFields()
|
||||||
|
: ValidationDetailSection(
|
||||||
|
title: 'Dossier professionnel',
|
||||||
|
fields: _photoProFields(_dossier),
|
||||||
|
rowLayout: _photoProRowLayout,
|
||||||
|
);
|
||||||
|
|
||||||
return Row(
|
return Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
@@ -651,22 +644,9 @@ class _AmDossierWizardState extends State<AmDossierWizard> {
|
|||||||
),
|
),
|
||||||
const SizedBox(width: _photoProGap),
|
const SizedBox(width: _photoProGap),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: LayoutBuilder(
|
child: Align(
|
||||||
builder: (context, constraints) {
|
alignment: Alignment.topCenter,
|
||||||
return SingleChildScrollView(
|
child: form,
|
||||||
child: ConstrainedBox(
|
|
||||||
constraints:
|
|
||||||
BoxConstraints(minWidth: constraints.maxWidth),
|
|
||||||
child: _isCreate
|
|
||||||
? _buildCreateProFields()
|
|
||||||
: ValidationDetailSection(
|
|
||||||
title: 'Dossier professionnel',
|
|
||||||
fields: _photoProFields(_dossier),
|
|
||||||
rowLayout: _photoProRowLayout,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -6,6 +6,50 @@ import 'package:p_tits_pas/utils/phone_utils.dart';
|
|||||||
import 'package:p_tits_pas/utils/postal_utils.dart';
|
import 'package:p_tits_pas/utils/postal_utils.dart';
|
||||||
import 'admin_detail_modal.dart';
|
import 'admin_detail_modal.dart';
|
||||||
|
|
||||||
|
/// Réglages des formulaires validation / wizard AM — **jouer sur ces 3 leviers**.
|
||||||
|
class ValidationFormMetrics {
|
||||||
|
ValidationFormMetrics._();
|
||||||
|
|
||||||
|
// --- 1. Titres de section ---
|
||||||
|
static const double sectionTitleFontSize = 16;
|
||||||
|
static const double sectionTitleGapBelow = 12;
|
||||||
|
|
||||||
|
// --- 2. TF : texte intérieur + padding vertical (= hauteur) ---
|
||||||
|
static const double fieldTextFontSize = 14;
|
||||||
|
static const double fieldContentPaddingV = 12;
|
||||||
|
static const double fieldContentPaddingH = 12;
|
||||||
|
/// Hauteur estimée du TF (texte + padding haut/bas + bordure).
|
||||||
|
static const double fieldHeight =
|
||||||
|
fieldTextFontSize + fieldContentPaddingV * 2 + 4;
|
||||||
|
|
||||||
|
// --- 3. Espace entre les lignes de TF ---
|
||||||
|
static const double rowGapBelow = 12;
|
||||||
|
|
||||||
|
// Libellé au-dessus du TF (titre du champ)
|
||||||
|
static const double fieldLabelFontSize = 13;
|
||||||
|
static const double fieldLabelGapBelow = 4;
|
||||||
|
|
||||||
|
static const TextStyle fieldTextStyle = TextStyle(
|
||||||
|
color: Colors.black87,
|
||||||
|
fontSize: fieldTextFontSize,
|
||||||
|
);
|
||||||
|
|
||||||
|
static double get sectionTitleBlockHeight =>
|
||||||
|
sectionTitleFontSize + sectionTitleGapBelow;
|
||||||
|
|
||||||
|
static double get labeledRowHeight =>
|
||||||
|
fieldLabelFontSize + fieldLabelGapBelow + fieldHeight + rowGapBelow;
|
||||||
|
|
||||||
|
/// Corps modale AM : padding wizard + titre + [rows] lignes + nav.
|
||||||
|
static double shellBodyHeightForRows(int rows) =>
|
||||||
|
20 * 2 + // padding wizard
|
||||||
|
4 + // espace haut
|
||||||
|
sectionTitleBlockHeight +
|
||||||
|
rows * labeledRowHeight +
|
||||||
|
24 + // avant nav
|
||||||
|
44; // boutons
|
||||||
|
}
|
||||||
|
|
||||||
/// Bloc type formulaire (titre de section + champs read-only) pour les modales de validation.
|
/// Bloc type formulaire (titre de section + champs read-only) pour les modales de validation.
|
||||||
/// [rowLayout] : même disposition que la création de compte, ex. [2, 2, 1, 2] = ligne de 2, ligne de 2, plein largeur, ligne de 2.
|
/// [rowLayout] : même disposition que la création de compte, ex. [2, 2, 1, 2] = ligne de 2, ligne de 2, plein largeur, ligne de 2.
|
||||||
/// [rowFlex] : flex par index de ligne (optionnel). Ex. {3: [2, 5]} = 4e ligne : code postal étroit (2), ville large (5).
|
/// [rowFlex] : flex par index de ligne (optionnel). Ex. {3: [2, 5]} = 4e ligne : code postal étroit (2), ville large (5).
|
||||||
@@ -20,12 +64,16 @@ class ValidationDetailSection extends StatelessWidget {
|
|||||||
/// Flex par ligne (index de ligne -> [flex1, flex2, ...]). Ex. {3: [2, 5]} pour Code postal | Ville.
|
/// Flex par ligne (index de ligne -> [flex1, flex2, ...]). Ex. {3: [2, 5]} pour Code postal | Ville.
|
||||||
final Map<int, List<int>>? rowFlex;
|
final Map<int, List<int>>? rowFlex;
|
||||||
|
|
||||||
|
/// Remplit la hauteur disponible (wizard AM étapes 1–2).
|
||||||
|
final bool expandVertically;
|
||||||
|
|
||||||
const ValidationDetailSection({
|
const ValidationDetailSection({
|
||||||
super.key,
|
super.key,
|
||||||
this.title,
|
this.title,
|
||||||
required this.fields,
|
required this.fields,
|
||||||
this.rowLayout,
|
this.rowLayout,
|
||||||
this.rowFlex,
|
this.rowFlex,
|
||||||
|
this.expandVertically = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -34,6 +82,7 @@ class ValidationDetailSection extends StatelessWidget {
|
|||||||
title: title,
|
title: title,
|
||||||
rowLayout: rowLayout,
|
rowLayout: rowLayout,
|
||||||
rowFlex: rowFlex,
|
rowFlex: rowFlex,
|
||||||
|
expandVertically: expandVertically,
|
||||||
fields: fields
|
fields: fields
|
||||||
.map(
|
.map(
|
||||||
(f) => ValidationLabeledField(
|
(f) => ValidationLabeledField(
|
||||||
@@ -53,6 +102,8 @@ class ValidationFormGrid extends StatelessWidget {
|
|||||||
final List<int>? rowLayout;
|
final List<int>? rowLayout;
|
||||||
final Map<int, List<int>>? rowFlex;
|
final Map<int, List<int>>? rowFlex;
|
||||||
final bool compact;
|
final bool compact;
|
||||||
|
/// Répartit la hauteur dispo entre les lignes (remplit le blanc sans scroll).
|
||||||
|
final bool expandVertically;
|
||||||
|
|
||||||
const ValidationFormGrid({
|
const ValidationFormGrid({
|
||||||
super.key,
|
super.key,
|
||||||
@@ -61,6 +112,7 @@ class ValidationFormGrid extends StatelessWidget {
|
|||||||
this.rowLayout,
|
this.rowLayout,
|
||||||
this.rowFlex,
|
this.rowFlex,
|
||||||
this.compact = false,
|
this.compact = false,
|
||||||
|
this.expandVertically = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -68,7 +120,7 @@ class ValidationFormGrid extends StatelessWidget {
|
|||||||
final layout = rowLayout ?? List.filled(fields.length, 1);
|
final layout = rowLayout ?? List.filled(fields.length, 1);
|
||||||
int index = 0;
|
int index = 0;
|
||||||
int rowIndex = 0;
|
int rowIndex = 0;
|
||||||
final rows = <Widget>[];
|
final rowWidgets = <Widget>[];
|
||||||
for (final count in layout) {
|
for (final count in layout) {
|
||||||
if (index >= fields.length) break;
|
if (index >= fields.length) break;
|
||||||
final rowFields = fields.skip(index).take(count).toList();
|
final rowFields = fields.skip(index).take(count).toList();
|
||||||
@@ -76,48 +128,73 @@ class ValidationFormGrid extends StatelessWidget {
|
|||||||
if (rowFields.isEmpty) continue;
|
if (rowFields.isEmpty) continue;
|
||||||
final flexForRow = rowFlex?[rowIndex];
|
final flexForRow = rowFlex?[rowIndex];
|
||||||
rowIndex++;
|
rowIndex++;
|
||||||
|
final labeled = rowFields
|
||||||
|
.map(
|
||||||
|
(f) => ValidationLabeledField(
|
||||||
|
label: f.label,
|
||||||
|
field: f.field,
|
||||||
|
expand: expandVertically,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
Widget row;
|
||||||
if (count == 1) {
|
if (count == 1) {
|
||||||
rows.add(Padding(
|
row = labeled.first;
|
||||||
padding: EdgeInsets.only(bottom: compact ? 8 : 12),
|
|
||||||
child: rowFields.first,
|
|
||||||
));
|
|
||||||
} else {
|
} else {
|
||||||
rows.add(Padding(
|
row = Row(
|
||||||
padding: EdgeInsets.only(bottom: compact ? 8 : 12),
|
crossAxisAlignment: expandVertically
|
||||||
child: Row(
|
? CrossAxisAlignment.stretch
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
for (int i = 0; i < rowFields.length; i++) ...[
|
for (int i = 0; i < labeled.length; i++) ...[
|
||||||
if (i > 0) SizedBox(width: compact ? 12 : 16),
|
if (i > 0) SizedBox(width: compact ? 12 : 16),
|
||||||
Expanded(
|
Expanded(
|
||||||
flex: (flexForRow != null && i < flexForRow.length)
|
flex: (flexForRow != null && i < flexForRow.length)
|
||||||
? flexForRow[i]
|
? flexForRow[i]
|
||||||
: 1,
|
: 1,
|
||||||
child: rowFields[i],
|
child: labeled[i],
|
||||||
),
|
),
|
||||||
],
|
|
||||||
],
|
],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expandVertically) {
|
||||||
|
rowWidgets.add(Expanded(child: row));
|
||||||
|
} else {
|
||||||
|
rowWidgets.add(Padding(
|
||||||
|
padding: EdgeInsets.only(
|
||||||
|
bottom: compact ? 8 : ValidationFormMetrics.rowGapBelow,
|
||||||
),
|
),
|
||||||
|
child: row,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final showTitle = title != null && title!.trim().isNotEmpty;
|
final showTitle = title != null && title!.trim().isNotEmpty;
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: expandVertically ? MainAxisSize.max : MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
if (showTitle) ...[
|
if (showTitle) ...[
|
||||||
Text(
|
Text(
|
||||||
title!.trim(),
|
title!.trim(),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: compact ? 15 : 16,
|
fontSize: compact
|
||||||
|
? ValidationFormMetrics.sectionTitleFontSize - 1
|
||||||
|
: ValidationFormMetrics.sectionTitleFontSize,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: Colors.black87,
|
color: Colors.black87,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(height: compact ? 8 : 12),
|
SizedBox(
|
||||||
|
height: compact
|
||||||
|
? 8
|
||||||
|
: ValidationFormMetrics.sectionTitleGapBelow,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
...rows,
|
...rowWidgets,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -134,8 +211,8 @@ class ValidationFieldDecoration {
|
|||||||
fillColor: Colors.grey.shade50,
|
fillColor: Colors.grey.shade50,
|
||||||
hintText: hint,
|
hintText: hint,
|
||||||
contentPadding: EdgeInsets.symmetric(
|
contentPadding: EdgeInsets.symmetric(
|
||||||
horizontal: compact ? 10 : 12,
|
horizontal: compact ? 10 : ValidationFormMetrics.fieldContentPaddingH,
|
||||||
vertical: compact ? 7 : 10,
|
vertical: compact ? 7 : ValidationFormMetrics.fieldContentPaddingV,
|
||||||
),
|
),
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(6),
|
borderRadius: BorderRadius.circular(6),
|
||||||
@@ -184,29 +261,31 @@ class ValidationFieldDecoration {
|
|||||||
class ValidationLabeledField extends StatelessWidget {
|
class ValidationLabeledField extends StatelessWidget {
|
||||||
final String label;
|
final String label;
|
||||||
final Widget field;
|
final Widget field;
|
||||||
|
final bool expand;
|
||||||
|
|
||||||
const ValidationLabeledField({
|
const ValidationLabeledField({
|
||||||
super.key,
|
super.key,
|
||||||
required this.label,
|
required this.label,
|
||||||
required this.field,
|
required this.field,
|
||||||
|
this.expand = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: expand ? MainAxisSize.max : MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
label,
|
label,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: ValidationFormMetrics.fieldLabelFontSize,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
color: Colors.grey.shade700,
|
color: Colors.grey.shade700,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
SizedBox(height: ValidationFormMetrics.fieldLabelGapBelow),
|
||||||
field,
|
if (expand) Expanded(child: field) else field,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -268,13 +347,16 @@ class ValidationEditableField extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (!compact) {
|
if (!compact) {
|
||||||
return TextField(
|
return _validationFieldFillHeight(
|
||||||
controller: controller,
|
TextField(
|
||||||
keyboardType: keyboardType,
|
controller: controller,
|
||||||
inputFormatters: inputFormatters,
|
keyboardType: keyboardType,
|
||||||
maxLines: 1,
|
inputFormatters: inputFormatters,
|
||||||
style: const TextStyle(color: Colors.black87, fontSize: 14),
|
maxLines: 1,
|
||||||
decoration: ValidationFieldDecoration.input(hint: hintText),
|
textAlignVertical: TextAlignVertical.center,
|
||||||
|
style: ValidationFormMetrics.fieldTextStyle,
|
||||||
|
decoration: ValidationFieldDecoration.input(hint: hintText),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
@@ -299,6 +381,20 @@ class ValidationEditableField extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Étire le champ si le parent impose une hauteur (grille [expandVertically]).
|
||||||
|
Widget _validationFieldFillHeight(Widget field) {
|
||||||
|
return LayoutBuilder(
|
||||||
|
builder: (context, c) {
|
||||||
|
if (!c.hasBoundedHeight || !c.maxHeight.isFinite) return field;
|
||||||
|
return SizedBox(
|
||||||
|
height: c.maxHeight,
|
||||||
|
width: double.infinity,
|
||||||
|
child: field,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// E-mail style validation — même supervision que login / création de compte :
|
/// E-mail style validation — même supervision que login / création de compte :
|
||||||
/// [EmailMaxLengthFormatter] + à la perte de focus : trim/minuscules + validation.
|
/// [EmailMaxLengthFormatter] + à la perte de focus : trim/minuscules + validation.
|
||||||
class ValidationEmailField extends StatefulWidget {
|
class ValidationEmailField extends StatefulWidget {
|
||||||
@@ -354,23 +450,27 @@ class _ValidationEmailFieldState extends State<ValidationEmailField> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return TextFormField(
|
return _validationFieldFillHeight(
|
||||||
key: _fieldKey,
|
TextFormField(
|
||||||
controller: widget.controller,
|
key: _fieldKey,
|
||||||
focusNode: _focusNode,
|
controller: widget.controller,
|
||||||
keyboardType: TextInputType.emailAddress,
|
focusNode: _focusNode,
|
||||||
autocorrect: false,
|
keyboardType: TextInputType.emailAddress,
|
||||||
enableSuggestions: false,
|
autocorrect: false,
|
||||||
autofillHints: const [AutofillHints.email],
|
enableSuggestions: false,
|
||||||
textInputAction: TextInputAction.next,
|
autofillHints: const [AutofillHints.email],
|
||||||
inputFormatters: const [EmailMaxLengthFormatter()],
|
textInputAction: TextInputAction.next,
|
||||||
style: const TextStyle(color: Colors.black87, fontSize: 14),
|
textAlignVertical: TextAlignVertical.center,
|
||||||
decoration: ValidationFieldDecoration.input(hint: widget.hintText).copyWith(
|
inputFormatters: const [EmailMaxLengthFormatter()],
|
||||||
errorStyle: TextStyle(color: Colors.red.shade700, fontSize: 11),
|
style: ValidationFormMetrics.fieldTextStyle,
|
||||||
errorMaxLines: 2,
|
decoration:
|
||||||
|
ValidationFieldDecoration.input(hint: widget.hintText).copyWith(
|
||||||
|
errorStyle: TextStyle(color: Colors.red.shade700, fontSize: 11),
|
||||||
|
errorMaxLines: 2,
|
||||||
|
),
|
||||||
|
validator: (value) =>
|
||||||
|
validateEmail(value, allowEmpty: widget.allowEmpty),
|
||||||
),
|
),
|
||||||
validator: (value) =>
|
|
||||||
validateEmail(value, allowEmpty: widget.allowEmpty),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -431,22 +531,25 @@ class _ValidationPostalCodeFieldState extends State<ValidationPostalCodeField> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return TextFormField(
|
return _validationFieldFillHeight(
|
||||||
key: _fieldKey,
|
TextFormField(
|
||||||
controller: widget.controller,
|
key: _fieldKey,
|
||||||
focusNode: _focusNode,
|
controller: widget.controller,
|
||||||
keyboardType: TextInputType.number,
|
focusNode: _focusNode,
|
||||||
textInputAction: TextInputAction.next,
|
keyboardType: TextInputType.number,
|
||||||
inputFormatters: kFrenchPostalCodeInputFormatters,
|
textInputAction: TextInputAction.next,
|
||||||
style: const TextStyle(color: Colors.black87, fontSize: 14),
|
textAlignVertical: TextAlignVertical.center,
|
||||||
decoration: ValidationFieldDecoration.input(
|
inputFormatters: kFrenchPostalCodeInputFormatters,
|
||||||
hint: widget.hintText ?? '5 chiffres',
|
style: ValidationFormMetrics.fieldTextStyle,
|
||||||
).copyWith(
|
decoration: ValidationFieldDecoration.input(
|
||||||
errorStyle: TextStyle(color: Colors.red.shade700, fontSize: 11),
|
hint: widget.hintText ?? '5 chiffres',
|
||||||
errorMaxLines: 2,
|
).copyWith(
|
||||||
|
errorStyle: TextStyle(color: Colors.red.shade700, fontSize: 11),
|
||||||
|
errorMaxLines: 2,
|
||||||
|
),
|
||||||
|
validator: (value) =>
|
||||||
|
validateFrenchPostalCode(value, allowEmpty: widget.allowEmpty),
|
||||||
),
|
),
|
||||||
validator: (value) =>
|
|
||||||
validateFrenchPostalCode(value, allowEmpty: widget.allowEmpty),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -506,20 +609,24 @@ class _ValidationPhoneFieldState extends State<ValidationPhoneField> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return TextFormField(
|
return _validationFieldFillHeight(
|
||||||
key: _fieldKey,
|
TextFormField(
|
||||||
controller: widget.controller,
|
key: _fieldKey,
|
||||||
focusNode: _focusNode,
|
controller: widget.controller,
|
||||||
keyboardType: TextInputType.phone,
|
focusNode: _focusNode,
|
||||||
textInputAction: TextInputAction.next,
|
keyboardType: TextInputType.phone,
|
||||||
inputFormatters: frenchPhoneInputFormatters,
|
textInputAction: TextInputAction.next,
|
||||||
style: const TextStyle(color: Colors.black87, fontSize: 14),
|
textAlignVertical: TextAlignVertical.center,
|
||||||
decoration: ValidationFieldDecoration.input(hint: widget.hintText).copyWith(
|
inputFormatters: frenchPhoneInputFormatters,
|
||||||
errorStyle: TextStyle(color: Colors.red.shade700, fontSize: 11),
|
style: ValidationFormMetrics.fieldTextStyle,
|
||||||
errorMaxLines: 2,
|
decoration:
|
||||||
|
ValidationFieldDecoration.input(hint: widget.hintText).copyWith(
|
||||||
|
errorStyle: TextStyle(color: Colors.red.shade700, fontSize: 11),
|
||||||
|
errorMaxLines: 2,
|
||||||
|
),
|
||||||
|
validator: (value) =>
|
||||||
|
validateFrenchNationalPhone(value, allowEmpty: widget.allowEmpty),
|
||||||
),
|
),
|
||||||
validator: (value) =>
|
|
||||||
validateFrenchNationalPhone(value, allowEmpty: widget.allowEmpty),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -591,23 +698,26 @@ class _ValidationNirFieldState extends State<ValidationNirField> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return TextFormField(
|
return _validationFieldFillHeight(
|
||||||
key: _fieldKey,
|
TextFormField(
|
||||||
controller: widget.controller,
|
key: _fieldKey,
|
||||||
focusNode: _focusNode,
|
controller: widget.controller,
|
||||||
keyboardType: TextInputType.text,
|
focusNode: _focusNode,
|
||||||
textInputAction: TextInputAction.next,
|
keyboardType: TextInputType.text,
|
||||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
textInputAction: TextInputAction.next,
|
||||||
inputFormatters: const [NirInputFormatter()],
|
textAlignVertical: TextAlignVertical.center,
|
||||||
style: const TextStyle(color: Colors.black87, fontSize: 14),
|
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||||
decoration: ValidationFieldDecoration.input(
|
inputFormatters: const [NirInputFormatter()],
|
||||||
hint: widget.hintText ?? '1 12 34 56 789 012 - 34',
|
style: ValidationFormMetrics.fieldTextStyle,
|
||||||
).copyWith(
|
decoration: ValidationFieldDecoration.input(
|
||||||
errorStyle: TextStyle(color: Colors.red.shade700, fontSize: 11),
|
hint: widget.hintText ?? '1 12 34 56 789 012 - 34',
|
||||||
errorMaxLines: 2,
|
).copyWith(
|
||||||
|
errorStyle: TextStyle(color: Colors.red.shade700, fontSize: 11),
|
||||||
|
errorMaxLines: 2,
|
||||||
|
),
|
||||||
|
onChanged: (_) => _fieldKey.currentState?.validate(),
|
||||||
|
validator: _validator,
|
||||||
),
|
),
|
||||||
onChanged: (_) => _fieldKey.currentState?.validate(),
|
|
||||||
validator: _validator,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -685,16 +795,19 @@ class _ValidationReadOnlyFieldState extends State<ValidationReadOnlyField> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (!widget.compact && widget.maxLines == 1) {
|
if (!widget.compact && widget.maxLines == 1) {
|
||||||
return TextField(
|
return _validationFieldFillHeight(
|
||||||
controller: _controller,
|
TextField(
|
||||||
readOnly: true,
|
controller: _controller,
|
||||||
enableInteractiveSelection: false,
|
readOnly: true,
|
||||||
style: TextStyle(
|
enableInteractiveSelection: false,
|
||||||
color: widget.error ? Colors.red.shade800 : Colors.black87,
|
textAlignVertical: TextAlignVertical.center,
|
||||||
fontSize: 14,
|
style: TextStyle(
|
||||||
fontWeight: widget.error ? FontWeight.w600 : null,
|
color: widget.error ? Colors.red.shade800 : Colors.black87,
|
||||||
|
fontSize: ValidationFormMetrics.fieldTextFontSize,
|
||||||
|
fontWeight: widget.error ? FontWeight.w600 : null,
|
||||||
|
),
|
||||||
|
decoration: ValidationFieldDecoration.readOnly(error: widget.error),
|
||||||
),
|
),
|
||||||
decoration: ValidationFieldDecoration.readOnly(error: widget.error),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:p_tits_pas/models/dossier_unifie.dart';
|
import 'package:p_tits_pas/models/dossier_unifie.dart';
|
||||||
import 'package:p_tits_pas/services/user_service.dart';
|
import 'package:p_tits_pas/services/user_service.dart';
|
||||||
|
import 'package:p_tits_pas/widgets/admin/am_dossier_wizard.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/validation_am_wizard.dart';
|
import 'package:p_tits_pas/widgets/admin/validation_am_wizard.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/validation_family_wizard.dart';
|
import 'package:p_tits_pas/widgets/admin/validation_family_wizard.dart';
|
||||||
|
|
||||||
@@ -76,8 +77,13 @@ class _ValidationDossierModalState extends State<ValidationDossierModal> {
|
|||||||
|
|
||||||
/// Largeur modale = 1,5 × 620.
|
/// Largeur modale = 1,5 × 620.
|
||||||
static const double _modalWidth = 930; // 620 * 1.5
|
static const double _modalWidth = 930; // 620 * 1.5
|
||||||
// Hauteur uniforme (ajustée +5px pour éviter l'overflow des étapes parents sans scroll).
|
static const double _familyBodyHeight = 435;
|
||||||
static const double _bodyHeight = 435;
|
|
||||||
|
double get _bodyHeight {
|
||||||
|
final d = _dossier;
|
||||||
|
if (d != null && d.isAm) return AmDossierWizard.shellBodyHeight;
|
||||||
|
return _familyBodyHeight;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|||||||
@@ -68,7 +68,9 @@ class IdentityValues {
|
|||||||
|
|
||||||
/// Bloc identité : Nom/Prénom, Tél/Email, Adresse, CP/Ville.
|
/// Bloc identité : Nom/Prénom, Tél/Email, Adresse, CP/Ville.
|
||||||
/// Grille partagée (création de compte, validation AM/famille, fiches admin, etc.).
|
/// Grille partagée (création de compte, validation AM/famille, fiches admin, etc.).
|
||||||
class IdentityBlock extends StatelessWidget { final String? title;
|
class IdentityBlock extends StatelessWidget {
|
||||||
|
final String? title;
|
||||||
|
final bool expandVertically;
|
||||||
|
|
||||||
final String? _nom;
|
final String? _nom;
|
||||||
final String? _prenom;
|
final String? _prenom;
|
||||||
@@ -92,6 +94,7 @@ class IdentityBlock extends StatelessWidget { final String? title;
|
|||||||
const IdentityBlock.readOnly({
|
const IdentityBlock.readOnly({
|
||||||
super.key,
|
super.key,
|
||||||
this.title,
|
this.title,
|
||||||
|
this.expandVertically = false,
|
||||||
required String nom,
|
required String nom,
|
||||||
required String prenom,
|
required String prenom,
|
||||||
required String telephone,
|
required String telephone,
|
||||||
@@ -117,6 +120,7 @@ class IdentityBlock extends StatelessWidget { final String? title;
|
|||||||
const IdentityBlock.editable({
|
const IdentityBlock.editable({
|
||||||
super.key,
|
super.key,
|
||||||
this.title,
|
this.title,
|
||||||
|
this.expandVertically = false,
|
||||||
required TextEditingController nomController,
|
required TextEditingController nomController,
|
||||||
required TextEditingController prenomController,
|
required TextEditingController prenomController,
|
||||||
required TextEditingController telephoneController,
|
required TextEditingController telephoneController,
|
||||||
@@ -143,11 +147,13 @@ class IdentityBlock extends StatelessWidget { final String? title;
|
|||||||
factory IdentityBlock.readOnlyValues({
|
factory IdentityBlock.readOnlyValues({
|
||||||
Key? key,
|
Key? key,
|
||||||
String? title,
|
String? title,
|
||||||
|
bool expandVertically = false,
|
||||||
required IdentityValues values,
|
required IdentityValues values,
|
||||||
}) {
|
}) {
|
||||||
return IdentityBlock.readOnly(
|
return IdentityBlock.readOnly(
|
||||||
key: key,
|
key: key,
|
||||||
title: title,
|
title: title,
|
||||||
|
expandVertically: expandVertically,
|
||||||
nom: values.nom,
|
nom: values.nom,
|
||||||
prenom: values.prenom,
|
prenom: values.prenom,
|
||||||
telephone: values.telephone,
|
telephone: values.telephone,
|
||||||
@@ -164,10 +170,12 @@ class IdentityBlock extends StatelessWidget { final String? title;
|
|||||||
Key? key,
|
Key? key,
|
||||||
String? title,
|
String? title,
|
||||||
String emptyLabel = 'Non défini',
|
String emptyLabel = 'Non défini',
|
||||||
|
bool expandVertically = false,
|
||||||
}) {
|
}) {
|
||||||
return IdentityBlock.readOnlyValues(
|
return IdentityBlock.readOnlyValues(
|
||||||
key: key,
|
key: key,
|
||||||
title: title,
|
title: title,
|
||||||
|
expandVertically: expandVertically,
|
||||||
values: IdentityValues.fromUser(user, emptyLabel: emptyLabel),
|
values: IdentityValues.fromUser(user, emptyLabel: emptyLabel),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -197,6 +205,7 @@ class IdentityBlock extends StatelessWidget { final String? title;
|
|||||||
title: title,
|
title: title,
|
||||||
rowLayout: rowLayout,
|
rowLayout: rowLayout,
|
||||||
rowFlex: rowFlex,
|
rowFlex: rowFlex,
|
||||||
|
expandVertically: expandVertically,
|
||||||
fields: [
|
fields: [
|
||||||
ValidationLabeledField(
|
ValidationLabeledField(
|
||||||
label: 'Nom',
|
label: 'Nom',
|
||||||
@@ -243,6 +252,7 @@ class IdentityBlock extends StatelessWidget { final String? title;
|
|||||||
title: title,
|
title: title,
|
||||||
rowLayout: rowLayout,
|
rowLayout: rowLayout,
|
||||||
rowFlex: rowFlex,
|
rowFlex: rowFlex,
|
||||||
|
expandVertically: expandVertically,
|
||||||
fields: [
|
fields: [
|
||||||
ValidationLabeledField(
|
ValidationLabeledField(
|
||||||
label: 'Nom',
|
label: 'Nom',
|
||||||
|
|||||||
Reference in New Issue
Block a user