petitspas/frontend/lib/utils/date_display_utils.dart
2026-07-17 18:51:00 +02:00

180 lines
5.1 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
}
}