Merge branch 'main' of http://nas.percolouco.com:3500/perco/HouseHub into parent_id #4
@@ -374,9 +374,11 @@ CREATE TABLE IF NOT EXISTS pf_todos (
|
|||||||
|
|
||||||
-- ─── Module Liste (multi-listes) ──────────────────────────────────────────────
|
-- ─── Module Liste (multi-listes) ──────────────────────────────────────────────
|
||||||
CREATE TABLE IF NOT EXISTS pf_lists (
|
CREATE TABLE IF NOT EXISTS pf_lists (
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
name VARCHAR(255) NOT NULL DEFAULT 'Ma liste',
|
name VARCHAR(255) NOT NULL DEFAULT 'Ma liste',
|
||||||
position INT NOT NULL DEFAULT 0,
|
color VARCHAR(20) DEFAULT NULL,
|
||||||
|
list_type VARCHAR(50) DEFAULT NULL,
|
||||||
|
position INT NOT NULL DEFAULT 0,
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
|||||||
@@ -1,105 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* HouseHub OS - Source Extractor 🦙🚀
|
|
||||||
* Génère un fichier Markdown contenant tout le code source du projet.
|
|
||||||
* Idéal pour alimenter une GEM ou un LLM.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// 1. 🚨 SÉCURITÉ : Ne jamais laisser ce fichier accessible publiquement sans protection !
|
|
||||||
// Appelle le script via : https://househub.nas.../extract_source.php?key=TON_MOT_DE_PASSE
|
|
||||||
$secretKey = 'mR83s7MmXbP$jer$9C4HnGkry6xhGL6';
|
|
||||||
|
|
||||||
if (!isset($_GET['key']) || $_GET['key'] !== $secretKey) {
|
|
||||||
http_response_code(403);
|
|
||||||
die("⛔ Accès refusé. Veuillez fournir la clé de sécurité.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Forcer l'affichage en texte brut dans le navigateur pour un copier-coller facile
|
|
||||||
header('Content-Type: text/plain; charset=utf-8');
|
|
||||||
|
|
||||||
$root = __DIR__;
|
|
||||||
|
|
||||||
// 2. CONFIGURATION DU FILTRAGE
|
|
||||||
// Dossiers à exclure (ne mets pas de slash au début)
|
|
||||||
$excludeDirs = [
|
|
||||||
'.git',
|
|
||||||
'.gitea',
|
|
||||||
'.devtools',
|
|
||||||
'vendor',
|
|
||||||
'node_modules',
|
|
||||||
'assets/img',
|
|
||||||
'modules/holidays/assets/img',
|
|
||||||
'modules/family-calendar/assets/img',
|
|
||||||
'modules/gift-list/assets/img'
|
|
||||||
];
|
|
||||||
|
|
||||||
// Fichiers spécifiques à exclure (Sécurité & Bruit)
|
|
||||||
$excludeFiles = [
|
|
||||||
'.env',
|
|
||||||
'.env.example',
|
|
||||||
'extract_source.php', // On s'exclut soi-même
|
|
||||||
'pachafamily_source.md',
|
|
||||||
'docker-compose.override.yml'
|
|
||||||
];
|
|
||||||
|
|
||||||
// Extensions autorisées (on ignore les images, pdf, zip, etc.)
|
|
||||||
$allowedExtensions = ['php', 'js', 'css', 'sql', 'json', 'yml', 'yaml', 'md', 'sh'];
|
|
||||||
|
|
||||||
// 3. INITIALISATION DU PARCOURS
|
|
||||||
$iterator = new RecursiveIteratorIterator(
|
|
||||||
new RecursiveDirectoryIterator($root, RecursiveDirectoryIterator::SKIP_DOTS),
|
|
||||||
RecursiveIteratorIterator::SELF_FIRST
|
|
||||||
);
|
|
||||||
|
|
||||||
$report = "# 🦙 Source Code HouseHub OS\n\n";
|
|
||||||
$report .= "> *Généré le " . date('Y-m-d H:i:s') . "*\n\n";
|
|
||||||
$report .= "---\n\n";
|
|
||||||
|
|
||||||
// 4. BOUCLE D'EXTRACTION
|
|
||||||
foreach ($iterator as $file) {
|
|
||||||
if ($file->isDir()) continue;
|
|
||||||
|
|
||||||
$filePath = $file->getPathname();
|
|
||||||
$relativePath = str_replace($root . DIRECTORY_SEPARATOR, '', $filePath);
|
|
||||||
$relativePath = str_replace('\\', '/', $relativePath); // Uniformisation Windows/Linux
|
|
||||||
|
|
||||||
// Extraction de l'extension
|
|
||||||
$ext = strtolower(pathinfo($filePath, PATHINFO_EXTENSION));
|
|
||||||
|
|
||||||
// A. Filtrage par dossier
|
|
||||||
$skip = false;
|
|
||||||
foreach ($excludeDirs as $dir) {
|
|
||||||
// Si le chemin commence par ce dossier, ou contient /ce_dossier/
|
|
||||||
if (strpos($relativePath, $dir . '/') === 0 || strpos($relativePath, '/' . $dir . '/') !== false) {
|
|
||||||
$skip = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ($skip) continue;
|
|
||||||
|
|
||||||
// B. Filtrage par nom de fichier
|
|
||||||
if (in_array(basename($filePath), $excludeFiles)) continue;
|
|
||||||
|
|
||||||
// C. Filtrage par extension
|
|
||||||
if (!in_array($ext, $allowedExtensions)) continue;
|
|
||||||
|
|
||||||
// 5. LECTURE ET FORMATAGE
|
|
||||||
$content = file_get_contents($filePath);
|
|
||||||
|
|
||||||
if ($content === false) {
|
|
||||||
$content = "// Erreur lors de la lecture de ce fichier.";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ajustement du tag de langage pour le Markdown
|
|
||||||
$lang = $ext;
|
|
||||||
if ($ext === 'js') $lang = 'javascript';
|
|
||||||
if ($ext === 'yml') $lang = 'yaml';
|
|
||||||
|
|
||||||
$report .= "### 📄 Fichier : `$relativePath`\n";
|
|
||||||
$report .= "```$lang\n";
|
|
||||||
$report .= trim($content) . "\n";
|
|
||||||
$report .= "```\n\n---\n\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
// 6. SORTIE
|
|
||||||
echo $report;
|
|
||||||
@@ -18,14 +18,6 @@ require __DIR__ . '/header.php';
|
|||||||
<button class="liste-tab-add" id="btn-add-list" title="<?= htmlspecialchars(tr('liste_new_list')) ?>">+</button>
|
<button class="liste-tab-add" id="btn-add-list" title="<?= htmlspecialchars(tr('liste_new_list')) ?>">+</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Formulaire de création de liste inline -->
|
|
||||||
<div class="liste-new-form hidden" id="liste-new-form">
|
|
||||||
<input type="text" id="new-list-name" maxlength="100"
|
|
||||||
placeholder="<?= htmlspecialchars(tr('liste_list_name_placeholder')) ?>" autocomplete="off">
|
|
||||||
<button id="btn-confirm-new-list"><?= htmlspecialchars(tr('liste_create')) ?></button>
|
|
||||||
<button id="btn-cancel-new-list"><?= htmlspecialchars(tr('cancel')) ?></button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Corps de la liste active -->
|
<!-- Corps de la liste active -->
|
||||||
<div class="liste-body" id="liste-body">
|
<div class="liste-body" id="liste-body">
|
||||||
|
|
||||||
|
|||||||
+19
-7
@@ -15,6 +15,7 @@ require_once dirname(__DIR__, 2) . '/includes/db.php';
|
|||||||
// ── Auto-create tables ────────────────────────────────────────────────────────
|
// ── Auto-create tables ────────────────────────────────────────────────────────
|
||||||
$pdo->exec("CREATE TABLE IF NOT EXISTS pf_lists (
|
$pdo->exec("CREATE TABLE IF NOT EXISTS pf_lists (
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255) NOT NULL DEFAULT 'Ma liste',
|
id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255) NOT NULL DEFAULT 'Ma liste',
|
||||||
|
color VARCHAR(20) DEFAULT NULL, list_type VARCHAR(50) DEFAULT NULL,
|
||||||
position INT NOT NULL DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
position INT NOT NULL DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
|
||||||
|
|
||||||
@@ -48,6 +49,8 @@ $pdo->exec("CREATE TABLE IF NOT EXISTS pf_item_category_rules (
|
|||||||
try { $pdo->exec("ALTER TABLE pf_grocery_items ADD COLUMN list_id INT NOT NULL DEFAULT 1 AFTER id"); } catch (\Exception $e) {}
|
try { $pdo->exec("ALTER TABLE pf_grocery_items ADD COLUMN list_id INT NOT NULL DEFAULT 1 AFTER id"); } catch (\Exception $e) {}
|
||||||
try { $pdo->exec("ALTER TABLE pf_grocery_items ADD COLUMN category_id INT DEFAULT NULL AFTER list_id"); } catch (\Exception $e) {}
|
try { $pdo->exec("ALTER TABLE pf_grocery_items ADD COLUMN category_id INT DEFAULT NULL AFTER list_id"); } catch (\Exception $e) {}
|
||||||
try { $pdo->exec("ALTER TABLE pf_grocery_items ADD KEY idx_items_list (list_id)"); } catch (\Exception $e) {}
|
try { $pdo->exec("ALTER TABLE pf_grocery_items ADD KEY idx_items_list (list_id)"); } catch (\Exception $e) {}
|
||||||
|
try { $pdo->exec("ALTER TABLE pf_lists ADD COLUMN color VARCHAR(20) DEFAULT NULL AFTER name"); } catch (\Exception $e) {}
|
||||||
|
try { $pdo->exec("ALTER TABLE pf_lists ADD COLUMN list_type VARCHAR(50) DEFAULT NULL AFTER color"); } catch (\Exception $e) {}
|
||||||
|
|
||||||
// ── Seed categories if empty ──────────────────────────────────────────────────
|
// ── Seed categories if empty ──────────────────────────────────────────────────
|
||||||
if ((int)$pdo->query("SELECT COUNT(*) FROM pf_list_categories")->fetchColumn() === 0) {
|
if ((int)$pdo->query("SELECT COUNT(*) FROM pf_list_categories")->fetchColumn() === 0) {
|
||||||
@@ -313,23 +316,32 @@ if ($action === 'set_category' && $method === 'POST') {
|
|||||||
if ($action === 'lists') {
|
if ($action === 'lists') {
|
||||||
if ($method === 'GET') {
|
if ($method === 'GET') {
|
||||||
$firstId = liste_ensure_default($pdo);
|
$firstId = liste_ensure_default($pdo);
|
||||||
$rows = $pdo->query("SELECT id, name, position FROM pf_lists ORDER BY position, id")->fetchAll(PDO::FETCH_ASSOC);
|
$rows = $pdo->query("SELECT id, name, color, list_type, position FROM pf_lists ORDER BY position, id")->fetchAll(PDO::FETCH_ASSOC);
|
||||||
echo json_encode(['lists' => $rows, 'default_id' => $firstId]);
|
echo json_encode(['lists' => $rows, 'default_id' => $firstId]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
if ($method === 'POST') {
|
if ($method === 'POST') {
|
||||||
$name = mb_substr(trim($body['name'] ?? ''), 0, 100);
|
$name = mb_substr(trim($body['name'] ?? ''), 0, 100);
|
||||||
|
$color = mb_substr(trim($body['color'] ?? ''), 0, 20) ?: null;
|
||||||
|
$list_type = mb_substr(trim($body['list_type'] ?? ''), 0, 50) ?: null;
|
||||||
if (!$name) { http_response_code(400); echo json_encode(['error' => 'name required']); exit; }
|
if (!$name) { http_response_code(400); echo json_encode(['error' => 'name required']); exit; }
|
||||||
$pos = (int)$pdo->query("SELECT COALESCE(MAX(position),0)+1 FROM pf_lists")->fetchColumn();
|
$pos = (int)$pdo->query("SELECT COALESCE(MAX(position),0)+1 FROM pf_lists")->fetchColumn();
|
||||||
$pdo->prepare("INSERT INTO pf_lists (name, position) VALUES (?,?)")->execute([$name, $pos]);
|
$pdo->prepare("INSERT INTO pf_lists (name, color, list_type, position) VALUES (?,?,?,?)")->execute([$name, $color, $list_type, $pos]);
|
||||||
echo json_encode(['id' => (int)$pdo->lastInsertId(), 'name' => $name, 'position' => $pos]);
|
echo json_encode(['id' => (int)$pdo->lastInsertId(), 'name' => $name, 'color' => $color, 'list_type' => $list_type, 'position' => $pos]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
if ($method === 'PUT') {
|
if ($method === 'PUT') {
|
||||||
$id = (int)($_GET['id'] ?? 0);
|
$id = (int)($_GET['id'] ?? 0);
|
||||||
$name = mb_substr(trim($body['name'] ?? ''), 0, 100);
|
$name = mb_substr(trim($body['name'] ?? ''), 0, 100);
|
||||||
|
$color = array_key_exists('color', $body) ? (mb_substr(trim($body['color'] ?? ''), 0, 20) ?: null) : false;
|
||||||
|
$list_type = array_key_exists('list_type', $body) ? (mb_substr(trim($body['list_type'] ?? ''), 0, 50) ?: null) : false;
|
||||||
if (!$id || !$name) { http_response_code(400); echo json_encode(['error' => 'invalid']); exit; }
|
if (!$id || !$name) { http_response_code(400); echo json_encode(['error' => 'invalid']); exit; }
|
||||||
$pdo->prepare("UPDATE pf_lists SET name=? WHERE id=?")->execute([$name, $id]);
|
$sets = ['name=?'];
|
||||||
|
$vals = [$name];
|
||||||
|
if ($color !== false) { $sets[] = 'color=?'; $vals[] = $color; }
|
||||||
|
if ($list_type !== false) { $sets[] = 'list_type=?'; $vals[] = $list_type; }
|
||||||
|
$vals[] = $id;
|
||||||
|
$pdo->prepare("UPDATE pf_lists SET " . implode(', ', $sets) . " WHERE id=?")->execute($vals);
|
||||||
echo json_encode(['ok' => true]); exit;
|
echo json_encode(['ok' => true]); exit;
|
||||||
}
|
}
|
||||||
if ($method === 'DELETE') {
|
if ($method === 'DELETE') {
|
||||||
|
|||||||
@@ -310,6 +310,135 @@
|
|||||||
}
|
}
|
||||||
.liste-empty-icon { font-size: 3rem; margin-bottom: .75rem; }
|
.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 ──────────────────────────────────────────────── */
|
/* ── Category section headers ──────────────────────────────────────────────── */
|
||||||
|
|
||||||
.liste-cat-section-header {
|
.liste-cat-section-header {
|
||||||
@@ -429,6 +558,13 @@
|
|||||||
[data-theme='dark'] .btn-tool:hover { background: #2a2a3e; }
|
[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 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-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 { border-color: #3a3a4e; }
|
||||||
[data-theme='dark'] .liste-cat-badge:hover { background: #313155; border-color: #4361ee; }
|
[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-section-count { background: #2a2a3e; color: #8892b0; }
|
||||||
|
|||||||
+169
-66
@@ -11,6 +11,35 @@ const state = {
|
|||||||
catMap: {},
|
catMap: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ── Constants ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const LIST_COLORS = [
|
||||||
|
{ id: 'none', hex: null },
|
||||||
|
{ id: 'blue', hex: '#3b82f6' },
|
||||||
|
{ id: 'red', hex: '#ef4444' },
|
||||||
|
{ id: 'amber', hex: '#f59e0b' },
|
||||||
|
{ id: 'purple', hex: '#8b5cf6' },
|
||||||
|
{ id: 'pink', hex: '#ec4899' },
|
||||||
|
{ id: 'emerald', hex: '#10b981' },
|
||||||
|
{ id: 'orange', hex: '#f97316' },
|
||||||
|
{ id: 'cyan', hex: '#06b6d4' },
|
||||||
|
{ id: 'indigo', hex: '#6366f1' },
|
||||||
|
{ id: 'teal', hex: '#14b8a6' },
|
||||||
|
{ id: 'gray', hex: '#6b7280' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const LIST_TYPES = [
|
||||||
|
{ value: '', label: '— Aucun —', emoji: '' },
|
||||||
|
{ value: 'courses', label: 'Courses', emoji: '🛒' },
|
||||||
|
{ value: 'todo', label: 'To-do', emoji: '✅' },
|
||||||
|
{ value: 'voyage', label: 'Voyage', emoji: '✈️' },
|
||||||
|
{ value: 'travail', label: 'Travail', emoji: '💼' },
|
||||||
|
{ value: 'maison', label: 'Maison', emoji: '🏠' },
|
||||||
|
{ value: 'sante', label: 'Santé', emoji: '💊' },
|
||||||
|
{ value: 'loisirs', label: 'Loisirs', emoji: '🎮' },
|
||||||
|
{ value: 'autre', label: 'Autre', emoji: '📋' },
|
||||||
|
];
|
||||||
|
|
||||||
// ── Utilities ──────────────────────────────────────────────────────────────────
|
// ── Utilities ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function esc(s) {
|
function esc(s) {
|
||||||
@@ -42,6 +71,116 @@ async function api(action, method = 'GET', body = null, params = {}) {
|
|||||||
return r.json();
|
return r.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── List modal ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
let activeModal = null;
|
||||||
|
|
||||||
|
function openListModal(listId = null) {
|
||||||
|
if (activeModal) { activeModal.remove(); activeModal = null; }
|
||||||
|
const list = listId ? state.lists.find(l => l.id === listId) : null;
|
||||||
|
const isNew = !listId;
|
||||||
|
|
||||||
|
const backdrop = document.createElement('div');
|
||||||
|
backdrop.className = 'liste-modal-backdrop';
|
||||||
|
backdrop.innerHTML = `
|
||||||
|
<div class="liste-modal" role="dialog" aria-modal="true">
|
||||||
|
<div class="liste-modal-header">
|
||||||
|
<h3>${isNew ? 'Nouvelle liste' : 'Modifier la liste'}</h3>
|
||||||
|
<button class="liste-modal-close" aria-label="Fermer">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="liste-modal-body">
|
||||||
|
<div class="liste-form-group">
|
||||||
|
<label class="liste-form-label">Nom</label>
|
||||||
|
<input class="liste-modal-name" type="text" maxlength="100"
|
||||||
|
value="${esc(list?.name ?? '')}" placeholder="Ma liste…" autocomplete="off">
|
||||||
|
</div>
|
||||||
|
<div class="liste-form-group">
|
||||||
|
<label class="liste-form-label">Couleur</label>
|
||||||
|
<div class="liste-color-picker">
|
||||||
|
${LIST_COLORS.map(c => {
|
||||||
|
const isActive = (c.hex === null && !list?.color) || (list?.color === c.hex);
|
||||||
|
const cls = 'liste-color-swatch' + (c.hex === null ? ' liste-color-swatch-none' : '') + (isActive ? ' active' : '');
|
||||||
|
const style = c.hex ? `style="--swatch:${c.hex}"` : '';
|
||||||
|
return `<button class="${cls}" data-color="${c.hex ?? ''}" ${style} title="${c.id}"></button>`;
|
||||||
|
}).join('')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="liste-form-group">
|
||||||
|
<label class="liste-form-label">Type de liste</label>
|
||||||
|
<select class="liste-modal-type">
|
||||||
|
${LIST_TYPES.map(t =>
|
||||||
|
`<option value="${esc(t.value)}" ${list?.list_type === t.value ? 'selected' : ''}>${t.emoji ? t.emoji + ' ' : ''}${esc(t.label)}</option>`
|
||||||
|
).join('')}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="liste-modal-footer">
|
||||||
|
${!isNew && state.lists.length > 1
|
||||||
|
? `<button class="btn-tool btn-tool-danger liste-modal-delete">Supprimer</button>`
|
||||||
|
: ''}
|
||||||
|
<button class="btn-tool liste-modal-cancel">Annuler</button>
|
||||||
|
<button class="btn-tool btn-tool-primary liste-modal-save">Enregistrer</button>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
document.body.appendChild(backdrop);
|
||||||
|
activeModal = backdrop;
|
||||||
|
|
||||||
|
const nameInput = backdrop.querySelector('.liste-modal-name');
|
||||||
|
nameInput.focus();
|
||||||
|
nameInput.setSelectionRange(nameInput.value.length, nameInput.value.length);
|
||||||
|
|
||||||
|
// Color swatches
|
||||||
|
backdrop.querySelectorAll('.liste-color-swatch').forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
backdrop.querySelectorAll('.liste-color-swatch').forEach(b => b.classList.remove('active'));
|
||||||
|
btn.classList.add('active');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const close = () => { backdrop.remove(); activeModal = null; };
|
||||||
|
backdrop.querySelector('.liste-modal-close').addEventListener('click', close);
|
||||||
|
backdrop.querySelector('.liste-modal-cancel').addEventListener('click', close);
|
||||||
|
backdrop.addEventListener('click', e => { if (e.target === backdrop) close(); });
|
||||||
|
|
||||||
|
backdrop.querySelector('.liste-modal-delete')?.addEventListener('click', () => { close(); deleteList(listId); });
|
||||||
|
backdrop.querySelector('.liste-modal-save').addEventListener('click', () => saveListModal(backdrop, listId));
|
||||||
|
nameInput.addEventListener('keydown', e => {
|
||||||
|
if (e.key === 'Enter') saveListModal(backdrop, listId);
|
||||||
|
if (e.key === 'Escape') close();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveListModal(backdrop, listId) {
|
||||||
|
const name = backdrop.querySelector('.liste-modal-name').value.trim();
|
||||||
|
if (!name) { backdrop.querySelector('.liste-modal-name').focus(); return; }
|
||||||
|
|
||||||
|
const activeSwatch = backdrop.querySelector('.liste-color-swatch.active');
|
||||||
|
const color = activeSwatch?.dataset.color || null;
|
||||||
|
const list_type = backdrop.querySelector('.liste-modal-type').value || null;
|
||||||
|
|
||||||
|
if (listId) {
|
||||||
|
const r = await api('lists', 'PUT', { name, color, list_type }, { id: listId });
|
||||||
|
if (r.ok) {
|
||||||
|
const list = state.lists.find(l => l.id === listId);
|
||||||
|
if (list) { list.name = name; list.color = color; list.list_type = list_type; }
|
||||||
|
backdrop.remove(); activeModal = null;
|
||||||
|
renderTabs();
|
||||||
|
toast('Liste mise à jour');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const r = await api('lists', 'POST', { name, color, list_type });
|
||||||
|
if (r.id) {
|
||||||
|
state.lists.push({ id: r.id, name, color: r.color, list_type: r.list_type, position: r.position });
|
||||||
|
state.currentListId = r.id;
|
||||||
|
backdrop.remove(); activeModal = null;
|
||||||
|
await loadItems();
|
||||||
|
renderTabs();
|
||||||
|
toast('Liste créée : ' + name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Categories ─────────────────────────────────────────────────────────────────
|
// ── Categories ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
async function loadCategories() {
|
async function loadCategories() {
|
||||||
@@ -104,49 +243,54 @@ function closeCategoryPicker() {
|
|||||||
|
|
||||||
// ── Tabs ───────────────────────────────────────────────────────────────────────
|
// ── Tabs ───────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function typeEmoji(list_type) {
|
||||||
|
return LIST_TYPES.find(t => t.value === list_type)?.emoji || '';
|
||||||
|
}
|
||||||
|
|
||||||
function renderTabs() {
|
function renderTabs() {
|
||||||
const container = document.getElementById('liste-tabs');
|
const container = document.getElementById('liste-tabs');
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
container.innerHTML = '';
|
container.innerHTML = '';
|
||||||
|
|
||||||
state.lists.forEach(list => {
|
state.lists.forEach(list => {
|
||||||
|
const isActive = list.id === state.currentListId;
|
||||||
const tab = document.createElement('div');
|
const tab = document.createElement('div');
|
||||||
tab.className = 'liste-tab' + (list.id === state.currentListId ? ' active' : '');
|
tab.className = 'liste-tab' + (isActive ? ' active' : '');
|
||||||
tab.dataset.id = list.id;
|
tab.dataset.id = list.id;
|
||||||
|
|
||||||
if (list.id === state.currentListId) {
|
// Apply custom color to entire tab
|
||||||
|
if (list.color) {
|
||||||
|
if (isActive) {
|
||||||
|
tab.style.cssText = `background:${list.color};border-color:${list.color};color:#fff`;
|
||||||
|
} else {
|
||||||
|
tab.style.cssText = `border-color:${list.color};color:${list.color}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const emoji = typeEmoji(list.list_type);
|
||||||
|
const prefix = emoji ? `<span class="liste-tab-type">${emoji}</span>` : '';
|
||||||
|
|
||||||
|
if (isActive) {
|
||||||
tab.innerHTML = `
|
tab.innerHTML = `
|
||||||
|
${prefix}
|
||||||
<span class="liste-tab-name">${esc(list.name)}</span>
|
<span class="liste-tab-name">${esc(list.name)}</span>
|
||||||
<button class="liste-tab-btn liste-tab-rename" title="${esc(T.rename_list)}" data-id="${list.id}">✏</button>
|
<button class="liste-tab-btn liste-tab-edit" title="Modifier" data-id="${list.id}">✏</button>
|
||||||
${state.lists.length > 1 ? `<button class="liste-tab-btn liste-tab-delete" title="Supprimer" data-id="${list.id}">×</button>` : ''}
|
|
||||||
`;
|
`;
|
||||||
} else {
|
} else {
|
||||||
tab.innerHTML = `<span class="liste-tab-name">${esc(list.name)}</span>`;
|
tab.innerHTML = `${prefix}<span class="liste-tab-name">${esc(list.name)}</span>`;
|
||||||
tab.addEventListener('click', () => switchList(list.id));
|
tab.addEventListener('click', () => switchList(list.id));
|
||||||
}
|
}
|
||||||
container.appendChild(tab);
|
container.appendChild(tab);
|
||||||
});
|
});
|
||||||
|
|
||||||
container.querySelectorAll('.liste-tab-rename').forEach(btn => {
|
container.querySelectorAll('.liste-tab-edit').forEach(btn => {
|
||||||
btn.addEventListener('click', e => { e.stopPropagation(); startRename(parseInt(btn.dataset.id)); });
|
btn.addEventListener('click', e => { e.stopPropagation(); openListModal(parseInt(btn.dataset.id)); });
|
||||||
});
|
|
||||||
container.querySelectorAll('.liste-tab-delete').forEach(btn => {
|
|
||||||
btn.addEventListener('click', e => { e.stopPropagation(); deleteList(parseInt(btn.dataset.id)); });
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function startRename(listId) {
|
// ── List management ─────────────────────────────────────────────────────────────
|
||||||
const list = state.lists.find(l => l.id === listId);
|
|
||||||
if (!list) return;
|
document.getElementById('btn-add-list')?.addEventListener('click', () => openListModal(null));
|
||||||
const name = prompt(T.new_name || 'Nouveau nom :', list.name);
|
|
||||||
if (name === null || name.trim() === '') return;
|
|
||||||
api('lists', 'PUT', { name: name.trim() }, { id: listId }).then(r => {
|
|
||||||
if (r.ok) {
|
|
||||||
list.name = name.trim();
|
|
||||||
renderTabs();
|
|
||||||
toast(T.list_renamed || 'Liste renommée');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function deleteList(listId) {
|
async function deleteList(listId) {
|
||||||
if (!confirm(T.confirm_delete_list || 'Supprimer cette liste et tous ses articles ?')) return;
|
if (!confirm(T.confirm_delete_list || 'Supprimer cette liste et tous ses articles ?')) return;
|
||||||
@@ -161,41 +305,6 @@ async function deleteList(listId) {
|
|||||||
toast(T.list_deleted || 'Liste supprimée');
|
toast(T.list_deleted || 'Liste supprimée');
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── List management ─────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
document.getElementById('btn-add-list')?.addEventListener('click', () => {
|
|
||||||
document.getElementById('liste-new-form')?.classList.remove('hidden');
|
|
||||||
document.getElementById('btn-add-list')?.classList.add('hidden');
|
|
||||||
document.getElementById('new-list-name')?.focus();
|
|
||||||
});
|
|
||||||
document.getElementById('btn-cancel-new-list')?.addEventListener('click', cancelNewList);
|
|
||||||
document.getElementById('btn-confirm-new-list')?.addEventListener('click', confirmNewList);
|
|
||||||
document.getElementById('new-list-name')?.addEventListener('keydown', e => {
|
|
||||||
if (e.key === 'Enter') confirmNewList();
|
|
||||||
if (e.key === 'Escape') cancelNewList();
|
|
||||||
});
|
|
||||||
|
|
||||||
function cancelNewList() {
|
|
||||||
document.getElementById('liste-new-form')?.classList.add('hidden');
|
|
||||||
document.getElementById('btn-add-list')?.classList.remove('hidden');
|
|
||||||
if (document.getElementById('new-list-name')) document.getElementById('new-list-name').value = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
async function confirmNewList() {
|
|
||||||
const input = document.getElementById('new-list-name');
|
|
||||||
const name = input?.value.trim();
|
|
||||||
if (!name) return;
|
|
||||||
const r = await api('lists', 'POST', { name });
|
|
||||||
if (r.id) {
|
|
||||||
state.lists.push({ id: r.id, name: r.name, position: r.position });
|
|
||||||
state.currentListId = r.id;
|
|
||||||
cancelNewList();
|
|
||||||
await loadItems();
|
|
||||||
renderTabs();
|
|
||||||
toast((T.list_created || 'Liste créée') + ' : ' + r.name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Switch list ─────────────────────────────────────────────────────────────────
|
// ── Switch list ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
async function switchList(listId) {
|
async function switchList(listId) {
|
||||||
@@ -207,10 +316,7 @@ async function switchList(listId) {
|
|||||||
// ── Items ───────────────────────────────────────────────────────────────────────
|
// ── Items ───────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
async function loadItems() {
|
async function loadItems() {
|
||||||
if (!state.currentListId) {
|
if (!state.currentListId) { renderItems(); return; }
|
||||||
renderItems();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const r = await api('items', 'GET', null, { list_id: state.currentListId });
|
const r = await api('items', 'GET', null, { list_id: state.currentListId });
|
||||||
state.items = r.items || [];
|
state.items = r.items || [];
|
||||||
renderItems();
|
renderItems();
|
||||||
@@ -236,15 +342,12 @@ function renderItems() {
|
|||||||
let html = '';
|
let html = '';
|
||||||
|
|
||||||
if (hasCategories && state.categories.length) {
|
if (hasCategories && state.categories.length) {
|
||||||
// Group pending items by category
|
|
||||||
const groups = new Map();
|
const groups = new Map();
|
||||||
pending.forEach(item => {
|
pending.forEach(item => {
|
||||||
const key = item.category_id ?? 0;
|
const key = item.category_id ?? 0;
|
||||||
if (!groups.has(key)) groups.set(key, []);
|
if (!groups.has(key)) groups.set(key, []);
|
||||||
groups.get(key).push(item);
|
groups.get(key).push(item);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Render in category order, uncategorized last
|
|
||||||
const catIds = state.categories.map(c => parseInt(c.id));
|
const catIds = state.categories.map(c => parseInt(c.id));
|
||||||
const orderedKeys = catIds.filter(id => groups.has(id));
|
const orderedKeys = catIds.filter(id => groups.has(id));
|
||||||
if (groups.has(0)) orderedKeys.push(0);
|
if (groups.has(0)) orderedKeys.push(0);
|
||||||
@@ -284,7 +387,7 @@ function rowHtml(item) {
|
|||||||
const checked = item.in_cart ? 'checked' : '';
|
const checked = item.in_cart ? 'checked' : '';
|
||||||
const cls = item.in_cart ? 'liste-row in-cart' : 'liste-row';
|
const cls = item.in_cart ? 'liste-row in-cart' : 'liste-row';
|
||||||
const cat = item.category_id ? state.catMap[item.category_id] : null;
|
const cat = item.category_id ? state.catMap[item.category_id] : null;
|
||||||
const badgeCls = 'liste-cat-badge' + (cat ? '' : ' liste-cat-badge-empty');
|
const badgeCls = 'liste-cat-badge' + (cat ? '' : ' liste-cat-badge-empty');
|
||||||
const badgeIcon = cat ? esc(cat.icon) : '🏷️';
|
const badgeIcon = cat ? esc(cat.icon) : '🏷️';
|
||||||
const badgeTip = cat ? esc(cat.name) : 'Non classé';
|
const badgeTip = cat ? esc(cat.name) : 'Non classé';
|
||||||
return `<div class="${cls}" data-id="${item.id}">
|
return `<div class="${cls}" data-id="${item.id}">
|
||||||
|
|||||||
Reference in New Issue
Block a user