Files
mesreleves-php/app/Services/GedcomExportService.php
T
yann64 d064f8d28e É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>
2026-06-04 17:17:53 +02:00

229 lines
8.2 KiB
PHP

<?php
namespace App\Services;
use App\Models\Releve;
use App\Models\Source;
use Illuminate\Support\Collection;
class GedcomExportService
{
// Mapping des noms de champs vers les tags GEDCOM
private const FIELD_EVENT_MAP = [
'naissance' => 'BIRT', 'birth' => 'BIRT',
'mariage' => 'MARR', 'marriage' => 'MARR',
'deces' => 'DEAT', 'décès' => 'DEAT', 'death' => 'DEAT',
];
public function __construct(
private readonly DateConversionService $dates,
) {}
public function exportSource(Source $source): string
{
$source->load('sourceType.fields', 'releves.createur');
return $this->buildGedcom($source->releves, $source->nom, $source->sourceType->nom);
}
public function exportReleves(Collection $releves, string $titre = 'Export'): string
{
return $this->buildGedcom($releves, $titre);
}
private function buildGedcom(Collection $releves, string $sourceName, string $sourceTypeName = ''): string
{
$lines = [];
$lines[] = $this->header($sourceName);
$sourceTag = '@S1@';
$famId = 0;
$indiId = 0;
// Détecter le type d'événement depuis le nom du type de source
$eventType = $this->detectEventType($sourceTypeName ?: $sourceName);
$individuals = [];
$families = [];
foreach ($releves as $releve) {
$data = $releve->data ?? [];
$indiId++;
$indiTag = "@I{$indiId}@";
$nom = $data['nom'] ?? '';
$prenom = $data['prenom'] ?? $data['prenom_epoux'] ?? $data['prenom_epouse'] ?? '';
// ── Individu principal ──────────────────────────────────────────
$indi = [];
$indi[] = "0 {$indiTag} INDI";
if ($nom || $prenom) {
$indi[] = "1 NAME {$prenom} /{$nom}/";
if ($prenom) $indi[] = "1 GIVN {$prenom}";
if ($nom) $indi[] = "1 SURN {$nom}";
}
// Événement principal
$dateField = $data['date_evenement'] ?? $data['date_naissance'] ?? $data['date_mariage'] ?? null;
$gedDate = is_array($dateField) ? $this->dates->toGedcomDate($dateField) : null;
$lieu = $data['lieu_naissance'] ?? $data['lieu_evenement'] ?? $data['lieu_mariage'] ?? null;
$indi[] = "1 {$eventType}";
if ($gedDate) $indi[] = "2 DATE {$gedDate}";
if ($lieu) $indi[] = "2 PLAC {$lieu}";
// Référence à la source
$indi[] = "1 SOUR {$sourceTag}";
if (isset($data['numero_acte'])) {
$indi[] = "2 PAGE Acte n°{$data['numero_acte']}";
}
// Notes (champs non mappés)
$noteFields = ['note', 'observation', 'remarque'];
foreach ($noteFields as $nf) {
if (!empty($data[$nf])) {
foreach ($this->wrapNote($data[$nf]) as $noteLine) {
$indi[] = $noteLine;
}
}
}
$individuals[] = implode("\n", $indi);
// ── Famille (père/mère ou époux/épouse) ─────────────────────────
$hasFather = !empty($data['nom_pere']) || !empty($data['prenom_pere']);
$hasMother = !empty($data['nom_mere']) || !empty($data['prenom_mere']);
$isMarriage = $eventType === 'MARR';
if ($hasFather || $hasMother || $isMarriage) {
$famId++;
$famTag = "@F{$famId}@";
$fam = [];
$fam[] = "0 {$famTag} FAM";
if ($isMarriage) {
// Pour un mariage, créer époux et épouse séparément
if (!empty($data['nom_epoux']) || !empty($data['prenom_epoux'])) {
$indiId++;
$epouxTag = "@I{$indiId}@";
$individuals[] = implode("\n", [
"0 {$epouxTag} INDI",
"1 NAME {$data['prenom_epoux']} /{$data['nom_epoux']}/",
"1 FAMS {$famTag}",
"1 SOUR {$sourceTag}",
]);
$fam[] = "1 HUSB {$epouxTag}";
}
if (!empty($data['nom_epouse']) || !empty($data['prenom_epouse'])) {
$indiId++;
$epouseTag = "@I{$indiId}@";
$individuals[] = implode("\n", [
"0 {$epouseTag} INDI",
"1 NAME {$data['prenom_epouse']} /{$data['nom_epouse']}/",
"1 FAMS {$famTag}",
"1 SOUR {$sourceTag}",
]);
$fam[] = "1 WIFE {$epouseTag}";
}
$fam[] = "1 MARR";
if ($gedDate) $fam[] = "2 DATE {$gedDate}";
if ($lieu) $fam[] = "2 PLAC {$lieu}";
// Lier l'individu principal comme enfant si besoin
} else {
// Naissance/décès : père et mère
if ($hasFather) {
$indiId++;
$pereTag = "@I{$indiId}@";
$individuals[] = implode("\n", [
"0 {$pereTag} INDI",
"1 NAME " . ($data['prenom_pere'] ?? '') . " /" . ($data['nom_pere'] ?? '') . "/",
"1 FAMS {$famTag}",
"1 SOUR {$sourceTag}",
]);
$fam[] = "1 HUSB {$pereTag}";
}
if ($hasMother) {
$indiId++;
$mereTag = "@I{$indiId}@";
$individuals[] = implode("\n", [
"0 {$mereTag} INDI",
"1 NAME " . ($data['prenom_mere'] ?? '') . " /" . ($data['nom_mere'] ?? '') . "/",
"1 FAMS {$famTag}",
"1 SOUR {$sourceTag}",
]);
$fam[] = "1 WIFE {$mereTag}";
}
$fam[] = "1 CHIL {$indiTag}";
// Lier l'enfant à sa famille
$indi[] = "1 FAMC {$famTag}";
}
$families[] = implode("\n", $fam);
}
}
foreach ($individuals as $indi) {
$lines[] = $indi;
}
foreach ($families as $fam) {
$lines[] = $fam;
}
// Enregistrement SOURCE
$lines[] = implode("\n", [
"0 {$sourceTag} SOUR",
"1 TITL {$sourceName}",
"1 AUTH MesRelevés",
]);
$lines[] = "0 TRLR";
return implode("\n", $lines) . "\n";
}
private function header(string $sourceName): string
{
$date = now()->format('d M Y');
$time = now()->format('H:i:s');
return implode("\n", [
'0 HEAD',
'1 SOUR MESRELEVES',
'2 NAME MesRelevés',
'2 VERS 1.0',
'1 DEST ANY',
"1 DATE {$date}",
"2 TIME {$time}",
'1 GEDC',
'2 VERS 5.5.1',
'2 FORM LINEAGE-LINKED',
'1 CHAR UTF-8',
"1 FILE {$sourceName}.ged",
'1 LANG French',
]);
}
private function detectEventType(string $name): string
{
$lower = mb_strtolower($name);
foreach (self::FIELD_EVENT_MAP as $keyword => $tag) {
if (str_contains($lower, $keyword)) {
return $tag;
}
}
return 'EVEN';
}
/** Découpe une note longue en lignes GEDCOM (max 248 chars par ligne) */
private function wrapNote(string $text): array
{
$lines = [];
$chunks = mb_str_split($text, 248);
foreach ($chunks as $i => $chunk) {
$lines[] = ($i === 0 ? '1 NOTE ' : '2 CONT ') . $chunk;
}
return $lines;
}
}