Create food planning & merge liste into food
Deploy HouseHub / deploy (push) Successful in 2s

This commit is contained in:
FeFeClochette committed 2026-09-15 10:58:23 +02:00
1 parent 3c615e3021
commit 4cfef07dc6
22 files changed
+2918 -780

No files matched your search

+277
View File
@@ -0,0 +1,277 @@
<?php
// modules/meals/includes/api/import-pdf.php
require dirname(__DIR__, 4) . '/includes/auth.php';
require dirname(__DIR__, 4) . '/includes/db.php';
require_login();
header('Content-Type: application/json; charset=utf-8');
if ($_SERVER['REQUEST_METHOD'] !== 'POST' || empty($_FILES['pdf_file'])) {
echo json_encode(['success' => false, 'error' => 'Aucun fichier reçu.']);
exit;
}
$personId = (int)$_POST['person_id'];
if ($personId <= 0) {
echo json_encode(['success' => false, 'error' => 'Enfant non spécifié.']);
exit;
}
// ---------------------------------------------------------
// PARSEUR PDF SPATIAL NATIF (100% PHP SANS LIBRAIRIE)
// ---------------------------------------------------------
class NativePdfParser {
public static function getMealData($filename) {
$content = file_get_contents($filename);
$texts = [];
// 1. Décompression et Tokenization Mathématique
if (preg_match_all('/stream(.*?)endstream/is', $content, $matches)) {
foreach ($matches[1] as $stream) {
$stream = ltrim($stream, "\r\n");
$decoded = @gzuncompress($stream);
if (!$decoded) $decoded = $stream;
// On éclate le flux par opérateurs PDF clés (Position et Texte)
$parts = preg_split('/(Tm|Td|Tj|TJ)/', $decoded, -1, PREG_SPLIT_DELIM_CAPTURE);
$x = 0; $y = 0;
for ($i = 0; $i < count($parts); $i++) {
$token = $parts[$i];
if ($token === 'Tm' || $token === 'Td') {
$prev = $parts[$i-1];
preg_match_all('/[0-9.-]+/', $prev, $nums);
if (count($nums[0]) >= 2) {
$yRaw = array_pop($nums[0]);
$xRaw = array_pop($nums[0]);
if ($token === 'Tm') {
$x = (float)$xRaw; $y = (float)$yRaw;
} else {
$x += (float)$xRaw; $y += (float)$yRaw;
}
}
} elseif ($token === 'Tj') {
$prev = $parts[$i-1];
if (preg_match('/\((.*?)\)$/', trim($prev), $m)) {
self::addText($texts, $m[1], $x, $y);
}
} elseif ($token === 'TJ') {
$prev = $parts[$i-1];
if (preg_match('/\[(.*?)\]$/', trim($prev), $m)) {
preg_match_all('/\((.*?)\)/', $m[1], $tjs);
$tjX = $x;
foreach ($tjs[1] as $tj) {
self::addText($texts, $tj, $tjX, $y);
$tjX += 10; // Décalage estimé pour les mots suivants du tableau
}
}
}
}
}
}
if (empty($texts)) throw new Exception("Impossible de lire le contenu du PDF. Le fichier est peut-être crypté ou corrompu.");
// 2. Définition des Colonnes Verticles (X) via les En-têtes de Jour
$dayLabels = ['LUNDI', 'MARDI', 'MERCREDI', 'JEUDI', 'VENDREDI'];
$colXs = [];
foreach ($texts as $t) {
$clean = strtoupper(preg_replace('/[^A-Z]/', '', $t['text']));
if (in_array($clean, $dayLabels)) {
$colXs[$clean] = $t['x'];
}
}
// Sécurité si les jours exacts ne sont pas détectés à cause de la police
if (count($colXs) < 5) {
$allXs = array_column($texts, 'x');
sort($allXs);
$minX = $allXs[0] ?? 0;
$maxX = end($allXs) ?? 500;
$step = ($maxX - $minX) / 5;
$colXs = [
'LUNDI' => $minX + $step * 0.5,
'MARDI' => $minX + $step * 1.5,
'MERCREDI' => $minX + $step * 2.5,
'JEUDI' => $minX + $step * 3.5,
'VENDREDI' => $minX + $step * 4.5,
];
}
$sortedXs = array_values($colXs);
sort($sortedXs);
// 3. Définition des Tranches Horizontales (Y) via les Titres de Rangée (Zone Extrême Gauche)
$rowHeaders = [];
foreach ($texts as $t) {
$clean = strtoupper(preg_replace('/[^A-Z]/', '', $t['text']));
if (in_array($clean, ['HORSDEUVRE', 'DECLINAISON', 'PLATCHAUD', 'GARNITURE', 'PRODUITLAITIER', 'DESSERT', 'LESGOUTERS'])) {
// On s'assure de ne capter que la colonne tout à gauche
if ($t['x'] < ($sortedXs[0] - 20)) {
$rowHeaders[] = ['label' => $clean, 'y' => $t['y']];
}
}
}
// Tri de haut en bas (Y du PDF est décroissant du haut vers le bas)
usort($rowHeaders, fn($a, $b) => $b['y'] <=> $a['y']);
$yPlatTop = null; $yPlatBot = null;
$yGarTop = null; $yGarBot = null;
for ($i = 0; $i < count($rowHeaders); $i++) {
if ($rowHeaders[$i]['label'] === 'PLATCHAUD') {
$yPlatTop = $rowHeaders[$i]['y'];
// La limite basse est le label suivant, ou -50 unités
$yPlatBot = $rowHeaders[$i+1]['y'] ?? ($yPlatTop - 50);
}
if ($rowHeaders[$i]['label'] === 'GARNITURE') {
$yGarTop = $rowHeaders[$i]['y'];
$yGarBot = $rowHeaders[$i+1]['y'] ?? ($yGarTop - 50);
}
}
if (!$yPlatTop || !$yGarTop) {
throw new Exception("Structure du menu non reconnue (Mots-clés 'PLAT CHAUD' et 'GARNITURE' introuvables).");
}
// 4. Affectation Chirurgicale par Intersection Grille (X, Y)
$days = [[], [], [], [], []];
foreach ($texts as $t) {
$cleanAlpha = strtoupper(preg_replace('/[^A-Z]/', '', $t['text']));
// Rejet des labels génériques et de la fameuse ligne "Déclinaison"
if (in_array($cleanAlpha, ['DECLINAISON', 'MENUVEGETARIEN', 'PLATCHAUD', 'GARNITURE', 'PRODUITLAITIER', 'HORSDEUVRE', 'LESGOUTERS', 'DESSERT'])) continue;
// Retrait des labels BIO/HVE et mentions superflues
$textVal = trim(preg_replace('/\b(AB|HVE|MSC|Label Rouge)\b/i', '', $t['text']));
$textVal = str_replace(['(plat', 'complet)'], '', $textVal);
$textVal = trim(preg_replace('/[^a-zA-ZÀ-ÿ0-9\s&\-]/u', '', $textVal));
if (strlen($textVal) < 3) continue;
// Filtre Vertical : Est-on dans la tranche Plat Chaud ou Garniture ? (Tolérance +/- 5 unités)
$inPlat = ($t['y'] <= $yPlatTop + 5 && $t['y'] >= $yPlatBot + 5);
$inGar = ($t['y'] <= $yGarTop + 5 && $t['y'] >= $yGarBot + 5);
if ($inPlat || $inGar) {
// Filtre Horizontal : Quel est le jour le plus proche ?
$bestCol = 0;
$minDiff = PHP_FLOAT_MAX;
foreach ($sortedXs as $idx => $cx) {
$diff = abs($t['x'] - $cx);
if ($diff < $minDiff) {
$minDiff = $diff;
$bestCol = $idx;
}
}
$days[$bestCol][] = ['text' => $textVal, 'y' => $t['y'], 'type' => $inPlat ? 'plat' : 'gar'];
}
}
// 5. Concaténation Finale
$finalDays = [];
for ($i = 0; $i < 5; $i++) {
$plats = array_filter($days[$i], fn($item) => $item['type'] === 'plat');
$gars = array_filter($days[$i], fn($item) => $item['type'] === 'gar');
usort($plats, fn($a, $b) => $b['y'] <=> $a['y']);
usort($gars, fn($a, $b) => $b['y'] <=> $a['y']);
$platText = self::concatLines($plats);
$garText = self::concatLines($gars);
$full = trim("$platText + $garText", " +");
$finalDays[] = preg_replace('/\s+/', ' ', $full);
}
return $finalDays;
}
private static function concatLines($items) {
$lines = [];
$currentLine = "";
$lastY = null;
foreach ($items as $item) {
// Si le décalage Y est trop fort, on considère que c'est une nouvelle ligne
if ($lastY !== null && abs($lastY - $item['y']) > 8) {
if ($currentLine) $lines[] = trim($currentLine);
$currentLine = $item['text'];
} else {
$currentLine .= " " . $item['text'];
}
$lastY = $item['y'];
}
if ($currentLine) $lines[] = trim($currentLine);
return implode(' ', array_filter($lines, fn($l) => strlen(trim($l)) > 2));
}
private static function addText(&$texts, $rawText, $x, $y) {
// Décodage de l'octal \123
$m = preg_replace_callback('/\\\\([0-7]{1,3})/', function($cb) { return chr(octdec($cb[1])); }, $rawText);
// Détection de l'UTF-16BE (Le BOM \xFE\xFF indique que le texte est double encodé)
if (str_starts_with($m, "\xFE\xFF")) {
$m = mb_convert_encoding(substr($m, 2), 'UTF-8', 'UTF-16BE');
} else {
// Fallback Latin-1 si pas de BOM
$m = mb_convert_encoding($m, 'UTF-8', 'ISO-8859-1');
}
// Suppression radicale des octets nuls responsables de l'espacement forcé (E s c a l o p e)
$m = str_replace("\0", "", $m);
$m = trim(preg_replace('/[^\p{L}\p{N}\p{P}\p{Z}\+&\'’\-]/u', '', $m));
if ($m !== '') {
$texts[] = ['text' => $m, 'x' => $x, 'y' => $y];
}
}
}
try {
$filename = $_FILES['pdf_file']['name'];
// 1. Extraction robuste de la date depuis le nom de fichier (Menus-du-14-au-20-septembre-2026.pdf)
$mondayDate = null;
if (preg_match('/(\d{1,2})[\s-]+au[\s-]+\d{1,2}[\s-]+([a-zèéû]+)[\s-]+(\d{4})/i', $filename, $m)) {
$monthsMap = ['janvier'=>'01','fevrier'=>'02','février'=>'02','mars'=>'03','avril'=>'04','mai'=>'05','juin'=>'06','juillet'=>'07','aout'=>'08','août'=>'08','septembre'=>'09','octobre'=>'10','novembre'=>'11','decembre'=>'12','décembre'=>'12'];
$month = $monthsMap[strtolower($m[2])] ?? '09';
$mondayDate = $m[3] . '-' . $month . '-' . str_pad($m[1], 2, '0', STR_PAD_LEFT);
}
if (!$mondayDate) {
echo json_encode(['success' => false, 'error' => 'Veuillez nommer le fichier "Menus-du-14-au-20-septembre-2026.pdf".']);
exit;
}
// 2. Lancement de l'Analyse Spatiale
$plats = NativePdfParser::getMealData($_FILES['pdf_file']['tmp_name']);
if (empty($plats) || count($plats) !== 5) {
echo json_encode(['success' => false, 'error' => 'Erreur lors du découpage des 5 colonnes.']);
exit;
}
// 3. Insertion en BDD (Lundi au Vendredi)
$pdo->beginTransaction();
$stmt = $pdo->prepare("
INSERT INTO pf_meals_plan (plan_date, service, person_id, meal_name)
VALUES (?, 'lunch', ?, ?)
ON DUPLICATE KEY UPDATE meal_name = VALUES(meal_name)
");
$currentDate = new DateTime($mondayDate);
for ($i = 0; $i < 5; $i++) {
$fullMeal = $plats[$i];
if (!empty($fullMeal) && $fullMeal !== '+') {
$stmt->execute([$currentDate->format('Y-m-d'), $personId, $fullMeal]);
}
$currentDate->modify('+1 day');
}
$pdo->commit();
echo json_encode(['success' => true]);
} catch (Exception $e) {
if ($pdo->inTransaction()) $pdo->rollBack();
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}