Étapes 6-9 + types de lieux + picker + filtres

- Étape 6 : formulaire de saisie dynamique des relevés (piloté par source_type_fields, calendriers grégorien/julien/républicain)
- Étape 7 : workflow de statut des sources + notifications mail+DB (SourceAValider, SourceRejetee)
- Étape 8 : recherche fulltext PostgreSQL avec filtres type/lieu/années et CTE récursive pour les subdivisions de lieux
- Étape 9 : export GEDCOM 5.5.1 (GedcomExportService + DateConversionService)
- Types de lieux : CRUD admin (LieuTypeController) avec champ ordre
- Composant lieu-picker : modale Alpine.js avec recherche AJAX + debounce
- Filtres sources : statut, type, lieu (CTE récursive), période annee_debut/annee_fin
- Filtres lieux : type, texte, lieu parent avec descendants (CTE récursive)
- Migration : lieu_id + annee_debut + annee_fin sur sources

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-04 17:17:53 +02:00
parent 7609d35287
commit d064f8d28e
54 changed files with 2861 additions and 116 deletions
+152
View File
@@ -0,0 +1,152 @@
<?php
namespace App\Services;
use DateTime;
class DateConversionService
{
// Dates de début du 1er Vendémiaire pour chaque An républicain
private const YEAR_STARTS = [
1 => '1792-09-22',
2 => '1793-09-22',
3 => '1794-09-23',
4 => '1795-09-23',
5 => '1796-09-22',
6 => '1797-09-22',
7 => '1798-09-22',
8 => '1799-09-23',
9 => '1800-09-23',
10 => '1801-09-23',
11 => '1802-09-23',
12 => '1803-09-23',
13 => '1804-09-23',
14 => '1805-09-23',
];
private const MONTHS = [
'vendémiaire' => 1, 'vendemiaire' => 1,
'brumaire' => 2,
'frimaire' => 3,
'nivôse' => 4, 'nivose' => 4,
'pluviôse' => 5, 'pluviose' => 5,
'ventôse' => 6, 'ventose' => 6,
'germinal' => 7,
'floréal' => 8, 'floreal' => 8,
'prairial' => 9,
'messidor' => 10,
'thermidor' => 11,
'fructidor' => 12,
];
private const ROMAN = [
'XIV' => 14, 'XIII' => 13, 'XII' => 12, 'XI' => 11, 'X' => 10,
'IX' => 9, 'VIII' => 8, 'VII' => 7, 'VI' => 6, 'V' => 5,
'IV' => 4, 'III' => 3, 'II' => 2, 'I' => 1,
];
private const GEDCOM_MONTHS = [
1 => 'JAN', 2 => 'FEB', 3 => 'MAR', 4 => 'APR',
5 => 'MAY', 6 => 'JUN', 7 => 'JUL', 8 => 'AUG',
9 => 'SEP', 10 => 'OCT', 11 => 'NOV', 12 => 'DEC',
];
/**
* Convertit un champ date JSONB { valeur, calendrier } en chaîne GEDCOM.
* Retourne null si la conversion échoue.
*/
public function toGedcomDate(?array $dateField): ?string
{
if (empty($dateField['valeur'])) {
return null;
}
$valeur = trim($dateField['valeur']);
$calendrier = $dateField['calendrier'] ?? 'gregorien';
return match ($calendrier) {
'gregorien' => $this->gregorianToGedcom($valeur),
'julien' => $this->julianToGedcom($valeur),
'republicain' => $this->republicanToGedcom($valeur),
default => null,
};
}
/** YYYY-MM-DD → "D MON YYYY" */
public function gregorianToGedcom(string $date): ?string
{
$d = DateTime::createFromFormat('Y-m-d', $date);
if (! $d) {
return null;
}
$day = (int) $d->format('j');
$month = self::GEDCOM_MONTHS[(int) $d->format('n')];
$year = $d->format('Y');
return "{$day} {$month} {$year}";
}
/** Julien : même format YYYY-MM-DD, marqué @#DJULIAN@ */
private function julianToGedcom(string $date): ?string
{
$g = $this->gregorianToGedcom($date);
return $g ? "@#DJULIAN@ {$g}" : null;
}
/**
* "15 Vendémiaire An III" date grégorienne GEDCOM.
* Retourne la date avec préfixe @#DFRENCH R@ (standard GEDCOM pour calendrier républicain).
*/
private function republicanToGedcom(string $date): ?string
{
$gregory = $this->republicanToGregorian($date);
if ($gregory) {
return $this->gregorianToGedcom($gregory);
}
// Fallback : on conserve la date telle quelle dans un format lisible
return "({$date})";
}
/**
* Convertit "15 Vendémiaire An III" "1794-10-06".
*/
public function republicanToGregorian(string $input): ?string
{
// Normaliser l'entrée
$input = trim($input);
// Pattern : "DD NomDuMois An N" ou "DD NomDuMois An XIV"
$pattern = '/^(\d{1,2})\s+([\wéèêôûî]+)\s+[Aa]n\s+([IVXLCDM\d]+)$/iu';
if (! preg_match($pattern, $input, $m)) {
return null;
}
$day = (int) $m[1];
$monthStr = mb_strtolower(trim($m[2]));
$yearStr = strtoupper(trim($m[3]));
// Résoudre le mois
$monthNum = self::MONTHS[$monthStr] ?? null;
if (! $monthNum || $day < 1 || $day > 30) {
return null;
}
// Résoudre l'année (chiffres arabes ou romains)
$yearNum = is_numeric($yearStr)
? (int) $yearStr
: (self::ROMAN[$yearStr] ?? null);
if (! $yearNum || ! isset(self::YEAR_STARTS[$yearNum])) {
return null;
}
// Calculer le nombre de jours depuis le 1er Vendémiaire
$daysOffset = ($monthNum - 1) * 30 + ($day - 1);
$start = new DateTime(self::YEAR_STARTS[$yearNum]);
$start->modify("+{$daysOffset} days");
return $start->format('Y-m-d');
}
}