fix bêtises et update schema sql

This commit is contained in:
2026-06-04 13:09:47 +02:00
parent b44688a654
commit 0ff3c42d35
7 changed files with 712 additions and 367 deletions
+2 -1
View File
@@ -36,6 +36,7 @@ CREATE TABLE IF NOT EXISTS user_calendar_integrations (
calendar_url VARCHAR(1024) DEFAULT NULL,
status VARCHAR(30) DEFAULT 'connected',
last_sync_at DATETIME DEFAULT NULL,
calendar_prefs_json TEXT DEFAULT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uq_user_provider (user_id, provider),
@@ -44,4 +45,4 @@ CREATE TABLE IF NOT EXISTS user_calendar_integrations (
-- Donner au user applicatif le droit de créer des DBs famille
GRANT ALL PRIVILEGES ON `househub_f%`.* TO 'househub'@'%';
FLUSH PRIVILEGES;
FLUSH PRIVILEGES;
+66 -59
View File
@@ -1,18 +1,8 @@
-- HouseHub — Schéma MySQL
-- Créer la base : CREATE DATABASE househub CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- HouseHub — Schéma MySQL (Modèle pour une base famille)
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ─── Utilisateurs ─────────────────────────────────────────────────────────────
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 ─────────────────────────────────────────────────────────────
-- ─── Configuration du foyer ───────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS pf_foyer_settings (
id INT AUTO_INCREMENT PRIMARY KEY,
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
) 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');
-- ─── 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 ────────────────────────────────────────────────────────────────
CREATE TABLE `pf_people` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(50) NOT NULL,
`user_id` int(11) DEFAULT NULL,
`name` varchar(100) NOT NULL,
`user_id` int(11) DEFAULT NULL,
`role` varchar(50) DEFAULT NULL,
`color` varchar(7) DEFAULT '#0891b2',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ─── Calendrier familial ──────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS pf_events (
@@ -115,20 +113,22 @@ CREATE TABLE IF NOT EXISTS pf_expenses (
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS pf_alloc_categories (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
target DECIMAL(10,2) DEFAULT 0,
holiday_id INT DEFAULT NULL,
sort_order INT DEFAULT 0
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
target DECIMAL(10,2) DEFAULT 0.00,
transfer_dest VARCHAR(50) DEFAULT NULL,
holiday_id INT DEFAULT NULL,
sort_order INT DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS pf_alloc_values (
id INT AUTO_INCREMENT PRIMARY KEY,
month_date VARCHAR(10) NOT NULL,
cat_id INT NOT NULL,
amount_alex DECIMAL(10,2) DEFAULT 0,
amount_laia DECIMAL(10,2) DEFAULT 0,
UNIQUE KEY uq_alloc (month_date, cat_id)
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;
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 (
id INT AUTO_INCREMENT PRIMARY KEY,
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;
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 (
id INT AUTO_INCREMENT PRIMARY KEY,
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)
) 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 ───────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS pf_holidays (
id INT AUTO_INCREMENT PRIMARY KEY,
@@ -239,14 +275,6 @@ CREATE TABLE IF NOT EXISTS pf_gifts (
amount DECIMAL(8,2) DEFAULT 0
) 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 ───────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS pf_vehicles (
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
) 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 ─────────────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS pf_todo_lists (
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
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ─── Module Liste (multi-listes) ──────────────────────────────────────────────
-- ─── 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',
@@ -382,7 +386,7 @@ CREATE TABLE IF NOT EXISTS pf_lists (
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
) 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 (
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_etag VARCHAR(255) DEFAULT NULL,
calendar_url VARCHAR(1024) DEFAULT NULL,
external_href VARCHAR(2048) DEFAULT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
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,
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
SET FOREIGN_KEY_CHECKS = 1;
+161 -110
View File
@@ -1,9 +1,26 @@
<?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';
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 != ''");
$families = $stmt->fetchAll();
@@ -15,153 +32,187 @@ foreach ($families as $f) {
$db_name = $f['db_name'];
$family_id = $f['id'];
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;
$p2_name = null;
$parents = [];
try {
$fam_pdo = new PDO("mysql:host=$host;dbname=$db_name;charset=utf8mb4", $user, $pass, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
]);
// ==========================================
// 1. GESTION DE PF_PEOPLE (user_id, role, color)
// ==========================================
echo "<li><strong>Table pf_people :</strong> ";
// user_id
// 1. Table pf_foyer_settings
$fam_pdo->exec("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;");
$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>";
// 2. Colonne external_href pour le Calendrier iOS
try {
$fam_pdo->exec("ALTER TABLE pf_people ADD COLUMN user_id INT NULL DEFAULT NULL");
echo "<span style='color:green'>user_id OK</span> - ";
$fam_pdo->exec("ALTER TABLE pf_calendar_event_links ADD COLUMN external_href VARCHAR(2048) DEFAULT NULL");
echo "<li><span style='color:green'>Colonne 'external_href' ajoutée à pf_calendar_event_links.</span></li>";
} catch (\PDOException $e) {
if ($e->getCode() == '42S21' || strpos($e->getMessage(), '1060') !== false) {
echo "<span style='color:gray'>user_id déjà présent</span> - ";
} else { throw $e; }
if ($e->getCode() != '42S21' && strpos($e->getMessage(), '1060') === false) 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->execute([$family_id]);
$users = $stmtUsers->fetchAll();
$mappedCount = 0;
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->execute([$u['id'], $u['username']]);
$exists = $stmtCheck->fetchColumn();
if ($exists) {
// S'il existe, on s'assure que son user_id est bien renseigné
$stmtUpdate = $fam_pdo->prepare("UPDATE pf_people SET user_id = ? WHERE id = ?");
$stmtUpdate->execute([$u['id'], $exists]);
$mappedCount += $stmtUpdate->rowCount();
$fam_pdo->prepare("UPDATE pf_people SET user_id = ? WHERE id = ?")->execute([$u['id'], $exists]);
} else {
// S'il n'existe pas du tout (base f1 vide), on le crée !
$stmtInsert = $fam_pdo->prepare("INSERT INTO pf_people (name, user_id) VALUES (?, ?)");
$stmtInsert->execute([$u['display_name'] ?: $u['username'], $u['id']]);
$mappedCount++;
$fam_pdo->prepare("INSERT INTO pf_people (name, user_id) VALUES (?, ?)")->execute([$u['display_name'] ?: $u['username'], $u['id']]);
}
}
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';");
$pIds = $fam_pdo->query("SELECT id FROM pf_people WHERE role = 'parent' ORDER BY id ASC")->fetchAll(PDO::FETCH_COLUMN);
if (isset($pIds[0])) $fam_pdo->exec("UPDATE pf_people SET color = '#0891b2' WHERE id = " . (int)$pIds[0] . " AND color IS 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:blue'>OK</span></li>";
// ==========================================
// 6. REFONTE RELATIONNELLE : pf_alloc_values (Colonnes -> Lignes)
// ==========================================
echo "<li><strong>Table pf_alloc_values (Normalisation) :</strong> ";
// Vérifier si la table est déjà convertie
$checkNewFormat = $fam_pdo->query("SHOW COLUMNS FROM pf_alloc_values LIKE 'person_id'")->rowCount();
// role
try {
$fam_pdo->exec("ALTER TABLE pf_people ADD COLUMN role VARCHAR(50) DEFAULT NULL");
echo "<span style='color:green'>role OK</span> - ";
} 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)
// ==========================================
echo "<li><strong>Table pf_alloc_values :</strong> ";
$checkAlex = $fam_pdo->query("SHOW COLUMNS FROM pf_alloc_values LIKE 'amount_alex'")->rowCount();
if ($checkAlex > 0) {
$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 {
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();
if ($checkLaia > 0) {
$fam_pdo->exec("ALTER TABLE pf_alloc_values CHANGE amount_laia amount_p2 FLOAT DEFAULT 0");
echo "<span style='color:green'>amount_laia -> amount_p2</span></li>";
} else {
echo "<span style='color:gray'>amount_p2 OK</span></li>";
}
// 7. GESTION DU BUDGET (pf_alloc_categories & pf_salary_config)
$fam_pdo->exec("UPDATE pf_alloc_categories SET name = 'Eco P1' WHERE name LIKE 'Eco Alex%'");
$fam_pdo->exec("UPDATE pf_alloc_categories SET name = 'Eco P2' WHERE name LIKE 'Eco Laia%'");
// ==========================================
// 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");
$parents = $stmtParents->fetchAll();
if (count($parents) >= 2) {
$p1_name = $parents[0]['name'];
$p2_name = $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>";
$fam_pdo->prepare("UPDATE pf_salary_config SET person = ? WHERE person = 'Alex'")->execute([$parents[0]['name']]);
$fam_pdo->prepare("UPDATE pf_salary_config SET person = ? WHERE person = 'Laia'")->execute([$parents[1]['name']]);
}
} catch (\PDOException $e) {
echo "<li style='color:red'>❌ Erreur : " . $e->getMessage() . "</li>";
}
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>";
?>
+87 -75
View File
@@ -60,59 +60,85 @@ if ($action === 'update_salary_config') {
// 2. MISE A JOUR TABLEAU REPARTITION (AJAX)
if ($action === 'update_allocation') {
header('Content-Type: application/json');
$date = $_POST['month_date'];
$catId = $_POST['cat_id'];
$person = $_POST['person']; // 'amount_alex' ou 'amount_laia'
$value = floatval($_POST['value']);
header('Content-Type: application/json');
$date = $_POST['month_date'];
$catId = (int)$_POST['cat_id'];
$personId = (int)$_POST['person_id']; // 🟢 Reçoit l'ID de la personne directement !
$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)");
$stmt->execute([$date, $catId, $value]);
if ($catId <= 0 || $personId <= 0) {
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]);
exit;
}
// 3. GESTION DES CATEGORIES (Ajout)
if ($action === 'add_category') {
$name = trim($_POST['name']);
$target = trim($_POST['target']);
$holiday_id = !empty($_POST['holiday_id']) ? (int)$_POST['holiday_id'] : null;
if (!empty($name)) {
$stmt = $pdo->prepare("INSERT INTO pf_alloc_categories (name, target, holiday_id) VALUES (?, ?, ?)");
$stmt->execute([$name, $target, $holiday_id]);
}
// --- NOUVEAU : Réponse AJAX ---
if (isset($_POST['ajax'])) {
header('Content-Type: application/json');
echo json_encode(['success' => true]);
try {
$name = trim($_POST['name']);
$transfer_dest = trim($_POST['transfer_dest'] ?? '');
$target = floatval($_POST['target'] ?? 0);
$holiday_id = !empty($_POST['holiday_id']) ? (int)$_POST['holiday_id'] : null;
if (!empty($name)) {
$stmt = $pdo->prepare("INSERT INTO pf_alloc_categories (name, target, transfer_dest, holiday_id) VALUES (?, ?, ?, ?)");
$stmt->execute([$name, $target, $transfer_dest, $holiday_id]);
}
if (isset($_POST['ajax'])) {
header('Content-Type: application/json');
echo json_encode(['success' => true]);
exit;
}
header("Location: " . $_SERVER['HTTP_REFERER']);
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());
}
header("Location: " . $_SERVER['HTTP_REFERER']);
exit;
}
// 4. MODIFICATION D'UNE CATEGORIE
if ($action === 'update_category') {
$id = (int)$_POST['cat_id'];
$name = trim($_POST['name']);
$target = trim($_POST['target']);
$holiday_id = !empty($_POST['holiday_id']) ? (int)$_POST['holiday_id'] : null;
try {
$id = (int)$_POST['cat_id'];
$name = trim($_POST['name']);
$transfer_dest = trim($_POST['transfer_dest'] ?? '');
$target = floatval($_POST['target'] ?? 0);
$holiday_id = !empty($_POST['holiday_id']) ? (int)$_POST['holiday_id'] : null;
if ($id > 0 && !empty($name)) {
$stmt = $pdo->prepare("UPDATE pf_alloc_categories SET name = ?, target = ?, holiday_id = ? WHERE id = ?");
$stmt->execute([$name, $target, $holiday_id, $id]);
}
if ($id > 0 && !empty($name)) {
$stmt = $pdo->prepare("UPDATE pf_alloc_categories SET name = ?, target = ?, transfer_dest = ?, holiday_id = ? WHERE id = ?");
$stmt->execute([$name, $target, $transfer_dest, $holiday_id, $id]);
}
// --- NOUVEAU : Réponse AJAX ---
if (isset($_POST['ajax'])) {
header('Content-Type: application/json');
echo json_encode(['success' => true]);
if (isset($_POST['ajax'])) {
header('Content-Type: application/json');
echo json_encode(['success' => true]);
exit;
}
header("Location: " . $_SERVER['HTTP_REFERER']);
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());
}
header("Location: " . $_SERVER['HTTP_REFERER']);
exit;
}
// 5. SUPPRESSION CATEGORIE
@@ -136,43 +162,48 @@ if ($action === 'delete_category') {
// 6. VALIDATION DES VIREMENTS (Complex Business Logic)
if ($action === 'validate_transfers') {
header('Content-Type: application/json');
$person = $_POST['person'];
$personId = (int)$_POST['person_id']; // Reçoit l'ID parent
$monthDate = $_POST['month_date'];
try {
$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("
SELECT v.*, c.name as cat_name, c.target, c.holiday_id
FROM pf_alloc_values v
SELECT v.amount, c.name as cat_name, c.transfer_dest, c.holiday_id
FROM pf_alloc_values v
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);
$transfersToDo = [];
foreach ($budgetLines as $line) {
$amount = ($person === 'Alex') ? $line['amount_alex'] : $line['amount_laia'];
if ($amount <= 0) continue;
$amount = (float)$line['amount'];
if ($amount <= 0) continue;
$target = trim($line['target']);
$dest = trim($line['transfer_dest']);
$catName = trim($line['cat_name']);
$holidayId = $line['holiday_id'];
$holidayId = $line['holiday_id'];
$targetOwner = null;
if ($target === 'vers L.Perso') { $targetOwner = $person; }
elseif ($target === 'vers L.Pol') { $targetOwner = 'Pol'; }
elseif ($target === 'vers L.Pep') { $targetOwner = 'Pep'; }
elseif ($target === 'vers commune') { continue; }
if ($dest === 'vers L.Perso') { $targetOwner = $dbPersonName; }
elseif ($dest === 'vers L.Pol') { $targetOwner = 'Pol'; }
elseif ($dest === 'vers L.Pep') { $targetOwner = 'Pep'; }
elseif ($dest === 'vers commune') { continue; }
if ($targetOwner) {
if (!isset($transfersToDo[$targetOwner])) {
$transfersToDo[$targetOwner] = ['total_add' => 0, 'cats' => []];
}
$transfersToDo[$targetOwner]['total_add'] += $amount;
if (!isset($transfersToDo[$targetOwner]['cats'][$catName])) {
$transfersToDo[$targetOwner]['cats'][$catName] = ['amount' => 0, 'holiday_id' => $holidayId];
}
@@ -181,7 +212,6 @@ if ($action === 'validate_transfers') {
}
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->execute([$owner, $monthDate]);
$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->execute([$data['total_add'], $owner, $monthDate]);
// C. UPDATE CATÉGORIES (Modifié pour gérer le holiday_id)
foreach ($data['cats'] as $catName => $catInfo) {
$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->execute([$owner, $monthDate, $catName]);
$catId = $stmtCheckCat->fetchColumn();
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->execute([$catAmount, $catHolidayId, $catId]);
} else {
// Insert
$stmtInsertCat = $pdo->prepare("INSERT INTO pf_savings (month_date, owner, category, amount, holiday_id) VALUES (?, ?, ?, ?, ?)");
$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->execute();
$sysCatId = $stmtSys->fetchColumn();
if ($sysCatId) {
if ($person === 'Alex') {
$sql = "INSERT INTO pf_alloc_values (month_date, cat_id, amount_alex, amount_laia)
VALUES (?, ?, 1, 0)
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]);
$stmtVal = $pdo->prepare("INSERT INTO pf_alloc_values (month_date, cat_id, person_id, amount)
VALUES (?, ?, ?, 1)
ON DUPLICATE KEY UPDATE amount = 1");
$stmtVal->execute([$monthDate, $sysCatId, $personId]);
}
$pdo->commit();
@@ -258,6 +272,4 @@ if ($action === 'validate_transfers') {
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}
exit;
}
+97 -85
View File
@@ -16,7 +16,6 @@ foreach ($budgetParents as $index => $parent) {
$parentMapping[] = [
'id' => (int)$parent['id'],
'name' => $parent['name'],
'db_field' => 'amount_p' . $num,
'css' => 'p' . $num,
'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));
$nextMonthLink = date('Y-m-01', strtotime("+1 month", $focusTs));
// Récupération des Cycles configurés dans pf_notes
$cycleConfigs = [];
$stmtNotes = $pdo->query("SELECT reference_id, content FROM pf_notes WHERE note_type = 'month_config'");
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), '?'));
$stmt = $pdo->prepare("SELECT * FROM pf_alloc_values WHERE month_date IN ($inQuery)");
$stmt->execute($months);
$allocs = [];
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
@@ -78,7 +76,7 @@ $sysCatId = null;
foreach ($cats as $key => $c) {
if ($c['name'] === 'SYSTEM_VALIDATION') {
$sysCatId = $c['id'];
unset($cats[$key]);
unset($cats[$key]);
break;
}
}
@@ -86,14 +84,10 @@ foreach ($cats as $key => $c) {
// Statuts de validation
$focusDate = $months[0];
$isValidated = [];
if ($sysCatId && isset($allocs[$focusDate][$sysCatId])) {
$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;
foreach ($parentMapping as $map) {
$isValidated[$map['css']] = false;
if ($sysCatId && isset($allocs[$focusDate][$sysCatId][$map['id']])) {
$isValidated[$map['css']] = ($allocs[$focusDate][$sysCatId][$map['id']] == 1);
}
}
@@ -110,13 +104,13 @@ 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 class="prev-section-header">
<h2><?= tr('bud_prev_incomes') ?> <?= $currentYear ?></h2>
</div>
<table class="prev-salary-table">
<thead>
<tr>
@@ -130,9 +124,9 @@ function getTranslatedMonthName($dateString) {
</tr>
</thead>
<tbody>
<?php
foreach ($parentMapping as $map):
$d = $salaryConfig[$map['name']];
<?php
foreach ($parentMapping as $map):
$d = $salaryConfig[$map['name']];
$restant = $d['salary'] - ($d['mensualite'] + $d['frais_func'] + $d['eco_perso'] + $d['eco_family']);
?>
<tr data-person="<?= htmlspecialchars($map['name']) ?>">
@@ -180,14 +174,14 @@ function getTranslatedMonthName($dateString) {
<thead>
<tr>
<th class="col-sticky header-cell"></th>
<?php foreach ($months as $month):
<?php foreach ($months as $month):
$isCurrent = ($month == date('Y-m-01'));
$cls = $isCurrent ? 'current' : '';
$colspan = count($parentMapping) + 1;
?>
<th colspan="<?= $colspan ?>" class="th-month <?= $cls ?>">
<span><?= getTranslatedMonthName($month) ?></span>
<?php
<?php
if (isset($cycleConfigs[$month]) && !empty($cycleConfigs[$month]['start_date'])) {
$cStart = date('d/m', strtotime($cycleConfigs[$month]['start_date']));
echo "<div class='cycle-start-label'>" . sprintf(tr('bud_sav_from_date'), $cStart) . "</div>";
@@ -227,11 +221,11 @@ function getTranslatedMonthName($dateString) {
<?php endforeach; ?>
</tr>
<?php foreach ($cats as $cat):
<?php foreach ($cats as $cat):
$isIndicative = (strpos($cat['name'], 'Eco P') === 0);
$rowClass = $isIndicative ? 'row-indicative' : '';
$inputClass = $isIndicative ? 'ignore-calc' : '';
$inputClass = $isIndicative ? 'ignore-calc' : '';
$catDisplayName = $cat['name'];
if ($catDisplayName === 'Eco P1') $catDisplayName = 'Eco ' . $p1_name;
if ($catDisplayName === 'Eco P2') $catDisplayName = 'Eco ' . $p2_name;
@@ -239,34 +233,38 @@ function getTranslatedMonthName($dateString) {
<tr class="<?= $rowClass ?>">
<td class="col-sticky">
<div class="cat-name-label">
<?= htmlspecialchars($catDisplayName) ?>
<?= htmlspecialchars($catDisplayName) ?>
<?php if(!empty($cat['holiday_id'])) echo " 🌴"; ?>
<?php if($isIndicative): ?><span>Info</span><?php endif; ?>
</div>
<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 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>
</div>
</td>
<?php foreach ($months as $m):
$val = $allocs[$m][$cat['id']] ?? [];
?>
<?php foreach ($months as $m): ?>
<td class="txt-global sum-target" id="g_<?= $m ?>_<?= $cat['id'] ?>">0</td>
<?php foreach ($parentMapping as $map):
$dbField = $map['db_field'];
$cellValue = isset($val[$dbField]) ? $val[$dbField] : 0;
<?php foreach ($parentMapping as $map):
$cellValue = $allocs[$m][$cat['id']][$map['id']] ?? 0;
?>
<td>
<input type="number" step="1" class="prev-input txt-<?= $map['css'] ?> inp-<?= $map['css'] ?>-<?= $m ?> <?= $inputClass ?>"
value="<?= $cellValue == 0 ? '' : round($cellValue) ?>"
<input type="number" step="1" class="prev-input txt-<?= $map['css'] ?> inp-<?= $map['css'] ?>-<?= $m ?> <?= $inputClass ?>"
value="<?= $cellValue == 0 ? '' : round($cellValue) ?>"
placeholder="-"
data-target="<?= htmlspecialchars($cat['target']) ?>"
onchange="updateAlloc('<?= $m ?>', <?= $cat['id'] ?>, '<?= $dbField ?>', this)">
data-transfer-dest="<?= htmlspecialchars($cat['transfer_dest'] ?? '') ?>"
onchange="updateAlloc('<?= $m ?>', <?= $cat['id'] ?>, <?= $map['id'] ?>, this)">
</td>
<?php endforeach; ?>
<?php endforeach; ?>
@@ -282,21 +280,24 @@ function getTranslatedMonthName($dateString) {
<h3>📝 <?= tr('bud_prev_notes_for') ?> <span><?= getTranslatedMonthName($focusDate) ?></span></h3>
<span id="note-save-indicator" class="note-save-indicator">✓ <?= tr('bud_prev_saved') ?></span>
</div>
<textarea id="monthNoteArea" class="pf-input" rows="3" placeholder="<?= tr('bud_prev_notes_ph') ?>"><?= htmlspecialchars((string)$currentNote) ?></textarea>
<div class="notes-footer">
<button type="button" class="pf-btn" onclick="saveGenericNote('budget_prev', '<?= $focusDate ?>', document.getElementById('monthNoteArea').value)"><?= tr('bud_prev_save_note') ?></button>
</div>
</div>
<?php
$focusMonth = $months[0];
$focusMonth = $months[0];
$targetsOrder = ['vers commune', 'vers L.Pol', 'vers L.Pep', 'vers L.Perso'];
$allTargets = $targetsOrder;
foreach($cats as $c) {
$t = trim($c['target']);
if(!empty($t) && !in_array($t, $allTargets)) { $allTargets[] = $t; }
$t = trim($c['transfer_dest'] ?? '');
if(!empty($t) && !in_array($t, $allTargets)) {
$allTargets[] = $t;
}
}
$allTargets = array_unique($allTargets);
?>
@@ -306,7 +307,7 @@ function getTranslatedMonthName($dateString) {
<div class="recap-header">
<?= tr('bud_prev_transfers_to_make') ?> - <span><?= getTranslatedMonthName($focusMonth) ?></span>
</div>
<table class="recap-table">
<thead>
<tr>
@@ -364,9 +365,13 @@ function getTranslatedMonthName($dateString) {
<input type="text" name="name" class="pf-input" required>
</div>
<div class="form-group">
<label class="pf-label"><?= tr('bud_prev_label_target') ?></label>
<select name="target" class="pf-input" required>
<option value="" disabled selected>-- <?= tr('bud_prev_choose') ?> --</option>
<label class="pf-label">Objectif Mensuel (€)</label>
<input type="number" step="1" name="target" class="pf-input" placeholder="Ex: 150">
</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.Pep">vers L.Pep</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>
</div>
<div class="form-group">
<label class="pf-label"><?= tr('bud_prev_label_target') ?></label>
<select name="target" id="edit_cat_target" class="pf-input" required>
<label class="pf-label">Objectif Mensuel (€)</label>
<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.Pep">vers L.Pep</option>
<option value="vers L.Perso">vers L.Perso</option>
@@ -452,6 +462,7 @@ window.I18N = {
window.CONFIG = window.CONFIG || {};
window.CONFIG.parentMapping = <?= json_encode($parentMapping) ?>;
window.CONFIG.CURRENCY = '<?= defined('CURRENCY') ? CURRENCY : "€" ?>';
const currentYear = <?= $currentYear ?>;
const months = <?= json_encode($months) ?>;
@@ -460,7 +471,8 @@ function openEditModal(btn) {
document.getElementById('edit_cat_id').value = btn.getAttribute('data-id');
document.getElementById('edit_cat_name').value = btn.getAttribute('data-name');
document.getElementById('edit_cat_target').value = btn.getAttribute('data-target');
document.getElementById('edit_cat_holiday').value = btn.getAttribute('data-holiday');
document.getElementById('edit_cat_transfer_dest').value = btn.getAttribute('data-transfer-dest');
document.getElementById('edit_cat_holiday').value = btn.getAttribute('data-holiday');
document.getElementById('editCatModal').style.display = 'flex';
document.body.classList.add('no-scroll');
}
@@ -474,7 +486,7 @@ function updateSalary(person, input) {
const ecoF = parseFloat(row.querySelector('[data-field="eco_family"]').value) || 0;
const restant = salary - (mens + frais + ecoP + ecoF);
const parentMap = window.CONFIG.parentMapping.find(m => m.name === person);
if(parentMap) {
document.getElementById('restant_' + parentMap.css).innerText = Math.round(restant).toLocaleString(window.appLang) + ' €';
@@ -484,8 +496,8 @@ function updateSalary(person, input) {
recalcAllAllocations();
}
function updateAlloc(month, catId, personField, input) {
saveData('update_allocation', { month_date: month, cat_id: catId, person: personField, value: input.value || 0 });
function updateAlloc(month, catId, personId, input) {
saveData('update_allocation', { month_date: month, cat_id: catId, person_id: personId, value: input.value || 0 });
recalcAllAllocations();
}
@@ -504,12 +516,12 @@ function duplicateMonth() {
return str.charAt(0).toUpperCase() + str.slice(1);
};
const sourceName = formatMonth(sourceDateStr);
const targetName = formatMonth(targetDateStr);
const sourceName = formatMonth(sourceDateStr);
const targetName = formatMonth(targetDateStr);
const message = window.I18N['bud_prev_confirm_copy'].replace('%s', sourceName).replace('%t', targetName);
if(!confirm(message)) return;
const firstCss = parentMap[0].css;
document.querySelectorAll('.inp-' + firstCss + '-' + sourceDateStr).forEach(sourceInput => {
const catIdMatch = sourceInput.getAttribute('onchange').match(/, (\d+),/);
@@ -520,9 +532,9 @@ function duplicateMonth() {
parentMap.forEach(map => {
const sInp = row.querySelector('.inp-' + map.css + '-' + sourceDateStr);
const tInp = row.querySelector('.inp-' + map.css + '-' + targetDateStr);
if(sInp && tInp) {
tInp.value = sInp.value;
updateAlloc(targetDateStr, catId, map.db_field, tInp);
if(sInp && tInp) {
tInp.value = sInp.value;
updateAlloc(targetDateStr, catId, map.id, tInp);
}
});
});
@@ -548,7 +560,7 @@ function recalcAllAllocations() {
}
globalSum += val;
});
const onchangeStr = inp.getAttribute('onchange');
const matches = onchangeStr.match(/, (\d+),/);
if(matches && matches[1]) {
@@ -561,7 +573,7 @@ function recalcAllAllocations() {
parentMap.forEach(map => {
const sumEl = document.getElementById('total_' + map.css + '_' + m);
if(sumEl) sumEl.innerText = Math.round(sums[map.css]) + ' €';
totalGlobal += sums[map.css];
const budget = parseFloat(document.getElementById('eco_family_' + map.css).value) || 0;
@@ -570,16 +582,16 @@ function recalcAllAllocations() {
const elRest = document.getElementById('restant_alloc_' + map.css + '_' + m);
if (elRest) {
elRest.innerText = Math.round(rest) + ' €';
elRest.className = 'val-' + (rest >= 0 ? 'ok' : 'ko') + ' sum-target';
elRest.className = 'val-' + (rest >= 0 ? 'ok' : 'ko') + ' sum-target';
}
});
const globEl = document.getElementById('total_global_' + m);
if(globEl) globEl.innerText = Math.round(totalGlobal) + ' €';
});
updateSummaryTable();
if(isSumModeActive) updateSumResult();
if(isSumModeActive) updateSumResult();
}
function updateSummaryTable() {
@@ -591,12 +603,12 @@ function updateSummaryTable() {
parentMap.forEach(map => {
grandTotals[map.css] = 0;
dataByTarget[map.css] = {};
document.querySelectorAll('.inp-' + map.css + '-' + focusMonth).forEach(inp => {
const target = inp.getAttribute('data-target');
if(target) {
if(!dataByTarget[map.css][target]) dataByTarget[map.css][target] = 0;
dataByTarget[map.css][target] += (parseFloat(inp.value) || 0);
const dest = inp.getAttribute('data-transfer-dest');
if(dest) {
if(!dataByTarget[map.css][dest]) dataByTarget[map.css][dest] = 0;
dataByTarget[map.css][dest] += (parseFloat(inp.value) || 0);
}
});
});
@@ -604,7 +616,7 @@ function updateSummaryTable() {
const tbody = document.querySelector('.recap-table tbody');
if(tbody) {
Array.from(tbody.rows).forEach(row => {
const targetName = row.cells[0].innerText.trim();
const targetName = row.cells[0].innerText.trim();
let globalSum = 0;
parentMap.forEach((map, idx) => {
@@ -624,7 +636,7 @@ function updateSummaryTable() {
if(grandEl) grandEl.innerText = Math.round(grandTotals[map.css]).toLocaleString(window.appLang) + ' ' + window.CONFIG.CURRENCY;
totalGrandGlobal += grandTotals[map.css];
});
const globGrandEl = document.getElementById('grand_total_global');
if(globGrandEl) globGrandEl.innerText = Math.round(totalGrandGlobal).toLocaleString(window.appLang) + ' ' + window.CONFIG.CURRENCY;
}
@@ -637,17 +649,17 @@ function saveData(action, data) {
}
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 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();
formData.append('action', 'validate_transfers');
formData.append('person', dbPersonName);
formData.append('person_id', parentMap.id);
formData.append('month_date', month);
fetch('modules/budget/includes/api/save-budget.php', { method: 'POST', body: formData })
.then(r => r.json())
.then(data => {
@@ -660,9 +672,9 @@ function validateTransfers(personCss, month) {
function saveGenericNote(noteType, refId, content) {
const formData = new FormData();
formData.append('action', 'save_note');
formData.append('note_type', noteType);
formData.append('reference_id', refId);
formData.append('content', content);
formData.append('note_type', noteType);
formData.append('reference_id', refId);
formData.append('content', content);
fetch('modules/budget/includes/api/save-budget.php', { method: 'POST', body: formData })
.then(async r => {
@@ -723,10 +735,10 @@ async function deleteCategory(id) {
const formData = new FormData();
formData.append('action', 'delete_category');
formData.append('id', id);
formData.append('ajax', '1');
formData.append('ajax', '1');
try {
const result = await pachaFetch('modules/budget/includes/api/save-budget.php', { method: 'POST', body: formData });
if (result.success) window.location.reload();
if (result.success) window.location.reload();
} catch(e) { console.error(e); }
}
@@ -734,7 +746,7 @@ document.addEventListener('click', function(e) {
if (!isSumModeActive) return;
const targetElement = e.target.closest('input[type="number"], .sum-target');
if (targetElement) {
e.preventDefault();
e.preventDefault();
if (selectedElementsForSum.has(targetElement)) {
selectedElementsForSum.delete(targetElement);
targetElement.classList.remove('sum-selected');
@@ -758,12 +770,12 @@ document.querySelectorAll('#addCatModal form, #editCatModal form').forEach(form
submitBtn.innerText = '⏳ ...';
const formData = new FormData(form);
formData.append('ajax', '1');
const actionUrl = form.getAttribute('action');
const actionUrl = form.getAttribute('action');
const result = await pachaFetch(actionUrl, { method: 'POST', body: formData });
if (result.success) {
form.closest('.pf-modal').style.display = 'none';
document.body.classList.remove('no-scroll');
window.location.reload();
window.location.reload();
} else {
alert((window.I18N['bud_err_tech'] || 'Erreur') + " : " + (result.error || "Inconnue"));
}
+39 -13
View File
@@ -57,12 +57,11 @@ if (isset($_POST['action']) && $_POST['action'] === 'reopen_month') {
exit;
}
if (isset($_POST['action']) && $_POST['action'] === 'save_import') {
$count = 0;
$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'])) {
foreach ($_POST['lines'] as $line) {
@@ -81,7 +80,7 @@ if (isset($_POST['action']) && $_POST['action'] === 'save_import') {
try {
$stmtExp->execute([$dateToSave, $gestionMonthLine, $cat, $line['label'], $finalAmount, $line['ref'], $budgetItemId, $holidayId]);
$stmtRule->execute([$line['label'], $cat]);
$stmtRule->execute([$line['label'], $cat, $budgetItemId]);
$count++;
} catch (Exception $e) { continue; }
}
@@ -110,7 +109,15 @@ $showPreview = false;
if (isset($_FILES['csv_file']) && $_FILES['csv_file']['error'] == 0) {
$file = $_FILES['csv_file']['tmp_name'];
$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){}
fgetcsv($handle, 1000, ";", "\"", "\\");
@@ -129,9 +136,16 @@ if (isset($_FILES['csv_file']) && $_FILES['csv_file']['error'] == 0) {
$isDuplicate = in_array($uniqueKey, $existingRefs);
$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);
$showPreview = true;
@@ -224,6 +238,7 @@ $categoriesConfig = [
'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'=>[]],
'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']]
];
@@ -695,11 +710,15 @@ $monthName = $monthNames[(int)$viewM] . ' ' . $viewY;
</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>
<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 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>
<?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>
</div>
</td>
@@ -792,10 +811,14 @@ function openEditModal(e) {
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 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; }
else if (select.value === 'Income') { iSel.style.display = 'block'; iSel.disabled = false; }
checkValidation();
@@ -815,7 +838,10 @@ function checkValidation() {
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) => {
if (e.target.classList.contains('pf-modal')) {
@@ -826,7 +852,7 @@ window.addEventListener('click', (e) => {
// --- 2. SUPPRESSION ASYNCHRONE ---
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;
const formData = new FormData();
+260 -24
View File
@@ -1,10 +1,18 @@
-- HouseHub — Schéma MySQL
-- Créer la base : CREATE DATABASE househub CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- HouseHub — Schéma MySQL (Modèle pour une base famille)
SET NAMES utf8mb4;
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 (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(100) NOT NULL UNIQUE,
@@ -15,11 +23,12 @@ CREATE TABLE IF NOT EXISTS pf_users (
-- ─── Personnes ────────────────────────────────────────────────────────────────
CREATE TABLE `pf_people` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(50) NOT NULL,
`user_id` int(11) DEFAULT NULL,
`name` varchar(100) NOT NULL,
`user_id` int(11) DEFAULT NULL,
`role` varchar(50) DEFAULT NULL,
`color` varchar(7) DEFAULT '#0891b2',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ─── Calendrier familial ──────────────────────────────────────────────────────
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 (
id INT AUTO_INCREMENT PRIMARY KEY,
date_exp DATE NOT NULL,
gestion_month VARCHAR(7) NOT NULL,
gestion_month VARCHAR(10) NOT NULL,
category VARCHAR(100),
label VARCHAR(255),
amount DECIMAL(10,2) DEFAULT 0,
@@ -104,26 +113,28 @@ CREATE TABLE IF NOT EXISTS pf_expenses (
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS pf_alloc_categories (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
target DECIMAL(10,2) DEFAULT 0,
holiday_id INT DEFAULT NULL,
sort_order INT DEFAULT 0
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
target DECIMAL(10,2) DEFAULT 0.00,
transfer_dest VARCHAR(50) DEFAULT NULL,
holiday_id INT DEFAULT NULL,
sort_order INT DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS pf_alloc_values (
id INT AUTO_INCREMENT PRIMARY KEY,
month_date VARCHAR(7) NOT NULL,
month_date VARCHAR(10) NOT NULL,
cat_id INT NOT NULL,
amount_alex DECIMAL(10,2) DEFAULT 0,
amount_laia DECIMAL(10,2) DEFAULT 0,
UNIQUE KEY uq_alloc (month_date, cat_id)
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;
CREATE TABLE IF NOT EXISTS pf_savings (
id INT AUTO_INCREMENT PRIMARY KEY,
owner VARCHAR(50) NOT NULL,
month_date VARCHAR(7) NOT NULL,
month_date VARCHAR(10) NOT NULL,
category VARCHAR(100) NOT NULL,
amount DECIMAL(10,2) DEFAULT 0,
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 (
id INT AUTO_INCREMENT PRIMARY KEY,
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;
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 (
id INT AUTO_INCREMENT PRIMARY KEY,
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)
) 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 ───────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS pf_holidays (
id INT AUTO_INCREMENT PRIMARY KEY,
@@ -228,13 +275,153 @@ CREATE TABLE IF NOT EXISTS pf_gifts (
amount DECIMAL(8,2) DEFAULT 0
) 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 ────────────────────────────────────────────────────────
-- 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');
CREATE TABLE IF NOT EXISTS pf_maintenances (
id INT AUTO_INCREMENT PRIMARY KEY,
vehicle_id INT NOT NULL,
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 ─────────────────────────────────────────────────────────────
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_etag VARCHAR(255) DEFAULT NULL,
calendar_url VARCHAR(1024) DEFAULT NULL,
external_href VARCHAR(2048) DEFAULT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uq_calendar_event (calendar_event_id),
UNIQUE KEY uq_external_link (external_uid)
) 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;