180 lines
5.1 KiB
Dart
180 lines
5.1 KiB
Dart
import 'package:flutter/services.dart';
|
||
import 'package:intl/intl.dart';
|
||
import 'package:p_tits_pas/utils/enfant_status_utils.dart';
|
||
|
||
/// Affiche une date ISO / parseable en `dd/MM/yyyy`, avec repli sur la chaîne ou [ifEmpty].
|
||
String formatIsoDateFr(String? s, {String ifEmpty = '–'}) {
|
||
if (s == null || s.trim().isEmpty) return ifEmpty;
|
||
try {
|
||
return DateFormat('dd/MM/yyyy').format(DateTime.parse(s.trim()));
|
||
} catch (_) {
|
||
return s.trim();
|
||
}
|
||
}
|
||
|
||
/// Affiche une date ISO en saisie guidée `jj / mm / aaaa`.
|
||
String formatIsoDateFrInput(String? s, {String ifEmpty = ''}) {
|
||
if (s == null || s.trim().isEmpty) return ifEmpty;
|
||
try {
|
||
final dt = DateTime.parse(s.trim());
|
||
return formatFrenchDateDigits(
|
||
DateFormat('ddMMyyyy').format(dt),
|
||
);
|
||
} catch (_) {
|
||
return formatFrenchDateDigits(s.replaceAll(RegExp(r'\D'), ''));
|
||
}
|
||
}
|
||
|
||
/// Formate jusqu’à 8 chiffres en `jj / mm / aaaa`.
|
||
String formatFrenchDateDigits(String digits) {
|
||
final d = digits.replaceAll(RegExp(r'\D'), '');
|
||
final limited = d.length > 8 ? d.substring(0, 8) : d;
|
||
final buf = StringBuffer();
|
||
for (var i = 0; i < limited.length; i++) {
|
||
if (i == 2 || i == 4) buf.write(' / ');
|
||
buf.write(limited[i]);
|
||
}
|
||
return buf.toString();
|
||
}
|
||
|
||
/// Convertit `dd/MM/yyyy`, `dd / MM / yyyy`, 8 chiffres ou ISO en `yyyy-MM-dd`.
|
||
String? parseFrDateToIso(String text) {
|
||
final t = text.trim();
|
||
if (t.isEmpty) return null;
|
||
|
||
final digits = t.replaceAll(RegExp(r'\D'), '');
|
||
if (digits.length == 8) {
|
||
final dd = digits.substring(0, 2);
|
||
final mm = digits.substring(2, 4);
|
||
final yyyy = digits.substring(4, 8);
|
||
try {
|
||
return DateFormat('dd/MM/yyyy')
|
||
.parseStrict('$dd/$mm/$yyyy')
|
||
.toIso8601String()
|
||
.split('T')
|
||
.first;
|
||
} catch (_) {}
|
||
}
|
||
|
||
try {
|
||
return DateFormat('dd/MM/yyyy')
|
||
.parseStrict(t)
|
||
.toIso8601String()
|
||
.split('T')
|
||
.first;
|
||
} catch (_) {
|
||
try {
|
||
return DateTime.parse(t).toIso8601String().split('T').first;
|
||
} catch (_) {
|
||
return null;
|
||
}
|
||
}
|
||
}
|
||
|
||
({int years, int months, int days}) computeChildAgeParts(
|
||
DateTime birth,
|
||
DateTime reference,
|
||
) {
|
||
final birthDay = DateTime(birth.year, birth.month, birth.day);
|
||
final refDay = DateTime(reference.year, reference.month, reference.day);
|
||
|
||
var years = refDay.year - birthDay.year;
|
||
var months = refDay.month - birthDay.month;
|
||
var days = refDay.day - birthDay.day;
|
||
|
||
if (days < 0) {
|
||
months--;
|
||
final prevMonth = DateTime(refDay.year, refDay.month, 0);
|
||
days += prevMonth.day;
|
||
}
|
||
if (months < 0) {
|
||
years--;
|
||
months += 12;
|
||
}
|
||
|
||
return (years: years, months: months, days: days);
|
||
}
|
||
|
||
String _yearLabel(int years) => years == 1 ? '1 an' : '$years ans';
|
||
|
||
String _monthLabel(int months) => months == 1 ? '1 mois' : '$months mois';
|
||
|
||
/// Libellé d'âge pour un enfant (liste admin, fiche parent, etc.).
|
||
String formatChildAgeLabel({
|
||
String? birthDate,
|
||
String? dueDate,
|
||
String? status,
|
||
}) {
|
||
if (status == 'a_naitre' || normalizeEnfantStatus(status) == 'a_naitre') {
|
||
final due = formatIsoDateFr(dueDate, ifEmpty: '');
|
||
if (due.isNotEmpty) return 'Naissance prévue : $due';
|
||
return 'À naître';
|
||
}
|
||
|
||
if (birthDate == null || birthDate.trim().isEmpty) return '';
|
||
|
||
try {
|
||
final birth = DateTime.parse(birthDate.trim().split('T').first);
|
||
final now = DateTime.now();
|
||
final parts = computeChildAgeParts(birth, now);
|
||
|
||
if (parts.years >= 1) {
|
||
if (parts.months > 0) {
|
||
return 'Âge : ${_yearLabel(parts.years)} ${parts.months} mois';
|
||
}
|
||
return 'Âge : ${_yearLabel(parts.years)}';
|
||
}
|
||
if (parts.months >= 1) {
|
||
return 'Âge : ${_monthLabel(parts.months)}';
|
||
}
|
||
|
||
final totalDays = DateTime(now.year, now.month, now.day)
|
||
.difference(DateTime(birth.year, birth.month, birth.day))
|
||
.inDays;
|
||
if (totalDays <= 0) return 'Âge : né récemment';
|
||
return 'Âge : $totalDays jour${totalDays > 1 ? 's' : ''}';
|
||
} catch (_) {
|
||
return 'Né le ${formatIsoDateFr(birthDate)}';
|
||
}
|
||
}
|
||
|
||
/// Saisie date FR : 8 chiffres → `jj / mm / aaaa`.
|
||
class FrenchDateInputFormatter extends TextInputFormatter {
|
||
const FrenchDateInputFormatter();
|
||
|
||
@override
|
||
TextEditingValue formatEditUpdate(
|
||
TextEditingValue oldValue,
|
||
TextEditingValue newValue,
|
||
) {
|
||
final digits = newValue.text.replaceAll(RegExp(r'\D'), '');
|
||
final limited = digits.length > 8 ? digits.substring(0, 8) : digits;
|
||
final formatted = formatFrenchDateDigits(limited);
|
||
|
||
final digitsBeforeCursor = newValue.text
|
||
.substring(0, newValue.selection.start.clamp(0, newValue.text.length))
|
||
.replaceAll(RegExp(r'\D'), '')
|
||
.length
|
||
.clamp(0, limited.length);
|
||
|
||
return TextEditingValue(
|
||
text: formatted,
|
||
selection: TextSelection.collapsed(
|
||
offset: _cursorOffset(formatted, digitsBeforeCursor),
|
||
),
|
||
);
|
||
}
|
||
|
||
static int _cursorOffset(String formatted, int digitsBeforeCursor) {
|
||
if (digitsBeforeCursor <= 0) return 0;
|
||
var seen = 0;
|
||
for (var i = 0; i < formatted.length; i++) {
|
||
if (RegExp(r'\d').hasMatch(formatted[i])) {
|
||
seen++;
|
||
if (seen >= digitsBeforeCursor) return i + 1;
|
||
}
|
||
}
|
||
return formatted.length;
|
||
}
|
||
}
|