This commit is contained in:
1 parent
3c615e3021
commit
4cfef07dc6
22 files changed
+2918
-780
No files matched your search
@@ -48,43 +48,6 @@
|
||||
}
|
||||
|
||||
/* --- 2. NAVIGATION TABS (Pills) --- */
|
||||
.budget-tabs-container {
|
||||
display: inline-flex;
|
||||
background: var(--bg-panel);
|
||||
padding: 6px;
|
||||
border-radius: 50px;
|
||||
box-shadow: var(--shadow);
|
||||
gap: 8px;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
.tab-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 24px;
|
||||
border-radius: 40px;
|
||||
text-decoration: none;
|
||||
color: var(--text-muted);
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
.tab-item:hover {
|
||||
background-color: var(--bg-page);
|
||||
color: var(--text-main);
|
||||
}
|
||||
.tab-item.active {
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
box-shadow: 0 4px 12px rgba(37, 99, 235, 0.3);
|
||||
transform: scale(1.02);
|
||||
}
|
||||
.tab-icon {
|
||||
font-size: 1.1em;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.owner-tabs {
|
||||
display: inline-flex;
|
||||
@@ -1486,20 +1449,7 @@ body.sum-mode-active .sum-target {
|
||||
}
|
||||
|
||||
/* Ajouts Structure Global Budget */
|
||||
.budget-hero {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.budget-title-group {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.budget-title-group h1 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.btn-settings-gear {
|
||||
position: relative;
|
||||
}
|
||||
@@ -1582,23 +1532,7 @@ body.sum-mode-active .sum-target {
|
||||
max-width: 100vw !important;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.budget-tabs-container {
|
||||
display: flex !important;
|
||||
flex-wrap: nowrap !important;
|
||||
overflow-x: auto !important;
|
||||
justify-content: flex-start !important;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
width: 100%;
|
||||
padding-bottom: 5px;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
.budget-tabs-container::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
.tab-item {
|
||||
flex: 0 0 auto !important;
|
||||
padding: 8px 16px !important;
|
||||
}
|
||||
|
||||
.budget-view > div[style*="display:grid"] {
|
||||
gap: 10px !important;
|
||||
margin-bottom: 15px !important;
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,6 +1,7 @@
|
||||
<?php
|
||||
// modules/food/includes/api/api-liste.php
|
||||
ob_start();
|
||||
require_once dirname(__DIR__, 2) . '/includes/auth.php';
|
||||
require_once dirname(__DIR__, 4) . '/includes/auth.php';
|
||||
require_login();
|
||||
|
||||
header('Content-Type: application/json');
|
||||
@@ -10,7 +11,7 @@ set_exception_handler(function (\Throwable $e) {
|
||||
echo json_encode(['ok' => false, 'error' => $e->getMessage()]); exit;
|
||||
});
|
||||
|
||||
require_once dirname(__DIR__, 2) . '/includes/db.php';
|
||||
require_once dirname(__DIR__, 4) . '/includes/db.php';
|
||||
|
||||
// ── Auto-create tables ────────────────────────────────────────────────────────
|
||||
$pdo->exec("CREATE TABLE IF NOT EXISTS pf_lists (
|
||||
@@ -96,11 +97,9 @@ function liste_ensure_default(PDO $pdo): int {
|
||||
return (int)$pdo->query("SELECT id FROM pf_lists ORDER BY position, id LIMIT 1")->fetchColumn();
|
||||
}
|
||||
|
||||
// Built-in keyword → category position mapping (position maps to seeded order above)
|
||||
function liste_builtin_detect(string $normalized): ?int {
|
||||
static $map = null;
|
||||
if ($map === null) $map = [
|
||||
// 1 = Fruits & Légumes
|
||||
'pomme'=>1,'poire'=>1,'banane'=>1,'orange'=>1,'citron'=>1,'raisin'=>1,
|
||||
'fraise'=>1,'framboise'=>1,'myrtille'=>1,'cerise'=>1,'peche'=>1,'abricot'=>1,
|
||||
'mangue'=>1,'kiwi'=>1,'ananas'=>1,'melon'=>1,'pasteque'=>1,'prune'=>1,
|
||||
@@ -113,120 +112,96 @@ function liste_builtin_detect(string $normalized): ?int {
|
||||
'gingembre'=>1,'avocat'=>1,'endive'=>1,'ciboulette'=>1,'persil'=>1,
|
||||
'basilic'=>1,'menthe'=>1,'thym'=>1,'romarin'=>1,'coriandre'=>1,
|
||||
'girofle'=>1,'citron vert'=>1,'lime'=>1,'pamplemousse'=>1,'litchi'=>1,
|
||||
// 2 = Viande
|
||||
'boeuf'=>2,'veau'=>2,'poulet'=>2,'porc'=>2,'agneau'=>2,'dinde'=>2,
|
||||
'lapin'=>2,'jambon'=>2,'lardon'=>2,'saucisse'=>2,'saucisson'=>2,
|
||||
'merguez'=>2,'chipolata'=>2,'steak'=>2,'escalope'=>2,'roti'=>2,
|
||||
'gigot'=>2,'filet'=>2,'cuisses'=>2,'viande hachee'=>2,'andouille'=>2,
|
||||
'magret'=>2,'canard'=>2,'foie'=>2,'boudin'=>2,'paupiette'=>2,
|
||||
'entrecote'=>2,'cote de boeuf'=>2,'onglet'=>2,'bavette'=>2,
|
||||
// 3 = Poissonnerie
|
||||
'saumon'=>3,'thon'=>3,'dorade'=>3,'bar'=>3,'cabillaud'=>3,'sole'=>3,
|
||||
'truite'=>3,'lieu'=>3,'maquereau'=>3,'sardine'=>3,'anchois'=>3,
|
||||
'crevette'=>3,'moule'=>3,'huitre'=>3,'coquille'=>3,'poulpe'=>3,
|
||||
'seiche'=>3,'langoustine'=>3,'lotte'=>3,'merlu'=>3,'colin'=>3,
|
||||
'tilapia'=>3,'daurade'=>3,'aiglefin'=>3,'homard'=>3,'crabe'=>3,
|
||||
// 4 = Boulangerie
|
||||
'pain'=>4,'baguette'=>4,'brioche'=>4,'croissant'=>4,'pain de mie'=>4,
|
||||
'ficelle'=>4,'fougasse'=>4,'ciabatta'=>4,'biscotte'=>4,'naan'=>4,
|
||||
'pita'=>4,'toast'=>4,'bagel'=>4,'pain burger'=>4,'wrap'=>4,
|
||||
// 5 = Frais
|
||||
'lait'=>5,'creme'=>5,'yaourt'=>5,'yourt'=>5,'beurre'=>5,
|
||||
'fromage'=>5,'camembert'=>5,'brie'=>5,'comte'=>5,'gruyere'=>5,
|
||||
'emmental'=>5,'mozzarella'=>5,'ricotta'=>5,'mascarpone'=>5,
|
||||
'feta'=>5,'chevre'=>5,'oeuf'=>5,'oeufs'=>5,'creme fraiche'=>5,
|
||||
'fromage blanc'=>5,'petit suisse'=>5,'kefir'=>5,
|
||||
// 6 = Surgelés
|
||||
'surgele'=>6,'pizza surgelee'=>6,'frite surgelee'=>6,'poisson pane'=>6,
|
||||
'legume surgele'=>6,'glace'=>6,'sorbet'=>6,'plat surgele'=>6,
|
||||
'nugget'=>6,'edamame'=>6,'petits pois'=>6,'epinards'=>6,
|
||||
// 7 = Boissons
|
||||
'eau'=>7,'coca'=>7,'cola'=>7,'limonade'=>7,'sirop'=>7,
|
||||
'biere'=>7,'vin'=>7,'champagne'=>7,'cafe'=>7,'the'=>7,
|
||||
'tisane'=>7,'soda'=>7,'jus'=>7,'kombucha'=>7,'cidre'=>7,
|
||||
'whisky'=>7,'rhum'=>7,'vodka'=>7,'prosecco'=>7,'rose'=>7,
|
||||
// 8 = Pâtes, Riz, Féculents
|
||||
'pate'=>8,'spaghetti'=>8,'tagliatelle'=>8,'penne'=>8,'fusilli'=>8,
|
||||
'macaroni'=>8,'riz'=>8,'couscous'=>8,'quinoa'=>8,'lentille'=>8,
|
||||
'pois chiche'=>8,'farine'=>8,'semoule'=>8,'polenta'=>8,'boulgour'=>8,
|
||||
'orge'=>8,'feculent'=>8,'vermicelle'=>8,'nouille'=>8,'gnocchi'=>8,
|
||||
// 9 = Épicerie salée
|
||||
'chips'=>9,'crackers'=>9,'olives'=>9,'cornichon'=>9,'capres'=>9,
|
||||
'tapenade'=>9,'houmous'=>9,'tzatziki'=>9,'sel'=>9,'poivre'=>9,
|
||||
'epice'=>9,'cube bouillon'=>9,'bouillon'=>9,'noix de cajou'=>9,
|
||||
'amande'=>9,'pistache'=>9,'noix'=>9,'cacahuete'=>9,
|
||||
// 10 = Conserves
|
||||
'conserve'=>10,'boite de tomate'=>10,'concentre de tomate'=>10,
|
||||
'mais'=>10,'ratatouille'=>10,'cassoulet'=>10,'pate de foie'=>10,
|
||||
'sardine boite'=>10,'thon boite'=>10,'maquereau boite'=>10,
|
||||
'soupe boite'=>10,'haricot boite'=>10,
|
||||
// 11 = Plats cuisinés
|
||||
'quiche'=>11,'lasagne'=>11,'gratin'=>11,'pizza'=>11,'tartiflette'=>11,
|
||||
'croque'=>11,'hachis parmentier'=>11,'paella'=>11,'moussaka'=>11,
|
||||
'tarte'=>11,'flamiche'=>11,'pissaladiere'=>11,
|
||||
// 12 = Sauces & Condiments
|
||||
'ketchup'=>12,'mayonnaise'=>12,'moutarde'=>12,'vinaigrette'=>12,
|
||||
'sauce soja'=>12,'tabasco'=>12,'huile'=>12,'vinaigre'=>12,
|
||||
'sauce tomate'=>12,'pesto'=>12,'sriracha'=>12,'worcester'=>12,
|
||||
'nuoc mam'=>12,'sauce'=>12,'condiment'=>12,'worcestershire'=>12,
|
||||
// 13 = Petit déjeuner
|
||||
'cereale'=>13,'muesli'=>13,'granola'=>13,'corn flakes'=>13,
|
||||
'miel'=>13,'confiture'=>13,'nutella'=>13,'beurre de cacahuete'=>13,
|
||||
'sirop d erable'=>13,'sirop erable'=>13,'chocolat en poudre'=>13,
|
||||
// 14 = Biscuits & Gâteaux
|
||||
'biscuit'=>14,'gateau'=>14,'cookie'=>14,'madeleine'=>14,
|
||||
'financier'=>14,'quatre quarts'=>14,'brownie'=>14,'sable'=>14,
|
||||
'speculoos'=>14,'oreo'=>14,'lu'=>14,'petit beurre'=>14,'galette'=>14,
|
||||
'palmier'=>14,'macaron'=>14,'eclair'=>14,
|
||||
// 15 = Confiserie
|
||||
'chocolat'=>15,'bonbon'=>15,'reglisse'=>15,'caramel'=>15,
|
||||
'nougat'=>15,'marshmallow'=>15,'guimauve'=>15,'sucette'=>15,
|
||||
'chewing gum'=>15,'pastille'=>15,'calisson'=>15,
|
||||
// 16 = Dessert
|
||||
'yaourt dessert'=>16,'creme dessert'=>16,'mousse au chocolat'=>16,
|
||||
'tiramisu'=>16,'creme brulee'=>16,'flan'=>16,'crepe'=>16,
|
||||
'madeleine'=>16,'profiterole'=>16,'eclair'=>16,
|
||||
// 17 = Beauté & Hygiène
|
||||
'profiterole'=>16,
|
||||
'shampoing'=>17,'gel douche'=>17,'savon'=>17,'dentifrice'=>17,
|
||||
'deodorant'=>17,'crème visage'=>17,'crème corps'=>17,'rasoir'=>17,
|
||||
'mousse a raser'=>17,'coton'=>17,'lingette'=>17,'maquillage'=>17,
|
||||
'parfum'=>17,'brosse a dents'=>17,'fil dentaire'=>17,'serum'=>17,
|
||||
'hydratant'=>17,'demaquillant'=>17,
|
||||
// 18 = Bébé
|
||||
'couche'=>18,'biberon'=>18,'lait infantile'=>18,'compote bebe'=>18,
|
||||
'pot bebe'=>18,'puree bebe'=>18,'lingette bebe'=>18,'savon bebe'=>18,
|
||||
'creme bebe'=>18,'sucette bebe'=>18,
|
||||
// 19 = Entretien
|
||||
'lessive'=>19,'liquide vaisselle'=>19,'nettoyant'=>19,'degraissant'=>19,
|
||||
'deboucheur'=>19,'anticalcaire'=>19,'eponge'=>19,'serpillere'=>19,
|
||||
'sac poubelle'=>19,'papier toilette'=>19,'sopalin'=>19,'essuie tout'=>19,
|
||||
'aluminium'=>19,'film plastique'=>19,'sac congelation'=>19,
|
||||
'nettoyant wc'=>19,'desinfectant'=>19,'vitre'=>19,'javel'=>19,
|
||||
// 20 = Animaux
|
||||
'croquette'=>20,'patee'=>20,'litiere'=>20,'os'=>20,
|
||||
'friandise animale'=>20,'nourriture chat'=>20,'nourriture chien'=>20,
|
||||
// 21 = Maison & Jardin
|
||||
'ampoule'=>21,'pile'=>21,'bougie'=>21,'allumette'=>21,
|
||||
'ruban adhesif'=>21,'colle'=>21,'vis'=>21,'terreau'=>21,
|
||||
'engrais'=>21,'pot de fleur'=>21,'arrosoir'=>21,'graine'=>21,
|
||||
'tournevis'=>21,'marteau'=>21,'clou'=>21,'cle'=>21,
|
||||
// 22 = Pharmacie
|
||||
'doliprane'=>22,'ibuprofene'=>22,'paracetamol'=>22,'aspirine'=>22,
|
||||
'bandage'=>22,'pansement'=>22,'thermometre'=>22,'serum physiologique'=>22,
|
||||
'vitamine'=>22,'complement'=>22,'masque'=>22,'gant medical'=>22,
|
||||
'sirop'=>22,'antihistaminique'=>22,'antidouleur'=>22,
|
||||
'antihistaminique'=>22,'antidouleur'=>22,
|
||||
];
|
||||
|
||||
// Check full phrase
|
||||
if (isset($map[$normalized])) return $map[$normalized];
|
||||
|
||||
// Check individual words
|
||||
$words = explode(' ', $normalized);
|
||||
foreach ($words as $w) {
|
||||
if (strlen($w) >= 3 && isset($map[$w])) return $map[$w];
|
||||
}
|
||||
|
||||
// Partial match (contains)
|
||||
foreach ($map as $kw => $catPos) {
|
||||
if (str_contains($normalized, $kw)) return $catPos;
|
||||
}
|
||||
@@ -237,12 +212,10 @@ function liste_builtin_detect(string $normalized): ?int {
|
||||
function liste_detect_category(PDO $pdo, string $label): ?int {
|
||||
$norm = liste_normalize_detect($label);
|
||||
|
||||
// 1. Learned rules (exact normalized label)
|
||||
$r = $pdo->prepare("SELECT category_id FROM pf_item_category_rules WHERE keyword=? LIMIT 1");
|
||||
$r->execute([$norm]);
|
||||
if ($row = $r->fetch()) return (int)$row['category_id'];
|
||||
|
||||
// 2. Learned rules (individual words)
|
||||
foreach (explode(' ', $norm) as $w) {
|
||||
if (strlen($w) < 3) continue;
|
||||
$r = $pdo->prepare("SELECT category_id FROM pf_item_category_rules WHERE keyword=? LIMIT 1");
|
||||
@@ -250,11 +223,9 @@ function liste_detect_category(PDO $pdo, string $label): ?int {
|
||||
if ($row = $r->fetch()) return (int)$row['category_id'];
|
||||
}
|
||||
|
||||
// 3. Built-in dictionary → map position to real category id
|
||||
$pos = liste_builtin_detect($norm);
|
||||
if ($pos === null) return null;
|
||||
|
||||
// Get category id by position order
|
||||
$r = $pdo->query("SELECT id FROM pf_list_categories ORDER BY position, id");
|
||||
$ids = $r->fetchAll(PDO::FETCH_COLUMN);
|
||||
return $ids[$pos - 1] ?? null;
|
||||
@@ -266,7 +237,6 @@ function liste_learn(PDO $pdo, string $label, int $category_id): void {
|
||||
$pdo->prepare("INSERT INTO pf_item_category_rules (keyword, category_id) VALUES (?,?)
|
||||
ON DUPLICATE KEY UPDATE category_id=VALUES(category_id)")
|
||||
->execute([$norm, $category_id]);
|
||||
// Also learn individual meaningful words
|
||||
foreach (explode(' ', $norm) as $w) {
|
||||
if (strlen($w) >= 4) {
|
||||
$pdo->prepare("INSERT IGNORE INTO pf_item_category_rules (keyword, category_id) VALUES (?,?)")
|
||||
@@ -301,7 +271,6 @@ if ($action === 'set_category' && $method === 'POST') {
|
||||
|
||||
$pdo->prepare("UPDATE pf_grocery_items SET category_id=? WHERE id=?")->execute([$category_id, $item_id]);
|
||||
|
||||
// Learn from manual assignment
|
||||
if ($category_id !== null) {
|
||||
$row = $pdo->prepare("SELECT label FROM pf_grocery_items WHERE id=?");
|
||||
$row->execute([$item_id]);
|
||||
@@ -431,11 +400,11 @@ if ($action === 'clear_all' && $method === 'POST') {
|
||||
// ── HISTORY ───────────────────────────────────────────────────────────────────
|
||||
if ($action === 'history' && $method === 'GET') {
|
||||
$max = 20;
|
||||
$n = $pdo->prepare("SELECT content FROM pf_notes WHERE note_type='setting' AND reference_id='liste_history_max'"); $n->execute();
|
||||
$n = $pdo->prepare("SELECT content FROM pf_notes WHERE note_type='grocery_settings' AND reference_id='history_max'"); $n->execute();
|
||||
if ($r = $n->fetch()) $max = max(1, min(50, (int)$r['content']));
|
||||
$s = $pdo->query("SELECT label_display FROM pf_grocery_history ORDER BY last_used_at DESC LIMIT $max");
|
||||
echo json_encode(['history' => $s->fetchAll(PDO::FETCH_COLUMN)]); exit;
|
||||
}
|
||||
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'unknown action: ' . $action]);
|
||||
echo json_encode(['error' => 'unknown action: ' . $action]);
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
// Désactive l'affichage des erreurs HTML pour garantir un retour JSON strict
|
||||
ini_set('display_errors', 0);
|
||||
error_reporting(0);
|
||||
|
||||
require dirname(__DIR__, 4) . '/includes/auth.php';
|
||||
require dirname(__DIR__, 4) . '/includes/db.php';
|
||||
require dirname(__DIR__, 4) . '/includes/i18n.php';
|
||||
require_login();
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
try {
|
||||
$dateParam = $_GET['date'] ?? date('Y-m-d');
|
||||
$baseDate = new DateTime($dateParam);
|
||||
|
||||
// Aligner sur le lundi de la semaine
|
||||
$monday = clone $baseDate;
|
||||
if ($monday->format('N') != 1) {
|
||||
$monday->modify('last monday');
|
||||
}
|
||||
|
||||
$sunday = clone $monday;
|
||||
$sunday->modify('+6 days');
|
||||
|
||||
$weekData = [
|
||||
'week_label' => tr('meal_week') . ' ' . $monday->format('W'),
|
||||
'days' => []
|
||||
];
|
||||
|
||||
// Extraction des repas de la base (person_id = 0 pour le foyer global)
|
||||
$stmt = $pdo->prepare("SELECT plan_date, service, meal_name FROM pf_meals_plan WHERE plan_date BETWEEN ? AND ? AND person_id = 0");
|
||||
$stmt->execute([$monday->format('Y-m-d'), $sunday->format('Y-m-d')]);
|
||||
|
||||
$meals = [];
|
||||
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
|
||||
$meals[$row['plan_date']][$row['service']] = $row['meal_name'];
|
||||
}
|
||||
|
||||
$current = clone $monday;
|
||||
|
||||
// Mappage vers les clés i18n existantes de HouseHub
|
||||
$daysMap = [
|
||||
1 => 'day_mon', 2 => 'day_tue', 3 => 'day_wed',
|
||||
4 => 'day_thu', 5 => 'day_fri', 6 => 'day_sat', 7 => 'day_sun'
|
||||
];
|
||||
|
||||
for ($i = 0; $i < 7; $i++) {
|
||||
$dStr = $current->format('Y-m-d');
|
||||
|
||||
$dayNum = $current->format('N'); // 1 à 7
|
||||
$monthNum = $current->format('m'); // 01 à 12
|
||||
$dayDate = $current->format('d'); // 01 à 31
|
||||
|
||||
$dayName = tr($daysMap[$dayNum]);
|
||||
$monthName = tr('month_' . $monthNum);
|
||||
|
||||
$weekData['days'][] = [
|
||||
'date' => $dStr,
|
||||
'day_name' => $dayName,
|
||||
'date_formatted' => $dayDate . ' ' . $monthName,
|
||||
'lunch' => $meals[$dStr]['lunch'] ?? '',
|
||||
'dinner' => $meals[$dStr]['dinner'] ?? ''
|
||||
];
|
||||
$current->modify('+1 day');
|
||||
}
|
||||
|
||||
echo json_encode(['success' => true, 'data' => $weekData], JSON_UNESCAPED_UNICODE);
|
||||
|
||||
} catch (Exception $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||
}
|
||||
@@ -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()]);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
require dirname(__DIR__, 4) . '/includes/auth.php';
|
||||
require dirname(__DIR__, 4) . '/includes/db.php';
|
||||
require_login();
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
echo json_encode(['success' => false, 'error' => 'Méthode non autorisée']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$action = $_POST['action'] ?? '';
|
||||
|
||||
try {
|
||||
if ($action === 'save_cell') {
|
||||
$date = $_POST['plan_date'];
|
||||
$service = $_POST['service'];
|
||||
$person_id = (int)$_POST['person_id'];
|
||||
$meal_name = trim($_POST['meal_name']);
|
||||
|
||||
if (empty($meal_name)) {
|
||||
$stmt = $pdo->prepare("DELETE FROM pf_meals_plan WHERE plan_date=? AND service=? AND person_id=?");
|
||||
$stmt->execute([$date, $service, $person_id]);
|
||||
} else {
|
||||
$stmt = $pdo->prepare("INSERT INTO pf_meals_plan (plan_date, service, person_id, meal_name) VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE meal_name = VALUES(meal_name)");
|
||||
$stmt->execute([$date, $service, $person_id, $meal_name]);
|
||||
}
|
||||
echo json_encode(['success' => true]);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($action === 'save_bulk') {
|
||||
$updates = json_decode($_POST['updates'], true);
|
||||
if (is_array($updates)) {
|
||||
$pdo->beginTransaction();
|
||||
$stmtDel = $pdo->prepare("DELETE FROM pf_meals_plan WHERE plan_date=? AND service=? AND person_id=?");
|
||||
$stmtIns = $pdo->prepare("INSERT INTO pf_meals_plan (plan_date, service, person_id, meal_name) VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE meal_name = VALUES(meal_name)");
|
||||
|
||||
foreach ($updates as $u) {
|
||||
if (empty(trim($u['meal_name']))) {
|
||||
$stmtDel->execute([$u['plan_date'], $u['service'], $u['person_id']]);
|
||||
} else {
|
||||
$stmtIns->execute([$u['plan_date'], $u['service'], $u['person_id'], trim($u['meal_name'])]);
|
||||
}
|
||||
}
|
||||
$pdo->commit();
|
||||
}
|
||||
echo json_encode(['success' => true]);
|
||||
exit;
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
if ($pdo->inTransaction()) $pdo->rollBack();
|
||||
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||
}
|
||||
@@ -1,5 +1,53 @@
|
||||
/* Liste module JS */
|
||||
const API = '/modules/liste/api.php';
|
||||
<?php
|
||||
// modules/food/views/liste.php
|
||||
if (!isset($pdo)) { exit; }
|
||||
?>
|
||||
|
||||
<div class="liste-main">
|
||||
<!-- Tabs des listes -->
|
||||
<div class="liste-tabs-bar">
|
||||
<div class="liste-tabs" id="liste-tabs"></div>
|
||||
<button class="liste-tab-add" id="btn-add-list" title="<?= htmlspecialchars(tr('liste_new_list')) ?>">+</button>
|
||||
</div>
|
||||
|
||||
<!-- Corps de la liste active -->
|
||||
<div class="liste-body" id="liste-body">
|
||||
<div class="liste-add-card">
|
||||
<input type="text" id="liste-input" maxlength="500" placeholder="<?= htmlspecialchars(tr('liste_placeholder_add')) ?>" autocomplete="off">
|
||||
<button id="btn-liste-add"><?= htmlspecialchars(tr('liste_btn_add')) ?></button>
|
||||
</div>
|
||||
|
||||
<div class="liste-toolbar" id="liste-toolbar">
|
||||
<button class="btn-tool" id="btn-uncheck-all"><?= htmlspecialchars(tr('liste_btn_uncheck_all')) ?></button>
|
||||
<button class="btn-tool btn-tool-danger" id="btn-delete-picked"><?= htmlspecialchars(tr('liste_btn_delete_picked')) ?></button>
|
||||
<button class="btn-tool btn-tool-danger" id="btn-clear-all"><?= htmlspecialchars(tr('liste_btn_clear_all')) ?></button>
|
||||
</div>
|
||||
|
||||
<div id="liste-items-root"></div>
|
||||
<div id="liste-history-root"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const LISTE_TRANSLATIONS = {
|
||||
already_in: <?= json_encode(tr('liste_already_in')) ?>,
|
||||
confirm_clear: <?= json_encode(tr('liste_confirm_clear')) ?>,
|
||||
confirm_delete_list: <?= json_encode(tr('liste_confirm_delete_list')) ?>,
|
||||
rename_list: <?= json_encode(tr('liste_rename_list')) ?>,
|
||||
new_name: <?= json_encode(tr('liste_new_name')) ?>,
|
||||
list_deleted: <?= json_encode(tr('liste_list_deleted')) ?>,
|
||||
list_created: <?= json_encode(tr('liste_list_created')) ?>,
|
||||
list_renamed: <?= json_encode(tr('liste_list_renamed')) ?>,
|
||||
edit_placeholder: <?= json_encode(tr('liste_edit_placeholder')) ?>,
|
||||
history_title: <?= json_encode(tr('liste_history_title')) ?>,
|
||||
added: <?= json_encode(tr('liste_added')) ?>,
|
||||
updated: <?= json_encode(tr('liste_updated')) ?>,
|
||||
deleted: <?= json_encode(tr('liste_deleted')) ?>,
|
||||
cancel: <?= json_encode(tr('cancel')) ?>,
|
||||
};
|
||||
|
||||
// ⚠️ NOUVEAU CHEMIN D'API
|
||||
const API = '/modules/food/includes/api/api-liste.php';
|
||||
const T = window.LISTE_TRANSLATIONS || {};
|
||||
|
||||
const state = {
|
||||
@@ -515,3 +563,5 @@ async function init() {
|
||||
}
|
||||
|
||||
init();
|
||||
|
||||
</script>
|
||||
@@ -0,0 +1,357 @@
|
||||
<?php
|
||||
// modules/meals/main.php
|
||||
|
||||
// Récupération stricte des membres du foyer (Exclusion des intervenants et proches)
|
||||
$stmt = $pdo->query("SELECT id, name FROM pf_people WHERE role NOT IN ('helper', 'nounou', 'proche_adulte', 'proche_enfant', 'relative') AND is_active = 1 ORDER BY id ASC");
|
||||
$members = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
if (empty($members)) {
|
||||
echo "<div class='pf-alert pf-alert--warning'>" . tr('meals_no_members') . "</div>";
|
||||
return;
|
||||
}
|
||||
|
||||
// Calcul des 14 jours (Lundi semaine courante -> Dimanche semaine prochaine)
|
||||
$start_date = new DateTime();
|
||||
$start_date->modify('monday this week');
|
||||
$dates = [];
|
||||
for ($i = 0; $i < 14; $i++) {
|
||||
$d = clone $start_date;
|
||||
$d->modify("+$i days");
|
||||
$dates[] = $d->format('Y-m-d');
|
||||
}
|
||||
|
||||
$stmtMeals = $pdo->prepare("SELECT plan_date, service, person_id, meal_name FROM pf_meals_plan WHERE plan_date BETWEEN ? AND ?");
|
||||
$stmtMeals->execute([$dates[0], end($dates)]);
|
||||
$mealsData = [];
|
||||
while ($row = $stmtMeals->fetch(PDO::FETCH_ASSOC)) {
|
||||
$mealsData[$row['plan_date']][$row['service']][$row['person_id']] = $row['meal_name'];
|
||||
}
|
||||
?>
|
||||
|
||||
<div class="pf-container meals-container">
|
||||
<div class="meals-header-bar">
|
||||
<h1 class="meals-title">
|
||||
<span class="meals-title-icon">🍽️</span>
|
||||
<span data-i18n="meals_board_title"><?= tr('meals_board_title') ?></span>
|
||||
</h1>
|
||||
<div class="meals-actions">
|
||||
<!-- NOUVEAU : Bouton Import PDF -->
|
||||
<button type="button" class="pf-btn btn-secondary" onclick="document.getElementById('importMenuModal').classList.add('open'); document.body.classList.add('no-scroll');">
|
||||
📄 <?= tr('meal_btn_import_pdf') ?>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Barre contextuelle (Fixe en bas) -->
|
||||
<div class="context-toolbar" id="contextToolbar">
|
||||
<button class="ctx-btn" onmousedown="event.preventDefault(); applyToAllInRow()">➡️ <span data-i18n="btn_apply_all"><?= tr('btn_apply_all') ?></span></button>
|
||||
<button class="ctx-btn primary" onmousedown="event.preventDefault(); openCopySheetFromCell()">🔄 <span data-i18n="btn_copy_to"><?= tr('btn_copy_to') ?></span></button>
|
||||
</div>
|
||||
|
||||
<div class="meals-wrapper">
|
||||
<?php
|
||||
$weeks = array_chunk($dates, 7);
|
||||
foreach ($weeks as $weekIndex => $weekDates):
|
||||
$startWeekStr = date('d/m', strtotime($weekDates[0]));
|
||||
$endWeekStr = date('d/m', strtotime(end($weekDates)));
|
||||
?>
|
||||
<h2 class="meals-week-title"><?= tr('meal_week') ?> (<?= $startWeekStr ?> - <?= $endWeekStr ?>)</h2>
|
||||
<div class="meals-table-container">
|
||||
<table class="meals-table" id="mealsTable_<?= $weekIndex ?>">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="col-sticky"></th>
|
||||
<?php foreach ($members as $m): ?>
|
||||
<th><?= htmlspecialchars(strtoupper($m['name'])) ?></th>
|
||||
<?php endforeach; ?>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($weekDates as $dateStr):
|
||||
$dayKey = strtolower(date('D', strtotime($dateStr)));
|
||||
$dayName = tr('day_' . $dayKey);
|
||||
$dayDate = date('d/m', strtotime($dateStr));
|
||||
$fullDayName = tr('day_full_' . $dayKey) . ' ' . $dayDate;
|
||||
?>
|
||||
<tr class="row-lunch">
|
||||
<td class="col-sticky">
|
||||
<span class="day-label"><?= $dayName ?> <?= $dayDate ?></span>
|
||||
<div class="service-icon" onmousedown="event.preventDefault(); openCopySheetFromRow('<?= $dateStr ?>', 'lunch', '<?= addslashes($fullDayName) ?>')" title="<?= tr('meals_copy_lunch') ?>">☀️</div>
|
||||
</td>
|
||||
<?php foreach ($members as $m):
|
||||
$val = $mealsData[$dateStr]['lunch'][$m['id']] ?? '';
|
||||
?>
|
||||
<td class="meal-input-cell">
|
||||
<textarea class="meal-input js-meal-input" data-d="<?= $dateStr ?>" data-s="lunch" data-p="<?= $m['id'] ?>" placeholder="-"><?= htmlspecialchars($val) ?></textarea>
|
||||
</td>
|
||||
<?php endforeach; ?>
|
||||
</tr>
|
||||
|
||||
<tr class="row-dinner">
|
||||
<td class="col-sticky">
|
||||
<span class="day-label invisible"><?= $dayName ?> <?= $dayDate ?></span>
|
||||
<div class="service-icon" onmousedown="event.preventDefault(); openCopySheetFromRow('<?= $dateStr ?>', 'dinner', '<?= addslashes($fullDayName) ?>')" title="<?= tr('meals_copy_dinner') ?>">🌙</div>
|
||||
</td>
|
||||
<?php foreach ($members as $m):
|
||||
$val = $mealsData[$dateStr]['dinner'][$m['id']] ?? '';
|
||||
?>
|
||||
<td class="meal-input-cell">
|
||||
<textarea class="meal-input js-meal-input" data-d="<?= $dateStr ?>" data-s="dinner" data-p="<?= $m['id'] ?>" placeholder="-"><?= htmlspecialchars($val) ?></textarea>
|
||||
</td>
|
||||
<?php endforeach; ?>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bottom Sheet de Copie (Format Grille Ultra-compacte) -->
|
||||
<div id="copyModal" class="pf-modal pf-bottom-sheet">
|
||||
<div class="pf-modal-content pf-bs-content copy-modal-large">
|
||||
<div class="bs-header">
|
||||
<h3 class="bs-title">🔄 <span data-i18n="modal_copy_title"><?= tr('modal_copy_title') ?></span></h3>
|
||||
<button class="bs-close" onclick="closeCopyModal()">×</button>
|
||||
</div>
|
||||
<div id="copySourceLabel" class="meals-copy-source">Source : ...</div>
|
||||
|
||||
<div class="week-section-title" data-i18n="week_current"><?= tr('week_current') ?></div>
|
||||
<div class="copy-days-grid">
|
||||
<?php for($i=0; $i<7; $i++): $dStr = $dates[$i]; $dName = tr('day_full_' . strtolower(date('D', strtotime($dStr)))) . ' ' . date('d/m', strtotime($dStr)); ?>
|
||||
<div class="copy-day-card">
|
||||
<div class="copy-day-name"><?= $dName ?></div>
|
||||
<div class="copy-day-actions">
|
||||
<button type="button" class="btn-target-service" onmousedown="event.preventDefault(); executeCopy('<?= $dStr ?>', 'lunch')" title="<?= tr('meal_lunch') ?>">☀️</button>
|
||||
<button type="button" class="btn-target-service" onmousedown="event.preventDefault(); executeCopy('<?= $dStr ?>', 'dinner')" title="<?= tr('meal_dinner') ?>">🌙</button>
|
||||
</div>
|
||||
</div>
|
||||
<?php endfor; ?>
|
||||
</div>
|
||||
|
||||
<div class="week-section-title week-section-mt" data-i18n="week_next"><?= tr('week_next') ?></div>
|
||||
<div class="copy-days-grid">
|
||||
<?php for($i=7; $i<14; $i++): $dStr = $dates[$i]; $dName = tr('day_full_' . strtolower(date('D', strtotime($dStr)))) . ' ' . date('d/m', strtotime($dStr)); ?>
|
||||
<div class="copy-day-card">
|
||||
<div class="copy-day-name"><?= $dName ?></div>
|
||||
<div class="copy-day-actions">
|
||||
<button type="button" class="btn-target-service" onmousedown="event.preventDefault(); executeCopy('<?= $dStr ?>', 'lunch')" title="<?= tr('meal_lunch') ?>">☀️</button>
|
||||
<button type="button" class="btn-target-service" onmousedown="event.preventDefault(); executeCopy('<?= $dStr ?>', 'dinner')" title="<?= tr('meal_dinner') ?>">🌙</button>
|
||||
</div>
|
||||
</div>
|
||||
<?php endfor; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- NOUVEAU : Modale d'import PDF Cantine -->
|
||||
<div id="importMenuModal" class="pf-modal">
|
||||
<div class="pf-modal-content">
|
||||
<div class="pf-modal-header" style="display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid var(--border-light); padding-bottom: 15px; margin-bottom: 20px;">
|
||||
<h3 class="pf-modal-title" style="margin: 0; border: none; padding: 0;">📄 <?= tr('meal_import_title') ?></h3>
|
||||
<button type="button" class="pf-modal-close" style="background: none; border: none; font-size: 1.5rem; cursor: pointer; color: var(--text-muted);" onclick="document.getElementById('importMenuModal').classList.remove('open'); document.body.classList.remove('no-scroll');">×</button>
|
||||
</div>
|
||||
<form id="importMenuForm" onsubmit="importPdfMenu(event)" action="/modules/food/includes/api/import-pdf.php" enctype="multipart/form-data">
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(csrf_token()) ?>">
|
||||
|
||||
<div class="pf-form-group">
|
||||
<label class="pf-label"><?= tr('meal_import_child') ?></label>
|
||||
<select name="person_id" class="pf-input" required>
|
||||
<option value="" disabled selected>-- <?= tr('meal_select_child') ?> --</option>
|
||||
<?php foreach ($members as $m): ?>
|
||||
<option value="<?= $m['id'] ?>"><?= htmlspecialchars($m['name']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="pf-form-group">
|
||||
<label class="pf-label"><?= tr('meal_pdf_file') ?></label>
|
||||
<div class="meal-dropzone" onclick="document.getElementById('canteenPdf').click()">
|
||||
<span class="meal-dropzone-icon">📥</span>
|
||||
<span id="pdfFileName"><?= tr('meal_drop_pdf') ?></span>
|
||||
<input type="file" id="canteenPdf" name="pdf_file" accept=".pdf" style="display: none;" required onchange="document.getElementById('pdfFileName').innerText = this.files[0] ? this.files[0].name : '<?= addslashes(tr('meal_drop_pdf')) ?>';">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="pf-btn btn-secondary" onclick="document.getElementById('importMenuModal').classList.remove('open'); document.body.classList.remove('no-scroll');"><?= tr('btn_cancel') ?></button>
|
||||
<button type="submit" class="pf-btn" id="btnImportPdf"><?= tr('btn_import') ?></button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Sauvegarde automatique AJAX à chaque modification
|
||||
document.querySelectorAll('.js-meal-input').forEach(input => {
|
||||
input.addEventListener('change', async function() {
|
||||
const fd = new FormData();
|
||||
fd.append('action', 'save_cell');
|
||||
fd.append('plan_date', this.dataset.d);
|
||||
fd.append('service', this.dataset.s);
|
||||
fd.append('person_id', this.dataset.p);
|
||||
fd.append('meal_name', this.value);
|
||||
|
||||
try {
|
||||
await pachaFetch('/modules/food/includes/api/save-meals.php', { method: 'POST', body: fd });
|
||||
flashSuccess(this);
|
||||
} catch(e) {
|
||||
console.error("Save error", e);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Focus Context Bar
|
||||
let copyState = { type: null, sourceDate: null, service: null, personIdx: null, sourceElements: [], fullDayName: '' };
|
||||
|
||||
document.addEventListener('focusin', (e) => {
|
||||
if(e.target.classList.contains('js-meal-input')) {
|
||||
copyState = {
|
||||
type: 'cell',
|
||||
sourceDate: e.target.getAttribute('data-d'),
|
||||
service: e.target.getAttribute('data-s'),
|
||||
personIdx: e.target.getAttribute('data-p'),
|
||||
sourceElements: [e.target],
|
||||
fullDayName: ''
|
||||
};
|
||||
document.getElementById('contextToolbar').classList.add('active');
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('focusout', (e) => {
|
||||
setTimeout(() => {
|
||||
if(document.activeElement && !document.activeElement.classList.contains('js-meal-input')) {
|
||||
document.getElementById('contextToolbar').classList.remove('active');
|
||||
}
|
||||
}, 150);
|
||||
});
|
||||
|
||||
// Actions Toolbar
|
||||
window.applyToAllInRow = async function() {
|
||||
if(copyState.type !== 'cell' || !copyState.sourceElements[0].value.trim()) return;
|
||||
const val = copyState.sourceElements[0].value;
|
||||
const inputs = document.querySelectorAll(`.js-meal-input[data-d="${copyState.sourceDate}"][data-s="${copyState.service}"]`);
|
||||
|
||||
let updates = [];
|
||||
inputs.forEach(inp => {
|
||||
if(inp !== copyState.sourceElements[0]) {
|
||||
inp.value = val;
|
||||
flashSuccess(inp);
|
||||
updates.push({ plan_date: inp.dataset.d, service: inp.dataset.s, person_id: inp.dataset.p, meal_name: val });
|
||||
}
|
||||
});
|
||||
|
||||
if (updates.length > 0) {
|
||||
const fd = new FormData();
|
||||
fd.append('action', 'save_bulk');
|
||||
fd.append('updates', JSON.stringify(updates));
|
||||
await pachaFetch('/modules/food/includes/api/save-meals.php', { method: 'POST', body: fd });
|
||||
}
|
||||
};
|
||||
|
||||
window.openCopySheetFromCell = function() {
|
||||
if(!copyState.sourceElements[0] || !copyState.sourceElements[0].value.trim()) return;
|
||||
if(document.activeElement) document.activeElement.blur();
|
||||
|
||||
const svcLabel = copyState.service === 'lunch' ? (window.I18N['meal_lunch'] || 'Midi') : (window.I18N['meal_dinner'] || 'Soir');
|
||||
const dObj = new Date(copyState.sourceDate);
|
||||
const dayKey = dObj.toLocaleDateString('en-US', {weekday: 'short'}).toLowerCase();
|
||||
const dayName = window.I18N['day_full_' + dayKey] || dObj.toLocaleDateString();
|
||||
|
||||
document.getElementById('copySourceLabel').innerText = `Source : ${window.I18N['source_individual'] || 'Individuel'} (${dayName} ${svcLabel})`;
|
||||
|
||||
document.getElementById('copyModal').classList.add('open');
|
||||
document.body.classList.add('no-scroll');
|
||||
};
|
||||
|
||||
window.openCopySheetFromRow = function(dateStr, service, fullDayName) {
|
||||
const inputs = document.querySelectorAll(`.js-meal-input[data-d="${dateStr}"][data-s="${service}"]`);
|
||||
if (!Array.from(inputs).some(inp => inp.value.trim() !== '')) return;
|
||||
|
||||
copyState = { type: 'row', sourceDate: dateStr, service: service, sourceElements: Array.from(inputs), fullDayName: fullDayName };
|
||||
const svcLabel = service === 'lunch' ? (window.I18N['meal_lunch'] || 'Midi') : (window.I18N['meal_dinner'] || 'Soir');
|
||||
document.getElementById('copySourceLabel').innerText = `Source : ${window.I18N['source_row'] || 'Repas commun'} (${fullDayName} ${svcLabel})`;
|
||||
|
||||
document.getElementById('copyModal').classList.add('open');
|
||||
document.body.classList.add('no-scroll');
|
||||
};
|
||||
|
||||
window.closeCopyModal = function() {
|
||||
document.getElementById('copyModal').classList.remove('open');
|
||||
document.body.classList.remove('no-scroll');
|
||||
};
|
||||
|
||||
window.executeCopy = async function(targetDate, targetService) {
|
||||
let updates = [];
|
||||
|
||||
if(copyState.type === 'cell') {
|
||||
const val = copyState.sourceElements[0].value;
|
||||
const targetInput = document.querySelector(`.js-meal-input[data-d="${targetDate}"][data-s="${targetService}"][data-p="${copyState.personIdx}"]`);
|
||||
if(targetInput) {
|
||||
targetInput.value = val;
|
||||
flashSuccess(targetInput);
|
||||
updates.push({ plan_date: targetDate, service: targetService, person_id: copyState.personIdx, meal_name: val });
|
||||
}
|
||||
} else if (copyState.type === 'row') {
|
||||
const targetInputs = document.querySelectorAll(`.js-meal-input[data-d="${targetDate}"][data-s="${targetService}"]`);
|
||||
copyState.sourceElements.forEach((inp, idx) => {
|
||||
if(inp.value.trim() !== '') {
|
||||
targetInputs[idx].value = inp.value;
|
||||
flashSuccess(targetInputs[idx]);
|
||||
updates.push({ plan_date: targetDate, service: targetService, person_id: targetInputs[idx].dataset.p, meal_name: inp.value });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
closeCopyModal();
|
||||
|
||||
if (updates.length > 0) {
|
||||
const fd = new FormData();
|
||||
fd.append('action', 'save_bulk');
|
||||
fd.append('updates', JSON.stringify(updates));
|
||||
await pachaFetch('/modules/meals/includes/api/save-meals.php', { method: 'POST', body: fd });
|
||||
}
|
||||
};
|
||||
|
||||
function flashSuccess(input) {
|
||||
input.style.transition = 'background 0.3s';
|
||||
input.style.background = 'var(--alert-success-bg)';
|
||||
setTimeout(() => input.style.background = 'transparent', 400);
|
||||
}
|
||||
|
||||
// NOUVEAU : Fonction Javascript d'Import PDF
|
||||
window.importPdfMenu = async function(e) {
|
||||
e.preventDefault();
|
||||
const form = e.target;
|
||||
const btn = document.getElementById('btnImportPdf');
|
||||
const oldText = btn.innerText;
|
||||
btn.disabled = true;
|
||||
btn.innerText = '⏳...';
|
||||
|
||||
const fd = new FormData(form);
|
||||
const actionUrl = form.getAttribute('action');
|
||||
|
||||
try {
|
||||
const res = await pachaFetch(actionUrl, { method: 'POST', body: fd });
|
||||
if (res && res.success) {
|
||||
document.getElementById('importMenuModal').classList.remove('open');
|
||||
document.body.classList.remove('no-scroll');
|
||||
if (window.showToast) showToast(window.I18N['meal_import_success'] || 'Menus importés !', 'success');
|
||||
setTimeout(() => window.location.reload(), 1000);
|
||||
} else {
|
||||
if (window.showToast) showToast(res.error || window.I18N['error_occured'], 'error');
|
||||
else alert(res.error);
|
||||
}
|
||||
} catch(err) {
|
||||
if (window.showToast) showToast(window.I18N['error_occured'], 'error');
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.innerText = oldText;
|
||||
form.reset();
|
||||
document.getElementById('pdfFileName').innerText = window.I18N['meal_drop_pdf'] || 'Cliquer pour ajouter le PDF';
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -1,575 +0,0 @@
|
||||
/* ── Module Liste ─────────────────────────────────────────────────────────── */
|
||||
|
||||
.liste-main {
|
||||
max-width: 680px;
|
||||
margin: 0 auto;
|
||||
padding: 1rem 1rem 4rem;
|
||||
}
|
||||
|
||||
/* ── Tabs bar ──────────────────────────────────────────────────────────────── */
|
||||
|
||||
.liste-tabs-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: .4rem;
|
||||
margin-bottom: .75rem;
|
||||
overflow-x: auto;
|
||||
padding-bottom: 2px;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
.liste-tabs-bar::-webkit-scrollbar { display: none; }
|
||||
|
||||
.liste-tabs {
|
||||
display: flex;
|
||||
gap: .35rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.liste-tab {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: .25rem;
|
||||
padding: .4rem .75rem;
|
||||
border-radius: 20px;
|
||||
border: 1px solid var(--border, #dee2e6);
|
||||
background: var(--card-bg, #fff);
|
||||
cursor: pointer;
|
||||
font-size: .875rem;
|
||||
white-space: nowrap;
|
||||
transition: background .15s, border-color .15s;
|
||||
user-select: none;
|
||||
}
|
||||
.liste-tab:hover { background: var(--hover-bg, #f8f9fa); }
|
||||
.liste-tab.active {
|
||||
background: var(--primary, #4361ee);
|
||||
color: #fff;
|
||||
border-color: var(--primary, #4361ee);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.liste-tab-name { pointer-events: none; }
|
||||
|
||||
.liste-tab-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0 .1rem;
|
||||
cursor: pointer;
|
||||
font-size: .8rem;
|
||||
color: rgba(255,255,255,.75);
|
||||
line-height: 1;
|
||||
transition: color .15s;
|
||||
}
|
||||
.liste-tab-btn:hover { color: #fff; }
|
||||
|
||||
.liste-tab-add {
|
||||
flex-shrink: 0;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
border-radius: 50%;
|
||||
border: 2px dashed var(--border, #dee2e6);
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
font-size: 1.1rem;
|
||||
color: var(--muted, #6c757d);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: border-color .15s, color .15s;
|
||||
line-height: 1;
|
||||
}
|
||||
.liste-tab-add:hover {
|
||||
border-color: var(--primary, #4361ee);
|
||||
color: var(--primary, #4361ee);
|
||||
}
|
||||
|
||||
/* ── New list form ─────────────────────────────────────────────────────────── */
|
||||
|
||||
.liste-new-form {
|
||||
display: flex;
|
||||
gap: .5rem;
|
||||
align-items: center;
|
||||
margin-bottom: .75rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.liste-new-form input {
|
||||
flex: 1;
|
||||
min-width: 160px;
|
||||
padding: .45rem .75rem;
|
||||
border: 1px solid var(--primary, #4361ee);
|
||||
border-radius: 8px;
|
||||
font-size: .9rem;
|
||||
background: var(--input-bg, #fff);
|
||||
color: var(--text, #212529);
|
||||
}
|
||||
.liste-new-form button {
|
||||
padding: .45rem .9rem;
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: .875rem;
|
||||
}
|
||||
.liste-new-form #btn-confirm-new-list {
|
||||
background: var(--primary, #4361ee);
|
||||
color: #fff;
|
||||
}
|
||||
.liste-new-form #btn-cancel-new-list {
|
||||
background: var(--muted-bg, #e9ecef);
|
||||
color: var(--text, #212529);
|
||||
}
|
||||
|
||||
.hidden { display: none !important; }
|
||||
|
||||
/* ── Add card ──────────────────────────────────────────────────────────────── */
|
||||
|
||||
.liste-add-card {
|
||||
display: flex;
|
||||
gap: .5rem;
|
||||
background: var(--card-bg, #fff);
|
||||
border: 1px solid var(--border, #dee2e6);
|
||||
border-radius: 12px;
|
||||
padding: .6rem .75rem;
|
||||
margin-bottom: .75rem;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,.05);
|
||||
}
|
||||
.liste-add-card input {
|
||||
flex: 1;
|
||||
border: none;
|
||||
outline: none;
|
||||
font-size: .95rem;
|
||||
background: transparent;
|
||||
color: var(--text, #212529);
|
||||
}
|
||||
.liste-add-card button {
|
||||
background: var(--primary, #4361ee);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: .4rem .9rem;
|
||||
cursor: pointer;
|
||||
font-size: .875rem;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* ── Toolbar ───────────────────────────────────────────────────────────────── */
|
||||
|
||||
.liste-toolbar {
|
||||
display: flex;
|
||||
gap: .4rem;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: .75rem;
|
||||
}
|
||||
.btn-tool {
|
||||
padding: .35rem .75rem;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border, #dee2e6);
|
||||
background: var(--card-bg, #fff);
|
||||
cursor: pointer;
|
||||
font-size: .8rem;
|
||||
color: var(--text, #212529);
|
||||
transition: background .15s;
|
||||
}
|
||||
.btn-tool:hover { background: var(--hover-bg, #f8f9fa); }
|
||||
.btn-tool-danger { color: var(--danger, #e63946); border-color: var(--danger, #e63946); }
|
||||
.btn-tool-danger:hover { background: rgba(230,57,70,.08); }
|
||||
|
||||
/* ── Items ─────────────────────────────────────────────────────────────────── */
|
||||
|
||||
.liste-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: .5rem;
|
||||
padding: .55rem .75rem;
|
||||
background: var(--card-bg, #fff);
|
||||
border: 1px solid var(--border, #dee2e6);
|
||||
border-radius: 10px;
|
||||
margin-bottom: .4rem;
|
||||
transition: opacity .2s;
|
||||
}
|
||||
.liste-row.in-cart { opacity: .55; }
|
||||
.liste-row.in-cart .liste-label { text-decoration: line-through; }
|
||||
|
||||
.liste-check-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: .6rem;
|
||||
flex: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
.liste-check {
|
||||
width: 1.1rem;
|
||||
height: 1.1rem;
|
||||
accent-color: var(--primary, #4361ee);
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.liste-label { font-size: .95rem; }
|
||||
|
||||
.liste-row-actions {
|
||||
display: flex;
|
||||
gap: .25rem;
|
||||
opacity: 0;
|
||||
transition: opacity .15s;
|
||||
}
|
||||
.liste-row:hover .liste-row-actions,
|
||||
.liste-row:focus-within .liste-row-actions { opacity: 1; }
|
||||
|
||||
.btn-edit-item, .btn-delete-item {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: .2rem .3rem;
|
||||
border-radius: 6px;
|
||||
font-size: .9rem;
|
||||
transition: background .15s;
|
||||
}
|
||||
.btn-edit-item:hover { background: var(--hover-bg, #f0f0f0); }
|
||||
.btn-delete-item:hover { background: rgba(230,57,70,.1); }
|
||||
|
||||
.liste-cart-divider {
|
||||
font-size: .78rem;
|
||||
color: var(--muted, #6c757d);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .04em;
|
||||
padding: .5rem .25rem .25rem;
|
||||
margin-top: .25rem;
|
||||
}
|
||||
|
||||
.liste-empty-items {
|
||||
text-align: center;
|
||||
padding: 2rem 1rem;
|
||||
color: var(--muted, #6c757d);
|
||||
}
|
||||
.liste-empty-items span { font-size: 2rem; display: block; margin-bottom: .5rem; }
|
||||
|
||||
/* ── History ───────────────────────────────────────────────────────────────── */
|
||||
|
||||
.liste-history-card {
|
||||
margin-top: 1.25rem;
|
||||
padding: .75rem;
|
||||
background: var(--card-bg, #fff);
|
||||
border: 1px solid var(--border, #dee2e6);
|
||||
border-radius: 12px;
|
||||
}
|
||||
.liste-history-title {
|
||||
font-size: .75rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .05em;
|
||||
color: var(--muted, #6c757d);
|
||||
margin-bottom: .5rem;
|
||||
}
|
||||
.liste-history-chips { display: flex; flex-wrap: wrap; gap: .35rem; }
|
||||
.liste-chip {
|
||||
padding: .25rem .65rem;
|
||||
border-radius: 20px;
|
||||
border: 1px solid var(--border, #dee2e6);
|
||||
background: var(--muted-bg, #f8f9fa);
|
||||
cursor: pointer;
|
||||
font-size: .82rem;
|
||||
color: var(--text, #212529);
|
||||
transition: background .15s, border-color .15s;
|
||||
}
|
||||
.liste-chip:hover {
|
||||
background: var(--primary-light, #eef0fd);
|
||||
border-color: var(--primary, #4361ee);
|
||||
color: var(--primary, #4361ee);
|
||||
}
|
||||
|
||||
/* ── Toast ─────────────────────────────────────────────────────────────────── */
|
||||
|
||||
.liste-toast-container {
|
||||
position: fixed;
|
||||
bottom: 1.5rem;
|
||||
right: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: .4rem;
|
||||
z-index: 9999;
|
||||
}
|
||||
.liste-toast {
|
||||
padding: .55rem 1rem;
|
||||
border-radius: 8px;
|
||||
font-size: .875rem;
|
||||
background: #333;
|
||||
color: #fff;
|
||||
box-shadow: 0 3px 12px rgba(0,0,0,.2);
|
||||
opacity: 0;
|
||||
transform: translateY(8px);
|
||||
transition: opacity .2s, transform .2s;
|
||||
}
|
||||
.liste-toast.show { opacity: 1; transform: translateY(0); }
|
||||
.liste-toast-warn { background: #f0a500; }
|
||||
.liste-toast-error { background: #e63946; }
|
||||
|
||||
/* ── Empty state (no lists at all) ────────────────────────────────────────── */
|
||||
|
||||
.liste-empty-state {
|
||||
text-align: center;
|
||||
padding: 3rem 1rem;
|
||||
color: var(--muted, #6c757d);
|
||||
}
|
||||
.liste-empty-icon { font-size: 3rem; margin-bottom: .75rem; }
|
||||
|
||||
/* ── List modal ────────────────────────────────────────────────────────────── */
|
||||
|
||||
.liste-modal-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0,0,0,.45);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 2000;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.liste-modal {
|
||||
background: var(--card-bg, #fff);
|
||||
border: 1px solid var(--border, #dee2e6);
|
||||
border-radius: 14px;
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
box-shadow: 0 20px 60px rgba(0,0,0,.2);
|
||||
}
|
||||
|
||||
.liste-modal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 1rem 1.25rem;
|
||||
border-bottom: 1px solid var(--border, #dee2e6);
|
||||
}
|
||||
.liste-modal-header h3 { margin: 0; font-size: .95rem; font-weight: 700; color: var(--text, #212529); }
|
||||
|
||||
.liste-modal-close {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.3rem;
|
||||
cursor: pointer;
|
||||
color: var(--muted, #6c757d);
|
||||
line-height: 1;
|
||||
padding: 0 .25rem;
|
||||
transition: color .15s;
|
||||
}
|
||||
.liste-modal-close:hover { color: var(--text, #212529); }
|
||||
|
||||
.liste-modal-body {
|
||||
padding: 1.25rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: .85rem;
|
||||
}
|
||||
|
||||
.liste-modal-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: .5rem;
|
||||
padding: .85rem 1.25rem;
|
||||
border-top: 1px solid var(--border, #dee2e6);
|
||||
}
|
||||
.liste-modal-footer .btn-tool-danger { margin-right: auto; }
|
||||
|
||||
.liste-form-group { display: flex; flex-direction: column; gap: .3rem; }
|
||||
.liste-form-label {
|
||||
font-size: .73rem;
|
||||
font-weight: 700;
|
||||
color: var(--muted, #6c757d);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .04em;
|
||||
}
|
||||
|
||||
.liste-modal-name, .liste-modal-type {
|
||||
padding: .5rem .7rem;
|
||||
border: 1px solid var(--border, #dee2e6);
|
||||
border-radius: 8px;
|
||||
background: var(--input-bg, #fff);
|
||||
color: var(--text, #212529);
|
||||
font-size: .875rem;
|
||||
font-family: inherit;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
transition: border-color .15s;
|
||||
}
|
||||
.liste-modal-name:focus, .liste-modal-type:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary, #4361ee);
|
||||
box-shadow: 0 0 0 3px rgba(67,97,238,.15);
|
||||
}
|
||||
|
||||
.btn-tool-primary {
|
||||
background: var(--primary, #4361ee);
|
||||
color: #fff;
|
||||
border-color: var(--primary, #4361ee);
|
||||
}
|
||||
.btn-tool-primary:hover { filter: brightness(1.1); background: var(--primary, #4361ee); }
|
||||
|
||||
/* ── Color picker ──────────────────────────────────────────────────────────── */
|
||||
|
||||
.liste-color-picker {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: .45rem;
|
||||
}
|
||||
|
||||
.liste-color-swatch {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border-radius: 50%;
|
||||
border: 3px solid transparent;
|
||||
background: var(--swatch, transparent);
|
||||
cursor: pointer;
|
||||
transition: transform .1s;
|
||||
outline: 2px solid transparent;
|
||||
outline-offset: 2px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.liste-color-swatch:hover { transform: scale(1.2); }
|
||||
.liste-color-swatch.active {
|
||||
outline-color: var(--swatch, var(--primary, #4361ee));
|
||||
border-color: var(--card-bg, #fff);
|
||||
}
|
||||
.liste-color-swatch-none {
|
||||
background: transparent !important;
|
||||
border: 2px dashed var(--border, #dee2e6) !important;
|
||||
}
|
||||
.liste-color-swatch-none.active { outline-color: var(--primary, #4361ee); }
|
||||
|
||||
/* ── Tab type emoji ────────────────────────────────────────────────────────── */
|
||||
|
||||
.liste-tab-type { font-size: .85rem; line-height: 1; }
|
||||
|
||||
/* ── Category section headers ──────────────────────────────────────────────── */
|
||||
|
||||
.liste-cat-section-header {
|
||||
font-size: .75rem;
|
||||
font-weight: 700;
|
||||
color: var(--muted, #6c757d);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .05em;
|
||||
padding: .6rem .25rem .2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: .4rem;
|
||||
}
|
||||
.liste-cat-section-header:first-child { padding-top: .1rem; }
|
||||
|
||||
.liste-cat-section-count {
|
||||
background: var(--muted-bg, #e9ecef);
|
||||
color: var(--muted, #6c757d);
|
||||
border-radius: 999px;
|
||||
font-size: .7rem;
|
||||
padding: .05rem .4rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0;
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
/* ── Category badge (per-item) ─────────────────────────────────────────────── */
|
||||
|
||||
.liste-cat-badge {
|
||||
flex-shrink: 0;
|
||||
background: none;
|
||||
border: 1px solid var(--border, #dee2e6);
|
||||
border-radius: 6px;
|
||||
padding: .1rem .3rem;
|
||||
cursor: pointer;
|
||||
font-size: .85rem;
|
||||
line-height: 1.4;
|
||||
color: inherit;
|
||||
transition: border-color .15s, background .15s;
|
||||
}
|
||||
.liste-cat-badge:hover {
|
||||
border-color: var(--primary, #4361ee);
|
||||
background: var(--primary-light, #eef0fd);
|
||||
}
|
||||
.liste-cat-badge-empty {
|
||||
opacity: .35;
|
||||
border-style: dashed;
|
||||
}
|
||||
.liste-row:hover .liste-cat-badge-empty { opacity: .7; }
|
||||
|
||||
/* ── Category picker popover ───────────────────────────────────────────────── */
|
||||
|
||||
.liste-cat-picker {
|
||||
position: fixed;
|
||||
z-index: 9999;
|
||||
background: var(--card-bg, #fff);
|
||||
border: 1px solid var(--border, #dee2e6);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,.15);
|
||||
width: 216px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.liste-cat-picker-title {
|
||||
font-size: .72rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .05em;
|
||||
color: var(--muted, #6c757d);
|
||||
padding: .6rem .75rem .35rem;
|
||||
border-bottom: 1px solid var(--border, #dee2e6);
|
||||
}
|
||||
|
||||
.liste-cat-picker-list {
|
||||
overflow-y: auto;
|
||||
max-height: 300px;
|
||||
padding: .3rem;
|
||||
}
|
||||
|
||||
.liste-cat-picker-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: .4rem;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
background: none;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: .4rem .6rem;
|
||||
font-size: .83rem;
|
||||
color: var(--text, #212529);
|
||||
cursor: pointer;
|
||||
transition: background .1s;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.liste-cat-picker-item:hover { background: var(--hover-bg, #f8f9fa); }
|
||||
.liste-cat-picker-item.active {
|
||||
background: var(--primary-light, #eef0fd);
|
||||
color: var(--primary, #4361ee);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ── Dark mode ─────────────────────────────────────────────────────────────── */
|
||||
|
||||
[data-theme='dark'] .liste-tab { background: #1e1e2e; border-color: #3a3a4e; color: #cdd6f4; }
|
||||
[data-theme='dark'] .liste-tab:hover { background: #2a2a3e; }
|
||||
[data-theme='dark'] .liste-tab.active { background: #4361ee; color: #fff; border-color: #4361ee; }
|
||||
[data-theme='dark'] .liste-add-card,
|
||||
[data-theme='dark'] .liste-row,
|
||||
[data-theme='dark'] .liste-history-card { background: #1e1e2e; border-color: #3a3a4e; }
|
||||
[data-theme='dark'] .liste-add-card input { color: #cdd6f4; }
|
||||
[data-theme='dark'] .liste-chip { background: #2a2a3e; border-color: #3a3a4e; color: #cdd6f4; }
|
||||
[data-theme='dark'] .liste-chip:hover { background: #313155; border-color: #4361ee; color: #89b4fa; }
|
||||
[data-theme='dark'] .btn-tool { background: #1e1e2e; border-color: #3a3a4e; color: #cdd6f4; }
|
||||
[data-theme='dark'] .btn-tool:hover { background: #2a2a3e; }
|
||||
[data-theme='dark'] .liste-new-form input { background: #1e1e2e; color: #cdd6f4; border-color: #4361ee; }
|
||||
[data-theme='dark'] .liste-new-form #btn-cancel-new-list { background: #2a2a3e; color: #cdd6f4; }
|
||||
[data-theme='dark'] .liste-modal { background: #1e1e2e; border-color: #3a3a4e; }
|
||||
[data-theme='dark'] .liste-modal-header,
|
||||
[data-theme='dark'] .liste-modal-footer { border-color: #3a3a4e; }
|
||||
[data-theme='dark'] .liste-modal-header h3 { color: #cdd6f4; }
|
||||
[data-theme='dark'] .liste-modal-name,
|
||||
[data-theme='dark'] .liste-modal-type { background: #2a2a3e; border-color: #3a3a4e; color: #cdd6f4; }
|
||||
[data-theme='dark'] .liste-color-swatch.active { border-color: #1e1e2e; }
|
||||
[data-theme='dark'] .liste-cat-badge { border-color: #3a3a4e; }
|
||||
[data-theme='dark'] .liste-cat-badge:hover { background: #313155; border-color: #4361ee; }
|
||||
[data-theme='dark'] .liste-cat-section-count { background: #2a2a3e; color: #8892b0; }
|
||||
[data-theme='dark'] .liste-cat-picker { background: #1e1e2e; border-color: #3a3a4e; }
|
||||
[data-theme='dark'] .liste-cat-picker-title { border-color: #3a3a4e; }
|
||||
[data-theme='dark'] .liste-cat-picker-item { color: #cdd6f4; }
|
||||
[data-theme='dark'] .liste-cat-picker-item:hover { background: #2a2a3e; }
|
||||
[data-theme='dark'] .liste-cat-picker-item.active { background: #313155; color: #89b4fa; }
|
||||
Reference in new issue
Block a user