Deploiement staging #5

Merged
perco merged 2 commits from deploiement-staging into main 2026-06-04 14:14:20 +02:00
7 changed files with 712 additions and 367 deletions
+1
View File
@@ -36,6 +36,7 @@ CREATE TABLE IF NOT EXISTS user_calendar_integrations (
calendar_url VARCHAR(1024) DEFAULT NULL, calendar_url VARCHAR(1024) DEFAULT NULL,
status VARCHAR(30) DEFAULT 'connected', status VARCHAR(30) DEFAULT 'connected',
last_sync_at DATETIME DEFAULT NULL, last_sync_at DATETIME DEFAULT NULL,
calendar_prefs_json TEXT DEFAULT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP, created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uq_user_provider (user_id, provider), UNIQUE KEY uq_user_provider (user_id, provider),
+61 -54
View File
@@ -1,18 +1,8 @@
-- HouseHub — Schéma MySQL -- HouseHub — Schéma MySQL (Modèle pour une base famille)
-- Créer la base : CREATE DATABASE househub CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
SET NAMES utf8mb4; SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0; SET FOREIGN_KEY_CHECKS = 0;
-- ─── Utilisateurs ───────────────────────────────────────────────────────────── -- ─── Configuration du foyer ───────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS pf_users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(100) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
display_name VARCHAR(100) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ─── Familles ─────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS pf_foyer_settings ( CREATE TABLE IF NOT EXISTS pf_foyer_settings (
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
currency VARCHAR(10) NOT NULL DEFAULT '', currency VARCHAR(10) NOT NULL DEFAULT '',
@@ -20,17 +10,25 @@ CREATE TABLE IF NOT EXISTS pf_foyer_settings (
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Initialisation de la ligne unique par défaut
INSERT IGNORE INTO pf_foyer_settings (id, currency, zone_scolaire) VALUES (1, '', 'C'); INSERT IGNORE INTO pf_foyer_settings (id, currency, zone_scolaire) VALUES (1, '', 'C');
-- ─── Utilisateurs (Legacy) ────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS pf_users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(100) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
display_name VARCHAR(100) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ─── Personnes ──────────────────────────────────────────────────────────────── -- ─── Personnes ────────────────────────────────────────────────────────────────
CREATE TABLE `pf_people` ( CREATE TABLE `pf_people` (
`id` int(11) NOT NULL AUTO_INCREMENT, `id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(50) NOT NULL, `name` varchar(100) NOT NULL,
`user_id` int(11) DEFAULT NULL, `user_id` int(11) DEFAULT NULL,
`role` varchar(50) DEFAULT NULL, `role` varchar(50) DEFAULT NULL,
`color` varchar(7) DEFAULT '#0891b2',
PRIMARY KEY (`id`) PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ─── Calendrier familial ────────────────────────────────────────────────────── -- ─── Calendrier familial ──────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS pf_events ( CREATE TABLE IF NOT EXISTS pf_events (
@@ -117,7 +115,8 @@ CREATE TABLE IF NOT EXISTS pf_expenses (
CREATE TABLE IF NOT EXISTS pf_alloc_categories ( CREATE TABLE IF NOT EXISTS pf_alloc_categories (
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL, name VARCHAR(100) NOT NULL,
target DECIMAL(10,2) DEFAULT 0, target DECIMAL(10,2) DEFAULT 0.00,
transfer_dest VARCHAR(50) DEFAULT NULL,
holiday_id INT DEFAULT NULL, holiday_id INT DEFAULT NULL,
sort_order INT DEFAULT 0 sort_order INT DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
@@ -126,9 +125,10 @@ CREATE TABLE IF NOT EXISTS pf_alloc_values (
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
month_date VARCHAR(10) NOT NULL, month_date VARCHAR(10) NOT NULL,
cat_id INT NOT NULL, cat_id INT NOT NULL,
amount_alex DECIMAL(10,2) DEFAULT 0, person_id INT NOT NULL,
amount_laia DECIMAL(10,2) DEFAULT 0, amount DECIMAL(10,2) DEFAULT 0.00,
UNIQUE KEY uq_alloc (month_date, cat_id) UNIQUE KEY uq_alloc_person (month_date, cat_id, person_id),
FOREIGN KEY (person_id) REFERENCES pf_people(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS pf_savings ( CREATE TABLE IF NOT EXISTS pf_savings (
@@ -161,9 +161,22 @@ CREATE TABLE IF NOT EXISTS pf_salary_config (
CREATE TABLE IF NOT EXISTS pf_import_rules ( CREATE TABLE IF NOT EXISTS pf_import_rules (
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
keyword VARCHAR(255) NOT NULL UNIQUE, keyword VARCHAR(255) NOT NULL UNIQUE,
category VARCHAR(100) NOT NULL category VARCHAR(100) NOT NULL,
budget_item_id INT(11) NULL DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS pf_advances (
id int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
advance_date date NOT NULL,
payer varchar(50) NOT NULL,
description varchar(255) NOT NULL,
amount decimal(10,2) DEFAULT 0.00,
from_savings tinyint(1) DEFAULT 0,
is_resolved tinyint(1) DEFAULT 0,
created_at datetime DEFAULT current_timestamp()
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ─── Notes / Memo ─────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS pf_notes ( CREATE TABLE IF NOT EXISTS pf_notes (
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
note_type VARCHAR(100) NOT NULL, note_type VARCHAR(100) NOT NULL,
@@ -172,6 +185,29 @@ CREATE TABLE IF NOT EXISTS pf_notes (
UNIQUE KEY uq_note (note_type, reference_id) UNIQUE KEY uq_note (note_type, reference_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS pf_memo_notes (
id INT AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(255) NOT NULL,
content LONGTEXT DEFAULT '',
tags VARCHAR(1000) DEFAULT '',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FULLTEXT KEY ft_notes (title, content, tags)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS pf_memo_attachments (
id INT AUTO_INCREMENT PRIMARY KEY,
note_id INT NOT NULL,
type ENUM('image','file','url') NOT NULL DEFAULT 'file',
filename VARCHAR(255) DEFAULT NULL,
original_name VARCHAR(255) DEFAULT NULL,
url TEXT DEFAULT NULL,
label VARCHAR(255) DEFAULT NULL,
size INT DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (note_id) REFERENCES pf_memo_notes(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ─── Voyages / Holidays ─────────────────────────────────────────────────────── -- ─── Voyages / Holidays ───────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS pf_holidays ( CREATE TABLE IF NOT EXISTS pf_holidays (
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
@@ -239,14 +275,6 @@ CREATE TABLE IF NOT EXISTS pf_gifts (
amount DECIMAL(8,2) DEFAULT 0 amount DECIMAL(8,2) DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
SET FOREIGN_KEY_CHECKS = 1;
-- ─── Données initiales ────────────────────────────────────────────────────────
-- Utilisateur admin (mot de passe : changeme → à modifier !)
INSERT IGNORE INTO pf_users (id, username, password_hash, display_name)
VALUES (1, 'admin', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'Admin');
-- ─── Garage Manager ─────────────────────────────────────────────────────────── -- ─── Garage Manager ───────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS pf_vehicles ( CREATE TABLE IF NOT EXISTS pf_vehicles (
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
@@ -321,30 +349,6 @@ CREATE TABLE IF NOT EXISTS pf_garage_documents (
FOREIGN KEY (maintenance_id) REFERENCES pf_maintenances(id) ON DELETE CASCADE FOREIGN KEY (maintenance_id) REFERENCES pf_maintenances(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ─── Notes / Memo ─────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS pf_memo_notes (
id INT AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(255) NOT NULL,
content LONGTEXT DEFAULT '',
tags VARCHAR(1000) DEFAULT '',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FULLTEXT KEY ft_notes (title, content, tags)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS pf_memo_attachments (
id INT AUTO_INCREMENT PRIMARY KEY,
note_id INT NOT NULL,
type ENUM('image','file','url') NOT NULL DEFAULT 'file',
filename VARCHAR(255) DEFAULT NULL,
original_name VARCHAR(255) DEFAULT NULL,
url TEXT DEFAULT NULL,
label VARCHAR(255) DEFAULT NULL,
size INT DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (note_id) REFERENCES pf_memo_notes(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ─── Todo ───────────────────────────────────────────────────────────────────── -- ─── Todo ─────────────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS pf_todo_lists ( CREATE TABLE IF NOT EXISTS pf_todo_lists (
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
@@ -372,7 +376,7 @@ CREATE TABLE IF NOT EXISTS pf_todos (
FOREIGN KEY (list_id) REFERENCES pf_todo_lists(id) ON DELETE SET NULL FOREIGN KEY (list_id) REFERENCES pf_todo_lists(id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ─── Module Liste (multi-listes) ────────────────────────────────────────────── -- ─── Module Liste (Courses partagées) ─────────────────────────────────────────
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',
@@ -382,7 +386,7 @@ CREATE TABLE IF NOT EXISTS pf_lists (
created_at DATETIME DEFAULT CURRENT_TIMESTAMP created_at DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT INTO pf_lists (id, name, position) VALUES (1, 'Ma liste', 0); INSERT IGNORE INTO pf_lists (id, name, position) VALUES (1, 'Ma liste', 0);
CREATE TABLE IF NOT EXISTS pf_grocery_items ( CREATE TABLE IF NOT EXISTS pf_grocery_items (
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
@@ -447,6 +451,7 @@ CREATE TABLE IF NOT EXISTS pf_calendar_event_links (
external_uid VARCHAR(255) NOT NULL, external_uid VARCHAR(255) NOT NULL,
external_etag VARCHAR(255) DEFAULT NULL, external_etag VARCHAR(255) DEFAULT NULL,
calendar_url VARCHAR(1024) DEFAULT NULL, calendar_url VARCHAR(1024) DEFAULT NULL,
external_href VARCHAR(2048) DEFAULT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP, created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uq_calendar_event (calendar_event_id), UNIQUE KEY uq_calendar_event (calendar_event_id),
@@ -498,3 +503,5 @@ CREATE TABLE IF NOT EXISTS pf_planka_config (
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id) PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
SET FOREIGN_KEY_CHECKS = 1;
+153 -102
View File
@@ -1,9 +1,26 @@
<?php <?php
// Script de migration : Refonte globale Multi-Tenant (pf_people & Budget) // Script de migration : Refonte globale Multi-Tenant & Synchronisation des schémas
require_once __DIR__ . '/includes/meta_db.php'; require_once __DIR__ . '/includes/meta_db.php';
echo "<h1>🚀 Début de la migration Globale ...</h1>"; echo "<h1>🚀 Début de la migration Globale ...</h1>";
// ==========================================
// 0. MISE À JOUR DE LA META DB
// ==========================================
echo "<h3>Mise à jour Meta DB (househub_meta)</h3><ul>";
try {
$meta_pdo->exec("ALTER TABLE user_calendar_integrations ADD COLUMN calendar_prefs_json TEXT DEFAULT NULL");
echo "<li><span style='color:green'>Colonne 'calendar_prefs_json' ajoutée à user_calendar_integrations.</span></li>";
} catch (\PDOException $e) {
if ($e->getCode() == '42S21' || strpos($e->getMessage(), '1060') !== false) {
echo "<li><span style='color:gray'>Colonne 'calendar_prefs_json' déjà présente.</span></li>";
} else { throw $e; }
}
echo "</ul>";
// ==========================================
// MIGRATIONS PAR FAMILLE
// ==========================================
$stmt = $meta_pdo->query("SELECT id, name, db_name FROM families WHERE db_name != ''"); $stmt = $meta_pdo->query("SELECT id, name, db_name FROM families WHERE db_name != ''");
$families = $stmt->fetchAll(); $families = $stmt->fetchAll();
@@ -16,152 +33,186 @@ foreach ($families as $f) {
$family_id = $f['id']; $family_id = $f['id'];
echo "<h3>Mise à jour de <strong>{$f['name']}</strong> ($db_name)</h3><ul>"; echo "<h3>Mise à jour de <strong>{$f['name']}</strong> ($db_name)</h3><ul>";
// 🟢 SÉCURITÉ : On vide les variables à chaque famille pour éviter les fuites (le bug f2 vers f1)
$p1_name = null; $p1_name = null;
$p2_name = null; $p2_name = null;
$parents = [];
try { try {
$fam_pdo = new PDO("mysql:host=$host;dbname=$db_name;charset=utf8mb4", $user, $pass, [ $fam_pdo = new PDO("mysql:host=$host;dbname=$db_name;charset=utf8mb4", $user, $pass, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
]); ]);
// ========================================== // 1. Table pf_foyer_settings
// 1. GESTION DE PF_PEOPLE (user_id, role, color) $fam_pdo->exec("CREATE TABLE IF NOT EXISTS pf_foyer_settings (
// ========================================== id INT AUTO_INCREMENT PRIMARY KEY,
echo "<li><strong>Table pf_people :</strong> "; currency VARCHAR(10) NOT NULL DEFAULT '€',
zone_scolaire VARCHAR(5) NOT NULL DEFAULT 'C',
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;");
$fam_pdo->exec("INSERT IGNORE INTO pf_foyer_settings (id, currency, zone_scolaire) VALUES (1, '€', 'C');");
echo "<li><span style='color:green'>Table pf_foyer_settings vérifiée/créée.</span></li>";
// user_id // 2. Colonne external_href pour le Calendrier iOS
try { try {
$fam_pdo->exec("ALTER TABLE pf_people ADD COLUMN user_id INT NULL DEFAULT NULL"); $fam_pdo->exec("ALTER TABLE pf_calendar_event_links ADD COLUMN external_href VARCHAR(2048) DEFAULT NULL");
echo "<span style='color:green'>user_id OK</span> - "; echo "<li><span style='color:green'>Colonne 'external_href' ajoutée à pf_calendar_event_links.</span></li>";
} catch (\PDOException $e) { } catch (\PDOException $e) {
if ($e->getCode() == '42S21' || strpos($e->getMessage(), '1060') !== false) { if ($e->getCode() != '42S21' && strpos($e->getMessage(), '1060') === false) throw $e;
echo "<span style='color:gray'>user_id déjà présent</span> - ";
} else { throw $e; }
} }
// 🟢 CORRECTION DE L'AUTO-MAPPING : Si pf_people est vide, on la remplit ! // 3. Colonne budget_item_id pour les règles d'import
try {
$fam_pdo->exec("ALTER TABLE pf_import_rules ADD COLUMN budget_item_id INT(11) DEFAULT NULL");
echo "<li><span style='color:green'>Colonne 'budget_item_id' ajoutée à pf_import_rules.</span></li>";
} catch (\PDOException $e) {
if ($e->getCode() != '42S21' && strpos($e->getMessage(), '1060') === false) throw $e;
}
// 4. Uniformisation des VARCHAR de dates pour le budget (YYYY-MM-01 = 10 chars)
$fam_pdo->exec("ALTER TABLE pf_expenses MODIFY gestion_month VARCHAR(10) NOT NULL");
$fam_pdo->exec("ALTER TABLE pf_alloc_values MODIFY month_date VARCHAR(10) NOT NULL");
$fam_pdo->exec("ALTER TABLE pf_savings MODIFY month_date VARCHAR(10) NOT NULL");
echo "<li><span style='color:green'>Formats de dates (VARCHAR 10) uniformisés pour le budget.</span></li>";
// 5. GESTION DE PF_PEOPLE (user_id, role, color)
echo "<li><strong>Mise à jour pf_people :</strong> ";
try { $fam_pdo->exec("ALTER TABLE pf_people ADD COLUMN user_id INT NULL DEFAULT NULL"); } catch (\Exception $e) {}
try { $fam_pdo->exec("ALTER TABLE pf_people ADD COLUMN role VARCHAR(50) DEFAULT NULL"); } catch (\Exception $e) {}
try { $fam_pdo->exec("ALTER TABLE pf_people ADD COLUMN color VARCHAR(7) DEFAULT '#0891b2'"); } catch (\Exception $e) {}
$stmtUsers = $meta_pdo->prepare("SELECT id, username, display_name FROM users WHERE family_id = ? ORDER BY id ASC"); $stmtUsers = $meta_pdo->prepare("SELECT id, username, display_name FROM users WHERE family_id = ? ORDER BY id ASC");
$stmtUsers->execute([$family_id]); $stmtUsers->execute([$family_id]);
$users = $stmtUsers->fetchAll(); $users = $stmtUsers->fetchAll();
$mappedCount = 0;
foreach ($users as $u) { foreach ($users as $u) {
// On vérifie si l'utilisateur existe déjà dans pf_people (via user_id ou son nom)
$stmtCheck = $fam_pdo->prepare("SELECT id FROM pf_people WHERE user_id = ? OR LOWER(name) = LOWER(?)"); $stmtCheck = $fam_pdo->prepare("SELECT id FROM pf_people WHERE user_id = ? OR LOWER(name) = LOWER(?)");
$stmtCheck->execute([$u['id'], $u['username']]); $stmtCheck->execute([$u['id'], $u['username']]);
$exists = $stmtCheck->fetchColumn(); $exists = $stmtCheck->fetchColumn();
if ($exists) { if ($exists) {
// S'il existe, on s'assure que son user_id est bien renseigné $fam_pdo->prepare("UPDATE pf_people SET user_id = ? WHERE id = ?")->execute([$u['id'], $exists]);
$stmtUpdate = $fam_pdo->prepare("UPDATE pf_people SET user_id = ? WHERE id = ?");
$stmtUpdate->execute([$u['id'], $exists]);
$mappedCount += $stmtUpdate->rowCount();
} else { } else {
// S'il n'existe pas du tout (base f1 vide), on le crée ! $fam_pdo->prepare("INSERT INTO pf_people (name, user_id) VALUES (?, ?)")->execute([$u['display_name'] ?: $u['username'], $u['id']]);
$stmtInsert = $fam_pdo->prepare("INSERT INTO pf_people (name, user_id) VALUES (?, ?)");
$stmtInsert->execute([$u['display_name'] ?: $u['username'], $u['id']]);
$mappedCount++;
} }
} }
echo "<span style='color:blue'>$mappedCount profils liés ou créés</span> - "; $fam_pdo->exec("UPDATE pf_people SET role = 'parent' WHERE user_id IS NOT NULL AND role IS NULL;");
$fam_pdo->exec("UPDATE pf_people SET role = 'nounou' WHERE LOWER(name) = 'carole';");
// role $pIds = $fam_pdo->query("SELECT id FROM pf_people WHERE role = 'parent' ORDER BY id ASC")->fetchAll(PDO::FETCH_COLUMN);
try { if (isset($pIds[0])) $fam_pdo->exec("UPDATE pf_people SET color = '#0891b2' WHERE id = " . (int)$pIds[0] . " AND color IS NULL");
$fam_pdo->exec("ALTER TABLE pf_people ADD COLUMN role VARCHAR(50) DEFAULT NULL"); if (isset($pIds[1])) $fam_pdo->exec("UPDATE pf_people SET color = '#f59e0b' WHERE id = " . (int)$pIds[1] . " AND color IS NULL");
echo "<span style='color:green'>role OK</span> - "; echo "<span style='color:blue'>OK</span></li>";
} catch (\PDOException $e) {
if ($e->getCode() == '42S21' || strpos($e->getMessage(), '1060') !== false) {
echo "<span style='color:gray'>role déjà présent</span> - ";
} else { throw $e; }
}
// color
try {
$fam_pdo->exec("ALTER TABLE pf_people ADD COLUMN color VARCHAR(7) DEFAULT '#0891b2'");
echo "<span style='color:green'>colonne 'color' ajoutée</span> - ";
} catch (\PDOException $e) {
if ($e->getCode() == '42S21' || strpos($e->getMessage(), '1060') !== false) {
echo "<span style='color:gray'>colonne 'color' déjà présente</span> - ";
} else { throw $e; }
}
// Configuration des valeurs par défaut et des rôles
$fam_pdo->exec("
UPDATE pf_people SET role = 'parent' WHERE user_id IS NOT NULL;
UPDATE pf_people SET role = 'nounou' WHERE LOWER(name) = 'carole';
");
// On donne des couleurs distinctes par défaut aux deux parents (P1 = Cyan, P2 = Orange)
$stmtP = $fam_pdo->query("SELECT id FROM pf_people WHERE role = 'parent' ORDER BY id ASC");
$pIds = $stmtP->fetchAll(PDO::FETCH_COLUMN);
if (isset($pIds[0])) {
$fam_pdo->exec("UPDATE pf_people SET color = '#0891b2' WHERE id = " . (int)$pIds[0]);
}
if (isset($pIds[1])) {
$fam_pdo->exec("UPDATE pf_people SET color = '#f59e0b' WHERE id = " . (int)$pIds[1]);
}
echo "<span style='color:blue'>Rôles et Couleurs initialisés.</span></li>";
// ========================================== // ==========================================
// 2. GESTION DU BUDGET (pf_alloc_values) // 6. REFONTE RELATIONNELLE : pf_alloc_values (Colonnes -> Lignes)
// ========================================== // ==========================================
echo "<li><strong>Table pf_alloc_values :</strong> "; echo "<li><strong>Table pf_alloc_values (Normalisation) :</strong> ";
$checkAlex = $fam_pdo->query("SHOW COLUMNS FROM pf_alloc_values LIKE 'amount_alex'")->rowCount(); // Vérifier si la table est déjà convertie
if ($checkAlex > 0) { $checkNewFormat = $fam_pdo->query("SHOW COLUMNS FROM pf_alloc_values LIKE 'person_id'")->rowCount();
$fam_pdo->exec("ALTER TABLE pf_alloc_values CHANGE amount_alex amount_p1 FLOAT DEFAULT 0");
echo "<span style='color:green'>amount_alex -> amount_p1</span> - "; if ($checkNewFormat > 0) {
echo "<span style='color:gray'>Déjà convertie au format relationnel.</span></li>";
} else { } else {
echo "<span style='color:gray'>amount_p1 OK</span> - "; // A. Récupérer l'ordre des parents réels pour faire le mapping d'index
$stmtParents = $fam_pdo->query("SELECT id, name FROM pf_people WHERE role = 'parent' ORDER BY id ASC");
$orderedParents = $stmtParents->fetchAll();
// B. Sauvegarder les anciennes données à migrer
$oldAllocations = $fam_pdo->query("SELECT * FROM pf_alloc_values")->fetchAll(PDO::FETCH_ASSOC);
// C. Supprimer l'ancienne table
$fam_pdo->exec("DROP TABLE IF EXISTS pf_alloc_values");
// D. Créer la nouvelle table normalisée
$fam_pdo->exec("CREATE TABLE pf_alloc_values (
id INT AUTO_INCREMENT PRIMARY KEY,
month_date VARCHAR(10) NOT NULL,
cat_id INT NOT NULL,
person_id INT NOT NULL,
amount DECIMAL(10,2) DEFAULT 0.00,
UNIQUE KEY uq_alloc_person (month_date, cat_id, person_id),
FOREIGN KEY (person_id) REFERENCES pf_people(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;");
// E. Migration des données
$stmtInsert = $fam_pdo->prepare("INSERT INTO pf_alloc_values (month_date, cat_id, person_id, amount) VALUES (?, ?, ?, ?)");
$migratedRows = 0;
foreach ($oldAllocations as $oldRow) {
// Mapping des anciennes colonnes potentielles vers les index de parents
$possibleColumns = [
0 => ['amount_p1', 'amount_alex'],
1 => ['amount_p2', 'amount_laia'],
2 => ['amount_p3'],
3 => ['amount_p4']
];
foreach ($possibleColumns as $parentIndex => $colNames) {
if (!isset($orderedParents[$parentIndex])) continue;
$targetParentId = $orderedParents[$parentIndex]['id'];
foreach ($colNames as $col) {
if (isset($oldRow[$col]) && (float)$oldRow[$col] > 0) {
$stmtInsert->execute([
$oldRow['month_date'],
$oldRow['cat_id'],
$targetParentId,
(float)$oldRow[$col]
]);
$migratedRows++;
break; // Passer à l'index parent suivant dès qu'on a trouvé une valeur
}
}
}
}
echo "<span style='color:green'>Succès ! Nouvelle table créée, $migratedRows lignes migrées.</span></li>";
} }
$checkLaia = $fam_pdo->query("SHOW COLUMNS FROM pf_alloc_values LIKE 'amount_laia'")->rowCount(); // 7. GESTION DU BUDGET (pf_alloc_categories & pf_salary_config)
if ($checkLaia > 0) { $fam_pdo->exec("UPDATE pf_alloc_categories SET name = 'Eco P1' WHERE name LIKE 'Eco Alex%'");
$fam_pdo->exec("ALTER TABLE pf_alloc_values CHANGE amount_laia amount_p2 FLOAT DEFAULT 0"); $fam_pdo->exec("UPDATE pf_alloc_categories SET name = 'Eco P2' WHERE name LIKE 'Eco Laia%'");
echo "<span style='color:green'>amount_laia -> amount_p2</span></li>";
} else {
echo "<span style='color:gray'>amount_p2 OK</span></li>";
}
// ==========================================
// 3. GESTION DU BUDGET (pf_alloc_categories)
// ==========================================
echo "<li><strong>Table pf_alloc_categories :</strong> ";
$stmtCats1 = $fam_pdo->exec("UPDATE pf_alloc_categories SET name = 'Eco P1' WHERE name LIKE 'Eco Alex%'");
$stmtCats2 = $fam_pdo->exec("UPDATE pf_alloc_categories SET name = 'Eco P2' WHERE name LIKE 'Eco Laia%'");
echo "<span style='color:green'>Catégories 'Eco' génériques mises à jour (" . ($stmtCats1 + $stmtCats2) . " lignes).</span></li>";
// ==========================================
// 4. GESTION DU BUDGET (pf_salary_config)
// ==========================================
echo "<li><strong>Table pf_salary_config :</strong> ";
// On récupère les vrais prénoms fraîchement créés/mappés
$stmtParents = $fam_pdo->query("SELECT name FROM pf_people WHERE role = 'parent' ORDER BY id ASC"); $stmtParents = $fam_pdo->query("SELECT name FROM pf_people WHERE role = 'parent' ORDER BY id ASC");
$parents = $stmtParents->fetchAll(); $parents = $stmtParents->fetchAll();
if (count($parents) >= 2) { if (count($parents) >= 2) {
$p1_name = $parents[0]['name']; $fam_pdo->prepare("UPDATE pf_salary_config SET person = ? WHERE person = 'Alex'")->execute([$parents[0]['name']]);
$p2_name = $parents[1]['name']; $fam_pdo->prepare("UPDATE pf_salary_config SET person = ? WHERE person = 'Laia'")->execute([$parents[1]['name']]);
$stmtSal1 = $fam_pdo->prepare("UPDATE pf_salary_config SET person = ? WHERE person = 'Alex'");
$stmtSal1->execute([$p1_name]);
$stmtSal2 = $fam_pdo->prepare("UPDATE pf_salary_config SET person = ? WHERE person = 'Laia'");
$stmtSal2->execute([$p2_name]);
echo "<span style='color:green'>Salaires mappés vers $p1_name et $p2_name.</span></li>";
} else {
echo "<span style='color:orange'>Pas assez de parents trouvés pour mapper les salaires.</span></li>";
} }
} catch (\PDOException $e) { } catch (\PDOException $e) {
echo "<li style='color:red'>❌ Erreur : " . $e->getMessage() . "</li>"; echo "<li style='color:red'>❌ Erreur : " . $e->getMessage() . "</li>";
} }
echo "</ul>"; echo "</ul>";
// ==========================================
// 8. SEPARATION CIBLE BUDGET / DESTINATION VIREMENT
// ==========================================
echo "<li><strong>Table pf_alloc_categories (Fix Collision) :</strong> ";
try {
$fam_pdo->exec("ALTER TABLE pf_alloc_categories ADD COLUMN transfer_dest VARCHAR(50) DEFAULT NULL AFTER target");
echo "<span style='color:green'>Succès ! Colonne 'transfer_dest' ajoutée pour préserver vos objectifs chiffrés.</span></li>";
} catch (\PDOException $e) {
echo "<span style='color:gray'>La colonne transfer_dest existe déjà.</span></li>";
}
// ==========================================
// 9. TRANSFERT DES DONNÉES (Cible -> Destination)
// ==========================================
echo "<li><strong>Table pf_alloc_categories (Récupération des données) :</strong> ";
try {
// Déplacer les textes (qui commencent par "vers") dans transfer_dest
$updated = $fam_pdo->exec("UPDATE pf_alloc_categories SET transfer_dest = target, target = '0' WHERE target LIKE 'vers %'");
echo "<span style='color:green'>$updated destinations récupérées et déplacées proprement.</span></li>";
// Remettre la colonne target en format numérique maintenant qu'elle est propre
$fam_pdo->exec("ALTER TABLE pf_alloc_categories MODIFY target DECIMAL(10,2) DEFAULT 0.00");
echo "<li><span style='color:green'>Colonne 'target' re-sécurisée en format monétaire DECIMAL(10,2).</span></li>";
} catch (\PDOException $e) {
echo "<span style='color:red'>Erreur : " . $e->getMessage() . "</span></li>";
}
} }
echo "<h2>🎉 Migration terminée avec succès !</h2>"; echo "<h2>🎉 Migration terminée avec succès !</h2>";
?> ?>
+59 -47
View File
@@ -62,28 +62,37 @@ if ($action === 'update_salary_config') {
if ($action === 'update_allocation') { if ($action === 'update_allocation') {
header('Content-Type: application/json'); header('Content-Type: application/json');
$date = $_POST['month_date']; $date = $_POST['month_date'];
$catId = $_POST['cat_id']; $catId = (int)$_POST['cat_id'];
$person = $_POST['person']; // 'amount_alex' ou 'amount_laia' $personId = (int)$_POST['person_id']; // 🟢 Reçoit l'ID de la personne directement !
$value = floatval($_POST['value']); $value = floatval($_POST['value']);
$stmt = $pdo->prepare("INSERT INTO pf_alloc_values (month_date, cat_id, $person) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE $person = VALUES($person)"); if ($catId <= 0 || $personId <= 0) {
$stmt->execute([$date, $catId, $value]); echo json_encode(['success' => false, 'error' => 'Données invalides.']);
exit;
}
$stmt = $pdo->prepare("INSERT INTO pf_alloc_values (month_date, cat_id, person_id, amount)
VALUES (?, ?, ?, ?)
ON DUPLICATE KEY UPDATE amount = VALUES(amount)");
$stmt->execute([$date, $catId, $personId, $value]);
echo json_encode(['success' => true]); echo json_encode(['success' => true]);
exit; exit;
} }
// 3. GESTION DES CATEGORIES (Ajout) // 3. GESTION DES CATEGORIES (Ajout)
if ($action === 'add_category') { if ($action === 'add_category') {
try {
$name = trim($_POST['name']); $name = trim($_POST['name']);
$target = trim($_POST['target']); $transfer_dest = trim($_POST['transfer_dest'] ?? '');
$target = floatval($_POST['target'] ?? 0);
$holiday_id = !empty($_POST['holiday_id']) ? (int)$_POST['holiday_id'] : null; $holiday_id = !empty($_POST['holiday_id']) ? (int)$_POST['holiday_id'] : null;
if (!empty($name)) { if (!empty($name)) {
$stmt = $pdo->prepare("INSERT INTO pf_alloc_categories (name, target, holiday_id) VALUES (?, ?, ?)"); $stmt = $pdo->prepare("INSERT INTO pf_alloc_categories (name, target, transfer_dest, holiday_id) VALUES (?, ?, ?, ?)");
$stmt->execute([$name, $target, $holiday_id]); $stmt->execute([$name, $target, $transfer_dest, $holiday_id]);
} }
// --- NOUVEAU : Réponse AJAX ---
if (isset($_POST['ajax'])) { if (isset($_POST['ajax'])) {
header('Content-Type: application/json'); header('Content-Type: application/json');
echo json_encode(['success' => true]); echo json_encode(['success' => true]);
@@ -91,21 +100,30 @@ if ($action === 'add_category') {
} }
header("Location: " . $_SERVER['HTTP_REFERER']); header("Location: " . $_SERVER['HTTP_REFERER']);
exit; exit;
} catch (Exception $e) {
if (isset($_POST['ajax'])) {
header('Content-Type: application/json');
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
exit;
}
die($e->getMessage());
}
} }
// 4. MODIFICATION D'UNE CATEGORIE // 4. MODIFICATION D'UNE CATEGORIE
if ($action === 'update_category') { if ($action === 'update_category') {
try {
$id = (int)$_POST['cat_id']; $id = (int)$_POST['cat_id'];
$name = trim($_POST['name']); $name = trim($_POST['name']);
$target = trim($_POST['target']); $transfer_dest = trim($_POST['transfer_dest'] ?? '');
$target = floatval($_POST['target'] ?? 0);
$holiday_id = !empty($_POST['holiday_id']) ? (int)$_POST['holiday_id'] : null; $holiday_id = !empty($_POST['holiday_id']) ? (int)$_POST['holiday_id'] : null;
if ($id > 0 && !empty($name)) { if ($id > 0 && !empty($name)) {
$stmt = $pdo->prepare("UPDATE pf_alloc_categories SET name = ?, target = ?, holiday_id = ? WHERE id = ?"); $stmt = $pdo->prepare("UPDATE pf_alloc_categories SET name = ?, target = ?, transfer_dest = ?, holiday_id = ? WHERE id = ?");
$stmt->execute([$name, $target, $holiday_id, $id]); $stmt->execute([$name, $target, $transfer_dest, $holiday_id, $id]);
} }
// --- NOUVEAU : Réponse AJAX ---
if (isset($_POST['ajax'])) { if (isset($_POST['ajax'])) {
header('Content-Type: application/json'); header('Content-Type: application/json');
echo json_encode(['success' => true]); echo json_encode(['success' => true]);
@@ -113,6 +131,14 @@ if ($action === 'update_category') {
} }
header("Location: " . $_SERVER['HTTP_REFERER']); header("Location: " . $_SERVER['HTTP_REFERER']);
exit; exit;
} catch (Exception $e) {
if (isset($_POST['ajax'])) {
header('Content-Type: application/json');
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
exit;
}
die($e->getMessage());
}
} }
// 5. SUPPRESSION CATEGORIE // 5. SUPPRESSION CATEGORIE
@@ -136,36 +162,41 @@ if ($action === 'delete_category') {
// 6. VALIDATION DES VIREMENTS (Complex Business Logic) // 6. VALIDATION DES VIREMENTS (Complex Business Logic)
if ($action === 'validate_transfers') { if ($action === 'validate_transfers') {
header('Content-Type: application/json'); header('Content-Type: application/json');
$person = $_POST['person']; $personId = (int)$_POST['person_id']; // Reçoit l'ID parent
$monthDate = $_POST['month_date']; $monthDate = $_POST['month_date'];
try { try {
$pdo->beginTransaction(); $pdo->beginTransaction();
$stmtP = $pdo->prepare("SELECT name FROM pf_people WHERE id = ?");
$stmtP->execute([$personId]);
$dbPersonName = $stmtP->fetchColumn();
if (!$dbPersonName) throw new Exception("Parent introuvable.");
$stmt = $pdo->prepare(" $stmt = $pdo->prepare("
SELECT v.*, c.name as cat_name, c.target, c.holiday_id SELECT v.amount, c.name as cat_name, c.transfer_dest, c.holiday_id
FROM pf_alloc_values v FROM pf_alloc_values v
JOIN pf_alloc_categories c ON v.cat_id = c.id JOIN pf_alloc_categories c ON v.cat_id = c.id
WHERE v.month_date = ? WHERE v.month_date = ? AND v.person_id = ?
"); ");
$stmt->execute([$monthDate]); $stmt->execute([$monthDate, $personId]);
$budgetLines = $stmt->fetchAll(PDO::FETCH_ASSOC); $budgetLines = $stmt->fetchAll(PDO::FETCH_ASSOC);
$transfersToDo = []; $transfersToDo = [];
foreach ($budgetLines as $line) { foreach ($budgetLines as $line) {
$amount = ($person === 'Alex') ? $line['amount_alex'] : $line['amount_laia']; $amount = (float)$line['amount'];
if ($amount <= 0) continue; if ($amount <= 0) continue;
$target = trim($line['target']); $dest = trim($line['transfer_dest']);
$catName = trim($line['cat_name']); $catName = trim($line['cat_name']);
$holidayId = $line['holiday_id']; $holidayId = $line['holiday_id'];
$targetOwner = null; $targetOwner = null;
if ($target === 'vers L.Perso') { $targetOwner = $person; } if ($dest === 'vers L.Perso') { $targetOwner = $dbPersonName; }
elseif ($target === 'vers L.Pol') { $targetOwner = 'Pol'; } elseif ($dest === 'vers L.Pol') { $targetOwner = 'Pol'; }
elseif ($target === 'vers L.Pep') { $targetOwner = 'Pep'; } elseif ($dest === 'vers L.Pep') { $targetOwner = 'Pep'; }
elseif ($target === 'vers commune') { continue; } elseif ($dest === 'vers commune') { continue; }
if ($targetOwner) { if ($targetOwner) {
if (!isset($transfersToDo[$targetOwner])) { if (!isset($transfersToDo[$targetOwner])) {
@@ -181,7 +212,6 @@ if ($action === 'validate_transfers') {
} }
foreach ($transfersToDo as $owner => $data) { foreach ($transfersToDo as $owner => $data) {
// A. VERIFIER EXISTENCE (Inchangé)
$stmtCheck = $pdo->prepare("SELECT COUNT(*) FROM pf_savings WHERE owner = ? AND month_date = ? AND category = 'TOTAL_BANQUE'"); $stmtCheck = $pdo->prepare("SELECT COUNT(*) FROM pf_savings WHERE owner = ? AND month_date = ? AND category = 'TOTAL_BANQUE'");
$stmtCheck->execute([$owner, $monthDate]); $stmtCheck->execute([$owner, $monthDate]);
$exists = $stmtCheck->fetchColumn() > 0; $exists = $stmtCheck->fetchColumn() > 0;
@@ -200,54 +230,38 @@ if ($action === 'validate_transfers') {
} }
} }
// B. UPDATE TOTAL (Inchangé)
$stmtUpdTotal = $pdo->prepare("UPDATE pf_savings SET amount = amount + ? WHERE owner = ? AND month_date = ? AND category = 'TOTAL_BANQUE'"); $stmtUpdTotal = $pdo->prepare("UPDATE pf_savings SET amount = amount + ? WHERE owner = ? AND month_date = ? AND category = 'TOTAL_BANQUE'");
$stmtUpdTotal->execute([$data['total_add'], $owner, $monthDate]); $stmtUpdTotal->execute([$data['total_add'], $owner, $monthDate]);
// C. UPDATE CATÉGORIES (Modifié pour gérer le holiday_id)
foreach ($data['cats'] as $catName => $catInfo) { foreach ($data['cats'] as $catName => $catInfo) {
$catAmount = $catInfo['amount']; $catAmount = $catInfo['amount'];
$catHolidayId = $catInfo['holiday_id']; // NOUVEAU $catHolidayId = $catInfo['holiday_id'];
if ($catName === 'Eco Alex' || $catName === 'Eco Laia') { continue; } if (strpos($catName, 'Eco P') === 0) { continue; }
$stmtCheckCat = $pdo->prepare("SELECT id FROM pf_savings WHERE owner = ? AND month_date = ? AND category = ?"); $stmtCheckCat = $pdo->prepare("SELECT id FROM pf_savings WHERE owner = ? AND month_date = ? AND category = ?");
$stmtCheckCat->execute([$owner, $monthDate, $catName]); $stmtCheckCat->execute([$owner, $monthDate, $catName]);
$catId = $stmtCheckCat->fetchColumn(); $catId = $stmtCheckCat->fetchColumn();
if ($catId) { if ($catId) {
// Update : On actualise aussi le holiday_id au cas où il aurait changé
$stmtUpdateCat = $pdo->prepare("UPDATE pf_savings SET amount = amount + ?, holiday_id = ? WHERE id = ?"); $stmtUpdateCat = $pdo->prepare("UPDATE pf_savings SET amount = amount + ?, holiday_id = ? WHERE id = ?");
$stmtUpdateCat->execute([$catAmount, $catHolidayId, $catId]); $stmtUpdateCat->execute([$catAmount, $catHolidayId, $catId]);
} else { } else {
// Insert
$stmtInsertCat = $pdo->prepare("INSERT INTO pf_savings (month_date, owner, category, amount, holiday_id) VALUES (?, ?, ?, ?, ?)"); $stmtInsertCat = $pdo->prepare("INSERT INTO pf_savings (month_date, owner, category, amount, holiday_id) VALUES (?, ?, ?, ?, ?)");
$stmtInsertCat->execute([$monthDate, $owner, $catName, $catAmount, $catHolidayId]); $stmtInsertCat->execute([$monthDate, $owner, $catName, $catAmount, $catHolidayId]);
} }
} }
} }
// 3. ENREGISTRER LA VALIDATION (Mise à jour table existante)
// a. Trouver l'ID de la catégorie système
$stmtSys = $pdo->prepare("SELECT id FROM pf_alloc_categories WHERE name = 'SYSTEM_VALIDATION' LIMIT 1"); $stmtSys = $pdo->prepare("SELECT id FROM pf_alloc_categories WHERE name = 'SYSTEM_VALIDATION' LIMIT 1");
$stmtSys->execute(); $stmtSys->execute();
$sysCatId = $stmtSys->fetchColumn(); $sysCatId = $stmtSys->fetchColumn();
if ($sysCatId) { if ($sysCatId) {
$stmtVal = $pdo->prepare("INSERT INTO pf_alloc_values (month_date, cat_id, person_id, amount)
if ($person === 'Alex') { VALUES (?, ?, ?, 1)
$sql = "INSERT INTO pf_alloc_values (month_date, cat_id, amount_alex, amount_laia) ON DUPLICATE KEY UPDATE amount = 1");
VALUES (?, ?, 1, 0) $stmtVal->execute([$monthDate, $sysCatId, $personId]);
ON DUPLICATE KEY UPDATE amount_alex = 1";
} else {
$sql = "INSERT INTO pf_alloc_values (month_date, cat_id, amount_alex, amount_laia)
VALUES (?, ?, 0, 1)
ON DUPLICATE KEY UPDATE amount_laia = 1";
}
$stmtVal = $pdo->prepare($sql);
$stmtVal->execute([$monthDate, $sysCatId]);
} }
$pdo->commit(); $pdo->commit();
@@ -258,6 +272,4 @@ if ($action === 'validate_transfers') {
echo json_encode(['success' => false, 'error' => $e->getMessage()]); echo json_encode(['success' => false, 'error' => $e->getMessage()]);
} }
exit; exit;
} }
+52 -40
View File
@@ -16,7 +16,6 @@ foreach ($budgetParents as $index => $parent) {
$parentMapping[] = [ $parentMapping[] = [
'id' => (int)$parent['id'], 'id' => (int)$parent['id'],
'name' => $parent['name'], 'name' => $parent['name'],
'db_field' => 'amount_p' . $num,
'css' => 'p' . $num, 'css' => 'p' . $num,
'color' => $parent['color'] ?? (($num === 1) ? '#0891b2' : '#f59e0b') 'color' => $parent['color'] ?? (($num === 1) ? '#0891b2' : '#f59e0b')
]; ];
@@ -52,7 +51,6 @@ for ($i = 0; $i < 6; $i++) {
$prevMonthLink = date('Y-m-01', strtotime("-1 month", $focusTs)); $prevMonthLink = date('Y-m-01', strtotime("-1 month", $focusTs));
$nextMonthLink = date('Y-m-01', strtotime("+1 month", $focusTs)); $nextMonthLink = date('Y-m-01', strtotime("+1 month", $focusTs));
// Récupération des Cycles configurés dans pf_notes
$cycleConfigs = []; $cycleConfigs = [];
$stmtNotes = $pdo->query("SELECT reference_id, content FROM pf_notes WHERE note_type = 'month_config'"); $stmtNotes = $pdo->query("SELECT reference_id, content FROM pf_notes WHERE note_type = 'month_config'");
while ($row = $stmtNotes->fetch(PDO::FETCH_ASSOC)) { while ($row = $stmtNotes->fetch(PDO::FETCH_ASSOC)) {
@@ -63,14 +61,14 @@ while ($row = $stmtNotes->fetch(PDO::FETCH_ASSOC)) {
} }
} }
// 5. Récupération Valeurs Répartition // 5. Récupération Valeurs Répartition Relationnelles
$inQuery = implode(',', array_fill(0, count($months), '?')); $inQuery = implode(',', array_fill(0, count($months), '?'));
$stmt = $pdo->prepare("SELECT * FROM pf_alloc_values WHERE month_date IN ($inQuery)"); $stmt = $pdo->prepare("SELECT * FROM pf_alloc_values WHERE month_date IN ($inQuery)");
$stmt->execute($months); $stmt->execute($months);
$allocs = []; $allocs = [];
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
$allocs[$row['month_date']][$row['cat_id']] = $row; $allocs[$row['month_date']][$row['cat_id']][$row['person_id']] = (float)$row['amount'];
} }
// 6. Récupération de l'ID de la catégorie système // 6. Récupération de l'ID de la catégorie système
@@ -86,14 +84,10 @@ foreach ($cats as $key => $c) {
// Statuts de validation // Statuts de validation
$focusDate = $months[0]; $focusDate = $months[0];
$isValidated = []; $isValidated = [];
if ($sysCatId && isset($allocs[$focusDate][$sysCatId])) { foreach ($parentMapping as $map) {
$row = $allocs[$focusDate][$sysCatId];
foreach ($parentMapping as $map) {
$isValidated[$map['css']] = ($row[$map['db_field']] == 1);
}
} else {
foreach ($parentMapping as $map) {
$isValidated[$map['css']] = false; $isValidated[$map['css']] = false;
if ($sysCatId && isset($allocs[$focusDate][$sysCatId][$map['id']])) {
$isValidated[$map['css']] = ($allocs[$focusDate][$sysCatId][$map['id']] == 1);
} }
} }
@@ -110,7 +104,7 @@ function getTranslatedMonthName($dateString) {
} }
?> ?>
<div class="prev-container" style="--p1-main: <?= $parentMapping[0]['color'] ?>; --p2-main: <?= $parentMapping[1]['color'] ?>;"> <div class="prev-container" style="--p1-main: <?= $parentMapping[0]['color'] ?>; --p2-main: <?= $parentMapping[1]['color'] ?? '#f59e0b' ?>;">
<div> <div>
<div class="prev-section-header"> <div class="prev-section-header">
@@ -244,29 +238,33 @@ function getTranslatedMonthName($dateString) {
<?php if($isIndicative): ?><span>Info</span><?php endif; ?> <?php if($isIndicative): ?><span>Info</span><?php endif; ?>
</div> </div>
<div class="cat-target-label"> <div class="cat-target-label">
<?= htmlspecialchars($cat['target']) ?> <?php if((float)$cat['target'] > 0) echo 'Obj: ' . round((float)$cat['target']) . '€ '; ?>
<?php if(!empty($cat['transfer_dest'])) echo '➔ ' . htmlspecialchars($cat['transfer_dest']); ?>
</div> </div>
<div class="row-actions"> <div class="row-actions">
<button type="button" class="btn-icon-action edit" title="<?= tr('edit') ?>" data-id="<?= $cat['id'] ?>" data-name="<?= htmlspecialchars($cat['name']) ?>" data-target="<?= htmlspecialchars($cat['target']) ?>" data-holiday="<?= $cat['holiday_id'] ?? '' ?>" onclick="openEditModal(this)">✎</button> <button type="button" class="btn-icon-action edit" title="<?= tr('edit') ?>"
data-id="<?= $cat['id'] ?>"
data-name="<?= htmlspecialchars($cat['name']) ?>"
data-target="<?= htmlspecialchars($cat['target'] ?? 0) ?>"
data-transfer-dest="<?= htmlspecialchars($cat['transfer_dest'] ?? '') ?>"
data-holiday="<?= $cat['holiday_id'] ?? '' ?>"
onclick="openEditModal(this)">✎</button>
<button type="button" onclick="deleteCategory(<?= $cat['id'] ?>)" class="btn-icon-action delete" title="<?= tr('delete') ?>">🗑️</button> <button type="button" onclick="deleteCategory(<?= $cat['id'] ?>)" class="btn-icon-action delete" title="<?= tr('delete') ?>">🗑️</button>
</div> </div>
</td> </td>
<?php foreach ($months as $m): <?php foreach ($months as $m): ?>
$val = $allocs[$m][$cat['id']] ?? [];
?>
<td class="txt-global sum-target" id="g_<?= $m ?>_<?= $cat['id'] ?>">0</td> <td class="txt-global sum-target" id="g_<?= $m ?>_<?= $cat['id'] ?>">0</td>
<?php foreach ($parentMapping as $map): <?php foreach ($parentMapping as $map):
$dbField = $map['db_field']; $cellValue = $allocs[$m][$cat['id']][$map['id']] ?? 0;
$cellValue = isset($val[$dbField]) ? $val[$dbField] : 0;
?> ?>
<td> <td>
<input type="number" step="1" class="prev-input txt-<?= $map['css'] ?> inp-<?= $map['css'] ?>-<?= $m ?> <?= $inputClass ?>" <input type="number" step="1" class="prev-input txt-<?= $map['css'] ?> inp-<?= $map['css'] ?>-<?= $m ?> <?= $inputClass ?>"
value="<?= $cellValue == 0 ? '' : round($cellValue) ?>" value="<?= $cellValue == 0 ? '' : round($cellValue) ?>"
placeholder="-" placeholder="-"
data-target="<?= htmlspecialchars($cat['target']) ?>" data-transfer-dest="<?= htmlspecialchars($cat['transfer_dest'] ?? '') ?>"
onchange="updateAlloc('<?= $m ?>', <?= $cat['id'] ?>, '<?= $dbField ?>', this)"> onchange="updateAlloc('<?= $m ?>', <?= $cat['id'] ?>, <?= $map['id'] ?>, this)">
</td> </td>
<?php endforeach; ?> <?php endforeach; ?>
<?php endforeach; ?> <?php endforeach; ?>
@@ -293,10 +291,13 @@ function getTranslatedMonthName($dateString) {
<?php <?php
$focusMonth = $months[0]; $focusMonth = $months[0];
$targetsOrder = ['vers commune', 'vers L.Pol', 'vers L.Pep', 'vers L.Perso']; $targetsOrder = ['vers commune', 'vers L.Pol', 'vers L.Pep', 'vers L.Perso'];
$allTargets = $targetsOrder; $allTargets = $targetsOrder;
foreach($cats as $c) { foreach($cats as $c) {
$t = trim($c['target']); $t = trim($c['transfer_dest'] ?? '');
if(!empty($t) && !in_array($t, $allTargets)) { $allTargets[] = $t; } if(!empty($t) && !in_array($t, $allTargets)) {
$allTargets[] = $t;
}
} }
$allTargets = array_unique($allTargets); $allTargets = array_unique($allTargets);
?> ?>
@@ -364,9 +365,13 @@ function getTranslatedMonthName($dateString) {
<input type="text" name="name" class="pf-input" required> <input type="text" name="name" class="pf-input" required>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="pf-label"><?= tr('bud_prev_label_target') ?></label> <label class="pf-label">Objectif Mensuel (€)</label>
<select name="target" class="pf-input" required> <input type="number" step="1" name="target" class="pf-input" placeholder="Ex: 150">
<option value="" disabled selected>-- <?= tr('bud_prev_choose') ?> --</option> </div>
<div class="form-group">
<label class="pf-label">Destination Virement (Optionnel)</label>
<select name="transfer_dest" class="pf-input">
<option value="" selected>-- Aucune --</option>
<option value="vers L.Pol">vers L.Pol</option> <option value="vers L.Pol">vers L.Pol</option>
<option value="vers L.Pep">vers L.Pep</option> <option value="vers L.Pep">vers L.Pep</option>
<option value="vers L.Perso">vers L.Perso</option> <option value="vers L.Perso">vers L.Perso</option>
@@ -404,8 +409,13 @@ function getTranslatedMonthName($dateString) {
<input type="text" name="name" id="edit_cat_name" class="pf-input" required> <input type="text" name="name" id="edit_cat_name" class="pf-input" required>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="pf-label"><?= tr('bud_prev_label_target') ?></label> <label class="pf-label">Objectif Mensuel (€)</label>
<select name="target" id="edit_cat_target" class="pf-input" required> <input type="number" step="1" name="target" id="edit_cat_target" class="pf-input" placeholder="Ex: 150">
</div>
<div class="form-group">
<label class="pf-label">Destination Virement (Optionnel)</label>
<select name="transfer_dest" id="edit_cat_transfer_dest" class="pf-input">
<option value="">-- Aucune --</option>
<option value="vers L.Pol">vers L.Pol</option> <option value="vers L.Pol">vers L.Pol</option>
<option value="vers L.Pep">vers L.Pep</option> <option value="vers L.Pep">vers L.Pep</option>
<option value="vers L.Perso">vers L.Perso</option> <option value="vers L.Perso">vers L.Perso</option>
@@ -452,6 +462,7 @@ window.I18N = {
window.CONFIG = window.CONFIG || {}; window.CONFIG = window.CONFIG || {};
window.CONFIG.parentMapping = <?= json_encode($parentMapping) ?>; window.CONFIG.parentMapping = <?= json_encode($parentMapping) ?>;
window.CONFIG.CURRENCY = '<?= defined('CURRENCY') ? CURRENCY : "€" ?>';
const currentYear = <?= $currentYear ?>; const currentYear = <?= $currentYear ?>;
const months = <?= json_encode($months) ?>; const months = <?= json_encode($months) ?>;
@@ -460,6 +471,7 @@ function openEditModal(btn) {
document.getElementById('edit_cat_id').value = btn.getAttribute('data-id'); document.getElementById('edit_cat_id').value = btn.getAttribute('data-id');
document.getElementById('edit_cat_name').value = btn.getAttribute('data-name'); document.getElementById('edit_cat_name').value = btn.getAttribute('data-name');
document.getElementById('edit_cat_target').value = btn.getAttribute('data-target'); document.getElementById('edit_cat_target').value = btn.getAttribute('data-target');
document.getElementById('edit_cat_transfer_dest').value = btn.getAttribute('data-transfer-dest');
document.getElementById('edit_cat_holiday').value = btn.getAttribute('data-holiday'); document.getElementById('edit_cat_holiday').value = btn.getAttribute('data-holiday');
document.getElementById('editCatModal').style.display = 'flex'; document.getElementById('editCatModal').style.display = 'flex';
document.body.classList.add('no-scroll'); document.body.classList.add('no-scroll');
@@ -484,8 +496,8 @@ function updateSalary(person, input) {
recalcAllAllocations(); recalcAllAllocations();
} }
function updateAlloc(month, catId, personField, input) { function updateAlloc(month, catId, personId, input) {
saveData('update_allocation', { month_date: month, cat_id: catId, person: personField, value: input.value || 0 }); saveData('update_allocation', { month_date: month, cat_id: catId, person_id: personId, value: input.value || 0 });
recalcAllAllocations(); recalcAllAllocations();
} }
@@ -522,7 +534,7 @@ function duplicateMonth() {
const tInp = row.querySelector('.inp-' + map.css + '-' + targetDateStr); const tInp = row.querySelector('.inp-' + map.css + '-' + targetDateStr);
if(sInp && tInp) { if(sInp && tInp) {
tInp.value = sInp.value; tInp.value = sInp.value;
updateAlloc(targetDateStr, catId, map.db_field, tInp); updateAlloc(targetDateStr, catId, map.id, tInp);
} }
}); });
}); });
@@ -593,10 +605,10 @@ function updateSummaryTable() {
dataByTarget[map.css] = {}; dataByTarget[map.css] = {};
document.querySelectorAll('.inp-' + map.css + '-' + focusMonth).forEach(inp => { document.querySelectorAll('.inp-' + map.css + '-' + focusMonth).forEach(inp => {
const target = inp.getAttribute('data-target'); const dest = inp.getAttribute('data-transfer-dest');
if(target) { if(dest) {
if(!dataByTarget[map.css][target]) dataByTarget[map.css][target] = 0; if(!dataByTarget[map.css][dest]) dataByTarget[map.css][dest] = 0;
dataByTarget[map.css][target] += (parseFloat(inp.value) || 0); dataByTarget[map.css][dest] += (parseFloat(inp.value) || 0);
} }
}); });
}); });
@@ -637,15 +649,15 @@ function saveData(action, data) {
} }
function validateTransfers(personCss, month) { function validateTransfers(personCss, month) {
const msg = window.I18N['bud_prev_confirm_transfers'].replace('%p', personCss).replace('%m', month);
if (!confirm(msg)) return;
const parentMap = window.CONFIG.parentMapping.find(m => m.css === personCss); const parentMap = window.CONFIG.parentMapping.find(m => m.css === personCss);
const dbPersonName = parentMap ? parentMap.name : personCss; if (!parentMap) return;
const msg = window.I18N['bud_prev_confirm_transfers'].replace('%p', parentMap.name).replace('%m', month);
if (!confirm(msg)) return;
const formData = new FormData(); const formData = new FormData();
formData.append('action', 'validate_transfers'); formData.append('action', 'validate_transfers');
formData.append('person', dbPersonName); formData.append('person_id', parentMap.id);
formData.append('month_date', month); formData.append('month_date', month);
fetch('modules/budget/includes/api/save-budget.php', { method: 'POST', body: formData }) fetch('modules/budget/includes/api/save-budget.php', { method: 'POST', body: formData })
+39 -13
View File
@@ -57,12 +57,11 @@ if (isset($_POST['action']) && $_POST['action'] === 'reopen_month') {
exit; exit;
} }
if (isset($_POST['action']) && $_POST['action'] === 'save_import') { if (isset($_POST['action']) && $_POST['action'] === 'save_import') {
$count = 0; $count = 0;
$stmtExp = $pdo->prepare("INSERT INTO pf_expenses (date_exp, gestion_month, category, label, amount, import_ref, budget_item_id, holiday_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"); $stmtExp = $pdo->prepare("INSERT INTO pf_expenses (date_exp, gestion_month, category, label, amount, import_ref, budget_item_id, holiday_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
$stmtRule = $pdo->prepare("INSERT INTO pf_import_rules (keyword, category) VALUES (?, ?) ON DUPLICATE KEY UPDATE category = VALUES(category)"); // Mémorisation du mapping étendu (incluant budget_item_id)
$stmtRule = $pdo->prepare("INSERT INTO pf_import_rules (keyword, category, budget_item_id) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE category = VALUES(category), budget_item_id = VALUES(budget_item_id)");
if (isset($_POST['lines']) && is_array($_POST['lines'])) { if (isset($_POST['lines']) && is_array($_POST['lines'])) {
foreach ($_POST['lines'] as $line) { foreach ($_POST['lines'] as $line) {
@@ -81,7 +80,7 @@ if (isset($_POST['action']) && $_POST['action'] === 'save_import') {
try { try {
$stmtExp->execute([$dateToSave, $gestionMonthLine, $cat, $line['label'], $finalAmount, $line['ref'], $budgetItemId, $holidayId]); $stmtExp->execute([$dateToSave, $gestionMonthLine, $cat, $line['label'], $finalAmount, $line['ref'], $budgetItemId, $holidayId]);
$stmtRule->execute([$line['label'], $cat]); $stmtRule->execute([$line['label'], $cat, $budgetItemId]);
$count++; $count++;
} catch (Exception $e) { continue; } } catch (Exception $e) { continue; }
} }
@@ -110,7 +109,15 @@ $showPreview = false;
if (isset($_FILES['csv_file']) && $_FILES['csv_file']['error'] == 0) { if (isset($_FILES['csv_file']) && $_FILES['csv_file']['error'] == 0) {
$file = $_FILES['csv_file']['tmp_name']; $file = $_FILES['csv_file']['tmp_name'];
$handle = fopen($file, "r"); $handle = fopen($file, "r");
$rules = []; try { $rules = $pdo->query("SELECT keyword, category FROM pf_import_rules")->fetchAll(PDO::FETCH_KEY_PAIR); } catch(Exception $e){}
$rules = [];
try {
$stmtRules = $pdo->query("SELECT keyword, category, budget_item_id FROM pf_import_rules");
while ($r = $stmtRules->fetch(PDO::FETCH_ASSOC)) {
$rules[$r['keyword']] = ['cat' => $r['category'], 'budget_item_id' => $r['budget_item_id']];
}
} catch(Exception $e){}
$existingRefs = []; try { $existingRefs = $pdo->query("SELECT import_ref FROM pf_expenses WHERE import_ref IS NOT NULL")->fetchAll(PDO::FETCH_COLUMN); } catch(Exception $e){} $existingRefs = []; try { $existingRefs = $pdo->query("SELECT import_ref FROM pf_expenses WHERE import_ref IS NOT NULL")->fetchAll(PDO::FETCH_COLUMN); } catch(Exception $e){}
fgetcsv($handle, 1000, ";", "\"", "\\"); fgetcsv($handle, 1000, ";", "\"", "\\");
@@ -129,9 +136,16 @@ if (isset($_FILES['csv_file']) && $_FILES['csv_file']['error'] == 0) {
$isDuplicate = in_array($uniqueKey, $existingRefs); $isDuplicate = in_array($uniqueKey, $existingRefs);
$suggestedCat = ''; $suggestedCat = '';
foreach ($rules as $kw => $c) { if (stripos($label, $kw) !== false) { $suggestedCat = $c; break; } } $suggestedItemId = null;
foreach ($rules as $kw => $ruleData) {
if (stripos($label, $kw) !== false) {
$suggestedCat = $ruleData['cat'];
$suggestedItemId = $ruleData['budget_item_id'];
break;
}
}
$csvData[] = ['date'=>$dateSql, 'label'=>$label, 'amount'=>$amount, 'cat'=>$suggestedCat, 'ref'=>$uniqueKey, 'is_duplicate'=>$isDuplicate, 'is_credit'=>$isCredit]; $csvData[] = ['date'=>$dateSql, 'label'=>$label, 'amount'=>$amount, 'cat'=>$suggestedCat, 'suggested_item_id'=>$suggestedItemId, 'ref'=>$uniqueKey, 'is_duplicate'=>$isDuplicate, 'is_credit'=>$isCredit];
} }
fclose($handle); fclose($handle);
$showPreview = true; $showPreview = true;
@@ -224,6 +238,7 @@ $categoriesConfig = [
'School' => ['type'=>'debit', 'label'=>tr('cat_school'), 'budget'=>$budget_school, 'color'=>'#10b981', 'suggestions'=>[]], 'School' => ['type'=>'debit', 'label'=>tr('cat_school'), 'budget'=>$budget_school, 'color'=>'#10b981', 'suggestions'=>[]],
'Frais' => ['type'=>'debit', 'label'=>tr('cat_fixed'), 'budget'=>$budget_frais, 'color'=>'#ef4444', 'suggestions'=>[]], 'Frais' => ['type'=>'debit', 'label'=>tr('cat_fixed'), 'budget'=>$budget_frais, 'color'=>'#ef4444', 'suggestions'=>[]],
'Autres' => ['type'=>'debit', 'label'=>tr('cat_others'), 'budget'=>$budget_autres, 'color'=>'#64748b', 'suggestions'=>['Restaurant', 'Cadeau']], 'Autres' => ['type'=>'debit', 'label'=>tr('cat_others'), 'budget'=>$budget_autres, 'color'=>'#64748b', 'suggestions'=>['Restaurant', 'Cadeau']],
'Apports' => ['type'=>'debit', 'label'=>tr('cat_contributions') ?? 'Apports & Projets', 'budget'=>0, 'color'=>'#0ea5e9', 'suggestions'=>['Alex', 'Laia', 'Remboursement']],
'LivretA' => ['type'=>'debit', 'label'=>tr('cat_savings'),'budget'=>0, 'color'=>'#8b5cf6', 'suggestions'=>['Virement']] 'LivretA' => ['type'=>'debit', 'label'=>tr('cat_savings'),'budget'=>0, 'color'=>'#8b5cf6', 'suggestions'=>['Virement']]
]; ];
@@ -695,11 +710,15 @@ $monthName = $monthNames[(int)$viewM] . ' ' . $viewY;
</select> </select>
<select name="lines[<?= $idx ?>][budget_item_id]" class="pf-input select-frais" onchange="checkValidation()" style="display:none; padding:4px; font-size:0.85rem; flex:1;" disabled> <select name="lines[<?= $idx ?>][budget_item_id]" class="pf-input select-frais" onchange="checkValidation()" style="display:none; padding:4px; font-size:0.85rem; flex:1;" disabled>
<option value="">-- <?= tr('bud_is_charge') ?> --</option> <option value="">-- <?= tr('bud_is_charge') ?> --</option>
<?php foreach ($fixedChargesList as $fc): ?><option value="<?= $fc['id'] ?>"><?= htmlspecialchars($fc['name']) ?></option><?php endforeach; ?> <?php foreach ($fixedChargesList as $fc): ?>
<option value="<?= $fc['id'] ?>" <?= ($row['suggested_item_id'] == $fc['id']) ? 'selected' : '' ?>><?= htmlspecialchars($fc['name']) ?></option>
<?php endforeach; ?>
</select> </select>
<select name="lines[<?= $idx ?>][budget_item_id]" class="pf-input select-income" onchange="checkValidation()" style="display:none; padding:4px; font-size:0.85rem; flex:1;" disabled> <select name="lines[<?= $idx ?>][budget_item_id]" class="pf-input select-income" onchange="checkValidation()" style="display:none; padding:4px; font-size:0.85rem; flex:1;" disabled>
<option value="">-- <?= tr('bud_is_income') ?> --</option> <option value="">-- <?= tr('bud_is_income') ?> --</option>
<?php foreach ($incomeList as $inc): ?><option value="<?= $inc['id'] ?>"><?= htmlspecialchars($inc['name']) ?></option><?php endforeach; ?> <?php foreach ($incomeList as $inc): ?>
<option value="<?= $inc['id'] ?>" <?= ($row['suggested_item_id'] == $inc['id']) ? 'selected' : '' ?>><?= htmlspecialchars($inc['name']) ?></option>
<?php endforeach; ?>
</select> </select>
</div> </div>
</td> </td>
@@ -792,10 +811,14 @@ function openEditModal(e) {
else if (e.category === 'Income') document.getElementById('incomeSelect').value = e.budget_item_id; else if (e.category === 'Income') document.getElementById('incomeSelect').value = e.budget_item_id;
} }
function handleLineCatChange(select) { function handleLineCatChange(select, isInit = false) {
const row = select.closest('tr'); const row = select.closest('tr');
const fSel = row.querySelector('.select-frais'); const iSel = row.querySelector('.select-income'); const fSel = row.querySelector('.select-frais'); const iSel = row.querySelector('.select-income');
fSel.style.display = 'none'; iSel.style.display = 'none'; fSel.value = ''; iSel.value = ''; fSel.disabled = true; iSel.disabled = true;
fSel.style.display = 'none'; iSel.style.display = 'none';
if (!isInit) { fSel.value = ''; iSel.value = ''; }
fSel.disabled = true; iSel.disabled = true;
if (select.value === 'Frais') { fSel.style.display = 'block'; fSel.disabled = false; } if (select.value === 'Frais') { fSel.style.display = 'block'; fSel.disabled = false; }
else if (select.value === 'Income') { iSel.style.display = 'block'; iSel.disabled = false; } else if (select.value === 'Income') { iSel.style.display = 'block'; iSel.disabled = false; }
checkValidation(); checkValidation();
@@ -815,7 +838,10 @@ function checkValidation() {
else { btn.disabled = false; btn.style.opacity=1; msg.style.display='none'; } else { btn.disabled = false; btn.style.opacity=1; msg.style.display='none'; }
} }
if(document.getElementById('formMapping')) { document.querySelectorAll('.line-select').forEach(s => handleLineCatChange(s)); checkValidation(); } if(document.getElementById('formMapping')) {
document.querySelectorAll('.line-select').forEach(s => handleLineCatChange(s, true));
checkValidation();
}
window.addEventListener('click', (e) => { window.addEventListener('click', (e) => {
if (e.target.classList.contains('pf-modal')) { if (e.target.classList.contains('pf-modal')) {
@@ -826,7 +852,7 @@ window.addEventListener('click', (e) => {
// --- 2. SUPPRESSION ASYNCHRONE --- // --- 2. SUPPRESSION ASYNCHRONE ---
async function deleteExpense(id) { async function deleteExpense(id) {
const confirmed = await pachaConfirm("Suppression", tr('bud_confirm_delete')); const confirmed = await pachaConfirm("Suppression", window.I18N['bud_confirm_delete']);
if (!confirmed) return; if (!confirmed) return;
const formData = new FormData(); const formData = new FormData();
+255 -19
View File
@@ -1,10 +1,18 @@
-- HouseHub — Schéma MySQL -- HouseHub — Schéma MySQL (Modèle pour une base famille)
-- Créer la base : CREATE DATABASE househub CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
SET NAMES utf8mb4; SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0; SET FOREIGN_KEY_CHECKS = 0;
-- ─── Utilisateurs ───────────────────────────────────────────────────────────── -- ─── Configuration du foyer ───────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS pf_foyer_settings (
id INT AUTO_INCREMENT PRIMARY KEY,
currency VARCHAR(10) NOT NULL DEFAULT '',
zone_scolaire VARCHAR(5) NOT NULL DEFAULT 'C',
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT IGNORE INTO pf_foyer_settings (id, currency, zone_scolaire) VALUES (1, '', 'C');
-- ─── Utilisateurs (Legacy) ────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS pf_users ( CREATE TABLE IF NOT EXISTS pf_users (
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(100) NOT NULL UNIQUE, username VARCHAR(100) NOT NULL UNIQUE,
@@ -15,11 +23,12 @@ CREATE TABLE IF NOT EXISTS pf_users (
-- ─── Personnes ──────────────────────────────────────────────────────────────── -- ─── Personnes ────────────────────────────────────────────────────────────────
CREATE TABLE `pf_people` ( CREATE TABLE `pf_people` (
`id` int(11) NOT NULL AUTO_INCREMENT, `id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(50) NOT NULL, `name` varchar(100) NOT NULL,
`user_id` int(11) DEFAULT NULL, `user_id` int(11) DEFAULT NULL,
`role` varchar(50) DEFAULT NULL, `role` varchar(50) DEFAULT NULL,
`color` varchar(7) DEFAULT '#0891b2',
PRIMARY KEY (`id`) PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ─── Calendrier familial ────────────────────────────────────────────────────── -- ─── Calendrier familial ──────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS pf_events ( CREATE TABLE IF NOT EXISTS pf_events (
@@ -94,7 +103,7 @@ CREATE TABLE IF NOT EXISTS pf_budget_items (
CREATE TABLE IF NOT EXISTS pf_expenses ( CREATE TABLE IF NOT EXISTS pf_expenses (
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
date_exp DATE NOT NULL, date_exp DATE NOT NULL,
gestion_month VARCHAR(7) NOT NULL, gestion_month VARCHAR(10) NOT NULL,
category VARCHAR(100), category VARCHAR(100),
label VARCHAR(255), label VARCHAR(255),
amount DECIMAL(10,2) DEFAULT 0, amount DECIMAL(10,2) DEFAULT 0,
@@ -106,24 +115,26 @@ CREATE TABLE IF NOT EXISTS pf_expenses (
CREATE TABLE IF NOT EXISTS pf_alloc_categories ( CREATE TABLE IF NOT EXISTS pf_alloc_categories (
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL, name VARCHAR(100) NOT NULL,
target DECIMAL(10,2) DEFAULT 0, target DECIMAL(10,2) DEFAULT 0.00,
transfer_dest VARCHAR(50) DEFAULT NULL,
holiday_id INT DEFAULT NULL, holiday_id INT DEFAULT NULL,
sort_order INT DEFAULT 0 sort_order INT DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS pf_alloc_values ( CREATE TABLE IF NOT EXISTS pf_alloc_values (
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
month_date VARCHAR(7) NOT NULL, month_date VARCHAR(10) NOT NULL,
cat_id INT NOT NULL, cat_id INT NOT NULL,
amount_alex DECIMAL(10,2) DEFAULT 0, person_id INT NOT NULL,
amount_laia DECIMAL(10,2) DEFAULT 0, amount DECIMAL(10,2) DEFAULT 0.00,
UNIQUE KEY uq_alloc (month_date, cat_id) UNIQUE KEY uq_alloc_person (month_date, cat_id, person_id),
FOREIGN KEY (person_id) REFERENCES pf_people(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS pf_savings ( CREATE TABLE IF NOT EXISTS pf_savings (
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
owner VARCHAR(50) NOT NULL, owner VARCHAR(50) NOT NULL,
month_date VARCHAR(7) NOT NULL, month_date VARCHAR(10) NOT NULL,
category VARCHAR(100) NOT NULL, category VARCHAR(100) NOT NULL,
amount DECIMAL(10,2) DEFAULT 0, amount DECIMAL(10,2) DEFAULT 0,
holiday_id INT DEFAULT NULL holiday_id INT DEFAULT NULL
@@ -150,9 +161,22 @@ CREATE TABLE IF NOT EXISTS pf_salary_config (
CREATE TABLE IF NOT EXISTS pf_import_rules ( CREATE TABLE IF NOT EXISTS pf_import_rules (
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
keyword VARCHAR(255) NOT NULL UNIQUE, keyword VARCHAR(255) NOT NULL UNIQUE,
category VARCHAR(100) NOT NULL category VARCHAR(100) NOT NULL,
budget_item_id INT(11) NULL DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS pf_advances (
id int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
advance_date date NOT NULL,
payer varchar(50) NOT NULL,
description varchar(255) NOT NULL,
amount decimal(10,2) DEFAULT 0.00,
from_savings tinyint(1) DEFAULT 0,
is_resolved tinyint(1) DEFAULT 0,
created_at datetime DEFAULT current_timestamp()
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ─── Notes / Memo ─────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS pf_notes ( CREATE TABLE IF NOT EXISTS pf_notes (
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
note_type VARCHAR(100) NOT NULL, note_type VARCHAR(100) NOT NULL,
@@ -161,6 +185,29 @@ CREATE TABLE IF NOT EXISTS pf_notes (
UNIQUE KEY uq_note (note_type, reference_id) UNIQUE KEY uq_note (note_type, reference_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS pf_memo_notes (
id INT AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(255) NOT NULL,
content LONGTEXT DEFAULT '',
tags VARCHAR(1000) DEFAULT '',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FULLTEXT KEY ft_notes (title, content, tags)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS pf_memo_attachments (
id INT AUTO_INCREMENT PRIMARY KEY,
note_id INT NOT NULL,
type ENUM('image','file','url') NOT NULL DEFAULT 'file',
filename VARCHAR(255) DEFAULT NULL,
original_name VARCHAR(255) DEFAULT NULL,
url TEXT DEFAULT NULL,
label VARCHAR(255) DEFAULT NULL,
size INT DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (note_id) REFERENCES pf_memo_notes(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ─── Voyages / Holidays ─────────────────────────────────────────────────────── -- ─── Voyages / Holidays ───────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS pf_holidays ( CREATE TABLE IF NOT EXISTS pf_holidays (
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
@@ -228,13 +275,153 @@ CREATE TABLE IF NOT EXISTS pf_gifts (
amount DECIMAL(8,2) DEFAULT 0 amount DECIMAL(8,2) DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
SET FOREIGN_KEY_CHECKS = 1; -- ─── Garage Manager ───────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS pf_vehicles (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
brand VARCHAR(100) NOT NULL,
model VARCHAR(100) NOT NULL,
year INT DEFAULT NULL,
license_plate VARCHAR(50) DEFAULT NULL,
vin VARCHAR(100) DEFAULT NULL,
fuel_type VARCHAR(50) DEFAULT 'Essence',
color VARCHAR(50) DEFAULT NULL,
purchase_date DATE DEFAULT NULL,
purchase_price DECIMAL(10,2) DEFAULT NULL,
current_km INT DEFAULT 0,
photo VARCHAR(255) DEFAULT NULL,
notes TEXT DEFAULT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ─── Données initiales ──────────────────────────────────────────────────────── CREATE TABLE IF NOT EXISTS pf_maintenances (
-- Utilisateur admin (mot de passe : changeme → à modifier !) id INT AUTO_INCREMENT PRIMARY KEY,
INSERT IGNORE INTO pf_users (id, username, password_hash, display_name) vehicle_id INT NOT NULL,
VALUES (1, 'admin', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'Admin'); type VARCHAR(100) NOT NULL,
description TEXT DEFAULT NULL,
date DATE NOT NULL,
km INT DEFAULT NULL,
cost DECIMAL(10,2) DEFAULT 0,
mechanic VARCHAR(100) DEFAULT NULL,
garage_name VARCHAR(100) DEFAULT NULL,
next_km INT DEFAULT NULL,
next_date DATE DEFAULT NULL,
invoice_photo VARCHAR(255) DEFAULT NULL,
notes TEXT DEFAULT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (vehicle_id) REFERENCES pf_vehicles(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS pf_parts (
id INT AUTO_INCREMENT PRIMARY KEY,
vehicle_id INT DEFAULT NULL,
maintenance_id INT DEFAULT NULL,
brand VARCHAR(100) DEFAULT NULL,
reference VARCHAR(100) DEFAULT NULL,
name VARCHAR(255) NOT NULL,
category VARCHAR(100) DEFAULT 'Autre',
price DECIMAL(10,2) DEFAULT 0,
quantity INT DEFAULT 1,
unit VARCHAR(50) DEFAULT 'pièce',
supplier VARCHAR(100) DEFAULT NULL,
purchase_date DATE DEFAULT NULL,
photo VARCHAR(255) DEFAULT NULL,
notes TEXT DEFAULT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (vehicle_id) REFERENCES pf_vehicles(id) ON DELETE SET NULL,
FOREIGN KEY (maintenance_id) REFERENCES pf_maintenances(id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS pf_garage_documents (
id INT AUTO_INCREMENT PRIMARY KEY,
vehicle_id INT NOT NULL,
maintenance_id INT DEFAULT NULL,
label VARCHAR(255) DEFAULT NULL,
filename VARCHAR(255) NOT NULL,
original_name VARCHAR(255) DEFAULT NULL,
mime VARCHAR(120) DEFAULT NULL,
size INT DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
KEY idx_gd_vehicle (vehicle_id),
KEY idx_gd_maint (maintenance_id),
FOREIGN KEY (vehicle_id) REFERENCES pf_vehicles(id) ON DELETE CASCADE,
FOREIGN KEY (maintenance_id) REFERENCES pf_maintenances(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ─── Todo ─────────────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS pf_todo_lists (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
color VARCHAR(20) DEFAULT '#3b82f6',
icon VARCHAR(10) DEFAULT '📋',
position INT DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS pf_todos (
id INT AUTO_INCREMENT PRIMARY KEY,
list_id INT DEFAULT NULL,
title VARCHAR(500) NOT NULL,
notes TEXT DEFAULT NULL,
due_date DATE DEFAULT NULL,
due_time TIME DEFAULT NULL,
notified TINYINT(1) DEFAULT 0,
notified_date DATE DEFAULT NULL,
priority ENUM('none','low','medium','high') DEFAULT 'none',
done TINYINT(1) DEFAULT 0,
done_at DATETIME DEFAULT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (list_id) REFERENCES pf_todo_lists(id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ─── Module Liste (Courses partagées) ─────────────────────────────────────────
CREATE TABLE IF NOT EXISTS pf_lists (
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
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT IGNORE INTO pf_lists (id, name, position) VALUES (1, 'Ma liste', 0);
CREATE TABLE IF NOT EXISTS pf_grocery_items (
id INT AUTO_INCREMENT PRIMARY KEY,
list_id INT NOT NULL DEFAULT 1,
category_id INT DEFAULT NULL,
label VARCHAR(500) NOT NULL,
in_cart TINYINT(1) NOT NULL DEFAULT 0,
position INT NOT NULL DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
KEY idx_items_list (list_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS pf_grocery_history (
id INT AUTO_INCREMENT PRIMARY KEY,
label_hash CHAR(64) NOT NULL,
label_display VARCHAR(500) NOT NULL,
last_used_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uq_grocery_hist_hash (label_hash),
KEY idx_hist_last (last_used_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS pf_list_categories (
id INT AUTO_INCREMENT PRIMARY KEY,
icon VARCHAR(10) NOT NULL DEFAULT '🏷️',
name VARCHAR(100) NOT NULL,
position INT NOT NULL DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS pf_item_category_rules (
id INT AUTO_INCREMENT PRIMARY KEY,
keyword VARCHAR(255) NOT NULL,
category_id INT NOT NULL,
UNIQUE KEY uq_keyword (keyword)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ─── Calendar iOS ───────────────────────────────────────────────────────────── -- ─── Calendar iOS ─────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS pf_calendar_events ( CREATE TABLE IF NOT EXISTS pf_calendar_events (
@@ -264,8 +451,57 @@ CREATE TABLE IF NOT EXISTS pf_calendar_event_links (
external_uid VARCHAR(255) NOT NULL, external_uid VARCHAR(255) NOT NULL,
external_etag VARCHAR(255) DEFAULT NULL, external_etag VARCHAR(255) DEFAULT NULL,
calendar_url VARCHAR(1024) DEFAULT NULL, calendar_url VARCHAR(1024) DEFAULT NULL,
external_href VARCHAR(2048) DEFAULT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP, created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uq_calendar_event (calendar_event_id), UNIQUE KEY uq_calendar_event (calendar_event_id),
UNIQUE KEY uq_external_link (external_uid) UNIQUE KEY uq_external_link (external_uid)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ─── PrintVault ───────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS pf_pv_models (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
description TEXT DEFAULT '',
category VARCHAR(100) DEFAULT 'Non classé',
tags VARCHAR(500) DEFAULT '',
file_type VARCHAR(20) NOT NULL,
filename VARCHAR(255) NOT NULL,
original_name VARCHAR(255) NOT NULL,
file_size INT DEFAULT 0,
dim_x DECIMAL(10,3) DEFAULT 0,
dim_y DECIMAL(10,3) DEFAULT 0,
dim_z DECIMAL(10,3) DEFAULT 0,
volume DECIMAL(10,3) DEFAULT 0,
gcode_time VARCHAR(50) DEFAULT '',
gcode_filament VARCHAR(50) DEFAULT '',
gcode_nozzle VARCHAR(20) DEFAULT '',
gcode_bed VARCHAR(20) DEFAULT '',
thumb VARCHAR(255) DEFAULT '',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS pf_pv_categories (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL UNIQUE,
color VARCHAR(20) DEFAULT '#8b5cf6'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT IGNORE INTO pf_pv_categories (name, color) VALUES
('Non classé','#64748b'),('Déco','#ec4899'),('Fonctionnel','#3b82f6'),
('Mécanique','#f59e0b'),('Jouets','#10b981'),('Outils','#ef4444'),
('Architecture','#8b5cf6'),('Art','#06b6d4');
-- ─── Planka ───────────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS pf_planka_config (
id INT NOT NULL DEFAULT 1,
project_id VARCHAR(32),
admin_token TEXT,
token_expires_at DATETIME,
active_board_id VARCHAR(32),
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
SET FOREIGN_KEY_CHECKS = 1;